role_fu 0.1.0 → 0.2.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: 3a8521c0afe782f8985577cf76d9b59b950522b1c445150a11db74e7f944718b
4
+ data.tar.gz: 0a82d9b19260c3ea95b2506cb45ad2e12ab23b64bdb0e3b1c0b84c8f93d8061f
5
5
  SHA512:
6
- metadata.gz: 0c7448aa048ad1836888270417e15d2acf5375cef727059cac77e9dea5c644d2b5156fc9e90134d9918367b38e4c127f753f5dda01b836c24794baf0102646dd
7
- data.tar.gz: 57a6f48677cb49653b3c22e298b13fb3ac13003bb7e8d895e9c17396304e8385b7fb7162db426a7862ce1aad3031396e66dda60a897f898952594ee6011a4784
6
+ metadata.gz: 386f524a75c3116601af67eaf6612c0bc23893695120a2646ba6d0caafba8bcf7e0bc12dc05d93dc7ddcbc9c508da936cfebf679d973675f5b9742438ed85c50
7
+ data.tar.gz: d9db11c9b4490d35714acb1f20a4eb576a53584fd235bf0328ca33959f53756221478e5af877dfec5833f1431ccdde9fafded39980d5f90ce87e995dfef20695
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.2.0] - 2026-02-03
4
+
5
+ ### Added
6
+ - **Temporal Roles**: Support for role expiration (`expires_at`) and automatic filtering.
7
+ - **Audit Logging**: Built-in generator for audit trails (`RoleAssignmentAudit`) and actor tracking (`RoleFu.with_actor`).
8
+ - **Role Abilities**: Granular permissions system with `Permission` model and `role_fu_can?` helper.
9
+ - **Metadata**: Support for attaching arbitrary JSON metadata to role assignments.
10
+ - **Adapters**: Seamless integration with **Pundit** and **CanCanCan**.
11
+ - **Permissive Mode**: Configuration option `global_roles_override` to match Rolify behavior.
12
+ - **Cleanup Automation**: `rake role_fu:cleanup` task and ActiveJob generator for expired roles.
13
+ - **Custom Aliases**: `role_fu_alias` to generate domain-specific methods (e.g., `add_group`, `in_group`).
14
+
3
15
  ## [0.1.0] - 2026-02-03
4
16
 
5
- - Initial release: modern role management for Rails with explicit models and N+1 prevention.
17
+ - 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).
@@ -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
 
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RoleFu
4
+ module Permission
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ belongs_to :role, class_name: RoleFu.configuration.role_class_name
9
+ validates :action, presence: true
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/railtie"
4
+
5
+ module RoleFu
6
+ class Railtie < Rails::Railtie
7
+ rake_tasks do
8
+ load "tasks/role_fu.rake"
9
+ end
10
+ end
11
+ end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RoleFu
4
- # Resourceable concern - provides resource management for models with roles
5
4
  module Resourceable
6
5
  extend ActiveSupport::Concern
7
6
 
@@ -13,144 +12,111 @@ module RoleFu
13
12
  end
14
13
 
15
14
  module ClassMethods
16
- # Find roles defined on any instance of this resource class
17
- # @param role_name [String, Symbol, nil] Filter by role name
18
- # @param user [User, nil] Filter by user
19
- # @return [ActiveRecord::Relation] Roles
20
15
  def find_roles(role_name = nil, user = nil)
21
- RoleFu.role_class.table_name
22
16
  query = RoleFu.role_class.where(resource_type: name)
23
17
  query = query.where(name: role_name.to_s) if role_name
24
18
  query = query.joins(:users).where(RoleFu.user_class.table_name => {id: user.id}) if user
25
19
  query
26
20
  end
27
21
 
28
- # Find resources that have a specific role applied (to a user)
29
- # @param role_name [String, Symbol] The role name
30
- # @param user [User, nil] Filter by specific user having the role
31
- # @return [ActiveRecord::Relation] Resources
32
22
  def with_role(role_name, user = nil)
