legion-rbac 0.3.3 → 0.3.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: 2805fec42a72b9618d9790becf4439f3fba5bbe3af5f60f4fd6ce22b230ca4b6
4
- data.tar.gz: b4648e10753c56aec63c122f765a00cc545fe6626a3fcd25a344fa829df5e615
3
+ metadata.gz: 2c62ae3d8b8ed4b7285f13ecc1e5d90d79f343cf98a4d56a342af761aba03185
4
+ data.tar.gz: f705bad75d3d459e82463c50d3110df25e847d36f2e092e88e10ae545ba67cac
5
5
  SHA512:
6
- metadata.gz: 2f1a104089a800dbfd7fdd6e062f24cef9205aed00c0accd543deee71327705b5f20dbb259f0cf9c579a67df5046b441ecdf1cfa33f84e485105f4b2fb18c4b9
7
- data.tar.gz: 76c1c1466a6956c8406626c414173476181c2b936675aee21e6b7590afd7eb5021b4718c1db3dc01b1ee27f016039673387b54cfbf44933be7915b1c0e22e0ff
6
+ metadata.gz: c7d12ee4a0eaac6ea80132816a429dac7621253a4d720ef69dceaedcf1a00b63af70f0004b4c3e1bbec3f48ff9c8e8eb34eee8f0365e3abbd6a96b94c86727c4
7
+ data.tar.gz: 7f62612de461d2339ea9d70c9de87d5b8cc601622986a51ae3dfa02a22ac78d709b46dbc4ff5ad7cd78d8418f6ee2defa9912d9d2cc187ddb48a8f0541305fbf
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.5] - 2026-10-30
4
+
5
+ ### Fixed
6
+ - Deliver authorization decisions and policy-change audit events through the Core event bus keyword payload contract instead of losing events to a rescued argument error.
7
+
8
+ ## [0.3.4] - 2026-05-09
9
+
10
+ ### Removed
11
+ - Unnecessary `defined?(Legion::Logging)` guards from route handlers — legion-logging is a hard gemspec dependency and always available
12
+
3
13
  ## [0.3.3] - 2026-04-08
4
14
 
5
15
  ### Added
data/README.md CHANGED
@@ -90,6 +90,12 @@ result = Legion::Rbac::PolicyEngine.evaluate(principal: principal, action: :read
90
90
  # => { allowed: true, reason: "...", would_deny: false }
91
91
  ```
92
92
 
93
+ ### Audit Events
94
+
95
+ When the optional Core `Legion::Events` bus is loaded and `rbac.emit_events` is enabled, subscribers receive `rbac.granted` and `rbac.denied` decisions with principal, resource/capability, enforcement and request-correlation context. Dry-run denials still emit `rbac.denied` with `would_deny: true`; authorization remains nonblocking.
96
+
97
+ Successful policy mutations emit `rbac.policy_changed` with the actor, target record and request correlation. RBAC 0.3.5 uses Core's keyword payload contract, so these notifications reach ordinary `Legion::Events.on` subscribers rather than being lost to an argument error. Event-delivery failures are logged without changing the authorization decision.
98
+
93
99
  ### CLI
94
100
 
95
101
  ```bash
@@ -34,8 +34,8 @@ module Legion
34
34
  def self.pattern_to_regex(pattern)
35
35
  parts = pattern.split('/').map do |segment|
36
36
  case segment
37
- when '*' then '.*'
38
- when '+' then '[^/]+'
37
+ when '*' then '.*'
38
+ when '+' then '[^/]+'
39
39
  else Regexp.escape(segment)
40
40
  end
41
41
  end
@@ -80,8 +80,8 @@ module Legion
80
80
  def self.pattern_to_regex(pattern)
81
81
  parts = pattern.split('/').map do |segment|
82
82
  case segment
83
- when '*' then '.*'
84
- when '+' then '[^/]+'
83
+ when '*' then '.*'
84
+ when '+' then '[^/]+'
85
85
  else Regexp.escape(segment)
86
86
  end
87
87
  end
@@ -393,7 +393,7 @@ module Legion
393
393
  enforce: context[:enforce],
394
394
  reason: result[:reason]
395
395
  }.merge(context[:event_context]).compact
396
- Legion::Events.emit(event_name, payload)
396
+ Legion::Events.emit(event_name, **payload)
397
397
  rescue StandardError => e
398
398
  handle_exception(e, level: :warn, operation: 'rbac.policy_engine.emit_decision_event', event: event_name)
399
399
  end
@@ -146,7 +146,7 @@ module Legion
146
146
 
147
147
  def self.register_check(app)
148
148
  app.post '/api/rbac/check' do
149
- Legion::Logging.debug "API: POST /api/rbac/check params=#{params.keys}" if defined?(Legion::Logging)
149
+ Legion::Logging.debug "API: POST /api/rbac/check params=#{params.keys}"
150
150
  return json_error('rbac_unavailable', 'legion-rbac not installed', status_code: 501) unless defined?(Legion::Rbac)
