rabarber 2.0.0 → 3.0.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 +24 -2
- data/README.md +101 -20
- data/lib/generators/rabarber/templates/create_rabarber_roles.rb.erb +4 -1
- data/lib/rabarber/audit/events/base.rb +13 -15
- data/lib/rabarber/audit/events/roles_assigned.rb +3 -3
- data/lib/rabarber/audit/events/roles_revoked.rb +3 -3
- data/lib/rabarber/audit/events/unauthorized_attempt.rb +5 -5
- data/lib/rabarber/controllers/concerns/authorization.rb +10 -3
- data/lib/rabarber/core/access.rb +7 -8
- data/lib/rabarber/core/cache.rb +13 -9
- data/lib/rabarber/core/permissions.rb +2 -2
- data/lib/rabarber/core/roleable.rb +9 -3
- data/lib/rabarber/core/rule.rb +22 -10
- data/lib/rabarber/helpers/helpers.rb +4 -4
- data/lib/rabarber/input/action.rb +2 -4
- data/lib/rabarber/input/authorization_context.rb +25 -0
- data/lib/rabarber/input/context.rb +33 -0
- data/lib/rabarber/input/dynamic_rule.rb +2 -4
- data/lib/rabarber/models/concerns/has_roles.rb +43 -20
- data/lib/rabarber/models/role.rb +27 -17
- data/lib/rabarber/version.rb +1 -1
- data/lib/rabarber.rb +2 -0
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 51c1cba47b8af312f38ff403b77ef716099101d5bed3226c3e7d2bad37c23874
|
4
|
+
data.tar.gz: 3da6569ceef5c624aa649240341fcb72ce879005e458044006ba547aadbe316e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9d5c8fd851b43d71f0f1ca9bfe6671c5d23fe3db1c6b08b8d78ff6415e0e7528e4de1c86c1c5bb9fcdd9143ca050183268e94a981fe8bd89002e75f7e437064f
|
7
|
+
data.tar.gz: a11d2aa5c8bb3ff69b1db5d8d6aae6b5f2590ae03f5fe99388db8ff809cf6ff3b9d8c9461a1936f08faade2a7b7711d4010fa7d49bfad36ab582f80d14708893
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,25 @@
|
|
1
|
+
## v3.0.0
|
2
|
+
|
3
|
+
### Breaking:
|
4
|
+
|
5
|
+
- Changed Rabarber roles table structure
|
6
|
+
|
7
|
+
To upgrade to v3.0.0, please refer to the [migration guide](https://github.com/enjaku4/rabarber/discussions/58)
|
8
|
+
|
9
|
+
### Features:
|
10
|
+
|
11
|
+
- Introduced the ability to define and authorize roles within a specific context
|
12
|
+
|
13
|
+
### Misc:
|
14
|
+
|
15
|
+
- Revised log messages in the audit trail for clarity and conciseness
|
16
|
+
|
17
|
+
## v2.1.0
|
18
|
+
|
19
|
+
### Features:
|
20
|
+
|
21
|
+
- Added `Rabarber::Authorization.skip_authorization` method to skip authorization checks
|
22
|
+
|
1
23
|
## v2.0.0
|
2
24
|
|
3
25
|
### Breaking:
|
@@ -6,7 +28,7 @@
|
|
6
28
|
- Replaced `when_unauthorized` configuration option with an overridable controller method
|
7
29
|
- Renamed `Rabarber::Role.assignees_for` method to `Rabarber::Role.assignees`
|
8
30
|
|
9
|
-
To upgrade to v2.0.0, please refer to the [migration guide](https://github.com/enjaku4/rabarber/discussions/52)
|
31
|
+
To upgrade to v2.0.0, please refer to the [migration guide](https://github.com/enjaku4/rabarber/discussions/52)
|
10
32
|
|
11
33
|
### Features:
|
12
34
|
|
@@ -14,7 +36,7 @@ To upgrade to v2.0.0, please refer to the [migration guide](https://github.com/e
|
|
14
36
|
|
15
37
|
### Bugs:
|
16
38
|
|
17
|
-
- Fixed the issue where an error would occur if the user was not authenticated
|
39
|
+
- Fixed the issue where an error would occur when using view helpers if the user was not authenticated
|
18
40
|
|
19
41
|
### Misc:
|
20
42
|
|
data/README.md
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
[](http://badge.fury.io/rb/rabarber)
|
4
4
|
[](https://github.com/enjaku4/rabarber/actions/workflows/ci.yml)
|
5
5
|
|
6
|
-
Rabarber is a role-based authorization library for Ruby on Rails
|
6
|
+
Rabarber is a role-based authorization library for Ruby on Rails. It provides a set of tools for managing user roles and defining authorization rules, supports multi-tenancy and comes with audit logging for enhanced security.
|
7
7
|
|
8
8
|
---
|
9
9
|
|
@@ -39,7 +39,9 @@ This means that `admin` users can access everything in `TicketsController`, whil
|
|
39
39
|
- [Roles](#roles)
|
40
40
|
- [Authorization Rules](#authorization-rules)
|
41
41
|
- [Dynamic Authorization Rules](#dynamic-authorization-rules)
|
42
|
+
- [Context / Multi-tenancy](#context--multi-tenancy)
|
42
43
|
- [When Unauthorized](#when-unauthorized)
|
44
|
+
- [Skip Authorization](#skip-authorization)
|
43
45
|
- [View Helpers](#view-helpers)
|
44
46
|
- [Audit Trail](#audit-trail)
|
45
47
|
|
@@ -114,7 +116,7 @@ end
|
|
114
116
|
|
115
117
|
This adds the following methods:
|
116
118
|
|
117
|
-
**`#assign_roles(*roles, create_new: true)`**
|
119
|
+
**`#assign_roles(*roles, context: nil, create_new: true)`**
|
118
120
|
|
119
121
|
To assign roles, use:
|
120
122
|
|
@@ -127,7 +129,7 @@ user.assign_roles(:accountant, :marketer, create_new: false)
|
|
127
129
|
```
|
128
130
|
The method returns an array of roles assigned to the user.
|
129
131
|
|
130
|
-
**`#revoke_roles(*roles)`**
|
132
|
+
**`#revoke_roles(*roles, context: nil)`**
|
131
133
|
|
132
134
|
To revoke roles, use:
|
133
135
|
|
@@ -138,7 +140,7 @@ If the user doesn't have the role you want to revoke, it will be ignored.
|
|
138
140
|
|
139
141
|
The method returns an array of roles assigned to the user.
|
140
142
|
|
141
|
-
**`#has_role?(*roles)`**
|
143
|
+
**`#has_role?(*roles, context: nil)`**
|
142
144
|
|
143
145
|
To check whether the user has a role, use:
|
144
146
|
|
@@ -148,9 +150,9 @@ user.has_role?(:accountant, :marketer)
|
|
148
150
|
|
149
151
|
It returns `true` if the user has at least one role and `false` otherwise.
|
150
152
|
|
151
|
-
**`#roles`**
|
153
|
+
**`#roles(context: nil)`**
|
152
154
|
|
153
|
-
To get
|
155
|
+
To get the list of roles assigned to the user, use:
|
154
156
|
|
155
157
|
```rb
|
156
158
|
user.roles
|
@@ -160,7 +162,7 @@ user.roles
|
|
160
162
|
|
161
163
|
To manipulate roles directly, you can use `Rabarber::Role` methods:
|
162
164
|
|
163
|
-
**`.add(role_name)`**
|
165
|
+
**`.add(role_name, context: nil)`**
|
164
166
|
|
165
167
|
To add a new role, use:
|
166
168
|
|
@@ -170,7 +172,7 @@ Rabarber::Role.add(:admin)
|
|
170
172
|
|
171
173
|
This will create a new role with the specified name and return `true`. If the role already exists, it will return `false`.
|
172
174
|
|
173
|
-
**`.rename(old_role_name, new_role_name, force: false)`**
|
175
|
+
**`.rename(old_role_name, new_role_name, context: nil, force: false)`**
|
174
176
|
|
175
177
|
To rename a role, use:
|
176
178
|
|
@@ -184,7 +186,7 @@ The method won't rename the role and will return `false` if it is assigned to an
|
|
184
186
|
Rabarber::Role.rename(:admin, :administrator, force: true)
|
185
187
|
```
|
186
188
|
|
187
|
-
**`.remove(role_name, force: false)`**
|
189
|
+
**`.remove(role_name, context: nil, force: false)`**
|
188
190
|
|
189
191
|
To remove a role, use:
|
190
192
|
|
@@ -199,15 +201,15 @@ The method won't remove the role and will return `false` if it is assigned to an
|
|
199
201
|
Rabarber::Role.remove(:admin, force: true)
|
200
202
|
```
|
201
203
|
|
202
|
-
**`.names`**
|
204
|
+
**`.names(context: nil)`**
|
203
205
|
|
204
|
-
If you need to list
|
206
|
+
If you need to list the role names available in your application, use:
|
205
207
|
|
206
208
|
```rb
|
207
209
|
Rabarber::Role.names
|
208
210
|
```
|
209
211
|
|
210
|
-
**`.assignees(role_name)`**
|
212
|
+
**`.assignees(role_name, context: nil)`**
|
211
213
|
|
212
214
|
To get all the users to whom the role is assigned, use:
|
213
215
|
|
@@ -225,7 +227,7 @@ class ApplicationController < ActionController::Base
|
|
225
227
|
# ...
|
226
228
|
end
|
227
229
|
```
|
228
|
-
This adds `.grant_access(action: nil, roles: nil, if: nil, unless: nil)` method which allows you to define the authorization rules.
|
230
|
+
This adds `.grant_access(action: nil, roles: nil, context: nil, if: nil, unless: nil)` method which allows you to define the authorization rules.
|
229
231
|
|
230
232
|
The most basic usage of the method is as follows:
|
231
233
|
|
@@ -246,8 +248,6 @@ end
|
|
246
248
|
```
|
247
249
|
This grants access to `index` action for users with `accountant` or `admin` role, and access to `destroy` action for `admin` users only.
|
248
250
|
|
249
|
-
Please note that Rabarber does not provide any built-in data scoping mechanism as it is not a part of the authorization layer and is not necessarily role specific or has anything to do with the current user. The business logic can vary drastically depending on the application, so you're encouraged to limit the data visibility yourself, for example, in the same way as in the example above, where `accountant` role can only see paid invoices.
|
250
|
-
|
251
251
|
You can also define controller-wide rules (without `action` argument):
|
252
252
|
|
253
253
|
```rb
|
@@ -289,11 +289,9 @@ class InvoicesController < ApplicationController
|
|
289
289
|
end
|
290
290
|
```
|
291
291
|
|
292
|
-
This allows everyone to access `OrdersController` and its children and also `index` action in `InvoicesController`.
|
293
|
-
|
294
|
-
If the user is not authenticated (the method responsible for returning the currently authenticated user in your application returns `nil`), Rabarber will handle this situation as if the user has no roles.
|
292
|
+
This allows everyone to access `OrdersController` and its children and also `index` action in `InvoicesController`.
|
295
293
|
|
296
|
-
If you've set `must_have_roles` setting to `true`, then only the users with at least one role can gain access. This setting can be useful if your requirements are such that users without roles
|
294
|
+
If you've set `must_have_roles` setting to `true`, then only the users with at least one role can gain access. This setting can be useful if your requirements are such that users without roles are not allowed to access anything.
|
297
295
|
|
298
296
|
Also keep in mind that rules defined in child classes don't override parent rules but rather add to them:
|
299
297
|
```rb
|
@@ -349,7 +347,7 @@ class Crm::InvoicesController < ApplicationController
|
|
349
347
|
end
|
350
348
|
end
|
351
349
|
```
|
352
|
-
You can pass a dynamic rule as `if` or `unless` argument. It can be a symbol, in which case the method with that name will be called
|
350
|
+
You can pass a dynamic rule as `if` or `unless` argument. It can be a symbol, in which case the method with that name will be called, or alternatively it can be a proc that will be executed within the context of the controller instance at request time.
|
353
351
|
|
354
352
|
You can use only dynamic rules without specifying roles if that suits your needs:
|
355
353
|
```rb
|
@@ -370,6 +368,76 @@ class InvoicesController < ApplicationController
|
|
370
368
|
end
|
371
369
|
```
|
372
370
|
|
371
|
+
## Context / Multi-tenancy
|
372
|
+
|
373
|
+
Rabarber supports multi-tenancy by providing a context feature. This allows you to define and authorize roles and rules within a specific context.
|
374
|
+
|
375
|
+
Every Rabarber method that accepts roles can also accept a context as an additional keyword argument. By default, the context is set to `nil`, meaning the roles are global. Thus, all examples from other sections of this README are valid for global roles. Apart from being global, the context can be an instance of ActiveRecord model or a class.
|
376
|
+
|
377
|
+
E.g., consider a model named `Project`, where each project has its owner and regular members. Roles can be defined like this:
|
378
|
+
|
379
|
+
```rb
|
380
|
+
user.assign_roles(:owner, context: project)
|
381
|
+
another_user.assign_roles(:member, context: project)
|
382
|
+
```
|
383
|
+
|
384
|
+
Then the roles can be verified:
|
385
|
+
|
386
|
+
```rb
|
387
|
+
user.has_role?(:owner, context: project)
|
388
|
+
another_user.has_role?(:member, context: project)
|
389
|
+
```
|
390
|
+
|
391
|
+
A role can also be added using a class as a context, e.g., for project admins who can manage all projects:
|
392
|
+
|
393
|
+
```rb
|
394
|
+
user.assign_roles(:admin, context: Project)
|
395
|
+
```
|
396
|
+
|
397
|
+
And then it can also be verified:
|
398
|
+
|
399
|
+
```rb
|
400
|
+
user.has_role?(:admin, context: Project)
|
401
|
+
```
|
402
|
+
|
403
|
+
In authorization rules, the context can be used in the same way, but it also can be a proc or a symbol (similar to dynamic rules):
|
404
|
+
|
405
|
+
```rb
|
406
|
+
class ProjectsController < ApplicationController
|
407
|
+
grant_access roles: :admin, context: Project
|
408
|
+
|
409
|
+
grant_access action: :show, roles: :member, context: :project
|
410
|
+
def show
|
411
|
+
# ...
|
412
|
+
end
|
413
|
+
|
414
|
+
grant_access action: :update, roles: :owner, context: -> { Project.find(params[:id]) }
|
415
|
+
def update
|
416
|
+
# ...
|
417
|
+
end
|
418
|
+
|
419
|
+
private
|
420
|
+
|
421
|
+
def project
|
422
|
+
Project.find(params[:id])
|
423
|
+
end
|
424
|
+
end
|
425
|
+
```
|
426
|
+
|
427
|
+
It's important to note that role names are not unique globally but are unique within the scope of their context. E.g., `user.assign_roles(:admin, context: Project)` and `user.assign_roles(:admin)` assign different roles to the user. The same as `Rabarber::Role.add(:admin, context: Project)` and `Rabarber::Role.add(:admin)` create different roles.
|
428
|
+
|
429
|
+
If you want to see all the roles assigned to a user within a specific context, you can use:
|
430
|
+
|
431
|
+
```rb
|
432
|
+
user.roles(context: project)
|
433
|
+
```
|
434
|
+
|
435
|
+
Or if you want to get all the roles available in a specific context, you can use:
|
436
|
+
|
437
|
+
```rb
|
438
|
+
Rabarber::Role.names(context: Project)
|
439
|
+
```
|
440
|
+
|
373
441
|
## When Unauthorized
|
374
442
|
|
375
443
|
By default, in the event of an unauthorized attempt, Rabarber redirects the user back if the request format is HTML (with fallback to the root path), and returns a 401 (Unauthorized) status code otherwise.
|
@@ -392,6 +460,19 @@ end
|
|
392
460
|
|
393
461
|
The method can be overridden in different controllers, providing flexibility in handling unauthorized access attempts.
|
394
462
|
|
463
|
+
## Skip Authorization
|
464
|
+
|
465
|
+
To skip authorization, use `.skip_authorization(options = {})` method:
|
466
|
+
|
467
|
+
```rb
|
468
|
+
class TicketsController < ApplicationController
|
469
|
+
skip_authorization only: :index
|
470
|
+
# ...
|
471
|
+
end
|
472
|
+
```
|
473
|
+
|
474
|
+
This method accepts the same options as `skip_before_action` method in Rails.
|
475
|
+
|
395
476
|
## View Helpers
|
396
477
|
|
397
478
|
Rabarber also provides a couple of helpers that can be used in views: `visible_to(*roles, &block)` and `hidden_from(*roles, &block)`. To use them, simply include `Rabarber::Helpers` in the desired helper. Usually it is `ApplicationHelper`, but it can be any helper of your choice.
|
@@ -3,10 +3,13 @@
|
|
3
3
|
class CreateRabarberRoles < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version.to_s %>]
|
4
4
|
def change
|
5
5
|
create_table :rabarber_roles<%= ", id: :uuid" if options[:uuid] %> do |t|
|
6
|
-
t.string :name, null: false
|
6
|
+
t.string :name, null: false
|
7
|
+
t.belongs_to :context, polymorphic: true, index: true<%= ", type: :uuid" if options[:uuid] %>
|
7
8
|
t.timestamps
|
8
9
|
end
|
9
10
|
|
11
|
+
add_index :rabarber_roles, [:name, :context_type, :context_id], unique: true
|
12
|
+
|
10
13
|
create_table :rabarber_roles_roleables, id: false do |t|
|
11
14
|
t.belongs_to :role, null: false, index: true, foreign_key: { to_table: :rabarber_roles }<%= ", type: :uuid" if options[:uuid] %>
|
12
15
|
t.belongs_to :roleable, null: false, index: true, foreign_key: { to_table: <%= table_name.to_sym.inspect %> }<%= ", type: :uuid" if options[:uuid] %>
|
@@ -38,24 +38,22 @@ module Rabarber
|
|
38
38
|
end
|
39
39
|
|
40
40
|
def identity
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
def identity_with_roles?
|
45
|
-
raise NotImplementedError
|
46
|
-
end
|
47
|
-
|
48
|
-
def roleable_identity(with_roles:)
|
49
|
-
if roleable
|
41
|
+
if roleable.is_a?(Rabarber::Core::NullRoleable)
|
42
|
+
"Unauthenticated #{Rabarber::HasRoles.roleable_class.model_name.human.downcase}"
|
43
|
+
else
|
50
44
|
model_name = roleable.model_name.human
|
51
|
-
|
52
|
-
roleable_id = roleable.public_send(primary_key)
|
45
|
+
roleable_id = roleable.public_send(roleable.class.primary_key)
|
53
46
|
|
54
|
-
|
47
|
+
"#{model_name}##{roleable_id}"
|
48
|
+
end
|
49
|
+
end
|
55
50
|
|
56
|
-
|
57
|
-
|
58
|
-
|
51
|
+
def human_context
|
52
|
+
case context
|
53
|
+
in { context_type: nil, context_id: nil } then "Global"
|
54
|
+
in { context_type: context_type, context_id: nil } then context_type
|
55
|
+
in { context_type: context_type, context_id: context_id } then "#{context_type}##{context_id}"
|
56
|
+
else raise "Unexpected context: #{context}"
|
59
57
|
end
|
60
58
|
end
|
61
59
|
end
|
@@ -15,11 +15,11 @@ module Rabarber
|
|
15
15
|
end
|
16
16
|
|
17
17
|
def message
|
18
|
-
"[Role Assignment] #{identity}
|
18
|
+
"[Role Assignment] #{identity} | context: #{human_context} | assigned: #{roles_to_assign} | current: #{current_roles}"
|
19
19
|
end
|
20
20
|
|
21
|
-
def
|
22
|
-
|
21
|
+
def context
|
22
|
+
specifics.fetch(:context)
|
23
23
|
end
|
24
24
|
|
25
25
|
def roles_to_assign
|
@@ -15,11 +15,11 @@ module Rabarber
|
|
15
15
|
end
|
16
16
|
|
17
17
|
def message
|
18
|
-
"[Role Revocation] #{identity}
|
18
|
+
"[Role Revocation] #{identity} | context: #{human_context} | revoked: #{roles_to_revoke} | current: #{current_roles}"
|
19
19
|
end
|
20
20
|
|
21
|
-
def
|
22
|
-
|
21
|
+
def context
|
22
|
+
specifics.fetch(:context)
|
23
23
|
end
|
24
24
|
|
25
25
|
def roles_to_revoke
|
@@ -15,16 +15,16 @@ module Rabarber
|
|
15
15
|
end
|
16
16
|
|
17
17
|
def message
|
18
|
-
"[Unauthorized Attempt] #{identity}
|
19
|
-
end
|
20
|
-
|
21
|
-
def identity_with_roles?
|
22
|
-
true
|
18
|
+
"[Unauthorized Attempt] #{identity} | request: #{request_method} #{path}"
|
23
19
|
end
|
24
20
|
|
25
21
|
def path
|
26
22
|
specifics.fetch(:path)
|
27
23
|
end
|
24
|
+
|
25
|
+
def request_method
|
26
|
+
specifics.fetch(:request_method)
|
27
|
+
end
|
28
28
|
end
|
29
29
|
end
|
30
30
|
end
|
@@ -11,13 +11,18 @@ module Rabarber
|
|
11
11
|
end
|
12
12
|
|
13
13
|
class_methods do
|
14
|
-
def
|
14
|
+
def skip_authorization(options = {})
|
15
|
+
skip_before_action :verify_access, **options
|
16
|
+
end
|
17
|
+
|
18
|
+
def grant_access(action: nil, roles: nil, context: nil, if: nil, unless: nil)
|
15
19
|
dynamic_rule, negated_dynamic_rule = binding.local_variable_get(:if), binding.local_variable_get(:unless)
|
16
20
|
|
17
21
|
Rabarber::Core::Permissions.add(
|
18
22
|
self,
|
19
23
|
Rabarber::Input::Action.new(action).process,
|
20
24
|
Rabarber::Input::Roles.new(roles).process,
|
25
|
+
Rabarber::Input::AuthorizationContext.new(context).process,
|
21
26
|
Rabarber::Input::DynamicRule.new(dynamic_rule).process,
|
22
27
|
Rabarber::Input::DynamicRule.new(negated_dynamic_rule).process
|
23
28
|
)
|
@@ -29,9 +34,11 @@ module Rabarber
|
|
29
34
|
def verify_access
|
30
35
|
Rabarber::Core::PermissionsIntegrityChecker.new(self.class).run! unless Rails.configuration.eager_load
|
31
36
|
|
32
|
-
return if Rabarber::Core::Permissions.access_granted?(
|
37
|
+
return if Rabarber::Core::Permissions.access_granted?(roleable, action_name.to_sym, self)
|
33
38
|
|
34
|
-
Rabarber::Audit::Events::UnauthorizedAttempt.trigger(
|
39
|
+
Rabarber::Audit::Events::UnauthorizedAttempt.trigger(
|
40
|
+
roleable, path: request.path, request_method: request.request_method
|
41
|
+
)
|
35
42
|
|
36
43
|
when_unauthorized
|
37
44
|
end
|
data/lib/rabarber/core/access.rb
CHANGED
@@ -3,20 +3,19 @@
|
|
3
3
|
module Rabarber
|
4
4
|
module Core
|
5
5
|
module Access
|
6
|
-
def access_granted?(
|
7
|
-
controller_accessible?(
|
8
|
-
action_accessible?(roles, controller, action, dynamic_rule_receiver)
|
6
|
+
def access_granted?(roleable, action, controller_instance)
|
7
|
+
controller_accessible?(roleable, controller_instance) || action_accessible?(roleable, action, controller_instance)
|
9
8
|
end
|
10
9
|
|
11
|
-
def controller_accessible?(
|
10
|
+
def controller_accessible?(roleable, controller_instance)
|
12
11
|
controller_rules.any? do |rule_controller, rule|
|
13
|
-
|
12
|
+
controller_instance.class <= rule_controller && rule.verify_access(roleable, controller_instance)
|
14
13
|
end
|
15
14
|
end
|
16
15
|
|
17
|
-
def action_accessible?(
|
18
|
-
action_rules[
|
19
|
-
rule.action == action && rule.verify_access(
|
16
|
+
def action_accessible?(roleable, action, controller_instance)
|
17
|
+
action_rules[controller_instance.class].any? do |rule|
|
18
|
+
rule.action == action && rule.verify_access(roleable, controller_instance)
|
20
19
|
end
|
21
20
|
end
|
22
21
|
end
|
data/lib/rabarber/core/cache.rb
CHANGED
@@ -1,19 +1,25 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "digest/sha2"
|
4
|
+
|
3
5
|
module Rabarber
|
4
6
|
module Core
|
5
7
|
module Cache
|
6
|
-
|
8
|
+
module_function
|
7
9
|
|
8
10
|
CACHE_PREFIX = "rabarber"
|
9
11
|
private_constant :CACHE_PREFIX
|
10
12
|
|
11
|
-
def fetch(roleable_id,
|
12
|
-
enabled?
|
13
|
+
def fetch(roleable_id, context:, &block)
|
14
|
+
if enabled?
|
15
|
+
Rails.cache.fetch(key_for(roleable_id, context), expires_in: 1.hour, race_condition_ttl: 5.seconds, &block)
|
16
|
+
else
|
17
|
+
yield
|
18
|
+
end
|
13
19
|
end
|
14
20
|
|
15
|
-
def delete(*roleable_ids)
|
16
|
-
keys = roleable_ids.map { |roleable_id| key_for(roleable_id) }
|
21
|
+
def delete(*roleable_ids, context:)
|
22
|
+
keys = roleable_ids.map { |roleable_id| key_for(roleable_id, context) }
|
17
23
|
Rails.cache.delete_multi(keys) if enabled? && keys.any?
|
18
24
|
end
|
19
25
|
|
@@ -25,10 +31,8 @@ module Rabarber
|
|
25
31
|
Rails.cache.delete_matched(/^#{CACHE_PREFIX}/o)
|
26
32
|
end
|
27
33
|
|
28
|
-
|
29
|
-
|
30
|
-
def key_for(id)
|
31
|
-
"#{CACHE_PREFIX}:roles_#{id}"
|
34
|
+
def key_for(id, context)
|
35
|
+
"#{CACHE_PREFIX}:#{Digest::SHA2.hexdigest("#{id}#{context}")}"
|
32
36
|
end
|
33
37
|
end
|
34
38
|
end
|
@@ -19,8 +19,8 @@ module Rabarber
|
|
19
19
|
end
|
20
20
|
|
21
21
|
class << self
|
22
|
-
def add(controller, action, roles, dynamic_rule, negated_dynamic_rule)
|
23
|
-
rule = Rabarber::Core::Rule.new(action, roles, dynamic_rule, negated_dynamic_rule)
|
22
|
+
def add(controller, action, roles, context, dynamic_rule, negated_dynamic_rule)
|
23
|
+
rule = Rabarber::Core::Rule.new(action, roles, context, dynamic_rule, negated_dynamic_rule)
|
24
24
|
|
25
25
|
if action
|
26
26
|
instance.storage[:action_rules][controller] += [rule]
|
@@ -4,11 +4,17 @@ module Rabarber
|
|
4
4
|
module Core
|
5
5
|
module Roleable
|
6
6
|
def roleable
|
7
|
-
send(Rabarber::Configuration.instance.current_user_method)
|
7
|
+
send(Rabarber::Configuration.instance.current_user_method) || NullRoleable.new
|
8
8
|
end
|
9
9
|
|
10
|
-
def roleable_roles
|
11
|
-
roleable
|
10
|
+
def roleable_roles(context: nil)
|
11
|
+
roleable.roles(context: context)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class NullRoleable
|
16
|
+
def roles(context:) # rubocop:disable Lint/UnusedMethodArgument
|
17
|
+
[]
|
12
18
|
end
|
13
19
|
end
|
14
20
|
end
|
data/lib/rabarber/core/rule.rb
CHANGED
@@ -3,44 +3,56 @@
|
|
3
3
|
module Rabarber
|
4
4
|
module Core
|
5
5
|
class Rule
|
6
|
-
attr_reader :action, :roles, :dynamic_rule, :negated_dynamic_rule
|
6
|
+
attr_reader :action, :roles, :context, :dynamic_rule, :negated_dynamic_rule
|
7
7
|
|
8
|
-
def initialize(action, roles, dynamic_rule, negated_dynamic_rule)
|
8
|
+
def initialize(action, roles, context, dynamic_rule, negated_dynamic_rule)
|
9
9
|
@action = action
|
10
10
|
@roles = Array(roles)
|
11
|
+
@context = context
|
11
12
|
@dynamic_rule = dynamic_rule
|
12
13
|
@negated_dynamic_rule = negated_dynamic_rule
|
13
14
|
end
|
14
15
|
|
15
|
-
def verify_access(
|
16
|
-
roles_permitted?(
|
16
|
+
def verify_access(roleable, controller_instance)
|
17
|
+
roles_permitted?(roleable, controller_instance) && dynamic_rule_followed?(controller_instance)
|
17
18
|
end
|
18
19
|
|
19
|
-
def roles_permitted?(
|
20
|
+
def roles_permitted?(roleable, controller_instance)
|
21
|
+
processed_context = get_context(controller_instance)
|
22
|
+
roleable_roles = roleable.roles(context: processed_context)
|
23
|
+
|
20
24
|
return false if Rabarber::Configuration.instance.must_have_roles && roleable_roles.empty?
|
21
25
|
|
22
26
|
roles.empty? || roles.intersection(roleable_roles).any?
|
23
27
|
end
|
24
28
|
|
25
|
-
def dynamic_rule_followed?(
|
26
|
-
!!(execute_dynamic_rule(
|
29
|
+
def dynamic_rule_followed?(controller_instance)
|
30
|
+
!!(execute_dynamic_rule(controller_instance, false) && execute_dynamic_rule(controller_instance, true))
|
27
31
|
end
|
28
32
|
|
29
33
|
private
|
30
34
|
|
31
|
-
def execute_dynamic_rule(
|
35
|
+
def execute_dynamic_rule(controller_instance, is_negated)
|
32
36
|
rule = is_negated ? negated_dynamic_rule : dynamic_rule
|
33
37
|
|
34
38
|
return true if rule.nil?
|
35
39
|
|
36
40
|
result = if rule.is_a?(Proc)
|
37
|
-
|
41
|
+
controller_instance.instance_exec(&rule)
|
38
42
|
else
|
39
|
-
|
43
|
+
controller_instance.send(rule)
|
40
44
|
end
|
41
45
|
|
42
46
|
is_negated ? !result : result
|
43
47
|
end
|
48
|
+
|
49
|
+
def get_context(controller_instance)
|
50
|
+
case context
|
51
|
+
when Proc then Rabarber::Input::Context.new(controller_instance.instance_exec(&context)).process
|
52
|
+
when Symbol then Rabarber::Input::Context.new(controller_instance.send(context)).process
|
53
|
+
else context
|
54
|
+
end
|
55
|
+
end
|
44
56
|
end
|
45
57
|
end
|
46
58
|
end
|
@@ -4,14 +4,14 @@ module Rabarber
|
|
4
4
|
module Helpers
|
5
5
|
include Rabarber::Core::Roleable
|
6
6
|
|
7
|
-
def visible_to(*roles, &block)
|
8
|
-
return
|
7
|
+
def visible_to(*roles, context: nil, &block)
|
8
|
+
return if roleable_roles(context: Rabarber::Input::Context.new(context).process).intersection(Rabarber::Input::Roles.new(roles).process).none?
|
9
9
|
|
10
10
|
capture(&block)
|
11
11
|
end
|
12
12
|
|
13
|
-
def hidden_from(*roles, &block)
|
14
|
-
return if roleable_roles.intersection(Rabarber::Input::Roles.new(roles).process).any?
|
13
|
+
def hidden_from(*roles, context: nil, &block)
|
14
|
+
return if roleable_roles(context: Rabarber::Input::Context.new(context).process).intersection(Rabarber::Input::Roles.new(roles).process).any?
|
15
15
|
|
16
16
|
capture(&block)
|
17
17
|
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rabarber
|
4
|
+
module Input
|
5
|
+
class AuthorizationContext < Rabarber::Input::Base
|
6
|
+
def valid?
|
7
|
+
Rabarber::Input::Context.new(value).valid? || Rabarber::Input::DynamicRule.new(value).valid?
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def processed_value
|
13
|
+
case value
|
14
|
+
when Symbol, String then value.to_sym
|
15
|
+
when Proc then value
|
16
|
+
else Rabarber::Input::Context.new(value).process
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def default_error_message
|
21
|
+
"Context must be a Class, an instance of ActiveRecord model, a Symbol, a String, or a Proc"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rabarber
|
4
|
+
module Input
|
5
|
+
class Context < Rabarber::Input::Base
|
6
|
+
def valid?
|
7
|
+
value.nil? || value.is_a?(Class) || value.is_a?(ActiveRecord::Base) && value.persisted? || already_processed?
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def processed_value
|
13
|
+
case value
|
14
|
+
when nil then { context_type: nil, context_id: nil }
|
15
|
+
when Class then { context_type: value.to_s, context_id: nil }
|
16
|
+
when ActiveRecord::Base then { context_type: value.class.to_s, context_id: value.public_send(value.class.primary_key) }
|
17
|
+
else value
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def default_error_message
|
22
|
+
"Context must be a Class or an instance of ActiveRecord model"
|
23
|
+
end
|
24
|
+
|
25
|
+
def already_processed?
|
26
|
+
case value
|
27
|
+
in { context_type: NilClass | String, context_id: NilClass | String | Integer } then true
|
28
|
+
else false
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -14,43 +14,62 @@ module Rabarber
|
|
14
14
|
join_table: "rabarber_roles_roleables"
|
15
15
|
end
|
16
16
|
|
17
|
-
def roles
|
18
|
-
|
17
|
+
def roles(context: nil)
|
18
|
+
processed_context = process_context(context)
|
19
|
+
Rabarber::Core::Cache.fetch(roleable_id, context: processed_context) { rabarber_roles.names(context: processed_context) }
|
19
20
|
end
|
20
21
|
|
21
|
-
def has_role?(*role_names)
|
22
|
-
|
22
|
+
def has_role?(*role_names, context: nil)
|
23
|
+
processed_context = process_context(context)
|
24
|
+
roles(context: processed_context).intersection(process_role_names(role_names)).any?
|
23
25
|
end
|
24
26
|
|
25
|
-
def assign_roles(*role_names, create_new: true)
|
27
|
+
def assign_roles(*role_names, context: nil, create_new: true)
|
26
28
|
processed_role_names = process_role_names(role_names)
|
29
|
+
processed_context = process_context(context)
|
27
30
|
|
28
|
-
create_new_roles(processed_role_names) if create_new
|
31
|
+
create_new_roles(processed_role_names, context: processed_context) if create_new
|
29
32
|
|
30
|
-
roles_to_assign = Rabarber::Role.where(
|
33
|
+
roles_to_assign = Rabarber::Role.where(
|
34
|
+
name: (processed_role_names - rabarber_roles.names(context: processed_context)), **processed_context
|
35
|
+
)
|
31
36
|
|
32
37
|
if roles_to_assign.any?
|
33
|
-
delete_roleable_cache
|
38
|
+
delete_roleable_cache(context: processed_context)
|
34
39
|
rabarber_roles << roles_to_assign
|
35
40
|
|
36
|
-
Rabarber::Audit::Events::RolesAssigned.trigger(
|
41
|
+
Rabarber::Audit::Events::RolesAssigned.trigger(
|
42
|
+
self,
|
43
|
+
roles_to_assign: roles_to_assign.names(context: processed_context),
|
44
|
+
current_roles: roles(context: processed_context),
|
45
|
+
context: processed_context
|
46
|
+
)
|
37
47
|
end
|
38
48
|
|
39
|
-
roles
|
49
|
+
roles(context: processed_context)
|
40
50
|
end
|
41
51
|
|
42
|
-
def revoke_roles(*role_names)
|
52
|
+
def revoke_roles(*role_names, context: nil)
|
43
53
|
processed_role_names = process_role_names(role_names)
|
44
|
-
|
54
|
+
processed_context = process_context(context)
|
55
|
+
|
56
|
+
roles_to_revoke = Rabarber::Role.where(
|
57
|
+
name: processed_role_names.intersection(roles(context: processed_context)), **processed_context
|
58
|
+
)
|
45
59
|
|
46
60
|
if roles_to_revoke.any?
|
47
|
-
delete_roleable_cache
|
61
|
+
delete_roleable_cache(context: processed_context)
|
48
62
|
self.rabarber_roles -= roles_to_revoke
|
49
63
|
|
50
|
-
Rabarber::Audit::Events::RolesRevoked.trigger(
|
64
|
+
Rabarber::Audit::Events::RolesRevoked.trigger(
|
65
|
+
self,
|
66
|
+
roles_to_revoke: roles_to_revoke.names(context: processed_context),
|
67
|
+
current_roles: roles(context: processed_context),
|
68
|
+
context: processed_context
|
69
|
+
)
|
51
70
|
end
|
52
71
|
|
53
|
-
roles
|
72
|
+
roles(context: processed_context)
|
54
73
|
end
|
55
74
|
|
56
75
|
def roleable_class
|
@@ -60,17 +79,21 @@ module Rabarber
|
|
60
79
|
|
61
80
|
private
|
62
81
|
|
63
|
-
def create_new_roles(role_names)
|
64
|
-
new_roles = role_names - Rabarber::Role.names
|
65
|
-
new_roles.each { |role_name| Rabarber::Role.create!(name: role_name) }
|
82
|
+
def create_new_roles(role_names, context:)
|
83
|
+
new_roles = role_names - Rabarber::Role.names(context: context)
|
84
|
+
new_roles.each { |role_name| Rabarber::Role.create!(name: role_name, **context) }
|
66
85
|
end
|
67
86
|
|
68
87
|
def process_role_names(role_names)
|
69
88
|
Rabarber::Input::Roles.new(role_names).process
|
70
89
|
end
|
71
90
|
|
72
|
-
def
|
73
|
-
Rabarber::
|
91
|
+
def process_context(context)
|
92
|
+
Rabarber::Input::Context.new(context).process
|
93
|
+
end
|
94
|
+
|
95
|
+
def delete_roleable_cache(context:)
|
96
|
+
Rabarber::Core::Cache.delete(roleable_id, context: context)
|
74
97
|
end
|
75
98
|
|
76
99
|
def roleable_id
|
data/lib/rabarber/models/role.rb
CHANGED
@@ -4,54 +4,60 @@ module Rabarber
|
|
4
4
|
class Role < ActiveRecord::Base
|
5
5
|
self.table_name = "rabarber_roles"
|
6
6
|
|
7
|
-
validates :name, presence: true,
|
7
|
+
validates :name, presence: true,
|
8
|
+
uniqueness: { scope: [:context_type, :context_id] },
|
9
|
+
format: { with: Rabarber::Input::Role::REGEX },
|
10
|
+
strict: true
|
8
11
|
|
9
12
|
has_and_belongs_to_many :roleables, join_table: "rabarber_roles_roleables"
|
10
13
|
|
11
14
|
class << self
|
12
|
-
def names
|
13
|
-
pluck(:name).map(&:to_sym)
|
15
|
+
def names(context: nil)
|
16
|
+
where(Rabarber::Input::Context.new(context).process).pluck(:name).map(&:to_sym)
|
14
17
|
end
|
15
18
|
|
16
|
-
def add(name)
|
19
|
+
def add(name, context: nil)
|
17
20
|
name = process_role_name(name)
|
21
|
+
processed_context = process_context(context)
|
18
22
|
|
19
|
-
return false if exists?(name: name)
|
23
|
+
return false if exists?(name: name, **processed_context)
|
20
24
|
|
21
|
-
!!create!(name: name)
|
25
|
+
!!create!(name: name, **processed_context)
|
22
26
|
end
|
23
27
|
|
24
|
-
def rename(old_name, new_name, force: false)
|
25
|
-
|
28
|
+
def rename(old_name, new_name, context: nil, force: false)
|
29
|
+
processed_context = process_context(context)
|
30
|
+
role = find_by(name: process_role_name(old_name), **processed_context)
|
26
31
|
name = process_role_name(new_name)
|
27
32
|
|
28
|
-
return false if !role || exists?(name: name) || assigned_to_roleables(role).any? && !force
|
33
|
+
return false if !role || exists?(name: name, **processed_context) || assigned_to_roleables(role).any? && !force
|
29
34
|
|
30
|
-
delete_roleables_cache(role)
|
35
|
+
delete_roleables_cache(role, context: processed_context)
|
31
36
|
|
32
37
|
role.update!(name: name)
|
33
38
|
end
|
34
39
|
|
35
|
-
def remove(name, force: false)
|
36
|
-
|
40
|
+
def remove(name, context: nil, force: false)
|
41
|
+
processed_context = process_context(context)
|
42
|
+
role = find_by(name: process_role_name(name), **processed_context)
|
37
43
|
|
38
44
|
return false if !role || assigned_to_roleables(role).any? && !force
|
39
45
|
|
40
|
-
delete_roleables_cache(role)
|
46
|
+
delete_roleables_cache(role, context: processed_context)
|
41
47
|
|
42
48
|
!!role.destroy!
|
43
49
|
end
|
44
50
|
|
45
|
-
def assignees(name)
|
51
|
+
def assignees(name, context: nil)
|
46
52
|
Rabarber::HasRoles.roleable_class.joins(:rabarber_roles).where(
|
47
|
-
rabarber_roles: { name: Rabarber::Input::Role.new(name).process }
|
53
|
+
rabarber_roles: { name: Rabarber::Input::Role.new(name).process, **process_context(context) }
|
48
54
|
)
|
49
55
|
end
|
50
56
|
|
51
57
|
private
|
52
58
|
|
53
|
-
def delete_roleables_cache(role)
|
54
|
-
Rabarber::Core::Cache.delete(*assigned_to_roleables(role))
|
59
|
+
def delete_roleables_cache(role, context:)
|
60
|
+
Rabarber::Core::Cache.delete(*assigned_to_roleables(role), context: context)
|
55
61
|
end
|
56
62
|
|
57
63
|
def assigned_to_roleables(role)
|
@@ -65,6 +71,10 @@ module Rabarber
|
|
65
71
|
def process_role_name(name)
|
66
72
|
Rabarber::Input::Role.new(name).process
|
67
73
|
end
|
74
|
+
|
75
|
+
def process_context(context)
|
76
|
+
Rabarber::Input::Context.new(context).process
|
77
|
+
end
|
68
78
|
end
|
69
79
|
end
|
70
80
|
end
|
data/lib/rabarber/version.rb
CHANGED
data/lib/rabarber.rb
CHANGED
@@ -8,6 +8,8 @@ require "active_support"
|
|
8
8
|
|
9
9
|
require_relative "rabarber/input/base"
|
10
10
|
require_relative "rabarber/input/action"
|
11
|
+
require_relative "rabarber/input/authorization_context"
|
12
|
+
require_relative "rabarber/input/context"
|
11
13
|
require_relative "rabarber/input/dynamic_rule"
|
12
14
|
require_relative "rabarber/input/role"
|
13
15
|
require_relative "rabarber/input/roles"
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rabarber
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 3.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- enjaku4
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2024-
|
12
|
+
date: 2024-07-07 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rails
|
@@ -59,7 +59,9 @@ files:
|
|
59
59
|
- lib/rabarber/core/rule.rb
|
60
60
|
- lib/rabarber/helpers/helpers.rb
|
61
61
|
- lib/rabarber/input/action.rb
|
62
|
+
- lib/rabarber/input/authorization_context.rb
|
62
63
|
- lib/rabarber/input/base.rb
|
64
|
+
- lib/rabarber/input/context.rb
|
63
65
|
- lib/rabarber/input/dynamic_rule.rb
|
64
66
|
- lib/rabarber/input/role.rb
|
65
67
|
- lib/rabarber/input/roles.rb
|