lex-identity-entra 0.3.0

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.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +34 -0
  3. data/.gitignore +11 -0
  4. data/.rubocop.yml +20 -0
  5. data/CHANGELOG.md +39 -0
  6. data/Gemfile +11 -0
  7. data/README.md +120 -0
  8. data/lex-identity-entra.gemspec +37 -0
  9. data/lib/legion/extensions/identity/entra/application/actors/token_refresher.rb +71 -0
  10. data/lib/legion/extensions/identity/entra/application/client.rb +45 -0
  11. data/lib/legion/extensions/identity/entra/application/runners/credential.rb +81 -0
  12. data/lib/legion/extensions/identity/entra/application/scope_registry.rb +15 -0
  13. data/lib/legion/extensions/identity/entra/application/scopes.rb +47 -0
  14. data/lib/legion/extensions/identity/entra/application.rb +46 -0
  15. data/lib/legion/extensions/identity/entra/client.rb +181 -0
  16. data/lib/legion/extensions/identity/entra/delegated/actors/auth_validator.rb +149 -0
  17. data/lib/legion/extensions/identity/entra/delegated/actors/token_refresher.rb +91 -0
  18. data/lib/legion/extensions/identity/entra/delegated/cli/auth.rb +82 -0
  19. data/lib/legion/extensions/identity/entra/delegated/client.rb +46 -0
  20. data/lib/legion/extensions/identity/entra/delegated/hooks/auth.rb +23 -0
  21. data/lib/legion/extensions/identity/entra/delegated/identity.rb +116 -0
  22. data/lib/legion/extensions/identity/entra/delegated/runners/login.rb +185 -0
  23. data/lib/legion/extensions/identity/entra/delegated/runners/on_behalf_of.rb +64 -0
  24. data/lib/legion/extensions/identity/entra/delegated/scope_registry.rb +15 -0
  25. data/lib/legion/extensions/identity/entra/delegated/scopes.rb +100 -0
  26. data/lib/legion/extensions/identity/entra/delegated.rb +54 -0
  27. data/lib/legion/extensions/identity/entra/helpers/account_discovery.rb +85 -0
  28. data/lib/legion/extensions/identity/entra/helpers/browser_auth.rb +212 -0
  29. data/lib/legion/extensions/identity/entra/helpers/callback_server.rb +106 -0
  30. data/lib/legion/extensions/identity/entra/helpers/graph_client.rb +63 -0
  31. data/lib/legion/extensions/identity/entra/helpers/scope_gate.rb +96 -0
  32. data/lib/legion/extensions/identity/entra/helpers/scope_registry.rb +72 -0
  33. data/lib/legion/extensions/identity/entra/helpers/scopes.rb +61 -0
  34. data/lib/legion/extensions/identity/entra/helpers/token_manager.rb +362 -0
  35. data/lib/legion/extensions/identity/entra/managed_identity/actors/token_refresher.rb +64 -0
  36. data/lib/legion/extensions/identity/entra/managed_identity/client.rb +34 -0
  37. data/lib/legion/extensions/identity/entra/managed_identity/runners/token.rb +61 -0
  38. data/lib/legion/extensions/identity/entra/managed_identity/scope_registry.rb +15 -0
  39. data/lib/legion/extensions/identity/entra/managed_identity/scopes.rb +25 -0
  40. data/lib/legion/extensions/identity/entra/managed_identity.rb +42 -0
  41. data/lib/legion/extensions/identity/entra/version.rb +11 -0
  42. data/lib/legion/extensions/identity/entra/workload_identity/actors/token_refresher.rb +64 -0
  43. data/lib/legion/extensions/identity/entra/workload_identity/client.rb +34 -0
  44. data/lib/legion/extensions/identity/entra/workload_identity/runners/token.rb +99 -0
  45. data/lib/legion/extensions/identity/entra/workload_identity/scope_registry.rb +15 -0
  46. data/lib/legion/extensions/identity/entra/workload_identity/scopes.rb +25 -0
  47. data/lib/legion/extensions/identity/entra/workload_identity.rb +43 -0
  48. data/lib/legion/extensions/identity/entra.rb +51 -0
  49. metadata +178 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ec4cedd630cdcaaf6941ad0fe0c7c9b56b0ab0311f442406b5e32992ea8d15df
