rbacan 0.3.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8927d4673bc1ccba40ab9d6380a1cfcc33c18b9b5117ad4e68d7bd00fb93d3e6
4
- data.tar.gz: 1f8ae03be85e98f6bf340e2c7792bdb1f4312bb1f47d2b87d52a179fece4696d
3
+ metadata.gz: d73e61377cfc8c44c60d79857c8a5c09be75ddd8c7e6bd1547895dc11f03a673
4
+ data.tar.gz: 7027bf24c300f0996fd12cb94875bfb16ab24f0119b4442547da1ea054fb365b
5
5
  SHA512:
6
- metadata.gz: 5f46b855672b6b46fae4702a163966762e08f18a19bbf562968e2a00a0e1dfef56558af2ea72d61de605b6676629d2aea893226db007fc7e40f5cd5d1b5b4f36
7
- data.tar.gz: 91f15017848fc97fb1b16fa7355696281c8227dc53e7bb774c72d04cf40bdc53157c00f2390fde8e56ec07ad2ff4ca12835232eb0067ae2f96ca304c83f8f6dc
6
+ metadata.gz: 5363d5c569e9b65c754066baac8407b7d4047b9ecb11c5ba81ac66cec9f2fe2096837feb4e1ce3940cf53374fddeaa0feea034553917285e1871c0f6f968f2b7
7
+ data.tar.gz: ac1e65f2064108e82a9686b895b1ae1ea6f7badc9a49b3bcdc5e299f861413b9aaedc0bc3ad17605c0cbd10ea95ca5abf2872abaa23af9067bf7480dd0692a8a
data/CHANGELOG.md CHANGED
@@ -2,6 +2,59 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.5.0] - 2026-09-12
6
+
7
+ ### Added
8
+
9
+ - Host-owned authorization through immutable, value-based `Rbacan::AuthorizationContext` and pluggable authorization/assignment adapters.
10
+ - Association-free `Rbacan::Authorizable` concern for applications that own their RBAC schema and lifecycle.
11
+ - Explicit user and context resolvers for hardened route constraints.
12
+ - Stable denial codes on `Rbacan::NotAuthorized` for host-side localization.
13
+ - Configurable `role_key` and `permission_key` columns.
14
+ - Deeply immutable JSON-like authorization metadata and copied immutable target codes.
15
+
16
+ ### Changed
17
+
18
+ - Hardened mode is the default. Authorization requires an explicit context and fails closed without a host adapter.
19
+ - Destructive/unscoped `Rbacan::Permittable`, catalog, role-permission, and seed-helper APIs require explicit `legacy_mode = true` opt-in.
20
+ - Authorization contexts require a tenant and snapshot/freeze JSON-like tenant, property, vertical, and subject identifiers plus their timestamp.
21
+ - Controller and view authorization use the host adapter in hardened mode.
22
+ - Hardened redirect denials resolve their stable code through the host I18n backend.
23
+ - Assignment dispatch rejects missing users or blank roles before invoking the host adapter.
24
+ - Gem packaging uses an explicit runtime/documentation allowlist and excludes repository-only files.
25
+
26
+ ### Compatibility
27
+
28
+ - Applications using the generated 0.4 schema must consciously enable legacy mode while migrating.
29
+
30
+ ## [0.4.0] - 2026-04-13
31
+
32
+ ### Added
33
+
34
+ - `primary_key_type` config option — auto-detects host app's primary key type (supports `:uuid` and `:bigint`)
35
+ - `tenant_scoped` config option — when `true`, adds `tenant_id` to `roles` and `user_roles` tables
36
+ - `tenant_class` config option — configurable tenant model class name (default: `"Tenant"`)
37
+ - `Rbacan.resolve_primary_key_type` — helper to resolve the effective PK type at runtime
38
+ - Tenant-aware methods on `Permittable`:
39
+ - `roles_for_tenant(tenant_id)` — returns roles assigned in a specific tenant (+ global)
40
+ - `has_role_in_tenant?(role_name, tenant_id)` — checks role in tenant context
41
+ - `can_in_tenant?(permission_name, tenant_id)` — checks permission in tenant context
42
+ - `assign_role` now accepts optional `tenant_id:` keyword argument for tenant-scoped assignments
43
+ - `remove_role` now accepts optional `tenant_id:` keyword argument
44
+ - `Role` scopes: `.global` (where tenant_id IS NULL), `.for_tenant(tenant_id)` (tenant + global roles)
45
+ - `Role` model: conditional `belongs_to :tenant` when `tenant_scoped` is enabled
46
+ - `UserRole` model: conditional `belongs_to :tenant` when `tenant_scoped` is enabled
47
+ - Initializer template now documents all new configuration options
48
+ - Migration templates now use explicit `type:` on `t.references` for FK columns
49
+
50
+ ### Fixed
51
+
52
+ - `t.bigint :user_id` was hardcoded in `create_user_roles` migration — now uses `t.references :user` with proper type detection matching the host app's PK type
53
+ - Migration templates now conditionally pass `id: :uuid` to `create_table` based on host app configuration
54
+ - `create_role_permissions` migration now specifies explicit `type:` on foreign key references
55
+
56
+ ---
57
+
5
58
  ## [0.3.0] - 2026-03-31
