lex-identity 0.2.0 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +27 -0
- data/Gemfile +6 -3
- data/LICENSE +21 -0
- data/README.md +230 -0
- data/lex-identity.gemspec +15 -12
- data/lib/legion/extensions/identity/helpers/browser.rb +44 -0
- data/lib/legion/extensions/identity/helpers/callback_server.rb +170 -0
- data/lib/legion/extensions/identity/helpers/oauth.rb +62 -0
- data/lib/legion/extensions/identity/helpers/pkce.rb +20 -0
- data/lib/legion/extensions/identity/helpers/provider.rb +52 -0
- data/lib/legion/extensions/identity/runners/base/oauth.rb +140 -0
- data/lib/legion/extensions/identity/version.rb +1 -1
- data/lib/legion/extensions/identity.rb +8 -14
- data/spec/support/provider_contract.rb +49 -0
- metadata +67 -33
- data/lib/legion/extensions/identity/actors/orphan_check.rb +0 -48
- data/lib/legion/extensions/identity/client.rb +0 -23
- data/lib/legion/extensions/identity/helpers/dimensions.rb +0 -71
- data/lib/legion/extensions/identity/helpers/fingerprint.rb +0 -166
- data/lib/legion/extensions/identity/helpers/vault_secrets.rb +0 -76
- data/lib/legion/extensions/identity/local_migrations/20260316000030_create_fingerprint.rb +0 -20
- data/lib/legion/extensions/identity/runners/entra.rb +0 -223
- data/lib/legion/extensions/identity/runners/identity.rb +0 -86
- data/spec/legion/extensions/identity/actors/orphan_check_spec.rb +0 -104
- data/spec/legion/extensions/identity/client_spec.rb +0 -32
- data/spec/legion/extensions/identity/helpers/dimensions_spec.rb +0 -51
- data/spec/legion/extensions/identity/helpers/fingerprint_spec.rb +0 -66
- data/spec/legion/extensions/identity/runners/entra_spec.rb +0 -405
- data/spec/legion/extensions/identity/runners/identity_spec.rb +0 -61
- data/spec/local_persistence_spec.rb +0 -329
- data/spec/spec_helper.rb +0 -33
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Identity
|
|
6
|
+
module Helpers
|
|
7
|
+
module Provider
|
|
8
|
+
def identity_register(**)
|
|
9
|
+
if defined?(Legion::Identity::Resolver) && Legion::Identity::Resolver.respond_to?(:register)
|
|
10
|
+
Legion::Identity::Resolver.register(self)
|
|
11
|
+
:registered
|
|
12
|
+
elsif defined?(Legion::Identity) &&
|
|
13
|
+
Legion::Identity.respond_to?(:pending_registrations) &&
|
|
14
|
+
Legion::Identity.pending_registrations.respond_to?(:<<)
|
|
15
|
+
Legion::Identity.pending_registrations << self
|
|
16
|
+
:pending
|
|
17
|
+
else
|
|
18
|
+
:unavailable
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def identity_build_lease(credential:, **attributes)
|
|
23
|
+
attributes = attributes.merge(provider: provider_name, credential: credential)
|
|
24
|
+
return attributes unless defined?(Legion::Identity::Lease)
|
|
25
|
+
|
|
26
|
+
Legion::Identity::Lease.new(**attributes)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def identity_register_credential(lease:, qualifier: :default, default: false, **)
|
|
30
|
+
unless defined?(Legion::Identity::Broker) && Legion::Identity::Broker.respond_to?(:register_provider)
|
|
31
|
+
raise 'Credential registration requires Legion::Identity::Broker'
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
Legion::Identity::Broker.register_provider(provider_name, provider: self, lease: lease, qualifier: qualifier, default: default)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def identity_credentials_available(**)
|
|
38
|
+
return [] unless defined?(Legion::Identity::Broker)
|
|
39
|
+
|
|
40
|
+
Legion::Identity::Broker.credentials_available(provider_name)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def identity_token_for(qualifier: nil, **)
|
|
44
|
+
return nil unless defined?(Legion::Identity::Broker)
|
|
45
|
+
|
|
46
|
+
Legion::Identity::Broker.token_for(provider_name, qualifier: qualifier, **)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/identity/helpers/oauth'
|
|
4
|
+
require 'legion/extensions/identity/helpers/browser'
|
|
5
|
+
require 'legion/extensions/identity/helpers/callback_server'
|
|
6
|
+
require 'legion/extensions/identity/helpers/pkce'
|
|
7
|
+
require 'securerandom'
|
|
8
|
+
require 'legion/logging'
|
|
9
|
+
|
|
10
|
+
module Legion
|
|
11
|
+
module Extensions
|
|
12
|
+
module Identity
|
|
13
|
+
module Runners
|
|
14
|
+
module Oauth
|
|
15
|
+
extend self
|
|
16
|
+
extend Legion::Extensions::Identity::Helpers::Oauth
|
|
17
|
+
extend Legion::Extensions::Identity::Helpers::Browser
|
|
18
|
+
extend Legion::Extensions::Identity::Helpers::Pkce
|
|
19
|
+
|
|
20
|
+
DEVICE_CODE_GRANT = 'urn:ietf:params:oauth:grant-type:device_code'
|
|
21
|
+
|
|
22
|
+
def browser_login(authorization_endpoint:, token_endpoint:, client_id:, scope:, callback_port: 0, callback_timeout: 120, **)
|
|
23
|
+
unless callback_timeout.is_a?(Numeric) && callback_timeout.real? && callback_timeout.finite? && callback_timeout.positive?
|
|
24
|
+
raise ArgumentError, 'callback_timeout must be positive and finite'
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
return { error: 'browser_unavailable', description: 'Interactive browser login requires a graphical session' } unless browser_gui_available?
|
|
28
|
+
|
|
29
|
+
begin
|
|
30
|
+
pkce = pkce_generate
|
|
31
|
+
state = SecureRandom.hex(32)
|
|
32
|
+
server = Legion::Extensions::Identity::Helpers::CallbackServer.new(port: callback_port)
|
|
33
|
+
redirect_uri = server.redirect_uri
|
|
34
|
+
url = oauth_authorize_url(authorization_endpoint: authorization_endpoint, client_id: client_id,
|
|
35
|
+
redirect_uri: redirect_uri, scope: scope, state: state,
|
|
36
|
+
code_challenge: pkce[:code_challenge], code_challenge_method: pkce[:code_challenge_method])
|
|
37
|
+
if browser_open(url: url)
|
|
38
|
+
callback = server.wait_for_callback(state: state, timeout: callback_timeout)
|
|
39
|
+
if callback[:error]
|
|
40
|
+
callback
|
|
41
|
+
else
|
|
42
|
+
exchange_code(token_endpoint: token_endpoint, client_id: client_id, code: callback[:code],
|
|
43
|
+
redirect_uri: redirect_uri, code_verifier: pkce[:code_verifier], scope: scope)
|
|
44
|
+
end
|
|
45
|
+
else
|
|
46
|
+
{ error: 'browser_launch_failed', description: 'Unable to launch the browser' }
|
|
47
|
+
end
|
|
48
|
+
rescue StandardError => e
|
|
49
|
+
oauth_runner_error(operation: 'identity.oauth.browser_login', exception: e)
|
|
50
|
+
ensure
|
|
51
|
+
server&.shutdown
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def request_device_code(device_authorization_endpoint:, client_id:, scope:, **)
|
|
56
|
+
result = oauth_post(endpoint: device_authorization_endpoint,
|
|
57
|
+
form: { client_id: client_id, scope: scope })
|
|
58
|
+
oauth_runner_result(result: result, required_key: :device_code)
|
|
59
|
+
rescue StandardError => e
|
|
60
|
+
oauth_runner_error(operation: 'identity.oauth.request_device_code', exception: e)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def exchange_code(token_endpoint:, client_id:, code:, redirect_uri:, code_verifier:, scope: nil, **)
|
|
64
|
+
form = {
|
|
65
|
+
grant_type: 'authorization_code', client_id: client_id, code: code,
|
|
66
|
+
redirect_uri: redirect_uri, code_verifier: code_verifier
|
|
67
|
+
}
|
|
68
|
+
form[:scope] = scope unless scope.nil?
|
|
69
|
+
result = oauth_post(endpoint: token_endpoint, form: form)
|
|
70
|
+
oauth_runner_result(result: result, required_key: :access_token)
|
|
71
|
+
rescue StandardError => e
|
|
72
|
+
oauth_runner_error(operation: 'identity.oauth.exchange_code', exception: e)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def refresh_token(token_endpoint:, client_id:, refresh_token:, scope: nil, **)
|
|
76
|
+
form = { grant_type: 'refresh_token', client_id: client_id, refresh_token: refresh_token }
|
|
77
|
+
form[:scope] = scope unless scope.nil?
|
|
78
|
+
result = oauth_post(endpoint: token_endpoint, form: form)
|
|
79
|
+
oauth_runner_result(result: result, required_key: :access_token)
|
|
80
|
+
rescue StandardError => e
|
|
81
|
+
oauth_runner_error(operation: 'identity.oauth.refresh_token', exception: e)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def poll_device_code(token_endpoint:, client_id:, device_code:, interval: 5, timeout: 300, **)
|
|
85
|
+
raise ArgumentError, 'interval must be positive' unless interval.positive?
|
|
86
|
+
raise ArgumentError, 'timeout must not be negative' if timeout.negative?
|
|
87
|
+
|
|
88
|
+
deadline = oauth_monotonic_now + timeout
|
|
89
|
+
current_interval = interval
|
|
90
|
+
outcome = { error: 'timeout', description: "Device code flow timed out after #{timeout}s" }
|
|
91
|
+
begin
|
|
92
|
+
while (remaining = deadline - oauth_monotonic_now).positive?
|
|
93
|
+
result = oauth_post(endpoint: token_endpoint,
|
|
94
|
+
form: { grant_type: DEVICE_CODE_GRANT, client_id: client_id, device_code: device_code },
|
|
95
|
+
open_timeout: [remaining, 5].min,
|
|
96
|
+
request_timeout: [remaining, 15].min)
|
|
97
|
+
if result[:error] == 'slow_down'
|
|
98
|
+
current_interval += 5
|
|
99
|
+
elsif result[:error] != 'authorization_pending'
|
|
100
|
+
outcome = oauth_runner_result(result: result, required_key: :access_token)
|
|
101
|
+
break
|
|
102
|
+
end
|
|
103
|
+
sleep_for = [current_interval, deadline - oauth_monotonic_now].min
|
|
104
|
+
break if sleep_for <= 0
|
|
105
|
+
|
|
106
|
+
sleep(sleep_for)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
outcome
|
|
110
|
+
rescue StandardError => e
|
|
111
|
+
oauth_runner_error(operation: 'identity.oauth.poll_device_code', exception: e)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
|
|
117
|
+
def oauth_runner_result(result:, required_key:)
|
|
118
|
+
return { error: result[:error].to_s, description: (result[:error_description] || result[:description]).to_s } if result[:error]
|
|
119
|
+
unless result[required_key].is_a?(String) && !result[required_key].empty?
|
|
120
|
+
return { error: 'invalid_response', description: "OAuth response did not include #{required_key}" }
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
{ result: result }
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def oauth_runner_error(operation:, exception:)
|
|
127
|
+
sanitized = StandardError.new("OAuth runner failed (#{exception.class})")
|
|
128
|
+
sanitized.set_backtrace(exception.backtrace)
|
|
129
|
+
handle_exception(sanitized, level: :error, operation: operation)
|
|
130
|
+
{ error: 'request_failed', description: exception.message }
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def oauth_monotonic_now
|
|
134
|
+
::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
@@ -1,24 +1,18 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'legion/logging'
|
|
3
4
|
require 'legion/extensions/identity/version'
|
|
4
|
-
require 'legion/extensions/identity/helpers/
|
|
5
|
-
require 'legion/extensions/identity/helpers/fingerprint'
|
|
6
|
-
require 'legion/extensions/identity/helpers/vault_secrets'
|
|
7
|
-
require 'legion/extensions/identity/runners/identity'
|
|
8
|
-
require 'legion/extensions/identity/runners/entra'
|
|
9
|
-
require 'legion/extensions/identity/actors/orphan_check'
|
|
5
|
+
require 'legion/extensions/identity/helpers/provider'
|
|
10
6
|
|
|
11
7
|
module Legion
|
|
12
8
|
module Extensions
|
|
13
9
|
module Identity
|
|
14
|
-
extend Legion::Extensions::Core if Legion::Extensions.const_defined?
|
|
10
|
+
extend Legion::Extensions::Core if Legion::Extensions.const_defined?(:Core, false)
|
|
11
|
+
|
|
12
|
+
def self.identity_provider? = false
|
|
13
|
+
def self.remote_invocable? = false
|
|
14
|
+
def self.transport_required? = false
|
|
15
|
+
def self.crypt_required? = false
|
|
15
16
|
end
|
|
16
17
|
end
|
|
17
18
|
end
|
|
18
|
-
|
|
19
|
-
if defined?(Legion::Data::Local)
|
|
20
|
-
Legion::Data::Local.register_migrations(
|
|
21
|
-
name: :identity,
|
|
22
|
-
path: File.join(__dir__, 'identity', 'local_migrations')
|
|
23
|
-
)
|
|
24
|
-
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.shared_examples 'identity provider contract' do
|
|
4
|
+
let(:canonical_name) { 'example-user' }
|
|
5
|
+
|
|
6
|
+
it 'exposes provider metadata with contract types' do
|
|
7
|
+
provider = identity_provider
|
|
8
|
+
|
|
9
|
+
expect(provider.provider_name).to be_a(Symbol)
|
|
10
|
+
expect(%i[auth profile fallback]).to include(provider.provider_type)
|
|
11
|
+
expect(provider.priority).to be_a(Numeric)
|
|
12
|
+
expect(provider.trust_weight).to be_a(Numeric)
|
|
13
|
+
expect(provider.capabilities).to be_an(Array)
|
|
14
|
+
expect(provider.capabilities).to all(be_a(Symbol))
|
|
15
|
+
|
|
16
|
+
trust_levels = if defined?(Legion::Identity::Trust) && Legion::Identity::Trust.respond_to?(:levels)
|
|
17
|
+
Legion::Identity::Trust.levels
|
|
18
|
+
else
|
|
19
|
+
%i[verified authenticated configured cached unverified]
|
|
20
|
+
end
|
|
21
|
+
expect(trust_levels).to include(provider.trust_level)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'uses the core resolution call contract' do
|
|
25
|
+
provider = identity_provider
|
|
26
|
+
result = case provider.provider_type
|
|
27
|
+
when :profile
|
|
28
|
+
if provider.respond_to?(:resolve_all)
|
|
29
|
+
provider.resolve_all(canonical_name: canonical_name)
|
|
30
|
+
else
|
|
31
|
+
provider.resolve(canonical_name: canonical_name)
|
|
32
|
+
end
|
|
33
|
+
when :auth, :fallback
|
|
34
|
+
provider.resolve
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
expect(result).to be_nil.or be_a(Hash)
|
|
38
|
+
next if result.nil?
|
|
39
|
+
|
|
40
|
+
if %i[auth fallback].include?(provider.provider_type)
|
|
41
|
+
expect(result).to include(:canonical_name, :kind, :source)
|
|
42
|
+
expect(result[:canonical_name]).to match(/\A[a-z0-9][a-z0-9_-]*\z/)
|
|
43
|
+
expect(result[:kind]).not_to be_nil
|
|
44
|
+
expect(result[:source]).not_to be_nil
|
|
45
|
+
else
|
|
46
|
+
expect(result).not_to have_key(:canonical_name)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
metadata
CHANGED
|
@@ -1,78 +1,112 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lex-identity
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
|
-
-
|
|
7
|
+
- LegionIO
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
|
-
name:
|
|
13
|
+
name: faraday
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '2.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '2.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: legion-cache
|
|
14
28
|
requirement: !ruby/object:Gem::Requirement
|
|
15
29
|
requirements:
|
|
16
30
|
- - ">="
|
|
17
31
|
- !ruby/object:Gem::Version
|
|
18
|
-
version:
|
|
19
|
-
type: :
|
|
32
|
+
version: 1.3.0
|
|
33
|
+
type: :runtime
|
|
20
34
|
prerelease: false
|
|
21
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
36
|
requirements:
|
|
23
37
|
- - ">="
|
|
24
38
|
- !ruby/object:Gem::Version
|
|
25
|
-
version:
|
|
39
|
+
version: 1.3.0
|
|
26
40
|
- !ruby/object:Gem::Dependency
|
|
27
|
-
name:
|
|
41
|
+
name: legion-json
|
|
28
42
|
requirement: !ruby/object:Gem::Requirement
|
|
29
43
|
requirements:
|
|
30
44
|
- - ">="
|
|
31
45
|
- !ruby/object:Gem::Version
|
|
32
|
-
version:
|
|
33
|
-
type: :
|
|
46
|
+
version: 1.2.1
|
|
47
|
+
type: :runtime
|
|
34
48
|
prerelease: false
|
|
35
49
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
50
|
requirements:
|
|
37
51
|
- - ">="
|
|
38
52
|
- !ruby/object:Gem::Version
|
|
39
|
-
version:
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
53
|
+
version: 1.2.1
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: legion-logging
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: 1.5.3
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: 1.5.3
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: legion-settings
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - ">="
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: 1.3.14
|
|
75
|
+
type: :runtime
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: 1.3.14
|
|
82
|
+
description: Base extension gem for shared behavior across LegionIO identity provider
|
|
83
|
+
extensions.
|
|
44
84
|
executables: []
|
|
45
85
|
extensions: []
|
|
46
86
|
extra_rdoc_files: []
|
|
47
87
|
files:
|
|
88
|
+
- CHANGELOG.md
|
|
48
89
|
- Gemfile
|
|
90
|
+
- LICENSE
|
|
91
|
+
- README.md
|
|
49
92
|
- lex-identity.gemspec
|
|
50
93
|
- lib/legion/extensions/identity.rb
|
|
51
|
-
- lib/legion/extensions/identity/
|
|
52
|
-
- lib/legion/extensions/identity/
|
|
53
|
-
- lib/legion/extensions/identity/helpers/
|
|
54
|
-
- lib/legion/extensions/identity/helpers/
|
|
55
|
-
- lib/legion/extensions/identity/helpers/
|
|
56
|
-
- lib/legion/extensions/identity/
|
|
57
|
-
- lib/legion/extensions/identity/runners/entra.rb
|
|
58
|
-
- lib/legion/extensions/identity/runners/identity.rb
|
|
94
|
+
- lib/legion/extensions/identity/helpers/browser.rb
|
|
95
|
+
- lib/legion/extensions/identity/helpers/callback_server.rb
|
|
96
|
+
- lib/legion/extensions/identity/helpers/oauth.rb
|
|
97
|
+
- lib/legion/extensions/identity/helpers/pkce.rb
|
|
98
|
+
- lib/legion/extensions/identity/helpers/provider.rb
|
|
99
|
+
- lib/legion/extensions/identity/runners/base/oauth.rb
|
|
59
100
|
- lib/legion/extensions/identity/version.rb
|
|
60
|
-
- spec/
|
|
61
|
-
- spec/legion/extensions/identity/client_spec.rb
|
|
62
|
-
- spec/legion/extensions/identity/helpers/dimensions_spec.rb
|
|
63
|
-
- spec/legion/extensions/identity/helpers/fingerprint_spec.rb
|
|
64
|
-
- spec/legion/extensions/identity/runners/entra_spec.rb
|
|
65
|
-
- spec/legion/extensions/identity/runners/identity_spec.rb
|
|
66
|
-
- spec/local_persistence_spec.rb
|
|
67
|
-
- spec/spec_helper.rb
|
|
101
|
+
- spec/support/provider_contract.rb
|
|
68
102
|
homepage: https://github.com/LegionIO/lex-identity
|
|
69
103
|
licenses:
|
|
70
104
|
- MIT
|
|
71
105
|
metadata:
|
|
72
106
|
homepage_uri: https://github.com/LegionIO/lex-identity
|
|
73
107
|
source_code_uri: https://github.com/LegionIO/lex-identity
|
|
74
|
-
documentation_uri: https://github.com/LegionIO/lex-identity
|
|
75
|
-
changelog_uri: https://github.com/LegionIO/lex-identity
|
|
108
|
+
documentation_uri: https://github.com/LegionIO/lex-identity/blob/main/README.md
|
|
109
|
+
changelog_uri: https://github.com/LegionIO/lex-identity/blob/main/CHANGELOG.md
|
|
76
110
|
bug_tracker_uri: https://github.com/LegionIO/lex-identity/issues
|
|
77
111
|
rubygems_mfa_required: 'true'
|
|
78
112
|
rdoc_options: []
|
|
@@ -91,5 +125,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
91
125
|
requirements: []
|
|
92
126
|
rubygems_version: 3.6.9
|
|
93
127
|
specification_version: 4
|
|
94
|
-
summary:
|
|
128
|
+
summary: Shared LegionIO identity extension base
|
|
95
129
|
test_files: []
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'legion/extensions/actors/every'
|
|
4
|
-
|
|
5
|
-
module Legion
|
|
6
|
-
module Extensions
|
|
7
|
-
module Identity
|
|
8
|
-
module Actor
|
|
9
|
-
# Periodic orphan detection: scans active workers for disabled Entra apps
|
|
10
|
-
# or inactive owners. Runs every 4 hours by default.
|
|
11
|
-
# Requires legion-data for worker records.
|
|
12
|
-
class OrphanCheck < Legion::Extensions::Actors::Every
|
|
13
|
-
ORPHAN_CHECK_INTERVAL = 14_400 # 4 hours in seconds
|
|
14
|
-
|
|
15
|
-
def runner_class
|
|
16
|
-
Legion::Extensions::Identity::Runners::Entra
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def runner_function
|
|
20
|
-
'check_orphans'
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def time
|
|
24
|
-
ORPHAN_CHECK_INTERVAL
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def enabled?
|
|
28
|
-
defined?(Legion::Data) && Legion::Settings[:data][:connected] != false
|
|
29
|
-
rescue StandardError
|
|
30
|
-
false
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def use_runner?
|
|
34
|
-
false
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def check_subtask?
|
|
38
|
-
false
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def generate_task?
|
|
42
|
-
false
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
end
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'legion/extensions/identity/helpers/dimensions'
|
|
4
|
-
require 'legion/extensions/identity/helpers/fingerprint'
|
|
5
|
-
require 'legion/extensions/identity/runners/identity'
|
|
6
|
-
|
|
7
|
-
module Legion
|
|
8
|
-
module Extensions
|
|
9
|
-
module Identity
|
|
10
|
-
class Client
|
|
11
|
-
include Runners::Identity
|
|
12
|
-
|
|
13
|
-
def initialize(**)
|
|
14
|
-
@identity_fingerprint = Helpers::Fingerprint.new
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
private
|
|
18
|
-
|
|
19
|
-
attr_reader :identity_fingerprint
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
end
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Legion
|
|
4
|
-
module Extensions
|
|
5
|
-
module Identity
|
|
6
|
-
module Helpers
|
|
7
|
-
module Dimensions
|
|
8
|
-
# The 6 behavioral dimensions that constitute identity
|
|
9
|
-
IDENTITY_DIMENSIONS = %i[
|
|
10
|
-
communication_cadence
|
|
11
|
-
vocabulary_patterns
|
|
12
|
-
emotional_response
|
|
13
|
-
decision_patterns
|
|
14
|
-
contextual_consistency
|
|
15
|
-
temporal_patterns
|
|
16
|
-
].freeze
|
|
17
|
-
|
|
18
|
-
# Entropy thresholds (from tick-loop-spec Phase 4)
|
|
19
|
-
HIGH_ENTROPY_THRESHOLD = 0.70
|
|
20
|
-
LOW_ENTROPY_THRESHOLD = 0.20
|
|
21
|
-
OPTIMAL_ENTROPY_RANGE = (0.20..0.70)
|
|
22
|
-
|
|
23
|
-
# EMA alpha for dimension updates
|
|
24
|
-
OBSERVATION_ALPHA = 0.1
|
|
25
|
-
|
|
26
|
-
module_function
|
|
27
|
-
|
|
28
|
-
def new_identity_model
|
|
29
|
-
IDENTITY_DIMENSIONS.to_h do |dim|
|
|
30
|
-
[dim, { mean: 0.5, variance: 0.1, observations: 0, last_observed: nil }]
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def compute_entropy(observations, model)
|
|
35
|
-
return 0.5 if observations.empty?
|
|
36
|
-
|
|
37
|
-
divergences = IDENTITY_DIMENSIONS.filter_map do |dim|
|
|
38
|
-
obs = observations[dim]
|
|
39
|
-
next unless obs
|
|
40
|
-
|
|
41
|
-
baseline = model[dim]
|
|
42
|
-
next 0.0 unless baseline && baseline[:observations].positive?
|
|
43
|
-
|
|
44
|
-
# Weighted divergence from established baseline
|
|
45
|
-
(obs - baseline[:mean]).abs / [baseline[:variance].to_f, 0.1].max
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
return 0.5 if divergences.empty?
|
|
49
|
-
|
|
50
|
-
raw = divergences.sum / divergences.size
|
|
51
|
-
clamp(raw / 3.0) # normalize: divergence of 3.0 stddevs = entropy 1.0
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def classify_entropy(entropy)
|
|
55
|
-
if entropy > HIGH_ENTROPY_THRESHOLD
|
|
56
|
-
:high_entropy
|
|
57
|
-
elsif entropy < LOW_ENTROPY_THRESHOLD
|
|
58
|
-
:low_entropy
|
|
59
|
-
else
|
|
60
|
-
:normal
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
def clamp(value, min = 0.0, max = 1.0)
|
|
65
|
-
value.clamp(min, max)
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
end
|