rabarber 6.0.3 → 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: c7481864a3dc409501c34dc66d1f7bfe3399118285fb99cbc6979b17c1b75ff3
4
- data.tar.gz: ea2c8e12a4a28be3166d4fb60db78e5c5857e838697a5b2308db2486e81d92ad
3
+ metadata.gz: d6a04e1ab84c89a6ab0271fb9c75021c6a5b0ecf042ae53914e58c969ed8a533
4
+ data.tar.gz: 4dc99eacf060c21b9a08d23a882a96ce41c3cf8675d1eae73a2dc00ee925eae5
5
5
  SHA512:
6
- metadata.gz: 239a2ba8dc2541688ab09fdbf143d9246e6a725d5ac8adebd253c2ef5d2a7935a4b51d4dffadfb6f490cb6871955b1832c6f24188bfe389abbf22287627652d8
7
- data.tar.gz: bdf91f9803ec0fd770a7bdb8b0267e63566078a7496b736b0cfdc8a2b7084c73be0836148e0d89e887f8753338fc9efa3a15213f3fa839229b041497349f8908
6
+ metadata.gz: 876ded94c228f0318c809c2d93ae4b0a7187fd85a958cd85c826b2b6659dae1550d90b665e63b1221866e72fdd133f39811aa458de2e6755676b4f9590e0062b
7
+ data.tar.gz: 43e014c4ce4f3e57f7c3bb9668b40d00a355d0f5c9129a8e36d130d01568377dbfa2f7a1a27a8e3224b77b13d276a820966c7c765ea52dcf0b628e5cfa037452
data/CHANGELOG.md CHANGED
@@ -1,3 +1,26 @@
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
+
1
24
  ## v6.0.3
2
25
 
3
26
  ### Bugs:
@@ -49,7 +72,7 @@ On databases without partial index support, such as MySQL, uniqueness of global
49
72
  - Optimized cache key generation
50
73
  - Dropped some unnecessary dependencies
51
74
 
52
- 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)
53
76
 
54
77
  ## v5.2.5
55
78
 
@@ -135,7 +158,7 @@ To upgrade to v6.0.0, please refer to the [migration guide](https://github.com/e
135
158
  - Added `with_authorization` method for more granular authorization control
136
159
  - `Rabarber::Role.rename` and `Rabarber::Role.remove` now require the role to exist
137
160
 
138
- 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)
139
162
 
140
163
  ### Features:
141
164
 
@@ -240,7 +263,7 @@ To upgrade to v5.0.0, please refer to the [migration guide](https://github.com/e
240
263
 
241
264
  - Changed Rabarber roles table structure
242
265
 
243
- 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)
244
267
 
245
268
  ### Features:
246
269
 
@@ -264,7 +287,7 @@ To upgrade to v3.0.0, please refer to the [migration guide](https://github.com/e
264
287
  - Replaced `when_unauthorized` configuration option with an overridable controller method
265
288
  - Renamed `Rabarber::Role.assignees_for` method to `Rabarber::Role.assignees`
266
289
 
267
- 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)
268
291
 
269
292
  ### Features:
270
293
 
data/README.md CHANGED
@@ -2,8 +2,8 @@
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
- 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. However, if that wasn't done, you can clean up the orphaned roles as a fallback:
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
@@ -11,7 +11,7 @@ module Rabarber
11
11
  unless current_roleable.is_a?(Rabarber::Configuration.user_model)
12
12
  raise(
13
13
  Rabarber::Error,
14
- "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}`"
15
15
  )
16
16
  end
17
17
 
@@ -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
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "rails/railtie"
3
+ require "rails/engine"
4
4
 
5
5
  module Rabarber
6
- class Railtie < Rails::Railtie
6
+ class Engine < Rails::Engine
7
7
  initializer "rabarber.include_roleable" do
8
8
  Rails.autoloaders.each do |autoloader|
9
9
  autoloader.on_load do |cpath, klass, _abspath|