33
23
  role_table = RoleFu.role_class.table_name
34
-
35
24
  query = joins(:roles).where(role_table => {name: role_name.to_s})
36
-
37
- if user
38
- query = query.joins(roles: :users).where(RoleFu.user_class.table_name => {id: user.id})
39
- end
40
-
25
+ query = query.joins(roles: :users).where(RoleFu.user_class.table_name => {id: user.id}) if user
41
26
  query.distinct
42
27
  end
43
28
 
44
- # Find resources that do NOT have a specific role applied
45
- # @param role_name [String, Symbol] The role name
46
- # @param user [User, nil] Filter by user
47
- # @return [ActiveRecord::Relation] Resources
48
29
  def without_role(role_name, user = nil)
49
30
  where.not(id: with_role(role_name, user).select(:id))
50
31
  end
32
+
33
+ # Dynamically create aliases for role methods
34
+ # @param name [Symbol, String] The alias name (e.g. :group)
35
+ def role_fu_alias(name)
36
+ singular = name.to_s.singularize
37
+ plural = name.to_s.pluralize
38
+
39
+ # Class methods
40
+ singleton_class.class_eval do
41
+ alias_method "find_#{plural}", :find_roles
42
+ alias_method "with_#{singular}", :with_role
43
+ alias_method "without_#{singular}", :without_role
44
+ end
45
+
46
+ # Instance methods
47
+ alias_method "users_with_#{singular}", :users_with_role
48
+ alias_method "users_with_any_#{singular}", :users_with_any_role
49
+ alias_method "users_with_all_#{plural}", :users_with_all_roles
50
+ alias_method "users_with_#{plural}", :users_with_roles
51
+ alias_method "available_#{plural}", :available_roles
52
+ alias_method "has_#{singular}?", :has_role?
53
+ alias_method "count_users_with_#{singular}", :count_users_with_role
54
+ alias_method "user_has_#{singular}?", :user_has_role?
55
+ alias_method "add_#{singular}_to_user", :add_role_to_user
56
+ alias_method "remove_#{singular}_from_user", :remove_role_from_user
57
+ end
51
58
  end
52
59
 
53
- # Get roles applied to this resource instance (plus global class-level roles if any - though RoleFu focuses on instance roles)
54
- # @return [ActiveRecord::Relation] Roles
55
60
  def applied_roles
56
61
  roles
57
62
  end
58
63
 
59
- # Get users with a specific role on this resource
60
- # @param role_name [String, Symbol] The role name
61
- # @return [ActiveRecord::Relation] Relation of users
62
64
  def users_with_role(role_name)
63
65
  role_table = RoleFu.role_class.table_name
64
- user_class.joins(:roles)
66
+ RoleFu.user_class.joins(:roles)
65
67
  .where(role_table => {name: role_name.to_s, resource_type: self.class.name, resource_id: id})
66
68
  .distinct
67
69
  end
68
70
 
69
- # Get users with any role on this resource
70
- # @param role_names [Array<String, Symbol>] Array of role names
71
- # @return [ActiveRecord::Relation] Relation of users
72
71
  def users_with_any_role(*role_names)
73
72
  role_table = RoleFu.role_class.table_name
74
- user_class.joins(:roles)
73
+ RoleFu.user_class.joins(:roles)
75
74
  .where(role_table => {name: role_names.flatten.map(&:to_s), resource_type: self.class.name, resource_id: id})
76
75
  .distinct
77
76
  end
78
77
 
79
- # Get users with all specified roles on this resource
80
- # @param role_names [Array<String, Symbol>] Array of role names
81
- # @return [Array<User>] Array of users
82
78
  def users_with_all_roles(*role_names)
83
79
  role_names = role_names.flatten.map(&:to_s)
84
80
  role_table = RoleFu.role_class.table_name
81
+ user_table = RoleFu.user_class.table_name
82
+ user_pk = RoleFu.user_class.primary_key
85
83
 
86
- user_class.joins(:roles)
84
+ RoleFu.user_class.joins(:roles)
87
85
  .where(role_table => {name: role_names, resource_type: self.class.name, resource_id: id})