6
59
 
7
60
  ### Added
data/README.md CHANGED
@@ -38,6 +38,60 @@ bundle install
38
38
 
39
39
  ## Setup
40
40
 
41
+ RBACan 0.5 has two explicit integration modes:
42
+
43
+ - **Host-owned mode (recommended):** the application owns schema, tenancy, scopes, assignment history,
44
+ and record policies. Include `Rbacan::Authorizable` and configure adapters. Do not run the generator.
45
+ - **Legacy generated-schema mode:** preserves the 0.4 Active Record API only after conscious
46
+ `config.legacy_mode = true` opt-in. Its removal methods delete join rows and are unsuitable for
47
+ immutable assignment-history requirements.
48
+
49
+ ### Host-owned mode
50
+
51
+ ```ruby
52
+ class User < ApplicationRecord
53
+ include Rbacan::Authorizable
54
+ end
55
+
56
+ Rbacan.configure do |config|
57
+ config.role_class = 'Role'
58
+ config.role_key = :code
59
+ config.permission_class = 'Permission'
60
+ config.permission_key = :code
61
+ config.authorization_adapter = MyAuthorizationAdapter.new
62
+ config.assignment_adapter = MyAssignmentAdapter.new
63
+ end
64
+ ```
65
+
66
+ Every check receives an explicit immutable value context. Pass stable identifiers or JSON-like
67
+ references, not live Active Record objects:
68
+
69
+ ```ruby
70
+ context = Rbacan::AuthorizationContext.new(
71
+ tenant: current_tenant.id,
72
+ property: current_property.id,
73
+ vertical: current_property.property_type.to_s,
74
+ subject: { type: reservation.class.name, id: reservation.id },
75
+ at: Time.current
76
+ )
77
+
78
+ current_user.can?(:hotel_reservations_read, context: context)
79
+ ```
80
+
81
+ The tenant identifier is required and cannot be nil, boolean, or empty. Tenant, property, vertical,
82
+ subject, metadata, and timestamp values are copied
83
+ and frozen when the context is created. Context identifiers must come from records already loaded
84
+ through the host application's authenticated scope; the authorization adapter resolves and validates
85
+ their relationships under the host's tenancy/RLS rules. The request's `user` remains the live host
86
+ principal object.
87
+
88
+ Adapters receive one request object. Authorization adapters respond to `allowed?(request)` or `call`;
89
+ assignment adapters respond to `assign(request)` and `revoke(request)`. RBACan never prescribes deletion
90
+ or caching in host-owned mode. Optional context metadata is deeply copied and frozen and accepts only
91
+ hashes, arrays, strings, finite integers/floats, booleans, nil, and symbols.
92
+
93
+ ### Legacy generated-schema mode
94
+
41
95
  **1. Run the generator**
42
96
 