@@ -15,14 +15,13 @@ module Rabarber
15
15
  initializer "rabarber.to_prepare" do |app|
16
16
  app.config.to_prepare do
17
17
  Rabarber::Core::Permissions.reset! unless app.config.eager_load
18
-
19
- Rabarber::Role.send(:remove_const, :HABTM_Roleables) if Rabarber::Role.const_defined?(:HABTM_Roleables, false)
20
- Rabarber::Role.has_and_belongs_to_many :roleables, class_name: Rabarber::Configuration.user_model_name,
21
- association_foreign_key: "roleable_id",
22
- join_table: "rabarber_roles_roleables"
23
18
  end
24
19
  end
25
20
 
21
+ initializer "rabarber.deprecator" do |app|
22
+ app.deprecators[:rabarber] = Rabarber.deprecator
23
+ end
24
+
26
25
  initializer "rabarber.extend_migration_helpers" do
27
26
  ActiveSupport.on_load :active_record do
28
27
  ActiveRecord::Migration.include Rabarber::MigrationHelpers
@@ -3,27 +3,16 @@
3
3
  module Rabarber
4
4
  module MigrationHelpers
5
5
  def migrate_authorization_context!(old_context, new_context)
6
- roles = Rabarber::Role.where(context_type: old_context.to_s)
6
+ old_context_type = Rabarber::Inputs::NonEmptyString.new(old_context).process
7
+ new_context_type = Rabarber::Inputs::NonEmptyString.new(new_context).process
7
8
 
8
- raise Rabarber::InvalidArgumentError, "No roles exist in context #{old_context.inspect}" unless roles.exists?
9
- raise Rabarber::InvalidArgumentError, "Cannot migrate context to #{new_context.inspect}: class does not exist" unless new_context.to_s.safe_constantize
10
-
11
- roles.update_all(context_type: new_context.to_s)
9
+ Rabarber::Role.where(context_type: old_context_type).update_all(context_type: new_context_type)
12
10
  end
13
11
 
14
12
  def delete_authorization_context!(context)
15
- roles = Rabarber::Role.where(context_type: context.to_s)
16
-
17
- raise Rabarber::InvalidArgumentError, "No roles exist in context #{context.inspect}" unless roles.exists?
13
+ context_type = Rabarber::Inputs::NonEmptyString.new(context).process
18
14
 
19
- ActiveRecord::Base.transaction do
20
- ActiveRecord::Base.connection.execute(
21
- ActiveRecord::Base.sanitize_sql(
22
- ["DELETE FROM rabarber_roles_roleables WHERE role_id IN (?)", roles.pluck(:id)]
23
- )
24
- )
25
- roles.delete_all
26
- end
15
+ Rabarber::Role.delete_with_assignments(Rabarber::Role.where(context_type:))
27
16
  end
28
17
  end
29
18
  end
@@ -3,7 +3,7 @@
3
3
  module Rabarber
4
4
  module Inputs
5
5
  class Base
6
- def initialize(value, optional: false, error: Rabarber::InvalidArgumentError, message: nil)
6
+ def initialize(value, optional: false, error: nil, message: nil)
7
7
  @value = value
8
8
  @optional = optional
9
9
  @error = error
@@ -22,8 +22,11 @@ module Rabarber
22
22
  raise NotImplementedError
23
23
  end
24
24
 
25
+ def default_error = Rabarber::InvalidArgumentError
26
+ def default_message = nil
27
+
25
28
  def raise_error
26
- raise @error, @message
29
+ raise @error || default_error, @message || default_message
27
30
  end
28
31
  end
29
32
  end
@@ -5,7 +5,9 @@ module Rabarber
5
5
  class Boolean < Rabarber::Inputs::Base
6
6
  private
7
7
 
8
- def validate_and_normalize = @value == true || @value == false ? @value : raise_error
8
+ def default_message = "Expected a boolean, got `#{@value.inspect}`"
9
+
10
+ def validate_and_normalize = @value.equal?(true) || @value.equal?(false) ? @value : raise_error
9
11
  end