88
- .group("#{user_class.table_name}.#{user_class.primary_key}")
86
+ .group("#{user_table}.#{user_pk}")
89
87
  .having("COUNT(DISTINCT #{role_table}.name) = ?", role_names.size)
90
88
  .distinct
91
89
  end
92
90
 
93
- # Get all users with any role on this resource
94
- # @return [ActiveRecord::Relation] Relation of users
95
91
  def users_with_roles
96
92
  role_table = RoleFu.role_class.table_name
97
- user_class.joins(:roles)
93
+ RoleFu.user_class.joins(:roles)
98
94
  .where(role_table => {resource_type: self.class.name, resource_id: id})
99
95
  .distinct
100
96
  end
101
97
 
102
- # Get all role names defined for this resource
103
- # @return [Array<String>] Array of role names
104
98
  def available_roles
105
99
  roles.pluck(:name).uniq
106
100
  end
107
101
 
108
- # Check if resource has any users with a specific role
109
- # @param role_name [String, Symbol] The role name
110
- # @return [Boolean] true if any user has this role
111
102
  def has_role?(role_name)
112
103
  roles.exists?(name: role_name.to_s)
113
104
  end
114
105
 
115
- # Count users with a specific role
116
- # @param role_name [String, Symbol] The role name
117
- # @return [Integer] Number of users
118
106
  def count_users_with_role(role_name)
119
107
  users_with_role(role_name).count
120
108
  end
121
109
 
122
- # Check if a specific user has a role on this resource
123
- # @param user [User] The user
124
- # @param role_name [String, Symbol] The role name
125
- # @return [Boolean] true if user has the role
126
110
  def user_has_role?(user, role_name)
127
- return false if user.nil?
128
-
129
- user.has_role?(role_name, self)
111
+ user&.has_role?(role_name, self) || false
130
112
  end
131
113
 
132
- # Add a role to a user on this resource
133
- # @param user [User] The user
134
- # @param role_name [String, Symbol] The role name
135
- # @return [Role] The role
136
114
  def add_role_to_user(user, role_name)
137
115
  user.add_role(role_name, self)
138
116
  end
139
117
 
140
- # Remove a role from a user on this resource
141
- # @param user [User] The user
142
- # @param role_name [String, Symbol] The role name
143
- # @return [Array<Role>] The removed roles
144
118
  def remove_role_from_user(user, role_name)
145
119
  user.remove_role(role_name, self)
146
120
  end
147
-
148
- private
149
-
150
- # Get the User class
151
- # @return [Class] User class
152
- def user_class
153
- RoleFu.user_class
154
- end
155
121
  end
156
122
  end
@@ -12,11 +12,48 @@ module RoleFu
12
12
  scope :global_roles, -> { joins(:role).where(RoleFu.role_class.table_name => {resource_type: nil, resource_id: nil}) }
13
13
  scope :resource_specific, -> { joins(:role).where.not(RoleFu.role_class.table_name => {resource_type: nil}) }
14
14
 
15
+ after_create :audit_create
16
+ after_update :audit_update
17
+ after_destroy :audit_destroy
18
+
15
19
  after_destroy :cleanup_orphaned_role
16
20
  end
17
21
 
18
22
  private
19
23
 
24
+ def audit_create
25
+ audit_log("INSERT")
26
+ end
27
+
28
+ def audit_update
29
+ audit_log("UPDATE")
30
+ end
31
+
32
+ def audit_destroy
33
+ audit_log("DELETE")
34
+ end
35
+
36
+ def audit_log(operation)
37
+ # Only audit if the Audit model exists
38
+ audit_class = "RoleAssignmentAudit".safe_constantize
39
+ return unless audit_class
40
+
41
+ actor = RoleFu.current_actor
42
+
43
+ audit_class.create(
44
+ role_assignment_id: id,
45
+ user_id: user_id,
46
+ role_id: role_id,
47
+ operation: operation,
48
+ whodunnit: actor.try(:id) || actor.to_s,
49
+ meta_snapshot: try(:meta),
50
+ expires_at_snapshot: try(:expires_at)
51
+ )
52
+ rescue
53
+ # Fail silently or log error? Logging is safer to avoid breaking the main flow.
54
+ # defined?(Rails) ? Rails.logger.error("RoleFu Audit Error: #{e.message}") : puts("RoleFu Audit Error: #{e.message}")
55
+ end
56
+
20
57
  def cleanup_orphaned_role