43
97
  ```bash
@@ -72,13 +126,21 @@ rails db:seed
72
126
 
73
127
  ## Configuration
74
128
 
75
- The generator creates `config/initializers/rbacan.rb`. All options are optional — the defaults work for a standard Devise setup.
129
+ The generator creates `config/initializers/rbacan.rb`. Generated-schema applications must explicitly
130
+ enable legacy mode; the remaining defaults then work for a standard Devise setup.
76
131
 
77
132
  ```ruby
78
133
  Rbacan.configure do |config|
134
+ # Conscious opt-in required for the destructive/unscoped 0.4 API while migrating.
135
+ config.legacy_mode = true
136
+
79
137
  # Your user model name (default: "User")
80
138
  config.permittable_class = "User"
81
139
 
140
+ # Host key columns default to :name.
141
+ # config.role_key = :code
142
+ # config.permission_key = :code
143
+
82
144
  # Override model class names if you have custom implementations
83
145
  # config.role_class = "Rbacan::Role"
84
146
  # config.permission_class = "Rbacan::Permission"
@@ -107,7 +169,7 @@ end
107
169
 
108
170
  ## Defining Roles and Permissions
109
171
 
110
- Use the `Rbacan::RolesAndPermissions` module in your seeds. All methods are idempotent — safe to run multiple times.
172
+ In legacy mode, use the `Rbacan::RolesAndPermissions` module in your seeds. All methods are idempotent — safe to run multiple times. Hardened mode rejects these unscoped helpers; host-owned applications must seed their own scoped authorization records.
111
173
 
112
174
  ```ruby
113
175
  # db/seeds.rb
@@ -201,9 +263,20 @@ Include `Rbacan::Authorization` in your `ApplicationController` (or any specific
201
263
  ```ruby
202
264
  class ApplicationController < ActionController::Base
203
265
  include Rbacan::Authorization
266
+ helper_method :rbacan_authorization_context
267
+
268
+ private
269
+
270
+ def rbacan_authorization_context
271
+ AuthorizationContextResolver.call(request: request, user: current_user, subject: @post)
272
+ end
204
273
  end
205
274
  ```
206
275
 
276
+ The host resolver must build the immutable identifier context only after authenticating the user and
277
+ loading tenant, property, and optional subject records through the authorized scope.
278
+ `AuthorizationContextResolver` is application-defined; RBACan does not infer these values.
279
+
207
280
  Then use `authorize!` or `authorize_role!` as `before_action` callbacks:
208
281
 
209
282
  ```ruby