10
12
  end
11
13
  end
@@ -10,20 +10,19 @@ module Rabarber
10
10
  when Class
11
11
  { context_type: context.to_s, context_id: nil }
12
12
  when ActiveRecord::Base
13
- raise_error unless context.persisted?
14
- { context_type: context.class.to_s, context_id: context.public_send(context.class.primary_key) }
15
- else
16
- context
13
+ { context_type: context.class.to_s, context_id: context.id }
17
14
  end
18
15
  end
19
16
 
20
17
  private
21
18
 
19
+ def default_error = Rabarber::InvalidContextError
20
+ def default_message = "Expected an instance of ActiveRecord model, a Class, or nil, got `#{@value.inspect}`"
21
+
22
22
  def validate_and_normalize
23
23
  return @value if @value.nil?
24
24
  return @value if @value.is_a?(Class)
25
- return @value if @value.is_a?(ActiveRecord::Base)
26
- return @value if @value in { context_type: String | nil, context_id: String | Integer | nil }
25
+ return @value if @value.is_a?(ActiveRecord::Base) && @value.persisted?
27
26
 
28
27
  raise_error
29
28
  end
@@ -4,15 +4,10 @@ module Rabarber
4
4
  module Inputs
5
5
  module Contexts
6
6
  class Authorizational < Rabarber::Inputs::Context
7
- def resolve
8
- result = process
9
- return result if result.is_a?(Symbol) || result.is_a?(Proc)
10
-
11
- super
12
- end
13
-
14
7
  private
15
8
 
9
+ def default_message = "Expected a Class, an instance of ActiveRecord model, a symbol, a string, or a proc, got `#{@value.inspect}`"
10
+
16
11
  def validate_and_normalize
17
12
  return @value if @value.is_a?(Proc)
18
13
  return @value.to_sym if (@value.is_a?(String) || @value.is_a?(Symbol)) && @value.present?
@@ -5,10 +5,12 @@ module Rabarber
5
5
  class DynamicRule < Rabarber::Inputs::Base
6
6
  private
7
7
 
8
+ def default_message = "Expected a symbol, a string, or a proc, got `#{@value.inspect}`"
9
+
8
10
  def validate_and_normalize
9
11
  return @value if @value.is_a?(Proc)
10
12
 
11
- Rabarber::Inputs::NonEmptySymbol.new(@value, error: @error, message: @message).process
13
+ Rabarber::Inputs::NonEmptySymbol.new(@value, error: @error || default_error, message: @message || default_message).process
12
14
  end
13
15
  end
14
16
  end
@@ -5,6 +5,8 @@ module Rabarber
5
5
  class NonEmptyString < Rabarber::Inputs::Base
6
6
  private
7
7
 
8
+ def default_message = "Expected a non-empty string, got `#{@value.inspect}`"
9
+
8
10
  def validate_and_normalize
9
11
  @value.is_a?(String) && @value.present? ? @value : raise_error
10
12
  end
@@ -5,6 +5,8 @@ module Rabarber
5
5
  class NonEmptySymbol < Rabarber::Inputs::Base
6
6
  private
7
7
 
8
+ def default_message = "Expected a symbol or a string, got `#{@value.inspect}`"
9
+
8
10
  def validate_and_normalize
9
11
  raise_error unless (@value.is_a?(String) || @value.is_a?(Symbol)) && @value.present?
10
12
 
@@ -5,6 +5,8 @@ module Rabarber
5
5
  class Role < Rabarber::Inputs::Base
6
6
  private
7
7
 
8
+ def default_message = "Expected a symbol or a string containing only lowercase letters, numbers, and underscores, got `#{@value.inspect}`"
9
+
8
10
  def validate_and_normalize
9
11
  raise_error unless (@value.is_a?(String) || @value.is_a?(Symbol)) && @value.to_s.match?(/\A[a-z0-9_]+\z/)