151
151
 
152
152
  body = parse_request_body
@@ -163,12 +163,12 @@ module Legion
163
163
  )
164
164
  json_response(result)
165
165
  rescue StandardError => e
166
- Legion::Logging.error "API POST /api/rbac/check: #{e.class} — #{e.message}" if defined?(Legion::Logging)
166
+ Legion::Logging.error "API POST /api/rbac/check: #{e.class} — #{e.message}"
167
167
  json_error('rbac_error', e.message, status_code: 500)
168
168
  end
169
169
  end
170
170
 
171
- def self.register_assignments(app) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
171
+ def self.register_assignments(app) # rubocop:disable Metrics/AbcSize
172
172
  app.get '/api/rbac/assignments' do
173
173
  return json_error('rbac_unavailable', 'legion-rbac not installed', status_code: 501) unless defined?(Legion::Rbac)
174
174
  return json_error('db_unavailable', 'legion-data not connected', status_code: 503) unless Legion::Rbac::Store.db_available?
@@ -181,7 +181,7 @@ module Legion
181
181
  end
182
182
 
183
183
  app.post '/api/rbac/assignments' do
184
- Legion::Logging.debug "API: POST /api/rbac/assignments params=#{params.keys}" if defined?(Legion::Logging)
184
+ Legion::Logging.debug "API: POST /api/rbac/assignments params=#{params.keys}"
185
185
  return json_error('rbac_unavailable', 'legion-rbac not installed', status_code: 501) unless defined?(Legion::Rbac)
186
186
  return json_error('db_unavailable', 'legion-data not connected', status_code: 503) unless Legion::Rbac::Store.db_available?
187
187
 
@@ -194,13 +194,13 @@ module Legion
194
194
  granted_by: current_owner_msid || 'api',
195
195
  expires_at: parse_optional_time(body[:expires_at], field: 'expires_at')
196
196
  )
197
- Legion::Logging.info "API: created RBAC assignment #{record.id} role=#{body[:role]} principal=#{body[:principal_id]}" if defined?(Legion::Logging)
197
+ Legion::Logging.info "API: created RBAC assignment #{record.id} role=#{body[:role]} principal=#{body[:principal_id]}"
198
198
  emit_rbac_policy_changed('assignment.created', 'role_assignment', record.values)
199
199
  json_response(record.values, status_code: 201)
200
200
  rescue Legion::Rbac::Routes::InvalidTimestamp => e
201
201
  json_error('validation_error', e.message, status_code: 422)
202
202
  rescue Sequel::ValidationFailed => e
203
- Legion::Logging.warn "API POST /api/rbac/assignments returned 422: #{e.message}" if defined?(Legion::Logging)
203
+ Legion::Logging.warn "API POST /api/rbac/assignments returned 422: #{e.message}"
204
204
  json_error('validation_error', e.message, status_code: 422)
205
205
  end
206
206
 
@@ -213,7 +213,7 @@ module Legion
213
213
 
214
214
  snapshot = record.values.dup
215
215
  record.destroy
216
- Legion::Logging.info "API: deleted RBAC assignment #{params[:id]}" if defined?(Legion::Logging)
216
+ Legion::Logging.info "API: deleted RBAC assignment #{params[:id]}"
217
217
  emit_rbac_policy_changed('assignment.deleted', 'role_assignment', snapshot)
218
218
  json_response({ deleted: true })
219
219
  end
@@ -230,7 +230,7 @@ module Legion
230
230
  end
231
231
 
232
232
  app.post '/api/rbac/grants' do
233
- Legion::Logging.debug "API: POST /api/rbac/grants params=#{params.keys}" if defined?(Legion::Logging)
233
+ Legion::Logging.debug "API: POST /api/rbac/grants params=#{params.keys}"
234
234
  return json_error('rbac_unavailable', 'legion-rbac not installed', status_code: 501) unless defined?(Legion::Rbac)
235
235
  return json_error('db_unavailable', 'legion-data not connected', status_code: 503) unless Legion::Rbac::Store.db_available?
236
236
 
@@ -241,11 +241,11 @@ module Legion
241
241
  actions: Array(body[:actions]).join(','),
242
242
  granted_by: current_owner_msid || 'api'
243
243
  )
244
- Legion::Logging.info "API: created RBAC grant #{record.id} team=#{body[:team]} pattern=#{body[:runner_pattern]}" if defined?(Legion::Logging)
244
+ Legion::Logging.info "API: created RBAC grant #{record.id} team=#{body[:team]} pattern=#{body[:runner_pattern]}"
245
245
  emit_rbac_policy_changed('runner_grant.created', 'runner_grant', record.values)
