rabarber 2.1.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 +17 -1
- data/README.md +84 -13
- 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 +6 -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,19 @@
|
|
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
|
+
|
1
17
|
## v2.1.0
|
2
18
|
|
3
19
|
### Features:
|
@@ -12,7 +28,7 @@
|
|
12
28
|
- Replaced `when_unauthorized` configuration option with an overridable controller method
|
13
29
|
- Renamed `Rabarber::Role.assignees_for` method to `Rabarber::Role.assignees`
|
14
30
|
|
15
|
-
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)
|
16
32
|
|
17
33
|
### Features:
|
18
34
|
|
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,6 +39,7 @@ 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)
|
43
44
|
- [Skip Authorization](#skip-authorization)
|
44
45
|
- [View Helpers](#view-helpers)
|
@@ -115,7 +116,7 @@ end
|
|
115
116
|
|
116
117
|
This adds the following methods:
|
117
118
|
|
118
|
-
**`#assign_roles(*roles, create_new: true)`**
|
119
|
+
**`#assign_roles(*roles, context: nil, create_new: true)`**
|
119
120
|
|
120
121
|
To assign roles, use:
|
121
122
|
|
@@ -128,7 +129,7 @@ user.assign_roles(:accountant, :marketer, create_new: false)
|
|
128
129
|
```
|
129
130
|
The method returns an array of roles assigned to the user.
|
130
131
|
|
131
|
-
**`#revoke_roles(*roles)`**
|
132
|
+
**`#revoke_roles(*roles, context: nil)`**
|
132
133
|
|
133
134
|
To revoke roles, use:
|
134
135
|
|
@@ -139,7 +140,7 @@ If the user doesn't have the role you want to revoke, it will be ignored.
|
|
139
140
|
|
140
141
|
The method returns an array of roles assigned to the user.
|
141
142
|
|
142
|
-
**`#has_role?(*roles)`**
|
143
|
+
**`#has_role?(*roles, context: nil)`**
|
143
144
|
|
144
145
|
To check whether the user has a role, use:
|
145
146
|
|
@@ -149,9 +150,9 @@ user.has_role?(:accountant, :marketer)
|
|
149
150
|
|
150
151
|
It returns `true` if the user has at least one role and `false` otherwise.
|
151
152
|
|
152
|
-
**`#roles`**
|
153
|
+
**`#roles(context: nil)`**
|
153
154
|
|
154
|
-
To get
|
155
|
+
To get the list of roles assigned to the user, use:
|
155
156
|
|
156
157
|
```rb
|
157
158
|
user.roles
|
@@ -161,7 +162,7 @@ user.roles
|
|
161
162
|
|
162
163
|
To manipulate roles directly, you can use `Rabarber::Role` methods:
|
163
164
|
|
164
|
-
**`.add(role_name)`**
|
165
|
+
**`.add(role_name, context: nil)`**
|
165
166
|
|
166
167
|
To add a new role, use:
|
167
168
|
|
@@ -171,7 +172,7 @@ Rabarber::Role.add(:admin)
|
|
171
172
|
|
172
173
|
This will create a new role with the specified name and return `true`. If the role already exists, it will return `false`.
|
173
174
|
|
174
|
-
**`.rename(old_role_name, new_role_name, force: false)`**
|
175
|
+
**`.rename(old_role_name, new_role_name, context: nil, force: false)`**
|
175
176
|
|
176
177
|
To rename a role, use:
|
177
178
|
|
@@ -185,7 +186,7 @@ The method won't rename the role and will return `false` if it is assigned to an
|
|
185
186
|
Rabarber::Role.rename(:admin, :administrator, force: true)
|
186
187
|
```
|
187
188
|
|
188
|
-
**`.remove(role_name, force: false)`**
|
189
|
+
**`.remove(role_name, context: nil, force: false)`**
|
189
190
|
|
190
191
|
To remove a role, use:
|
191
192
|
|
@@ -200,15 +201,15 @@ The method won't remove the role and will return `false` if it is assigned to an
|
|
200
201
|
Rabarber::Role.remove(:admin, force: true)
|
201
202
|
```
|
202
203
|
|
203
|
-
**`.names`**
|
204
|
+
**`.names(context: nil)`**
|
204
205
|
|
205
|
-
If you need to list
|
206
|
+
If you need to list the role names available in your application, use:
|
206
207
|
|
207
208
|
```rb
|
208
209
|
Rabarber::Role.names
|
209
210
|
```
|
210
211
|
|
211
|
-
**`.assignees(role_name)`**
|
212
|
+
**`.assignees(role_name, context: nil)`**
|
212
213
|
|
213
214
|
To get all the users to whom the role is assigned, use:
|
214
215
|
|
@@ -226,7 +227,7 @@ class ApplicationController < ActionController::Base
|
|
226
227
|
# ...
|
227
228
|
end
|
228
229
|
```
|
229
|
-
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.
|
230
231
|
|
231
232
|
The most basic usage of the method is as follows:
|
232
233
|
|
@@ -367,6 +368,76 @@ class InvoicesController < ApplicationController
|
|
367
368
|
end
|
368
369
|
```
|
369
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
|
+
|
370
441
|
## When Unauthorized
|
371
442
|
|
372
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.
|
@@ -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
|
@@ -15,13 +15,14 @@ module Rabarber
|
|
15
15
|
skip_before_action :verify_access, **options
|
16
16
|
end
|
17
17
|
|
18
|
-
def grant_access(action: nil, roles: nil, if: nil, unless: nil)
|
18
|
+
def grant_access(action: nil, roles: nil, context: nil, if: nil, unless: nil)
|
19
19
|
dynamic_rule, negated_dynamic_rule = binding.local_variable_get(:if), binding.local_variable_get(:unless)
|
20
20
|
|
21
21
|
Rabarber::Core::Permissions.add(
|
22
22
|
self,
|
23
23
|
Rabarber::Input::Action.new(action).process,
|
24
24
|
Rabarber::Input::Roles.new(roles).process,
|
25
|
+
Rabarber::Input::AuthorizationContext.new(context).process,
|
25
26
|
Rabarber::Input::DynamicRule.new(dynamic_rule).process,
|
26
27
|
Rabarber::Input::DynamicRule.new(negated_dynamic_rule).process
|
27
28
|
)
|
@@ -33,9 +34,11 @@ module Rabarber
|
|
33
34
|
def verify_access
|
34
35
|
Rabarber::Core::PermissionsIntegrityChecker.new(self.class).run! unless Rails.configuration.eager_load
|
35
36
|
|
36
|
-
return if Rabarber::Core::Permissions.access_granted?(
|
37
|
+
return if Rabarber::Core::Permissions.access_granted?(roleable, action_name.to_sym, self)
|
37
38
|
|
38
|
-
Rabarber::Audit::Events::UnauthorizedAttempt.trigger(
|
39
|
+
Rabarber::Audit::Events::UnauthorizedAttempt.trigger(
|
40
|
+
roleable, path: request.path, request_method: request.request_method
|
41
|
+
)
|
39
42
|
|
40
43
|
when_unauthorized
|
41
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
|