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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2cf7603eb6340263f349ffd661101f6428d210982b2a4dfb0d121f19814db3b7
4
- data.tar.gz: e31584352b9ea5565bfbc4e9a9f53084c6b9123643eb6919557ef52b9dfb3efb
3
+ metadata.gz: 51c1cba47b8af312f38ff403b77ef716099101d5bed3226c3e7d2bad37c23874
4
+ data.tar.gz: 3da6569ceef5c624aa649240341fcb72ce879005e458044006ba547aadbe316e
5
5
  SHA512:
6
- metadata.gz: 757631f95940e95d1640d16101b1fafe049808178ade0d0d97aac3163d0c31c581043bbeeab5b94da9ba4ed28e9edf890aba9adc845e80e9ab1b39ae22c6d66b
7
- data.tar.gz: 0eb8500deced87bea565d146de0dcd236ffeacac86e025bd6c5cd484fad94cb8594158575c88e31c69fec1f9d0a83f5328029ee261a343603505f847cad5e989
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
  [![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,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 all the roles assigned to the user, use:
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 all the role names available in your application, use:
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, 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
@@ -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?(roleable_roles, self.class, action_name.to_sym, self)
37
+ return if Rabarber::Core::Permissions.access_granted?(roleable, action_name.to_sym, self)
37
38
 
38
- 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
+ )
39
42
 
40
43
  when_unauthorized
41
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.1.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.1.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-05-28 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