legionio 1.4.43 → 1.4.44

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: 51e90ef95b551c27c044e9f9dac0d784a529d4947835ec0d6501276f73281fb9
4
- data.tar.gz: 533c1b9813327e486e33cb2c1d657907248517deb15935c68616a095be699280
3
+ metadata.gz: 16599e1c095434bfb65c5a5aa763cb821324e192f8e367cad1aff64fadec7e10
4
+ data.tar.gz: 2747877e468f15212ed3bbd19eaa287323d159a676a01e80e6b1ca11e0abed9b
5
5
  SHA512:
6
- metadata.gz: 298d051c13e5e658eb572971863a7039d3c08bd168b40b2e6806a3139ccae7261ff29c916bb84ab34fd2fea6c2e6905615f8689e80f5a415feafe75cbf0f56c5
7
- data.tar.gz: 1f0005c08f21b456e5d64923020e662efbe88a714514ad7bd27b59caa0efc05676551f8b87a150d0226aa1bc9237681d0688bb6b42f1430624ea5ba81e420903
6
+ metadata.gz: e522c7f831673baa38cf887b1493e78e7133d930f3e857b82fefddbeeffbcc88877e4d741330a222e744170bd0e5876ec08deca5c8a00d8dc6cc520fbd0f54dc
7
+ data.tar.gz: 802b45c5a8d13d79f0c52ac872080a6fde0e5b64bc7ee0f46a21fbdb5be56c23ddad2f6b242b535b9a862e13b446735cb57c44ac50a9a0b021a728c620defe20
data/.rubocop.yml CHANGED
@@ -37,6 +37,7 @@ Metrics/BlockLength:
37
37
  - 'lib/legion/cli/schedule_command.rb'
38
38
  - 'lib/legion/cli/update_command.rb'
39
39
  - 'lib/legion/api/auth.rb'
40
+ - 'lib/legion/api/auth_worker.rb'
40
41
 
41
42
  Metrics/AbcSize:
42
43
  Max: 60
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Legion Changelog
2
2
 
3
+ ## [1.4.44] - 2026-03-17
4
+
5
+ ### Added
6
+ - `POST /api/auth/worker-token`: Entra client credentials token exchange endpoint (validates client_credentials grant via JWKS, looks up worker by appid, issues scoped Legion worker JWT)
7
+ - Auth middleware SKIP_PATHS now includes `/api/auth/token` and `/api/auth/worker-token`
8
+
3
9
  ## [1.4.43] - 2026-03-17
4
10
 
