role_fu 0.4.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 +27 -0
- data/README.md +92 -0
- data/gemfiles/rails_7.2.gemfile.lock +116 -123
- data/gemfiles/rails_8.0.gemfile.lock +180 -187
- data/gemfiles/rails_8.1.gemfile.lock +182 -189
- 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 +19 -3
|
@@ -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
|
|
@@ -171,14 +187,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
171
187
|
requirements:
|
|
172
188
|
- - ">="
|
|
173
189
|
- !ruby/object:Gem::Version
|
|
174
|
-
version: 3.
|
|
190
|
+
version: 3.3.0
|
|
175
191
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
176
192
|
requirements:
|
|
177
193
|
- - ">="
|
|
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: []
|