10
12
 
@@ -5,9 +5,11 @@ module Rabarber
5
5
  class Roles < Rabarber::Inputs::Base
6
6
  private
7
7
 
8
+ def default_message = "Expected an array of symbols or strings containing only lowercase letters, numbers, and underscores, got `#{@value.inspect}`"
9
+
8
10
  def validate_and_normalize
9
11
  Array(@value).map do |role|
10
- Rabarber::Inputs::Role.new(role, error: @error, message: @message).process
12
+ Rabarber::Inputs::Role.new(role, error: @error || default_error, message: @message || default_message).process
11
13
  end
12
14
  end
13
15
  end
@@ -9,113 +9,105 @@ module Rabarber
9
9
 
10
10
  @@included = name
11
11
 
12
- has_and_belongs_to_many :rabarber_roles, class_name: "Rabarber::Role",
13
- foreign_key: "roleable_id",
14
- join_table: "rabarber_roles_roleables"
12
+ has_many :rabarber_role_assignments, class_name: "Rabarber::RoleAssignment", foreign_key: "roleable_id", inverse_of: :roleable, dependent: :delete_all
13
+ has_many :rabarber_roles, through: :rabarber_role_assignments, source: :role
15
14
 
16
15
  scope :with_role, -> (*role_names, context: nil) {
17
16
  joins(:rabarber_roles).where(
18
- rabarber_roles: { name: process_role_names(role_names), **process_context(context) }
17
+ rabarber_roles: { name: Rabarber::Inputs::Roles.new(role_names).process, **Rabarber::Inputs::Context.new(context).resolve }
19
18
  ).distinct
20
19
  }
21
-
22
- class << self
23
- def process_role_names(role_names)
24
- Rabarber::Inputs::Roles.new(
25
- role_names,
26
- message: "Expected an array of symbols or strings containing only lowercase letters, numbers, and underscores, got #{role_names.inspect}"
27
- ).process
28
- end
29
-
30
- def process_context(context)
31
- Rabarber::Inputs::Context.new(
32
- context,
33
- error: Rabarber::InvalidContextError,
34
- message: "Expected an instance of ActiveRecord model, a Class, or nil, got #{context.inspect}"
35
- ).resolve
36
- end
37
- end
38
- delegate :process_role_names, :process_context, to: :class, private: true
39
20
  end
40
21
 
41
22
  def roles(context: nil)
42
- processed_context = process_context(context)
43
- Rabarber::Core::Cache.fetch(roleable_id, processed_context) { rabarber_roles.list(context: processed_context) }
23
+ cached_roles(Rabarber::Inputs::Context.new(context).resolve)
44
24
  end
45
25
 
46
26
  def all_roles
47
- Rabarber::Core::Cache.fetch(roleable_id, :all) { rabarber_roles.list_all }
27
+ Rabarber::Core::Cache.fetch(id, :all) { assigned_roles.list_all }
48
28
  end
49
29
 
50
30
  def has_role?(*role_names, context: nil)
51
- processed_context = process_context(context)
52
- processed_roles = process_role_names(role_names)
53
- roles(context: processed_context).intersect?(processed_roles)
31
+ processed_context = Rabarber::Inputs::Context.new(context).resolve
32
+ processed_roles = Rabarber::Inputs::Roles.new(role_names).process
33
+ cached_roles(processed_context).intersect?(processed_roles)
54
34
  end
55
35
 
56
36
  def assign_roles(*role_names, context: nil, create_new: true)
57
- processed_role_names = process_role_names(role_names)
58
- processed_context = process_context(context)
37
+ processed_create_new = Rabarber::Inputs::Boolean.new(create_new).process
38
+ warn_if_unsaved_roleable(:assign_roles)
59
39
 
60
- create_new_roles(processed_role_names, context: processed_context) if create_new
40
+ processed_role_names = Rabarber::Inputs::Roles.new(role_names).process
41
+ processed_context = Rabarber::Inputs::Context.new(context).resolve
61
42
 