5
11
  ### Fixed
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ class API < Sinatra::Base
5
+ module Routes
6
+ module AuthWorker
7
+ def self.registered(app)
8
+ register_worker_token_exchange(app)
9
+ end
10
+
11
+ def self.register_worker_token_exchange(app) # rubocop:disable Metrics/MethodLength
12
+ app.post '/api/auth/worker-token' do
13
+ body = parse_request_body
14
+ grant_type = body[:grant_type]
15
+ entra_token = body[:entra_token]
16
+
17
+ unless grant_type == 'client_credentials'
18
+ halt 400, json_error('unsupported_grant_type', 'grant_type must be client_credentials',
19
+ status_code: 400)
20
+ end
21
+
22
+ halt 400, json_error('missing_entra_token', 'entra_token is required', status_code: 400) unless entra_token
23
+
24
+ unless defined?(Legion::Crypt::JWT) && Legion::Crypt::JWT.respond_to?(:verify_with_jwks)
25
+ halt 501, json_error('jwks_validation_not_available',
26
+ 'JWKS validation is not available', status_code: 501)
27
+ end
28
+
29
+ entra_settings = Routes::AuthWorker.resolve_entra_settings
30
+ tenant_id = entra_settings[:tenant_id]
31
+ unless tenant_id
32
+ halt 500, json_error('entra_tenant_not_configured',
33
+ 'Entra tenant_id is not configured', status_code: 500)
34
+ end
35
+
36
+ jwks_url = "https://login.microsoftonline.com/#{tenant_id}/discovery/v2.0/keys"
37
+ issuer = "https://login.microsoftonline.com/#{tenant_id}/v2.0"
38
+
39
+ begin
40
+ claims = Legion::Crypt::JWT.verify_with_jwks(
41
+ entra_token, jwks_url: jwks_url, issuers: [issuer]
42
+ )
43
+ rescue Legion::Crypt::JWT::ExpiredTokenError
44
+ halt 401, json_error('token_expired', 'Entra token has expired', status_code: 401)
45
+ rescue Legion::Crypt::JWT::InvalidTokenError => e
46
+ halt 401, json_error('invalid_token', e.message, status_code: 401)
47
+ rescue Legion::Crypt::JWT::Error => e
48
+ halt 502, json_error('identity_provider_unavailable', e.message, status_code: 502)
49
+ end
50
+
51
+ app_id = claims[:appid] || claims[:azp] || claims['appid'] || claims['azp']
52
+ halt 401, json_error('invalid_token', 'missing appid claim', status_code: 401) unless app_id
53
+
54
+ halt 503, json_error('data_unavailable', 'legion-data not connected', status_code: 503) unless defined?(Legion::Data::Model::DigitalWorker)
55
+
56
+ worker = Legion::Data::Model::DigitalWorker.first(entra_app_id: app_id)
57
+ unless worker
58
+ halt 404, json_error('worker_not_found',
59
+ "no worker registered for entra_app_id #{app_id}", status_code: 404)
60
+ end
61
+
62
+ unless worker.lifecycle_state == 'active'
63
+ halt 403, json_error('worker_not_active',
64
+ "worker is in #{worker.lifecycle_state} state", status_code: 403)
65
+ end
66
+
67
+ ttl = 3600
68
+ token = Legion::API::Token.issue_worker_token(
69
+ worker_id: worker.worker_id, owner_msid: worker.owner_msid, ttl: ttl
70
+ )
71
+
72
+ json_response({
73
+ access_token: token,
74
+ token_type: 'Bearer',
75
+ expires_in: ttl,
76
+ worker_id: worker.worker_id,
77
+ scope: 'worker'
78
+ })
79
+ end
80
+ end
81
+
82
+ def self.resolve_entra_settings
83
+ return {} unless defined?(Legion::Settings)
84
+
85
+ identity = Legion::Settings[:identity]
86
+ entra = identity.is_a?(Hash) ? identity[:entra] : nil
87
+ return entra if entra.is_a?(Hash)
88
+
89
+ rbac = Legion::Settings[:rbac]
90
+ entra = rbac.is_a?(Hash) ? rbac[:entra] : nil
91
+ return entra if entra.is_a?(Hash)
92
+
93
+ {}
94
+ rescue StandardError
95
+ {}
96
+ end
97
+
98
+ class << self
99
+ private :register_worker_token_exchange
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -4,7 +4,7 @@ module Legion
4
4
  class API < Sinatra::Base
5
5
  module Middleware
6
6
  class Auth
7
- SKIP_PATHS = %w[/api/health /api/ready /api/openapi.json /metrics].freeze
7
+ SKIP_PATHS = %w[/api/health /api/ready /api/openapi.json /metrics /api/auth/token /api/auth/worker-token].freeze
8
8
  AUTH_HEADER = 'HTTP_AUTHORIZATION'
9
9
  BEARER_PATTERN = /\ABearer\s+(.+)\z/i
10
10
  API_KEY_HEADER = 'HTTP_X_API_KEY'
data/lib/legion/api.rb CHANGED
@@ -27,6 +27,7 @@ require_relative 'api/oauth'
27
27
  require_relative 'api/openapi'
28
28
  require_relative 'api/rbac'
29
29
  require_relative 'api/auth'
30
+ require_relative 'api/auth_worker'
30
31
  require_relative 'api/audit'
31
32
  require_relative 'api/metrics'
32
33
 
@@ -98,6 +99,7 @@ module Legion
98
99
  register Routes::OAuth
99
100
  register Routes::Rbac
100
101
  register Routes::Auth
102
+ register Routes::AuthWorker
101
103
  register Routes::Audit
102
104
  register Routes::Metrics
103
105
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Legion
4
- VERSION = '1.4.43'
4
+ VERSION = '1.4.44'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: legionio
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.43
4
+ version: 1.4.44
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -318,6 +318,7 @@ files:
318
318
  - lib/legion/api.rb
319
319
  - lib/legion/api/audit.rb
320
320
  - lib/legion/api/auth.rb
321
+ - lib/legion/api/auth_worker.rb
321
322
  - lib/legion/api/chains.rb
322
323
  - lib/legion/api/coldstart.rb
323
324
  - lib/legion/api/events.rb