21
58
  # If this record is being destroyed as part of role destruction,
22
59
  # do not try to destroy the same role again.
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RoleFu
4
- # Roleable concern - provides role management for User model
5
4
  module Roleable
6
5
  extend ActiveSupport::Concern
7
6
 
@@ -21,16 +20,16 @@ module RoleFu
21
20
  self.role_fu_callbacks = options.slice(:before_add, :after_add, :before_remove, :after_remove)
22
21
  end
23
22
 
24
- # Find users with a specific role
25
- # @param role_name [String, Symbol] The name of the role
26
- # @param resource [ActiveRecord::Base, Class, nil, :any] The resource
27
- # @return [ActiveRecord::Relation] Users with the role
28
23
  def with_role(role_name, resource = nil)
29
24
  role_table = RoleFu.role_class.table_name
30
- RoleFu.role_assignment_class.table_name
25
+ assignment_table = RoleFu.role_assignment_class.table_name
31
26
 
32
27
  query = joins(:roles).where(role_table => {name: role_name.to_s})
33
28
 
29
+ if RoleFu.role_assignment_class.column_names.include?("expires_at")
30
+ query = query.where("#{assignment_table}.expires_at IS NULL OR #{assignment_table}.expires_at > ?", Time.current)
31
+ end
32
+
34
33
  if resource.nil?
35
34
  query.where(role_table => {resource_type: nil, resource_id: nil})
36
35
  elsif resource == :any
@@ -42,75 +41,87 @@ module RoleFu
42
41
  end.distinct
43
42
  end
44
43
 
45
- # Find users without a specific role
46
- # @param role_name [String, Symbol] The name of the role
47
- # @param resource [ActiveRecord::Base, Class, nil, :any] The resource
48
- # @return [ActiveRecord::Relation] Users without the role
49
44
  def without_role(role_name, resource = nil)
50
45
  where.not(id: with_role(role_name, resource).select(:id))
51
46
  end
52
47
 
53
- # Find users with any of the specified roles
54
- # @param args [Array<String, Symbol, Hash>] Roles to check
55
- # @return [ActiveRecord::Relation] Users with any of the roles
56
48
  def with_any_role(*args)
57
- ids = []
58
- args.each do |arg|
59
- ids += if arg.is_a?(Hash)
60
- with_role(arg[:name], arg[:resource]).pluck(:id)
61
- else
62
- with_role(arg).pluck(:id)
63
- end
49
+ ids = args.flat_map do |arg|
50
+ arg.is_a?(Hash) ? with_role(arg[:name], arg[:resource]).pluck(:id) : with_role(arg).pluck(:id)
64
51
  end
65
52
  where(id: ids.uniq)
66
53
  end
67
54
 
68
- # Find users with all of the specified roles
69
- # @param args [Array<String, Symbol, Hash>] Roles to check
70
- # @return [ActiveRecord::Relation] Users with all of the roles
71
55
  def with_all_roles(*args)
72
56
  ids = nil
73
57
  args.each do |arg|
74
- current_ids = if arg.is_a?(Hash)
75
- with_role(arg[:name], arg[:resource]).pluck(:id)
76
- else
77
- with_role(arg).pluck(:id)
78
- end
58
+ current_ids = arg.is_a?(Hash) ? with_role(arg[:name], arg[:resource]).pluck(:id) : with_role(arg).pluck(:id)
79
59
  ids = ids.nil? ? current_ids : ids & current_ids
80
60
  return none if ids.empty?
81
61
  end