62
- roles_to_assign = Rabarber::Role.where(
63
- name: (processed_role_names - rabarber_roles.list(context: processed_context)), **processed_context
64
- )
43
+ Rabarber::Role.register_missing(processed_role_names, processed_context) if processed_create_new
44
+
45
+ role_ids = Rabarber::Role.where(
46
+ name: (processed_role_names - assigned_roles.names_in(processed_context)), **processed_context
47
+ ).ids
65
48
 
66
- if roles_to_assign.any?
67
- begin
68
- rabarber_roles << roles_to_assign
69
- rescue ActiveRecord::RecordNotUnique
49
+ if role_ids.any?
50
+ if persisted?
51
+ rabarber_role_assignments.insert_all(role_ids.map { { role_id: _1 } })
70
52
  rabarber_roles.reset
53
+ else
54
+ rabarber_roles << Rabarber::Role.where(id: role_ids)
71
55
  end
72
56
  delete_roleable_cache(contexts: [processed_context])
73
57
  end
74
58
 
75
- roles(context: processed_context)
59
+ cached_roles(processed_context)
76
60
  end
77
61
 
78
62
  def revoke_roles(*role_names, context: nil)
79
- processed_role_names = process_role_names(role_names)
80
- processed_context = process_context(context)
63
+ warn_if_unsaved_roleable(:revoke_roles)
81
64
 
82
- roles_to_revoke = Rabarber::Role.where(
83
- name: processed_role_names.intersection(rabarber_roles.list(context: processed_context)), **processed_context
84
- )
65
+ processed_role_names = Rabarber::Inputs::Roles.new(role_names).process
66
+ processed_context = Rabarber::Inputs::Context.new(context).resolve
85
67
 
86
- if roles_to_revoke.any?
87
- self.rabarber_roles -= roles_to_revoke
68
+ if rabarber_role_assignments.where(role: Rabarber::Role.where(name: processed_role_names, **processed_context)).delete_all.positive?
69
+ rabarber_roles.reset
88
70
  delete_roleable_cache(contexts: [processed_context])
89
71
  end
90
72
 
91
- roles(context: processed_context)
73
+ cached_roles(processed_context)
92
74
  end
93
75
 
94
76
  def revoke_all_roles
95
- return [] if rabarber_roles.none?
77
+ warn_if_unsaved_roleable(:revoke_all_roles)
96
78
 
97
- contexts = all_roles.keys.map { process_context(_1) }
79
+ roles = assigned_roles.pluck(:id, :context_type, :context_id)
98
80
 
99
- rabarber_roles.clear
81
+ rabarber_role_assignments.where(role_id: roles.map(&:first)).delete_all
82
+ rabarber_role_assignments.reset
83
+ rabarber_roles.reset
100
84
 
101
- delete_roleable_cache(contexts:)
85
+ delete_roleable_cache(contexts: roles.map { |_, context_type, context_id| { context_type:, context_id: } }.uniq) if roles.any?
102
86
 
103
87
  []
104
88
  end
105
89
 
106
90
  private
107
91
 
108
- def create_new_roles(role_names, context:)
109
- new_roles = role_names - Rabarber.roles(context:)
110
- new_roles.each { |role_name| Rabarber::Role.register(role_name, context:) }
92
+ def warn_if_unsaved_roleable(method_name)
93
+ return unless new_record?
94
+
95
+ Rabarber.deprecator.warn(
96
+ "Calling ##{method_name} on an unsaved record is deprecated. Save the record before managing roles.",
97
+ caller_locations(2)
98
+ )
111
99
  end
112
100
 
113
- def delete_roleable_cache(contexts:)
114
- Rabarber::Core::Cache.delete(*contexts.map { [roleable_id, _1] }, [roleable_id, :all])
101
+ def assigned_roles
102
+ Rabarber::Role.where(id: rabarber_role_assignments.select(:role_id))
115
103
  end
