role_fu 0.1.0 → 0.3.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/Appraisals +0 -8
- data/CHANGELOG.md +24 -1
- data/README.md +148 -36
- data/gemfiles/rails_7.2.gemfile.lock +3 -3
- data/gemfiles/rails_8.0.gemfile.lock +3 -3
- data/gemfiles/rails_8.1.gemfile.lock +3 -3
- data/lib/generators/role_fu/abilities_generator.rb +51 -0
- data/lib/generators/role_fu/audit_generator.rb +25 -0
- data/lib/generators/role_fu/job_generator.rb +15 -0
- data/lib/generators/role_fu/templates/abilities_migration.rb.erb +15 -0
- data/lib/generators/role_fu/templates/audit_migration.rb.erb +23 -0
- data/lib/generators/role_fu/templates/audit_model.rb.erb +7 -0
- data/lib/generators/role_fu/templates/cleanup_job.rb.erb +12 -0
- data/lib/generators/role_fu/templates/migration.rb.erb +2 -0
- data/lib/generators/role_fu/templates/permission.rb.erb +5 -0
- data/lib/role_fu/ability.rb +23 -0
- data/lib/role_fu/adapters/cancancan.rb +29 -0
- data/lib/role_fu/adapters/pundit.rb +28 -0
- data/lib/role_fu/cleanup.rb +16 -0
- data/lib/role_fu/configuration.rb +2 -1
- data/lib/role_fu/permission.rb +12 -0
- data/lib/role_fu/railtie.rb +11 -0
- data/lib/role_fu/resourceable.rb +35 -69
- data/lib/role_fu/role_assignment.rb +37 -0
- data/lib/role_fu/roleable.rb +94 -120
- data/lib/role_fu/version.rb +1 -1
- data/lib/role_fu.rb +19 -1
- data/lib/tasks/role_fu.rake +9 -0
- metadata +18 -7
- data/gemfiles/rails_7.0.gemfile +0 -16
- data/gemfiles/rails_7.0.gemfile.lock +0 -419
- data/gemfiles/rails_7.1.gemfile +0 -16
- data/gemfiles/rails_7.1.gemfile.lock +0 -437
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d75b379be1f3096ca3580d3b5f545c7e10e96a76ade4e1ea8ddf9df11272cf61
|
|
4
|
+
data.tar.gz: 9dda6cef944474db4c9fa70979f0a633a3ad90f51bee73faa6cc7ed2e13bfa68
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 51e073987c36ffd2c6faa06256362a75f79f1a83931fe2bc82e5f4d6750c4a355df4324c3f33dc1dcbe298d3d060b787b6d819da1b693f63edb9d4ed4dc008c5
|
|
7
|
+
data.tar.gz: 9e1b6a068bce303db00d70f538a311321fbc2ea1d1032b1522b028d0cf71835c7107589b3f194b48a0c242ba9afab49755a1213ab4a6ce2ba195a06fef8f9a10
|
data/Appraisals
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.3.0] - 2026-02-03
|
|
4
|
+
|
|
5
|
+
### BREAKING CHANGES
|
|
6
|
+
|
|
7
|
+
- **Drop Rails 7.0 and 7.1 support**: Minimum required Rails version is now 7.2+
|
|
8
|
+
- Rails 7.0 and 7.1 require `sqlite3 ~> 1.4`, which conflicts with `sqlite3 ~> 2.0`
|
|
9
|
+
- Rails 7.2+ supports `sqlite3 >= 1.6.6`, ensuring compatibility with modern sqlite3 versions
|
|
10
|
+
- Updated `activerecord` dependency from `>= 7.0` to `>= 7.2`
|
|
11
|
+
- Removed Rails 7.0 and 7.1 from CI test matrix
|
|
12
|
+
|
|
13
|
+
## [0.2.0] - 2026-02-03
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- **Temporal Roles**: Support for role expiration (`expires_at`) and automatic filtering.
|
|
18
|
+
- **Audit Logging**: Built-in generator for audit trails (`RoleAssignmentAudit`) and actor tracking (`RoleFu.with_actor`).
|
|
19
|
+
- **Role Abilities**: Granular permissions system with `Permission` model and `role_fu_can?` helper.
|
|
20
|
+
- **Metadata**: Support for attaching arbitrary JSON metadata to role assignments.
|
|
21
|
+
- **Adapters**: Seamless integration with **Pundit** and **CanCanCan**.
|
|
22
|
+
- **Permissive Mode**: Configuration option `global_roles_override` to match Rolify behavior.
|
|
23
|
+
- **Cleanup Automation**: `rake role_fu:cleanup` task and ActiveJob generator for expired roles.
|
|
24
|
+
- **Custom Aliases**: `role_fu_alias` to generate domain-specific methods (e.g., `add_group`, `in_group`).
|
|
25
|
+
|
|
3
26
|
## [0.1.0] - 2026-02-03
|
|
4
27
|
|
|
5
|
-
- Initial release: modern role management for Rails with explicit models and N+1 prevention.
|
|
28
|
+
- Initial release: modern role management for Rails with explicit models and N+1 prevention.
|
data/README.md
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
# RoleFu
|
|
2
2
|
|
|
3
|
-
RoleFu is a modern, explicit role management gem for Ruby on Rails. It is designed as a cleaner, more performant alternative to
|
|
3
|
+
RoleFu is a modern, explicit role management gem for Ruby on Rails. It is designed as a cleaner, more performant alternative to legacy role gems, providing full control over role assignments and granular permissions.
|
|
4
4
|
|
|
5
5
|
## Why RoleFu?
|
|
6
6
|
|
|
7
|
-
- **Explicit Models**:
|
|
8
|
-
- **N+1 Prevention**: Built-in support for `has_cached_role?`
|
|
9
|
-
- **Strict by Default**:
|
|
10
|
-
- **
|
|
7
|
+
- **Explicit Models**: Uses an explicit `RoleAssignment` join model instead of hidden tables, making it easy to add metadata or audit trails.
|
|
8
|
+
- **N+1 Prevention**: Built-in support for `has_cached_role?` and optimized scopes.
|
|
9
|
+
- **Strict by Default**: Resource-specific checks are strict, ensuring global roles don't accidentally leak permissions unless configured otherwise.
|
|
10
|
+
- **Advanced Features**: Supports temporal (expiring) roles, metadata, audit logging, and granular abilities.
|
|
11
|
+
- **Modern Infrastructure**: Fully compatible with Rails 7.0 through 8.1, includes Lefthook and Appraisal support.
|
|
11
12
|
|
|
12
13
|
## Installation
|
|
13
14
|
|
|
@@ -25,17 +26,18 @@ bundle install
|
|
|
25
26
|
|
|
26
27
|
### Setup
|
|
27
28
|
|
|
28
|
-
1.
|
|
29
|
+
1. **Install Configuration:**
|
|
29
30
|
```bash
|
|
30
31
|
rails generate role_fu:install
|
|
31
32
|
```
|
|
32
33
|
|
|
33
|
-
2. Generate
|
|
34
|
+
2. **Generate Models:**
|
|
35
|
+
Default names are `Role` and `RoleAssignment`, linked to the `User` model.
|
|
34
36
|
```bash
|
|
35
37
|
rails generate role_fu Role User
|
|
36
38
|
```
|
|
37
39
|
|
|
38
|
-
3. Run
|
|
40
|
+
3. **Run Migrations:**
|
|
39
41
|
```bash
|
|
40
42
|
rails db:migrate
|
|
41
43
|
```
|
|
@@ -49,10 +51,10 @@ Include `RoleFu::Roleable` in your User model (done automatically by the generat
|
|
|
49
51
|
```ruby
|
|
50
52
|
class User < ApplicationRecord
|
|
51
53
|
include RoleFu::Roleable
|
|
52
|
-
|
|
54
|
+
|
|
53
55
|
# Optional callbacks
|
|
54
56
|
role_fu_options after_add: :notify_user
|
|
55
|
-
|
|
57
|
+
|
|
56
58
|
def notify_user(role)
|
|
57
59
|
# ...
|
|
58
60
|
end
|
|
@@ -65,35 +67,122 @@ end
|
|
|
65
67
|
user = User.find(1)
|
|
66
68
|
|
|
67
69
|
# Global roles
|
|
68
|
-
user.
|
|
69
|
-
user.grant(:admin) # Alias
|
|
70
|
+
user.grant(:admin)
|
|
70
71
|
user.has_role?(:admin) # => true
|
|
71
|
-
user.
|
|
72
|
-
user.revoke(:admin) # Alias
|
|
72
|
+
user.revoke(:admin)
|
|
73
73
|
|
|
74
74
|
# Resource-specific roles
|
|
75
75
|
org = Organization.first
|
|
76
|
-
user.
|
|
76
|
+
user.grant(:manager, org)
|
|
77
77
|
user.has_role?(:manager, org) # => true
|
|
78
78
|
user.has_role?(:manager) # => false (strict check)
|
|
79
79
|
user.has_role?(:manager, :any) # => true
|
|
80
80
|
user.only_has_role?(:manager, org) # => true if this is their only role
|
|
81
|
+
```
|
|
81
82
|
|
|
82
|
-
|
|
83
|
+
#### Scopes (Finders)
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
83
86
|
User.with_role(:admin)
|
|
84
87
|
User.with_role(:manager, org)
|
|
85
88
|
User.without_role(:admin)
|
|
86
89
|
User.with_any_role(:admin, :editor)
|
|
87
90
|
User.with_all_roles(:admin, :manager)
|
|
91
|
+
```
|
|
88
92
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
### Advanced Features
|
|
96
|
+
|
|
97
|
+
#### 1. Temporal Roles (Expiration)
|
|
98
|
+
Roles can be assigned with an expiration time. They are automatically filtered out from queries once expired.
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
user.grant(:manager, org, expires_at: 1.week.from_now)
|
|
102
|
+
|
|
103
|
+
# To physically remove expired roles from the database:
|
|
104
|
+
# rake role_fu:cleanup
|
|
105
|
+
|
|
106
|
+
# Or generate a scheduled ActiveJob:
|
|
107
|
+
# rails generate role_fu:job
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
#### 2. Metadata
|
|
111
|
+
Attach arbitrary metadata to a role assignment.
|
|
112
|
+
|
|
113
|
+
```ruby
|
|
114
|
+
user.grant(:manager, org, meta: { assigned_by: current_user.id, reason: "Project lead" })
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
#### 3. Audit Log
|
|
118
|
+
Track every grant, revoke, and update (e.g., expiration extensions).
|
|
119
|
+
|
|
120
|
+
**Setup:**
|
|
121
|
+
```bash
|
|
122
|
+
rails generate role_fu:audit
|
|
123
|
+
rails db:migrate
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
**Usage:**
|
|
127
|
+
Wrap changes in `with_actor` to capture the responsible user:
|
|
128
|
+
```ruby
|
|
129
|
+
RoleFu.with_actor(current_user) do
|
|
130
|
+
user.grant(:manager, org)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Check history
|
|
134
|
+
RoleAssignmentAudit.where(user: user).last
|
|
135
|
+
# => #<RoleAssignmentAudit operation: "INSERT", whodunnit: "1", ...>
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
#### 4. Role Abilities (Permissions)
|
|
139
|
+
Attach granular permissions to roles.
|
|
140
|
+
|
|
141
|
+
**Setup:**
|
|
142
|
+
```bash
|
|
143
|
+
rails generate role_fu:abilities
|
|
144
|
+
rails db:migrate
|
|
92
145
|
```
|
|
93
146
|
|
|
94
|
-
|
|
147
|
+
**Usage:**
|
|
148
|
+
```ruby
|
|
149
|
+
# Setup permissions
|
|
150
|
+
manager_role = Role.find_by(name: "manager")
|
|
151
|
+
manager_role.permissions.create(action: "posts.edit")
|
|
152
|
+
|
|
153
|
+
# Check abilities
|
|
154
|
+
user.role_fu_can?("posts.edit") # => true
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
### Adapters (Pundit & CanCanCan)
|
|
160
|
+
|
|
161
|
+
#### CanCanCan
|
|
162
|
+
```ruby
|
|
163
|
+
class Ability
|
|
164
|
+
include CanCan::Ability
|
|
165
|
+
include RoleFu::Adapters::CanCanCan
|
|
166
|
+
|
|
167
|
+
def initialize(user)
|
|
168
|
+
role_fu_load_permissions!(user)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
```
|
|
95
172
|
|
|
96
|
-
|
|
173
|
+
#### Pundit
|
|
174
|
+
```ruby
|
|
175
|
+
class ApplicationPolicy
|
|
176
|
+
include RoleFu::Adapters::Pundit
|
|
177
|
+
end
|
|
178
|
+
```
|
|
179
|
+
*`PostPolicy#update?` will automatically check `user.role_fu_can?('posts.update')`.*
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
### Performance (N+1 Prevention)
|
|
184
|
+
|
|
185
|
+
Use `has_cached_role?` when roles are preloaded to avoid database roundtrips.
|
|
97
186
|
|
|
98
187
|
```ruby
|
|
99
188
|
users = User.includes(:roles).all
|
|
@@ -102,7 +191,7 @@ users.each do |user|
|
|
|
102
191
|
end
|
|
103
192
|
```
|
|
104
193
|
|
|
105
|
-
### Resourceable
|
|
194
|
+
### Resourceable
|
|
106
195
|
|
|
107
196
|
Include `RoleFu::Resourceable` in any model that should have roles scoped to it.
|
|
108
197
|
|
|
@@ -113,32 +202,55 @@ end
|
|
|
113
202
|
|
|
114
203
|
org = Organization.first
|
|
115
204
|
org.users_with_role(:manager)
|
|
116
|
-
org.count_users_with_role(:admin)
|
|
117
205
|
org.available_roles # ["manager", "admin"]
|
|
118
|
-
```
|
|
119
206
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
| Join Model | Implicit (HABTM) | Explicit (RoleAssignment) |
|
|
125
|
-
| Performance | Frequent N+1 | Cached role support + Optimized Scopes |
|
|
126
|
-
| Orphaned Roles | Configurable cleanup | Automatic cleanup |
|
|
127
|
-
| Scopes | `User.with_role` | `User.with_role`, `without_role`, `with_any`, `with_all` |
|
|
128
|
-
| Modern Rails | Older codebase | Optimized for Rails 7+ |
|
|
207
|
+
# Scopes
|
|
208
|
+
Organization.with_role(:manager, user)
|
|
209
|
+
Organization.without_role(:manager, user)
|
|
210
|
+
```
|
|
129
211
|
|
|
130
212
|
## Configuration
|
|
131
213
|
|
|
132
|
-
|
|
214
|
+
Customize your model names in `config/initializers/role_fu.rb`:
|
|
133
215
|
|
|
134
216
|
```ruby
|
|
135
217
|
RoleFu.configure do |config|
|
|
136
218
|
config.user_class_name = "Account"
|
|
137
219
|
config.role_class_name = "Group"
|
|
138
|
-
|
|
220
|
+
|
|
221
|
+
# Enable Rolify-style permissive checks (Global roles override resource checks)
|
|
222
|
+
config.global_roles_override = true
|
|
139
223
|
end
|
|
140
224
|
```
|
|
141
225
|
|
|
226
|
+
## Custom Aliases (e.g. Groups)
|
|
227
|
+
|
|
228
|
+
If you prefer different terminology (e.g., "Groups" instead of "Roles"), you can alias the methods in your models:
|
|
229
|
+
|
|
230
|
+
```ruby
|
|
231
|
+
class User < ApplicationRecord
|
|
232
|
+
include RoleFu::Roleable
|
|
233
|
+
role_fu_alias :group
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Now you can use:
|
|
237
|
+
user.add_group(:admin)
|
|
238
|
+
user.has_group?(:admin)
|
|
239
|
+
User.in_group(:admin) # Alias for with_group/with_role
|
|
240
|
+
User.not_in_group(:admin) # Alias for without_group/without_role
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## Migrating from Rolify
|
|
244
|
+
|
|
245
|
+
1. **Code Changes**: Replace `rolify` with `include RoleFu::Roleable` and `resourcify` with `include RoleFu::Resourceable`.
|
|
246
|
+
2. **Data Migration**:
|
|
247
|
+
```sql
|
|
248
|
+
INSERT INTO role_assignments (user_id, role_id, created_at, updated_at)
|
|
249
|
+
SELECT user_id, role_id, NOW(), NOW()
|
|
250
|
+
FROM users_roles;
|
|
251
|
+
```
|
|
252
|
+
3. **Behavior**: Set `config.global_roles_override = true` if you rely on global roles satisfying resource checks.
|
|
253
|
+
|
|
142
254
|
## License
|
|
143
255
|
|
|
144
|
-
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
256
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: ..
|
|
3
3
|
specs:
|
|
4
|
-
role_fu (0.
|
|
5
|
-
activerecord (>= 7.
|
|
4
|
+
role_fu (0.3.0)
|
|
5
|
+
activerecord (>= 7.2)
|
|
6
6
|
|
|
7
7
|
GEM
|
|
8
8
|
remote: https://rubygems.org/
|
|
@@ -389,7 +389,7 @@ CHECKSUMS
|
|
|
389
389
|
rdoc (7.1.0) sha256=494899df0706c178596ca6e1d50f1b7eb285a9b2aae715be5abd742734f17363
|
|
390
390
|
regexp_parser (2.11.3) sha256=ca13f381a173b7a93450e53459075c9b76a10433caadcb2f1180f2c741fc55a4
|
|
391
391
|
reline (0.6.3) sha256=1198b04973565b36ec0f11542ab3f5cfeeec34823f4e54cebde90968092b1835
|
|
392
|
-
role_fu (0.
|
|
392
|
+
role_fu (0.3.0)
|
|
393
393
|
rspec (3.13.2) sha256=206284a08ad798e61f86d7ca3e376718d52c0bc944626b2349266f239f820587
|
|
394
394
|
rspec-core (3.13.6) sha256=a8823c6411667b60a8bca135364351dda34cd55e44ff94c4be4633b37d828b2d
|
|
395
395
|
rspec-expectations (3.13.5) sha256=33a4d3a1d95060aea4c94e9f237030a8f9eae5615e9bd85718fe3a09e4b58836
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: ..
|
|
3
3
|
specs:
|
|
4
|
-
role_fu (0.
|
|
5
|
-
activerecord (>= 7.
|
|
4
|
+
role_fu (0.3.0)
|
|
5
|
+
activerecord (>= 7.2)
|
|
6
6
|
|
|
7
7
|
GEM
|
|
8
8
|
remote: https://rubygems.org/
|
|
@@ -385,7 +385,7 @@ CHECKSUMS
|
|
|
385
385
|
rdoc (7.1.0) sha256=494899df0706c178596ca6e1d50f1b7eb285a9b2aae715be5abd742734f17363
|
|
386
386
|
regexp_parser (2.11.3) sha256=ca13f381a173b7a93450e53459075c9b76a10433caadcb2f1180f2c741fc55a4
|
|
387
387
|
reline (0.6.3) sha256=1198b04973565b36ec0f11542ab3f5cfeeec34823f4e54cebde90968092b1835
|
|
388
|
-
role_fu (0.
|
|
388
|
+
role_fu (0.3.0)
|
|
389
389
|
rspec (3.13.2) sha256=206284a08ad798e61f86d7ca3e376718d52c0bc944626b2349266f239f820587
|
|
390
390
|
rspec-core (3.13.6) sha256=a8823c6411667b60a8bca135364351dda34cd55e44ff94c4be4633b37d828b2d
|
|
391
391
|
rspec-expectations (3.13.5) sha256=33a4d3a1d95060aea4c94e9f237030a8f9eae5615e9bd85718fe3a09e4b58836
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: ..
|
|
3
3
|
specs:
|
|
4
|
-
role_fu (0.
|
|
5
|
-
activerecord (>= 7.
|
|
4
|
+
role_fu (0.3.0)
|
|
5
|
+
activerecord (>= 7.2)
|
|
6
6
|
|
|
7
7
|
GEM
|
|
8
8
|
remote: https://rubygems.org/
|
|
@@ -387,7 +387,7 @@ CHECKSUMS
|
|
|
387
387
|
rdoc (7.1.0) sha256=494899df0706c178596ca6e1d50f1b7eb285a9b2aae715be5abd742734f17363
|
|
388
388
|
regexp_parser (2.11.3) sha256=ca13f381a173b7a93450e53459075c9b76a10433caadcb2f1180f2c741fc55a4
|
|
389
389
|
reline (0.6.3) sha256=1198b04973565b36ec0f11542ab3f5cfeeec34823f4e54cebde90968092b1835
|
|
390
|
-
role_fu (0.
|
|
390
|
+
role_fu (0.3.0)
|
|
391
391
|
rspec (3.13.2) sha256=206284a08ad798e61f86d7ca3e376718d52c0bc944626b2349266f239f820587
|
|
392
392
|
rspec-core (3.13.6) sha256=a8823c6411667b60a8bca135364351dda34cd55e44ff94c4be4633b37d828b2d
|
|
393
393
|
rspec-expectations (3.13.5) sha256=33a4d3a1d95060aea4c94e9f237030a8f9eae5615e9bd85718fe3a09e4b58836
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators/active_record"
|
|
4
|
+
|
|
5
|
+
module RoleFu
|
|
6
|
+
module Generators
|
|
7
|
+
class AbilitiesGenerator < ActiveRecord::Generators::Base
|
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
|
9
|
+
|
|
10
|
+
def generate_permission_model
|
|
11
|
+
template "permission.rb.erb", "app/models/permission.rb"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def create_migration
|
|
15
|
+
migration_template "abilities_migration.rb.erb", "db/migrate/role_fu_create_permissions.rb", migration_version: migration_version
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def inject_into_role_model
|
|
19
|
+
role_cname = RoleFu.configuration.role_class_name
|
|
20
|
+
path = "app/models/#{role_cname.underscore}.rb"
|
|
21
|
+
|
|
22
|
+
if File.exist?(path)
|
|
23
|
+
inject_into_class(path, role_cname.constantize) do
|
|
24
|
+
" has_many :permissions, dependent: :destroy\n"
|
|
25
|
+
end
|
|
26
|
+
else
|
|
27
|
+
say "Role model #{role_cname} not found. Please add 'has_many :permissions' manually."
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def inject_into_user_model
|
|
32
|
+
user_cname = RoleFu.configuration.user_class_name
|
|
33
|
+
path = "app/models/#{user_cname.underscore}.rb"
|
|
34
|
+
|
|
35
|
+
if File.exist?(path)
|
|
36
|
+
inject_into_class(path, user_cname.constantize) do
|
|
37
|
+
" include RoleFu::Ability\n"
|
|
38
|
+
end
|
|
39
|
+
else
|
|
40
|
+
say "User model #{user_cname} not found. Please add 'include RoleFu::Ability' manually."
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def migration_version
|
|
47
|
+
"[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]" if Rails::VERSION::MAJOR >= 5
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators/active_record"
|
|
4
|
+
|
|
5
|
+
module RoleFu
|
|
6
|
+
module Generators
|
|
7
|
+
class AuditGenerator < ActiveRecord::Generators::Base
|
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
|
9
|
+
|
|
10
|
+
def create_migration
|
|
11
|
+
migration_template "audit_migration.rb.erb", "db/migrate/role_fu_create_audits.rb", migration_version: migration_version
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def create_model
|
|
15
|
+
template "audit_model.rb.erb", "app/models/role_assignment_audit.rb"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def migration_version
|
|
21
|
+
"[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]" if Rails::VERSION::MAJOR >= 5
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators/base"
|
|
4
|
+
|
|
5
|
+
module RoleFu
|
|
6
|
+
module Generators
|
|
7
|
+
class JobGenerator < Rails::Generators::Base
|
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
|
9
|
+
|
|
10
|
+
def create_job
|
|
11
|
+
template "cleanup_job.rb.erb", "app/jobs/role_fu/cleanup_job.rb"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class RoleFuCreatePermissions < ActiveRecord::Migration<%= migration_version %>
|
|
4
|
+
def change
|
|
5
|
+
create_table :permissions do |t|
|
|
6
|
+
t.references :role, null: false, index: true
|
|
7
|
+
t.string :action, null: false
|
|
8
|
+
t.jsonb :conditions, default: {}
|
|
9
|
+
|
|
10
|
+
t.timestamps
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
add_index :permissions, [:role_id, :action]
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class RoleFuCreateAudits < ActiveRecord::Migration<%= migration_version %>
|
|
4
|
+
def change
|
|
5
|
+
create_table :role_assignment_audits do |t|
|
|
6
|
+
t.references :user, index: true
|
|
7
|
+
t.references :role, index: true
|
|
8
|
+
t.references :role_assignment, index: true
|
|
9
|
+
t.string :operation, null: false # INSERT, UPDATE, DELETE
|
|
10
|
+
t.string :whodunnit
|
|
11
|
+
t.jsonb :meta_snapshot
|
|
12
|
+
t.datetime :expires_at_snapshot
|
|
13
|
+
|
|
14
|
+
t.timestamps
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# If using PostgreSQL, you can add a trigger here to automatically populate this table.
|
|
18
|
+
# Example (Conceptual):
|
|
19
|
+
# execute <<-SQL
|
|
20
|
+
# CREATE TRIGGER ...
|
|
21
|
+
# SQL
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RoleFu
|
|
4
|
+
class CleanupJob < ApplicationJob
|
|
5
|
+
queue_as :default
|
|
6
|
+
|
|
7
|
+
def perform
|
|
8
|
+
result = RoleFu::Cleanup.call
|
|
9
|
+
Rails.logger.info("RoleFu::CleanupJob: Deleted #{result[:deleted]} expired assignments.")
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -12,6 +12,8 @@ class RoleFuCreate<%= table_name.camelize %> < ActiveRecord::Migration<%= migrat
|
|
|
12
12
|
create_table :<%= table_name.singularize %>_assignments do |t|
|
|
13
13
|
t.references :<%= user_cname.underscore %>, index: true
|
|
14
14
|
t.references :<%= table_name.singularize %>, index: true
|
|
15
|
+
t.datetime :expires_at, index: true
|
|
16
|
+
t.jsonb :meta, default: {}
|
|
15
17
|
|
|
16
18
|
t.timestamps
|
|
17
19
|
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RoleFu
|
|
4
|
+
module Ability
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
def role_fu_can?(action, _resource = nil)
|
|
8
|
+
role_fu_permissions.include?(action.to_s)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def role_fu_permissions
|
|
12
|
+
return @_role_fu_permissions if defined?(@_role_fu_permissions) && @_role_fu_permissions
|
|
13
|
+
|
|
14
|
+
permission_class = "Permission".safe_constantize
|
|
15
|
+
return Set.new unless permission_class
|
|
16
|
+
|
|
17
|
+
scope = roles
|
|
18
|
+
scope = filter_expired(scope) if respond_to?(:filter_expired, true)
|
|
19
|
+
|
|
20
|
+
@_role_fu_permissions = scope.joins(:permissions).pluck("permissions.action").map(&:to_s).to_set
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RoleFu
|
|
4
|
+
module Adapters
|
|
5
|
+
module CanCanCan
|
|
6
|
+
# Mixin for CanCan::Ability class
|
|
7
|
+
def role_fu_load_permissions!(user)
|
|
8
|
+
return unless user.respond_to?(:role_fu_permissions)
|
|
9
|
+
|
|
10
|
+
user.role_fu_permissions.each do |action|
|
|
11
|
+
# Action format: "posts.update" (resource.action) or "manage_all"
|
|
12
|
+
parts = action.split(".")
|
|
13
|
+
|
|
14
|
+
if parts.size == 2
|
|
15
|
+
subject_name, rule = parts
|
|
16
|
+
begin
|
|
17
|
+
subject_class = subject_name.classify.constantize
|
|
18
|
+
can rule.to_sym, subject_class
|
|
19
|
+
rescue NameError
|
|
20
|
+
can rule.to_sym, subject_name.to_sym
|
|
21
|
+
end
|
|
22
|
+
else
|
|
23
|
+
can action.to_sym, :all
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RoleFu
|
|
4
|
+
module Adapters
|
|
5
|
+
module Pundit
|
|
6
|
+
# Mixin for Pundit Policies (e.g. ApplicationPolicy)
|
|
7
|
+
def method_missing(method, *args, &block)
|
|
8
|
+
if method.to_s.end_with?("?")
|
|
9
|
+
action = method.to_s.delete_suffix("?")
|
|
10
|
+
|
|
11
|
+
# Infer resource from policy name: PostPolicy -> "posts"
|
|
12
|
+
resource_name = self.class.to_s.gsub("Policy", "").underscore.pluralize
|
|
13
|
+
|
|
14
|
+
# Check "posts.update"
|
|
15
|
+
permission = "#{resource_name}.#{action}"
|
|
16
|
+
|
|
17
|
+
return true if user.role_fu_can?(permission)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
super
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def respond_to_missing?(method, include_private = false)
|
|
24
|
+
method.to_s.end_with?("?") || super
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RoleFu
|
|
4
|
+
class Cleanup
|
|
5
|
+
def self.call
|
|
6
|
+
assignment_class = RoleFu.configuration.role_assignment_class_name.constantize
|
|
7
|
+
|
|
8
|
+
if assignment_class.column_names.include?("expires_at")
|
|
9
|
+
count = assignment_class.where("expires_at < ?", Time.current).delete_all
|
|
10
|
+
{deleted: count}
|
|
11
|
+
else
|
|
12
|
+
{deleted: 0, error: "expires_at column missing"}
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
module RoleFu
|
|
4
4
|
class Configuration
|
|
5
|
-
attr_accessor :user_class_name, :role_class_name, :role_assignment_class_name
|
|
5
|
+
attr_accessor :user_class_name, :role_class_name, :role_assignment_class_name, :global_roles_override
|
|
6
6
|
|
|
7
7
|
def initialize
|
|
8
8
|
@user_class_name = "User"
|
|
9
9
|
@role_class_name = "Role"
|
|
10
10
|
@role_assignment_class_name = "RoleAssignment"
|
|
11
|
+
@global_roles_override = false
|
|
11
12
|
end
|
|
12
13
|
end
|
|
13
14
|
|