Module: CurrentScope

Defined in:
lib/current_scope.rb,
lib/current_scope/guard.rb,
lib/current_scope/engine.rb,
lib/current_scope/context.rb,
lib/current_scope/version.rb,
lib/current_scope/resolver.rb,
lib/current_scope/scopeable.rb,
app/models/current_scope/role.rb,
lib/current_scope/permissions.rb,
app/models/current_scope/event.rb,
lib/current_scope/test_helpers.rb,
lib/current_scope/configuration.rb,
app/models/current_scope/current.rb,
lib/current_scope/mutation_guard.rb,
lib/current_scope/gating_tripwire.rb,
lib/current_scope/permission_grid.rb,
lib/current_scope/permission_catalog.rb,
app/models/current_scope/role_assignment.rb,
app/models/current_scope/role_permission.rb,
app/models/current_scope/application_record.rb,
app/helpers/current_scope/application_helper.rb,
app/controllers/current_scope/roles_controller.rb,
app/controllers/current_scope/events_controller.rb,
app/models/current_scope/scoped_role_assignment.rb,
app/controllers/current_scope/subjects_controller.rb,
app/controllers/current_scope/application_controller.rb,
lib/generators/current_scope/install/install_generator.rb,
app/controllers/current_scope/role_assignments_controller.rb,
app/controllers/current_scope/scoped_role_assignments_controller.rb

Defined Under Namespace

Modules: ApplicationHelper, Context, GatingTripwire, Generators, Guard, MutationGuard, Permissions, Scopeable, TestHelpers Classes: AccessDenied, ApplicationController, ApplicationRecord, Configuration, ConfigurationError, Current, Engine, Event, EventsController, PermissionCatalog, PermissionGrid, Resolver, Role, RoleAssignment, RoleAssignmentsController, RolePermission, RolesController, ScopedRoleAssignment, ScopedRoleAssignmentsController, SubjectsController

Constant Summary collapse

VERSION =
"0.2.0"

Class Method Summary collapse

Class Method Details

.allowed?(action, subject:, record: nil, controller_path: nil, actor: nil) ⇒ Boolean

The single entry point behind every allowed_to? call. action is either a full permission key ("admin/reports#approve") or a bare action name resolved against record's route key, falling back to controller_path.

Returns:

  • (Boolean)


77
78
79
80
81
82
83
84
# File 'lib/current_scope.rb', line 77

def allowed?(action, subject:, record: nil, controller_path: nil, actor: nil)
  resolver.allow?(
    subject: subject,
    permission: permission_key(action, record: record, controller_path: controller_path),
    record: record,
    actor: actor
  )
end

.catalogObject



45
46
47
# File 'lib/current_scope.rb', line 45

def catalog
  @catalog ||= PermissionCatalog.new
end

.configObject



33
34
35
# File 'lib/current_scope.rb', line 33

def config
  @config ||= Configuration.new
end

.configure {|config| ... } ⇒ Object

Yields:



37
38
39
# File 'lib/current_scope.rb', line 37

def configure
  yield config
end

.grant!(subject, role: nil) ⇒ Object

Bootstrap the first admin: assign a role (default: the full_access Owner) to subject as its one org-wide role. Idempotent — re-running sets the same subject's org role to role rather than creating a duplicate (which the one-role-per-subject uniqueness would reject anyway). Backs the current_scope:grant rake task, so a fresh install doesn't need a console.



142
143
144
145
146
# File 'lib/current_scope.rb', line 142

def grant!(subject, role: nil)
  seed_defaults!
  role ||= Role.find_by!(name: "Owner")
  RoleAssignment.find_or_initialize_by(subject: subject).tap { |a| a.update!(role: role) }
end

.permission_key(action, record: nil, controller_path: nil) ⇒ Object

Raises:

  • (ArgumentError)


94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# File 'lib/current_scope.rb', line 94

def permission_key(action, record: nil, controller_path: nil)
  action = action.to_s
  return action if action.include?("#")

  if record.respond_to?(:model_name)
    route_key = record.model_name.route_key
    # When the current controller handles this record type (possibly under
    # a namespace — admin/reports for a Report), its path is the key the
    # Guard enforces, so prefer it: the view must agree with the gate.
    return "#{controller_path}##{action}" if controller_path&.split("/")&.last == route_key

    return "#{route_key}##{action}"
  end
  return "#{controller_path}##{action}" if controller_path

  raise ArgumentError,
        "cannot derive a permission key for #{action.inspect} — pass a record, " \
        "a full \"controller#action\" string, or call from a controller/view"
end

.record_impersonation_started!(subject) ⇒ Object

Impersonation boundary events. The impersonated identity is an EXPLICIT argument (not read from the ambient pair): at act-as START the ambient actor still equals the effective user — Current re-resolves next request — so an ambient-only recorder would lose who was impersonated. Call these from the host's start/stop-impersonation endpoints.



119
120
121
122
# File 'lib/current_scope.rb', line 119

def record_impersonation_started!(subject)
  require_actor_method!
  Event.record!(event: "impersonation.started", target: subject)
end

.record_impersonation_stopped!(subject) ⇒ Object



124
125
126
127
# File 'lib/current_scope.rb', line 124

def record_impersonation_stopped!(subject)
  require_actor_method!
  Event.record!(event: "impersonation.stopped", target: subject)
end

.register_scopeable(model_name) ⇒ Object



61
62
63
# File 'lib/current_scope.rb', line 61

def register_scopeable(model_name)
  scopeable_registry << model_name.to_s
end

.reset_catalog!Object



49
50
51
# File 'lib/current_scope.rb', line 49

def reset_catalog!
  @catalog = nil
end

.reset_scopeable_registry!Object



69
70
71
# File 'lib/current_scope.rb', line 69

def reset_scopeable_registry!
  @scopeable_registry = Set.new
end

.resolverObject



41
42
43
# File 'lib/current_scope.rb', line 41

def resolver
  @resolver ||= Resolver.new
end

.scope_for(subject:, model:, permission:) ⇒ Object

The list-side companion to allowed?. Returns a chainable relation of the records of model the subject may act on under permission — same grants, same fail-closed rules as the per-record gate. permission is a resolved key ("projects#index"); the mixin derives the default.



90
91
92
# File 'lib/current_scope.rb', line 90

def scope_for(subject:, model:, permission:)
  resolver.scope_for(subject: subject, model: model, permission: permission)
end

.scopeable_registryObject

Models that opted into the scoped-role picker via CurrentScope::Scopeable. Stored as class-name strings and resolved lazily so dev-mode reloading never pins a stale constant. Rebuilt from scratch on every engine to_prepare (see reset_scopeable_registry!).



57
58
59
# File 'lib/current_scope.rb', line 57

def scopeable_registry
  @scopeable_registry ||= Set.new
end

.scopeable_resourcesObject



65
66
67
# File 'lib/current_scope.rb', line 65

def scopeable_resources
  scopeable_registry.map(&:constantize).sort_by(&:name)
end

.seed_defaults!Object

Creates the two baseline roles every install needs: an Owner with full_access (present and future permissions) and a Member baseline. Call from db/seeds.rb.



132
133
134
135
# File 'lib/current_scope.rb', line 132

def seed_defaults!
  Role.find_or_create_by!(name: "Owner") { |r| r.full_access = true }
  Role.find_or_create_by!(name: "Member")
end