lex-identity-approle 0.1.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 630ee2dc79404f13449fd09d3bfd5e4a8608b7f453cae79c4077e75e247f6c55
4
+ data.tar.gz: 2b99766aece2ca3e516576664b11bc6066cac56ef0cdec2a0a6adfafe7e47b10
5
+ SHA512:
6
+ metadata.gz: 58e2dc76f64b7ad4871614edbc8991ba803dcc383d616e2e818d967bbc95b8ec605cda92256594e86ad538c3432d1b0e8a5df8e31ece433eeb206190215a0ad7
7
+ data.tar.gz: 318724056bc425c0ed4d5225af91c5ebee2622b7f8c396f30f2c0ce6c6dac1a49a927ba638ea16c2aea988864bc8750e7f868fcd04eef66a992686673efa11ea
@@ -0,0 +1,7 @@
1
+ # Auto-generated from team-config.yml
2
+ # Team: extensions
3
+ #
4
+ # To apply: scripts/apply-codeowners.sh lex-identity-approle
5
+
6
+ * @LegionIO/maintainers
7
+ * @LegionIO/extensions
@@ -0,0 +1,18 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: bundler
4
+ directory: /
5
+ schedule:
6
+ interval: weekly
7
+ day: monday
8
+ open-pull-requests-limit: 5
9
+ labels:
10
+ - "type:dependencies"
11
+ - package-ecosystem: github-actions
12
+ directory: /
13
+ schedule:
14
+ interval: weekly
15
+ day: monday
16
+ open-pull-requests-limit: 5
17
+ labels:
18
+ - "type:dependencies"
@@ -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,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ .rspec_status
10
+ Gemfile.lock
data/.rubocop.yml ADDED
@@ -0,0 +1,2 @@
1
+ inherit_gem:
2
+ rubocop-legion: config/lex.yml
data/CHANGELOG.md ADDED
@@ -0,0 +1,19 @@
1
+ # Changelog
2
+
3
+ ## [0.1.0] - 2026-04-07
4
+
5
+ ### Added
6
+
7
+ - Initial release
8
+ - `Identity` module implementing the LegionIO identity provider contract
9
+ - `provider_type: :auth`, `facing: :machine`, `priority: 100`, `trust_weight: 100`
10
+ - `capabilities: [:authenticate, :vault_auth]`
11
+ - `resolve` calls `provide_token` internally (single login, prevents double secret_id consumption)
12
+ - `provide_token` performs Vault AppRole login and returns a `Legion::Identity::Lease`
13
+ - Cached lease (`@cached_lease`) — prevents re-use of single-use secret_ids
14
+ - Expired-lease detection via `Lease#valid?`
15
+ - `role_id` and `secret_id` sourced from settings, ENV, or file mounts (K8s secret paths)
16
+ - Uses `Legion::Crypt::LeaseManager.instance.logical` for namespace-aware Vault routing when available
17
+ - Top-level delegation methods (`provider_name`, `provider_type`, `facing`) for extension registration
18
+ - `normalize` replaces non-alphanumeric chars (except `_` and `-`) with underscores
19
+ - Full spec coverage: provider contract, resolve, provide_token, settings, file-based credentials
data/CLAUDE.md ADDED
@@ -0,0 +1,99 @@
1
+ # lex-identity-approle: Vault AppRole Identity Provider for LegionIO
2
+
3
+ **Repository Level 3 Documentation**
4
+ - **Parent (Level 2)**: `/Users/miverso2/rubymine/legion/extensions/CLAUDE.md`
5
+ - **Parent (Level 1)**: `/Users/miverso2/rubymine/legion/CLAUDE.md`
6
+
7
+ ## Purpose
8
+
9
+ Vault AppRole machine identity provider. Authenticates to Vault at boot via AppRole, caches the lease, and exposes the identity to the LegionIO framework. Greenfield gem — no existing AppRole code in the codebase.
10
+
11
+ **GitHub**: https://github.com/LegionIO/lex-identity-approle
12
+ **License**: MIT
13
+ **Version**: 0.1.0
14
+
15
+ ## Architecture
16
+
17
+ ```
18
+ Legion::Extensions::Identity::Approle
19
+ ├── identity_provider? = true
20
+ ├── remote_invocable? = false
21
+ ├── provider_name (delegates to Identity)
22
+ ├── provider_type (delegates to Identity)
23
+ ├── facing (delegates to Identity)
24
+ └── Identity # provider contract: resolve, provide_token, normalize, ...
25
+ Settings # ENV + file-based credential loading
26
+ ```
27
+
28
+ No runners, no actors, no transport helpers.
29
+
30
+ ## File Map
31
+
32
+ | File | Purpose |
33
+ |------|---------|
34
+ | `lib/legion/extensions/identity/approle.rb` | Entry point: declares `identity_provider?`, `remote_invocable?`, delegation methods |
35
+ | `lib/legion/extensions/identity/approle/identity.rb` | Provider contract: `resolve`, `provide_token`, `normalize`, private helpers |
36
+ | `lib/legion/extensions/identity/approle/settings.rb` | Settings with ENV + Legion::Settings fallback |
37
+ | `lib/legion/extensions/identity/approle/version.rb` | `VERSION = '0.1.0'` |
38
+
39
+ ## Provider Contract
40
+
41
+ | Method | Return |
42
+ |--------|--------|
43
+ | `provider_name` | `:approle` |
44
+ | `provider_type` | `:auth` |
45
+ | `facing` | `:machine` |
46
+ | `priority` | `100` |
47
+ | `trust_weight` | `100` |
48
+ | `capabilities` | `%i[authenticate vault_auth]` |
49
+ | `resolve` | identity hash or `nil` |
50
+ | `provide_token` | `Legion::Identity::Lease` or `nil` |
51
+ | `normalize(val)` | String |
52
+
53
+ ## Key Rules
54
+
55
+ - `resolve` calls `provide_token` internally — single login per boot cycle
56
+ - `@cached_lease` prevents double-consumption of single-use secret_ids
57
+ - `private_class_method` with explicit method names: `build_lease`, `vault_approle_login`, `role_id`, `secret_id`, `read_file`, `settings`
58
+ - Uses `Legion::Crypt::LeaseManager.instance.logical` when available; falls back to `::Vault.logical`
59
+ - `normalize` replaces non-alphanumeric chars (except `_` and `-`) with underscores
60
+
61
+ ## Settings
62
+
63
+ ```json
64
+ {
65
+ "identity": {
66
+ "approle": {
67
+ "role_id": null,
68
+ "secret_id": null,
69
+ "role_id_file": "/run/secrets/vault-role-id",
70
+ "secret_id_file": "/run/secrets/vault-secret-id",
71
+ "auth_path": "approle"
72
+ }
73
+ }
74
+ }
75
+ ```
76
+
77
+ ENV fallbacks: `VAULT_APPROLE_ROLE_ID`, `VAULT_APPROLE_SECRET_ID`.
78
+
79
+ ## Dependencies
80
+
81
+ | Gem | Purpose |
82
+ |-----|---------|
83
+ | `legion-json` | JSON serialization (framework compat) |
84
+ | `legion-settings` | Configuration management (framework compat) |
85
+
86
+ Runtime optional (not declared in gemspec):
87
+ - `vault` gem — used via `::Vault.logical` or `Legion::Crypt::LeaseManager`
88
+
89
+ ## Testing
90
+
91
+ ```bash
92
+ bundle install
93
+ bundle exec rspec # full suite
94
+ bundle exec rubocop # clean
95
+ ```
96
+
97
+ ---
98
+
99
+ **Maintained By**: Matthew Iverson (@Esity)
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,62 @@
1
+ # lex-identity-approle
2
+
3
+ Vault AppRole identity provider for LegionIO. Machine identity for headless services.
4
+
5
+ ## Overview
6
+
7
+ `lex-identity-approle` is a Phase 6 identity provider that authenticates to HashiCorp Vault using the AppRole auth method. It provides machine identity for Nomad jobs, K8s pods, and any headless service that needs to bind a Vault token at boot.
8
+
9
+ **Provider contract:**
10
+
11
+ | Attribute | Value |
12
+ |-----------|-------|
13
+ | `provider_name` | `:approle` |
14
+ | `provider_type` | `:auth` |
15
+ | `facing` | `:machine` |
16
+ | `priority` | `100` (highest — tried first among machine auth providers) |
17
+ | `trust_weight` | `100` |
18
+ | `capabilities` | `[:authenticate, :vault_auth]` |
19
+
20
+ ## Credential Sources
21
+
22
+ `role_id` and `secret_id` are resolved in this order:
23
+
24
+ 1. `Legion::Settings[:identity][:approle][:role_id]` / `[:secret_id]`
25
+ 2. `ENV['VAULT_APPROLE_ROLE_ID']` / `ENV['VAULT_APPROLE_SECRET_ID']`
26
+ 3. File mounts: `/run/secrets/vault-role-id` / `/run/secrets/vault-secret-id` (K8s default)
27
+
28
+ Override file paths via settings:
29
+
30
+ ```json
31
+ {
32
+ "identity": {
33
+ "approle": {
34
+ "role_id_file": "/custom/path/role-id",
35
+ "secret_id_file": "/custom/path/secret-id",
36
+ "auth_path": "approle"
37
+ }
38
+ }
39
+ }
40
+ ```
41
+
42
+ ## Single-Login Design
43
+
44
+ `resolve` calls `provide_token` internally. `provide_token` caches the returned Lease in `@cached_lease` and returns it on subsequent calls as long as it remains valid. This prevents double-consumption of single-use `secret_id` values (the Vault AppRole default is `secret_id_num_uses = 1`).
45
+
46
+ To use unlimited secret_ids, configure Vault with `secret_id_num_uses = 0`.
47
+
48
+ ## Vault Routing
49
+
50
+ Uses `Legion::Crypt::LeaseManager.instance.logical` when available for namespace-aware routing. Falls back to `::Vault.logical` when legion-crypt is not loaded.
51
+
52
+ ## Installation
53
+
54
+ Add to your `Gemfile`:
55
+
56
+ ```ruby
57
+ gem 'lex-identity-approle'
58
+ ```
59
+
60
+ ## License
61
+
62
+ MIT
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/legion/extensions/identity/approle/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'lex-identity-approle'
7
+ spec.version = Legion::Extensions::Identity::Approle::VERSION
8
+ spec.authors = ['Esity']
9
+ spec.email = ['matthewdiverson@gmail.com']
10
+
11
+ spec.summary = 'LEX Identity AppRole'
12
+ spec.description = 'LegionIO Vault AppRole identity provider — machine identity for headless services authenticating to Vault'
13
+ spec.homepage = 'https://github.com/LegionIO/lex-identity-approle'
14
+ spec.license = 'MIT'
15
+ spec.required_ruby_version = '>= 3.4'
16
+
17
+ spec.metadata['homepage_uri'] = spec.homepage
18
+ spec.metadata['source_code_uri'] = 'https://github.com/LegionIO/lex-identity-approle'
19
+ spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-identity-approle'
20
+ spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-identity-approle'
21
+ spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-identity-approle/issues'
22
+ spec.metadata['rubygems_mfa_required'] = 'true'
23
+
24
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
25
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
26
+ end
27
+ spec.require_paths = ['lib']
28
+
29
+ spec.add_dependency 'legion-json', '>= 1.2.1'
30
+ spec.add_dependency 'legion-settings', '>= 1.3.14'
31
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Identity
6
+ module Approle
7
+ module Identity
8
+ module_function
9
+
10
+ def provider_name
11
+ :approle
12
+ end
13
+
14
+ def provider_type
15
+ :auth
16
+ end
17
+
18
+ def facing
19
+ :machine
20
+ end
21
+
22
+ def priority
23
+ 100
24
+ end
25
+
26
+ def trust_weight
27
+ 100
28
+ end
29
+
30
+ def capabilities
31
+ %i[authenticate vault_auth]
32
+ end
33
+
34
+ def resolve(canonical_name: nil) # rubocop:disable Lint/UnusedMethodArgument
35
+ return nil unless role_id && secret_id
36
+
37
+ lease = provide_token
38
+ return nil unless lease
39
+
40
+ meta = lease.metadata || {}
41
+ {
42
+ canonical_name: normalize(meta[:role_name] || "approle-#{role_id[0..7]}"),
43
+ kind: :machine,
44
+ source: :approle,
45
+ persistent: true,
46
+ groups: meta[:policies] || [],
47
+ metadata: { role_name: meta[:role_name], policies: meta[:policies] }
48
+ }
49
+ end
50
+
51
+ def provide_token
52
+ return @cached_lease if @cached_lease&.valid? # rubocop:disable ThreadSafety/ClassInstanceVariable
53
+ return nil unless role_id && secret_id
54
+
55
+ response = vault_approle_login(role_id: role_id, secret_id: secret_id)
56
+ return nil unless response
57
+
58
+ @cached_lease = build_lease(response) # rubocop:disable ThreadSafety/ClassInstanceVariable
59
+ end
60
+
61
+ def normalize(val)
62
+ val.to_s.downcase.strip.gsub(/[^a-z0-9_-]/, '_')
63
+ end
64
+
65
+ def build_lease(response)
66
+ Legion::Identity::Lease.new(
67
+ provider: :approle,
68
+ credential: response.auth.client_token,
69
+ lease_id: response.auth.lease_id,
70
+ expires_at: Time.now + response.auth.lease_duration,
71
+ renewable: response.auth.renewable,
72
+ issued_at: Time.now,
73
+ metadata: { policies: response.auth.policies }
74
+ )
75
+ end
76
+
77
+ def vault_approle_login(role_id:, secret_id:)
78
+ logical = if defined?(Legion::Crypt::LeaseManager)
79
+ Legion::Crypt::LeaseManager.instance.logical
80
+ else
81
+ ::Vault.logical
82
+ end
83
+ logical.write(
84
+ "auth/#{settings[:auth_path] || 'approle'}/login",
85
+ role_id: role_id,
86
+ secret_id: secret_id
87
+ )
88
+ rescue ::Vault::HTTPError => e
89
+ Legion::Logging.warn("AppRole login failed: #{e.message}") if defined?(Legion::Logging) # rubocop:disable Legion/HelperMigration/DirectLogging, Legion/HelperMigration/LoggingGuard
90
+ nil
91
+ end
92
+
93
+ def role_id
94
+ settings[:role_id] || read_file(settings[:role_id_file])
95
+ end
96
+
97
+ def secret_id
98
+ settings[:secret_id] || read_file(settings[:secret_id_file])
99
+ end
100
+
101
+ def read_file(path)
102
+ return nil unless path && File.exist?(path)
103
+
104
+ File.read(path).strip
105
+ end
106
+
107
+ def settings
108
+ Approle::Settings.settings
109
+ end
110
+
111
+ private_class_method :build_lease, :vault_approle_login, :role_id, :secret_id, :read_file, :settings
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Identity
6
+ module Approle
7
+ module Settings
8
+ DEFAULTS = {
9
+ role_id: ENV.fetch('VAULT_APPROLE_ROLE_ID', nil),
10
+ secret_id: ENV.fetch('VAULT_APPROLE_SECRET_ID', nil),
11
+ role_id_file: '/run/secrets/vault-role-id',
12
+ secret_id_file: '/run/secrets/vault-secret-id',
13
+ auth_path: 'approle'
14
+ }.freeze
15
+
16
+ def self.settings
17
+ return Legion::Settings[:identity][:approle] if defined?(Legion::Settings) &&
18
+ Legion::Settings[:identity].is_a?(Hash) &&
19
+ Legion::Settings[:identity][:approle].is_a?(Hash)
20
+
21
+ DEFAULTS
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Identity
6
+ module Approle
7
+ VERSION = '0.1.0'
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/identity/approle/version'
4
+ require 'legion/extensions/identity/approle/settings'
5
+ require 'legion/extensions/identity/approle/identity'
6
+
7
+ module Legion
8
+ module Extensions
9
+ module Identity
10
+ module Approle
11
+ extend Legion::Extensions::Core if Legion::Extensions.const_defined?(:Core, false)
12
+
13
+ def self.identity_provider?
14
+ true
15
+ end
16
+
17
+ def self.remote_invocable?
18
+ false
19
+ end
20
+
21
+ def self.provider_name
22
+ Identity.provider_name
23
+ end
24
+
25
+ def self.provider_type
26
+ Identity.provider_type
27
+ end
28
+
29
+ def self.facing
30
+ Identity.facing
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
metadata ADDED
@@ -0,0 +1,89 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lex-identity-approle
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Esity
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: legion-json
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: 1.2.1
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: 1.2.1
26
+ - !ruby/object:Gem::Dependency
27
+ name: legion-settings
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 1.3.14
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: 1.3.14
40
+ description: LegionIO Vault AppRole identity provider — machine identity for headless
41
+ services authenticating to Vault
42
+ email:
43
+ - matthewdiverson@gmail.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - ".github/CODEOWNERS"
49
+ - ".github/dependabot.yml"
50
+ - ".github/workflows/ci.yml"
51
+ - ".gitignore"
52
+ - ".rubocop.yml"
53
+ - CHANGELOG.md
54
+ - CLAUDE.md
55
+ - Gemfile
56
+ - README.md
57
+ - lex-identity-approle.gemspec
58
+ - lib/legion/extensions/identity/approle.rb
59
+ - lib/legion/extensions/identity/approle/identity.rb
60
+ - lib/legion/extensions/identity/approle/settings.rb
61
+ - lib/legion/extensions/identity/approle/version.rb
62
+ homepage: https://github.com/LegionIO/lex-identity-approle
63
+ licenses:
64
+ - MIT
65
+ metadata:
66
+ homepage_uri: https://github.com/LegionIO/lex-identity-approle
67
+ source_code_uri: https://github.com/LegionIO/lex-identity-approle
68
+ documentation_uri: https://github.com/LegionIO/lex-identity-approle
69
+ changelog_uri: https://github.com/LegionIO/lex-identity-approle
70
+ bug_tracker_uri: https://github.com/LegionIO/lex-identity-approle/issues
71
+ rubygems_mfa_required: 'true'
72
+ rdoc_options: []
73
+ require_paths:
74
+ - lib
75
+ required_ruby_version: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: '3.4'
80
+ required_rubygems_version: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ version: '0'
85
+ requirements: []
86
+ rubygems_version: 3.6.9
87
+ specification_version: 4
88
+ summary: LEX Identity AppRole
89
+ test_files: []