82
62
  where(id: ids)
83
63
  end
64
+
65
+ # Dynamically create aliases for role methods
66
+ # @param name [Symbol, String] The alias name (e.g. :group)
67
+ # @example
68
+ # role_fu_alias :group
69
+ # # Creates: add_group, remove_group, has_group?, with_group, in_group, etc.
70
+ def role_fu_alias(name)
71
+ singular = name.to_s.singularize
72
+ plural = name.to_s.pluralize
73
+
74
+ # Instance methods
75
+ alias_method "add_#{singular}", :add_role
76
+ alias_method "remove_#{singular}", :remove_role
77
+ alias_method "has_#{singular}?", :has_role?
78
+ alias_method "only_has_#{singular}?", :only_has_role?
79
+ alias_method "has_any_#{singular}?", :has_any_role?
80
+ alias_method "has_all_#{plural}?", :has_all_roles?
81
+ alias_method "#{plural}_name", :roles_name
82
+
83
+ # Class methods (Scopes)
84
+ singleton_class.class_eval do
85
+ alias_method "with_#{singular}", :with_role
86
+ alias_method "without_#{singular}", :without_role
87
+ alias_method "with_any_#{singular}", :with_any_role
88
+ alias_method "with_all_#{plural}", :with_all_roles
89
+
90
+ # Additional natural aliases
91
+ alias_method "in_#{singular}", :with_role
92
+ alias_method "not_in_#{singular}", :without_role
93
+ end
94
+ end
84
95
  end
85
96
 
86
- # Add a role to the user
87
- # @param role_name [String, Symbol] The name of the role
88
- # @param resource [ActiveRecord::Base, Class, nil] The resource (organization, etc.) or nil for global role
89
- # @return [Role] The role that was added
90
- def add_role(role_name, resource = nil)
97
+ def add_role(role_name, resource = nil, expires_at: nil, meta: nil)
91
98
  role = find_or_create_role(role_name, resource)
92
-
93
- return role if roles.include?(role)
99
+ existing_assignment = role_assignments.find_by(role: role)
100
+
101
+ if existing_assignment
102
+ if expires_at || meta
103
+ updates = {}
104
+ updates[:expires_at] = expires_at if expires_at
105
+ updates[:meta] = meta if meta
106
+ existing_assignment.update(updates)
107
+ end
108
+ return role
109
+ end
94
110
 
95
111
  run_role_fu_callback(:before_add, role)
96
- roles << role
112
+ role_assignments.create!(role: role, expires_at: expires_at, meta: meta)
113
+ roles.reload
97
114
  run_role_fu_callback(:after_add, role)
98
115
 
99
116
  role
100
117
  end
101
118
  alias_method :grant, :add_role
102
119
 
103
- # Remove a role from the user
104
- # @param role_name [String, Symbol] The name of the role
105
- # @param resource [ActiveRecord::Base, Class, nil] The resource or nil for global role
106
- # @return [Array<Role>] The roles that were removed
107
120
  def remove_role(role_name, resource = nil)
108
121
  roles_to_remove_relation = find_roles(role_name, resource)
109
122
  return [] if roles_to_remove_relation.empty?
110
123
 
111
- # Materialize before removing associations, because removing may trigger cleanup that deletes the role.
112
124
  removed_roles = roles_to_remove_relation.to_a
113
-
114
125
  removed_roles.each do |role|
115
126
  run_role_fu_callback(:before_remove, role)
116
127
  role_assignments.where(role_id: role.id).destroy_all
@@ -121,45 +132,38 @@ module RoleFu
121
132
  end
122
133
  alias_method :revoke, :remove_role
123
134
 
124
- # Check if user has a specific role
125
- # @param role_name [String, Symbol] The name of the role
126
- # @param resource [ActiveRecord::Base, Class, nil, :any] The resource, nil for global, or :any for any resource
127
- # @return [Boolean] true if user has the role
128
135
  def has_role?(role_name, resource = nil)
129
136
  return false if role_name.nil?
130
137
 
