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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7d8ca75bef3838c779e395c17a16c8e61ddb6a7fdab7b2819412cd9356213436
4
- data.tar.gz: b3b280add77cac1f4778c7e72b4ec3717e9554ab053cbcce869ef7180dd98ed2
3
+ metadata.gz: d75b379be1f3096ca3580d3b5f545c7e10e96a76ade4e1ea8ddf9df11272cf61
4
+ data.tar.gz: 9dda6cef944474db4c9fa70979f0a633a3ad90f51bee73faa6cc7ed2e13bfa68
5
5
  SHA512:
6
- metadata.gz: 0c7448aa048ad1836888270417e15d2acf5375cef727059cac77e9dea5c644d2b5156fc9e90134d9918367b38e4c127f753f5dda01b836c24794baf0102646dd
7
- data.tar.gz: 57a6f48677cb49653b3c22e298b13fb3ac13003bb7e8d895e9c17396304e8385b7fb7162db426a7862ce1aad3031396e66dda60a897f898952594ee6011a4784
6
+ metadata.gz: 51e073987c36ffd2c6faa06256362a75f79f1a83931fe2bc82e5f4d6750c4a355df4324c3f33dc1dcbe298d3d060b787b6d819da1b693f63edb9d4ed4dc008c5
7
+ data.tar.gz: 9e1b6a068bce303db00d70f538a311321fbc2ea1d1032b1522b028d0cf71835c7107589b3f194b48a0c242ba9afab49755a1213ab4a6ce2ba195a06fef8f9a10
data/Appraisals CHANGED
@@ -1,11 +1,3 @@
1
- appraise "rails-7.0" do
2
- gem "rails", "~> 7.0.0"
3
- end
4
-
5
- appraise "rails-7.1" do
6
- gem "rails", "~> 7.1.0"
7
- end
8
-
9
1
  appraise "rails-7.2" do
10
2
  gem "rails", "~> 7.2.0"
11
3
  end
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 `rolify`.
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**: Unlike Rolify which often uses hidden `has_and_belongs_to_many` tables, RoleFu uses an explicit `RoleAssignment` join model. This makes it easier to extend with metadata (e.g., `created_by`, `expires_at`).
8
- - **N+1 Prevention**: Built-in support for `has_cached_role?` to work with preloaded associations.
9
- - **Strict by Default**: Focused on resource-specific roles with clear `has_role?` semantics.
10
- - **Orphaned Role Cleanup**: Automatically deletes roles when the last user assignment is removed.
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. Run the install generator to create the configuration:
29
+ 1. **Install Configuration:**
29
30
  ```bash
30
31
  rails generate role_fu:install
31
32
  ```
32
33
 
33
- 2. Generate the Role models (default names are `Role` and `RoleAssignment`):
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 migrations:
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.add_role(:admin)
69
- user.grant(:admin) # Alias
70
+ user.grant(:admin)
70
71
  user.has_role?(:admin) # => true
71
- user.remove_role(:admin)
72
- user.revoke(:admin) # Alias
72
+ user.revoke(:admin)
73
73
 
74
74
  # Resource-specific roles
75
75
  org = Organization.first
76
- user.add_role(:manager, org)
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
- # Scopes (Finders)
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
- # Resource Scopes
90
- Organization.with_role(:manager, user)
91
- Organization.without_role(:manager, user)
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
- #### Performance (N+1 Prevention)
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
- Use `has_cached_role?` when roles are preloaded.
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 (Models with roles)
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
- ## Comparison with Rolify
121
-
122
- | Feature | Rolify | RoleFu |
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
- If your models are named differently (e.g., `Account` instead of `User`, or `Group` instead of `Role`), you can configure them in `config/initializers/role_fu.rb`:
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
- config.role_assignment_class_name = "GroupAssignment" # Optional
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.1.0)
5
- activerecord (>= 7.0)
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.1.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.1.0)
5
- activerecord (>= 7.0)
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.1.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.1.0)
5
- activerecord (>= 7.0)
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.1.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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RoleAssignmentAudit < ApplicationRecord
4
+ belongs_to :user, optional: true
5
+ belongs_to :role, optional: true
6
+ belongs_to :role_assignment, optional: true
7
+ 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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Permission < ApplicationRecord
4
+ include RoleFu::Permission
5
+ 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