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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: baa4ff8c1fb945a9a511b86be18ecf899e03b72713faebaa2f903068a556fd26
4
- data.tar.gz: cc7f9603db34f8363232db64520bfbc2f80cdd67b7488a508555dffaeba47fcf
3
+ metadata.gz: 8895d1ec974d84a207c9a1ef584910237b328442cb76f78d84a382b640527e43
4
+ data.tar.gz: da9cf4d703ecc9eef0e92bf2c883dc6cdacd49cd96e8c898289980465f96b445
5
5
  SHA512:
6
- metadata.gz: 6fc1e0fccbd72b33b2d6df3d4ccee047d513347dae12f6ab05a8b8626c7d2e115e0206044a02b72b1443cde6628686de1898ccce982859762b724c444eca6dcf
7
- data.tar.gz: c4ab6403ff5658dfa5f9a7335da11fabeaea677e5b33c4b42353e8ec1e310deb64f3157da7420ce3ae310ca9aee34b60ed77372804dbe6d526816fe78fa2d812
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.5.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.5.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.5.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.5.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.5.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.5.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,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RoleFuAddFieldToPermissions < ActiveRecord::Migration<%= migration_version %>
4
+ def change
5
+ add_column :permissions, :field, :string
6
+ add_index :permissions, [:role_id, :action, :field]
7
+ end
8
+ 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
@@ -4,20 +4,55 @@ module RoleFu
4
4
  module Ability
5
5
  extend ActiveSupport::Concern
6
6
 
7
- def role_fu_can?(action, _resource = nil)
8
- role_fu_permissions.include?(action.to_s)
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 Set.new unless permission_class
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
- @_role_fu_permissions = scope.joins(:permissions).pluck("permissions.action").map(&:to_s).to_set
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
- subject_class = subject_name.classify.constantize
18
- can rule.to_sym, subject_class
16
+ subject_class = begin
17
+ subject_name.classify.constantize
19
18
  rescue NameError
20
- can rule.to_sym, subject_name.to_sym
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
@@ -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.to_s})
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.to_s)).exists?
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.to_s
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.to_s,
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.to_s)
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)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RoleFu
4
- VERSION = "0.5.0"
4
+ VERSION = "0.6.0"
5
5
  end
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.5.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.16
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: []