131
138
  if resource == :any
132
- roles.exists?(name: role_name.to_s)
139
+ filter_expired(roles.where(name: role_name.to_s)).exists?
133
140
  else
134
- find_roles(role_name, resource).any?
141
+ return true if filter_expired(find_roles(role_name, resource)).any?
142
+
143
+ if RoleFu.configuration.global_roles_override && resource && !resource.is_a?(Class)
144
+ return filter_expired(find_roles(role_name, nil)).any?
145
+ end
146
+
147
+ false
135
148
  end
136
149
  end
137
150
 
138
- # Check if user has a specific role strictly (resource match must be exact, no globals overriding)
139
- # Note: In RoleFu, has_role? is already strict about resource matching unless :any is passed,
140
- # but this method explicitly bypasses any future global-fallback logic if we were to add it.
141
- # Included for API compatibility.
142
- # @param role_name [String, Symbol] The name of the role
143
- # @param resource [ActiveRecord::Base, Class, nil] The resource
144
- # @return [Boolean] true if user has the role strictly
145
151
  def has_strict_role?(role_name, resource = nil)
146
- has_role?(role_name, resource)
152
+ filter_expired(find_roles(role_name, resource)).any?
147
153
  end
148
154
 
149
- # Check if user only has this one role
150
- # @param role_name [String, Symbol] The name of the role
151
- # @param resource [ActiveRecord::Base, Class, nil] The resource
152
- # @return [Boolean] true if user has this role and no others
153
155
  def only_has_role?(role_name, resource = nil)
154
- has_role?(role_name, resource) && roles.count == 1
156
+ has_role?(role_name, resource) && filter_expired(roles).count == 1
155
157
  end
156
158
 
157
- # Check for role using preloaded association to avoid N+1
158
159
  def has_cached_role?(role_name, resource = nil)
159
160
  role_name = role_name.to_s
160
161
  roles.to_a.any? do |role|
161
162
  next false unless role.name == role_name
162
163
 
164
+ assignment = role_assignments.find { |ra| ra.role_id == role.id }
165
+ next false if assignment&.respond_to?(:expires_at) && assignment.expires_at && assignment.expires_at <= Time.current
166
+
163
167
  if resource == :any
164
168
  true
165
169
  elsif resource.is_a?(Class)
@@ -172,80 +176,58 @@ module RoleFu
172
176
  end
173
177
  end
174
178
 
175
- # Get all role names for this user
176
- # @param resource [ActiveRecord::Base, Class, nil] Filter by resource
177
- # @return [Array<String>] Array of role names
178
179
  def roles_name(resource = nil)
179
- if resource
180
- roles.where(resource: resource).pluck(:name)
181
- else
182
- roles.pluck(:name)
183
- end
180
+ scope = resource ? roles.where(resource: resource) : roles
181
+ filter_expired(scope).pluck(:name)
184
182
  end
185
183
 
186
- # Check if user has only global roles (no resource-specific roles)
187
- # @return [Boolean] true if user has only global roles
188
184
  def has_only_global_roles?
189
- roles.where.not(resource_type: nil).empty?
185
+ filter_expired(roles).where.not(resource_type: nil).empty?
190
186
  end
191
187
 
192
- # Check if user has any role (global or resource-specific)
193
- # @param resource [ActiveRecord::Base, Class, nil] Filter by resource
194
- # @return [Boolean] true if user has any role
195
188
  def has_any_role?(resource = nil)
196
- if resource
197
- roles.exists?(resource: resource)
198
- else
199
- roles.exists?
200
- end
189
+ scope = resource ? roles.where(resource: resource) : roles
190
+ filter_expired(scope).exists?
201
191
  end
202
192
 
203
- # Check if user has all specified roles
204
- # @param role_names [Array<String, Symbol>] Array of role names
205
- # @param resource [ActiveRecord::Base, Class, nil] The resource
206
- # @return [Boolean] true if user has all roles
207
193
  def has_all_roles?(*role_names, resource: nil)
