legion-rbac 0.2.0 → 0.2.5

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0a4b81abbd8b47fb1b0809f98cfc4e070553af729984f9994e2b70708b70e1db
4
- data.tar.gz: 29b556506bf933800fadc5af6837f325b65eca9924bc45b0c3b49521a487f750
3
+ metadata.gz: 9435f984911637f699631abe3a6a5d82fc3706b7799658cd5398103f8ffa85b3
4
+ data.tar.gz: 549a609998a6782bb783364b912acc4a93452ac8e10ec31427bd98cef6fd54c9
5
5
  SHA512:
6
- metadata.gz: 79b92ac1e96706268e165c0b613bf506a18e56764e7a0da547a3c0096e49e5f8389193fc34350f70f858ab2eca638c9674ec8d70ed244bae7375c4a9d2504dc6
7
- data.tar.gz: 00be7bf03360454dc126a73b0f65a66555ed4f56b5c16f8e82082f9bd93cacf82537e8bae173d6de85f1af41f43762e10f6ed0b2335f0f249bc04d71013de294
6
+ metadata.gz: bc76a38a026310d1f4debb2177f1ae41c6919b446859c10be852199a908da77c2a9221f8a1ec2fecc74506bcdde882f34966b1ca34e7931256125aa654e04215
7
+ data.tar.gz: 52c7dff1da836aeb87938f40ba75e535c6b67e1d0cde8a14f05ec3af9452240f7cf7959b7476a3491c80d9a73b9ee59f98fd0065c88d0af372cf4eec6fa312aa
data/CHANGELOG.md CHANGED
@@ -1,6 +1,45 @@
1
1
  # Changelog
2
2
 
3
- ## [Unreleased]
3
+ ## [0.2.5] - 2026-03-22
4
+
5
+ ### Changed
6
+ - Added logging to silent rescue block in middleware.rb enforce? method
7
+
8
+ ## [0.2.4] - 2026-03-22
9
+
10
+ ### Changed
11
+ - Bumped version for rbac.deny event emission
12
+
13
+ ## [0.2.3] - 2026-03-20
14
+
15
+ ### Added
16
+ - Emit `rbac.deny` event on access denial for safety metrics integration
17
+
18
+ ## [0.2.2] - 2026-03-18
19
+
20
+ ### Added
21
+ - Organizational profile attributes on `Principal`: `title`, `department`, `company`, `city`, `state`, `country`, `country_code`, `cn`, `ad_created_at`
22
+ - `Principal#profile` hash accessor for all extended identity attributes
23
+ - `PROFILE_KEYS` constant defines the full set of identity/org fields
24
+ - `define_method` generates individual accessors from `PROFILE_KEYS`
25
+
26
+ ### Changed
27
+ - `Principal` constructor accepts `**extra` kwargs for profile fields (replaces individual keyword args)
28
+ - `from_claims` iterates `PROFILE_KEYS` to extract all profile fields from claims
29
+ - `KerberosClaimsMapper.map` uses `**profile` splat directly (passes all profile kwargs through)
30
+
31
+ ## [0.2.1] - 2026-03-18
32
+
33
+ ### Added
34
+ - `samaccountname`, `ad_fqdn`, `first_name`, `last_name`, `email`, `display_name` attributes on `Principal`
35
+ - `from_claims` extracts identity attributes from Kerberos claims
36
+ - `KerberosClaimsMapper.map` emits `samaccountname` and `ad_fqdn` from principal, accepts `**profile` kwargs
37
+ - `KerberosClaimsMapper.map_with_fallback` passes profile through to `map`
38
+
39
+ ### Changed
40
+ - `KerberosClaimsMapper.map` now returns `.compact` hash (omits nil values)
41
+
42
+ ## [0.2.0] - 2026-03-17
4
43
 
5
44
  ### Added
6
45
  - `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. Injects `auth_method: 'kerberos'` for identity signal tracking in lex-identity.
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
- username = principal.split('@', 2).first
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: username,
17
- roles: roles,
18
- scope: 'human',
19
- auth_method: 'kerberos'
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: {}, default_role: DEFAULT_ROLE)
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,15 @@ 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 => e
89
+ Legion::Logging.warn("Legion::Rbac::Middleware#enforce? failed, defaulting to enforce: #{e.message}") if defined?(Legion::Logging)
90
+ true
91
+ end
92
+
81
93
  def denied_response(reason)
82
94
  body = Legion::JSON.dump({ error: 'access_denied', reason: reason })
83
95
  [403, { 'content-type' => 'application/json' }, [body]]
@@ -3,31 +3,45 @@
3
3
  module Legion
4
4
  module Rbac
5
5
  class Principal
6
- attr_reader :id, :type, :roles, :team
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
- def initialize(id:, type: :human, roles: [], team: nil)
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
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module Rbac
5
- VERSION = '0.2.0'
5
+ VERSION = '0.2.5'
6
6
  end
7
7
  end
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.0
4
+ version: 0.2.5
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: 4.0.8
99
+ rubygems_version: 3.6.9
100
100
  specification_version: 4
101
101
  summary: Legion::Rbac
102
102
  test_files: []