legion-rbac 0.2.9 → 0.3.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 +14 -0
- data/legion-rbac.gemspec +1 -0
- data/lib/legion/rbac/capability_audit.rb +48 -18
- data/lib/legion/rbac/capability_registry.rb +29 -6
- data/lib/legion/rbac/config_loader.rb +10 -1
- data/lib/legion/rbac/entra_claims_mapper.rb +50 -12
- data/lib/legion/rbac/kerberos_claims_mapper.rb +108 -10
- data/lib/legion/rbac/middleware.rb +193 -38
- data/lib/legion/rbac/permission.rb +25 -9
- data/lib/legion/rbac/policy_engine.rb +427 -55
- data/lib/legion/rbac/principal.rb +34 -7
- data/lib/legion/rbac/role.rb +14 -2
- data/lib/legion/rbac/routes.rb +177 -10
- data/lib/legion/rbac/settings.rb +23 -9
- data/lib/legion/rbac/store.rb +54 -12
- data/lib/legion/rbac/team_scope.rb +37 -7
- data/lib/legion/rbac/version.rb +1 -1
- data/lib/legion/rbac.rb +87 -11
- metadata +15 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 222914812dd7014e897e4aa3a6311ea7b4e8062c74ad1c532774f2f3c9e06fca
|
|
4
|
+
data.tar.gz: 30b265cf04e23b6456f839d5aeeeb47792751d3580f92b2085709b9f786388b5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5e9e541ca68dffe258a0766c0dbf58069faf39475ca94db674f38b8d33430dc04a14bf75b360ce1dc72807361a7d67f9bfff558541e3287ddf51a7325bf0a416
|
|
7
|
+
data.tar.gz: c327b53c9b782f461fc163aef47118c075f3367b9aa60585080faab7ebdf43c57092e580c86ea454a22c0ed5ecbd298a3027c442cc327988edd2dfe812047cb9
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.0] - 2026-04-02
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- Uplifted non-Sinatra RBAC library code to `Legion::Logging::Helper` with structured `log.*` usage instead of direct `Legion::Logging.*` calls.
|
|
7
|
+
- Added structured exception handling via `handle_exception` across the RBAC library surface and expanded operational `info`/`debug` logging for setup, authorization, store access, claims mapping, middleware, and capability audit flows.
|
|
8
|
+
- Promoted `legion-logging >= 1.5.0` to a runtime gem dependency and added coverage for the new logging rescue paths.
|
|
9
|
+
- Explicitly load full `legion/logging` from RBAC library files so `require 'legion/rbac'` boots cleanly without preloading logging elsewhere.
|
|
10
|
+
- Exposed `KerberosClaimsMapper` from the gem entrypoint and preserved caller-supplied fallback defaults/profile attributes when Kerberos fallback delegates to Entra.
|
|
11
|
+
- Made `rbac.enabled` disable RBAC setup/enforcement paths consistently and normalized malformed `expires_at` inputs into validation errors with explicit time parsing.
|
|
12
|
+
- Expanded middleware route coverage to include `/api/rbac/*`, honored `rbac.route_permissions` overrides, and compiled route matchers once per permission table.
|
|
13
|
+
- Wired static and DB-backed role assignments into policy evaluation, enforced `target_team` scope in the core evaluator, and made execution authorization intersect role policy with runner grants and cross-team grants.
|
|
14
|
+
- Hardened the smaller runtime edges: static assignment lookups now respect `principal_type`, capability registry reads return copies instead of live internals, capability denials render useful messages, resource regexes compile once, and RBAC collection routes use bounded `limit`/`offset` windows.
|
|
15
|
+
- Synchronized RBAC role index lifecycle state so setup/shutdown expose a stable frozen empty index instead of `nil`, avoiding transitional reads during authorization and route access.
|
|
16
|
+
|
|
3
17
|
## [0.2.9] - 2026-03-31
|
|
4
18
|
|
|
5
19
|
### Added
|
data/legion-rbac.gemspec
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'legion/logging'
|
|
4
|
+
|
|
3
5
|
module Legion
|
|
4
6
|
module Rbac
|
|
5
7
|
module CapabilityAudit
|
|
8
|
+
extend Legion::Logging::Helper
|
|
9
|
+
|
|
6
10
|
PATTERN_TO_CAPABILITY = {
|
|
7
11
|
/\bKernel\.system\b|\bsystem\s*\(/ => :shell_execute,
|
|
8
12
|
/\bKernel\.exec\b|\bexec\s*\(/ => :shell_execute,
|
|
@@ -49,24 +53,50 @@ module Legion
|
|
|
49
53
|
|
|
50
54
|
class << self
|
|
51
55
|
def audit(extension_name:, source_path:, declared_capabilities: [])
|
|
52
|
-
|
|
56
|
+
log.info(
|
|
57
|
+
"RBAC capability_audit start extension=#{extension_name} source_path=#{source_path} " \
|
|
58
|
+
"declared=#{Array(declared_capabilities).size}"
|
|
59
|
+
)
|
|
60
|
+
unless enabled?
|
|
61
|
+
result = skip_result(extension_name, 'capability audit disabled')
|
|
62
|
+
log.info("RBAC capability_audit skipped extension=#{extension_name} reason=#{result.reason}")
|
|
63
|
+
return result
|
|
64
|
+
end
|
|
53
65
|
|
|
54
|
-
|
|
66
|
+
unless source_path && Dir.exist?(source_path.to_s)
|
|
67
|
+
result = skip_result(extension_name, 'no source path')
|
|
68
|
+
log.info("RBAC capability_audit skipped extension=#{extension_name} reason=#{result.reason}")
|
|
69
|
+
return result
|
|
70
|
+
end
|
|
55
71
|
|
|
56
72
|
detected = scan_source(source_path)
|
|
57
73
|
declared_syms = Array(declared_capabilities).map(&:to_sym)
|
|
58
74
|
undeclared = (detected.uniq - declared_syms)
|
|
59
75
|
|
|
60
|
-
if undeclared.empty?
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
76
|
+
result = if undeclared.empty?
|
|
77
|
+
AuditResult.new(
|
|
78
|
+
extension_name: extension_name,
|
|
79
|
+
detected: detected,
|
|
80
|
+
declared: declared_syms,
|
|
81
|
+
allowed: true
|
|
82
|
+
)
|
|
83
|
+
else
|
|
84
|
+
handle_undeclared(extension_name, detected, declared_syms, undeclared)
|
|
85
|
+
end
|
|
86
|
+
log.info(
|
|
87
|
+
"RBAC capability_audit extension=#{extension_name} allowed=#{result.allowed} " \
|
|
88
|
+
"detected=#{result.detected_capabilities.size} undeclared=#{result.undeclared.size}"
|
|
89
|
+
)
|
|
90
|
+
result
|
|
91
|
+
rescue StandardError => e
|
|
92
|
+
handle_exception(
|
|
93
|
+
e,
|
|
94
|
+
level: :error,
|
|
95
|
+
operation: 'rbac.capability_audit.audit',
|
|
96
|
+
extension_name: extension_name,
|
|
97
|
+
source_path: source_path
|
|
98
|
+
)
|
|
99
|
+
raise
|
|
70
100
|
end
|
|
71
101
|
|
|
72
102
|
def enabled?
|
|
@@ -83,13 +113,15 @@ module Legion
|
|
|
83
113
|
|
|
84
114
|
def scan_source(source_path)
|
|
85
115
|
capabilities = []
|
|
86
|
-
Dir.glob(File.join(source_path, '**', '*.rb'))
|
|
116
|
+
files = Dir.glob(File.join(source_path, '**', '*.rb'))
|
|
117
|
+
files.each do |file|
|
|
87
118
|
File.foreach(file) do |line|
|
|
88
119
|
PATTERN_TO_CAPABILITY.each do |pattern, capability|
|
|
89
120
|
capabilities << capability if line.match?(pattern)
|
|
90
121
|
end
|
|
91
122
|
end
|
|
92
123
|
end
|
|
124
|
+
log.debug("RBAC capability_audit scanned source_path=#{source_path} files=#{files.size}")
|
|
93
125
|
capabilities.uniq
|
|
94
126
|
end
|
|
95
127
|
|
|
@@ -104,6 +136,7 @@ module Legion
|
|
|
104
136
|
reason: "undeclared capabilities (warn mode): #{undeclared.join(', ')}"
|
|
105
137
|
)
|
|
106
138
|
else
|
|
139
|
+
log.warn("CapabilityAudit: #{extension_name} blocked for undeclared capabilities: #{undeclared.join(', ')}")
|
|
107
140
|
AuditResult.new(
|
|
108
141
|
extension_name: extension_name,
|
|
109
142
|
detected: detected,
|
|
@@ -115,14 +148,11 @@ module Legion
|
|
|
115
148
|
end
|
|
116
149
|
|
|
117
150
|
def log_warning(extension_name, undeclared)
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
Legion::Logging.warn(
|
|
121
|
-
"CapabilityAudit: #{extension_name} uses undeclared capabilities: #{undeclared.join(', ')}"
|
|
122
|
-
)
|
|
151
|
+
log.warn("CapabilityAudit: #{extension_name} uses undeclared capabilities: #{undeclared.join(', ')}")
|
|
123
152
|
end
|
|
124
153
|
|
|
125
154
|
def skip_result(extension_name, reason)
|
|
155
|
+
log.debug("RBAC capability_audit skip_result extension=#{extension_name} reason=#{reason}")
|
|
126
156
|
AuditResult.new(
|
|
127
157
|
extension_name: extension_name,
|
|
128
158
|
detected: [],
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'legion/logging'
|
|
3
4
|
require 'monitor'
|
|
4
5
|
|
|
5
6
|
module Legion
|
|
6
7
|
module Rbac
|
|
7
8
|
module CapabilityRegistry
|
|
8
9
|
class << self
|
|
10
|
+
include Legion::Logging::Helper
|
|
11
|
+
|
|
9
12
|
def register(extension_name, capabilities:, audit_result: nil)
|
|
10
13
|
mon.synchronize do
|
|
11
14
|
entries[extension_name.to_s] = {
|
|
@@ -14,39 +17,59 @@ module Legion
|
|
|
14
17
|
registered_at: Time.now
|
|
15
18
|
}
|
|
16
19
|
end
|
|
20
|
+
log.info("RBAC capability_registry register extension=#{extension_name} count=#{Array(capabilities).uniq.size}")
|
|
17
21
|
end
|
|
18
22
|
|
|
19
23
|
def for_extension(extension_name)
|
|
20
|
-
mon.synchronize do
|
|
24
|
+
capabilities = mon.synchronize do
|
|
21
25
|
entry = entries[extension_name.to_s]
|
|
22
|
-
entry ? entry[:capabilities] : []
|
|
26
|
+
entry ? entry[:capabilities].dup : []
|
|
23
27
|
end
|
|
28
|
+
log.debug("RBAC capability_registry for_extension extension=#{extension_name} count=#{capabilities.size}")
|
|
29
|
+
capabilities
|
|
24
30
|
end
|
|
25
31
|
|
|
26
32
|
def extensions_with(capability)
|
|
27
33
|
cap_sym = capability.to_sym
|
|
28
|
-
mon.synchronize do
|
|
34
|
+
extensions = mon.synchronize do
|
|
29
35
|
entries.select { |_, entry| entry[:capabilities].include?(cap_sym) }.keys
|
|
30
36
|
end
|
|
37
|
+
log.debug("RBAC capability_registry extensions_with capability=#{capability} count=#{extensions.size}")
|
|
38
|
+
extensions
|
|
31
39
|
end
|
|
32
40
|
|
|
33
41
|
def audit_result_for(extension_name)
|
|
34
|
-
mon.synchronize do
|
|
42
|
+
audit_result = mon.synchronize do
|
|
35
43
|
entry = entries[extension_name.to_s]
|
|
36
44
|
entry&.dig(:audit_result)
|
|
37
45
|
end
|
|
46
|
+
log.debug("RBAC capability_registry audit_result_for extension=#{extension_name} present=#{!audit_result.nil?}")
|
|
47
|
+
audit_result
|
|
38
48
|
end
|
|
39
49
|
|
|
40
50
|
def all
|
|
41
|
-
mon.synchronize
|
|
51
|
+
registry = mon.synchronize do
|
|
52
|
+
entries.each_with_object({}) do |(extension_name, entry), copy|
|
|
53
|
+
copy[extension_name] = {
|
|
54
|
+
capabilities: entry[:capabilities].dup,
|
|
55
|
+
audit_result: entry[:audit_result],
|
|
56
|
+
registered_at: entry[:registered_at]
|
|
57
|
+
}
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
log.debug("RBAC capability_registry all count=#{registry.size}")
|
|
61
|
+
registry
|
|
42
62
|
end
|
|
43
63
|
|
|
44
64
|
def registered?(extension_name)
|
|
45
|
-
mon.synchronize { entries.key?(extension_name.to_s) }
|
|
65
|
+
registered = mon.synchronize { entries.key?(extension_name.to_s) }
|
|
66
|
+
log.debug("RBAC capability_registry registered extension=#{extension_name} value=#{registered}")
|
|
67
|
+
registered
|
|
46
68
|
end
|
|
47
69
|
|
|
48
70
|
def clear!
|
|
49
71
|
mon.synchronize { @entries = {} }
|
|
72
|
+
log.info('RBAC capability_registry cleared')
|
|
50
73
|
end
|
|
51
74
|
|
|
52
75
|
private
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'legion/logging'
|
|
3
4
|
require 'legion/rbac/role'
|
|
4
5
|
|
|
5
6
|
module Legion
|
|
6
7
|
module Rbac
|
|
7
8
|
module ConfigLoader
|
|
9
|
+
extend Legion::Logging::Helper
|
|
10
|
+
|
|
8
11
|
def self.load_roles(roles_config = nil)
|
|
9
12
|
roles_config ||= Legion::Settings[:rbac][:roles]
|
|
10
|
-
roles_config.each_with_object({}) do |(name, config), index|
|
|
13
|
+
roles = roles_config.each_with_object({}) do |(name, config), index|
|
|
11
14
|
index[name.to_sym] = Role.new(
|
|
12
15
|
name: name,
|
|
13
16
|
description: config[:description] || '',
|
|
@@ -17,7 +20,13 @@ module Legion
|
|
|
17
20
|
capability_grants: config[:capability_grants] || [],
|
|
18
21
|
capability_denials: config[:capability_denials] || []
|
|
19
22
|
)
|
|
23
|
+
log.debug("RBAC role loaded name=#{name} permissions=#{config[:permissions]&.size || 0} deny=#{config[:deny]&.size || 0}")
|
|
20
24
|
end
|
|
25
|
+
log.info("RBAC roles loaded count=#{roles.size}")
|
|
26
|
+
roles
|
|
27
|
+
rescue StandardError => e
|
|
28
|
+
handle_exception(e, level: :error, operation: 'rbac.config_loader.load_roles')
|
|
29
|
+
raise
|
|
21
30
|
end
|
|
22
31
|
end
|
|
23
32
|
end
|
|
@@ -1,41 +1,79 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'legion/logging'
|
|
4
|
+
|
|
3
5
|
module Legion
|
|
4
6
|
module Rbac
|
|
5
7
|
module EntraClaimsMapper
|
|
8
|
+
extend Legion::Logging::Helper
|
|
9
|
+
|
|
6
10
|
DEFAULT_ROLE_MAP = {
|
|
7
11
|
'Legion.Admin' => 'admin',
|
|
8
12
|
'Legion.Supervisor' => 'supervisor',
|
|
9
13
|
'Legion.Worker' => 'worker',
|
|
10
14
|
'Legion.Observer' => 'governance-observer'
|
|
11
15
|
}.freeze
|
|
16
|
+
DEFAULT_TEAM_KEYS = %i[legion_team extension_legion_team tid].freeze
|
|
12
17
|
|
|
13
18
|
module_function
|
|
14
19
|
|
|
15
|
-
def map_claims(entra_claims, role_map: DEFAULT_ROLE_MAP, group_map: {}, default_role: 'worker'
|
|
20
|
+
def map_claims(entra_claims, role_map: DEFAULT_ROLE_MAP, group_map: {}, default_role: 'worker',
|
|
21
|
+
team_keys: DEFAULT_TEAM_KEYS, team_map: nil)
|
|
22
|
+
roles = resolve_roles(entra_claims, role_map: role_map, group_map: group_map)
|
|
23
|
+
used_default_role = roles.empty?
|
|
24
|
+
roles << default_role if used_default_role
|
|
25
|
+
team = resolve_team(entra_claims, team_keys: team_keys, team_map: team_map)
|
|
26
|
+
|
|
27
|
+
claims = {
|
|
28
|
+
sub: claim_value(entra_claims, :oid, :sub),
|
|
29
|
+
name: claim_value(entra_claims, :name, :preferred_username),
|
|
30
|
+
roles: roles.to_a,
|
|
31
|
+
team: team,
|
|
32
|
+
scope: 'human'
|
|
33
|
+
}
|
|
34
|
+
log.info(
|
|
35
|
+
"RBAC entra_claims map sub=#{claims[:sub]} roles=#{claims[:roles].size} " \
|
|
36
|
+
"team=#{claims[:team]} default_role=#{used_default_role}"
|
|
37
|
+
)
|
|
38
|
+
claims
|
|
39
|
+
rescue StandardError => e
|
|
40
|
+
handle_exception(e, level: :error, operation: 'rbac.entra_claims_mapper.map_claims')
|
|
41
|
+
raise
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def resolve_roles(entra_claims, role_map:, group_map:)
|
|
16
45
|
roles = Set.new
|
|
17
46
|
|
|
18
|
-
Array(entra_claims
|
|
47
|
+
Array(claim_value(entra_claims, :roles)).each do |entra_role|
|
|
19
48
|
legion_role = role_map[entra_role]
|
|
20
49
|
roles << legion_role if legion_role
|
|
21
50
|
end
|
|
22
51
|
|
|
23
|
-
Array(entra_claims
|
|
52
|
+
Array(claim_value(entra_claims, :groups)).each do |group_oid|
|
|
24
53
|
legion_role = group_map[group_oid]
|
|
25
54
|
roles << legion_role if legion_role
|
|
26
55
|
end
|
|
27
56
|
|
|
28
|
-
roles
|
|
57
|
+
roles
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def resolve_team(entra_claims, team_keys:, team_map:)
|
|
61
|
+
raw_team = claim_value(entra_claims, *Array(team_keys))
|
|
62
|
+
return raw_team if raw_team.nil? || team_map.nil? || team_map.empty?
|
|
29
63
|
|
|
30
|
-
|
|
31
|
-
sub: entra_claims[:oid] || entra_claims[:sub] || entra_claims['oid'] || entra_claims['sub'],
|
|
32
|
-
name: entra_claims[:name] || entra_claims[:preferred_username] ||
|
|
33
|
-
entra_claims['name'] || entra_claims['preferred_username'],
|
|
34
|
-
roles: roles.to_a,
|
|
35
|
-
team: entra_claims[:tid] || entra_claims['tid'],
|
|
36
|
-
scope: 'human'
|
|
37
|
-
}
|
|
64
|
+
team_map[raw_team] || team_map[raw_team.to_s] || team_map[raw_team.to_sym]
|
|
38
65
|
end
|
|
66
|
+
|
|
67
|
+
def claim_value(claims, *keys)
|
|
68
|
+
keys.each do |key|
|
|
69
|
+
value = claims[key] || claims[key.to_s]
|
|
70
|
+
return value unless value.nil?
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
nil
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private :resolve_roles, :resolve_team, :claim_value
|
|
39
77
|
end
|
|
40
78
|
end
|
|
41
79
|
end
|
|
@@ -1,43 +1,141 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'legion/logging'
|
|
4
|
+
|
|
3
5
|
module Legion
|
|
4
6
|
module Rbac
|
|
5
7
|
module KerberosClaimsMapper
|
|
8
|
+
extend Legion::Logging::Helper
|
|
9
|
+
|
|
6
10
|
DEFAULT_ROLE = 'worker'
|
|
11
|
+
DEFAULT_TEAM_KEYS = %i[team legion_team].freeze
|
|
7
12
|
|
|
8
13
|
module_function
|
|
9
14
|
|
|
10
|
-
def map(principal:, groups:, role_map: {}, default_role: DEFAULT_ROLE,
|
|
15
|
+
def map(principal:, groups:, role_map: {}, default_role: DEFAULT_ROLE, team_keys: DEFAULT_TEAM_KEYS,
|
|
16
|
+
team_map: nil, **profile)
|
|
11
17
|
parts = principal.split('@', 2)
|
|
12
18
|
username = parts.first
|
|
13
19
|
realm = parts.length > 1 ? parts.last : nil
|
|
14
20
|
roles = Array(groups).filter_map { |g| role_map[g] }.uniq
|
|
15
|
-
|
|
21
|
+
used_default_role = roles.empty?
|
|
22
|
+
roles = [default_role] if used_default_role
|
|
23
|
+
team = resolve_team(profile, team_keys: team_keys, team_map: team_map)
|
|
16
24
|
|
|
17
|
-
{
|
|
25
|
+
claims = {
|
|
18
26
|
sub: username,
|
|
19
27
|
samaccountname: username,
|
|
20
28
|
ad_fqdn: realm&.downcase,
|
|
21
29
|
roles: roles,
|
|
22
30
|
scope: 'human',
|
|
23
31
|
auth_method: 'kerberos',
|
|
24
|
-
**profile
|
|
32
|
+
**profile,
|
|
33
|
+
team: team
|
|
25
34
|
}.compact
|
|
35
|
+
log.info(
|
|
36
|
+
"RBAC kerberos_claims map principal=#{username} roles=#{claims[:roles].size} " \
|
|
37
|
+
"default_role=#{used_default_role} realm=#{claims[:ad_fqdn]} team=#{claims[:team] || 'none'}"
|
|
38
|
+
)
|
|
39
|
+
claims
|
|
40
|
+
rescue StandardError => e
|
|
41
|
+
handle_exception(e, level: :error, operation: 'rbac.kerberos_claims_mapper.map', principal: principal)
|
|
42
|
+
raise
|
|
26
43
|
end
|
|
27
44
|
|
|
28
45
|
def map_with_fallback(principal:, groups: nil, fallback: :entra, role_map: {},
|
|
29
46
|
default_role: DEFAULT_ROLE, **profile)
|
|
47
|
+
profile, team_resolution = extract_team_resolution(profile)
|
|
30
48
|
if groups&.any?
|
|
31
|
-
|
|
49
|
+
claims = mapped_claims(
|
|
50
|
+
principal: principal,
|
|
51
|
+
groups: groups,
|
|
52
|
+
role_map: role_map,
|
|
53
|
+
default_role: default_role,
|
|
54
|
+
team_resolution: team_resolution,
|
|
55
|
+
profile: profile
|
|
56
|
+
)
|
|
57
|
+
path = 'groups'
|
|
32
58
|
elsif fallback == :entra && defined?(Legion::Rbac::EntraClaimsMapper)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
59
|
+
claims = entra_fallback_claims(
|
|
60
|
+
principal: principal,
|
|
61
|
+
role_map: role_map,
|
|
62
|
+
default_role: default_role,
|
|
63
|
+
team_resolution: team_resolution,
|
|
64
|
+
profile: profile
|
|
65
|
+
)
|
|
66
|
+
path = 'entra'
|
|
37
67
|
else
|
|
38
|
-
|
|
68
|
+
claims = mapped_claims(
|
|
69
|
+
principal: principal,
|
|
70
|
+
groups: [],
|
|
71
|
+
role_map: role_map,
|
|
72
|
+
default_role: default_role,
|
|
73
|
+
team_resolution: team_resolution,
|
|
74
|
+
profile: profile
|
|
75
|
+
)
|
|
76
|
+
path = 'default_role'
|
|
39
77
|
end
|
|
78
|
+
log.info("RBAC kerberos_claims fallback principal=#{principal} path=#{path}")
|
|
79
|
+
claims
|
|
80
|
+
rescue StandardError => e
|
|
81
|
+
handle_exception(
|
|
82
|
+
e,
|
|
83
|
+
level: :error,
|
|
84
|
+
operation: 'rbac.kerberos_claims_mapper.map_with_fallback',
|
|
85
|
+
principal: principal,
|
|
86
|
+
fallback: fallback
|
|
87
|
+
)
|
|
88
|
+
raise
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def resolve_team(profile, team_keys:, team_map:)
|
|
92
|
+
Array(team_keys).each do |key|
|
|
93
|
+
value = profile[key] || profile[key.to_s]
|
|
94
|
+
return value if value && (team_map.nil? || team_map.empty?)
|
|
95
|
+
return team_map[value] || team_map[value.to_s] || team_map[value.to_sym] if value
|
|
96
|
+
end
|
|
97
|
+
nil
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def extract_team_resolution(profile)
|
|
101
|
+
sanitized_profile = profile.dup
|
|
102
|
+
team_resolution = {
|
|
103
|
+
team_keys: sanitized_profile.delete(:team_keys) || DEFAULT_TEAM_KEYS,
|
|
104
|
+
team_map: sanitized_profile.delete(:team_map)
|
|
105
|
+
}
|
|
106
|
+
[sanitized_profile, team_resolution]
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def mapped_claims(principal:, groups:, role_map:, default_role:, team_resolution:, profile:)
|
|
110
|
+
map(
|
|
111
|
+
principal: principal,
|
|
112
|
+
groups: groups,
|
|
113
|
+
role_map: role_map,
|
|
114
|
+
default_role: default_role,
|
|
115
|
+
**team_resolution,
|
|
116
|
+
**profile
|
|
117
|
+
)
|
|
40
118
|
end
|
|
119
|
+
|
|
120
|
+
def entra_fallback_claims(principal:, role_map:, default_role:, team_resolution:, profile:)
|
|
121
|
+
entra_claims = { sub: principal, preferred_username: principal, **profile }.compact
|
|
122
|
+
result = EntraClaimsMapper.map_claims(
|
|
123
|
+
entra_claims,
|
|
124
|
+
role_map: role_map,
|
|
125
|
+
default_role: default_role,
|
|
126
|
+
**team_resolution
|
|
127
|
+
)
|
|
128
|
+
result&.merge(**profile, auth_method: 'kerberos', team: result[:team]) || mapped_claims(
|
|
129
|
+
principal: principal,
|
|
130
|
+
groups: [],
|
|
131
|
+
role_map: role_map,
|
|
132
|
+
default_role: default_role,
|
|
133
|
+
team_resolution: team_resolution,
|
|
134
|
+
profile: profile
|
|
135
|
+
)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
private :resolve_team, :extract_team_resolution, :mapped_claims, :entra_fallback_claims
|
|
41
139
|
end
|
|
42
140
|
end
|
|
43
141
|
end
|