208
194
  role_names.flatten.all? { |role_name| has_role?(role_name, resource) }
209
195
  end
210
196
 
211
- # Check if user has any of the specified roles
212
- # @param role_names [Array<String, Symbol>] Array of role names
213
- # @param resource [ActiveRecord::Base, Class, nil] The resource
214
- # @return [Boolean] true if user has any of the roles
215
197
  def has_any_role_of?(*role_names, resource: nil)
216
198
  role_names.flatten.any? { |role_name| has_role?(role_name, resource) }
217
199
  end
218
200
 
219
- # Get all resources of a specific type where user has a role
220
- # @param resource_class [Class] The resource class (e.g., Organization)
221
- # @return [ActiveRecord::Relation] Relation of resources
222
201
  def resources(resource_class)
223
- resource_class.joins(:roles)
224
- .merge(roles.where(resource_type: resource_class.name))
225
- .distinct
202
+ assignment_table = RoleFu.role_assignment_class.table_name
203
+ query = resource_class.joins(:roles).merge(roles.where(resource_type: resource_class.name))
204
+
205
+ if RoleFu.role_assignment_class.column_names.include?("expires_at")
206
+ query = query.where("#{assignment_table}.expires_at IS NULL OR #{assignment_table}.expires_at > ?", Time.current)
207
+ end
208
+
209
+ query.distinct
226
210
  end
227
211
 
228
212
  private
229
213
 
230
- # Find or create a role
231
- # @param role_name [String, Symbol] The role name
232
- # @param resource [ActiveRecord::Base, Class, nil] The resource
233
- # @return [Role] The found or created role
234
- def find_or_create_role(role_name, resource)
235
- resource_type = resource_type_for(resource)
236
- resource_id = resource_id_for(resource)
214
+ def filter_expired(relation)
215
+ return relation unless RoleFu.role_assignment_class.column_names.include?("expires_at")
216
+
217
+ assignment_table = RoleFu.role_assignment_class.table_name
218
+ relation.joins(:role_assignments)
219
+ .where("#{assignment_table}.user_id = ? AND (#{assignment_table}.expires_at IS NULL OR #{assignment_table}.expires_at > ?)", id, Time.current)
220
+ .distinct
221
+ end
237
222
 
223
+ def find_or_create_role(role_name, resource)
238
224
  RoleFu.role_class.find_or_create_by(
239
225
  name: role_name.to_s,
240
- resource_type: resource_type,
241
- resource_id: resource_id
226
+ resource_type: resource_type_for(resource),
227
+ resource_id: resource_id_for(resource)
242
228
  )
243
229
  end
244
230
 
245
- # Find roles matching criteria
246
- # @param role_name [String, Symbol] The role name
247
- # @param resource [ActiveRecord::Base, Class, nil] The resource
248
- # @return [ActiveRecord::Relation] Relation of matching roles
249
231
  def find_roles(role_name, resource)
250
232
  query = roles.where(name: role_name.to_s)
251
233
 
@@ -258,20 +240,14 @@ module RoleFu
258
240
  end
259
241
  end
260
242
 
261
- # Get resource type for a resource
262
- # @param resource [ActiveRecord::Base, Class, nil] The resource
263
- # @return [String, nil] The resource type
264
243
  def resource_type_for(resource)
265
- return nil if resource.nil?
244
+ return if resource.nil?
266
245
 
267
246
  resource.is_a?(Class) ? resource.to_s : resource.class.name
268
247
  end
269
248
 
270
- # Get resource id for a resource
271
- # @param resource [ActiveRecord::Base, Class, nil] The resource
272
- # @return [Integer, nil] The resource id
273
249
  def resource_id_for(resource)
274
- return nil if resource.nil? || resource.is_a?(Class)
250
+ return if resource.nil? || resource.is_a?(Class)
275
251
 
276
252
  resource.id
277
253
  end
@@ -280,10 +256,8 @@ module RoleFu
280
256
  method_name = role_fu_callbacks[callback_name]
281
257
  return unless method_name
282
258
 
