rabarber 6.0.2 → 6.1.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: d9b600e0f228b103c91be66a391b115f5478785e25cb590b6377a55584db33e5
4
- data.tar.gz: b4d93e408b7e6b6b9cba4b56ea9d4b5e60ddb6299b43f90b4ccc0d40f9db0b4e
3
+ metadata.gz: d6a04e1ab84c89a6ab0271fb9c75021c6a5b0ecf042ae53914e58c969ed8a533
4
+ data.tar.gz: 4dc99eacf060c21b9a08d23a882a96ce41c3cf8675d1eae73a2dc00ee925eae5
5
5
  SHA512:
6
- metadata.gz: b85dc2c9c0d9ed86e1de5811e2b62254816632b98cba73d7a2da351db093281329b39ef34891d02cffae23c613ef48f2a10d79a9241d677c51a68199ff7aed21
7
- data.tar.gz: c9fbf1d765b960b43f335078e9e4b6714f8a21eb33db85ed7c7c74ac8f6a1c943f293e630981e3c60e800e333ea94db260266a5de0e85ce72fc6702dbd015d3b
6
+ metadata.gz: 876ded94c228f0318c809c2d93ae4b0a7187fd85a958cd85c826b2b6659dae1550d90b665e63b1221866e72fdd133f39811aa458de2e6755676b4f9590e0062b
7
+ data.tar.gz: 43e014c4ce4f3e57f7c3bb9668b40d00a355d0f5c9129a8e36d130d01568377dbfa2f7a1a27a8e3224b77b13d276a820966c7c765ea52dcf0b628e5cfa037452
data/CHANGELOG.md CHANGED
@@ -1,3 +1,32 @@
1
+ ## v6.1.0
2
+
3
+ ### Features:
4
+
5
+ - Added `Rabarber.create_roles` and `Rabarber.delete_roles` to create and delete several roles at once
6
+ - Added support for binary UUID primary keys in MySQL
7
+
8
+ ### Bugs:
9
+
10
+ - Fixed a race condition where assigning several roles at once could assign none of them
11
+ - Fixed context migration helpers raising when no roles exist in the context
12
+ - Fixed the generated migration creating a broken foreign key when the user table's primary key is not `id`
13
+ - Fixed roles assigned or revoked inside a rolled back transaction staying cached
14
+ - Fixed concurrent role creation or renaming discarding the surrounding transaction
15
+ - Fixed the generated migration not working with UUID primary keys on some databases
16
+ - Fixed roles assigned during `revoke_all_roles` staying cached after being revoked
17
+
18
+ ### Misc:
19
+
20
+ - Deprecated calling `assign_roles`, `revoke_roles`, or `revoke_all_roles` on an unsaved record
21
+ - Deprecated `Rabarber.create_role` and `Rabarber.delete_role`
22
+ - The generator now takes the primary key type from the existing user table, making `--uuid` unnecessary
23
+
24
+ ## v6.0.3
25
+
26
+ ### Bugs:
27
+
28
+ - Fixed `Rabarber.all_roles` raising `Rabarber::NotFoundError` when a role's context is a class that is not an ActiveRecord model
29
+
1
30
  ## v6.0.2
2
31
 
3
32
  ### Bugs:
@@ -43,7 +72,7 @@ On databases without partial index support, such as MySQL, uniqueness of global
43
72
  - Optimized cache key generation
44
73
  - Dropped some unnecessary dependencies
45
74
 
