rbacan 0.3.2 → 0.5.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/CHANGELOG.md +53 -0
- data/README.md +97 -15
- data/app/models/rbacan/role.rb +22 -9
- data/app/models/rbacan/user_role.rb +8 -5
- data/lib/generators/install/templates/create_permissions.rb +1 -1
- data/lib/generators/install/templates/create_role_permissions.rb +3 -3
- data/lib/generators/install/templates/create_roles.rb +10 -1
- data/lib/generators/install/templates/create_user_roles.rb +11 -4
- data/lib/generators/install/templates/rbacan.rb +32 -17
- data/lib/generators/rbacan/install_generator.rb +26 -7
- data/lib/rbacan/authorizable.rb +15 -0
- data/lib/rbacan/authorization.rb +37 -9
- data/lib/rbacan/host_authorization.rb +165 -0
- data/lib/rbacan/not_authorized.rb +10 -3
- data/lib/rbacan/permittable.rb +74 -36
- data/lib/rbacan/roles_and_permissions.rb +12 -4
- data/lib/rbacan/route_constraint.rb +21 -18
- data/lib/rbacan/version.rb +1 -1
- data/lib/rbacan/view_helpers.rb +13 -8
- data/lib/rbacan.rb +63 -14
- metadata +37 -44
- data/.gitignore +0 -11
- data/.rspec +0 -3
- data/.travis.yml +0 -7
- data/Gemfile +0 -4
- data/Gemfile.lock +0 -319
- data/Rakefile +0 -6
- data/bin/console +0 -14
- data/bin/setup +0 -8
- data/rbacan.gemspec +0 -42
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
module Rbacan
|
|
2
|
+
class ConfigurationError < StandardError; end
|
|
3
|
+
class UnsafeLegacyApi < StandardError; end
|
|
4
|
+
|
|
5
|
+
class AuthorizationContext
|
|
6
|
+
ATTRIBUTES = %i[tenant property vertical subject at metadata].freeze
|
|
7
|
+
|
|
8
|
+
attr_reader(*ATTRIBUTES)
|
|
9
|
+
|
|
10
|
+
def initialize(tenant:, property: nil, vertical: nil, subject: nil, at: Time.now, metadata: {})
|
|
11
|
+
raise ArgumentError, 'authorization tenant must identify a tenant' unless identifying_tenant?(tenant)
|
|
12
|
+
|
|
13
|
+
@tenant = deep_copy_and_freeze(tenant, label: 'authorization tenant')
|
|
14
|
+
@property = deep_copy_and_freeze(property, label: 'authorization property')
|
|
15
|
+
@vertical = deep_copy_and_freeze(vertical, label: 'authorization vertical')
|
|
16
|
+
@subject = deep_copy_and_freeze(subject, label: 'authorization subject')
|
|
17
|
+
@at = copy_and_freeze_time(at)
|
|
18
|
+
@metadata = deep_copy_and_freeze(metadata, label: 'authorization metadata')
|
|
19
|
+
freeze
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def deep_copy_and_freeze(value, label:)
|
|
25
|
+
copy = case value
|
|
26
|
+
when Hash
|
|
27
|
+
value.to_h do |key, item|
|
|
28
|
+
[ deep_copy_and_freeze(key, label: label), deep_copy_and_freeze(item, label: label) ]
|
|
29
|
+
end
|
|
30
|
+
when Array
|
|
31
|
+
value.map { |item| deep_copy_and_freeze(item, label: label) }
|
|
32
|
+
when String
|
|
33
|
+
value.dup
|
|
34
|
+
when Integer
|
|
35
|
+
value
|
|
36
|
+
when Float
|
|
37
|
+
raise ArgumentError, "#{label} supports only finite JSON numbers" unless value.finite?
|
|
38
|
+
|
|
39
|
+
value
|
|
40
|
+
when NilClass, TrueClass, FalseClass, Symbol
|
|
41
|
+
value
|
|
42
|
+
else
|
|
43
|
+
raise ArgumentError,
|
|
44
|
+
"#{label} supports only hashes, arrays, strings, numbers, booleans, nil, and symbols"
|
|
45
|
+
end
|
|
46
|
+
copy.freeze
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def copy_and_freeze_time(value)
|
|
50
|
+
time = value.to_time if value.respond_to?(:to_time)
|
|
51
|
+
raise ArgumentError, 'authorization timestamp must be time-like' unless time.is_a?(Time)
|
|
52
|
+
|
|
53
|
+
time.dup.freeze
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def identifying_tenant?(value)
|
|
57
|
+
case value
|
|
58
|
+
when String, Array, Hash
|
|
59
|
+
!value.empty?
|
|
60
|
+
when Integer
|
|
61
|
+
true
|
|
62
|
+
when Float
|
|
63
|
+
value.finite?
|
|
64
|
+
when Symbol
|
|
65
|
+
!value.to_s.empty?
|
|
66
|
+
else
|
|
67
|
+
false
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
class AuthorizationRequest
|
|
73
|
+
attr_reader :user, :permission, :role, :context
|
|
74
|
+
|
|
75
|
+
def initialize(user:, permission:, role:, context:)
|
|
76
|
+
@user = user
|
|
77
|
+
@permission = permission&.to_s&.dup&.freeze
|
|
78
|
+
@role = role&.to_s&.dup&.freeze
|
|
79
|
+
@context = context
|
|
80
|
+
freeze
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
class AssignmentRequest
|
|
85
|
+
attr_reader :user, :role, :context
|
|
86
|
+
|
|
87
|
+
def initialize(user:, role:, context:)
|
|
88
|
+
@user = user
|
|
89
|
+
@role = role.to_s.dup.freeze
|
|
90
|
+
@context = context
|
|
91
|
+
freeze
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
class << self
|
|
96
|
+
def ensure_legacy_mode!(method_name)
|
|
97
|
+
return if legacy_mode
|
|
98
|
+
|
|
99
|
+
raise UnsafeLegacyApi,
|
|
100
|
+
"#{method_name} is a legacy unscoped API; set Rbacan.legacy_mode = true to opt in"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def authorized?(user:, permission: nil, role: nil, context:)
|
|
104
|
+
validate_authorization_target!(permission, role)
|
|
105
|
+
validate_context!(context)
|
|
106
|
+
return false unless user && authorization_adapter
|
|
107
|
+
|
|
108
|
+
request = AuthorizationRequest.new(
|
|
109
|
+
user: user,
|
|
110
|
+
permission: permission&.to_s,
|
|
111
|
+
role: role&.to_s,
|
|
112
|
+
context: context
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
!!call_authorization_adapter(request)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def assign_role(user:, role:, context:)
|
|
119
|
+
call_assignment_adapter(:assign, user: user, role: role, context: context)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def revoke_role(user:, role:, context:)
|
|
123
|
+
call_assignment_adapter(:revoke, user: user, role: role, context: context)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
private
|
|
127
|
+
|
|
128
|
+
def validate_authorization_target!(permission, role)
|
|
129
|
+
if permission.nil? ^ role.nil?
|
|
130
|
+
target = permission || role
|
|
131
|
+
raise ArgumentError, 'authorization permission or role cannot be blank' if target.to_s.empty?
|
|
132
|
+
|
|
133
|
+
return
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
raise ArgumentError, 'Provide exactly one permission or role'
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def validate_context!(context)
|
|
140
|
+
raise ArgumentError, 'authorization context is required' unless context.is_a?(AuthorizationContext)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def call_authorization_adapter(request)
|
|
144
|
+
if authorization_adapter.respond_to?(:allowed?)
|
|
145
|
+
authorization_adapter.allowed?(request)
|
|
146
|
+
elsif authorization_adapter.respond_to?(:call)
|
|
147
|
+
authorization_adapter.call(request)
|
|
148
|
+
else
|
|
149
|
+
raise ConfigurationError, 'authorization adapter must respond to allowed? or call'
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def call_assignment_adapter(action, user:, role:, context:)
|
|
154
|
+
validate_context!(context)
|
|
155
|
+
raise ArgumentError, 'assignment user is required' unless user
|
|
156
|
+
raise ArgumentError, 'assignment role is required' if role.nil? || role.to_s.empty?
|
|
157
|
+
unless assignment_adapter&.respond_to?(action)
|
|
158
|
+
raise ConfigurationError, "assignment adapter must respond to #{action}"
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
request = AssignmentRequest.new(user: user, role: role.to_s, context: context)
|
|
162
|
+
assignment_adapter.public_send(action, request)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
module Rbacan
|
|
2
2
|
class NotAuthorized < StandardError
|
|
3
|
-
attr_reader :permission, :role
|
|
3
|
+
attr_reader :permission, :role, :code
|
|
4
4
|
|
|
5
5
|
def initialize(message = nil, permission: nil, role: nil)
|
|
6
6
|
@permission = permission
|
|
7
|
-
@role
|
|
7
|
+
@role = role
|
|
8
|
+
@code = if permission
|
|
9
|
+
'rbacan.not_authorized.permission'
|
|
10
|
+
elsif role
|
|
11
|
+
'rbacan.not_authorized.role'
|
|
12
|
+
else
|
|
13
|
+
'rbacan.not_authorized'
|
|
14
|
+
end
|
|
8
15
|
super(message || default_message)
|
|
9
16
|
end
|
|
10
17
|
|
|
@@ -16,7 +23,7 @@ module Rbacan
|
|
|
16
23
|
elsif role
|
|
17
24
|
"Not authorized: missing role '#{role}'"
|
|
18
25
|
else
|
|
19
|
-
|
|
26
|
+
'Not authorized'
|
|
20
27
|
end
|
|
21
28
|
end
|
|
22
29
|
end
|
data/lib/rbacan/permittable.rb
CHANGED
|
@@ -7,72 +7,110 @@ module Rbacan
|
|
|
7
7
|
included do
|
|
8
8
|
has_many :user_roles,
|
|
9
9
|
class_name: Rbacan.user_role_class,
|
|
10
|
-
dependent:
|
|
10
|
+
dependent: :destroy
|
|
11
11
|
accepts_nested_attributes_for :user_roles
|
|
12
12
|
has_many :roles,
|
|
13
13
|
class_name: Rbacan.role_class,
|
|
14
|
-
through:
|
|
14
|
+
through: :user_roles
|
|
15
15
|
|
|
16
16
|
# Returns users that have the given role (by name).
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
scope :with_role, lambda { |role_name|
|
|
18
|
+
Rbacan.ensure_legacy_mode!(:with_role)
|
|
19
|
+
joins(:roles).where(Rbacan.role_table => { Rbacan.role_key => role_name.to_s })
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
# Returns users that have the given permission via any of their roles.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
scope :with_permission, lambda { |permission_name|
|
|
24
|
+
Rbacan.ensure_legacy_mode!(:with_permission)
|
|
25
|
+
joins(roles: { role_permissions: :permission })
|
|
26
|
+
.where(Rbacan.permission_table => { Rbacan.permission_key => permission_name.to_s })
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
# Assigns a role to the user. Idempotent — safe to call multiple times.
|
|
28
|
-
#
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
# When tenant_scoped is enabled, pass tenant_id: to scope the assignment.
|
|
31
|
+
def assign_role(role_name, tenant_id: nil)
|
|
32
|
+
Rbacan.ensure_legacy_mode!(:assign_role)
|
|
33
|
+
assigned_role = Rbacan.role_class.constantize.find_by(Rbacan.role_key => role_name.to_s)
|
|
31
34
|
raise ArgumentError, "Role '#{role_name}' not found" unless assigned_role
|
|
32
35
|
|
|
33
|
-
|
|
36
|
+
attrs = { role_id: assigned_role.id }
|
|
37
|
+
attrs[:tenant_id] = tenant_id if Rbacan.tenant_scoped
|
|
38
|
+
user_roles.find_or_create_by(attrs)
|
|
34
39
|
end
|
|
35
40
|
|
|
36
41
|
# Removes a role from the user.
|
|
37
|
-
#
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
42
|
+
# When tenant_scoped is enabled, pass tenant_id: to remove from a specific tenant.
|
|
43
|
+
def remove_role(role_name, tenant_id: nil)
|
|
44
|
+
Rbacan.ensure_legacy_mode!(:remove_role)
|
|
45
|
+
removed_role = Rbacan.role_class.constantize.find_by(Rbacan.role_key => role_name.to_s)
|
|
41
46
|
return unless removed_role
|
|
42
47
|
|
|
43
|
-
|
|
48
|
+
scope = user_roles.where(role_id: removed_role.id)
|
|
49
|
+
scope = scope.where(tenant_id: tenant_id) if Rbacan.tenant_scoped
|
|
50
|
+
scope.destroy_all
|
|
44
51
|
end
|
|
45
52
|
|
|
46
53
|
# Returns true if the user has the named permission via any of their roles.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
54
|
+
def can?(permission_name)
|
|
55
|
+
Rbacan.ensure_legacy_mode!(:can?)
|
|
56
|
+
roles
|
|
57
|
+
.joins(role_permissions: :permission)
|
|
58
|
+
.where(Rbacan.permission_table => { Rbacan.permission_key => permission_name.to_s })
|
|
59
|
+
.exists?
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Returns true if the user has the named permission in a specific tenant context.
|
|
63
|
+
# Checks both global role assignments (tenant_id NULL) and tenant-specific ones.
|
|
64
|
+
def can_in_tenant?(permission_name, tenant_id)
|
|
65
|
+
Rbacan.ensure_legacy_mode!(:can_in_tenant?)
|
|
66
|
+
roles
|
|
67
|
+
.joins(role_permissions: :permission)
|
|
68
|
+
.where(Rbacan.permission_table => { Rbacan.permission_key => permission_name.to_s })
|
|
69
|
+
.where(Rbacan.user_role_table => { tenant_id: [tenant_id, nil] })
|
|
70
|
+
.exists?
|
|
53
71
|
end
|
|
54
72
|
|
|
55
73
|
# Returns true if the user has the named role.
|
|
56
|
-
|
|
57
|
-
|
|
74
|
+
def has_role?(role_name)
|
|
75
|
+
Rbacan.ensure_legacy_mode!(:has_role?)
|
|
76
|
+
roles.where(Rbacan.role_key => role_name.to_s).exists?
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Returns true if the user has the named role in a specific tenant context.
|
|
80
|
+
# Checks both global role assignments (tenant_id NULL) and tenant-specific ones.
|
|
81
|
+
def has_role_in_tenant?(role_name, tenant_id)
|
|
82
|
+
Rbacan.ensure_legacy_mode!(:has_role_in_tenant?)
|
|
83
|
+
user_roles
|
|
84
|
+
.joins(:role)
|
|
85
|
+
.where(Rbacan.role_table => { Rbacan.role_key => role_name.to_s })
|
|
86
|
+
.where(Rbacan.user_role_table => { tenant_id: [tenant_id, nil] })
|
|
87
|
+
.exists?
|
|
58
88
|
end
|
|
59
89
|
|
|
60
90
|
# Returns true if the user has ANY of the listed roles.
|
|
61
|
-
|
|
62
|
-
|
|
91
|
+
def has_any_role?(*role_names)
|
|
92
|
+
Rbacan.ensure_legacy_mode!(:has_any_role?)
|
|
93
|
+
roles.where(Rbacan.role_key => role_names.map(&:to_s)).exists?
|
|
63
94
|
end
|
|
64
95
|
|
|
65
96
|
# Returns true if the user has ALL of the listed permissions.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
97
|
+
def can_all?(*permission_names)
|
|
98
|
+
Rbacan.ensure_legacy_mode!(:can_all?)
|
|
99
|
+
names = permission_names.map(&:to_s)
|
|
100
|
+
roles
|
|
101
|
+
.joins(role_permissions: :permission)
|
|
102
|
+
.where(Rbacan.permission_table => { Rbacan.permission_key => names })
|
|
103
|
+
.select("#{Rbacan.permission_table}.#{Rbacan.permission_key}")
|
|
104
|
+
.distinct
|
|
105
|
+
.count == names.uniq.size
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Returns roles assigned to this user for a specific tenant.
|
|
109
|
+
# Includes global role assignments (tenant_id NULL).
|
|
110
|
+
def roles_for_tenant(tenant_id)
|
|
111
|
+
Rbacan.ensure_legacy_mode!(:roles_for_tenant)
|
|
112
|
+
roles
|
|
113
|
+
.where(Rbacan.user_role_table => { tenant_id: [tenant_id, nil] })
|
|
76
114
|
end
|
|
77
115
|
end
|
|
78
116
|
end
|
|
@@ -3,25 +3,33 @@ module Rbacan
|
|
|
3
3
|
|
|
4
4
|
# Creates roles by name. Idempotent — skips existing ones.
|
|
5
5
|
def self.create_roles(roles)
|
|
6
|
+
Rbacan.ensure_legacy_mode!(:create_roles)
|
|
7
|
+
|
|
6
8
|
roles.each do |role|
|
|
7
|
-
Rbacan.role_class.constantize.find_or_create_by(
|
|
9
|
+
Rbacan.role_class.constantize.find_or_create_by(Rbacan.role_key => role.to_s)
|
|
8
10
|
end
|
|
9
11
|
end
|
|
10
12
|
|
|
11
13
|
# Creates permissions by name. Idempotent — skips existing ones.
|
|
12
14
|
def self.create_permissions(permissions)
|
|
15
|
+
Rbacan.ensure_legacy_mode!(:create_permissions)
|
|
16
|
+
|
|
13
17
|
permissions.each do |permission|
|
|
14
|
-
Rbacan.permission_class.constantize.find_or_create_by(
|
|
18
|
+
Rbacan.permission_class.constantize.find_or_create_by(Rbacan.permission_key => permission.to_s)
|
|
15
19
|
end
|
|
16
20
|
end
|
|
17
21
|
|
|
18
22
|
# Assigns a list of permissions to a role. Idempotent — skips duplicates.
|
|
19
23
|
def self.assign_permissions_to_role(role_name, permissions)
|
|
20
|
-
|
|
24
|
+
Rbacan.ensure_legacy_mode!(:assign_permissions_to_role)
|
|
25
|
+
|
|
26
|
+
chosen_role = Rbacan.role_class.constantize.find_by(Rbacan.role_key => role_name.to_s)
|
|
21
27
|
return unless chosen_role
|
|
22
28
|
|
|
23
29
|
permissions.each do |permission|
|
|
24
|
-
given_permission = Rbacan.permission_class.constantize.
|
|
30
|
+
given_permission = Rbacan.permission_class.constantize.find_by(
|
|
31
|
+
Rbacan.permission_key => permission.to_s
|
|
32
|
+
)
|
|
25
33
|
next unless given_permission
|
|
26
34
|
|
|
27
35
|
Rbacan.role_permission_class.constantize.find_or_create_by(
|
|
@@ -1,30 +1,22 @@
|
|
|
1
1
|
module Rbacan
|
|
2
2
|
class RouteConstraint
|
|
3
|
-
|
|
4
|
-
#
|
|
5
|
-
# Usage in config/routes.rb:
|
|
6
|
-
# constraints Rbacan::RouteConstraint.new(role: :admin) do
|
|
7
|
-
# namespace :admin do
|
|
8
|
-
# resources :users
|
|
9
|
-
# end
|
|
10
|
-
# end
|
|
11
|
-
#
|
|
12
|
-
# constraints Rbacan::RouteConstraint.new(permission: :access_admin_panel) do
|
|
13
|
-
# ...
|
|
14
|
-
# end
|
|
15
|
-
def initialize(role: nil, permission: nil)
|
|
3
|
+
def initialize(role: nil, permission: nil, user_resolver: nil, context_resolver: nil)
|
|
16
4
|
if role.nil? && permission.nil?
|
|
17
|
-
raise ArgumentError,
|
|
5
|
+
raise ArgumentError, 'Provide exactly one role: or permission:'
|
|
18
6
|
end
|
|
19
7
|
if role && permission
|
|
20
|
-
raise ArgumentError,
|
|
8
|
+
raise ArgumentError, 'Provide exactly one role: or permission:, not both'
|
|
21
9
|
end
|
|
22
10
|
|
|
23
|
-
@role
|
|
11
|
+
@role = role&.to_s
|
|
24
12
|
@permission = permission&.to_s
|
|
13
|
+
@user_resolver = user_resolver
|
|
14
|
+
@context_resolver = context_resolver
|
|
25
15
|
end
|
|
26
16
|
|
|
27
17
|
def matches?(request)
|
|
18
|
+
return hardened_match?(request) unless Rbacan.legacy_mode
|
|
19
|
+
|
|
28
20
|
user = current_user_from(request)
|
|
29
21
|
return false unless user
|
|
30
22
|
|
|
@@ -33,12 +25,23 @@ module Rbacan
|
|
|
33
25
|
|
|
34
26
|
private
|
|
35
27
|
|
|
28
|
+
def hardened_match?(request)
|
|
29
|
+
return false unless @user_resolver&.respond_to?(:call) && @context_resolver&.respond_to?(:call)
|
|
30
|
+
|
|
31
|
+
Rbacan.authorized?(
|
|
32
|
+
user: @user_resolver.call(request),
|
|
33
|
+
permission: @permission,
|
|
34
|
+
role: @role,
|
|
35
|
+
context: @context_resolver.call(request)
|
|
36
|
+
)
|
|
37
|
+
rescue Rbacan::ConfigurationError, ArgumentError
|
|
38
|
+
false
|
|
39
|
+
end
|
|
40
|
+
|
|
36
41
|
def current_user_from(request)
|
|
37
|
-
# Warden (Devise) path — most common, no extra query needed.
|
|
38
42
|
warden = request.env['warden']
|
|
39
43
|
return warden.user if warden&.authenticated?
|
|
40
44
|
|
|
41
|
-
# Fallback: manual session lookup for apps not using Warden.
|
|
42
45
|
user_id = request.session[:user_id] || request.session['user_id']
|
|
43
46
|
return nil unless user_id
|
|
44
47
|
|
data/lib/rbacan/version.rb
CHANGED
data/lib/rbacan/view_helpers.rb
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
module Rbacan
|
|
2
2
|
module ViewHelpers
|
|
3
|
-
# Renders the block only
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
# Renders the block only when the host authorization adapter permits it.
|
|
4
|
+
def authorized?(permission, context: nil, &block)
|
|
5
|
+
allowed = if Rbacan.legacy_mode
|
|
6
|
+
current_user&.can?(permission.to_s)
|
|
7
|
+
else
|
|
8
|
+
Rbacan.authorized?(
|
|
9
|
+
user: current_user,
|
|
10
|
+
permission: permission,
|
|
11
|
+
context: context || rbacan_authorization_context
|
|
12
|
+
)
|
|
13
|
+
end
|
|
14
|
+
return unless allowed
|
|
15
|
+
|
|
11
16
|
capture(&block) if block_given?
|
|
12
17
|
end
|
|
13
18
|
end
|
data/lib/rbacan.rb
CHANGED
|
@@ -1,20 +1,34 @@
|
|
|
1
|
-
require
|
|
2
|
-
require
|
|
3
|
-
require
|
|
4
|
-
require
|
|
5
|
-
require
|
|
6
|
-
require
|
|
7
|
-
require
|
|
8
|
-
require
|
|
1
|
+
require 'rbacan/version'
|
|
2
|
+
require 'rbacan/not_authorized'
|
|
3
|
+
require 'rbacan/host_authorization'
|
|
4
|
+
require 'rbacan/authorizable'
|
|
5
|
+
require 'rbacan/permittable'
|
|
6
|
+
require 'rbacan/engine'
|
|
7
|
+
require 'rbacan/roles_and_permissions'
|
|
8
|
+
require 'rbacan/authorization'
|
|
9
|
+
require 'rbacan/view_helpers'
|
|
10
|
+
require 'rbacan/route_constraint'
|
|
9
11
|
|
|
10
12
|
module Rbacan
|
|
13
|
+
mattr_accessor :authorization_adapter
|
|
14
|
+
@@authorization_adapter = nil
|
|
15
|
+
|
|
16
|
+
mattr_accessor :assignment_adapter
|
|
17
|
+
@@assignment_adapter = nil
|
|
18
|
+
|
|
19
|
+
# Legacy Active Record mutation/query APIs are opt-in in the hardened API.
|
|
20
|
+
mattr_accessor :legacy_mode
|
|
21
|
+
@@legacy_mode = false
|
|
22
|
+
|
|
11
23
|
mattr_accessor :permittable_class
|
|
12
24
|
@@permittable_class = 'User'
|
|
13
25
|
|
|
14
26
|
mattr_accessor :role_class
|
|
15
27
|
mattr_accessor :role_table
|
|
28
|
+
mattr_accessor :role_key
|
|
16
29
|
@@role_class = 'Rbacan::Role'
|
|
17
30
|
@@role_table = 'roles'
|
|
31
|
+
@@role_key = :name
|
|
18
32
|
|
|
19
33
|
mattr_accessor :user_role_class
|
|
20
34
|
mattr_accessor :user_role_table
|
|
@@ -23,8 +37,10 @@ module Rbacan
|
|
|
23
37
|
|
|
24
38
|
mattr_accessor :permission_class
|
|
25
39
|
mattr_accessor :permission_table
|
|
40
|
+
mattr_accessor :permission_key
|
|
26
41
|
@@permission_class = 'Rbacan::Permission'
|
|
27
42
|
@@permission_table = 'permissions'
|
|
43
|
+
@@permission_key = :name
|
|
28
44
|
|
|
29
45
|
mattr_accessor :role_permission_class
|
|
30
46
|
mattr_accessor :role_permission_table
|
|
@@ -39,24 +55,57 @@ module Rbacan
|
|
|
39
55
|
mattr_accessor :unauthorized_redirect_path
|
|
40
56
|
@@unauthorized_redirect_path = '/'
|
|
41
57
|
|
|
58
|
+
# Primary key type for generated migrations.
|
|
59
|
+
# nil = auto-detect from Rails generator config (primary_key_type).
|
|
60
|
+
# Set to :uuid for UUID primary keys, or :bigint for standard integer keys.
|
|
61
|
+
mattr_accessor :primary_key_type
|
|
62
|
+
@@primary_key_type = nil
|
|
63
|
+
|
|
64
|
+
# When true, adds tenant_id to roles and user_roles tables,
|
|
65
|
+
# enabling tenant-scoped roles and role assignments.
|
|
66
|
+
# Permissions remain global (no tenant_id) regardless of this setting.
|
|
67
|
+
mattr_accessor :tenant_scoped
|
|
68
|
+
@@tenant_scoped = false
|
|
69
|
+
|
|
70
|
+
# The name of your tenant model class (default: "Tenant").
|
|
71
|
+
# Only used when tenant_scoped is true.
|
|
72
|
+
mattr_accessor :tenant_class
|
|
73
|
+
@@tenant_class = 'Tenant'
|
|
74
|
+
|
|
75
|
+
# Resolves the effective primary key type.
|
|
76
|
+
# Checks the explicit config first, then falls back to the Rails generator config.
|
|
77
|
+
def self.resolve_primary_key_type
|
|
78
|
+
return primary_key_type if primary_key_type.present?
|
|
79
|
+
|
|
80
|
+
Rails.configuration.generators.options.dig(:active_record, :primary_key_type)
|
|
81
|
+
rescue StandardError
|
|
82
|
+
nil
|
|
83
|
+
end
|
|
84
|
+
|
|
42
85
|
def self.create_role(role_name)
|
|
43
|
-
|
|
86
|
+
ensure_legacy_mode!(:create_role)
|
|
87
|
+
|
|
88
|
+
@@role_class.constantize.create(@@role_key => role_name.to_s)
|
|
44
89
|
end
|
|
45
90
|
|
|
46
91
|
def self.create_permission(permission_name)
|
|
47
|
-
|
|
92
|
+
ensure_legacy_mode!(:create_permission)
|
|
93
|
+
|
|
94
|
+
@@permission_class.constantize.create(@@permission_key => permission_name.to_s)
|
|
48
95
|
end
|
|
49
96
|
|
|
50
97
|
def self.assign_permission_to_role(role_name, permission_name)
|
|
51
|
-
|
|
52
|
-
|
|
98
|
+
ensure_legacy_mode!(:assign_permission_to_role)
|
|
99
|
+
|
|
100
|
+
chosen_role = @@role_class.constantize.find_by(@@role_key => role_name.to_s)
|
|
101
|
+
given_permission = @@permission_class.constantize.find_by(@@permission_key => permission_name.to_s)
|
|
53
102
|
@@role_permission_class.constantize.create(
|
|
54
|
-
role_id:
|
|
103
|
+
role_id: chosen_role.id,
|
|
55
104
|
permission_id: given_permission.id
|
|
56
105
|
)
|
|
57
106
|
end
|
|
58
107
|
|
|
59
|
-
def self.configure
|
|
108
|
+
def self.configure
|
|
60
109
|
yield self
|
|
61
110
|
end
|
|
62
111
|
|