role_fu 0.5.0 → 0.6.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/.ruby-version +1 -0
- data/CHANGELOG.md +15 -0
- data/README.md +92 -0
- data/gemfiles/rails_7.2.gemfile.lock +2 -2
- data/gemfiles/rails_8.0.gemfile.lock +2 -2
- data/gemfiles/rails_8.1.gemfile.lock +2 -2
- data/lib/generators/role_fu/templates/abilities_migration.rb.erb +3 -2
- data/lib/generators/role_fu/templates/upgrade_permissions_field_migration.rb.erb +8 -0
- data/lib/generators/role_fu/upgrade_generator.rb +83 -0
- data/lib/role_fu/ability.rb +39 -4
- data/lib/role_fu/adapters/cancancan.rb +13 -4
- data/lib/role_fu/authorizable.rb +45 -0
- data/lib/role_fu/role.rb +8 -0
- data/lib/role_fu/roleable.rb +5 -5
- data/lib/role_fu/version.rb +1 -1
- data/lib/role_fu.rb +15 -2
- data/mise.toml +14 -0
- metadata +18 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8895d1ec974d84a207c9a1ef584910237b328442cb76f78d84a382b640527e43
|
|
4
|
+
data.tar.gz: da9cf4d703ecc9eef0e92bf2c883dc6cdacd49cd96e8c898289980465f96b445
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 84626ecc3ac908297ad8dd95649126b851d990975ea74fb86fc73b8fe0f38af1c72630232393ce418720b722a3e695a64b55b9c05874c8d0e4b1010235d8770c
|
|
7
|
+
data.tar.gz: 4cd9382ad8db4b60fda9ebdc256cfe44f15662e07392c0b27170acb744c3947705acb58b985bf8811f816b9e5f111f470f17988680bf297219da78cb1a530126
|
data/.ruby-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
4.0.6
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
## [0.6.0] - 2026-09-22
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
|
|
5
|
+
- **Field-level Abilities**: `role_fu_can?(action, field:)` and `role_fu_permitted_fields(action)` allow scoping a `Permission` to a single attribute (e.g. `reports.update` + `field: "status"`). A permission granted without a `field` remains a wildcard for that action, so existing action-only permissions are unaffected. The `permissions` table gains an optional `field` column (nullable — existing installs keep working without it).
|
|
6
|
+
- **CanCanCan Adapter**: field-scoped permissions now translate into CanCanCan's own native attribute restriction (`can :update, Report, :status`) instead of role_fu re-implementing attribute-level authorization.
|
|
7
|
+
- **Role Name Normalization**: role names are now canonicalized (`"Admin"`, `"admin"`, `"Admin User"`, `"admin-user"` all resolve to the same role) to prevent case/formatting duplicates. Does not singularize.
|
|
8
|
+
- **`RoleFu::Authorizable`**: a minimal, framework-agnostic guard concern (`role_fu_authorize!`, `role_fu_can!`, raising `RoleFu::AccessDenied`) for apps that want a guard clause without adding a full authorization gem. Deliberately not an `allow`/`deny` DSL — see README for the Pundit/CanCanCan adapters when you need real rule resolution.
|
|
9
|
+
- **`role_fu:upgrade` generator**: run after bumping the gem to catch up on optional schema/data changes. Inspects your actual schema/data instead of tracking "which version you were on" (nothing persists that reliably), so it's safe to run regardless of how many releases you skipped and safe to re-run. Currently detects the missing `permissions.field` column (generates the migration) and denormalized role names (reports only — never auto-merges, since two differently-cased roles may already coexist with their own assignments).
|
|
10
|
+
- **Post-install banner**: `gem install`/`bundle install` now print a reminder to run `rails generate role_fu:upgrade` after upgrading.
|
|
11
|
+
|
|
12
|
+
### BREAKING CHANGES
|
|
13
|
+
|
|
14
|
+
- **Role name normalization** changes the stored `name` for newly created/looked-up roles (e.g. `"Admin"` -> `"admin"`). Existing rows with mixed-case names are **not** migrated automatically — run `rails generate role_fu:upgrade` after upgrading; it will detect and report them so you can decide how to merge duplicates before backfilling.
|
|
15
|
+
|
|
1
16
|
## [0.5.0] - 2026-07-15
|
|
2
17
|
|
|
3
18
|
### BREAKING CHANGES
|
data/README.md
CHANGED
|
@@ -99,6 +99,16 @@ user.has_role?(:manager, :any) # => true
|
|
|
99
99
|
user.only_has_role?(:manager, org) # => true if this is their only role
|
|
100
100
|
```
|
|
101
101
|
|
|
102
|
+
> **Role names are normalized** (`"Admin"`, `"admin"`, `"Admin User"` and
|
|
103
|
+
> `"admin-user"` all resolve to the same role) so casing/formatting mistakes
|
|
104
|
+
> don't silently create duplicate roles. Normalization does **not**
|
|
105
|
+
> singularize — `"sales"` stays `"sales"` — since blindly singularizing
|
|
106
|
+
> arbitrary business terms tends to mangle them. **Upgrading?** Existing rows
|
|
107
|
+
> with mixed-case names aren't rewritten automatically —
|
|
108
|
+
> `rails generate role_fu:upgrade` will detect and warn about them (it won't
|
|
109
|
+
> backfill on its own: two differently-cased roles may already coexist and
|
|
110
|
+
> need a human to decide how to merge them).
|
|
111
|
+
|
|
102
112
|
#### Scopes (Finders)
|
|
103
113
|
|
|
104
114
|
```ruby
|
|
@@ -185,6 +195,36 @@ manager_role.permissions.create(action: "posts.edit")
|
|
|
185
195
|
user.role_fu_can?("posts.edit") # => true
|
|
186
196
|
```
|
|
187
197
|
|
|
198
|
+
**Field-level granularity:**
|
|
199
|
+
|
|
200
|
+
Permissions can optionally be scoped to a single attribute. A permission granted
|
|
201
|
+
*without* a `field` is a wildcard that satisfies any field-scoped check for that
|
|
202
|
+
same action — so existing `action`-only permissions keep working unchanged.
|
|
203
|
+
|
|
204
|
+
```ruby
|
|
205
|
+
support_role.permissions.create(action: "reports.update", field: "status")
|
|
206
|
+
|
|
207
|
+
user.role_fu_can?("reports.update") # => true (can do *something* here)
|
|
208
|
+
user.role_fu_can?("reports.update", field: :status) # => true
|
|
209
|
+
user.role_fu_can?("reports.update", field: :amount) # => false
|
|
210
|
+
|
|
211
|
+
# Build a form / strong params allowlist dynamically:
|
|
212
|
+
user.role_fu_permitted_fields("reports.update")
|
|
213
|
+
# => ["status"] if scoped to specific fields
|
|
214
|
+
# => :all if granted without a field restriction
|
|
215
|
+
# => [] if the action isn't granted at all
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
> **Upgrading an existing `permissions` table?** Run `rails generate role_fu:upgrade`
|
|
219
|
+
> — it inspects your current schema/config and generates only the migrations
|
|
220
|
+
> you're actually missing (here: adding `field` to `permissions`). Safe to
|
|
221
|
+
> re-run any time, regardless of which version you're upgrading from. Without
|
|
222
|
+
> the column, RoleFu transparently falls back to action-only checks.
|
|
223
|
+
|
|
224
|
+
The **CanCanCan adapter** translates field-scoped permissions into CanCanCan's
|
|
225
|
+
own native attribute restriction (`can :update, Report, :status`) instead of
|
|
226
|
+
role_fu re-implementing attribute-level authorization itself.
|
|
227
|
+
|
|
188
228
|
---
|
|
189
229
|
|
|
190
230
|
### Adapters (Pundit & CanCanCan)
|
|
@@ -212,6 +252,40 @@ end
|
|
|
212
252
|
|
|
213
253
|
_`PostPolicy#update?` will automatically check `user.role_fu_can?('posts.update')`._
|
|
214
254
|
|
|
255
|
+
#### Lightweight Guard (no authorization gem)
|
|
256
|
+
|
|
257
|
+
If you don't want to add Pundit or CanCanCan as a dependency just to raise on
|
|
258
|
+
a missing role, `RoleFu::Authorizable` gives you two guard-clause helpers
|
|
259
|
+
built on top of `has_role?` / `role_fu_can?`. **It is intentionally not an
|
|
260
|
+
`allow`/`deny` DSL or a rule-resolution engine** — for anything beyond "raise
|
|
261
|
+
unless this check passes" (policy objects, scopes, composable rules), use the
|
|
262
|
+
Pundit or CanCanCan adapters instead.
|
|
263
|
+
|
|
264
|
+
```ruby
|
|
265
|
+
class ApplicationController < ActionController::Base
|
|
266
|
+
include RoleFu::Authorizable
|
|
267
|
+
|
|
268
|
+
rescue_from RoleFu::AccessDenied, with: :render_forbidden
|
|
269
|
+
|
|
270
|
+
private
|
|
271
|
+
|
|
272
|
+
def render_forbidden
|
|
273
|
+
head :forbidden
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
class PostsController < ApplicationController
|
|
278
|
+
def destroy
|
|
279
|
+
role_fu_authorize!(:admin) # raises RoleFu::AccessDenied unless current_user.has_role?(:admin)
|
|
280
|
+
role_fu_can!("posts.destroy") # raises RoleFu::AccessDenied unless current_user.role_fu_can?("posts.destroy")
|
|
281
|
+
# ...
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Resolves the acting user via `current_user` by default; override
|
|
287
|
+
`role_fu_current_user` for jobs/service objects that don't have one.
|
|
288
|
+
|
|
215
289
|
---
|
|
216
290
|
|
|
217
291
|
### Performance (N+1 Prevention)
|
|
@@ -315,6 +389,24 @@ User.in_group(:admin) # Alias for with_group/with_role
|
|
|
315
389
|
User.not_in_group(:admin) # Alias for without_group/without_role
|
|
316
390
|
```
|
|
317
391
|
|
|
392
|
+
## Upgrading RoleFu
|
|
393
|
+
|
|
394
|
+
After bumping the gem version, run:
|
|
395
|
+
|
|
396
|
+
```bash
|
|
397
|
+
rails generate role_fu:upgrade
|
|
398
|
+
rails db:migrate
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
It inspects your actual schema/config (not "which version you were on" —
|
|
402
|
+
nothing persists that reliably) and generates only what's missing, so it's
|
|
403
|
+
safe to run regardless of how many releases you skipped, and safe to re-run.
|
|
404
|
+
Currently checks for: the `field` column on `permissions` (see
|
|
405
|
+
[Field-level granularity](#4-role-abilities-permissions)), and role names
|
|
406
|
+
that don't match RoleFu's [normalized format](#roleable-user-model) (reported
|
|
407
|
+
only — never auto-merged, since two differently-cased roles may already
|
|
408
|
+
coexist).
|
|
409
|
+
|
|
318
410
|
## Migrating from Rolify
|
|
319
411
|
|
|
320
412
|
1. **Code Changes**: Replace `rolify` with `include RoleFu::Roleable` and `resourcify` with `include RoleFu::Resourceable`.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: ..
|
|
3
3
|
specs:
|
|
4
|
-
role_fu (0.
|
|
4
|
+
role_fu (0.6.0)
|
|
5
5
|
activerecord (>= 7.2)
|
|
6
6
|
|
|
7
7
|
GEM
|
|
@@ -386,7 +386,7 @@ CHECKSUMS
|
|
|
386
386
|
rdoc (8.0.0) sha256=03bf8c08a9639658855a0cfd77c0abca8325c227693f7f33f82957811348c469
|
|
387
387
|
regexp_parser (2.12.0) sha256=35a916a1d63190ab5c9009457136ae5f3c0c7512d60291d0d1378ba18ce08ebb
|
|
388
388
|
reline (0.6.3) sha256=1198b04973565b36ec0f11542ab3f5cfeeec34823f4e54cebde90968092b1835
|
|
389
|
-
role_fu (0.
|
|
389
|
+
role_fu (0.6.0)
|
|
390
390
|
rspec (3.13.2) sha256=206284a08ad798e61f86d7ca3e376718d52c0bc944626b2349266f239f820587
|
|
391
391
|
rspec-core (3.13.6) sha256=a8823c6411667b60a8bca135364351dda34cd55e44ff94c4be4633b37d828b2d
|
|
392
392
|
rspec-expectations (3.13.5) sha256=33a4d3a1d95060aea4c94e9f237030a8f9eae5615e9bd85718fe3a09e4b58836
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: ..
|
|
3
3
|
specs:
|
|
4
|
-
role_fu (0.
|
|
4
|
+
role_fu (0.6.0)
|
|
5
5
|
activerecord (>= 7.2)
|
|
6
6
|
|
|
7
7
|
GEM
|
|
@@ -382,7 +382,7 @@ CHECKSUMS
|
|
|
382
382
|
rdoc (8.0.0) sha256=03bf8c08a9639658855a0cfd77c0abca8325c227693f7f33f82957811348c469
|
|
383
383
|
regexp_parser (2.12.0) sha256=35a916a1d63190ab5c9009457136ae5f3c0c7512d60291d0d1378ba18ce08ebb
|
|
384
384
|
reline (0.6.3) sha256=1198b04973565b36ec0f11542ab3f5cfeeec34823f4e54cebde90968092b1835
|
|
385
|
-
role_fu (0.
|
|
385
|
+
role_fu (0.6.0)
|
|
386
386
|
rspec (3.13.2) sha256=206284a08ad798e61f86d7ca3e376718d52c0bc944626b2349266f239f820587
|
|
387
387
|
rspec-core (3.13.6) sha256=a8823c6411667b60a8bca135364351dda34cd55e44ff94c4be4633b37d828b2d
|
|
388
388
|
rspec-expectations (3.13.5) sha256=33a4d3a1d95060aea4c94e9f237030a8f9eae5615e9bd85718fe3a09e4b58836
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: ..
|
|
3
3
|
specs:
|
|
4
|
-
role_fu (0.
|
|
4
|
+
role_fu (0.6.0)
|
|
5
5
|
activerecord (>= 7.2)
|
|
6
6
|
|
|
7
7
|
GEM
|
|
@@ -384,7 +384,7 @@ CHECKSUMS
|
|
|
384
384
|
rdoc (8.0.0) sha256=03bf8c08a9639658855a0cfd77c0abca8325c227693f7f33f82957811348c469
|
|
385
385
|
regexp_parser (2.12.0) sha256=35a916a1d63190ab5c9009457136ae5f3c0c7512d60291d0d1378ba18ce08ebb
|
|
386
386
|
reline (0.6.3) sha256=1198b04973565b36ec0f11542ab3f5cfeeec34823f4e54cebde90968092b1835
|
|
387
|
-
role_fu (0.
|
|
387
|
+
role_fu (0.6.0)
|
|
388
388
|
rspec (3.13.2) sha256=206284a08ad798e61f86d7ca3e376718d52c0bc944626b2349266f239f820587
|
|
389
389
|
rspec-core (3.13.6) sha256=a8823c6411667b60a8bca135364351dda34cd55e44ff94c4be4633b37d828b2d
|
|
390
390
|
rspec-expectations (3.13.5) sha256=33a4d3a1d95060aea4c94e9f237030a8f9eae5615e9bd85718fe3a09e4b58836
|
|
@@ -5,11 +5,12 @@ class RoleFuCreatePermissions < ActiveRecord::Migration<%= migration_version %>
|
|
|
5
5
|
create_table :permissions<%= ", id: :uuid" if uuid_enabled? %> do |t|
|
|
6
6
|
t.references :role, null: false, index: true<%= ", type: :uuid" if uuid_enabled? %>
|
|
7
7
|
t.string :action, null: false
|
|
8
|
+
t.string :field
|
|
8
9
|
t.jsonb :conditions, default: {}
|
|
9
10
|
|
|
10
11
|
t.timestamps
|
|
11
12
|
end
|
|
12
|
-
|
|
13
|
-
add_index :permissions, [:role_id, :action]
|
|
13
|
+
|
|
14
|
+
add_index :permissions, [:role_id, :action, :field]
|
|
14
15
|
end
|
|
15
16
|
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators/active_record"
|
|
4
|
+
|
|
5
|
+
module RoleFu
|
|
6
|
+
module Generators
|
|
7
|
+
# Run after bumping the role_fu gem version. Does not track "which
|
|
8
|
+
# version you upgraded from" - there's nowhere reliable to read that from
|
|
9
|
+
# (Gemfile.lock only ever has the current version). Instead it inspects
|
|
10
|
+
# the actual schema/config already installed and generates whatever is
|
|
11
|
+
# missing, so it's safe to run regardless of how many versions you skipped,
|
|
12
|
+
# and safe to run again if nothing changed.
|
|
13
|
+
class UpgradeGenerator < ActiveRecord::Generators::Base
|
|
14
|
+
source_root File.expand_path("templates", __dir__)
|
|
15
|
+
|
|
16
|
+
argument :name, type: :string, default: "upgrade"
|
|
17
|
+
|
|
18
|
+
def self.banner
|
|
19
|
+
"bin/rails generate role_fu:upgrade\n\n" \
|
|
20
|
+
"Detects which optional role_fu columns/config are missing for your\n" \
|
|
21
|
+
"installed models and generates the migrations needed to catch up."
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
desc ""
|
|
25
|
+
|
|
26
|
+
def run_upgrade_checks
|
|
27
|
+
applied = upgrade_checks.count { |check| send(check) }
|
|
28
|
+
|
|
29
|
+
say "role_fu is already up to date - nothing to generate.", :green if applied.zero?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
# Each check returns true when it generated something or has a warning
|
|
35
|
+
# to report. Add new entries here as future optional columns/config land.
|
|
36
|
+
def upgrade_checks
|
|
37
|
+
[:check_permissions_field, :check_denormalized_role_names]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def check_permissions_field
|
|
41
|
+
return false unless table_exists?(:permissions)
|
|
42
|
+
return false if column_exists?(:permissions, :field)
|
|
43
|
+
|
|
44
|
+
say "Adding missing 'field' column to permissions (see README: Field-level granularity)", :yellow
|
|
45
|
+
migration_template "upgrade_permissions_field_migration.rb.erb", "db/migrate/role_fu_add_field_to_permissions.rb"
|
|
46
|
+
true
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Not auto-fixed: blindly renaming could collide two existing roles
|
|
50
|
+
# (e.g. "Admin" and "admin" both already present with their own
|
|
51
|
+
# role_assignments) into one, which needs a human to decide how to
|
|
52
|
+
# merge rather than a migration silently doing it.
|
|
53
|
+
def check_denormalized_role_names
|
|
54
|
+
role_class = RoleFu.configuration.role_class_name.safe_constantize
|
|
55
|
+
return false unless role_class&.table_exists?
|
|
56
|
+
|
|
57
|
+
denormalized = role_class.pluck(:name).any? { |name| name != RoleFu.normalize_role_name(name) }
|
|
58
|
+
return false unless denormalized
|
|
59
|
+
|
|
60
|
+
say "Found role names that don't match RoleFu's normalized format (e.g. \"Admin\" vs \"admin\").", :yellow
|
|
61
|
+
say "Not fixed automatically - two differently-cased roles may already coexist and need a human merge decision.", :yellow
|
|
62
|
+
say "Review, then backfill e.g.: UPDATE #{role_class.table_name} SET name = LOWER(name) WHERE name != LOWER(name)", :yellow
|
|
63
|
+
true
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def table_exists?(name)
|
|
67
|
+
ActiveRecord::Base.connection.table_exists?(name.to_s)
|
|
68
|
+
rescue
|
|
69
|
+
false
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def column_exists?(table, column)
|
|
73
|
+
ActiveRecord::Base.connection.column_exists?(table.to_s, column)
|
|
74
|
+
rescue
|
|
75
|
+
false
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def migration_version
|
|
79
|
+
"[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]" if Rails::VERSION::MAJOR >= 5
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
data/lib/role_fu/ability.rb
CHANGED
|
@@ -4,20 +4,55 @@ module RoleFu
|
|
|
4
4
|
module Ability
|
|
5
5
|
extend ActiveSupport::Concern
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
# @param action [String, Symbol] e.g. "posts.update"
|
|
8
|
+
# @param field [String, Symbol, nil] restrict the check to a single attribute
|
|
9
|
+
# (e.g. :description). A permission granted with field: nil is a wildcard
|
|
10
|
+
# that satisfies any field-scoped check for the same action.
|
|
11
|
+
def role_fu_can?(action, _resource = nil, field: nil)
|
|
12
|
+
fields = role_fu_permission_fields(action)
|
|
13
|
+
return false if fields.nil?
|
|
14
|
+
|
|
15
|
+
field.nil? || fields.include?(nil) || fields.include?(field.to_s)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Fields explicitly granted for `action`.
|
|
19
|
+
# Returns :all when the action was granted without a field restriction,
|
|
20
|
+
# [] when the action isn't granted at all, or the explicit field list otherwise.
|
|
21
|
+
def role_fu_permitted_fields(action)
|
|
22
|
+
fields = role_fu_permission_fields(action)
|
|
23
|
+
return [] if fields.nil?
|
|
24
|
+
return :all if fields.include?(nil)
|
|
25
|
+
|
|
26
|
+
fields.to_a
|
|
9
27
|
end
|
|
10
28
|
|
|
11
29
|
def role_fu_permissions
|
|
30
|
+
role_fu_permissions_index.keys.to_set
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def role_fu_permission_fields(action)
|
|
36
|
+
role_fu_permissions_index[action.to_s]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def role_fu_permissions_index
|
|
12
40
|
return @_role_fu_permissions if defined?(@_role_fu_permissions) && @_role_fu_permissions
|
|
13
41
|
|
|
14
42
|
permission_class = "Permission".safe_constantize
|
|
15
|
-
return
|
|
43
|
+
return (@_role_fu_permissions = {}) unless permission_class
|
|
16
44
|
|
|
17
45
|
scope = roles
|
|
18
46
|
scope = filter_expired(scope) if respond_to?(:filter_expired, true)
|
|
19
47
|
|
|
20
|
-
|
|
48
|
+
has_field_column = permission_class.column_names.include?("field")
|
|
49
|
+
columns = has_field_column ? %w[permissions.action permissions.field] : %w[permissions.action]
|
|
50
|
+
rows = scope.joins(:permissions).pluck(*columns)
|
|
51
|
+
|
|
52
|
+
@_role_fu_permissions = rows.each_with_object({}) do |row, index|
|
|
53
|
+
action, field = has_field_column ? row : [row, nil]
|
|
54
|
+
(index[action.to_s] ||= Set.new) << field
|
|
55
|
+
end
|
|
21
56
|
end
|
|
22
57
|
end
|
|
23
58
|
end
|
|
@@ -13,11 +13,20 @@ module RoleFu
|
|
|
13
13
|
|
|
14
14
|
if parts.size == 2
|
|
15
15
|
subject_name, rule = parts
|
|
16
|
-
begin
|
|
17
|
-
|
|
18
|
-
can rule.to_sym, subject_class
|
|
16
|
+
subject_class = begin
|
|
17
|
+
subject_name.classify.constantize
|
|
19
18
|
rescue NameError
|
|
20
|
-
|
|
19
|
+
subject_name.to_sym
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Field-scoped permissions map onto CanCanCan's own attribute
|
|
23
|
+
# restriction (`can :update, Post, :title`) instead of role_fu
|
|
24
|
+
# re-implementing attribute authorization itself.
|
|
25
|
+
fields = user.role_fu_permitted_fields(action)
|
|
26
|
+
if fields == :all
|
|
27
|
+
can rule.to_sym, subject_class
|
|
28
|
+
else
|
|
29
|
+
can rule.to_sym, subject_class, *fields.map(&:to_sym)
|
|
21
30
|
end
|
|
22
31
|
else
|
|
23
32
|
can action.to_sym, :all
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RoleFu
|
|
4
|
+
# Raised by RoleFu::Authorizable guard methods. Rescue it yourself
|
|
5
|
+
# (e.g. `rescue_from RoleFu::AccessDenied` in ApplicationController) —
|
|
6
|
+
# role_fu does not register a rescue handler on your behalf.
|
|
7
|
+
class AccessDenied < RoleFu::Error
|
|
8
|
+
def initialize(message = "You are not authorized to perform this action.")
|
|
9
|
+
super
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Minimal, framework-agnostic authorization guard for apps that don't want
|
|
14
|
+
# to pull in a full authorization gem just to raise on a missing role.
|
|
15
|
+
#
|
|
16
|
+
# This is deliberately NOT an `allow`/`deny` DSL or a rule-resolution
|
|
17
|
+
# engine — for anything beyond "raise unless this check passes", reach for
|
|
18
|
+
# the Pundit or CanCanCan adapters instead. Mixing this concern into a
|
|
19
|
+
# controller (or any object that responds to `current_user`, or overrides
|
|
20
|
+
# `role_fu_current_user`) just gives you two guard-clause helpers built on
|
|
21
|
+
# top of the `has_role?` / `role_fu_can?` methods you already have.
|
|
22
|
+
module Authorizable
|
|
23
|
+
extend ActiveSupport::Concern
|
|
24
|
+
|
|
25
|
+
# Override this if the current user isn't exposed via `current_user`
|
|
26
|
+
# (e.g. in a job or service object).
|
|
27
|
+
def role_fu_current_user
|
|
28
|
+
current_user if respond_to?(:current_user)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def role_fu_authorize!(role_name, resource = nil)
|
|
32
|
+
user = role_fu_current_user
|
|
33
|
+
raise RoleFu::AccessDenied unless user&.respond_to?(:has_role?) && user.has_role?(role_name, resource)
|
|
34
|
+
|
|
35
|
+
true
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def role_fu_can!(action, resource = nil, field: nil)
|
|
39
|
+
user = role_fu_current_user
|
|
40
|
+
raise RoleFu::AccessDenied unless user&.respond_to?(:role_fu_can?) && user.role_fu_can?(action, resource, field: field)
|
|
41
|
+
|
|
42
|
+
true
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
data/lib/role_fu/role.rb
CHANGED
|
@@ -15,7 +15,15 @@ module RoleFu
|
|
|
15
15
|
|
|
16
16
|
belongs_to :resource, polymorphic: true, optional: true
|
|
17
17
|
|
|
18
|
+
before_validation :normalize_role_fu_name
|
|
19
|
+
|
|
18
20
|
validates :name, presence: true, uniqueness: {scope: [:resource_type, :resource_id]}
|
|
19
21
|
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def normalize_role_fu_name
|
|
26
|
+
self.name = RoleFu.normalize_role_name(name) if name.present?
|
|
27
|
+
end
|
|
20
28
|
end
|
|
21
29
|
end
|
data/lib/role_fu/roleable.rb
CHANGED
|
@@ -24,7 +24,7 @@ module RoleFu
|
|
|
24
24
|
role_table = RoleFu.role_class.table_name
|
|
25
25
|
assignment_table = RoleFu.role_assignment_class.table_name
|
|
26
26
|
|
|
27
|
-
query = joins(:roles).where(role_table => {name: role_name
|
|
27
|
+
query = joins(:roles).where(role_table => {name: RoleFu.normalize_role_name(role_name)})
|
|
28
28
|
|
|
29
29
|
if RoleFu.role_assignment_class.column_names.include?("expires_at")
|
|
30
30
|
query = query.where("#{assignment_table}.expires_at IS NULL OR #{assignment_table}.expires_at > ?", Time.current)
|
|
@@ -136,7 +136,7 @@ module RoleFu
|
|
|
136
136
|
return false if role_name.nil?
|
|
137
137
|
|
|
138
138
|
if resource == :any
|
|
139
|
-
filter_expired(roles.where(name: role_name
|
|
139
|
+
filter_expired(roles.where(name: RoleFu.normalize_role_name(role_name))).exists?
|
|
140
140
|
else
|
|
141
141
|
return true if filter_expired(find_roles(role_name, resource)).any?
|
|
142
142
|
|
|
@@ -157,7 +157,7 @@ module RoleFu
|
|
|
157
157
|
end
|
|
158
158
|
|
|
159
159
|
def has_cached_role?(role_name, resource = nil)
|
|
160
|
-
role_name = role_name
|
|
160
|
+
role_name = RoleFu.normalize_role_name(role_name)
|
|
161
161
|
roles.to_a.any? do |role|
|
|
162
162
|
next false unless role.name == role_name
|
|
163
163
|
|
|
@@ -241,14 +241,14 @@ module RoleFu
|
|
|
241
241
|
|
|
242
242
|
def find_or_create_role(role_name, resource)
|
|
243
243
|
RoleFu.role_class.find_or_create_by(
|
|
244
|
-
name: role_name
|
|
244
|
+
name: RoleFu.normalize_role_name(role_name),
|
|
245
245
|
resource_type: resource_type_for(resource),
|
|
246
246
|
resource_id: resource_id_for(resource)
|
|
247
247
|
)
|
|
248
248
|
end
|
|
249
249
|
|
|
250
250
|
def find_roles(role_name, resource)
|
|
251
|
-
query = roles.where(name: role_name
|
|
251
|
+
query = roles.where(name: RoleFu.normalize_role_name(role_name))
|
|
252
252
|
|
|
253
253
|
if resource.is_a?(Class)
|
|
254
254
|
query.where(resource_type: resource.to_s, resource_id: nil)
|
data/lib/role_fu/version.rb
CHANGED
data/lib/role_fu.rb
CHANGED
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
require "active_support/concern"
|
|
4
4
|
require "active_support/core_ext/string/inflections"
|
|
5
5
|
|
|
6
|
+
module RoleFu
|
|
7
|
+
class Error < StandardError; end
|
|
8
|
+
end
|
|
9
|
+
|
|
6
10
|
require_relative "role_fu/version"
|
|
7
11
|
require_relative "role_fu/configuration"
|
|
8
12
|
require_relative "role_fu/role"
|
|
@@ -11,14 +15,13 @@ require_relative "role_fu/roleable"
|
|
|
11
15
|
require_relative "role_fu/resourceable"
|
|
12
16
|
require_relative "role_fu/permission"
|
|
13
17
|
require_relative "role_fu/ability"
|
|
18
|
+
require_relative "role_fu/authorizable"
|
|
14
19
|
require_relative "role_fu/cleanup"
|
|
15
20
|
require_relative "role_fu/adapters/cancancan"
|
|
16
21
|
require_relative "role_fu/adapters/pundit"
|
|
17
22
|
require_relative "role_fu/railtie" if defined?(Rails)
|
|
18
23
|
|
|
19
24
|
module RoleFu
|
|
20
|
-
class Error < StandardError; end
|
|
21
|
-
|
|
22
25
|
class << self
|
|
23
26
|
def with_actor(actor)
|
|
24
27
|
Thread.current[:role_fu_actor] = actor
|
|
@@ -30,5 +33,15 @@ module RoleFu
|
|
|
30
33
|
def current_actor
|
|
31
34
|
Thread.current[:role_fu_actor]
|
|
32
35
|
end
|
|
36
|
+
|
|
37
|
+
# Canonicalizes a role name so that "Admin", "admin", "Admin User" and
|
|
38
|
+
# "admin-user" all resolve to the same underlying Role row.
|
|
39
|
+
# Note: does NOT singularize (unlike acl9) — that silently mangles
|
|
40
|
+
# ordinary business terms (e.g. "sales" -> "sale").
|
|
41
|
+
def normalize_role_name(name)
|
|
42
|
+
return nil if name.nil?
|
|
43
|
+
|
|
44
|
+
name.to_s.strip.gsub(/[\s-]+/, "_").underscore
|
|
45
|
+
end
|
|
33
46
|
end
|
|
34
47
|
end
|
data/mise.toml
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
[tools]
|
|
2
|
+
ruby = "4.0.6"
|
|
3
|
+
|
|
4
|
+
[tasks.test]
|
|
5
|
+
description = "Run the RSpec suite"
|
|
6
|
+
run = "bundle exec rake spec"
|
|
7
|
+
|
|
8
|
+
[tasks.lint]
|
|
9
|
+
description = "Run RuboCop"
|
|
10
|
+
run = "bundle exec rubocop"
|
|
11
|
+
|
|
12
|
+
[tasks.release]
|
|
13
|
+
description = "Build and publish the gem (rake release)"
|
|
14
|
+
run = "bundle exec rake release"
|
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.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alexey Poimtsev
|
|
@@ -116,6 +116,7 @@ executables: []
|
|
|
116
116
|
extensions: []
|
|
117
117
|
extra_rdoc_files: []
|
|
118
118
|
files:
|
|
119
|
+
- ".ruby-version"
|
|
119
120
|
- Appraisals
|
|
120
121
|
- CHANGELOG.md
|
|
121
122
|
- LICENSE.txt
|
|
@@ -142,10 +143,13 @@ files:
|
|
|
142
143
|
- lib/generators/role_fu/templates/role.rb.erb
|
|
143
144
|
- lib/generators/role_fu/templates/role_assignment.rb.erb
|
|
144
145
|
- lib/generators/role_fu/templates/role_fu.rb
|
|
146
|
+
- lib/generators/role_fu/templates/upgrade_permissions_field_migration.rb.erb
|
|
147
|
+
- lib/generators/role_fu/upgrade_generator.rb
|
|
145
148
|
- lib/role_fu.rb
|
|
146
149
|
- lib/role_fu/ability.rb
|
|
147
150
|
- lib/role_fu/adapters/cancancan.rb
|
|
148
151
|
- lib/role_fu/adapters/pundit.rb
|
|
152
|
+
- lib/role_fu/authorizable.rb
|
|
149
153
|
- lib/role_fu/cleanup.rb
|
|
150
154
|
- lib/role_fu/configuration.rb
|
|
151
155
|
- lib/role_fu/permission.rb
|
|
@@ -156,6 +160,7 @@ files:
|
|
|
156
160
|
- lib/role_fu/roleable.rb
|
|
157
161
|
- lib/role_fu/version.rb
|
|
158
162
|
- lib/tasks/role_fu.rake
|
|
163
|
+
- mise.toml
|
|
159
164
|
- sig/role_fu.rbs
|
|
160
165
|
homepage: https://github.com/alec-c4/role_fu
|
|
161
166
|
licenses:
|
|
@@ -164,6 +169,17 @@ metadata:
|
|
|
164
169
|
homepage_uri: https://github.com/alec-c4/role_fu
|
|
165
170
|
source_code_uri: https://github.com/alec-c4/role_fu
|
|
166
171
|
changelog_uri: https://github.com/alec-c4/role_fu/blob/main/CHANGELOG.md
|
|
172
|
+
post_install_message: |
|
|
173
|
+
Thanks for installing role_fu 0.6.0!
|
|
174
|
+
|
|
175
|
+
Upgrading from an older role_fu? Run this to pick up any missing
|
|
176
|
+
schema/data changes (it inspects your app, so it's a no-op if there's
|
|
177
|
+
nothing to do):
|
|
178
|
+
|
|
179
|
+
rails generate role_fu:upgrade
|
|
180
|
+
rails db:migrate
|
|
181
|
+
|
|
182
|
+
Full changelog: https://github.com/alec-c4/role_fu/blob/main/CHANGELOG.md
|
|
167
183
|
rdoc_options: []
|
|
168
184
|
require_paths:
|
|
169
185
|
- lib
|
|
@@ -178,7 +194,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
178
194
|
- !ruby/object:Gem::Version
|
|
179
195
|
version: '0'
|
|
180
196
|
requirements: []
|
|
181
|
-
rubygems_version: 4.0.
|
|
197
|
+
rubygems_version: 4.0.20
|
|
182
198
|
specification_version: 4
|
|
183
199
|
summary: A modern role management gem for Rails, replacing rolify.
|
|
184
200
|
test_files: []
|