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 +4 -4
- data/CHANGELOG.md +53 -0
- data/README.md +97 -15
- data/app/models/rbacan/role.rb +22 -9
- data/app/models/rbacan/user_role.rb +8 -5
- data/lib/generators/install/templates/create_permissions.rb +1 -1
- data/lib/generators/install/templates/create_role_permissions.rb +3 -3
- data/lib/generators/install/templates/create_roles.rb +10 -1
- data/lib/generators/install/templates/create_user_roles.rb +11 -4
- data/lib/generators/install/templates/rbacan.rb +32 -17
- data/lib/generators/rbacan/install_generator.rb +26 -7
- data/lib/rbacan/authorizable.rb +15 -0
- data/lib/rbacan/authorization.rb +37 -9
- data/lib/rbacan/host_authorization.rb +165 -0
- data/lib/rbacan/not_authorized.rb +10 -3
- data/lib/rbacan/permittable.rb +74 -36
- data/lib/rbacan/roles_and_permissions.rb +12 -4
- data/lib/rbacan/route_constraint.rb +21 -18
- data/lib/rbacan/version.rb +1 -1
- data/lib/rbacan/view_helpers.rb +13 -8
- data/lib/rbacan.rb +63 -14
- metadata +37 -44
- data/.gitignore +0 -11
- data/.rspec +0 -3
- data/.travis.yml +0 -7
- data/Gemfile +0 -4
- data/Gemfile.lock +0 -319
- data/Rakefile +0 -6
- data/bin/console +0 -14
- data/bin/setup +0 -8
- data/rbacan.gemspec +0 -42
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d73e61377cfc8c44c60d79857c8a5c09be75ddd8c7e6bd1547895dc11f03a673
|
|
4
|
+
data.tar.gz: 7027bf24c300f0996fd12cb94875bfb16ab24f0119b4442547da1ea054fb365b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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`.
|
|
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
|
-
|
|
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.
|
|
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?`
|
|
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.
|
|
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
|
-
|
|
290
|
-
|
|
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
|
-
|
|
298
|
-
|
|
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
|
-
|
|
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
|
|
data/app/models/rbacan/role.rb
CHANGED
|
@@ -1,13 +1,26 @@
|
|
|
1
1
|
module Rbacan
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
class Role < ApplicationRecord
|
|
3
|
+
self.table_name = Rbacan.role_table
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
3
|
-
|
|
2
|
+
class UserRole < ApplicationRecord
|
|
3
|
+
self.table_name = Rbacan.user_role_table
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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.
|
|
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
|
-
#
|
|
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
|
|
6
|
-
# config.role_class
|
|
7
|
-
# config.permission_class
|
|
8
|
-
# config.user_role_class
|
|
9
|
-
# config.role_permission_class
|
|
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
|
|
13
|
-
# config.permission_table
|
|
14
|
-
# config.user_role_table
|
|
15
|
-
# config.role_permission_table
|
|
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('
|
|
9
|
-
desc
|
|
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
|
|
18
|
-
migration_template
|
|
19
|
-
migration_template
|
|
20
|
-
migration_template
|
|
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',
|
|
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
|
data/lib/rbacan/authorization.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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:
|
|
63
|
+
alert: alert
|
|
36
64
|
else
|
|
37
65
|
if handler.respond_to?(:call)
|
|
38
66
|
handler.call(self, permission: permission, role: role)
|