lex-microsoft_teams 0.6.45 → 0.6.47
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/.gitignore +1 -0
- data/CHANGELOG.md +2 -0
- data/CLAUDE.md +29 -266
- data/lex-microsoft_teams.gemspec +1 -0
- data/lib/legion/extensions/microsoft_teams/absorbers/channel.rb +29 -17
- data/lib/legion/extensions/microsoft_teams/absorbers/chat.rb +20 -14
- data/lib/legion/extensions/microsoft_teams/absorbers/meeting.rb +21 -14
- data/lib/legion/extensions/microsoft_teams/actors/absorb_channel.rb +7 -4
- data/lib/legion/extensions/microsoft_teams/actors/absorb_chat.rb +7 -4
- data/lib/legion/extensions/microsoft_teams/actors/absorb_meeting.rb +7 -4
- data/lib/legion/extensions/microsoft_teams/actors/api_ingest.rb +13 -15
- data/lib/legion/extensions/microsoft_teams/actors/cache_bulk_ingest.rb +3 -3
- data/lib/legion/extensions/microsoft_teams/actors/cache_sync.rb +2 -1
- data/lib/legion/extensions/microsoft_teams/actors/channel_poller.rb +25 -16
- data/lib/legion/extensions/microsoft_teams/actors/direct_chat_poller.rb +16 -10
- data/lib/legion/extensions/microsoft_teams/actors/incremental_sync.rb +8 -8
- data/lib/legion/extensions/microsoft_teams/actors/meeting_ingest.rb +30 -22
- data/lib/legion/extensions/microsoft_teams/actors/observed_chat_poller.rb +14 -8
- data/lib/legion/extensions/microsoft_teams/actors/presence_poller.rb +14 -8
- data/lib/legion/extensions/microsoft_teams/actors/profile_ingest.rb +13 -16
- data/lib/legion/extensions/microsoft_teams/helpers/client.rb +10 -4
- data/lib/legion/extensions/microsoft_teams/helpers/high_water_mark.rb +3 -2
- data/lib/legion/extensions/microsoft_teams/helpers/prompt_resolver.rb +4 -1
- data/lib/legion/extensions/microsoft_teams/helpers/session_manager.rb +8 -2
- data/lib/legion/extensions/microsoft_teams/helpers/subscription_registry.rb +5 -3
- data/lib/legion/extensions/microsoft_teams/helpers/trace_retriever.rb +6 -1
- data/lib/legion/extensions/microsoft_teams/local_cache/extractor.rb +1 -1
- data/lib/legion/extensions/microsoft_teams/local_cache/sstable_reader.rb +2 -1
- data/lib/legion/extensions/microsoft_teams/runners/activities.rb +43 -0
- data/lib/legion/extensions/microsoft_teams/runners/ai_insights.rb +62 -0
- data/lib/legion/extensions/microsoft_teams/runners/api_ingest.rb +20 -14
- data/lib/legion/extensions/microsoft_teams/runners/app_installations.rb +86 -0
- data/lib/legion/extensions/microsoft_teams/runners/auth.rb +4 -107
- data/lib/legion/extensions/microsoft_teams/runners/bot.rb +20 -12
- data/lib/legion/extensions/microsoft_teams/runners/cache_ingest.rb +9 -5
- data/lib/legion/extensions/microsoft_teams/runners/call_events.rb +72 -0
- data/lib/legion/extensions/microsoft_teams/runners/channel_messages.rb +85 -0
- data/lib/legion/extensions/microsoft_teams/runners/channels.rb +69 -0
- data/lib/legion/extensions/microsoft_teams/runners/chats.rb +57 -0
- data/lib/legion/extensions/microsoft_teams/runners/files.rb +77 -0
- data/lib/legion/extensions/microsoft_teams/runners/local_cache.rb +4 -0
- data/lib/legion/extensions/microsoft_teams/runners/loop.rb +6 -0
- data/lib/legion/extensions/microsoft_teams/runners/meeting_artifacts.rb +54 -0
- data/lib/legion/extensions/microsoft_teams/runners/meetings.rb +92 -0
- data/lib/legion/extensions/microsoft_teams/runners/messages.rb +62 -0
- data/lib/legion/extensions/microsoft_teams/runners/ownership.rb +11 -0
- data/lib/legion/extensions/microsoft_teams/runners/people.rb +25 -0
- data/lib/legion/extensions/microsoft_teams/runners/presence.rb +14 -0
- data/lib/legion/extensions/microsoft_teams/runners/profile_ingest.rb +18 -4
- data/lib/legion/extensions/microsoft_teams/runners/subscriptions.rb +86 -0
- data/lib/legion/extensions/microsoft_teams/runners/teams.rb +30 -0
- data/lib/legion/extensions/microsoft_teams/runners/transcripts.rb +35 -0
- data/lib/legion/extensions/microsoft_teams/version.rb +1 -1
- data/lib/legion/extensions/microsoft_teams.rb +10 -3
- metadata +20 -8
- data/lib/legion/extensions/microsoft_teams/actors/auth_validator.rb +0 -123
- data/lib/legion/extensions/microsoft_teams/actors/token_refresher.rb +0 -122
- data/lib/legion/extensions/microsoft_teams/cli/auth.rb +0 -94
- data/lib/legion/extensions/microsoft_teams/helpers/browser_auth.rb +0 -270
- data/lib/legion/extensions/microsoft_teams/helpers/callback_server.rb +0 -90
- data/lib/legion/extensions/microsoft_teams/helpers/token_cache.rb +0 -412
- data/lib/legion/extensions/microsoft_teams/hooks/auth.rb +0 -17
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'socket'
|
|
4
|
-
require 'uri'
|
|
5
|
-
|
|
6
|
-
module Legion
|
|
7
|
-
module Extensions
|
|
8
|
-
module MicrosoftTeams
|
|
9
|
-
module Helpers
|
|
10
|
-
class CallbackServer
|
|
11
|
-
RESPONSE_HTML = <<~HTML
|
|
12
|
-
<html><body style="font-family:sans-serif;text-align:center;padding:40px;">
|
|
13
|
-
<h2>Authentication complete</h2><p>You can close this window.</p></body></html>
|
|
14
|
-
HTML
|
|
15
|
-
|
|
16
|
-
attr_reader :port
|
|
17
|
-
|
|
18
|
-
def initialize
|
|
19
|
-
@server = nil
|
|
20
|
-
@port = nil
|
|
21
|
-
@result = nil
|
|
22
|
-
@mutex = Mutex.new
|
|
23
|
-
@cv = ConditionVariable.new
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def start
|
|
27
|
-
@server = TCPServer.new('127.0.0.1', 0)
|
|
28
|
-
@port = @server.addr[1]
|
|
29
|
-
@thread = Thread.new { listen } # rubocop:disable ThreadSafety/NewThread
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def wait_for_callback(timeout: 120)
|
|
33
|
-
@mutex.synchronize do
|
|
34
|
-
@cv.wait(@mutex, timeout) unless @result
|
|
35
|
-
@result
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def shutdown
|
|
40
|
-
@server&.close rescue nil # rubocop:disable Style/RescueModifier
|
|
41
|
-
@thread&.join(2)
|
|
42
|
-
@thread&.kill
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def redirect_uri
|
|
46
|
-
"http://127.0.0.1:#{@port}/callback"
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
private
|
|
50
|
-
|
|
51
|
-
def listen
|
|
52
|
-
loop do
|
|
53
|
-
client = @server.accept
|
|
54
|
-
request_line = client.gets
|
|
55
|
-
# drain headers
|
|
56
|
-
loop do
|
|
57
|
-
line = client.gets
|
|
58
|
-
break if line.nil? || line.strip.empty?
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
if request_line&.include?('/callback?')
|
|
62
|
-
query = request_line.split[1].split('?', 2).last
|
|
63
|
-
params = URI.decode_www_form(query).to_h
|
|
64
|
-
|
|
65
|
-
@mutex.synchronize do
|
|
66
|
-
@result = {
|
|
67
|
-
code: params['code'],
|
|
68
|
-
state: params['state']
|
|
69
|
-
}
|
|
70
|
-
@cv.broadcast
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
client.print "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nConnection: close\r\n\r\n#{RESPONSE_HTML}"
|
|
75
|
-
client.close
|
|
76
|
-
break if @result
|
|
77
|
-
end
|
|
78
|
-
rescue IOError # rubocop:disable Legion/RescueLogging/NoCapture
|
|
79
|
-
nil # server closed during shutdown
|
|
80
|
-
rescue StandardError => e
|
|
81
|
-
@mutex.synchronize do
|
|
82
|
-
@result ||= { error: e.message }
|
|
83
|
-
@cv.broadcast
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
end
|
|
@@ -1,412 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'time'
|
|
4
|
-
require 'json'
|
|
5
|
-
require 'fileutils'
|
|
6
|
-
require 'legion/extensions/microsoft_teams/runners/auth'
|
|
7
|
-
|
|
8
|
-
module Legion
|
|
9
|
-
module Extensions
|
|
10
|
-
module MicrosoftTeams
|
|
11
|
-
module Helpers
|
|
12
|
-
class TokenCache
|
|
13
|
-
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
|
|
14
|
-
Legion::Extensions::Helpers.const_defined?(:Lex, false)
|
|
15
|
-
include Legion::Crypt::Helper if defined?(Legion::Crypt::Helper)
|
|
16
|
-
|
|
17
|
-
REFRESH_BUFFER = 60
|
|
18
|
-
DEFAULT_LOCAL_DIR = File.join(Dir.home, '.legionio', 'tokens')
|
|
19
|
-
DEFAULT_LOCAL_FILE = File.join(DEFAULT_LOCAL_DIR, 'microsoft_teams.json')
|
|
20
|
-
|
|
21
|
-
@instance_mutex = Mutex.new
|
|
22
|
-
|
|
23
|
-
def self.instance
|
|
24
|
-
@instance_mutex.synchronize do
|
|
25
|
-
@instance ||= begin
|
|
26
|
-
cache = new
|
|
27
|
-
cache.load_from_vault
|
|
28
|
-
log.info('[Teams::TokenCache] Shared instance created and loaded')
|
|
29
|
-
cache
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def self.reset_instance!
|
|
35
|
-
@instance_mutex.synchronize { @instance = nil }
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def initialize
|
|
39
|
-
@token_cache = nil
|
|
40
|
-
@delegated_cache = nil
|
|
41
|
-
@mutex = Mutex.new
|
|
42
|
-
@app_token_warned = false
|
|
43
|
-
log.debug('TokenCache initialized')
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
# --- Application token (client_credentials) ---
|
|
47
|
-
|
|
48
|
-
def cached_app_token
|
|
49
|
-
@mutex.synchronize do
|
|
50
|
-
# Phase 8: Broker-managed Entra app token (short-circuits cache path)
|
|
51
|
-
if defined?(Legion::Identity::Broker)
|
|
52
|
-
token = Legion::Identity::Broker.token_for(:entra)
|
|
53
|
-
return token if token
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
# Legacy: self-managed client_credentials + cache
|
|
57
|
-
if @token_cache && !token_expired?(@token_cache)
|
|
58
|
-
log.debug('Using cached app token')
|
|
59
|
-
return @token_cache[:token]
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
result = refresh_app_token
|
|
63
|
-
return result if result
|
|
64
|
-
|
|
65
|
-
unless @app_token_warned
|
|
66
|
-
log.warn('No app token available for Graph API calls')
|
|
67
|
-
@app_token_warned = true
|
|
68
|
-
end
|
|
69
|
-
nil
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
alias cached_graph_token cached_app_token
|
|
74
|
-
|
|
75
|
-
def clear_token_cache!
|
|
76
|
-
@mutex.synchronize do
|
|
77
|
-
@token_cache = nil
|
|
78
|
-
@app_token_warned = false
|
|
79
|
-
end
|
|
80
|
-
log.debug('App token cache cleared')
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
# --- Delegated token (user auth) ---
|
|
84
|
-
|
|
85
|
-
def cached_delegated_token
|
|
86
|
-
needs_refresh = @mutex.synchronize do
|
|
87
|
-
unless @delegated_cache
|
|
88
|
-
log.debug('No delegated token in cache')
|
|
89
|
-
return nil
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
unless token_expired?(@delegated_cache)
|
|
93
|
-
log.debug("Using cached delegated token (expires #{@delegated_cache[:expires_at]})")
|
|
94
|
-
return @delegated_cache[:token]
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
true
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
return unless needs_refresh
|
|
101
|
-
|
|
102
|
-
log.info('Delegated token expired, attempting refresh')
|
|
103
|
-
refresh_delegated
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
def store_delegated_token(access_token:, refresh_token:, expires_in:, scopes:)
|
|
107
|
-
expires_at = Time.now + expires_in.to_i
|
|
108
|
-
@mutex.synchronize do
|
|
109
|
-
@delegated_cache = {
|
|
110
|
-
token: access_token,
|
|
111
|
-
refresh_token: refresh_token,
|
|
112
|
-
expires_at: expires_at,
|
|
113
|
-
scopes: scopes
|
|
114
|
-
}
|
|
115
|
-
@app_token_warned = false
|
|
116
|
-
end
|
|
117
|
-
log.info("Delegated token stored (expires_in=#{expires_in}s, expires_at=#{expires_at})")
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
def clear_delegated_token!
|
|
121
|
-
@mutex.synchronize { @delegated_cache = nil }
|
|
122
|
-
log.debug('Delegated token cache cleared')
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
def authenticated?
|
|
126
|
-
result = @mutex.synchronize { !@delegated_cache.nil? }
|
|
127
|
-
log.debug("authenticated? => #{result}")
|
|
128
|
-
result
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
def previously_authenticated?
|
|
132
|
-
path = local_token_path
|
|
133
|
-
result = File.exist?(path)
|
|
134
|
-
log.debug("previously_authenticated? => #{result} (#{path})")
|
|
135
|
-
result
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
def load_from_vault
|
|
139
|
-
if vault_available?
|
|
140
|
-
log.info("Loading delegated token from Vault (#{vault_path})")
|
|
141
|
-
data = vault_get(vault_path)
|
|
142
|
-
if data && data[:access_token]
|
|
143
|
-
@mutex.synchronize do
|
|
144
|
-
@delegated_cache = {
|
|
145
|
-
token: data[:access_token],
|
|
146
|
-
refresh_token: data[:refresh_token],
|
|
147
|
-
expires_at: Time.parse(data[:expires_at]),
|
|
148
|
-
scopes: data[:scopes]
|
|
149
|
-
}
|
|
150
|
-
end
|
|
151
|
-
log.info('Delegated token loaded from Vault')
|
|
152
|
-
true
|
|
153
|
-
else
|
|
154
|
-
log.warn('Vault had no delegated token, falling back to local')
|
|
155
|
-
load_from_local
|
|
156
|
-
end
|
|
157
|
-
else
|
|
158
|
-
log.debug('Vault not available, loading from local file')
|
|
159
|
-
load_from_local
|
|
160
|
-
end
|
|
161
|
-
rescue StandardError => e
|
|
162
|
-
log.error("Failed to load delegated token from Vault: #{e.message}")
|
|
163
|
-
load_from_local
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
def save_to_vault
|
|
167
|
-
save_to_local
|
|
168
|
-
|
|
169
|
-
unless vault_available?
|
|
170
|
-
log.debug('Vault not available, skipping Vault save')
|
|
171
|
-
return false
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
data = @mutex.synchronize { @delegated_cache&.dup }
|
|
175
|
-
unless data
|
|
176
|
-
log.warn('No delegated token to save to Vault')
|
|
177
|
-
return false
|
|
178
|
-
end
|
|
179
|
-
|
|
180
|
-
log.info("Saving delegated token to Vault (#{vault_path})")
|
|
181
|
-
vault_write(vault_path, access_token: data[:token],
|
|
182
|
-
refresh_token: data[:refresh_token],
|
|
183
|
-
expires_at: data[:expires_at].utc.iso8601,
|
|
184
|
-
scopes: data[:scopes])
|
|
185
|
-
log.info('Delegated token saved to Vault')
|
|
186
|
-
true
|
|
187
|
-
rescue StandardError => e
|
|
188
|
-
log.error("Failed to save delegated token to Vault: #{e.message}")
|
|
189
|
-
false
|
|
190
|
-
end
|
|
191
|
-
|
|
192
|
-
def load_from_local
|
|
193
|
-
path = local_token_path
|
|
194
|
-
unless File.exist?(path)
|
|
195
|
-
log.debug("Local token file not found: #{path}")
|
|
196
|
-
return false
|
|
197
|
-
end
|
|
198
|
-
|
|
199
|
-
log.info("Loading delegated token from local file: #{path}")
|
|
200
|
-
raw = File.read(path)
|
|
201
|
-
data = ::JSON.parse(raw)
|
|
202
|
-
unless data['access_token'] && data['refresh_token']
|
|
203
|
-
log.warn('Local token file missing access_token or refresh_token')
|
|
204
|
-
return false
|
|
205
|
-
end
|
|
206
|
-
|
|
207
|
-
expires_at = Time.parse(data['expires_at'])
|
|
208
|
-
@mutex.synchronize do
|
|
209
|
-
@delegated_cache = {
|
|
210
|
-
token: data['access_token'],
|
|
211
|
-
refresh_token: data['refresh_token'],
|
|
212
|
-
expires_at: expires_at,
|
|
213
|
-
scopes: data['scopes']
|
|
214
|
-
}
|
|
215
|
-
end
|
|
216
|
-
log.info("Delegated token loaded from local file (expires_at=#{expires_at})")
|
|
217
|
-
true
|
|
218
|
-
rescue StandardError => e
|
|
219
|
-
log.error("Failed to load delegated token from local file: #{e.message}")
|
|
220
|
-
false
|
|
221
|
-
end
|
|
222
|
-
|
|
223
|
-
def save_to_local
|
|
224
|
-
data = @mutex.synchronize { @delegated_cache&.dup }
|
|
225
|
-
unless data
|
|
226
|
-
log.warn('No delegated token to save locally')
|
|
227
|
-
return false
|
|
228
|
-
end
|
|
229
|
-
|
|
230
|
-
path = local_token_path
|
|
231
|
-
FileUtils.mkdir_p(File.dirname(path))
|
|
232
|
-
File.write(path, ::JSON.pretty_generate(
|
|
233
|
-
'access_token' => data[:token],
|
|
234
|
-
'refresh_token' => data[:refresh_token],
|
|
235
|
-
'expires_at' => data[:expires_at].utc.iso8601,
|
|
236
|
-
'scopes' => data[:scopes]
|
|
237
|
-
))
|
|
238
|
-
File.chmod(0o600, path)
|
|
239
|
-
log.info("Delegated token saved to local file: #{path}")
|
|
240
|
-
true
|
|
241
|
-
rescue StandardError => e
|
|
242
|
-
log.error("Failed to save delegated token to local file: #{e.message}")
|
|
243
|
-
false
|
|
244
|
-
end
|
|
245
|
-
|
|
246
|
-
private
|
|
247
|
-
|
|
248
|
-
def vault_available?
|
|
249
|
-
return false unless defined?(Legion::Crypt)
|
|
250
|
-
return false unless defined?(Legion::Settings)
|
|
251
|
-
|
|
252
|
-
connected = Legion::Settings.dig(:crypt, :vault, :connected) == true
|
|
253
|
-
log.debug("vault_available? => #{connected}")
|
|
254
|
-
connected
|
|
255
|
-
rescue StandardError => e
|
|
256
|
-
log.debug("TokenCache: vault_available? check failed: #{e.message}")
|
|
257
|
-
false
|
|
258
|
-
end
|
|
259
|
-
|
|
260
|
-
def token_expired?(cache_entry)
|
|
261
|
-
return true unless cache_entry
|
|
262
|
-
|
|
263
|
-
buffer = delegated_refresh_buffer
|
|
264
|
-
expired = Time.now >= (cache_entry[:expires_at] - buffer)
|
|
265
|
-
log.debug("token_expired? => #{expired} (expires_at=#{cache_entry[:expires_at]}, buffer=#{buffer}s)") if expired
|
|
266
|
-
expired
|
|
267
|
-
end
|
|
268
|
-
|
|
269
|
-
def delegated_refresh_buffer
|
|
270
|
-
settings = teams_auth_settings
|
|
271
|
-
delegated = settings[:delegated]
|
|
272
|
-
return REFRESH_BUFFER unless delegated.is_a?(Hash)
|
|
273
|
-
|
|
274
|
-
delegated[:refresh_buffer] || REFRESH_BUFFER
|
|
275
|
-
end
|
|
276
|
-
|
|
277
|
-
def vault_path(_suffix = nil)
|
|
278
|
-
settings = teams_auth_settings
|
|
279
|
-
delegated = settings[:delegated]
|
|
280
|
-
return delegated[:vault_path] if delegated.is_a?(Hash) && delegated[:vault_path]
|
|
281
|
-
|
|
282
|
-
identity = if defined?(Legion::Identity::Process) && Legion::Identity::Process.resolved?
|
|
283
|
-
Legion::Identity::Process.canonical_name
|
|
284
|
-
else
|
|
285
|
-
ENV.fetch('USER', 'default').split('@').first
|
|
286
|
-
end
|
|
287
|
-
"users/#{identity}/delegated_token"
|
|
288
|
-
end
|
|
289
|
-
|
|
290
|
-
def local_token_path
|
|
291
|
-
settings = teams_auth_settings
|
|
292
|
-
delegated = settings[:delegated]
|
|
293
|
-
return DEFAULT_LOCAL_FILE unless delegated.is_a?(Hash)
|
|
294
|
-
|
|
295
|
-
delegated[:local_token_path] || DEFAULT_LOCAL_FILE
|
|
296
|
-
end
|
|
297
|
-
|
|
298
|
-
def refresh_app_token
|
|
299
|
-
result = acquire_fresh_token
|
|
300
|
-
unless result
|
|
301
|
-
unless @app_token_warned
|
|
302
|
-
log.info('No client_secret configured, app token (client_credentials) unavailable')
|
|
303
|
-
@app_token_warned = true
|
|
304
|
-
end
|
|
305
|
-
return nil
|
|
306
|
-
end
|
|
307
|
-
|
|
308
|
-
access_token = result.dig(:result, 'access_token')
|
|
309
|
-
expires_in = result.dig(:result, 'expires_in') || 3600
|
|
310
|
-
|
|
311
|
-
@token_cache = {
|
|
312
|
-
token: access_token,
|
|
313
|
-
expires_at: Time.now + expires_in
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
log.info("App token refreshed (expires_in=#{expires_in}s)")
|
|
317
|
-
access_token
|
|
318
|
-
rescue StandardError => e
|
|
319
|
-
log.error("TokenCache app refresh failed: #{e.message}")
|
|
320
|
-
nil
|
|
321
|
-
end
|
|
322
|
-
|
|
323
|
-
def refresh_delegated
|
|
324
|
-
unless @delegated_cache&.dig(:refresh_token)
|
|
325
|
-
log.warn('No refresh token available for delegated refresh')
|
|
326
|
-
return nil
|
|
327
|
-
end
|
|
328
|
-
|
|
329
|
-
settings = teams_auth_settings
|
|
330
|
-
unless settings[:tenant_id] && settings[:client_id]
|
|
331
|
-
log.warn('Missing tenant_id or client_id for delegated refresh')
|
|
332
|
-
return nil
|
|
333
|
-
end
|
|
334
|
-
|
|
335
|
-
log.info('Refreshing delegated token via refresh_token grant')
|
|
336
|
-
auth = Object.new.extend(Legion::Extensions::MicrosoftTeams::Runners::Auth)
|
|
337
|
-
result = auth.refresh_delegated_token(
|
|
338
|
-
tenant_id: settings[:tenant_id],
|
|
339
|
-
client_id: settings[:client_id],
|
|
340
|
-
refresh_token: @delegated_cache[:refresh_token],
|
|
341
|
-
scope: @delegated_cache[:scopes]
|
|
342
|
-
)
|
|
343
|
-
|
|
344
|
-
body = result[:result]
|
|
345
|
-
unless body&.dig('access_token')
|
|
346
|
-
log.warn("Delegated token refresh failed: #{result[:error]}")
|
|
347
|
-
return handle_refresh_failure(result)
|
|
348
|
-
end
|
|
349
|
-
|
|
350
|
-
expires_in = (body['expires_in'] || 3600).to_i
|
|
351
|
-
@delegated_cache = {
|
|
352
|
-
token: body['access_token'],
|
|
353
|
-
refresh_token: body['refresh_token'] || @delegated_cache[:refresh_token],
|
|
354
|
-
expires_at: Time.now + expires_in,
|
|
355
|
-
scopes: @delegated_cache[:scopes]
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
log.info("Delegated token refreshed (expires_in=#{expires_in}s)")
|
|
359
|
-
save_to_vault
|
|
360
|
-
@delegated_cache[:token]
|
|
361
|
-
rescue StandardError => e
|
|
362
|
-
log.error("TokenCache delegated refresh failed: #{e.message}")
|
|
363
|
-
nil
|
|
364
|
-
end
|
|
365
|
-
|
|
366
|
-
def handle_refresh_failure(result)
|
|
367
|
-
if result[:error] == 'invalid_grant'
|
|
368
|
-
log.warn('Refresh token invalid (invalid_grant), clearing delegated cache')
|
|
369
|
-
@delegated_cache = nil
|
|
370
|
-
emit_expired_event
|
|
371
|
-
end
|
|
372
|
-
nil
|
|
373
|
-
end
|
|
374
|
-
|
|
375
|
-
def emit_expired_event
|
|
376
|
-
Legion::Events.emit('microsoft_teams.auth.expired') if defined?(Legion::Events)
|
|
377
|
-
end
|
|
378
|
-
|
|
379
|
-
def acquire_fresh_token
|
|
380
|
-
settings = teams_auth_settings
|
|
381
|
-
unless settings[:tenant_id] && settings[:client_id] && settings[:client_secret]
|
|
382
|
-
log.debug('Missing credentials for app token acquisition') unless @app_token_warned
|
|
383
|
-
return nil
|
|
384
|
-
end
|
|
385
|
-
|
|
386
|
-
log.debug('Acquiring fresh app token via client_credentials')
|
|
387
|
-
auth = Object.new.extend(Legion::Extensions::MicrosoftTeams::Runners::Auth)
|
|
388
|
-
auth.acquire_token(
|
|
389
|
-
tenant_id: settings[:tenant_id],
|
|
390
|
-
client_id: settings[:client_id],
|
|
391
|
-
client_secret: settings[:client_secret]
|
|
392
|
-
)
|
|
393
|
-
end
|
|
394
|
-
|
|
395
|
-
def teams_auth_settings
|
|
396
|
-
ms = settings
|
|
397
|
-
ms = Legion::Settings[:microsoft_teams] if defined?(Legion::Settings) && (ms.nil? || ms.empty?)
|
|
398
|
-
ms ||= {}
|
|
399
|
-
auth = ms[:auth].is_a?(Hash) ? ms[:auth].dup : {}
|
|
400
|
-
auth[:tenant_id] ||= ms[:tenant_id]
|
|
401
|
-
auth[:client_id] ||= ms[:client_id]
|
|
402
|
-
auth[:client_secret] ||= ms[:client_secret]
|
|
403
|
-
auth[:tenant_id] ||= ENV.fetch('AZURE_TENANT_ID', nil)
|
|
404
|
-
auth[:client_id] ||= ENV.fetch('AZURE_CLIENT_ID', nil)
|
|
405
|
-
auth[:client_secret] ||= ENV.fetch('AZURE_CLIENT_SECRET', nil)
|
|
406
|
-
auth
|
|
407
|
-
end
|
|
408
|
-
end
|
|
409
|
-
end
|
|
410
|
-
end
|
|
411
|
-
end
|
|
412
|
-
end
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Legion
|
|
4
|
-
module Extensions
|
|
5
|
-
module MicrosoftTeams
|
|
6
|
-
module Hooks
|
|
7
|
-
class Auth < Legion::Extensions::Hooks::Base # rubocop:disable Legion/Extension/HookMissingRunnerClass
|
|
8
|
-
mount '/callback'
|
|
9
|
-
|
|
10
|
-
def self.runner_class
|
|
11
|
-
'Legion::Extensions::MicrosoftTeams::Runners::Auth'
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
end
|