legion-rbac 0.3.1 → 0.3.3
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 +16 -0
- data/CLAUDE.md +4 -1
- data/README.md +1 -1
- data/lib/legion/rbac/group_role_mapper.rb +59 -0
- data/lib/legion/rbac/middleware.rb +52 -42
- data/lib/legion/rbac/settings.rb +2 -0
- data/lib/legion/rbac/version.rb +1 -1
- data/lib/legion/rbac.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2805fec42a72b9618d9790becf4439f3fba5bbe3af5f60f4fd6ce22b230ca4b6
|
|
4
|
+
data.tar.gz: b4648e10753c56aec63c122f765a00cc545fe6626a3fcd25a344fa829df5e615
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2f1a104089a800dbfd7fdd6e062f24cef9205aed00c0accd543deee71327705b5f20dbb259f0cf9c579a67df5046b441ecdf1cfa33f84e485105f4b2fb18c4b9
|
|
7
|
+
data.tar.gz: 76c1c1466a6956c8406626c414173476181c2b936675aee21e6b7590afd7eb5021b4718c1db3dc01b1ee27f016039673387b54cfbf44933be7915b1c0e22e0ff
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.3] - 2026-04-08
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `GroupRoleMapper` module: resolves RBAC roles from identity group memberships via exact-string `group_role_map` lookup; `enrich_principal` additive role enrichment; gated behind `Legion::Rbac.enabled?`
|
|
7
|
+
- `group_role_map: {}` default added to `Settings.default` for Phase 7 configuration surface
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- RBAC middleware audit mode fix: `enabled=false` now full-bypasses (unchanged); `enabled=true, enforce=false` now runs `PolicyEngine` in dry-run mode and logs `[RBAC audit] would_deny` for any policy-denied request instead of bypassing entirely
|
|
11
|
+
- Middleware principal resolution now reads `env['legion.rbac_principal']` first, falling back to `env['legion.principal']`, bridging Phase 7 Identity middleware handoff
|
|
12
|
+
- Removed dead private `enforce?` method from middleware; callers now use `Legion::Rbac.enabled?` and `Legion::Rbac.enforcing?` directly to eliminate parallel implementations
|
|
13
|
+
|
|
14
|
+
## [0.3.2] - 2026-04-08
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- `client_id: nil` default added to Entra settings block for explicit Azure AD app registration tracking
|
|
18
|
+
|
|
3
19
|
## [0.3.1] - 2026-04-03
|
|
4
20
|
|
|
5
21
|
### Fixed
|
data/CLAUDE.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Parent**: `/Users/miverso2/rubymine/legion/CLAUDE.md`
|
|
4
4
|
**GitHub**: https://github.com/LegionIO/legion-rbac
|
|
5
|
-
**Version**: 0.2
|
|
5
|
+
**Version**: 0.3.2
|
|
6
6
|
|
|
7
7
|
Optional RBAC gem for LegionIO. Vault-style flat policy model with deny-always-wins semantics.
|
|
8
8
|
|
|
@@ -29,6 +29,9 @@ lib/legion/rbac/policy_engine.rb # Core evaluator
|
|
|
29
29
|
lib/legion/rbac/team_scope.rb # Cross-team access validation
|
|
30
30
|
lib/legion/rbac/store.rb # Dual-mode data access
|
|
31
31
|
lib/legion/rbac/middleware.rb # Rack middleware
|
|
32
|
+
lib/legion/rbac/routes.rb # Sinatra REST API routes for RBAC management
|
|
33
|
+
lib/legion/rbac/capability_registry.rb # Per-extension capability declarations and querying
|
|
34
|
+
lib/legion/rbac/capability_audit.rb # Source code scanning for dangerous patterns; enforces declared capabilities
|
|
32
35
|
lib/legion/rbac/entra_claims_mapper.rb # Entra ID claims -> Legion roles
|
|
33
36
|
lib/legion/rbac/kerberos_claims_mapper.rb # Kerberos principal + AD groups -> Legion roles
|
|
34
37
|
```
|
data/README.md
CHANGED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Rbac
|
|
5
|
+
module GroupRoleMapper
|
|
6
|
+
# Resolve RBAC roles from group memberships using a configurable map.
|
|
7
|
+
#
|
|
8
|
+
# @param groups [Array<String>] group names or OIDs from identity provider
|
|
9
|
+
# @param group_role_map [Hash, nil] { group_name => role_name }; reads default_map when nil
|
|
10
|
+
# @return [Array<String>] resolved role names (may be empty)
|
|
11
|
+
#
|
|
12
|
+
# NOTE: v1 supports exact string match only. Regexp keys in group_role_map are NOT supported —
|
|
13
|
+
# JSON settings cannot represent Regexp objects. All map keys are compared via `to_s == to_s`.
|
|
14
|
+
# Pattern matching is deferred to Phase 9.
|
|
15
|
+
def self.resolve_roles(groups:, group_role_map: nil)
|
|
16
|
+
return [] unless Legion::Rbac.enabled?
|
|
17
|
+
|
|
18
|
+
map = group_role_map || default_map
|
|
19
|
+
return [] if groups.nil? || groups.empty? || map.empty?
|
|
20
|
+
|
|
21
|
+
normalized_map = {}
|
|
22
|
+
map.each do |key, role|
|
|
23
|
+
normalized_map[key.to_s] = role.to_s
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
roles = Set.new
|
|
27
|
+
groups.each do |group|
|
|
28
|
+
role = normalized_map[group.to_s]
|
|
29
|
+
roles << role if role
|
|
30
|
+
end
|
|
31
|
+
roles.to_a
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Enrich an RBAC principal hash with group-derived roles (additive, never removes).
|
|
35
|
+
#
|
|
36
|
+
# @param principal [Hash] from Identity::Request#to_rbac_principal
|
|
37
|
+
# @param groups [Array<String>] from identity provider
|
|
38
|
+
# @return [Hash] principal with :roles enriched
|
|
39
|
+
def self.enrich_principal(principal:, groups:)
|
|
40
|
+
return principal unless Legion::Rbac.enabled?
|
|
41
|
+
|
|
42
|
+
additional_roles = resolve_roles(groups: groups)
|
|
43
|
+
return principal if additional_roles.empty?
|
|
44
|
+
|
|
45
|
+
existing_roles = principal[:roles] || []
|
|
46
|
+
principal.merge(roles: (existing_roles + additional_roles).uniq)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.default_map
|
|
50
|
+
return {} unless defined?(Legion::Settings)
|
|
51
|
+
|
|
52
|
+
rbac_settings = Legion::Settings[:rbac]
|
|
53
|
+
rbac_settings&.dig(:group_role_map) || {}
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private_class_method :default_map
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -47,47 +47,19 @@ module Legion
|
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
def call(env)
|
|
50
|
-
return @app.call(env) unless
|
|
50
|
+
return @app.call(env) unless Legion::Rbac.enabled?
|
|
51
51
|
|
|
52
52
|
path = env['PATH_INFO']
|
|
53
|
-
if skip_path?(path)
|
|
54
|
-
|
|
55
|
-
return @app.call(env)
|
|
56
|
-
end
|
|
57
|
-
if invoke_route?(path)
|
|
58
|
-
log.debug("RBAC middleware bypass path=#{path} reason=invoke_route")
|
|
59
|
-
return @app.call(env)
|
|
60
|
-
end
|
|
53
|
+
return bypass(env, path, :skip_path) if skip_path?(path)
|
|
54
|
+
return bypass(env, path, :invoke_route) if invoke_route?(path)
|
|
61
55
|
|
|
62
|
-
principal = env['legion.principal']
|
|
63
|
-
unless principal
|
|
64
|
-
log.warn("RBAC middleware denied method=#{env['REQUEST_METHOD']} path=#{path} reason=unauthenticated")
|
|
65
|
-
return denied_response('unauthenticated')
|
|
66
|
-
end
|
|
56
|
+
principal = env['legion.rbac_principal'] || env['legion.principal']
|
|
57
|
+
return guard_missing(env, path, 'unauthenticated') unless principal
|
|
67
58
|
|
|
68
59
|
perm = find_permission(env['REQUEST_METHOD'], path)
|
|
69
|
-
unless perm
|
|
70
|
-
log.warn("RBAC middleware denied method=#{env['REQUEST_METHOD']} path=#{path} reason=unmapped_route")
|
|
71
|
-
return denied_response('unmapped route')
|
|
72
|
-
end
|
|
73
|
-
perm = effective_permission(env, perm)
|
|
74
|
-
result = policy_result(env, principal, perm)
|
|
60
|
+
return guard_missing(env, path, 'unmapped route') unless perm
|
|
75
61
|
|
|
76
|
-
|
|
77
|
-
log.info(
|
|
78
|
-
"RBAC middleware allowed principal=#{principal.id} method=#{env['REQUEST_METHOD']} " \
|
|
79
|
-
"path=#{path} resource=#{perm[:resource]} action=#{perm[:action]} " \
|
|
80
|
-
"target_team=#{env['legion.rbac.target_team'] || 'none'}"
|
|
81
|
-
)
|
|
82
|
-
@app.call(env)
|
|
83
|
-
else
|
|
84
|
-
log.warn(
|
|
85
|
-
"RBAC middleware denied principal=#{principal.id} method=#{env['REQUEST_METHOD']} " \
|
|
86
|
-
"path=#{path} resource=#{perm[:resource]} action=#{perm[:action]} " \
|
|
87
|
-
"target_team=#{env['legion.rbac.target_team'] || 'none'} reason=#{result[:reason]}"
|
|
88
|
-
)
|
|
89
|
-
denied_response(result[:reason])
|
|
90
|
-
end
|
|
62
|
+
dispatch_policy(env, principal, effective_permission(env, perm))
|
|
91
63
|
rescue StandardError => e
|
|
92
64
|
handle_exception(
|
|
93
65
|
e,
|
|
@@ -128,14 +100,52 @@ module Legion
|
|
|
128
100
|
nil
|
|
129
101
|
end
|
|
130
102
|
|
|
131
|
-
def
|
|
132
|
-
|
|
133
|
-
|
|
103
|
+
def bypass(env, path, reason)
|
|
104
|
+
log.debug("RBAC middleware bypass path=#{path} reason=#{reason}")
|
|
105
|
+
@app.call(env)
|
|
106
|
+
end
|
|
134
107
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
108
|
+
def guard_missing(env, path, reason)
|
|
109
|
+
if Legion::Rbac.enforcing?
|
|
110
|
+
log.warn("RBAC middleware denied method=#{env['REQUEST_METHOD']} path=#{path} reason=#{reason.tr(' ', '_')}")
|
|
111
|
+
denied_response(reason)
|
|
112
|
+
else
|
|
113
|
+
audit_and_proceed(env, reason)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def dispatch_policy(env, principal, perm)
|
|
118
|
+
result = policy_result(env, principal, perm)
|
|
119
|
+
path = env['PATH_INFO']
|
|
120
|
+
|
|
121
|
+
if result[:would_deny]
|
|
122
|
+
log.info(
|
|
123
|
+
"[RBAC audit] would_deny: #{result[:reason]} principal=#{result[:principal_id]} " \
|
|
124
|
+
"action=#{result[:action]} resource=#{result[:resource]}"
|
|
125
|
+
)
|
|
126
|
+
@app.call(env)
|
|
127
|
+
elsif result[:allowed]
|
|
128
|
+
log.info(
|
|
129
|
+
"RBAC middleware allowed principal=#{principal.id} method=#{env['REQUEST_METHOD']} " \
|
|
130
|
+
"path=#{path} resource=#{perm[:resource]} action=#{perm[:action]} " \
|
|
131
|
+
"target_team=#{env['legion.rbac.target_team'] || 'none'}"
|
|
132
|
+
)
|
|
133
|
+
@app.call(env)
|
|
134
|
+
else
|
|
135
|
+
log.warn(
|
|
136
|
+
"RBAC middleware denied principal=#{principal.id} method=#{env['REQUEST_METHOD']} " \
|
|
137
|
+
"path=#{path} resource=#{perm[:resource]} action=#{perm[:action]} " \
|
|
138
|
+
"target_team=#{env['legion.rbac.target_team'] || 'none'} reason=#{result[:reason]}"
|
|
139
|
+
)
|
|
140
|
+
denied_response(result[:reason])
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def audit_and_proceed(env, reason)
|
|
145
|
+
log.info(
|
|
146
|
+
"[RBAC audit] would_deny: #{reason} method=#{env['REQUEST_METHOD']} path=#{env['PATH_INFO']}"
|
|
147
|
+
)
|
|
148
|
+
@app.call(env)
|
|
139
149
|
end
|
|
140
150
|
|
|
141
151
|
def denied_response(reason)
|
data/lib/legion/rbac/settings.rb
CHANGED
|
@@ -18,6 +18,7 @@ module Legion
|
|
|
18
18
|
default_local_role: 'admin',
|
|
19
19
|
static_assignments: [],
|
|
20
20
|
route_permissions: {},
|
|
21
|
+
group_role_map: {},
|
|
21
22
|
roles: default_roles,
|
|
22
23
|
entra: entra_defaults,
|
|
23
24
|
capability_audit: capability_audit_defaults
|
|
@@ -47,6 +48,7 @@ module Legion
|
|
|
47
48
|
log.debug('RBAC Entra defaults requested')
|
|
48
49
|
{
|
|
49
50
|
tenant_id: nil,
|
|
51
|
+
client_id: nil,
|
|
50
52
|
role_map: {
|
|
51
53
|
'Legion.Admin' => 'admin',
|
|
52
54
|
'Legion.Supervisor' => 'supervisor',
|
data/lib/legion/rbac/version.rb
CHANGED
data/lib/legion/rbac.rb
CHANGED
|
@@ -13,6 +13,7 @@ require 'legion/rbac/team_scope'
|
|
|
13
13
|
require 'legion/rbac/store'
|
|
14
14
|
require 'legion/rbac/kerberos_claims_mapper'
|
|
15
15
|
require 'legion/rbac/entra_claims_mapper'
|
|
16
|
+
require 'legion/rbac/group_role_mapper'
|
|
16
17
|
require 'legion/rbac/middleware'
|
|
17
18
|
require 'legion/rbac/routes'
|
|
18
19
|
require 'legion/rbac/capability_audit'
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: legion-rbac
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -79,6 +79,7 @@ files:
|
|
|
79
79
|
- lib/legion/rbac/capability_registry.rb
|
|
80
80
|
- lib/legion/rbac/config_loader.rb
|
|
81
81
|
- lib/legion/rbac/entra_claims_mapper.rb
|
|
82
|
+
- lib/legion/rbac/group_role_mapper.rb
|
|
82
83
|
- lib/legion/rbac/kerberos_claims_mapper.rb
|
|
83
84
|
- lib/legion/rbac/middleware.rb
|
|
84
85
|
- lib/legion/rbac/permission.rb
|