@@ -255,7 +328,9 @@ config.unauthorized_handler = ->(controller, permission:, role:) {
255
328
 
256
329
  ## View Helpers
257
330
 
258
- `Rbacan::ViewHelpers` is automatically included in all views. Use `authorized?` to conditionally render content:
331
+ `Rbacan::ViewHelpers` is automatically included in all views. In host-owned mode the host must expose
332
+ `current_user` and `rbacan_authorization_context` to the view. Use `authorized?` to conditionally
333
+ render content:
259
334
 
260
335
  ```erb
261
336
  <% authorized?(:delete_post) do %>
@@ -267,40 +342,47 @@ config.unauthorized_handler = ->(controller, permission:, role:) {
267
342
  <% end %>
268
343
  ```
269
344
 
270
- You can also use `has_role?` and `has_any_role?` directly in views since they are instance methods on the user:
345
+ You can also use `has_role?` directly in views through the host-owned concern:
271
346
 
272
347
  ```erb
273
- <% if current_user.has_role?(:admin) %>
348
+ <% if current_user.has_role?(:admin, context: rbacan_authorization_context) %>
274
349
  <%= link_to "Admin Panel", admin_root_path %>
275
350
  <% end %>
276
-
277
- <% if current_user.has_any_role?(:admin, :moderator) %>
278
- <%= link_to "Moderation Queue", moderation_path %>
279
- <% end %>
280
351
  ```
281
352
 
282
353
  ---
283
354
 
284
355
  ## Route Constraints
285
356
 
286
- Restrict access to entire route namespaces based on role or permission. Add constraints in `config/routes.rb`:
357
+ Restrict access to entire route namespaces based on role or permission. Hardened mode requires explicit
358
+ request resolvers:
287
359
 
288
360
  ```ruby
289
- # Restrict by role
290
- constraints Rbacan::RouteConstraint.new(role: :admin) do
361
+ resolve_user = ->(request) { CurrentSessionResolver.call(request) }
362
+ resolve_context = ->(request) { AuthorizationContextResolver.call(request) }
363
+
364
+ constraints Rbacan::RouteConstraint.new(
365
+ role: :admin,
366
+ user_resolver: resolve_user,
367
+ context_resolver: resolve_context
368
+ ) do
291
369
  namespace :admin do
292
370
  resources :users
293
371
  resources :roles
294
372
  end
295
373
  end
296
374
 
297
- # Restrict by permission
298
- constraints Rbacan::RouteConstraint.new(permission: :access_dashboard) do
375
+ constraints Rbacan::RouteConstraint.new(
376
+ permission: :access_dashboard,
377
+ user_resolver: resolve_user,
378
+ context_resolver: resolve_context
379
+ ) do
299
380
  get "/dashboard", to: "dashboard#index"
300
381
  end
301
382
  ```
302
383
 
303
- The constraint reads the current user from the Warden session (used by Devise). For apps using a manual session, it falls back to looking up `session[:user_id]`.
384
+ Without both resolvers hardened constraints fail closed. In explicitly enabled legacy mode only, the
385
+ constraint retains the 0.4 behavior of reading Warden or `session[:user_id]`.
304
386
 
305
387
  ---
306
388
 
@@ -1,13 +1,26 @@
1
1
  module Rbacan
2
- class Role < ApplicationRecord
3
- self.table_name = Rbacan.role_table
2
+ class Role < ApplicationRecord
3
+ self.table_name = Rbacan.role_table
4
4
 
5
- validates :name, presence: true, uniqueness: true
6
-
7
- has_many :role_permissions, class_name: Rbacan.role_permission_class, dependent: :destroy
8
- has_many :permissions, class_name: Rbacan.permission_class, through: :role_permissions
5
+ validates :name, presence: true
6
+ validates :name, uniqueness: true, unless: -> { Rbacan.tenant_scoped && tenant_id.present? }
7
+ validates :name, uniqueness: { scope: :tenant_id }, if: -> { Rbacan.tenant_scoped && tenant_id.present? }
9
8
 
10
- has_many :user_roles, class_name: Rbacan.user_role_class, dependent: :destroy
11
- has_many :users, class_name: Rbacan.permittable_class, through: :user_roles
12
- end
9
+ has_many :role_permissions, class_name: Rbacan.role_permission_class, dependent: :destroy
10
+ has_many :permissions, class_name: Rbacan.permission_class, through: :role_permissions
11
+
12
+ has_many :user_roles, class_name: Rbacan.user_role_class, dependent: :destroy
13
+ has_many :users, class_name: Rbacan.permittable_class, through: :user_roles
14
+
15
+ # Tenant association — only meaningful when tenant_scoped is enabled.
16
+ # The column must exist in the database; it is added by the migration
17
+ # when tenant_scoped is true.
18
+ belongs_to :tenant, class_name: Rbacan.tenant_class, optional: true if Rbacan.tenant_scoped
19
+
20
+ # Global roles have no tenant (tenant_id IS NULL).
21
+ scope :global, -> { where(tenant_id: nil) }
22
+
23
+ # Returns roles available for a specific tenant: its own roles plus global roles.
24
+ scope :for_tenant, ->(tenant_id) { where(tenant_id: [tenant_id, nil]) }
25
+ end
13
26
  end
@@ -1,8 +1,11 @@
1
1
  module Rbacan
2
- class UserRole < ApplicationRecord
3
- self.table_name = Rbacan.user_role_table
2
+ class UserRole < ApplicationRecord
3
+ self.table_name = Rbacan.user_role_table
4
4
 
5
- belongs_to :role, class_name: Rbacan.role_class
6
- belongs_to :user, class_name: Rbacan.permittable_class
7
- end
5
+ belongs_to :role, class_name: Rbacan.role_class
6
+ belongs_to :user, class_name: Rbacan.permittable_class
7
+
8
+ # Tenant association — only meaningful when tenant_scoped is enabled.
9
+ belongs_to :tenant, class_name: Rbacan.tenant_class, optional: true if Rbacan.tenant_scoped
10
+ end
8
11
  end
@@ -1,6 +1,6 @@
1
1
  class CreatePermissions < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
2
2
  def change
3
- create_table :permissions do |t|
3
+ create_table :permissions<%= ", id: :uuid" if primary_key_type == :uuid %> do |t|
4
4
  t.string :name, null: false
5
5
 
6
6
  t.timestamps
@@ -1,8 +1,8 @@
1
1
  class CreateRolePermissions < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
2
2
  def change
3
- create_table :role_permissions do |t|
4
- t.references :role, null: false, index: true, foreign_key: { to_table: :roles, on_delete: :cascade }
5
- t.references :permission, null: false, index: true, foreign_key: { to_table: :permissions, on_delete: :cascade }
3
+ create_table :role_permissions<%= ", id: :uuid" if primary_key_type == :uuid %> do |t|
4
+ t.references :role, null: false, type: :<%= primary_key_type %>, index: true, foreign_key: { to_table: :roles, on_delete: :cascade }
5
+ t.references :permission, null: false, type: :<%= primary_key_type %>, index: true, foreign_key: { to_table: :permissions, on_delete: :cascade }
6
6
 
7
7
  t.timestamps
8
8
  end
@@ -1,11 +1,20 @@
1
1
  class CreateRoles < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
2
2
  def change
3
- create_table :roles do |t|
3
+ create_table :roles<%= ", id: :uuid" if primary_key_type == :uuid %> do |t|
4
4
  t.string :name, null: false
5
+ <% if tenant_scoped? %>
6
+ t.references :tenant, null: true, type: :<%= primary_key_type %>, index: true
7
+ <% end %>
5
8
 
6
9
  t.timestamps
7
10
  end
11
+ <% if tenant_scoped? %>
12
+
13
+ add_index :roles, :name, unique: true, where: "tenant_id IS NULL", name: "index_roles_on_name_global"
14
+ add_index :roles, [:tenant_id, :name], unique: true, name: "index_roles_on_tenant_id_and_name"
15
+ <% else %>
8
16
 
9
17
  add_index :roles, :name, unique: true
18
+ <% end %>
10
19
  end
11
20
  end
@@ -1,13 +1,20 @@
1
1
  class CreateUserRoles < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
2
2
  def change
3
- create_table :user_roles do |t|
4
- t.references :role, null: false, index: true, foreign_key: { to_table: :roles, on_delete: :cascade }
5
- t.bigint :user_id, null: false
3
+ create_table :user_roles<%= ", id: :uuid" if primary_key_type == :uuid %> do |t|
4
+ t.references :role, null: false, type: :<%= primary_key_type %>, index: true, foreign_key: { to_table: :roles, on_delete: :cascade }
5
+ t.references :user, null: false, type: :<%= primary_key_type %>, index: true
6
+ <% if tenant_scoped? %>
7
+ t.references :tenant, null: true, type: :<%= primary_key_type %>, index: true
8
+ <% end %>
6
9
 
7
10
  t.timestamps
8
11
  end
12
+ <% if tenant_scoped? %>
13
+
14
+ add_index :user_roles, [:user_id, :role_id, :tenant_id], unique: true, name: "index_user_roles_unique"
15
+ <% else %>
9
16
 
10
- add_index :user_roles, :user_id
11
17
  add_index :user_roles, [:user_id, :role_id], unique: true
18
+ <% end %>
12
19
  end
13
20
  end
@@ -1,30 +1,45 @@
1
1
  Rbacan.configure do |config|
2
- # The name of your user model (default: "User")
2
+ # Hardened host-owned mode is the default. The generated 0.4-style schema remains
3
+ # inactive until the host consciously opts into its destructive/unscoped APIs:
4
+ # config.legacy_mode = true
5
+
6
+ # Name of your user model (default: "User")
3
7
  # config.permittable_class = "User"
4
8
 
5
- # Override AR model class names if you have custom models
6
- # config.role_class = "Rbacan::Role"
7
- # config.permission_class = "Rbacan::Permission"
8
- # config.user_role_class = "Rbacan::UserRole"
9
- # config.role_permission_class = "Rbacan::RolePermission"
9
+ # Override ActiveRecord model class names if you use custom models
10
+ # config.role_class = "Rbacan::Role"
11
+ # config.permission_class = "Rbacan::Permission"
12
+ # config.user_role_class = "Rbacan::UserRole"
13
+ # config.role_permission_class = "Rbacan::RolePermission"
14
+
15
+ # Host role/permission lookup columns (default: :name)
16
+ # config.role_key = :code
17
+ # config.permission_key = :code
10
18
 
11
19
  # Override table names if needed
12
- # config.role_table = "roles"
13
- # config.permission_table = "permissions"
14
- # config.user_role_table = "user_roles"
15
- # config.role_permission_table = "role_permissions"
20
+ # config.role_table = "roles"
21
+ # config.permission_table = "permissions"
22
+ # config.user_role_table = "user_roles"
23
+ # config.role_permission_table = "role_permissions"
24
+
25
+ # Primary key type for migrations.
26
+ # Set :uuid if your app uses UUID primary keys.
27
+ # When nil, auto-detects Rails generator config (primary_key_type).
28
+ # config.primary_key_type = :uuid
29
+
30
+ # Enable tenant scoping for roles and user_roles.
31
+ # When true, adds tenant_id to roles and user_roles.
32
+ # Permissions remain global regardless of this setting.
33
+ # config.tenant_scoped = false
34
+
35
+ # Name of your tenant model class (default: "Tenant").
36
+ # Only used when tenant_scoped is true.
37
+ # config.tenant_class = "Tenant"
16
38
 
17
39
  # Authorization failure handling:
18
40
  # :raise — raises Rbacan::NotAuthorized (default)
19
41
  # :redirect — redirects to unauthorized_redirect_path
20
42
  # lambda — called with (controller, permission:, role:)
21
- #
22
43
  # config.unauthorized_handler = :raise
23
- # config.unauthorized_handler = :redirect
24
- # config.unauthorized_handler = ->(controller, permission:, role:) {
25
- # controller.render plain: "Forbidden", status: :forbidden
26
- # }
27
-
28
- # Path to redirect to when unauthorized_handler is :redirect
29
44
  # config.unauthorized_redirect_path = "/"
30
45
  end
@@ -5,8 +5,8 @@ module Rbacan
5
5
  module Generators
6
6
  class InstallGenerator < ::Rails::Generators::Base
7
7
  include Rails::Generators::Migration
8
- source_root File.expand_path('../../install/templates', __FILE__)
9
- desc "Add the migrations for roles and permissions"
8
+ source_root File.expand_path('../install/templates', __dir__)
9
+ desc 'Add the migrations for roles and permissions'
10
10
 
11
11
  def self.next_migration_number(path)
12
12
  next_migration_number = current_migration_number(path) + 1
@@ -14,19 +14,38 @@ module Rbacan
14
14
  end
15
15
 
16
16
  def copy_migrations
17
- migration_template "create_permissions.rb", "db/migrate/create_permissions.rb"
18
- migration_template "create_roles.rb", "db/migrate/create_roles.rb"
19
- migration_template "create_role_permissions.rb", "db/migrate/create_role_permissions.rb"
20
- migration_template "create_user_roles.rb", "db/migrate/create_user_roles.rb"
17
+ migration_template 'create_permissions.rb', 'db/migrate/create_permissions.rb'
18
+ migration_template 'create_roles.rb', 'db/migrate/create_roles.rb'
19
+ migration_template 'create_role_permissions.rb', 'db/migrate/create_role_permissions.rb'
20
+ migration_template 'create_user_roles.rb', 'db/migrate/create_user_roles.rb'
21
21
  end
22
22
 
23
23
  def copy_seed
24
- copy_file 'copy_to_seeds.rb', "db/copy_to_seeds.rb"
24
+ copy_file 'copy_to_seeds.rb', 'db/copy_to_seeds.rb'
25
25
  end
26
26
 
27
27
  def copy_initializer
28
28
  copy_file 'rbacan.rb', 'config/initializers/rbacan.rb'
29
29
  end
30
+
31
+ private
32
+
33
+ # Resolves the primary key type for migration templates.
34
+ # Checks the explicit Rbacan config first, then the Rails generator config.
35
+ def primary_key_type
36
+ configured = Rbacan.primary_key_type
37
+ configured ||= begin
38
+ Rails.configuration.generators.options.dig(:active_record, :primary_key_type)
39
+ rescue StandardError
40
+ nil
41
+ end
42
+ configured || :bigint
43
+ end
44
+
45
+ # Whether tenant scoping is enabled.
46
+ def tenant_scoped?
47
+ Rbacan.tenant_scoped
48
+ end
30
49
  end
31
50
  end
32
51
  end
@@ -0,0 +1,15 @@
1
+ require 'active_support/concern'
2
+
3
+ module Rbacan
4
+ module Authorizable
5
+ extend ActiveSupport::Concern
6
+
7
+ def can?(permission, context:)
8
+ Rbacan.authorized?(user: self, permission: permission, context: context)
9
+ end
10
+
11
+ def has_role?(role, context:)
12
+ Rbacan.authorized?(user: self, role: role, context: context)
13
+ end
14
+ end
15
+ end
@@ -1,29 +1,51 @@
1
1
  require 'active_support/concern'
2
+ require 'i18n'
2
3
 
3
4
  module Rbacan
4
5
  module Authorization
5
6
  extend ActiveSupport::Concern
6
7
 
7
- included do
8
- helper_method :authorized? if respond_to?(:helper_method)
9
- end
10
-
11
8
  # Calls the unauthorized handler if current_user lacks the given permission.
12
- def authorize!(permission)
13
- unless current_user&.can?(permission.to_s)
9
+ def authorize!(permission, context: nil)
10
+ allowed = if Rbacan.legacy_mode
11
+ current_user&.can?(permission.to_s)
12
+ else
13
+ Rbacan.authorized?(
14
+ user: current_user,
15
+ permission: permission,
16
+ context: context || rbacan_authorization_context
17
+ )
18
+ end
19
+
20
+ unless allowed
14
21
  _handle_unauthorized(permission: permission.to_s)
15
22
  end
16
23
  end
17
24
 
18
25
  # Calls the unauthorized handler if current_user lacks the given role.
19
- def authorize_role!(role)
20
- unless current_user&.has_role?(role.to_s)
26
+ def authorize_role!(role, context: nil)
27
+ allowed = if Rbacan.legacy_mode
28
+ current_user&.has_role?(role.to_s)
29
+ else
30
+ Rbacan.authorized?(
31
+ user: current_user,
32
+ role: role,
33
+ context: context || rbacan_authorization_context
34
+ )
35
+ end
36
+
37
+ unless allowed
21
38
  _handle_unauthorized(role: role.to_s)
22
39
  end
23
40
  end
24
41
 
25
42
  private
26
43
 
44
+ def rbacan_authorization_context
45
+ raise Rbacan::ConfigurationError,
46
+ 'authorization context is required; define rbacan_authorization_context in the host controller'
47
+ end
48
+
27
49
  def _handle_unauthorized(permission: nil, role: nil)
28
50
  handler = Rbacan.unauthorized_handler
29
51
 
@@ -31,8 +53,14 @@ module Rbacan
31
53
  when :raise
32
54
  raise Rbacan::NotAuthorized.new(nil, permission: permission, role: role)
33
55
  when :redirect
56
+ error = Rbacan::NotAuthorized.new(nil, permission: permission, role: role)
57
+ alert = if Rbacan.legacy_mode
58
+ error.message
59
+ else
60
+ I18n.t(error.code, permission: permission, role: role, raise: true)
61
+ end
34
62
  redirect_to Rbacan.unauthorized_redirect_path,
35
- alert: Rbacan::NotAuthorized.new(nil, permission: permission, role: role).message
63
+ alert: alert
36
64
  else
37
65
  if handler.respond_to?(:call)
38
66
  handler.call(self, permission: permission, role: role)