283
- if method_name.is_a?(Proc)
284
- instance_exec(role, &method_name)
285
- elsif respond_to?(method_name, true)
286
- send(method_name, role)
259
+ if respond_to?(method_name, true) || method_name.is_a?(Proc)
260
+ method_name.is_a?(Proc) ? instance_exec(role, &method_name) : send(method_name, role)
287
261
  end
288
262
  end
289
263
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RoleFu
4
- VERSION = "0.1.0"
5
- end
4
+ VERSION = "0.2.0"
5
+ end
data/lib/role_fu.rb CHANGED
@@ -9,8 +9,26 @@ require_relative "role_fu/role"
9
9
  require_relative "role_fu/role_assignment"
10
10
  require_relative "role_fu/roleable"
11
11
  require_relative "role_fu/resourceable"
12
+ require_relative "role_fu/permission"
13
+ require_relative "role_fu/ability"
14
+ require_relative "role_fu/cleanup"
15
+ require_relative "role_fu/adapters/cancancan"
16
+ require_relative "role_fu/adapters/pundit"
17
+ require_relative "role_fu/railtie" if defined?(Rails)
12
18
 
13
19
  module RoleFu
14
20
  class Error < StandardError; end
15
- # Your code goes here...
21
+
22
+ class << self
23
+ def with_actor(actor)
24
+ Thread.current[:role_fu_actor] = actor
25
+ yield
26
+ ensure
27
+ Thread.current[:role_fu_actor] = nil
28
+ end
29
+
30
+ def current_actor
31
+ Thread.current[:role_fu_actor]
32
+ end
33
+ end
16
34
  end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :role_fu do
4
+ desc "Clean up expired role assignments"
5
+ task cleanup: :environment do
6
+ result = RoleFu::Cleanup.call
7
+ puts result[:error] || "Deleted #{result[:deleted]} expired role assignment(s)."
8
+ end
9
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: role_fu
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexey Poimtsev
@@ -132,19 +132,34 @@ files:
132
132
  - gemfiles/rails_8.1.gemfile
133
133
  - gemfiles/rails_8.1.gemfile.lock
134
134
  - lefthook.yml
135
+ - lib/generators/role_fu/abilities_generator.rb
136
+ - lib/generators/role_fu/audit_generator.rb
135
137
  - lib/generators/role_fu/install_generator.rb
138
+ - lib/generators/role_fu/job_generator.rb
136
139
  - lib/generators/role_fu/role_fu_generator.rb
140
+ - lib/generators/role_fu/templates/abilities_migration.rb.erb
141
+ - lib/generators/role_fu/templates/audit_migration.rb.erb
142
+ - lib/generators/role_fu/templates/audit_model.rb.erb
143
+ - lib/generators/role_fu/templates/cleanup_job.rb.erb
137
144
  - lib/generators/role_fu/templates/migration.rb.erb
145
+ - lib/generators/role_fu/templates/permission.rb.erb
138
146
  - lib/generators/role_fu/templates/role.rb.erb
139
147
  - lib/generators/role_fu/templates/role_assignment.rb.erb
140
148
  - lib/generators/role_fu/templates/role_fu.rb
141
149
  - lib/role_fu.rb
150
+ - lib/role_fu/ability.rb
151
+ - lib/role_fu/adapters/cancancan.rb
152
+ - lib/role_fu/adapters/pundit.rb
153
+ - lib/role_fu/cleanup.rb
142
154
  - lib/role_fu/configuration.rb
155
+ - lib/role_fu/permission.rb
156
+ - lib/role_fu/railtie.rb
143
157
  - lib/role_fu/resourceable.rb
144
158
  - lib/role_fu/role.rb
145
159
  - lib/role_fu/role_assignment.rb
146
160
  - lib/role_fu/roleable.rb
147
161
  - lib/role_fu/version.rb
162
+ - lib/tasks/role_fu.rake
148
163
  - sig/role_fu.rbs
149
164
  homepage: https://github.com/alec-c4/role_fu
150
165
  licenses: