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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f719eb670170521e94a58a1c75d30110699ab941da6d5dae151fbf53e0eb9880
4
- data.tar.gz: bd06646e15ab2eb5ff7b49d2a18316c4b539b95aa98bd2ec1b4de5c6b5fb921a
3
+ metadata.gz: 51c1cba47b8af312f38ff403b77ef716099101d5bed3226c3e7d2bad37c23874
4
+ data.tar.gz: 3da6569ceef5c624aa649240341fcb72ce879005e458044006ba547aadbe316e
5
5
  SHA512:
6
- metadata.gz: 0db86c7d46337decbe9972ea3b0c1ad30e1c66f2b76b3ef27a494ef8c2ecc9bcf35ce3f135360e74045b7f20beb38149f5ca308af0cb3d3a0db829ff9dc7147b
7
- data.tar.gz: d1a20c732318d12ccfe49338df7338dcf09bd1222fd574e8581d035cb38b55295e501bfa45ef63ea0a556a8c21a28abdad7d06ca765c3f14eed6e6cd98db93be
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
  [![Gem Version](https://badge.fury.io/rb/rabarber.svg)](http://badge.fury.io/rb/rabarber)
4
4
  [![Github Actions badge](https://github.com/enjaku4/rabarber/actions/workflows/ci.yml/badge.svg)](https://github.com/enjaku4/rabarber/actions/workflows/ci.yml)
5
5
 
6
- Rabarber is a role-based authorization library for Ruby on Rails, primarily designed for use in the web layer of your application but not limited to that. It provides a set of tools for managing user roles and defining authorization rules, along with audit logging for enhanced security.
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 all the roles assigned to the user, use:
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 all the role names available in your application, use:
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`. This extends to scenarios where there is no user present, i.e. when the method responsible for returning the currently authenticated user in your application returns `nil`.
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 (or unauthenticated users) are not allowed to access anything.
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. Alternatively, it can be a proc, which will be executed within the context of the controller's instance.
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, index: { unique: true }
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
- roleable_identity(with_roles: identity_with_roles?)
42
- end
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
- primary_key = roleable.class.primary_key
52
- roleable_id = roleable.public_send(primary_key)
45
+ roleable_id = roleable.public_send(roleable.class.primary_key)
53
46
 
54
- roles = with_roles ? ", roles: #{roleable.roles}" : ""
47
+ "#{model_name}##{roleable_id}"
48
+ end
49
+ end
55
50
 
56
- "#{model_name} with #{primary_key}: '#{roleable_id}'#{roles}"
57
- else
58
- "Unauthenticated #{Rabarber::HasRoles.roleable_class.model_name.human.downcase}"
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} has been assigned the following roles: #{roles_to_assign}, current roles: #{current_roles}"
18
+ "[Role Assignment] #{identity} | context: #{human_context} | assigned: #{roles_to_assign} | current: #{current_roles}"
19
19
  end
20
20
 
21
- def identity_with_roles?
22
- false
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} has been revoked from the following roles: #{roles_to_revoke}, current roles: #{current_roles}"
18
+ "[Role Revocation] #{identity} | context: #{human_context} | revoked: #{roles_to_revoke} | current: #{current_roles}"
19
19
  end
20
20
 
21
- def identity_with_roles?
22
- false
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} attempted to access '#{path}'"
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 grant_access(action: nil, roles: nil, if: nil, unless: nil)
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?(roleable_roles, self.class, action_name.to_sym, self)
37
+ return if Rabarber::Core::Permissions.access_granted?(roleable, action_name.to_sym, self)
33
38
 
34
- Rabarber::Audit::Events::UnauthorizedAttempt.trigger(roleable, path: request.path)
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
@@ -3,20 +3,19 @@
3
3
  module Rabarber
4
4
  module Core
5
5
  module Access
6
- def access_granted?(roles, controller, action, dynamic_rule_receiver)
7
- controller_accessible?(roles, controller, dynamic_rule_receiver) ||
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?(roles, controller, dynamic_rule_receiver)
10
+ def controller_accessible?(roleable, controller_instance)
12
11
  controller_rules.any? do |rule_controller, rule|
13
- controller <= rule_controller && rule.verify_access(roles, dynamic_rule_receiver)
12
+ controller_instance.class <= rule_controller && rule.verify_access(roleable, controller_instance)
14
13
  end
15
14
  end
16
15
 
17
- def action_accessible?(roles, controller, action, dynamic_rule_receiver)
18
- action_rules[controller].any? do |rule|
19
- rule.action == action && rule.verify_access(roles, dynamic_rule_receiver)
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
@@ -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
- extend self
8
+ module_function
7
9
 
8
10
  CACHE_PREFIX = "rabarber"
9
11
  private_constant :CACHE_PREFIX
10
12
 
11
- def fetch(roleable_id, options = { expires_in: 1.hour, race_condition_ttl: 5.seconds }, &block)
12
- enabled? ? Rails.cache.fetch(key_for(roleable_id), **options, &block) : yield
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
- private
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&.roles.to_a
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
@@ -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(roleable_roles, dynamic_rule_receiver)
16
- roles_permitted?(roleable_roles) && dynamic_rule_followed?(dynamic_rule_receiver)
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?(roleable_roles)
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?(dynamic_rule_receiver)
26
- !!(execute_dynamic_rule(dynamic_rule_receiver, false) && execute_dynamic_rule(dynamic_rule_receiver, true))
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(dynamic_rule_receiver, is_negated)
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
- dynamic_rule_receiver.instance_exec(&rule)
41
+ controller_instance.instance_exec(&rule)
38
42
  else
39
- dynamic_rule_receiver.send(rule)
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 unless roleable_roles.intersection(Rabarber::Input::Roles.new(roles).process).any?
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
@@ -11,10 +11,8 @@ module Rabarber
11
11
 
12
12
  def processed_value
13
13
  case value
14
- when String, Symbol
15
- value.to_sym
16
- when nil
17
- value
14
+ when String, Symbol then value.to_sym
15
+ when nil then value
18
16
  end
19
17
  end
20
18
 
@@ -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
@@ -11,10 +11,8 @@ module Rabarber
11
11
 
12
12
  def processed_value
13
13
  case value
14
- when String, Symbol
15
- value.to_sym
16
- when Proc, nil
17
- value
14
+ when String, Symbol then value.to_sym
15
+ when Proc, nil then value
18
16
  end
19
17
  end
20
18
 
@@ -14,43 +14,62 @@ module Rabarber
14
14
  join_table: "rabarber_roles_roleables"
15
15
  end
16
16
 
17
- def roles
18
- Rabarber::Core::Cache.fetch(roleable_id) { rabarber_roles.names }
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
- roles.intersection(process_role_names(role_names)).any?
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(name: processed_role_names - rabarber_roles.names)
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(self, roles_to_assign: roles_to_assign.names, current_roles: roles)
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
- roles_to_revoke = Rabarber::Role.where(name: processed_role_names.intersection(roles))
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(self, roles_to_revoke: roles_to_revoke.names, current_roles: roles)
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 delete_roleable_cache
73
- Rabarber::Core::Cache.delete(roleable_id)
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
@@ -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, uniqueness: true, format: { with: Rabarber::Input::Role::REGEX }, strict: 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
- role = find_by(name: process_role_name(old_name))
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
- role = find_by(name: process_role_name(name))
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rabarber
4
- VERSION = "2.0.0"
4
+ VERSION = "3.0.0"
5
5
  end
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: 2.0.0
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-04-25 00:00:00.000000000 Z
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