4
+ data.tar.gz: a4024c49a2819dfa4a7b12a3243a39bedc93dd8eecf458acffee62f69b4a6909
5
+ SHA512:
6
+ metadata.gz: 7307e9b1ecb97fcbdcdc77672b8a7054fc851eb3289c2050a89e93336057fbdcf0cf160c4df2c33b84b7dc65d6465e036acdb5ec837c688e9d96feffee395996
7
+ data.tar.gz: eb935f1d0ce6e0dac4bf81001100b5a97025e894e884d2d8da4aa031974a9853d8d65f5af09417199967c3cf349a4150ff9ee6118940f028734ef81b6c1330dc
@@ -0,0 +1,34 @@
1
+ name: CI
2
+ on:
3
+ push:
4
+ branches: [main]
5
+ pull_request:
6
+ schedule:
7
+ - cron: '0 9 * * 1'
8
+
9
+ jobs:
10
+ ci:
11
+ uses: LegionIO/.github/.github/workflows/ci.yml@main
12
+
13
+ excluded-files:
14
+ uses: LegionIO/.github/.github/workflows/excluded-files.yml@main
15
+
16
+ security:
17
+ uses: LegionIO/.github/.github/workflows/security-scan.yml@main
18
+
19
+ version-changelog:
20
+ uses: LegionIO/.github/.github/workflows/version-changelog.yml@main
21
+
22
+ dependency-review:
23
+ uses: LegionIO/.github/.github/workflows/dependency-review.yml@main
24
+
25
+ stale:
26
+ if: github.event_name == 'schedule'
27
+ uses: LegionIO/.github/.github/workflows/stale.yml@main
28
+
29
+ release:
30
+ needs: [ci, excluded-files]
31
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main'
32
+ uses: LegionIO/.github/.github/workflows/release.yml@main
33
+ secrets:
34
+ rubygems-api-key: ${{ secrets.RUBYGEMS_API_KEY }}
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ .rspec_status
10
+ Gemfile.lock
11
+ *.gem
data/.rubocop.yml ADDED
@@ -0,0 +1,20 @@
1
+ inherit_gem:
2
+ rubocop-legion: config/lex.yml
3
+
4
+ Style/ModuleFunction:
5
+ Exclude:
6
+ - 'lib/legion/extensions/identity/entra/helpers/token_manager.rb'
7
+ - 'lib/legion/extensions/identity/entra/helpers/graph_client.rb'
8
+ - 'lib/legion/extensions/identity/entra/helpers/account_discovery.rb'
9
+
10
+ Legion/Extension/EveryActorRequiresTime:
11
+ Enabled: false
12
+
13
+ ThreadSafety/ClassInstanceVariable:
14
+ Exclude:
15
+ - 'lib/legion/extensions/identity/helpers/'
16
+ - 'lib/legion/extensions/identity/entra/helpers/'
17
+ - 'lib/legion/extensions/identity/entra/helpers/'
18
+ - 'lib/legion/extensions/identity/entra/client.rb'
19
+ - 'lib/legion/extensions/identity/entra/helpers/scope_gate.rb'
20
+
data/CHANGELOG.md ADDED
@@ -0,0 +1,39 @@
1
+ # Changelog
2
+
3
+ ## [Unreleased]
4
+
5
+ ## [0.3.0] - 2026-05-14
6
+
7
+ ### Added
8
+ - Delegated token persistence to HashiCorp Vault at `kv/data/users/<identity>/entra/delegated/auth`; disk file used only as fallback when Vault is unavailable.
9
+ - Automatic backfill: existing disk tokens are migrated to Vault on next load and the disk file is deleted once Vault write succeeds.
10
+ - `delete_local` cleans up the on-disk token file whenever Vault becomes authoritative.
11
+ - `AuthValidator` calls `Legion::Identity::Broker.register_provider` after successful auth so any extension can call `Legion::Identity::Broker.token_for(:entra_delegated, qualifier: :delegated)`.
12
+ - `AuthValidator` calls `Legion::Identity::Resolver.upgrade!` after auth to promote the Entra-verified identity into `Legion::Identity::Process` state.
13
+ - MD5 scope fingerprint stored with token across all backends; mismatch or missing fingerprint forces re-authentication.
14
+ - In-memory token store as tertiary fallback (vault → disk → memory).
15
+ - Delegated scopes expanded to full org-granted permission set: Teams, Chat, Channel, OnlineMeetings, OneNote, SharePoint, Yammer, Files, Presence, and core OpenID/profile scopes.
16
+ - OAuth callback page auto-closes after 10-second JavaScript countdown.
17
+
18
+ ### Changed
19
+ - Vault read skipped at boot until process identity is trusted (prevents 403 during resolver race).
20
+ - Vault read/write now use `Legion::Crypt.get`/`Legion::Crypt.write` via the `kv` mount directly, matching the policy path.
21
+ - `AuthValidator` delay reduced from 90s to 9s.
22
+
23
+ ## [0.2.0] - 2026-05-07
24
+
25
+ ### Added
26
+ - Browser OAuth with PKCE, local callback handling, and device-code fallback for delegated Entra auth.
27
+ - Microsoft identity OAuth runner for client credentials, authorization code, device code, and refresh-token grants.
28
+ - CLI auth entrypoint and manifest metadata for `legion lex exec entra auth login/status`.
29
+ - Refresh-aware token persistence with Vault, local file, and Broker fallback lookup.
30
+ - Multi-account Entra discovery for delegated, secondary, and privileged account token qualifiers.
31
+
32
+ ### Changed
33
+ - `resolve_all` now uses discovered Entra token qualifiers instead of wrapping only the default delegated account.
34
+ - `provide_token` includes qualifier and scope metadata from the persisted token record.
35
+
36
+ ## [0.1.0] - 2026-04-24
37
+
38
+ ### Added
39
+ - Initial Entra identity provider scaffold with cached-token Graph `/me` resolution.
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+ gemspec
5
+
6
+ group :development, :test do
7
+ gem 'rspec', '~> 3.13'
8
+ gem 'rubocop', '~> 1.75'
9
+ gem 'rubocop-legion', '~> 0.1'
10
+ gem 'simplecov', '~> 0.22'
11
+ end
data/README.md ADDED
@@ -0,0 +1,120 @@
1
+ # lex-identity-entra
2
+
3
+ LegionIO identity provider extension for Microsoft Entra ID (Azure AD). Implements the unified
4
+ identity provider contract for delegated (user), application (client credentials), managed identity,
5
+ and workload identity authentication patterns.
6
+
7
+ ## Features
8
+
9
+ - **Delegated auth** — OAuth2 PKCE browser flow with local callback server; device-code fallback
10
+ - **Token persistence** — Vault-first (`kv/data/users/<identity>/entra/<qualifier>/auth`), disk fallback, in-memory fallback; disk file deleted once Vault write succeeds
11
+ - **Scope fingerprinting** — MD5 fingerprint of active scopes stored with token; any scope change forces re-authentication on next boot
12
+ - **Identity integration** — `AuthValidator` upgrades `Legion::Identity::Process` via `Resolver.upgrade!` and registers with `Legion::Identity::Broker` for cross-extension token access
13
+ - **Application credentials** — client credentials flow for service-to-service auth
14
+ - **Managed Identity** — Azure IMDS token acquisition for hosted workloads
15
+ - **Workload Identity** — federated credential support for Kubernetes and CI/CD
16
+
17
+ ## Installation
18
+
19
+ Add to your `Gemfile`:
20
+
21
+ ```ruby
22
+ gem 'lex-identity-entra'
23
+ ```
24
+
25
+ ## Configuration
26
+
27
+ ```yaml
28
+ # ~/.legionio/settings/identity.yml
29
+ identity:
30
+ entra:
31
+ auth:
32
+ tenant_id: "your-tenant-id"
33
+ client_id: "your-client-id"
34
+ delegated:
35
+ browser_auth:
36
+ auto_authenticate: false # set true to open browser automatically at boot
37
+ callback_timeout: 120
38
+ token:
39
+ refresh_buffer: 60
40
+ refresh_interval: 900
41
+ scopes:
42
+ enabled_categories:
43
+ - microsoft_graph
44
+ - teams
45
+ - one_note
46
+ - sharepoint
47
+ - azure_communication_services
48
+ - yammer
49
+ ```
50
+
51
+ ## Usage
52
+
53
+ ### Delegated (user) authentication
54
+
55
+ ```bash
56
+ # Via CLI
57
+ legion lex exec entra auth login
58
+ legion lex exec entra auth status
59
+ ```
60
+
61
+ The `AuthValidator` actor fires 9 seconds after boot. If `auto_authenticate: true` is set,
62
+ it opens a browser window automatically. Otherwise trigger login via the CLI.
63
+
64
+ ### Accessing tokens from another extension
65
+
66
+ ```ruby
67
+ token = Legion::Identity::Broker.token_for(:entra_delegated, qualifier: :delegated)
68
+ ```
69
+
70
+ ### Application (client credentials)
71
+
72
+ ```yaml
73
+ identity:
74
+ entra:
75
+ application:
76
+ tenant_id: "your-tenant-id"
77
+ client_id: "your-client-id"
78
+ client_secret: "your-client-secret" # or use Vault
79
+ ```
80
+
81
+ ## Scope categories
82
+
83
+ | Category | Description |
84
+ |----------|-------------|
85
+ | `microsoft_graph` | Core Graph API: User.Read, Files, Devices, OpenID scopes |
86
+ | `teams` | Teams, Chat, Channel, OnlineMeetings, Presence, Activity |
87
+ | `one_note` | OneNote notebook read/write |
88
+ | `sharepoint` | SharePoint/OneDrive files and sites |
89
+ | `azure_communication_services` | Teams calls and chat management |
90
+ | `yammer` | Viva Engage / Yammer communities and conversations |
91
+
92
+ ## Token storage
93
+
94
+ | Backend | Path | Priority |
95
+ |---------|------|----------|
96
+ | HashiCorp Vault | `kv/data/users/<identity>/entra/delegated/auth` | 1 (preferred) |
97
+ | Local disk | `~/.legionio/tokens/entra_delegated.json` | 2 (fallback, deleted when Vault succeeds) |
98
+ | Memory | In-process store | 3 (runtime fallback) |
99
+
100
+ ## Identity provider contract
101
+
102
+ ```ruby
103
+ {
104
+ canonical_name: "jdoe", # normalized from onPremisesSamAccountName or mailNickname
105
+ kind: :human,
106
+ source: :entra_delegated,
107
+ provider_identity: "object-id-guid",
108
+ profile: { ... } # full Graph /me response
109
+ }
110
+ ```
111
+
112
+ ## Development
113
+
114
+ ```bash
115
+ bundle install
116
+ bundle exec rspec
117
+ bundle exec rubocop
118
+ ```
119
+
120
+ **GitHub**: https://github.com/LegionIO/lex-identity-entra
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/legion/extensions/identity/entra/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'lex-identity-entra'
7
+ spec.version = Legion::Extensions::Identity::Entra::VERSION
8
+ spec.authors = ['Esity']
9
+ spec.email = ['matthewdiverson@gmail.com']
10
+
11
+ spec.summary = 'LEX Identity: Entra ID (Azure AD) provider'
12
+ spec.description = 'LegionIO identity provider that resolves Entra ID (Azure AD) identity ' \
13
+ 'via Microsoft Graph API into the unified identity contract'
14
+ spec.homepage = 'https://github.com/LegionIO/lex-identity-entra'
15
+ spec.license = 'MIT'
16
+ spec.required_ruby_version = '>= 3.4'
17
+
18
+ spec.metadata['homepage_uri'] = spec.homepage
19
+ spec.metadata['source_code_uri'] = 'https://github.com/LegionIO/lex-identity-entra'
20
+ spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-identity-entra'
21
+ spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-identity-entra'
22
+ spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-identity-entra/issues'
23
+ spec.metadata['rubygems_mfa_required'] = 'true'
24
+
25
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
26
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
27
+ end
28
+ spec.require_paths = ['lib']
29
+
30
+ # Core framework dependencies
31
+ spec.add_dependency 'concurrent-ruby', '>= 1.2'
32
+ spec.add_dependency 'faraday', '>= 2.0'
33
+ spec.add_dependency 'legion-crypt', '>= 1.5.13'
34
+ spec.add_dependency 'legion-json', '>= 1.2.1'
35
+ spec.add_dependency 'legion-logging', '>= 1.5.3'
36
+ spec.add_dependency 'legion-settings', '>= 1.3.14'
37
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Identity
6
+ module Entra
7
+ module Application
8
+ module Actor
9
+ class TokenRefresher < Legion::Extensions::Actors::Every
10
+ DEFAULT_REFRESH_INTERVAL = 2700
11
+
12
+ def runner_class = self.class
13
+ def runner_function = 'manual'
14
+ def use_runner? = false
15
+ def check_subtask? = false
16
+ def generate_task? = false
17
+ def run_now? = false
18
+
19
+ def time
20
+ Legion::Settings.dig(:identity, :entra, :application, :token, :refresh_interval) ||
21
+ DEFAULT_REFRESH_INTERVAL
22
+ end
23
+
24
+ def enabled? # rubocop:disable Legion/Extension/ActorEnabledSideEffects
25
+ true
26
+ end
27
+
28
+ def manual
29
+ log.debug('Application TokenRefresher tick')
30
+ data = Legion::Extensions::Identity::Entra::Helpers::TokenManager.token_data(:application, refresh: false)
31
+
32
+ if data && !Legion::Extensions::Identity::Entra::Helpers::TokenManager.expired?(data)
33
+ log.debug('Application token still valid')
34
+ return
35
+ end
36
+
37
+ log.info('Application token nearing expiry, re-acquiring')
38
+ auth_settings = Legion::Extensions::Identity::Entra::Helpers::TokenManager.settings_auth
39
+ runner = Object.new.extend(Legion::Extensions::Identity::Entra::Application::Runners::Credential)
40
+ result = runner.acquire_token(
41
+ tenant_id: auth_settings[:tenant_id],
42
+ client_id: auth_settings[:client_id],
43
+ client_secret: auth_settings[:client_secret]
44
+ )
45
+
46
+ body = result&.dig(:result)
47
+ unless body&.dig(:access_token)
48
+ log.warn('Application token re-acquisition failed')
49
+ return
50
+ end
51
+
52
+ Legion::Extensions::Identity::Entra::Helpers::TokenManager.save_token(
53
+ :application,
54
+ access_token: body[:access_token],
55
+ expires_in: body[:expires_in],
56
+ scopes: body[:scope] || 'https://graph.microsoft.com/.default',
57
+ tenant_id: auth_settings[:tenant_id],
58
+ client_id: auth_settings[:client_id]
59
+ )
60
+ Legion::Extensions::Identity::Entra::Client.reset!(pattern: :application)
61
+ log.info('Application token refreshed successfully')
62
+ rescue StandardError => e
63
+ log.error("Application TokenRefresher: #{e.message}")
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Identity
6
+ module Entra
7
+ module Application
8
+ class Client < Legion::Extensions::Identity::Entra::Client
9
+ def pattern = :application
10
+
11
+ private
12
+
13
+ def authenticate
14
+ settings = auth_settings
15
+ return unless settings[:tenant_id] && settings[:client_id] && settings[:client_secret]
16
+
17
+ requested = Legion::Extensions::Identity::Entra::Helpers::Scopes.resolve(pattern: :application)
18
+ registry.record_requested(requested)
19
+
20
+ runner = Object.new.extend(Legion::Extensions::Identity::Entra::Application::Runners::Credential)
21
+ result = runner.acquire_token(
22
+ tenant_id: settings[:tenant_id],
23
+ client_id: settings[:client_id],
24
+ client_secret: settings[:client_secret]
25
+ )
26
+ body = result&.dig(:result)
27
+ return unless body&.dig(:access_token)
28
+
29
+ granted = body[:scope] || 'https://graph.microsoft.com/.default'
30
+ registry.record_granted(granted)
31
+ Legion::Extensions::Identity::Entra::Helpers::TokenManager.save_token(
32
+ :application,
33
+ access_token: body[:access_token],
34
+ expires_in: body[:expires_in],
35
+ scopes: granted,
36
+ tenant_id: settings[:tenant_id],
37
+ client_id: settings[:client_id]
38
+ )
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'faraday'
4
+ require 'legion/extensions/identity/entra/helpers/scopes'
5
+
6
+ module Legion
7
+ module Extensions
8
+ module Identity
9
+ module Entra
10
+ module Application
11
+ module Runners
12
+ module Credential
13
+ include Legion::Logging::Helper
14
+ include Legion::Settings::Helper
15
+
16
+ DEFAULT_SCOPE = 'https://graph.microsoft.com/.default'
17
+
18
+ def acquire_token(tenant_id:, client_id:, client_secret:,
19
+ scope: DEFAULT_SCOPE, **)
20
+ log.debug("Credential.acquire_token: tenant=#{tenant_id}")
21
+ result = credential_post(tenant_id,
22
+ grant_type: 'client_credentials',
23
+ client_id: client_id,
24
+ client_secret: client_secret,
25
+ scope: scope)
26
+ log.info('Credential.acquire_token: token acquired') if result[:access_token]
27
+ { result: result }
28
+ rescue StandardError => e
29
+ handle_exception(e, level: :error, operation: 'application.credential.acquire_token')
30
+ { error: 'request_failed', description: e.message }
31
+ end
32
+
33
+ def acquire_token_with_certificate(tenant_id:, client_id:, client_assertion:,
34
+ scope: DEFAULT_SCOPE, **)
35
+ log.debug("Credential.acquire_token_with_certificate: tenant=#{tenant_id}")
36
+ result = credential_post(tenant_id,
37
+ grant_type: 'client_credentials',
38
+ client_id: client_id,
39
+ client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
40
+ client_assertion: client_assertion,
41
+ scope: scope)
42
+ log.info('Credential.acquire_token_with_certificate: token acquired') if result[:access_token]
43
+ { result: result }
44
+ rescue StandardError => e
45
+ handle_exception(e, level: :error, operation: 'application.credential.acquire_token_with_certificate')
46
+ { error: 'request_failed', description: e.message }
47
+ end
48
+
49
+ def credential_post(tenant_id, form)
50
+ log.debug("Credential.credential_post: tenant=#{tenant_id}")
51
+ response = credential_connection(tenant_id).post('oauth2/v2.0/token',
52
+ URI.encode_www_form(form.transform_keys(&:to_s)))
53
+ body = response.body.to_s.empty? ? {} : json_load(response.body)
54
+ unless response.success?
55
+ body[:error] ||= "http_#{response.status}"
56
+ body[:error_description] ||= response.reason_phrase
57
+ log.debug("Credential.credential_post: error=#{body[:error]} status=#{response.status}")
58
+ end
59
+ body
60
+ end
61
+
62
+ private
63
+
64
+ def credential_connection(tenant_id)
65
+ Faraday.new(url: "https://login.microsoftonline.com/#{tenant_id}/") do |f|
66
+ f.headers['Accept'] = 'application/json'
67
+ f.headers['Content-Type'] = 'application/x-www-form-urlencoded'
68
+ f.options.open_timeout = 5
69
+ f.options.timeout = 15
70
+ end
71
+ end
72
+
73
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
74
+ Legion::Extensions::Helpers.const_defined?(:Lex, false)
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/identity/entra/helpers/scope_registry'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module Identity
8
+ module Entra
9
+ module Application
10
+ ScopeRegistry = Legion::Extensions::Identity::Entra::Helpers::ScopeRegistry.new(pattern: :application)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Identity
6
+ module Entra
7
+ module Application
8
+ module Scopes
9
+ CATEGORIES = {
10
+ microsoft_graph: %w[
11
+ User.Read.All
12
+ Application.Read.All
13
+ Directory.Read.All
14
+ Group.Read.All
15
+ Mail.Read
16
+ Mail.ReadBasic.All
17
+ Mail.Send
18
+ Calendars.Read
19
+ Sites.Read.All
20
+ Files.Read.All
21
+ TeamMember.Read.All
22
+ Channel.ReadBasic.All
23
+ ChannelMessage.Read.All
24
+ Chat.Read.All
25
+ ChatMessage.Read.All
26
+ OnlineMeetings.Read.All
27
+ OnlineMeetingTranscript.Read.All
28
+ OnlineMeetingRecording.Read.All
29
+ CallRecords.Read.All
30
+ ],
31
+ one_note: %w[
32
+ Notes.Read.All
33
+ Notes.ReadWrite.All
34
+ ],
35
+ sharepoint: %w[
36
+ Sites.Read.All
37
+ Sites.ReadWrite.All
38
+ Files.Read.All
39
+ Files.ReadWrite.All
40
+ ]
41
+ }.freeze
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'application/scopes'
4
+ require_relative 'application/scope_registry'
5
+ require_relative 'application/runners/credential'
6
+ require_relative 'application/actors/token_refresher'
7
+ require_relative 'application/client'
8
+
9
+ module Legion
10
+ module Extensions
11
+ module Identity
12
+ module Entra
13
+ module Application
14
+ extend Legion::Extensions::Core if Legion::Extensions.const_defined?(:Core, false)
15
+
16
+ def self.identity_provider? = false
17
+ def self.remote_invocable? = false
18
+
19
+ def self.default_settings
20
+ {
21
+ logger: { level: 'info' },
22
+ workers: 1,
23
+ runners: {},
24
+ auth: {
25
+ tenant_id: nil,
26
+ client_id: nil,
27
+ client_secret: nil,
28
+ certificate: nil
29
+ },
30
+ scopes: {
31
+ enabled_categories: [:microsoft_graph],
32
+ category_overrides: {}
33
+ },
34
+ token: {
35
+ vault_path: nil,
36
+ local_token_path: nil,
37
+ refresh_buffer: 60,
38
+ refresh_interval: 2700
39
+ }
40
+ }
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end