246
246
  json_response(record.values, status_code: 201)
247
247
  rescue Sequel::ValidationFailed => e
248
- Legion::Logging.warn "API POST /api/rbac/grants returned 422: #{e.message}" if defined?(Legion::Logging)
248
+ Legion::Logging.warn "API POST /api/rbac/grants returned 422: #{e.message}"
249
249
  json_error('validation_error', e.message, status_code: 422)
250
250
  end
251
251
 
@@ -258,7 +258,7 @@ module Legion
258
258
 
259
259
  snapshot = record.values.dup
260
260
  record.destroy
261
- Legion::Logging.info "API: deleted RBAC grant #{params[:id]}" if defined?(Legion::Logging)
261
+ Legion::Logging.info "API: deleted RBAC grant #{params[:id]}"
262
262
  emit_rbac_policy_changed('runner_grant.deleted', 'runner_grant', snapshot)
263
263
  json_response({ deleted: true })
264
264
  end
@@ -274,7 +274,7 @@ module Legion
274
274
  end
275
275
 
276
276
  app.post '/api/rbac/grants/cross-team' do
277
- Legion::Logging.debug "API: POST /api/rbac/grants/cross-team params=#{params.keys}" if defined?(Legion::Logging)
277
+ Legion::Logging.debug "API: POST /api/rbac/grants/cross-team params=#{params.keys}"
278
278
  return json_error('rbac_unavailable', 'legion-rbac not installed', status_code: 501) unless defined?(Legion::Rbac)
279
279
  return json_error('db_unavailable', 'legion-data not connected', status_code: 503) unless Legion::Rbac::Store.db_available?
280
280
 
@@ -287,13 +287,13 @@ module Legion
287
287
  granted_by: current_owner_msid || 'api',
288
288
  expires_at: parse_optional_time(body[:expires_at], field: 'expires_at')
289
289
  )
290
- Legion::Logging.info "API: created cross-team RBAC grant #{record.id} #{body[:source_team]}->#{body[:target_team]}" if defined?(Legion::Logging)
290
+ Legion::Logging.info "API: created cross-team RBAC grant #{record.id} #{body[:source_team]}->#{body[:target_team]}"
291
291
  emit_rbac_policy_changed('cross_team_grant.created', 'cross_team_grant', record.values)
292
292
  json_response(record.values, status_code: 201)
293
293
  rescue Legion::Rbac::Routes::InvalidTimestamp => e
294
294
  json_error('validation_error', e.message, status_code: 422)
295
295
  rescue Sequel::ValidationFailed => e
296
- Legion::Logging.warn "API POST /api/rbac/grants/cross-team returned 422: #{e.message}" if defined?(Legion::Logging)
296
+ Legion::Logging.warn "API POST /api/rbac/grants/cross-team returned 422: #{e.message}"
297
297
  json_error('validation_error', e.message, status_code: 422)
298
298
  end
299
299
 
@@ -306,7 +306,7 @@ module Legion
306
306
 
307
307
  snapshot = record.values.dup
308
308
  record.destroy
309
- Legion::Logging.info "API: deleted cross-team RBAC grant #{params[:id]}" if defined?(Legion::Logging)
309
+ Legion::Logging.info "API: deleted cross-team RBAC grant #{params[:id]}"
310
310
  emit_rbac_policy_changed('cross_team_grant.deleted', 'cross_team_grant', snapshot)
311
311
  json_response({ deleted: true })
312
312
  end
@@ -382,7 +382,7 @@ module Legion
382
382
 
383
383
  Legion::Events.emit(
384
384
  'rbac.policy_changed',
385
- policy_change_payload(
385
+ **policy_change_payload(
386
386
  change_type: change_type,
387
387
  target_type: target_type,
388
388
  record_values: record_values,
@@ -390,8 +390,6 @@ module Legion
390
390
  )
391
391
  )
392
392
  rescue StandardError => e
393
- return unless defined?(Legion::Logging)
394
-
395
393
  Legion::Logging.warn("API policy change event failed type=#{target_type} change=#{change_type} error=#{e.class}: #{e.message}")
396
394
  end
397
395
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module Rbac
5
- VERSION = '0.3.3'
5
+ VERSION = '0.3.5'
6
6
  end
7
7
  end
data/lib/legion/rbac.rb CHANGED
@@ -87,10 +87,10 @@ module Legion
87
87
 
88
88
  def events_enabled?
89
89
  return false unless defined?(Legion::Events)
90
- return false unless defined?(Legion::Settings)
91
90
 
92
91
  Legion::Settings[:rbac]&.fetch(:emit_events, true) != false
93
- rescue StandardError
92
+ rescue StandardError => e
93
+ handle_exception(e, level: :warn, operation: 'rbac.events_enabled')
94
94
  false
95
95
  end
96
96
 
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.3
4
+ version: 0.3.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity