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 +4 -4
- data/CHANGELOG.md +27 -4
- data/README.md +48 -19
- data/app/models/rabarber/role.rb +153 -0
- data/app/models/rabarber/role_assignment.rb +10 -0
- data/lib/generators/rabarber/roles_generator.rb +6 -2
- data/lib/generators/rabarber/templates/create_rabarber_roles.rb.erb +4 -4
- data/lib/rabarber/configuration.rb +4 -4
- data/lib/rabarber/controllers/concerns/authorization.rb +5 -24
- data/lib/rabarber/core/cache.rb +19 -9
- data/lib/rabarber/core/roleable.rb +1 -1
- data/lib/rabarber/core/rule.rb +7 -13
- data/lib/rabarber/{railtie.rb → engine.rb} +6 -7
- data/lib/rabarber/helpers/migration_helpers.rb +5 -16
- data/lib/rabarber/inputs/base.rb +5 -2
- data/lib/rabarber/inputs/boolean.rb +3 -1
- data/lib/rabarber/inputs/context.rb +5 -6
- data/lib/rabarber/inputs/contexts/authorizational.rb +2 -7
- data/lib/rabarber/inputs/dynamic_rule.rb +3 -1
- data/lib/rabarber/inputs/non_empty_string.rb +2 -0
- data/lib/rabarber/inputs/non_empty_symbol.rb +2 -0
- data/lib/rabarber/inputs/role.rb +2 -0
- data/lib/rabarber/inputs/roles.rb +3 -1
- data/lib/rabarber/models/concerns/roleable.rb +50 -58
- data/lib/rabarber/models/role_management.rb +8 -2
- data/lib/rabarber/version.rb +1 -1
- data/lib/rabarber.rb +7 -2
- data/rabarber.gemspec +2 -3
- metadata +11 -11
- data/lib/rabarber/models/role.rb +0 -126
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d6a04e1ab84c89a6ab0271fb9c75021c6a5b0ecf042ae53914e58c969ed8a533
|
|
4
|
+
data.tar.gz: 4dc99eacf060c21b9a08d23a882a96ce41c3cf8675d1eae73a2dc00ee925eae5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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
|
[](http://badge.fury.io/rb/rabarber)
|
|
4
4
|
[](https://rubygems.org/gems/rabarber)
|
|
5
|
-
[](https://github.com/brownboxdev/rabarber-authorization/actions/workflows/ci.yml)
|
|
6
|
+
[](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
|
-
|
|
69
|
-
|
|
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
|
|
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
|
-
#
|
|
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
|
|
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
|
-
#
|
|
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
|
|
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/
|
|
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/
|
|
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 [
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
501
|
-
| [v2.x.x](https://github.com/
|
|
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
|
|
5
|
+
create_table :rabarber_roles do |t|
|
|
6
6
|
t.string :name, null: false
|
|
7
|
-
t.belongs_to :context, polymorphic: true, index: true
|
|
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 }
|
|
20
|
-
t.belongs_to :roleable, null: false, index: true, foreign_key: { to_table: <%= table_name.to_sym.inspect %> }
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
data/lib/rabarber/core/cache.rb
CHANGED
|
@@ -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
|
|
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
|
-
|
|
21
|
+
delete_keys(pairs)
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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
|
|
37
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
data/lib/rabarber/core/rule.rb
CHANGED
|
@@ -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:
|
|
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
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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/
|
|
3
|
+
require "rails/engine"
|
|
4
4
|
|
|
5
5
|
module Rabarber
|
|
6
|
-
class
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
data/lib/rabarber/inputs/base.rb
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module Rabarber
|
|
4
4
|
module Inputs
|
|
5
5
|
class Base
|
|
6
|
-
def initialize(value, optional: false, error:
|
|
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
|
|
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
|
-
|
|
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 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
|
|
data/lib/rabarber/inputs/role.rb
CHANGED
|
@@ -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
|
-
|
|
13
|
-
|
|
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:
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
52
|
-
processed_roles =
|
|
53
|
-
|
|
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
|
-
|
|
58
|
-
|
|
37
|
+
processed_create_new = Rabarber::Inputs::Boolean.new(create_new).process
|
|
38
|
+
warn_if_unsaved_roleable(:assign_roles)
|
|
59
39
|
|
|
60
|
-
|
|
40
|
+
processed_role_names = Rabarber::Inputs::Roles.new(role_names).process
|
|
41
|
+
processed_context = Rabarber::Inputs::Context.new(context).resolve
|
|
61
42
|
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
59
|
+
cached_roles(processed_context)
|
|
76
60
|
end
|
|
77
61
|
|
|
78
62
|
def revoke_roles(*role_names, context: nil)
|
|
79
|
-
|
|
80
|
-
processed_context = process_context(context)
|
|
63
|
+
warn_if_unsaved_roleable(:revoke_roles)
|
|
81
64
|
|
|
82
|
-
|
|
83
|
-
|
|
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
|
|
87
|
-
|
|
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
|
-
|
|
73
|
+
cached_roles(processed_context)
|
|
92
74
|
end
|
|
93
75
|
|
|
94
76
|
def revoke_all_roles
|
|
95
|
-
|
|
77
|
+
warn_if_unsaved_roleable(:revoke_all_roles)
|
|
96
78
|
|
|
97
|
-
|
|
79
|
+
roles = assigned_roles.pluck(:id, :context_type, :context_id)
|
|
98
80
|
|
|
99
|
-
|
|
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
|
|
109
|
-
|
|
110
|
-
|
|
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
|
|
114
|
-
Rabarber::
|
|
101
|
+
def assigned_roles
|
|
102
|
+
Rabarber::Role.where(id: rabarber_role_assignments.select(:role_id))
|
|
115
103
|
end
|
|
116
104
|
|
|
117
|
-
def
|
|
118
|
-
|
|
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.
|
|
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.
|
|
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
|
data/lib/rabarber/version.rb
CHANGED
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/
|
|
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/
|
|
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
|
|
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/
|
|
73
|
+
homepage: https://github.com/brownboxdev/rabarber-authorization
|
|
73
74
|
licenses:
|
|
74
75
|
- MIT
|
|
75
76
|
metadata:
|
|
76
|
-
homepage_uri: https://github.com/
|
|
77
|
-
source_code_uri: https://github.com/
|
|
78
|
-
changelog_uri: https://github.com/
|
|
79
|
-
bug_tracker_uri: https://github.com/
|
|
80
|
-
documentation_uri: https://github.com/
|
|
81
|
-
mailing_list_uri: https://github.com/
|
|
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:
|
data/lib/rabarber/models/role.rb
DELETED
|
@@ -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
|