rabarber 6.0.2 → 6.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +33 -4
- data/README.md +49 -20
- 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 +3 -5
- data/lib/rabarber/core/rule.rb +7 -13
- data/lib/rabarber/engine.rb +31 -0
- 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 +13 -13
- data/lib/rabarber/models/role.rb +0 -124
- data/lib/rabarber/railtie.rb +0 -49
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,32 @@
|
|
|
1
|
+
## v6.1.0
|
|
2
|
+
|
|
3
|
+
### Features:
|
|
4
|
+
|
|
5
|
+
- Added `Rabarber.create_roles` and `Rabarber.delete_roles` to create and delete several roles at once
|
|
6
|
+
- Added support for binary UUID primary keys in MySQL
|
|
7
|
+
|
|
8
|
+
### Bugs:
|
|
9
|
+
|
|
10
|
+
- Fixed a race condition where assigning several roles at once could assign none of them
|
|
11
|
+
- Fixed context migration helpers raising when no roles exist in the context
|
|
12
|
+
- Fixed the generated migration creating a broken foreign key when the user table's primary key is not `id`
|
|
13
|
+
- Fixed roles assigned or revoked inside a rolled back transaction staying cached
|
|
14
|
+
- Fixed concurrent role creation or renaming discarding the surrounding transaction
|
|
15
|
+
- Fixed the generated migration not working with UUID primary keys on some databases
|
|
16
|
+
- Fixed roles assigned during `revoke_all_roles` staying cached after being revoked
|
|
17
|
+
|
|
18
|
+
### Misc:
|
|
19
|
+
|
|
20
|
+
- Deprecated calling `assign_roles`, `revoke_roles`, or `revoke_all_roles` on an unsaved record
|
|
21
|
+
- Deprecated `Rabarber.create_role` and `Rabarber.delete_role`
|
|
22
|
+
- The generator now takes the primary key type from the existing user table, making `--uuid` unnecessary
|
|
23
|
+
|
|
24
|
+
## v6.0.3
|
|
25
|
+
|
|
26
|
+
### Bugs:
|
|
27
|
+
|
|
28
|
+
- Fixed `Rabarber.all_roles` raising `Rabarber::NotFoundError` when a role's context is a class that is not an ActiveRecord model
|
|
29
|
+
|
|
1
30
|
## v6.0.2
|
|
2
31
|
|
|
3
32
|
### Bugs:
|
|
@@ -43,7 +72,7 @@ On databases without partial index support, such as MySQL, uniqueness of global
|
|
|
43
72
|
- Optimized cache key generation
|
|
44
73
|
- Dropped some unnecessary dependencies
|
|
45
74
|
|
|
46
|
-
To upgrade to v6.0.0, please refer to the [migration guide](https://github.com/
|
|
75
|
+
To upgrade to v6.0.0, please refer to the [migration guide](https://github.com/brownboxdev/rabarber-authorization/discussions/101)
|
|
47
76
|
|
|
48
77
|
## v5.2.5
|
|
49
78
|
|
|
@@ -129,7 +158,7 @@ To upgrade to v6.0.0, please refer to the [migration guide](https://github.com/e
|
|
|
129
158
|
- Added `with_authorization` method for more granular authorization control
|
|
130
159
|
- `Rabarber::Role.rename` and `Rabarber::Role.remove` now require the role to exist
|
|
131
160
|
|
|
132
|
-
To upgrade to v5.0.0, please refer to the [migration guide](https://github.com/
|
|
161
|
+
To upgrade to v5.0.0, please refer to the [migration guide](https://github.com/brownboxdev/rabarber-authorization/discussions/77)
|
|
133
162
|
|
|
134
163
|
### Features:
|
|
135
164
|
|
|
@@ -234,7 +263,7 @@ To upgrade to v5.0.0, please refer to the [migration guide](https://github.com/e
|
|
|
234
263
|
|
|
235
264
|
- Changed Rabarber roles table structure
|
|
236
265
|
|
|
237
|
-
To upgrade to v3.0.0, please refer to the [migration guide](https://github.com/
|
|
266
|
+
To upgrade to v3.0.0, please refer to the [migration guide](https://github.com/brownboxdev/rabarber-authorization/discussions/58)
|
|
238
267
|
|
|
239
268
|
### Features:
|
|
240
269
|
|
|
@@ -258,7 +287,7 @@ To upgrade to v3.0.0, please refer to the [migration guide](https://github.com/e
|
|
|
258
287
|
- Replaced `when_unauthorized` configuration option with an overridable controller method
|
|
259
288
|
- Renamed `Rabarber::Role.assignees_for` method to `Rabarber::Role.assignees`
|
|
260
289
|
|
|
261
|
-
To upgrade to v2.0.0, please refer to the [migration guide](https://github.com/
|
|
290
|
+
To upgrade to v2.0.0, please refer to the [migration guide](https://github.com/brownboxdev/rabarber-authorization/discussions/52)
|
|
262
291
|
|
|
263
292
|
### Features:
|
|
264
293
|
|
data/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
# Rabarber: Simple
|
|
1
|
+
# Rabarber: Simple Authorization for Rails
|
|
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
|
-
|
|
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
|
|
@@ -3,22 +3,20 @@
|
|
|
3
3
|
module Rabarber
|
|
4
4
|
module Core
|
|
5
5
|
module Roleable
|
|
6
|
+
private
|
|
7
|
+
|
|
6
8
|
def roleable
|
|
7
9
|
current_roleable = send(Rabarber::Configuration.current_user_method)
|
|
8
10
|
|
|
9
11
|
unless current_roleable.is_a?(Rabarber::Configuration.user_model)
|
|
10
12
|
raise(
|
|
11
13
|
Rabarber::Error,
|
|
12
|
-
"Expected `#{Rabarber::Configuration.current_user_method}` to return an instance of #{Rabarber::Configuration.user_model_name}, got
|
|
14
|
+
"Expected `#{Rabarber::Configuration.current_user_method}` to return an instance of #{Rabarber::Configuration.user_model_name}, got `#{current_roleable.inspect}`"
|
|
13
15
|
)
|
|
14
16
|
end
|
|
15
17
|
|
|
16
18
|
current_roleable
|
|
17
19
|
end
|
|
18
|
-
|
|
19
|
-
def roleable_roles(context: nil)
|
|
20
|
-
roleable.roles(context:)
|
|
21
|
-
end
|
|
22
20
|
end
|
|
23
21
|
end
|
|
24
22
|
end
|
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
|