legion-rbac 0.2.0 → 0.2.4
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 +30 -1
- data/CLAUDE.md +25 -1
- data/README.md +9 -0
- data/lib/legion/rbac/kerberos_claims_mapper.rb +16 -10
- data/lib/legion/rbac/middleware.rb +11 -0
- data/lib/legion/rbac/principal.rb +28 -14
- data/lib/legion/rbac/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a3b1a79a9bbe7774bc480993840934423d224605950bcc02f8f0fc6fdd20e432
|
|
4
|
+
data.tar.gz: e29db0b20c25a9b885f82a997f80f687eb65311cf5c5422f4addb894a6c9a581
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '044709e04e2f671b76dfc645a254b11d870af45012d23b5ea514cbeec152cc248db09382a68496d74f0152b30984d2be49d9a9a4e4fa655074b0d6bf3636caa2'
|
|
7
|
+
data.tar.gz: 546da78bffd9f7fbf5bac8fd77f5bc649caca1eb15694dc93fdada82ac7e05fc250ee938cb99e8b14dbe8fca3a4f24cfe07d07cf529c4cebc14df88acca0049c
|
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,35 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## [
|
|
3
|
+
## [0.2.3] - 2026-03-20
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Emit `rbac.deny` event on access denial for safety metrics integration
|
|
7
|
+
|
|
8
|
+
## [0.2.2] - 2026-03-18
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Organizational profile attributes on `Principal`: `title`, `department`, `company`, `city`, `state`, `country`, `country_code`, `cn`, `ad_created_at`
|
|
12
|
+
- `Principal#profile` hash accessor for all extended identity attributes
|
|
13
|
+
- `PROFILE_KEYS` constant defines the full set of identity/org fields
|
|
14
|
+
- `define_method` generates individual accessors from `PROFILE_KEYS`
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- `Principal` constructor accepts `**extra` kwargs for profile fields (replaces individual keyword args)
|
|
18
|
+
- `from_claims` iterates `PROFILE_KEYS` to extract all profile fields from claims
|
|
19
|
+
- `KerberosClaimsMapper.map` uses `**profile` splat directly (passes all profile kwargs through)
|
|
20
|
+
|
|
21
|
+
## [0.2.1] - 2026-03-18
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
- `samaccountname`, `ad_fqdn`, `first_name`, `last_name`, `email`, `display_name` attributes on `Principal`
|
|
25
|
+
- `from_claims` extracts identity attributes from Kerberos claims
|
|
26
|
+
- `KerberosClaimsMapper.map` emits `samaccountname` and `ad_fqdn` from principal, accepts `**profile` kwargs
|
|
27
|
+
- `KerberosClaimsMapper.map_with_fallback` passes profile through to `map`
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
- `KerberosClaimsMapper.map` now returns `.compact` hash (omits nil values)
|
|
31
|
+
|
|
32
|
+
## [0.2.0] - 2026-03-17
|
|
4
33
|
|
|
5
34
|
### Added
|
|
6
35
|
- `KerberosClaimsMapper` module: maps Kerberos principals and AD group memberships to Legion roles
|
data/CLAUDE.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# Legion::Rbac
|
|
2
2
|
|
|
3
3
|
**Parent**: `/Users/miverso2/rubymine/legion/CLAUDE.md`
|
|
4
|
+
**GitHub**: https://github.com/LegionIO/legion-rbac
|
|
5
|
+
**Version**: 0.2.2
|
|
4
6
|
|
|
5
7
|
Optional RBAC gem for LegionIO. Vault-style flat policy model with deny-always-wins semantics.
|
|
6
8
|
|
|
@@ -46,7 +48,29 @@ lib/legion/rbac/kerberos_claims_mapper.rb # Kerberos principal + AD groups -> Le
|
|
|
46
48
|
Two identity provider mappers convert external claims to Legion principals:
|
|
47
49
|
|
|
48
50
|
- **EntraClaimsMapper**: Maps Entra ID `roles` and `groups` claims to Legion roles. Uses `module_function` pattern. Configurable `role_map` and `group_map` with `default_role` fallback.
|
|
49
|
-
- **KerberosClaimsMapper**: Maps Kerberos principal (`user@REALM`) and AD group DNs to Legion roles. `map_with_fallback` tries LDAP groups first, falls back to Entra if configured.
|
|
51
|
+
- **KerberosClaimsMapper**: Maps Kerberos principal (`user@REALM`) and AD group DNs to Legion roles. `map_with_fallback` tries LDAP groups first, falls back to Entra if configured. Passes through all `**profile` kwargs (identity + org attributes) from LDAP into the claims hash.
|
|
52
|
+
|
|
53
|
+
## Principal Identity Model
|
|
54
|
+
|
|
55
|
+
`Principal` carries core identity (`id`, `type`, `roles`, `team`, `auth_method`, `samaccountname`, `ad_fqdn`) plus a `profile` hash of extended attributes populated from AD/LDAP:
|
|
56
|
+
|
|
57
|
+
| Accessor | LDAP Source | Example |
|
|
58
|
+
|----------|-------------|---------|
|
|
59
|
+
| `first_name` | `givenName` | Jane |
|
|
60
|
+
| `last_name` | `sn` | Doe |
|
|
61
|
+
| `email` | `mail` | jane.doe@example.com |
|
|
62
|
+
| `display_name` | `displayName` | Doe, Jane A |
|
|
63
|
+
| `title` | `title` | Senior Engineer |
|
|
64
|
+
| `department` | `department` | Platform Engineering |
|
|
65
|
+
| `company` | `company` | Acme Corp |
|
|
66
|
+
| `city` | `l` | Minneapolis |
|
|
67
|
+
| `state` | `st` | MN |
|
|
68
|
+
| `country` | `co` | USA |
|
|
69
|
+
| `country_code` | `c` | US |
|
|
70
|
+
| `cn` | `cn` | jdoe1 |
|
|
71
|
+
| `ad_created_at` | `whenCreated` | 20200115093012.0Z |
|
|
72
|
+
|
|
73
|
+
All profile fields are accessible as direct methods (`principal.title`) or via `principal.profile` hash.
|
|
50
74
|
|
|
51
75
|
## Guards
|
|
52
76
|
|
data/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Role-based access control for LegionIO, following Vault-style flat policy patterns.
|
|
4
4
|
|
|
5
|
+
**Version**: 0.2.2
|
|
6
|
+
|
|
5
7
|
## Features
|
|
6
8
|
|
|
7
9
|
- Flat policy model: deny-always-wins, no role inheritance
|
|
@@ -12,6 +14,7 @@ Role-based access control for LegionIO, following Vault-style flat policy patter
|
|
|
12
14
|
- Dual-mode store: DB-backed via Sequel or static fallback
|
|
13
15
|
- Entra ID claims mapping (roles and groups to Legion roles)
|
|
14
16
|
- Kerberos claims mapping (AD group DNs to Legion roles, with Entra fallback)
|
|
17
|
+
- Rich identity profile from AD/LDAP (name, email, title, department, company, location)
|
|
15
18
|
- Fully optional: guarded by `if defined?(Legion::Rbac)` in LegionIO
|
|
16
19
|
|
|
17
20
|
## Installation
|
|
@@ -36,6 +39,12 @@ Legion::Rbac.setup
|
|
|
36
39
|
```ruby
|
|
37
40
|
principal = Legion::Rbac::Principal.new(id: 'user-1', roles: [:worker], team: 'team-a')
|
|
38
41
|
Legion::Rbac.authorize!(principal: principal, action: :execute, resource: 'runners/lex-http/request/get')
|
|
42
|
+
|
|
43
|
+
# Kerberos principals carry full AD profile
|
|
44
|
+
principal.first_name # => "Jane"
|
|
45
|
+
principal.title # => "Senior Engineer"
|
|
46
|
+
principal.department # => "Platform Engineering"
|
|
47
|
+
principal.profile # => { first_name: "Jane", last_name: "Doe", ... }
|
|
39
48
|
```
|
|
40
49
|
|
|
41
50
|
### Execution Authorization
|
|
@@ -7,29 +7,35 @@ module Legion
|
|
|
7
7
|
|
|
8
8
|
module_function
|
|
9
9
|
|
|
10
|
-
def map(principal:, groups:, role_map: {}, default_role: DEFAULT_ROLE)
|
|
11
|
-
|
|
10
|
+
def map(principal:, groups:, role_map: {}, default_role: DEFAULT_ROLE, **profile)
|
|
11
|
+
parts = principal.split('@', 2)
|
|
12
|
+
username = parts.first
|
|
13
|
+
realm = parts.length > 1 ? parts.last : nil
|
|
12
14
|
roles = Array(groups).filter_map { |g| role_map[g] }.uniq
|
|
13
15
|
roles = [default_role] if roles.empty?
|
|
14
16
|
|
|
15
17
|
{
|
|
16
|
-
sub:
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
sub: username,
|
|
19
|
+
samaccountname: username,
|
|
20
|
+
ad_fqdn: realm&.downcase,
|
|
21
|
+
roles: roles,
|
|
22
|
+
scope: 'human',
|
|
23
|
+
auth_method: 'kerberos',
|
|
24
|
+
**profile
|
|
25
|
+
}.compact
|
|
21
26
|
end
|
|
22
27
|
|
|
23
|
-
def map_with_fallback(principal:, groups: nil, fallback: :entra, role_map: {},
|
|
28
|
+
def map_with_fallback(principal:, groups: nil, fallback: :entra, role_map: {},
|
|
29
|
+
default_role: DEFAULT_ROLE, **profile)
|
|
24
30
|
if groups&.any?
|
|
25
|
-
map(principal: principal, groups: groups, role_map: role_map, default_role: default_role)
|
|
31
|
+
map(principal: principal, groups: groups, role_map: role_map, default_role: default_role, **profile)
|
|
26
32
|
elsif fallback == :entra && defined?(Legion::Rbac::EntraClaimsMapper)
|
|
27
33
|
entra_claims = { sub: principal, preferred_username: principal }
|
|
28
34
|
result = EntraClaimsMapper.map_claims(entra_claims)
|
|
29
35
|
result&.merge(auth_method: 'kerberos') || map(principal: principal, groups: [],
|
|
30
36
|
role_map: role_map, default_role: default_role)
|
|
31
37
|
else
|
|
32
|
-
map(principal: principal, groups: [], role_map: role_map, default_role: default_role)
|
|
38
|
+
map(principal: principal, groups: [], role_map: role_map, default_role: default_role, **profile)
|
|
33
39
|
end
|
|
34
40
|
end
|
|
35
41
|
end
|
|
@@ -31,6 +31,8 @@ module Legion
|
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
def call(env)
|
|
34
|
+
return @app.call(env) unless enforce?
|
|
35
|
+
|
|
34
36
|
path = env['PATH_INFO']
|
|
35
37
|
return @app.call(env) if skip_path?(path)
|
|
36
38
|
return @app.call(env) if invoke_route?(path)
|
|
@@ -50,6 +52,7 @@ module Legion
|
|
|
50
52
|
if result[:allowed]
|
|
51
53
|
@app.call(env)
|
|
52
54
|
else
|
|
55
|
+
Legion::Events.emit('rbac.deny', reason: result[:reason]) if defined?(Legion::Events)
|
|
53
56
|
denied_response(result[:reason])
|
|
54
57
|
end
|
|
55
58
|
end
|
|
@@ -78,6 +81,14 @@ module Legion
|
|
|
78
81
|
nil
|
|
79
82
|
end
|
|
80
83
|
|
|
84
|
+
def enforce?
|
|
85
|
+
return false unless defined?(Legion::Settings)
|
|
86
|
+
|
|
87
|
+
Legion::Settings[:rbac][:enforce]
|
|
88
|
+
rescue StandardError
|
|
89
|
+
true
|
|
90
|
+
end
|
|
91
|
+
|
|
81
92
|
def denied_response(reason)
|
|
82
93
|
body = Legion::JSON.dump({ error: 'access_denied', reason: reason })
|
|
83
94
|
[403, { 'content-type' => 'application/json' }, [body]]
|
|
@@ -3,31 +3,45 @@
|
|
|
3
3
|
module Legion
|
|
4
4
|
module Rbac
|
|
5
5
|
class Principal
|
|
6
|
-
|
|
6
|
+
PROFILE_KEYS = %i[
|
|
7
|
+
first_name last_name email display_name cn title
|
|
8
|
+
department company country country_code city state ad_created_at
|
|
9
|
+
].freeze
|
|
7
10
|
|
|
8
|
-
|
|
11
|
+
attr_reader :id, :type, :roles, :team, :auth_method,
|
|
12
|
+
:samaccountname, :ad_fqdn, :profile
|
|
13
|
+
|
|
14
|
+
def initialize(id:, type: :human, roles: [], team: nil, auth_method: nil, # rubocop:disable Metrics/ParameterLists
|
|
15
|
+
samaccountname: nil, ad_fqdn: nil, **extra)
|
|
9
16
|
@id = id
|
|
10
17
|
@type = type.to_sym
|
|
11
18
|
@roles = roles.map(&:to_s)
|
|
12
19
|
@team = team
|
|
20
|
+
@auth_method = auth_method
|
|
21
|
+
@samaccountname = samaccountname
|
|
22
|
+
@ad_fqdn = ad_fqdn
|
|
23
|
+
@profile = extra.slice(*PROFILE_KEYS).compact
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
PROFILE_KEYS.each do |key|
|
|
27
|
+
define_method(key) { @profile[key] }
|
|
13
28
|
end
|
|
14
29
|
|
|
15
30
|
def self.from_claims(claims)
|
|
16
31
|
scope = claims[:scope] || claims['scope']
|
|
32
|
+
common = {
|
|
33
|
+
roles: claims[:roles] || claims['roles'] || [],
|
|
34
|
+
team: claims[:team] || claims['team'],
|
|
35
|
+
auth_method: claims[:auth_method] || claims['auth_method'],
|
|
36
|
+
samaccountname: claims[:samaccountname] || claims['samaccountname'],
|
|
37
|
+
ad_fqdn: claims[:ad_fqdn] || claims['ad_fqdn']
|
|
38
|
+
}
|
|
39
|
+
PROFILE_KEYS.each { |key| common[key] = claims[key] || claims[key.to_s] }
|
|
40
|
+
|
|
17
41
|
if scope == 'worker'
|
|
18
|
-
new(
|
|
19
|
-
id: claims[:worker_id] || claims['worker_id'],
|
|
20
|
-
type: :worker,
|
|
21
|
-
roles: claims[:roles] || claims['roles'] || [],
|
|
22
|
-
team: claims[:team] || claims['team']
|
|
23
|
-
)
|
|
42
|
+
new(id: claims[:worker_id] || claims['worker_id'], type: :worker, **common)
|
|
24
43
|
else
|
|
25
|
-
new(
|
|
26
|
-
id: claims[:sub] || claims['sub'],
|
|
27
|
-
type: :human,
|
|
28
|
-
roles: claims[:roles] || claims['roles'] || [],
|
|
29
|
-
team: claims[:team] || claims['team']
|
|
30
|
-
)
|
|
44
|
+
new(id: claims[:sub] || claims['sub'], type: :human, **common)
|
|
31
45
|
end
|
|
32
46
|
end
|
|
33
47
|
|
data/lib/legion/rbac/version.rb
CHANGED
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.2.
|
|
4
|
+
version: 0.2.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -96,7 +96,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
96
96
|
- !ruby/object:Gem::Version
|
|
97
97
|
version: '0'
|
|
98
98
|
requirements: []
|
|
99
|
-
rubygems_version:
|
|
99
|
+
rubygems_version: 3.6.9
|
|
100
100
|
specification_version: 4
|
|
101
101
|
summary: Legion::Rbac
|
|
102
102
|
test_files: []
|