116
104
 
117
- def roleable_id
118
- public_send(self.class.primary_key)
105
+ def cached_roles(processed_context)
106
+ Rabarber::Core::Cache.fetch(id, processed_context) { assigned_roles.names_in(processed_context) }
107
+ end
108
+
109
+ def delete_roleable_cache(contexts:)
110
+ Rabarber::Core::Cache.delete(*contexts.map { [id, _1] }, [id, :all])
119
111
  end
120
112
  end
121
113
  end
@@ -1,12 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_support/core_ext/module/deprecation"
4
+
3
5
  module Rabarber
4
6
  module RoleManagement
5
7
  def roles(context: nil) = Rabarber::Role.list(context:)
6
8
  def all_roles = Rabarber::Role.list_all
7
- def create_role(name, context: nil) = Rabarber::Role.register(name, context:)
9
+ def create_role(name, context: nil) = Rabarber::Role.deprecated_register(name, context:)
10
+ def create_roles(*names, context: nil) = Rabarber::Role.register(names, context:)
8
11
  def rename_role(old_name, new_name, context: nil, force: false) = Rabarber::Role.amend(old_name, new_name, context:, force:)
9
- def delete_role(name, context: nil, force: false) = Rabarber::Role.drop(name, context:, force:)
12
+ def delete_role(name, context: nil, force: false) = Rabarber::Role.deprecated_drop(name, context:, force:)
13
+ def delete_roles(*names, context: nil, force: false) = Rabarber::Role.drop(names, context:, force:)
10
14
  def prune = Rabarber::Role.prune # rubocop:disable Rails/Delegate
15
+
16
+ deprecate create_role: :create_roles, delete_role: :delete_roles, deprecator: Rabarber.deprecator
11
17
  end
12
18
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rabarber
4
- VERSION = "6.0.3"
4
+ VERSION = "6.1.0"
5
5
  end
data/lib/rabarber.rb CHANGED
@@ -25,6 +25,12 @@ module Rabarber
25
25
  class InvalidContextError < Rabarber::InvalidArgumentError; end
26
26
  class NotFoundError < Rabarber::Error; end
27
27
 
28
+ DEPRECATOR = ActiveSupport::Deprecation.new("7.0", "Rabarber")
29
+
30
+ def self.deprecator
31
+ DEPRECATOR
32
+ end
33
+
28
34
  delegate :configure, to: Rabarber::Configuration
29
35
  module_function :configure
30
36
  end
@@ -37,11 +43,10 @@ require_relative "rabarber/controllers/concerns/authorization"
37
43
  require_relative "rabarber/helpers/helpers"
38
44
  require_relative "rabarber/helpers/migration_helpers"
39
45
  require_relative "rabarber/models/concerns/roleable"
40
- require_relative "rabarber/models/role"
41
46
  require_relative "rabarber/models/role_management"
42
47
 
43
48
  Rabarber.extend Rabarber::RoleManagement
44
49
 
45
50
  require_relative "rabarber/core/permissions"
46
51
 
47
- require_relative "rabarber/railtie"
52
+ require_relative "rabarber/engine"
data/rabarber.gemspec CHANGED
@@ -7,14 +7,13 @@ Gem::Specification.new do |spec|
7
7
  spec.version = Rabarber::VERSION
8
8
  spec.authors = ["enjaku4", "trafium"]
9
9
  spec.email = ["contact@brownbox.dev"]
10
- spec.homepage = "https://github.com/enjaku4/rabarber"
10
+ spec.homepage = "https://github.com/brownboxdev/rabarber-authorization"
11
11
  spec.metadata["homepage_uri"] = spec.homepage
12
12
  spec.metadata["source_code_uri"] = spec.homepage
13
13
  spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
14
14
  spec.metadata["bug_tracker_uri"] = "#{spec.homepage}/issues"
