rails_iam 0.1.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 +7 -0
- data/CHANGELOG.md +23 -0
- data/CONTRIBUTING.md +145 -0
- data/LICENSE +20 -0
- data/README.md +192 -0
- data/Rakefile +14 -0
- data/app/controllers/concerns/rails_iam/auth_concern.rb +60 -0
- data/app/controllers/concerns/rails_iam/authentication/authenticatable.rb +44 -0
- data/app/controllers/concerns/rails_iam/authorization/authorizable.rb +170 -0
- data/app/controllers/rails_iam/application_controller.rb +7 -0
- data/app/controllers/rails_iam/auth_controller.rb +113 -0
- data/app/helpers/rails_iam/config_loader.rb +71 -0
- data/app/models/concerns/rails_iam/auditable.rb +57 -0
- data/app/models/concerns/rails_iam/user_concern.rb +59 -0
- data/app/models/rails_iam/application_record.rb +8 -0
- data/app/models/rails_iam/permission.rb +14 -0
- data/app/models/rails_iam/refresh_token.rb +7 -0
- data/app/models/rails_iam/role.rb +14 -0
- data/app/models/rails_iam/role_permission.rb +20 -0
- data/app/models/rails_iam/user.rb +16 -0
- data/app/models/rails_iam/user_denied_permission.rb +12 -0
- data/app/models/rails_iam/user_permission.rb +12 -0
- data/app/models/rails_iam/user_role.rb +12 -0
- data/app/services/application_service.rb +4 -0
- data/app/services/rails_iam/authentication/authenticated_user.rb +13 -0
- data/app/services/rails_iam/authentication/authenticator.rb +39 -0
- data/app/services/rails_iam/authentication/current.rb +9 -0
- data/app/services/rails_iam/authentication/exceptions/authentication_error.rb +10 -0
- data/app/services/rails_iam/authentication/jwt_decoder.rb +38 -0
- data/app/services/rails_iam/authentication/jwt_encoder.rb +45 -0
- data/app/services/rails_iam/authentication/login.rb +67 -0
- data/app/services/rails_iam/authentication/logout.rb +35 -0
- data/app/services/rails_iam/authentication/refresh_token_encoder.rb +11 -0
- data/app/services/rails_iam/authentication/skip_authentication_rule.rb +20 -0
- data/app/services/rails_iam/authentication/token_extractor.rb +64 -0
- data/app/services/rails_iam/authorization/authorization_context.rb +41 -0
- data/app/services/rails_iam/authorization/authorization_rule.rb +38 -0
- data/app/services/rails_iam/authorization/authorizer.rb +35 -0
- data/app/services/rails_iam/authorization/cache.rb +39 -0
- data/app/services/rails_iam/authorization/denied_permission_checker.rb +33 -0
- data/app/services/rails_iam/authorization/exceptions/authorization_error.rb +10 -0
- data/app/services/rails_iam/authorization/exceptions/missing_permission_error.rb +35 -0
- data/app/services/rails_iam/authorization/exceptions/missing_role_error.rb +35 -0
- data/app/services/rails_iam/authorization/permission_checker.rb +42 -0
- data/app/services/rails_iam/authorization/permission_resolver.rb +70 -0
- data/app/services/rails_iam/authorization/permission_sources/role_permission_source.rb +37 -0
- data/app/services/rails_iam/authorization/permission_sources/user_denied_permission_source.rb +29 -0
- data/app/services/rails_iam/authorization/permission_sources/user_permission_source.rb +28 -0
- data/app/services/rails_iam/authorization/resolved_permissions.rb +63 -0
- data/app/services/rails_iam/authorization/role_checker.rb +35 -0
- data/app/services/rails_iam/authorization/role_resolver.rb +30 -0
- data/app/services/rails_iam/authorization/skip_authorization_rule.rb +19 -0
- data/app/services/rails_iam/rails_iam_service.rb +11 -0
- data/config/routes.rb +25 -0
- data/lib/generators/rails_iam/install_generator.rb +49 -0
- data/lib/generators/rails_iam/templates/create_rails_iam_tables.rb.erb +119 -0
- data/lib/generators/rails_iam/templates/initializer.rb +162 -0
- data/lib/rails_iam/config/authentication.rb +65 -0
- data/lib/rails_iam/config/authorization.rb +36 -0
- data/lib/rails_iam/config/jwt.rb +27 -0
- data/lib/rails_iam/configuration.rb +30 -0
- data/lib/rails_iam/controller_dsl.rb +41 -0
- data/lib/rails_iam/engine.rb +34 -0
- data/lib/rails_iam/model_dsl.rb +13 -0
- data/lib/rails_iam/version.rb +5 -0
- data/lib/rails_iam.rb +27 -0
- data/lib/tasks/rails_iam_tasks.rake +6 -0
- data/rails_iam.gemspec +46 -0
- metadata +161 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsIam
|
|
4
|
+
module Authentication
|
|
5
|
+
class Logout < RailsIam::RailsIamService
|
|
6
|
+
def initialize(refresh_token: nil)
|
|
7
|
+
super()
|
|
8
|
+
|
|
9
|
+
@refresh_token = refresh_token
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def call
|
|
13
|
+
invalidate_refresh_token
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
attr_reader :refresh_token
|
|
19
|
+
|
|
20
|
+
def invalidate_refresh_token
|
|
21
|
+
return unless refresh_token
|
|
22
|
+
|
|
23
|
+
case session_strategy
|
|
24
|
+
when :single
|
|
25
|
+
refresh_token.user.refresh_tokens.delete_all
|
|
26
|
+
when :multi
|
|
27
|
+
refresh_token.destroy
|
|
28
|
+
|
|
29
|
+
else
|
|
30
|
+
raise RailsIam::Authentication::Exceptions::AuthenticationError, session_strategy_error
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsIam
|
|
4
|
+
module Authentication
|
|
5
|
+
SkipAuthenticationRule = Data.define(
|
|
6
|
+
:only,
|
|
7
|
+
:except,
|
|
8
|
+
) do
|
|
9
|
+
def matches?(action)
|
|
10
|
+
action = action.to_sym
|
|
11
|
+
|
|
12
|
+
return Array(only).map(&:to_sym).include?(action) if only.present?
|
|
13
|
+
|
|
14
|
+
return Array(except).map(&:to_sym).exclude?(action) if except.present?
|
|
15
|
+
|
|
16
|
+
true
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsIam
|
|
4
|
+
module Authentication
|
|
5
|
+
class CookieExtractor < RailsIam::RailsIamService
|
|
6
|
+
def initialize(request)
|
|
7
|
+
super()
|
|
8
|
+
@request = request
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def call
|
|
12
|
+
request.cookie_jar.encrypted[access_token_key]
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
attr_reader :request
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class BearerExtractor < RailsIam::RailsIamService
|
|
21
|
+
def initialize(request)
|
|
22
|
+
super()
|
|
23
|
+
@request = request
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def call
|
|
27
|
+
authorization = request.headers["Authorization"]
|
|
28
|
+
return if authorization.blank?
|
|
29
|
+
|
|
30
|
+
scheme, token = authorization.split(" ", 2)
|
|
31
|
+
|
|
32
|
+
scheme == "Bearer" ? token : nil
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
attr_reader :request
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
class TokenExtractor < RailsIam::RailsIamService
|
|
41
|
+
def initialize(request)
|
|
42
|
+
super()
|
|
43
|
+
@request = request
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def call
|
|
47
|
+
extractor.call(request)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def extractor
|
|
53
|
+
# Explicitly pick the extractor based on the incoming request signature
|
|
54
|
+
if authentication_strategies.include?(:bearer) && request.headers["Authorization"]&.start_with?("Bearer ")
|
|
55
|
+
BearerExtractor
|
|
56
|
+
else
|
|
57
|
+
CookieExtractor
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
attr_reader :request
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsIam
|
|
4
|
+
module Authorization
|
|
5
|
+
class AuthorizationContext
|
|
6
|
+
attr_reader :user,
|
|
7
|
+
:resolved_permissions,
|
|
8
|
+
:roles,
|
|
9
|
+
:required_roles,
|
|
10
|
+
:required_permissions,
|
|
11
|
+
:inferred_permission,
|
|
12
|
+
:role_match,
|
|
13
|
+
:permission_match
|
|
14
|
+
|
|
15
|
+
def initialize(
|
|
16
|
+
user:,
|
|
17
|
+
required_roles: [],
|
|
18
|
+
required_permissions: [],
|
|
19
|
+
inferred_permission: nil,
|
|
20
|
+
role_match: :any,
|
|
21
|
+
permission_match: :all
|
|
22
|
+
)
|
|
23
|
+
@user = user
|
|
24
|
+
@roles = RoleResolver.call(user)
|
|
25
|
+
@resolved_permissions = PermissionResolver.call(user)
|
|
26
|
+
@required_roles = required_roles.map(&:to_s)
|
|
27
|
+
@required_permissions = required_permissions.map(&:to_s)
|
|
28
|
+
@inferred_permission = inferred_permission
|
|
29
|
+
@role_match = role_match
|
|
30
|
+
@permission_match = permission_match
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def permissions_for_check
|
|
34
|
+
return required_permissions if required_permissions.any?
|
|
35
|
+
return [inferred_permission] if inferred_permission.present?
|
|
36
|
+
|
|
37
|
+
[]
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsIam
|
|
4
|
+
module Authorization
|
|
5
|
+
AuthorizationRule = Data.define(
|
|
6
|
+
:roles,
|
|
7
|
+
:permissions,
|
|
8
|
+
:role_match,
|
|
9
|
+
:permission_match,
|
|
10
|
+
:only,
|
|
11
|
+
:except,
|
|
12
|
+
) do
|
|
13
|
+
def matches?(action)
|
|
14
|
+
action = action.to_sym
|
|
15
|
+
|
|
16
|
+
return Array(only).map(&:to_sym).include?(action) if only.present?
|
|
17
|
+
|
|
18
|
+
return Array(except).map(&:to_sym).exclude?(action) if except.present?
|
|
19
|
+
|
|
20
|
+
true
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def authorizer_arguments(inferred_permission: nil)
|
|
24
|
+
{
|
|
25
|
+
required_roles: roles,
|
|
26
|
+
required_permissions: permissions,
|
|
27
|
+
inferred_permission: inferred_permission,
|
|
28
|
+
role_match: role_match,
|
|
29
|
+
permission_match: permission_match,
|
|
30
|
+
}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def valid?
|
|
34
|
+
roles.any? || permissions.any?
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsIam
|
|
4
|
+
module Authorization
|
|
5
|
+
class Authorizer < RailsIam::RailsIamService
|
|
6
|
+
def initialize(
|
|
7
|
+
user:,
|
|
8
|
+
required_roles: [],
|
|
9
|
+
required_permissions: [],
|
|
10
|
+
inferred_permission: nil,
|
|
11
|
+
role_match: :any,
|
|
12
|
+
permission_match: :all
|
|
13
|
+
)
|
|
14
|
+
super()
|
|
15
|
+
@context =
|
|
16
|
+
AuthorizationContext.new(
|
|
17
|
+
user: user,
|
|
18
|
+
required_roles: required_roles,
|
|
19
|
+
required_permissions: required_permissions,
|
|
20
|
+
inferred_permission: inferred_permission,
|
|
21
|
+
role_match: role_match,
|
|
22
|
+
permission_match: permission_match,
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def call
|
|
27
|
+
DeniedPermissionChecker.call(@context)
|
|
28
|
+
RoleChecker.call(@context)
|
|
29
|
+
PermissionChecker.call(@context)
|
|
30
|
+
|
|
31
|
+
true # success is the absence of an exception
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsIam
|
|
4
|
+
module Authorization
|
|
5
|
+
class Cache
|
|
6
|
+
ROLES = :roles
|
|
7
|
+
PERMISSIONS = :permissions
|
|
8
|
+
|
|
9
|
+
class << self
|
|
10
|
+
def read_or_write(namespace, user_id, &block)
|
|
11
|
+
cache.fetch(
|
|
12
|
+
cache_key(namespace, user_id),
|
|
13
|
+
expires_in: RailsIam.configuration.authorization.cache_ttl,
|
|
14
|
+
&block
|
|
15
|
+
)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def delete(namespace, user_id)
|
|
19
|
+
cache.delete(cache_key(namespace, user_id))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def delete_all(user_id)
|
|
23
|
+
delete(Cache::ROLES, user_id)
|
|
24
|
+
delete(Cache::PERMISSIONS, user_id)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def cache
|
|
30
|
+
RailsIam.configuration.authorization.cache_store || Rails.cache
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def cache_key(namespace, user_id)
|
|
34
|
+
"rails_iam:#{namespace}:#{user_id}"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsIam
|
|
4
|
+
module Authorization
|
|
5
|
+
class DeniedPermissionChecker < RailsIam::RailsIamService
|
|
6
|
+
def initialize(context)
|
|
7
|
+
super()
|
|
8
|
+
@context = context
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def call
|
|
12
|
+
return true if denied_permissions.empty?
|
|
13
|
+
|
|
14
|
+
raise RailsIam::Authorization::Exceptions::MissingPermissionError.new(
|
|
15
|
+
required: denied_permissions,
|
|
16
|
+
)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def denied_permissions
|
|
22
|
+
permissions = @context.permissions_for_check
|
|
23
|
+
|
|
24
|
+
denied =
|
|
25
|
+
@context.resolved_permissions.denied_codes
|
|
26
|
+
|
|
27
|
+
permissions.select do |permission|
|
|
28
|
+
denied.include?(permission)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsIam
|
|
4
|
+
module Authorization
|
|
5
|
+
module Exceptions
|
|
6
|
+
class MissingPermissionError < AuthorizationError
|
|
7
|
+
include RailsIam::ConfigLoader
|
|
8
|
+
|
|
9
|
+
def initialize(required:)
|
|
10
|
+
formatter = missing_permission_error
|
|
11
|
+
|
|
12
|
+
message = case formatter
|
|
13
|
+
# 1. If the user configured a plain String, use it directly
|
|
14
|
+
when String
|
|
15
|
+
formatter
|
|
16
|
+
# 2. If it's a Proc/Lambda, inspect its argument requirements
|
|
17
|
+
when Proc
|
|
18
|
+
if formatter.arity.zero?
|
|
19
|
+
# The user defined a lambda with no parameters: e.g., -> { "Access Denied" }
|
|
20
|
+
formatter.call
|
|
21
|
+
else
|
|
22
|
+
# The user defined a parameterized lambda: e.g., ->(required:, actual:) { ... }
|
|
23
|
+
formatter.call(required: required)
|
|
24
|
+
end
|
|
25
|
+
# 3. Fallback safeguard if config is empty or invalid type
|
|
26
|
+
else
|
|
27
|
+
"Missing permission(s): #{required.join(', ')}"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
super(message)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsIam
|
|
4
|
+
module Authorization
|
|
5
|
+
module Exceptions
|
|
6
|
+
class MissingRoleError < AuthorizationError
|
|
7
|
+
include RailsIam::ConfigLoader
|
|
8
|
+
|
|
9
|
+
def initialize(required:, actual:)
|
|
10
|
+
formatter = missing_role_error
|
|
11
|
+
|
|
12
|
+
message = case formatter
|
|
13
|
+
# 1. If the user configured a plain String, use it directly
|
|
14
|
+
when String
|
|
15
|
+
formatter
|
|
16
|
+
# 2. If it's a Proc/Lambda, inspect its argument requirements
|
|
17
|
+
when Proc
|
|
18
|
+
if formatter.arity.zero?
|
|
19
|
+
# The user defined a lambda with no parameters: e.g., -> { "Access Denied" }
|
|
20
|
+
formatter.call
|
|
21
|
+
else
|
|
22
|
+
# The user defined a parameterized lambda: e.g., ->(required:, actual:) { ... }
|
|
23
|
+
formatter.call(required: required, actual: actual)
|
|
24
|
+
end
|
|
25
|
+
# 3. Fallback safeguard if config is empty or invalid type
|
|
26
|
+
else
|
|
27
|
+
"Required role: #{required.join(', ')}, actual: #{actual.join(', ')}"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
super(message)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsIam
|
|
4
|
+
module Authorization
|
|
5
|
+
class PermissionChecker < RailsIam::RailsIamService
|
|
6
|
+
def initialize(context)
|
|
7
|
+
super()
|
|
8
|
+
@context = context
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def call
|
|
12
|
+
return true if permissions.empty?
|
|
13
|
+
|
|
14
|
+
success =
|
|
15
|
+
case @context.permission_match
|
|
16
|
+
when :any
|
|
17
|
+
permissions.any? do |permission|
|
|
18
|
+
@context.resolved_permissions.include?(permission)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
when :all
|
|
22
|
+
permissions.all? do |permission|
|
|
23
|
+
@context.resolved_permissions.include?(permission)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
else
|
|
27
|
+
raise ArgumentError, "Unknown permission_match #{@context.permission_match}"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
return true if success
|
|
31
|
+
|
|
32
|
+
raise RailsIam::Authorization::Exceptions::MissingPermissionError.new(
|
|
33
|
+
required: permissions,
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def permissions
|
|
38
|
+
@context.permissions_for_check
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsIam
|
|
4
|
+
module Authorization
|
|
5
|
+
PermissionMetadata = Data.define(
|
|
6
|
+
:source,
|
|
7
|
+
:role,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
class PermissionResolver < RailsIam::RailsIamService
|
|
11
|
+
ALLOW_SOURCES = [
|
|
12
|
+
Authorization::PermissionSources::RolePermissionSource,
|
|
13
|
+
Authorization::PermissionSources::UserPermissionSource,
|
|
14
|
+
].freeze
|
|
15
|
+
|
|
16
|
+
DENY_SOURCES = [
|
|
17
|
+
Authorization::PermissionSources::UserDeniedPermissionSource,
|
|
18
|
+
].freeze
|
|
19
|
+
|
|
20
|
+
def initialize(user)
|
|
21
|
+
super()
|
|
22
|
+
|
|
23
|
+
@user = user
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def call
|
|
27
|
+
Cache.read_or_write(:permissions, user.id) do
|
|
28
|
+
resolve
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
attr_reader :user
|
|
35
|
+
|
|
36
|
+
def resolve
|
|
37
|
+
allowed_codes = Set.new
|
|
38
|
+
denied_codes = Set.new
|
|
39
|
+
metadata = {}
|
|
40
|
+
|
|
41
|
+
# Resolve allowed permissions
|
|
42
|
+
ALLOW_SOURCES.each do |source|
|
|
43
|
+
source.call(@user).each do |row|
|
|
44
|
+
allowed_codes << row[:code]
|
|
45
|
+
|
|
46
|
+
metadata[row[:code]] ||= []
|
|
47
|
+
|
|
48
|
+
metadata[row[:code]] << PermissionMetadata.new(
|
|
49
|
+
source: row[:source],
|
|
50
|
+
role: row[:role],
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Resolve denied permissions
|
|
56
|
+
DENY_SOURCES.each do |source|
|
|
57
|
+
source.call(@user).each do |row|
|
|
58
|
+
denied_codes << row[:code]
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
ResolvedPermissions.new(
|
|
63
|
+
allowed_codes: allowed_codes,
|
|
64
|
+
denied_codes: denied_codes,
|
|
65
|
+
metadata: metadata,
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsIam
|
|
4
|
+
module Authorization
|
|
5
|
+
module PermissionSources
|
|
6
|
+
class RolePermissionSource < RailsIam::RailsIamService
|
|
7
|
+
def initialize(user)
|
|
8
|
+
super()
|
|
9
|
+
@user = user
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def call
|
|
13
|
+
rows = []
|
|
14
|
+
|
|
15
|
+
user
|
|
16
|
+
.roles
|
|
17
|
+
.includes(:permissions)
|
|
18
|
+
.find_each do |role|
|
|
19
|
+
role.permissions.each do |permission|
|
|
20
|
+
rows << {
|
|
21
|
+
code: permission.code,
|
|
22
|
+
source: :role,
|
|
23
|
+
role: role.name,
|
|
24
|
+
}
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
rows
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
attr_reader :user
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsIam
|
|
4
|
+
module Authorization
|
|
5
|
+
module PermissionSources
|
|
6
|
+
class UserDeniedPermissionSource < RailsIam::RailsIamService
|
|
7
|
+
def initialize(user)
|
|
8
|
+
super()
|
|
9
|
+
|
|
10
|
+
@user = user
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def call
|
|
14
|
+
user.user_denied_permissions.includes(:permission).map do |record|
|
|
15
|
+
{
|
|
16
|
+
code: record.permission.code,
|
|
17
|
+
source: :user_denied,
|
|
18
|
+
role: nil,
|
|
19
|
+
}
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
attr_reader :user
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsIam
|
|
4
|
+
module Authorization
|
|
5
|
+
module PermissionSources
|
|
6
|
+
class UserPermissionSource < RailsIam::RailsIamService
|
|
7
|
+
def initialize(user)
|
|
8
|
+
super()
|
|
9
|
+
|
|
10
|
+
@user = user
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def call
|
|
14
|
+
user.user_permissions.includes(:permission).map do |record|
|
|
15
|
+
{
|
|
16
|
+
code: record.permission.code,
|
|
17
|
+
source: :user,
|
|
18
|
+
}
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
attr_reader :user
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsIam
|
|
4
|
+
module Authorization
|
|
5
|
+
class ResolvedPermissions
|
|
6
|
+
attr_reader :allowed_codes,
|
|
7
|
+
:denied_codes,
|
|
8
|
+
:metadata
|
|
9
|
+
|
|
10
|
+
def initialize(
|
|
11
|
+
allowed_codes:,
|
|
12
|
+
denied_codes:,
|
|
13
|
+
metadata:
|
|
14
|
+
)
|
|
15
|
+
@allowed_codes = allowed_codes.to_set.freeze
|
|
16
|
+
@denied_codes = denied_codes.to_set.freeze
|
|
17
|
+
@metadata = metadata.freeze
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def include?(permission)
|
|
21
|
+
permission = permission.to_s
|
|
22
|
+
|
|
23
|
+
return false if denied?(permission)
|
|
24
|
+
|
|
25
|
+
allowed?(permission)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def codes
|
|
29
|
+
allowed_codes
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def effective_codes
|
|
33
|
+
@allowed_codes - @denied_codes
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def explain(permission)
|
|
37
|
+
metadata.fetch(permission.to_s, [])
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def allowed?(permission)
|
|
43
|
+
match?(allowed_codes, permission)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def denied?(permission)
|
|
47
|
+
match?(denied_codes, permission)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def match?(set, permission)
|
|
51
|
+
return true if set.include?(permission)
|
|
52
|
+
|
|
53
|
+
namespace = permission.split(":").first
|
|
54
|
+
|
|
55
|
+
return true if set.include?("#{namespace}:*")
|
|
56
|
+
|
|
57
|
+
return true if set.include?("*")
|
|
58
|
+
|
|
59
|
+
false
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|