46
- To upgrade to v6.0.0, please refer to the [migration guide](https://github.com/enjaku4/rabarber/discussions/101)
75
+ To upgrade to v6.0.0, please refer to the [migration guide](https://github.com/brownboxdev/rabarber-authorization/discussions/101)
47
76
 
48
77
  ## v5.2.5
49
78
 
@@ -129,7 +158,7 @@ To upgrade to v6.0.0, please refer to the [migration guide](https://github.com/e
129
158
  - Added `with_authorization` method for more granular authorization control
130
159
  - `Rabarber::Role.rename` and `Rabarber::Role.remove` now require the role to exist
131
160
 
132
- To upgrade to v5.0.0, please refer to the [migration guide](https://github.com/enjaku4/rabarber/discussions/77)
161
+ To upgrade to v5.0.0, please refer to the [migration guide](https://github.com/brownboxdev/rabarber-authorization/discussions/77)
133
162
 
134
163
  ### Features:
135
164
 
@@ -234,7 +263,7 @@ To upgrade to v5.0.0, please refer to the [migration guide](https://github.com/e
234
263
 
235
264
  - Changed Rabarber roles table structure
236
265
 
237
- To upgrade to v3.0.0, please refer to the [migration guide](https://github.com/enjaku4/rabarber/discussions/58)
266
+ To upgrade to v3.0.0, please refer to the [migration guide](https://github.com/brownboxdev/rabarber-authorization/discussions/58)
238
267
 
239
268
  ### Features:
240
269
 
@@ -258,7 +287,7 @@ To upgrade to v3.0.0, please refer to the [migration guide](https://github.com/e
258
287
  - Replaced `when_unauthorized` configuration option with an overridable controller method
259
288
  - Renamed `Rabarber::Role.assignees_for` method to `Rabarber::Role.assignees`
260
289
 
261
- To upgrade to v2.0.0, please refer to the [migration guide](https://github.com/enjaku4/rabarber/discussions/52)
290
+ To upgrade to v2.0.0, please refer to the [migration guide](https://github.com/brownboxdev/rabarber-authorization/discussions/52)
262
291
 
263
292
  ### Features:
264
293
 
data/README.md CHANGED
@@ -1,9 +1,9 @@
1
- # Rabarber: Simple Role-Based Authorization for Rails
1
+ # Rabarber: Simple Authorization for Rails
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/rabarber.svg)](http://badge.fury.io/rb/rabarber)
4
4
  [![Downloads](https://img.shields.io/gem/dt/rabarber.svg)](https://rubygems.org/gems/rabarber)
5
- [![Github Actions badge](https://github.com/enjaku4/rabarber/actions/workflows/ci.yml/badge.svg)](https://github.com/enjaku4/rabarber/actions/workflows/ci.yml)
6
- [![License](https://img.shields.io/github/license/enjaku4/rabarber.svg)](LICENSE)
5
+ [![Github Actions badge](https://github.com/brownboxdev/rabarber-authorization/actions/workflows/ci.yml/badge.svg)](https://github.com/brownboxdev/rabarber-authorization/actions/workflows/ci.yml)
6
+ [![License](https://img.shields.io/github/license/brownboxdev/rabarber-authorization.svg)](LICENSE)
7
7
 
8
8
  Rabarber is a role-based authorization library for Ruby on Rails. It provides a set of tools for managing user roles and defining access rules, with support for multi-tenancy through context.
9
9
 
@@ -62,11 +62,16 @@ bundle install
62
62
  Generate the migration to store roles (replace `users` with your table name if different):
63
63
 
64
64
  ```shell
65
- # For standard integer IDs
66
65
  rails generate rabarber:roles users
66
+ ```
67
+
68
+ Rabarber's tables use the same primary key type as your users table, so the table must exist before you run the generator. With binary UUIDs in MySQL, apply your UUID handling to Rabarber's models too, for example:
67
69
 
68
- # For UUID primary keys
69
- rails generate rabarber:roles users --uuid
70
+ ```rb
71
+ Rails.application.config.to_prepare do
72
+ Rabarber::Role.attribute :context_id, YourBinaryUuidType.new
73
+ Rabarber::RoleAssignment.attribute :roleable_id, YourBinaryUuidType.new
74
+ end
70
75
  ```
71
76
 
72
77
  Run the migration:
@@ -139,7 +144,10 @@ User.with_role(:admin, :manager)
139
144
  You can also directly manage roles available in the application:
140
145
 
141
146
  ```rb
142
- # Create a new role
147
+ # Create roles, leaving the ones that already exist as they are
148
+ Rabarber.create_roles(:admin, :manager)
149
+
150
+ # Create a new role (deprecated, use create_roles instead)
143
151
  Rabarber.create_role(:admin)
144
152
  # => true if created, false if already exists
145
153
 
@@ -150,11 +158,17 @@ Rabarber.rename_role(:admin, :administrator)
150
158
  # Force rename even if role is assigned
151
159
  Rabarber.rename_role(:admin, :administrator, force: true)
152
160
 
153
- # Remove a role
161
+ # Delete roles, keeping the ones that are assigned to users
162
+ Rabarber.delete_roles(:admin, :manager)
163
+
164
+ # Force deletion even if roles are assigned
165
+ Rabarber.delete_roles(:admin, :manager, force: true)
166
+
167
+ # Remove a role (deprecated, use delete_roles instead)
154
168
  Rabarber.delete_role(:admin)
155
169
  # => true if deleted, false if role is assigned
156
170
 
157
- # Force deletion even if role is assigned
171
+ # Force deletion even if role is assigned (deprecated, use delete_roles instead)
158
172
  Rabarber.delete_role(:admin, force: true)
159
173
 
160
174
  # List available roles in the global context
@@ -364,13 +378,19 @@ User.with_role(:member, context: project)
364
378
  ### Contextual Role Management
365
379
 
366
380
  ```rb
367
- # Create a new role within a context
381
+ # Create roles within a context
382
+ Rabarber.create_roles(:admin, :member, context: Project)
383
+
384
+ # Create a new role within a context (deprecated, use create_roles instead)
368
385
  Rabarber.create_role(:admin, context: Project)
369
386
 
370
387
  # Rename a role within a context
371
388
  Rabarber.rename_role(:admin, :owner, context: project)
372
389
 
373
- # Remove a contextual role
390
+ # Delete contextual roles
391
+ Rabarber.delete_roles(:admin, :member, context: project)
392
+
393
+ # Remove a contextual role (deprecated, use delete_roles instead)
374
394
  Rabarber.delete_role(:admin, context: project)
375
395
 
376
396
  # List available roles within a specific context
@@ -409,7 +429,16 @@ end
409
429
 
410
430
  When a context object is deleted from your database, its associated roles become orphaned and ignored by Rabarber.
411
431
 
412
- To clean up orphaned roles, use:
432
+ Ideally, the application should be designed so that this doesn't happen: whenever a context object is deleted, its roles are revoked and deleted as part of the same operation:
433
+
434
+ ```rb
435
+ Project.transaction do
436
+ Rabarber.delete_roles(*Rabarber.roles(context: project), context: project, force: true)
437
+ project.destroy!
438
+ end
439
+ ```
440
+
441
+ However, if that wasn't done, you can clean up the orphaned roles as a fallback:
413
442
 
414
443
  ```rb
415
444
  Rabarber.prune
@@ -465,7 +494,7 @@ Like authorization, these helpers require an authenticated user. In views that c
465
494
 
466
495
  ### Getting Help
467
496
 
468
- Have a question or need assistance? Open a discussion in the [discussions section](https://github.com/enjaku4/rabarber/discussions) for:
497
+ Have a question or need assistance? Open a discussion in the [discussions section](https://github.com/brownboxdev/rabarber-authorization/discussions) for:
469
498
 
470
499
  - Usage questions
471
500
  - Implementation guidance
@@ -473,7 +502,7 @@ Have a question or need assistance? Open a discussion in the [discussions sectio
473
502
 
474
503
  ### Issues
475
504
 
476
- [Issues](https://github.com/enjaku4/rabarber/issues) track bugs, planned features, and other work. When reporting a bug, please include:
505
+ [Issues](https://github.com/brownboxdev/rabarber-authorization/issues) track bugs, planned features, and other work. When reporting a bug, please include:
477
506
 
478
507
  - A clear description of the problem
479
508
  - Steps to reproduce the issue
@@ -481,21 +510,21 @@ Have a question or need assistance? Open a discussion in the [discussions sectio
481
510
 
482
511
  ### Contributing Code
483
512
 
484
- Any open issue is free to pick up or discuss. Check the [project board](https://github.com/users/enjaku4/projects/10) or [issues tab](https://github.com/enjaku4/rabarber/issues).
513
+ Any open issue is free to pick up or discuss. Check the [issues tab](https://github.com/brownboxdev/rabarber-authorization/issues).
485
514
 
486
- Before contributing, please read the [contributing guidelines](https://github.com/enjaku4/rabarber/blob/main/CONTRIBUTING.md).
515
+ Before contributing, please read the [contributing guidelines](https://github.com/brownboxdev/rabarber-authorization/blob/main/CONTRIBUTING.md).
487
516
 
488
517
  ## License
489
518
 
490
- The gem is available as open source under the terms of the [MIT License](https://github.com/enjaku4/rabarber/blob/main/LICENSE.txt).
519
+ The gem is available as open source under the terms of the [MIT License](https://github.com/brownboxdev/rabarber-authorization/blob/main/LICENSE.txt).
491
520
 
492
521
  ## Code of Conduct
493
522
 
494
- Everyone interacting in the Rabarber project is expected to follow the [code of conduct](https://github.com/enjaku4/rabarber/blob/main/CODE_OF_CONDUCT.md).
523
+ Everyone interacting in the Rabarber project is expected to follow the [code of conduct](https://github.com/brownboxdev/rabarber-authorization/blob/main/CODE_OF_CONDUCT.md).
495
524
 
496
525
  ## Old Versions
497
526
 
498
527
  Only the latest major version is actively maintained. READMEs for older versions are available here for reference:
499
528
 
500
- [v5.x.x](https://github.com/enjaku4/rabarber/blob/12a23858e974f5cc0a4ebddfc18bdf84171dd554/README.md) | [v4.x.x](https://github.com/enjaku4/rabarber/blob/9353e70281971154d5acd70693620197a132c543/README.md) | [v3.x.x](https://github.com/enjaku4/rabarber/blob/3bb273de7e342004abc7ef07fa4d0a9a3ce3e249/README.md)
501
- | [v2.x.x](https://github.com/enjaku4/rabarber/blob/875b357ea949404ddc3645ad66eddea7ed4e2ee4/README.md) | [v1.x.x](https://github.com/enjaku4/rabarber/blob/b81428429404e197d70317b763e7b2a21e02c296/README.md)
529
+ [v5.x.x](https://github.com/brownboxdev/rabarber-authorization/blob/12a23858e974f5cc0a4ebddfc18bdf84171dd554/README.md) | [v4.x.x](https://github.com/brownboxdev/rabarber-authorization/blob/9353e70281971154d5acd70693620197a132c543/README.md) | [v3.x.x](https://github.com/brownboxdev/rabarber-authorization/blob/3bb273de7e342004abc7ef07fa4d0a9a3ce3e249/README.md)
530
+ | [v2.x.x](https://github.com/brownboxdev/rabarber-authorization/blob/875b357ea949404ddc3645ad66eddea7ed4e2ee4/README.md) | [v1.x.x](https://github.com/brownboxdev/rabarber-authorization/blob/b81428429404e197d70317b763e7b2a21e02c296/README.md)
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rabarber
4
+ class Role < ActiveRecord::Base
5
+ self.table_name = "rabarber_roles"
6
+
7
+ belongs_to :context, polymorphic: true, optional: true
8
+
9
+ has_many :role_assignments, class_name: "Rabarber::RoleAssignment", inverse_of: :role, dependent: :delete_all
10
+
11
+ class << self
12
+ def list(context: nil)
13
+ names_in(Rabarber::Inputs::Context.new(context).resolve)
14
+ end
15
+
16
+ def list_all
17
+ roles = where(context_id: nil).to_a + where.not(context_id: nil).includes(:context).to_a
18
+
19
+ roles.each_with_object({}) do |role, hash|
20
+ (hash[role.context] ||= []) << role.name.to_sym
21
+ rescue ActiveRecord::RecordNotFound
22
+ next
23
+ end
24
+ end
25
+
26
+ def register(names, context: nil)
27
+ processed_names = Rabarber::Inputs::Roles.new(names).process
28
+ processed_context = Rabarber::Inputs::Context.new(context).resolve
29
+
30
+ register_missing(processed_names, processed_context)
31
+
32
+ names_in(processed_context)
33
+ end
34
+
35
+ def amend(old_name, new_name, context: nil, force: false)
36
+ processed_force = Rabarber::Inputs::Boolean.new(force).process
37
+ processed_context = Rabarber::Inputs::Context.new(context).resolve
38
+ processed_old_name = Rabarber::Inputs::Role.new(old_name).process
39
+ name = Rabarber::Inputs::Role.new(new_name).process
40
+
41
+ transaction(requires_new: true) do
42
+ role = lock.find_by(name: processed_old_name, **processed_context)
43
+
44
+ raise Rabarber::NotFoundError, "Role not found" unless role
45
+
46
+ next false if exists?(name:, **processed_context) || !processed_force && role.role_assignments.lock.exists?
47
+
48
+ role.update!(name:)
49
+
50
+ delete_roleables_cache(role.role_assignments.lock.pluck(:roleable_id), context: processed_context)
51
+
52
+ true
53
+ end
54
+ rescue ActiveRecord::RecordNotUnique
55
+ false
56
+ end
57
+
58
+ def drop(names, context: nil, force: false)
59
+ processed_names = Rabarber::Inputs::Roles.new(names).process
60
+ processed_context = Rabarber::Inputs::Context.new(context).resolve
61
+ processed_force = Rabarber::Inputs::Boolean.new(force).process
62
+
63
+ transaction do
64
+ role_ids = where(name: processed_names, **processed_context).lock.ids
65
+ role_ids.reject! { Rabarber::RoleAssignment.where(role_id: _1).lock.exists? } unless processed_force
66
+ roleable_ids = Rabarber::RoleAssignment.where(role_id: role_ids).lock.pluck(:roleable_id).uniq
67
+
68
+ delete_with_assignments(where(id: role_ids))
69
+
70
+ delete_roleables_cache(roleable_ids, context: processed_context)
71
+ end
72
+
73
+ names_in(processed_context)
74
+ end
75
+
76
+ def prune
77
+ where.not(context_id: nil).distinct.pluck(:context_type).each do |context_type|
78
+ context_class = context_type.constantize
79
+
80
+ delete_with_assignments(where(context_type:).where.not(context_id: context_class.select(context_class.primary_key)))
81
+ end
82
+ end
83
+
84
+ def deprecated_register(name, context: nil)
85
+ name = Rabarber::Inputs::Role.new(name).process
86
+ processed_context = Rabarber::Inputs::Context.new(context).resolve
87
+
88
+ return false if exists?(name:, **processed_context)
89
+
90
+ !!transaction(requires_new: true) { create!(name:, **processed_context) }
91
+ rescue ActiveRecord::RecordNotUnique
92
+ false
93
+ end
94
+
95
+ def deprecated_drop(name, context: nil, force: false)
96
+ processed_force = Rabarber::Inputs::Boolean.new(force).process
97
+ processed_context = Rabarber::Inputs::Context.new(context).resolve
98
+ processed_name = Rabarber::Inputs::Role.new(name).process
99
+
100
+ transaction do
101
+ role = lock.find_by(name: processed_name, **processed_context)
102
+
103
+ raise Rabarber::NotFoundError, "Role not found" unless role
104
+
105
+ next false if !processed_force && role.role_assignments.lock.exists?
106
+
107
+ roleable_ids = role.role_assignments.lock.pluck(:roleable_id)
108
+
109
+ role.destroy!
110
+
111
+ delete_roleables_cache(roleable_ids, context: processed_context)
112
+
113
+ true
114
+ end
115
+ end
116
+
117
+ def names_in(processed_context)
118
+ where(processed_context).pluck(:name).map(&:to_sym)
119
+ end
120
+
121
+ def register_missing(names, processed_context)
122
+ (names.uniq - names_in(processed_context)).each do |name|
123
+ transaction(requires_new: true) { create!(name:, **processed_context) }
124
+ rescue ActiveRecord::RecordNotUnique
125
+ next
126
+ end
127
+ end
128
+
129
+ def delete_with_assignments(roles)
130
+ transaction do
131
+ Rabarber::RoleAssignment.where(role: roles).delete_all
132
+ roles.delete_all
133
+ end
134
+ end
135
+
136
+ private
137
+
138
+ def delete_roleables_cache(roleable_ids, context:)
139
+ Rabarber::Core::Cache.delete(*roleable_ids.flat_map { [[_1, context], [_1, :all]] })
140
+ end
141
+ end
142
+
143
+ def context
144
+ return context_type.constantize if context_type.present? && context_id.blank?
145
+
146
+ record = super
147
+
148
+ raise ActiveRecord::RecordNotFound.new(nil, context_type, nil, context_id) if context_id.present? && !record
149
+
150
+ record
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rabarber
4
+ class RoleAssignment < ActiveRecord::Base
5
+ self.table_name = "rabarber_roles_roleables"
6
+
7
+ belongs_to :role, class_name: "Rabarber::Role"
8
+ belongs_to :roleable, class_name: Rabarber::Configuration.user_model_name
9
+ end
10
+ end
@@ -10,9 +10,13 @@ module Rabarber
10
10
 
11
11
  argument :table_name, type: :string, required: true
12
12
 
13
- class_option :uuid, type: :boolean, default: false, desc: "Use UUIDs as primary keys"
14
-
15
13
  def create_migrations
14
+ @id_type = ActiveRecord::Base.with_connection do |connection|
15
+ raise Thor::Error, "Table `#{table_name}` does not exist, run its migration first" unless connection.table_exists?(table_name)
16
+
17
+ connection.columns(table_name).find { |column| column.name == connection.primary_key(table_name) }.sql_type
18
+ end
19
+
16
20
  migration_template "create_rabarber_roles.rb.erb", "db/migrate/create_rabarber_roles.rb"
17
21
  end
18
22
 
@@ -2,9 +2,9 @@
2
2
 
3
3
  class CreateRabarberRoles < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version.to_s %>]
4
4
  def change
5
- create_table :rabarber_roles<%= ", id: :uuid" if options[:uuid] %> do |t|
5
+ create_table :rabarber_roles do |t|
6
6
  t.string :name, null: false
7
- t.belongs_to :context, polymorphic: true, index: true<%= ", type: :uuid" if options[:uuid] %>
7
+ t.belongs_to :context, polymorphic: true, index: true, type: <%= @id_type.inspect %>
8
8
  t.timestamps
9
9
  end
10
10
 
@@ -16,8 +16,8 @@ class CreateRabarberRoles < ActiveRecord::Migration[<%= ActiveRecord::Migration.
16
16
  end
17
17
 
18
18
  create_table :rabarber_roles_roleables, id: false do |t|
19
- t.belongs_to :role, null: false, index: true, foreign_key: { to_table: :rabarber_roles }<%= ", type: :uuid" if options[:uuid] %>
20
- t.belongs_to :roleable, null: false, index: true, foreign_key: { to_table: <%= table_name.to_sym.inspect %> }<%= ", type: :uuid" if options[:uuid] %>
19
+ t.belongs_to :role, null: false, index: true, foreign_key: { to_table: :rabarber_roles }
20
+ t.belongs_to :roleable, null: false, index: true, foreign_key: { to_table: <%= table_name.to_sym.inspect %>, primary_key: primary_key(<%= table_name.to_sym.inspect %>) }, type: <%= @id_type.inspect %>
21
21
  end
22
22
 
23
23
  add_index :rabarber_roles_roleables, [:role_id, :roleable_id], unique: true
@@ -16,7 +16,7 @@ module Rabarber
16
16
  @cache_enabled = Rabarber::Inputs::Boolean.new(
17
17
  value,
18
18
  error: Rabarber::ConfigurationError,
19
- message: "Invalid configuration `cache_enabled`, expected a boolean, got #{value.inspect}"
19
+ message: "Invalid configuration `cache_enabled`, expected a boolean, got `#{value.inspect}`"
20
20
  ).process
21
21
  end
22
22
 
@@ -24,7 +24,7 @@ module Rabarber
24
24
  @current_user_method = Rabarber::Inputs::NonEmptySymbol.new(
25
25
  value,
26
26
  error: Rabarber::ConfigurationError,
27
- message: "Invalid configuration `current_user_method`, expected a symbol or a string, got #{value.inspect}"
27
+ message: "Invalid configuration `current_user_method`, expected a symbol or a string, got `#{value.inspect}`"
28
28
  ).process
29
29
  end
30
30
 
@@ -32,7 +32,7 @@ module Rabarber
32
32
  @user_model_name = Rabarber::Inputs::NonEmptyString.new(
33
33
  value,
34
34
  error: Rabarber::ConfigurationError,
35
- message: "Invalid configuration `user_model_name`, expected an ActiveRecord model name, got #{value.inspect}"
35
+ message: "Invalid configuration `user_model_name`, expected an ActiveRecord model name, got `#{value.inspect}`"
36
36
  ).process
37
37
  end
38
38
 
@@ -46,7 +46,7 @@ module Rabarber
46
46
  Rabarber::Inputs::Model.new(
47
47
  user_model_name,
48
48
  error: Rabarber::ConfigurationError,
49
- message: "Invalid configuration `user_model_name`, expected an ActiveRecord model name, got #{user_model_name.inspect}"
49
+ message: "Invalid configuration `user_model_name`, expected an ActiveRecord model name, got `#{user_model_name.inspect}`"
50
50
  ).process
51
51
  end
52
52
 
@@ -24,30 +24,11 @@ module Rabarber
24
24
 
25
25
  Rabarber::Core::Permissions.add(
26
26
  self,
27
- Rabarber::Inputs::NonEmptySymbol.new(
28
- action,
29
- optional: true,
30
- message: "Expected a symbol or a string, got #{action.inspect}"
31
- ).process,
32
- Rabarber::Inputs::Roles.new(
33
- roles,
34
- message: "Expected an array of symbols or strings containing only lowercase letters, numbers, and underscores, got #{roles.inspect}"
35
- ).process,
36
- Rabarber::Inputs::Contexts::Authorizational.new(
37
- context,
38
- error: Rabarber::InvalidContextError,
39
- message: "Expected a Class, an instance of ActiveRecord model, a symbol, a string, or a proc, got #{context.inspect}"
40
- ).resolve,
41
- Rabarber::Inputs::DynamicRule.new(
42
- if_rule,
43
- optional: true,
44
- message: "Expected a symbol, a string, or a proc, got #{if_rule.inspect}"
45
- ).process,
46
- Rabarber::Inputs::DynamicRule.new(
47
- unless_rule,
48
- optional: true,
49
- message: "Expected a symbol, a string, or a proc, got #{unless_rule.inspect}"
50
- ).process
27
+ Rabarber::Inputs::NonEmptySymbol.new(action, optional: true).process,
28
+ Rabarber::Inputs::Roles.new(roles).process,
29
+ Rabarber::Inputs::Contexts::Authorizational.new(context).process,
30
+ Rabarber::Inputs::DynamicRule.new(if_rule, optional: true).process,
31
+ Rabarber::Inputs::DynamicRule.new(unless_rule, optional: true).process
51
32
  )
52
33
  end
53
34
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "digest/md5"
4
+ require "json"
4
5
  require "securerandom"
5
6
 
6
7
  module Rabarber
@@ -9,22 +10,22 @@ module Rabarber
9
10
  extend self
10
11
 
11
12
  def fetch(roleable_id, scope, &)
12
- return yield unless enabled?
13
+ return yield if !enabled? || ActiveRecord::Base.current_transaction.open?
13
14
 
14
- Rails.cache.fetch(prepare_key(roleable_id, scope), expires_in: 1.hour, race_condition_ttl: 5.seconds, &)
15
+ Rails.cache.fetch(prepare_key(current_version, roleable_id, scope), expires_in: 1.hour, race_condition_ttl: 5.seconds, &)
15
16
  end
16
17
 
17
18
  def delete(*pairs)
18
19
  return unless enabled? && pairs.any?
19
20
 
20
- keys = pairs.map { |roleable_id, scope| prepare_key(roleable_id, scope) }
21
+ delete_keys(pairs)
21
22
 
22
- Rails.cache.delete_multi(keys)
23
- ActiveRecord.after_all_transactions_commit { Rails.cache.delete_multi(keys) } if ActiveRecord::Base.connection.transaction_open?
23
+ transaction = ActiveRecord::Base.current_transaction
24
+ transaction.after_commit { delete_keys(pairs) } if transaction.open?
24
25
  end
25
26
 
26
27
  def clear
27
- Rails.cache.write(VERSION_KEY, SecureRandom.alphanumeric(8))
28
+ ActiveRecord::Base.current_transaction.after_commit { rotate_version }
28
29
  end
29
30
 
30
31
  private
@@ -33,8 +34,13 @@ module Rabarber
33
34
  Rabarber::Configuration.cache_enabled
34
35
  end
35
36
 
36
- def prepare_key(roleable_id, scope)
37
- Digest::MD5.base64digest(Marshal.dump([current_version, roleable_id, scope]))
37
+ def delete_keys(pairs)
38
+ version = current_version
39
+ Rails.cache.delete_multi(pairs.map { |roleable_id, scope| prepare_key(version, roleable_id, scope) })
40
+ end
41
+
42
+ def prepare_key(version, roleable_id, scope)
43
+ Digest::MD5.base64digest(JSON.generate([version, roleable_id, scope]))
38
44
  end
39
45
 
40
46
  def current_version
@@ -42,11 +48,15 @@ module Rabarber
42
48
 
43
49
  return version if version
44
50
 
45
- clear
51
+ rotate_version
46
52
 
47
53
  Rails.cache.read(VERSION_KEY)
48
54
  end
49
55
 
56
+ def rotate_version
57
+ Rails.cache.write(VERSION_KEY, SecureRandom.alphanumeric(8))
58
+ end
59
+
50
60
  VERSION_KEY = "rabarber"
51
61
  private_constant :VERSION_KEY
52
62
  end
@@ -3,22 +3,20 @@
3
3
  module Rabarber
4
4
  module Core
5
5
  module Roleable
6
+ private
7
+
6
8
  def roleable
7
9
  current_roleable = send(Rabarber::Configuration.current_user_method)
8
10
 
9
11
  unless current_roleable.is_a?(Rabarber::Configuration.user_model)
10
12
  raise(
11
13
  Rabarber::Error,
12
- "Expected `#{Rabarber::Configuration.current_user_method}` to return an instance of #{Rabarber::Configuration.user_model_name}, got #{current_roleable.inspect}"
14
+ "Expected `#{Rabarber::Configuration.current_user_method}` to return an instance of #{Rabarber::Configuration.user_model_name}, got `#{current_roleable.inspect}`"
13
15
  )
14
16
  end
15
17
 
16
18
  current_roleable
17
19
  end
18
-
19
- def roleable_roles(context: nil)
20
- roleable.roles(context:)
21
- end
22
20
  end
23
21
  end
24
22
  end
@@ -19,7 +19,7 @@ module Rabarber
19
19
  end
20
20
 
21
21
  def roles_permitted?(roleable, controller_instance)
22
- @roles.empty? || roleable.has_role?(*@roles, context: resolve_context(controller_instance))
22
+ @roles.empty? || roleable.has_role?(*@roles, context: context_for(controller_instance))
23
23
  end
24
24
 
25
25
  def dynamic_rules_followed?(controller_instance)
@@ -32,18 +32,12 @@ module Rabarber
32
32
  !!(rule.is_a?(Proc) ? controller_instance.instance_exec(&rule) : controller_instance.send(rule))
33
33
  end
34
34
 
35
- def resolve_context(controller_instance)
36
- context = case @context
37
- when Proc then controller_instance.instance_exec(&@context)
38
- when Symbol then controller_instance.send(@context)
39
- else @context
40
- end
41
-
42
- Rabarber::Inputs::Context.new(
43
- context,
44
- error: Rabarber::InvalidContextError,
45
- message: "Expected an instance of ActiveRecord model, a Class, or nil, got #{context.inspect}"
46
- ).resolve
35
+ def context_for(controller_instance)
36
+ case @context
37
+ when Proc then controller_instance.instance_exec(&@context)
38
+ when Symbol then controller_instance.send(@context)
39
+ else @context
40
+ end
47
41
  end
48
42
  end
49
43
  end