15
15
  spec.metadata["documentation_uri"] = "#{spec.homepage}/blob/main/README.md"
16
16
  spec.metadata["mailing_list_uri"] = "#{spec.homepage}/discussions"
17
- spec.metadata["funding_uri"] = "https://github.com/sponsors/enjaku4"
18
17
  spec.metadata["rubygems_mfa_required"] = "true"
19
18
  spec.summary = "Simple role-based authorization library for Ruby on Rails"
20
19
  spec.description = "Simple role-based authorization for Rails applications with multi-tenancy support"
@@ -23,7 +22,7 @@ Gem::Specification.new do |spec|
23
22
 
24
23
  spec.files = [
25
24
  "rabarber.gemspec", "README.md", "CHANGELOG.md", "LICENSE.txt"
26
- ] + Dir.glob("lib/**/*")
25
+ ] + Dir.glob("{app,lib}/**/*")
27
26
 
28
27
  spec.require_paths = ["lib"]
29
28
 
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: 6.0.3
4
+ version: 6.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - enjaku4
@@ -41,6 +41,8 @@ files:
41
41
  - CHANGELOG.md
42
42
  - LICENSE.txt
43
43
  - README.md
44
+ - app/models/rabarber/role.rb
45
+ - app/models/rabarber/role_assignment.rb
44
46
  - lib/generators/rabarber/roles_generator.rb
45
47
  - lib/generators/rabarber/templates/create_rabarber_roles.rb.erb
46
48
  - lib/rabarber.rb
@@ -51,6 +53,7 @@ files:
51
53
  - lib/rabarber/core/permissions.rb
52
54
  - lib/rabarber/core/roleable.rb
53
55
  - lib/rabarber/core/rule.rb
56
+ - lib/rabarber/engine.rb
54
57
  - lib/rabarber/helpers/helpers.rb
55
58
  - lib/rabarber/helpers/migration_helpers.rb
56
59
  - lib/rabarber/inputs/base.rb
@@ -64,22 +67,19 @@ files:
64
67
  - lib/rabarber/inputs/role.rb
65
68
  - lib/rabarber/inputs/roles.rb
66
69
  - lib/rabarber/models/concerns/roleable.rb
67
- - lib/rabarber/models/role.rb
68
70
  - lib/rabarber/models/role_management.rb
69
- - lib/rabarber/railtie.rb
70
71
  - lib/rabarber/version.rb
71
72
  - rabarber.gemspec
72
- homepage: https://github.com/enjaku4/rabarber
73
+ homepage: https://github.com/brownboxdev/rabarber-authorization
73
74
  licenses:
74
75
  - MIT
75
76
  metadata:
76
- homepage_uri: https://github.com/enjaku4/rabarber
77
- source_code_uri: https://github.com/enjaku4/rabarber
78
- changelog_uri: https://github.com/enjaku4/rabarber/blob/main/CHANGELOG.md
79
- bug_tracker_uri: https://github.com/enjaku4/rabarber/issues
80
- documentation_uri: https://github.com/enjaku4/rabarber/blob/main/README.md
81
- mailing_list_uri: https://github.com/enjaku4/rabarber/discussions
82
- funding_uri: https://github.com/sponsors/enjaku4
77
+ homepage_uri: https://github.com/brownboxdev/rabarber-authorization
78
+ source_code_uri: https://github.com/brownboxdev/rabarber-authorization
79
+ changelog_uri: https://github.com/brownboxdev/rabarber-authorization/blob/main/CHANGELOG.md
80
+ bug_tracker_uri: https://github.com/brownboxdev/rabarber-authorization/issues
81
+ documentation_uri: https://github.com/brownboxdev/rabarber-authorization/blob/main/README.md
82
+ mailing_list_uri: https://github.com/brownboxdev/rabarber-authorization/discussions
83
83
  rubygems_mfa_required: 'true'
84
84
  rdoc_options: []
85
85
  require_paths:
