lex-identity 0.3.0 → 0.4.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/Gemfile +4 -6
- data/lex-identity.gemspec +12 -15
- data/lib/legion/extensions/identity/actors/credential_refresh.rb +45 -0
- data/lib/legion/extensions/identity/actors/orphan_check.rb +48 -0
- data/lib/legion/extensions/identity/client.rb +23 -0
- data/lib/legion/extensions/identity/helpers/dimensions.rb +71 -0
- data/lib/legion/extensions/identity/helpers/fingerprint.rb +166 -0
- data/lib/legion/extensions/identity/helpers/graph_client.rb +25 -0
- data/lib/legion/extensions/identity/helpers/graph_token.rb +32 -0
- data/lib/legion/extensions/identity/helpers/token_cache.rb +55 -0
- data/lib/legion/extensions/identity/helpers/vault_secrets.rb +76 -0
- data/lib/legion/extensions/identity/local_migrations/20260316000030_create_fingerprint.rb +20 -0
- data/lib/legion/extensions/identity/runners/entra.rb +398 -0
- data/lib/legion/extensions/identity/runners/identity.rb +86 -0
- data/lib/legion/extensions/identity/version.rb +1 -1
- data/lib/legion/extensions/identity.rb +14 -8
- data/spec/legion/extensions/identity/actors/orphan_check_spec.rb +104 -0
- data/spec/legion/extensions/identity/client_spec.rb +32 -0
- data/spec/legion/extensions/identity/helpers/dimensions_spec.rb +51 -0
- data/spec/legion/extensions/identity/helpers/fingerprint_spec.rb +66 -0
- data/spec/legion/extensions/identity/helpers/graph_client_spec.rb +19 -0
- data/spec/legion/extensions/identity/helpers/graph_token_spec.rb +31 -0
- data/spec/legion/extensions/identity/helpers/token_cache_spec.rb +50 -0
- data/spec/legion/extensions/identity/runners/entra_spec.rb +655 -0
- data/spec/legion/extensions/identity/runners/identity_spec.rb +61 -0
- data/spec/local_persistence_spec.rb +329 -0
- data/spec/spec_helper.rb +33 -0
- metadata +40 -67
- data/CHANGELOG.md +0 -27
- data/LICENSE +0 -21
- data/README.md +0 -230
- data/lib/legion/extensions/identity/helpers/browser.rb +0 -44
- data/lib/legion/extensions/identity/helpers/callback_server.rb +0 -170
- data/lib/legion/extensions/identity/helpers/oauth.rb +0 -62
- data/lib/legion/extensions/identity/helpers/pkce.rb +0 -20
- data/lib/legion/extensions/identity/helpers/provider.rb +0 -52
- data/lib/legion/extensions/identity/runners/base/oauth.rb +0 -140
- data/spec/support/provider_contract.rb +0 -49
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::Identity::Helpers::Dimensions do
|
|
4
|
+
describe '.new_identity_model' do
|
|
5
|
+
it 'creates a model with 6 dimensions' do
|
|
6
|
+
model = described_class.new_identity_model
|
|
7
|
+
expect(model.size).to eq(6)
|
|
8
|
+
described_class::IDENTITY_DIMENSIONS.each do |dim|
|
|
9
|
+
expect(model[dim][:mean]).to eq(0.5)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
describe '.compute_entropy' do
|
|
15
|
+
it 'returns 0.5 for empty observations' do
|
|
16
|
+
model = described_class.new_identity_model
|
|
17
|
+
expect(described_class.compute_entropy({}, model)).to eq(0.5)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it 'returns low entropy for observations matching baseline' do
|
|
21
|
+
model = described_class.new_identity_model
|
|
22
|
+
model[:communication_cadence][:observations] = 50
|
|
23
|
+
obs = { communication_cadence: 0.5 }
|
|
24
|
+
entropy = described_class.compute_entropy(obs, model)
|
|
25
|
+
expect(entropy).to be < 0.3
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'returns high entropy for observations diverging from baseline' do
|
|
29
|
+
model = described_class.new_identity_model
|
|
30
|
+
model[:communication_cadence][:observations] = 50
|
|
31
|
+
model[:communication_cadence][:variance] = 0.1
|
|
32
|
+
obs = { communication_cadence: 1.0 }
|
|
33
|
+
entropy = described_class.compute_entropy(obs, model)
|
|
34
|
+
expect(entropy).to be > 0.3
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
describe '.classify_entropy' do
|
|
39
|
+
it 'classifies high entropy' do
|
|
40
|
+
expect(described_class.classify_entropy(0.8)).to eq(:high_entropy)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it 'classifies low entropy' do
|
|
44
|
+
expect(described_class.classify_entropy(0.1)).to eq(:low_entropy)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it 'classifies normal entropy' do
|
|
48
|
+
expect(described_class.classify_entropy(0.5)).to eq(:normal)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::Identity::Helpers::Fingerprint do
|
|
4
|
+
let(:fp) { described_class.new }
|
|
5
|
+
|
|
6
|
+
describe '#observe' do
|
|
7
|
+
it 'records observation for valid dimension' do
|
|
8
|
+
fp.observe(:communication_cadence, 0.7)
|
|
9
|
+
expect(fp.observation_count).to eq(1)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it 'ignores invalid dimensions' do
|
|
13
|
+
fp.observe(:nonexistent, 0.5)
|
|
14
|
+
expect(fp.observation_count).to eq(0)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it 'shifts mean toward observed values' do
|
|
18
|
+
original = fp.model[:vocabulary_patterns][:mean]
|
|
19
|
+
10.times { fp.observe(:vocabulary_patterns, 0.9) }
|
|
20
|
+
expect(fp.model[:vocabulary_patterns][:mean]).to be > original
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
describe '#observe_all' do
|
|
25
|
+
it 'records multiple dimensions at once' do
|
|
26
|
+
fp.observe_all(communication_cadence: 0.6, vocabulary_patterns: 0.7)
|
|
27
|
+
expect(fp.observation_count).to eq(2)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
describe '#current_entropy' do
|
|
32
|
+
it 'computes entropy against model' do
|
|
33
|
+
entropy = fp.current_entropy(communication_cadence: 0.5)
|
|
34
|
+
expect(entropy).to be_between(0.0, 1.0)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it 'tracks entropy history' do
|
|
38
|
+
3.times { fp.current_entropy(communication_cadence: 0.5) }
|
|
39
|
+
expect(fp.entropy_history.size).to eq(3)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
describe '#entropy_trend' do
|
|
44
|
+
it 'returns stable for insufficient data' do
|
|
45
|
+
expect(fp.entropy_trend).to eq(:stable)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it 'detects rising entropy' do
|
|
49
|
+
5.times { |i| fp.current_entropy(communication_cadence: 0.5 + (i * 0.1)) }
|
|
50
|
+
# Trend depends on actual computed values
|
|
51
|
+
trend = fp.entropy_trend(window: 5)
|
|
52
|
+
expect(%i[rising stable falling]).to include(trend)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
describe '#maturity' do
|
|
57
|
+
it 'starts as nascent' do
|
|
58
|
+
expect(fp.maturity).to eq(:nascent)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it 'progresses to developing' do
|
|
62
|
+
15.times { fp.observe(:communication_cadence, 0.5) }
|
|
63
|
+
expect(fp.maturity).to eq(:developing)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'legion/extensions/identity/helpers/graph_client'
|
|
5
|
+
|
|
6
|
+
RSpec.describe Legion::Extensions::Identity::Helpers::GraphClient do
|
|
7
|
+
describe '.connection' do
|
|
8
|
+
it 'returns a Faraday connection with bearer token' do
|
|
9
|
+
conn = described_class.connection(token: 'test-token')
|
|
10
|
+
expect(conn).to be_a(Faraday::Connection)
|
|
11
|
+
expect(conn.headers['Authorization']).to eq('Bearer test-token')
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it 'uses custom base URL when provided' do
|
|
15
|
+
conn = described_class.connection(token: 'tok', base: 'https://custom.api.com')
|
|
16
|
+
expect(conn.url_prefix.to_s).to eq('https://custom.api.com/')
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'legion/extensions/identity/helpers/graph_token'
|
|
5
|
+
|
|
6
|
+
RSpec.describe Legion::Extensions::Identity::Helpers::GraphToken do
|
|
7
|
+
describe '.fetch' do
|
|
8
|
+
it 'raises GraphTokenError on failure' do
|
|
9
|
+
stub_request = instance_double(Faraday::Response, success?: false,
|
|
10
|
+
body: { 'error_description' => 'invalid_client' })
|
|
11
|
+
conn = instance_double(Faraday::Connection)
|
|
12
|
+
allow(conn).to receive(:post).and_return(stub_request)
|
|
13
|
+
allow(Faraday).to receive(:new).and_return(conn)
|
|
14
|
+
|
|
15
|
+
expect do
|
|
16
|
+
described_class.fetch(tenant_id: 't1', client_id: 'c1', client_secret: 's1')
|
|
17
|
+
end.to raise_error(described_class::GraphTokenError, 'invalid_client')
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it 'returns access_token on success' do
|
|
21
|
+
stub_request = instance_double(Faraday::Response, success?: true,
|
|
22
|
+
body: { 'access_token' => 'tok-123' })
|
|
23
|
+
conn = instance_double(Faraday::Connection)
|
|
24
|
+
allow(conn).to receive(:post).and_return(stub_request)
|
|
25
|
+
allow(Faraday).to receive(:new).and_return(conn)
|
|
26
|
+
|
|
27
|
+
token = described_class.fetch(tenant_id: 't1', client_id: 'c1', client_secret: 's1')
|
|
28
|
+
expect(token).to eq('tok-123')
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'legion/extensions/identity/helpers/token_cache'
|
|
5
|
+
|
|
6
|
+
RSpec.describe Legion::Extensions::Identity::Helpers::TokenCache do
|
|
7
|
+
before { described_class.clear_all }
|
|
8
|
+
after { described_class.clear_all }
|
|
9
|
+
|
|
10
|
+
describe '.store and .fetch' do
|
|
11
|
+
it 'stores and retrieves a token' do
|
|
12
|
+
described_class.store(worker_id: 'w1', token: 'abc', expires_in: 3600)
|
|
13
|
+
entry = described_class.fetch(worker_id: 'w1')
|
|
14
|
+
expect(entry[:access_token]).to eq('abc')
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it 'returns nil for unknown worker' do
|
|
18
|
+
expect(described_class.fetch(worker_id: 'unknown')).to be_nil
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it 'returns nil for expired token' do
|
|
22
|
+
described_class.store(worker_id: 'w1', token: 'abc', expires_in: -1)
|
|
23
|
+
expect(described_class.fetch(worker_id: 'w1')).to be_nil
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
describe '.approaching_expiry?' do
|
|
28
|
+
it 'returns true when no token exists' do
|
|
29
|
+
expect(described_class.approaching_expiry?(worker_id: 'w1')).to be true
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it 'returns true when token is within buffer' do
|
|
33
|
+
described_class.store(worker_id: 'w1', token: 'abc', expires_in: 100)
|
|
34
|
+
expect(described_class.approaching_expiry?(worker_id: 'w1', buffer: 200)).to be true
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it 'returns false when token has plenty of time' do
|
|
38
|
+
described_class.store(worker_id: 'w1', token: 'abc', expires_in: 3600)
|
|
39
|
+
expect(described_class.approaching_expiry?(worker_id: 'w1', buffer: 300)).to be false
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
describe '.clear' do
|
|
44
|
+
it 'removes a specific worker token' do
|
|
45
|
+
described_class.store(worker_id: 'w1', token: 'abc', expires_in: 3600)
|
|
46
|
+
described_class.clear(worker_id: 'w1')
|
|
47
|
+
expect(described_class.fetch(worker_id: 'w1')).to be_nil
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|