@@ -1,126 +0,0 @@
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
- class << self
10
- def list(context: nil)
11
- where(process_context(context)).pluck(:name).map(&:to_sym)
12
- end
13
-
14
- def list_all
15
- roles = where(context_id: nil).to_a + where.not(context_id: nil).includes(:context).to_a
16
-
17
- roles.each_with_object({}) do |role, hash|
18
- (hash[role.context] ||= []) << role.name.to_sym
19
- rescue ActiveRecord::RecordNotFound
20
- next
21
- end
22
- rescue NameError => e
23
- raise Rabarber::NotFoundError, "Context not found: class #{e.name} may have been renamed or deleted"
24
- end
25
-
26
- def register(name, context: nil)
27
- name = process_role_name(name)
28
- processed_context = process_context(context)
29
-
30
- return false if exists?(name:, **processed_context)
31
-
32
- !!create!(name:, **processed_context)
33
- rescue ActiveRecord::RecordNotUnique
34
- false
35
- end
36
-
37
- def amend(old_name, new_name, context: nil, force: false)
38
- processed_context = process_context(context)
39
- role = find_by(name: process_role_name(old_name), **processed_context)
40
-
41
- raise Rabarber::NotFoundError, "Role not found" unless role
42
-
43
- name = process_role_name(new_name)
44
-
45
- return false if exists?(name:, **processed_context) || role.roleables.exists? && !force
46
-
47
- role.update!(name:)
48
-
49
- delete_roleables_cache(role.roleables.pluck(:id), context: processed_context)
50
-
51
- true
52
- rescue ActiveRecord::RecordNotUnique
53
- false
54
- end
55
-
56
- def drop(name, context: nil, force: false)
57
- processed_context = process_context(context)
58
- role = find_by(name: process_role_name(name), **processed_context)
59
-
60
- raise Rabarber::NotFoundError, "Role not found" unless role
61
-
62
- return false if role.roleables.exists? && !force
63
-
64
- roleable_ids = role.roleables.pluck(:id)
65
-
66
- role.destroy!
67
-
68
- delete_roleables_cache(roleable_ids, context: processed_context)
69
-
70
- true
71
- end
72
-
73
- def prune
74
- orphaned_roles = where.not(context_id: nil).includes(:context).filter_map do |role|
75
- !role.context
76
- rescue ActiveRecord::RecordNotFound
77
- role.id
78
- end
79
-
80
- return if orphaned_roles.empty?
81
-
82
- ActiveRecord::Base.transaction do
83
- ActiveRecord::Base.connection.execute(
84
- ActiveRecord::Base.sanitize_sql(
85
- ["DELETE FROM rabarber_roles_roleables WHERE role_id IN (?)", orphaned_roles]
86
- )
87
- )
88
- where(id: orphaned_roles).delete_all
89
- end
90
-
91
- Rabarber::Core::Cache.clear
92
- end
93
-
94
- private
95
-
96
- def delete_roleables_cache(roleable_ids, context:)
97
- Rabarber::Core::Cache.delete(*roleable_ids.flat_map { [[_1, context], [_1, :all]] })
98
- end
99
-
100
- def process_role_name(name)
101
- Rabarber::Inputs::Role.new(
102
- name,
103
- message: "Expected a symbol or a string containing only lowercase letters, numbers, and underscores, got #{name.inspect}"
104
- ).process
105
- end
106
-
107
- def process_context(context)
108
- Rabarber::Inputs::Context.new(
109
- context,
110
- error: Rabarber::InvalidContextError,
111
- message: "Expected an instance of ActiveRecord model, a Class, or nil, got #{context.inspect}"
112
- ).resolve
113
- end
114
- end
115
-
116
- def context
117
- return context_type.constantize if context_type.present? && context_id.blank?
118
-
119
- record = super
120
-
121
- raise ActiveRecord::RecordNotFound.new(nil, context_type, nil, context_id) if context_id.present? && !record
122
-
123
- record
124
- end
125
- end
126
- end