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
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lex-microsoft_teams
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.6.
|
|
4
|
+
version: 0.6.47
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -135,6 +135,20 @@ dependencies:
|
|
|
135
135
|
- - ">="
|
|
136
136
|
- !ruby/object:Gem::Version
|
|
137
137
|
version: 1.3.9
|
|
138
|
+
- !ruby/object:Gem::Dependency
|
|
139
|
+
name: lex-identity-entra
|
|
140
|
+
requirement: !ruby/object:Gem::Requirement
|
|
141
|
+
requirements:
|
|
142
|
+
- - ">="
|
|
143
|
+
- !ruby/object:Gem::Version
|
|
144
|
+
version: 0.3.0
|
|
145
|
+
type: :runtime
|
|
146
|
+
prerelease: false
|
|
147
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
148
|
+
requirements:
|
|
149
|
+
- - ">="
|
|
150
|
+
- !ruby/object:Gem::Version
|
|
151
|
+
version: 0.3.0
|
|
138
152
|
- !ruby/object:Gem::Dependency
|
|
139
153
|
name: snappy
|
|
140
154
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -182,7 +196,6 @@ files:
|
|
|
182
196
|
- lib/legion/extensions/microsoft_teams/actors/absorb_chat.rb
|
|
183
197
|
- lib/legion/extensions/microsoft_teams/actors/absorb_meeting.rb
|
|
184
198
|
- lib/legion/extensions/microsoft_teams/actors/api_ingest.rb
|
|
185
|
-
- lib/legion/extensions/microsoft_teams/actors/auth_validator.rb
|
|
186
199
|
- lib/legion/extensions/microsoft_teams/actors/cache_bulk_ingest.rb
|
|
187
200
|
- lib/legion/extensions/microsoft_teams/actors/cache_sync.rb
|
|
188
201
|
- lib/legion/extensions/microsoft_teams/actors/channel_poller.rb
|
|
@@ -193,11 +206,7 @@ files:
|
|
|
193
206
|
- lib/legion/extensions/microsoft_teams/actors/observed_chat_poller.rb
|
|
194
207
|
- lib/legion/extensions/microsoft_teams/actors/presence_poller.rb
|
|
195
208
|
- lib/legion/extensions/microsoft_teams/actors/profile_ingest.rb
|
|
196
|
-
- lib/legion/extensions/microsoft_teams/actors/token_refresher.rb
|
|
197
|
-
- lib/legion/extensions/microsoft_teams/cli/auth.rb
|
|
198
209
|
- lib/legion/extensions/microsoft_teams/client.rb
|
|
199
|
-
- lib/legion/extensions/microsoft_teams/helpers/browser_auth.rb
|
|
200
|
-
- lib/legion/extensions/microsoft_teams/helpers/callback_server.rb
|
|
201
210
|
- lib/legion/extensions/microsoft_teams/helpers/client.rb
|
|
202
211
|
- lib/legion/extensions/microsoft_teams/helpers/graph_client.rb
|
|
203
212
|
- lib/legion/extensions/microsoft_teams/helpers/high_water_mark.rb
|
|
@@ -205,24 +214,27 @@ files:
|
|
|
205
214
|
- lib/legion/extensions/microsoft_teams/helpers/prompt_resolver.rb
|
|
206
215
|
- lib/legion/extensions/microsoft_teams/helpers/session_manager.rb
|
|
207
216
|
- lib/legion/extensions/microsoft_teams/helpers/subscription_registry.rb
|
|
208
|
-
- lib/legion/extensions/microsoft_teams/helpers/token_cache.rb
|
|
209
217
|
- lib/legion/extensions/microsoft_teams/helpers/trace_retriever.rb
|
|
210
218
|
- lib/legion/extensions/microsoft_teams/helpers/transform_definitions.rb
|
|
211
|
-
- lib/legion/extensions/microsoft_teams/hooks/auth.rb
|
|
212
219
|
- lib/legion/extensions/microsoft_teams/local_cache/extractor.rb
|
|
213
220
|
- lib/legion/extensions/microsoft_teams/local_cache/record_parser.rb
|
|
214
221
|
- lib/legion/extensions/microsoft_teams/local_cache/sstable_reader.rb
|
|
222
|
+
- lib/legion/extensions/microsoft_teams/runners/activities.rb
|
|
215
223
|
- lib/legion/extensions/microsoft_teams/runners/adaptive_cards.rb
|
|
216
224
|
- lib/legion/extensions/microsoft_teams/runners/ai_insights.rb
|
|
217
225
|
- lib/legion/extensions/microsoft_teams/runners/api_ingest.rb
|
|
226
|
+
- lib/legion/extensions/microsoft_teams/runners/app_installations.rb
|
|
218
227
|
- lib/legion/extensions/microsoft_teams/runners/auth.rb
|
|
219
228
|
- lib/legion/extensions/microsoft_teams/runners/bot.rb
|
|
220
229
|
- lib/legion/extensions/microsoft_teams/runners/cache_ingest.rb
|
|
230
|
+
- lib/legion/extensions/microsoft_teams/runners/call_events.rb
|
|
221
231
|
- lib/legion/extensions/microsoft_teams/runners/channel_messages.rb
|
|
222
232
|
- lib/legion/extensions/microsoft_teams/runners/channels.rb
|
|
223
233
|
- lib/legion/extensions/microsoft_teams/runners/chats.rb
|
|
234
|
+
- lib/legion/extensions/microsoft_teams/runners/files.rb
|
|
224
235
|
- lib/legion/extensions/microsoft_teams/runners/local_cache.rb
|
|
225
236
|
- lib/legion/extensions/microsoft_teams/runners/loop.rb
|
|
237
|
+
- lib/legion/extensions/microsoft_teams/runners/meeting_artifacts.rb
|
|
226
238
|
- lib/legion/extensions/microsoft_teams/runners/meetings.rb
|
|
227
239
|
- lib/legion/extensions/microsoft_teams/runners/messages.rb
|
|
228
240
|
- lib/legion/extensions/microsoft_teams/runners/ownership.rb
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Legion
|
|
4
|
-
module Extensions
|
|
5
|
-
module MicrosoftTeams
|
|
6
|
-
module Actor
|
|
7
|
-
class AuthValidator < Legion::Extensions::Actors::Once # rubocop:disable Legion/Extension/SelfContainedActorRunnerClass
|
|
8
|
-
def use_runner? = false
|
|
9
|
-
def check_subtask? = false
|
|
10
|
-
def generate_task? = false
|
|
11
|
-
|
|
12
|
-
def delay
|
|
13
|
-
90.0
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def enabled?
|
|
17
|
-
defined?(Legion::Extensions::MicrosoftTeams::Helpers::TokenCache)
|
|
18
|
-
rescue StandardError => e
|
|
19
|
-
log.debug("AuthValidator#enabled?: #{e.message}")
|
|
20
|
-
false
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def token_cache
|
|
24
|
-
Legion::Extensions::MicrosoftTeams::Helpers::TokenCache.instance
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def manual
|
|
28
|
-
log.info('AuthValidator starting')
|
|
29
|
-
cache = token_cache
|
|
30
|
-
log.debug("Token loaded: authenticated?=#{cache.authenticated?}")
|
|
31
|
-
|
|
32
|
-
if cache.authenticated?
|
|
33
|
-
token = cache.cached_delegated_token
|
|
34
|
-
if token
|
|
35
|
-
log.info('Teams delegated auth restored (token valid)')
|
|
36
|
-
elsif cache.previously_authenticated? || auto_authenticate?
|
|
37
|
-
log.info('Token loaded but expired, attempting browser re-auth')
|
|
38
|
-
attempt_browser_reauth(cache)
|
|
39
|
-
else
|
|
40
|
-
log.debug('Token loaded but expired, no re-auth configured')
|
|
41
|
-
end
|
|
42
|
-
elsif cache.previously_authenticated?
|
|
43
|
-
log.warn('Token file found but could not load, attempting re-authentication')
|
|
44
|
-
attempt_browser_reauth(cache)
|
|
45
|
-
elsif auto_authenticate?
|
|
46
|
-
log.info('auto_authenticate enabled, opening browser for initial authentication...')
|
|
47
|
-
attempt_browser_reauth(cache)
|
|
48
|
-
else
|
|
49
|
-
log.debug('No Teams delegated auth configured, skipping')
|
|
50
|
-
end
|
|
51
|
-
log.info('AuthValidator complete')
|
|
52
|
-
rescue StandardError => e
|
|
53
|
-
log.error("AuthValidator: #{e.message}")
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
private
|
|
57
|
-
|
|
58
|
-
def attempt_browser_reauth(cache)
|
|
59
|
-
settings = teams_auth_settings
|
|
60
|
-
unless settings[:tenant_id] && settings[:client_id]
|
|
61
|
-
log.warn("Cannot re-auth: tenant_id=#{settings[:tenant_id] ? 'present' : 'nil'}, client_id=#{settings[:client_id] ? 'present' : 'nil'}")
|
|
62
|
-
return false
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
log.warn('Delegated token expired, opening browser for re-authentication...')
|
|
66
|
-
|
|
67
|
-
scopes = settings.dig(:delegated, :scopes) ||
|
|
68
|
-
Legion::Extensions::MicrosoftTeams::Helpers::BrowserAuth::DEFAULT_SCOPES
|
|
69
|
-
log.debug("Using scopes: #{scopes}")
|
|
70
|
-
browser_auth = Legion::Extensions::MicrosoftTeams::Helpers::BrowserAuth.new(
|
|
71
|
-
tenant_id: settings[:tenant_id],
|
|
72
|
-
client_id: settings[:client_id],
|
|
73
|
-
scopes: scopes
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
-
result = browser_auth.authenticate
|
|
77
|
-
if result[:error]
|
|
78
|
-
log.error("Browser auth returned error: #{result[:error]} - #{result[:description]}")
|
|
79
|
-
return false
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
body = result[:result]
|
|
83
|
-
log.info("Browser auth succeeded, storing token (expires_in=#{body['expires_in']})")
|
|
84
|
-
cache.store_delegated_token(
|
|
85
|
-
access_token: body['access_token'],
|
|
86
|
-
refresh_token: body['refresh_token'],
|
|
87
|
-
expires_in: body['expires_in'],
|
|
88
|
-
scopes: scopes
|
|
89
|
-
)
|
|
90
|
-
cache.save_to_vault
|
|
91
|
-
log.info('Teams delegated auth restored via browser')
|
|
92
|
-
true
|
|
93
|
-
rescue StandardError => e
|
|
94
|
-
log.error("Browser re-auth failed: #{e.message}")
|
|
95
|
-
false
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def auto_authenticate?
|
|
99
|
-
settings = teams_auth_settings
|
|
100
|
-
result = settings.dig(:delegated, :auto_authenticate) == true
|
|
101
|
-
log.debug("auto_authenticate? => #{result}")
|
|
102
|
-
result
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
def teams_auth_settings
|
|
106
|
-
settings = if defined?(Legion::Settings)
|
|
107
|
-
ms = Legion::Settings[:microsoft_teams]
|
|
108
|
-
auth = ms && ms[:auth].is_a?(Hash) ? ms[:auth].dup : {}
|
|
109
|
-
auth[:tenant_id] ||= ms[:tenant_id] if ms
|
|
110
|
-
auth[:client_id] ||= ms[:client_id] if ms
|
|
111
|
-
auth
|
|
112
|
-
else
|
|
113
|
-
{}
|
|
114
|
-
end
|
|
115
|
-
settings[:tenant_id] ||= ENV.fetch('AZURE_TENANT_ID', nil)
|
|
116
|
-
settings[:client_id] ||= ENV.fetch('AZURE_CLIENT_ID', nil)
|
|
117
|
-
settings
|
|
118
|
-
end
|
|
119
|
-
end
|
|
120
|
-
end
|
|
121
|
-
end
|
|
122
|
-
end
|
|
123
|
-
end
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Legion
|
|
4
|
-
module Extensions
|
|
5
|
-
module MicrosoftTeams
|
|
6
|
-
module Actor
|
|
7
|
-
class TokenRefresher < Legion::Extensions::Actors::Every
|
|
8
|
-
DEFAULT_REFRESH_INTERVAL = 900
|
|
9
|
-
|
|
10
|
-
def runner_class = Legion::Extensions::MicrosoftTeams::Helpers::TokenCache
|
|
11
|
-
def runner_function = 'cached_delegated_token'
|
|
12
|
-
def run_now? = false
|
|
13
|
-
def use_runner? = false
|
|
14
|
-
def check_subtask? = false
|
|
15
|
-
def generate_task? = false
|
|
16
|
-
|
|
17
|
-
def time
|
|
18
|
-
settings = teams_auth_settings
|
|
19
|
-
delegated = settings[:delegated]
|
|
20
|
-
return DEFAULT_REFRESH_INTERVAL unless delegated.is_a?(Hash)
|
|
21
|
-
|
|
22
|
-
delegated[:refresh_interval] || DEFAULT_REFRESH_INTERVAL
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def enabled?
|
|
26
|
-
defined?(Legion::Extensions::MicrosoftTeams::Helpers::TokenCache)
|
|
27
|
-
rescue StandardError => e
|
|
28
|
-
log.debug("TokenRefresher#enabled?: #{e.message}")
|
|
29
|
-
false
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def token_cache
|
|
33
|
-
Legion::Extensions::MicrosoftTeams::Helpers::TokenCache.instance
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def manual
|
|
37
|
-
log.debug('TokenRefresher tick')
|
|
38
|
-
unless token_cache.authenticated?
|
|
39
|
-
log.debug('No active delegated token, skipping refresh')
|
|
40
|
-
return
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
log.info('Checking delegated token freshness')
|
|
44
|
-
token = token_cache.cached_delegated_token
|
|
45
|
-
if token
|
|
46
|
-
log.info('Delegated token still valid, persisting')
|
|
47
|
-
token_cache.save_to_vault
|
|
48
|
-
elsif token_cache.previously_authenticated?
|
|
49
|
-
log.warn('Delegated token expired, attempting browser re-auth')
|
|
50
|
-
attempt_browser_reauth(token_cache)
|
|
51
|
-
else
|
|
52
|
-
log.warn('Delegated token expired, no previous auth to restore')
|
|
53
|
-
end
|
|
54
|
-
rescue StandardError => e
|
|
55
|
-
log.error("TokenRefresher: #{e.message}")
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
private
|
|
59
|
-
|
|
60
|
-
def attempt_browser_reauth(cache)
|
|
61
|
-
settings = teams_auth_settings
|
|
62
|
-
unless settings[:tenant_id] && settings[:client_id]
|
|
63
|
-
log.warn("Cannot re-auth: tenant_id=#{settings[:tenant_id] ? 'present' : 'nil'}, client_id=#{settings[:client_id] ? 'present' : 'nil'}")
|
|
64
|
-
return false
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
log.warn('Delegated token expired, opening browser for re-authentication...')
|
|
68
|
-
|
|
69
|
-
scopes = settings.dig(:delegated, :scopes) ||
|
|
70
|
-
Legion::Extensions::MicrosoftTeams::Helpers::BrowserAuth::DEFAULT_SCOPES
|
|
71
|
-
log.debug("Using scopes: #{scopes}")
|
|
72
|
-
browser_auth = Legion::Extensions::MicrosoftTeams::Helpers::BrowserAuth.new(
|
|
73
|
-
tenant_id: settings[:tenant_id],
|
|
74
|
-
client_id: settings[:client_id],
|
|
75
|
-
scopes: scopes
|
|
76
|
-
)
|
|
77
|
-
|
|
78
|
-
result = browser_auth.authenticate
|
|
79
|
-
if result[:error]
|
|
80
|
-
log.error("Browser auth returned error: #{result[:error]} - #{result[:description]}")
|
|
81
|
-
return false
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
body = result[:result]
|
|
85
|
-
log.info("Browser auth succeeded, storing token (expires_in=#{body['expires_in']})")
|
|
86
|
-
cache.store_delegated_token(
|
|
87
|
-
access_token: body['access_token'],
|
|
88
|
-
refresh_token: body['refresh_token'],
|
|
89
|
-
expires_in: body['expires_in'],
|
|
90
|
-
scopes: scopes
|
|
91
|
-
)
|
|
92
|
-
cache.save_to_vault
|
|
93
|
-
log.info('Teams delegated auth restored via browser')
|
|
94
|
-
true
|
|
95
|
-
rescue StandardError => e
|
|
96
|
-
log.error("Browser re-auth failed: #{e.message}")
|
|
97
|
-
false
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
def teams_auth_settings
|
|
101
|
-
settings = if defined?(Legion::Settings)
|
|
102
|
-
ms = Legion::Settings[:microsoft_teams]
|
|
103
|
-
auth = if ms && ms[:auth].is_a?(Hash)
|
|
104
|
-
ms[:auth].dup
|
|
105
|
-
else
|
|
106
|
-
{}
|
|
107
|
-
end
|
|
108
|
-
auth[:tenant_id] ||= ms[:tenant_id] if ms
|
|
109
|
-
auth[:client_id] ||= ms[:client_id] if ms
|
|
110
|
-
auth
|
|
111
|
-
else
|
|
112
|
-
{}
|
|
113
|
-
end
|
|
114
|
-
settings[:tenant_id] ||= ENV.fetch('AZURE_TENANT_ID', nil)
|
|
115
|
-
settings[:client_id] ||= ENV.fetch('AZURE_CLIENT_ID', nil)
|
|
116
|
-
settings
|
|
117
|
-
end
|
|
118
|
-
end
|
|
119
|
-
end
|
|
120
|
-
end
|
|
121
|
-
end
|
|
122
|
-
end
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'legion/extensions/microsoft_teams/helpers/browser_auth'
|
|
4
|
-
require 'legion/extensions/microsoft_teams/helpers/token_cache'
|
|
5
|
-
|
|
6
|
-
module Legion
|
|
7
|
-
module Extensions
|
|
8
|
-
module MicrosoftTeams
|
|
9
|
-
module CLI
|
|
10
|
-
class Auth
|
|
11
|
-
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
|
|
12
|
-
Legion::Extensions::Helpers.const_defined?(:Lex, false)
|
|
13
|
-
|
|
14
|
-
def self.cli_alias
|
|
15
|
-
'teams'
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def self.descriptions
|
|
19
|
-
{
|
|
20
|
-
login: 'Authenticate with Microsoft Teams via browser OAuth',
|
|
21
|
-
status: 'Show current Teams authentication state'
|
|
22
|
-
}
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def login(tenant_id: nil, client_id: nil)
|
|
26
|
-
settings = resolve_settings
|
|
27
|
-
tid = tenant_id || settings[:tenant_id] || ENV.fetch('AZURE_TENANT_ID', nil)
|
|
28
|
-
cid = client_id || settings[:client_id] || ENV.fetch('AZURE_CLIENT_ID', nil)
|
|
29
|
-
|
|
30
|
-
log.debug("Resolved tenant_id=#{tid ? 'present' : 'nil'}, client_id=#{cid ? 'present' : 'nil'}")
|
|
31
|
-
|
|
32
|
-
unless tid && cid
|
|
33
|
-
puts 'Error: tenant_id and client_id required (set in settings, env vars, or pass as args)'
|
|
34
|
-
return
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
log.info('Starting Teams delegated auth login')
|
|
38
|
-
browser_auth = Helpers::BrowserAuth.new(tenant_id: tid, client_id: cid, force_local_server: true)
|
|
39
|
-
result = browser_auth.authenticate
|
|
40
|
-
|
|
41
|
-
body = result&.dig(:result)
|
|
42
|
-
if body&.dig('access_token')
|
|
43
|
-
log.info('Authentication successful, storing token')
|
|
44
|
-
store_token(body)
|
|
45
|
-
puts 'Teams authenticated successfully.'
|
|
46
|
-
else
|
|
47
|
-
log.warn("Authentication result: #{result&.keys&.join(', ') || 'nil'}")
|
|
48
|
-
puts 'Teams authentication failed or was cancelled.'
|
|
49
|
-
end
|
|
50
|
-
rescue StandardError => e
|
|
51
|
-
log.error("Login failed: #{e.message}")
|
|
52
|
-
puts "Error: #{e.message}"
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def status
|
|
56
|
-
token_file = File.expand_path('~/.legionio/tokens/microsoft_teams.json')
|
|
57
|
-
if File.exist?(token_file)
|
|
58
|
-
log.info("Token file found: #{token_file}")
|
|
59
|
-
puts 'Teams: authenticated (token file present)'
|
|
60
|
-
else
|
|
61
|
-
log.info('No token file found')
|
|
62
|
-
puts 'Teams: not authenticated'
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
private
|
|
67
|
-
|
|
68
|
-
def resolve_settings
|
|
69
|
-
return {} unless defined?(Legion::Settings)
|
|
70
|
-
|
|
71
|
-
Legion::Settings[:microsoft_teams]&.dig(:auth) || {}
|
|
72
|
-
rescue StandardError => e
|
|
73
|
-
log.debug("Auth: resolve_settings failed: #{e.message}")
|
|
74
|
-
{}
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def store_token(body)
|
|
78
|
-
cache = Helpers::TokenCache.instance
|
|
79
|
-
cache.store_delegated_token(
|
|
80
|
-
access_token: body['access_token'],
|
|
81
|
-
refresh_token: body['refresh_token'],
|
|
82
|
-
expires_in: body['expires_in'],
|
|
83
|
-
scopes: body['scope']
|
|
84
|
-
)
|
|
85
|
-
cache.save_to_vault
|
|
86
|
-
log.info('Token stored successfully')
|
|
87
|
-
rescue StandardError => e
|
|
88
|
-
log.error("Failed to store token: #{e.message}")
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
end
|
|
94
|
-
end
|
|
@@ -1,270 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'securerandom'
|
|
4
|
-
require 'digest'
|
|
5
|
-
require 'base64'
|
|
6
|
-
require 'rbconfig'
|
|
7
|
-
|
|
8
|
-
require 'legion/extensions/microsoft_teams/runners/auth'
|
|
9
|
-
require 'legion/extensions/microsoft_teams/helpers/callback_server'
|
|
10
|
-
|
|
11
|
-
module Legion
|
|
12
|
-
module Extensions
|
|
13
|
-
module MicrosoftTeams
|
|
14
|
-
module Helpers
|
|
15
|
-
class BrowserAuth
|
|
16
|
-
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
|
|
17
|
-
Legion::Extensions::Helpers.const_defined?(:Lex, false)
|
|
18
|
-
|
|
19
|
-
DEFAULT_SCOPES = [
|
|
20
|
-
'offline_access', 'openid', 'profile', 'email',
|
|
21
|
-
'User.Read', 'People.Read', 'Presence.Read', 'Presence.Read.All',
|
|
22
|
-
'Chat.Read', 'Chat.ReadBasic', 'ChatMember.Read', 'ChatMessage.Read',
|
|
23
|
-
'Channel.ReadBasic.All', 'ChannelMember.Read.All', 'ChannelMessage.Read.All',
|
|
24
|
-
'Team.ReadBasic.All', 'Group-Conversation.Read.All',
|
|
25
|
-
'OnlineMeetings.Read', 'OnlineMeetingTranscript.Read.All',
|
|
26
|
-
'OnlineMeetingRecording.Read.All', 'OnlineMeetingArtifact.Read.All',
|
|
27
|
-
'OnlineMeetingAiInsight.Read.All', 'CallAiInsights.Read.All',
|
|
28
|
-
'CallEvents.Read', 'CallRecordings.Read.All', 'CallTranscripts.Read.All',
|
|
29
|
-
'TeamsActivity.Read', 'TeamsActivity.Send'
|
|
30
|
-
].join(' ').freeze
|
|
31
|
-
|
|
32
|
-
attr_reader :tenant_id, :client_id, :scopes
|
|
33
|
-
|
|
34
|
-
def initialize(tenant_id:, client_id:, scopes: DEFAULT_SCOPES, auth: nil, force_local_server: false, **)
|
|
35
|
-
@tenant_id = tenant_id
|
|
36
|
-
@client_id = client_id
|
|
37
|
-
@scopes = scopes
|
|
38
|
-
@auth = auth || Object.new.extend(Runners::Auth)
|
|
39
|
-
@force_local_server = force_local_server
|
|
40
|
-
log.debug("BrowserAuth initialized (tenant=#{tenant_id}, client=#{client_id}, force_local=#{force_local_server})")
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def authenticate
|
|
44
|
-
if gui_available?
|
|
45
|
-
log.info('GUI available, using browser auth')
|
|
46
|
-
authenticate_browser
|
|
47
|
-
else
|
|
48
|
-
log.info('No GUI detected, using device code flow')
|
|
49
|
-
authenticate_device_code
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def api_hook_available?
|
|
54
|
-
if @force_local_server
|
|
55
|
-
log.debug('api_hook_available? => false (force_local_server)')
|
|
56
|
-
return false
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
api_defined = defined?(Legion::API)
|
|
60
|
-
events_defined = defined?(Legion::Events)
|
|
61
|
-
hooks_defined = defined?(Legion::Extensions::Hooks::Base)
|
|
62
|
-
route_ok = api_defined && events_defined && hooks_defined && hook_route_registered?
|
|
63
|
-
|
|
64
|
-
log.debug("api_hook_available? => #{!route_ok.nil?} " \
|
|
65
|
-
"(API=#{!api_defined.nil?}, Events=#{!events_defined.nil?}, Hooks=#{!hooks_defined.nil?}, route=#{route_ok})")
|
|
66
|
-
!!route_ok
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def hook_redirect_uri
|
|
70
|
-
port = if defined?(Legion::Settings)
|
|
71
|
-
Legion::Settings.dig(:api, :port) || 4567
|
|
72
|
-
else
|
|
73
|
-
4567
|
|
74
|
-
end
|
|
75
|
-
"http://127.0.0.1:#{port}/api/extensions/microsoft_teams/hooks/auth/handle"
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def generate_pkce
|
|
79
|
-
verifier = SecureRandom.urlsafe_base64(32)
|
|
80
|
-
challenge = Base64.urlsafe_encode64(Digest::SHA256.digest(verifier), padding: false)
|
|
81
|
-
log.debug('PKCE challenge generated')
|
|
82
|
-
[verifier, challenge]
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def gui_available?
|
|
86
|
-
os = host_os
|
|
87
|
-
return true if /darwin|mswin|mingw/.match?(os)
|
|
88
|
-
|
|
89
|
-
!ENV['DISPLAY'].nil? || !ENV['WAYLAND_DISPLAY'].nil?
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
def open_browser(url)
|
|
93
|
-
cmd = case host_os
|
|
94
|
-
when /darwin/ then 'open'
|
|
95
|
-
when /linux/ then 'xdg-open'
|
|
96
|
-
when /mswin|mingw/ then 'start'
|
|
97
|
-
end
|
|
98
|
-
unless cmd
|
|
99
|
-
log.warn('No browser command found for this OS')
|
|
100
|
-
return false
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
log.debug("Opening browser with: #{cmd}")
|
|
104
|
-
system(cmd, url)
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
private
|
|
108
|
-
|
|
109
|
-
def host_os
|
|
110
|
-
RbConfig::CONFIG['host_os']
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
def hook_route_registered?
|
|
114
|
-
return false unless defined?(Legion::API)
|
|
115
|
-
|
|
116
|
-
log.debug("Probing hook route at http://127.0.0.1:#{api_port}/api/extensions/microsoft_teams/hooks/auth/handle")
|
|
117
|
-
conn = Faraday.new(url: "http://127.0.0.1:#{api_port}")
|
|
118
|
-
resp = conn.head('/api/extensions/microsoft_teams/hooks/auth/handle')
|
|
119
|
-
registered = resp.status != 404
|
|
120
|
-
log.debug("Hook route probe returned #{resp.status} (registered=#{registered})")
|
|
121
|
-
registered
|
|
122
|
-
rescue StandardError => e
|
|
123
|
-
log.debug("Hook route probe failed: #{e.message}")
|
|
124
|
-
false
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
def api_port
|
|
128
|
-
if defined?(Legion::Settings)
|
|
129
|
-
Legion::Settings.dig(:api, :port) || 4567
|
|
130
|
-
else
|
|
131
|
-
4567
|
|
132
|
-
end
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
def authenticate_browser
|
|
136
|
-
verifier, challenge = generate_pkce
|
|
137
|
-
state = SecureRandom.hex(32)
|
|
138
|
-
|
|
139
|
-
if api_hook_available?
|
|
140
|
-
log.info('Using API hook for OAuth callback')
|
|
141
|
-
authenticate_via_hook(verifier: verifier, challenge: challenge, state: state)
|
|
142
|
-
else
|
|
143
|
-
log.info('Using local callback server for OAuth callback')
|
|
144
|
-
authenticate_via_server(verifier: verifier, challenge: challenge, state: state)
|
|
145
|
-
end
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
def authenticate_via_hook(verifier:, challenge:, state:)
|
|
149
|
-
callback_uri = hook_redirect_uri
|
|
150
|
-
log.debug("Hook callback URI: #{callback_uri}")
|
|
151
|
-
result_holder = { result: nil }
|
|
152
|
-
mutex = Mutex.new
|
|
153
|
-
cv = ConditionVariable.new
|
|
154
|
-
|
|
155
|
-
listener = Legion::Events.once('microsoft_teams.oauth.callback') do |event|
|
|
156
|
-
log.debug('OAuth callback event received via Legion::Events')
|
|
157
|
-
mutex.synchronize do
|
|
158
|
-
result_holder[:result] = event
|
|
159
|
-
cv.broadcast
|
|
160
|
-
end
|
|
161
|
-
end
|
|
162
|
-
|
|
163
|
-
url = @auth.authorize_url(
|
|
164
|
-
tenant_id: tenant_id, client_id: client_id,
|
|
165
|
-
redirect_uri: callback_uri, scope: scopes,
|
|
166
|
-
state: state, code_challenge: challenge
|
|
167
|
-
)
|
|
168
|
-
|
|
169
|
-
log.info('Opening browser for authentication (using API hook)...')
|
|
170
|
-
unless open_browser(url)
|
|
171
|
-
Legion::Events.off('microsoft_teams.oauth.callback', listener)
|
|
172
|
-
log.warn('Could not open browser. Falling back to device code flow.')
|
|
173
|
-
return authenticate_device_code
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
log.debug('Waiting for OAuth callback (timeout=120s)...')
|
|
177
|
-
mutex.synchronize { cv.wait(mutex, 120) unless result_holder[:result] }
|
|
178
|
-
result = result_holder[:result]
|
|
179
|
-
|
|
180
|
-
unless result && result[:code]
|
|
181
|
-
log.error('OAuth callback timed out or missing code')
|
|
182
|
-
return { error: 'timeout', description: 'No callback received within timeout' }
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
unless result[:state] == state
|
|
186
|
-
log.error('OAuth state mismatch (possible CSRF)')
|
|
187
|
-
return { error: 'state_mismatch', description: 'CSRF state parameter mismatch' }
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
log.info('Exchanging authorization code for tokens (via hook)')
|
|
191
|
-
@auth.exchange_code(
|
|
192
|
-
tenant_id: tenant_id, client_id: client_id,
|
|
193
|
-
code: result[:code], redirect_uri: callback_uri,
|
|
194
|
-
code_verifier: verifier, scope: scopes
|
|
195
|
-
)
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
def authenticate_via_server(verifier:, challenge:, state:)
|
|
199
|
-
server = CallbackServer.new
|
|
200
|
-
server.start
|
|
201
|
-
callback_uri = server.redirect_uri
|
|
202
|
-
log.info("Local callback server started on #{callback_uri}")
|
|
203
|
-
|
|
204
|
-
url = @auth.authorize_url(
|
|
205
|
-
tenant_id: tenant_id, client_id: client_id,
|
|
206
|
-
redirect_uri: callback_uri, scope: scopes,
|
|
207
|
-
state: state, code_challenge: challenge
|
|
208
|
-
)
|
|
209
|
-
|
|
210
|
-
log.info("Opening browser for authentication (callback: #{callback_uri})...")
|
|
211
|
-
unless open_browser(url)
|
|
212
|
-
log.warn('Could not open browser. Falling back to device code flow.')
|
|
213
|
-
return authenticate_device_code
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
log.debug('Waiting for OAuth callback on local server (timeout=120s)...')
|
|
217
|
-
result = server.wait_for_callback(timeout: 120)
|
|
218
|
-
|
|
219
|
-
unless result && result[:code]
|
|
220
|
-
log.error('OAuth callback timed out or missing code')
|
|
221
|
-
return { error: 'timeout', description: 'No callback received within timeout' }
|
|
222
|
-
end
|
|
223
|
-
|
|
224
|
-
unless result[:state] == state
|
|
225
|
-
log.error('OAuth state mismatch (possible CSRF)')
|
|
226
|
-
return { error: 'state_mismatch', description: 'CSRF state parameter mismatch' }
|
|
227
|
-
end
|
|
228
|
-
|
|
229
|
-
log.info('Exchanging authorization code for tokens (via local server)')
|
|
230
|
-
@auth.exchange_code(
|
|
231
|
-
tenant_id: tenant_id, client_id: client_id,
|
|
232
|
-
code: result[:code], redirect_uri: callback_uri,
|
|
233
|
-
code_verifier: verifier, scope: scopes
|
|
234
|
-
)
|
|
235
|
-
ensure
|
|
236
|
-
server&.shutdown
|
|
237
|
-
log.debug('Local callback server shut down')
|
|
238
|
-
end
|
|
239
|
-
|
|
240
|
-
def authenticate_device_code
|
|
241
|
-
log.info('Starting device code flow')
|
|
242
|
-
dc = @auth.request_device_code(
|
|
243
|
-
tenant_id: tenant_id,
|
|
244
|
-
client_id: client_id,
|
|
245
|
-
scope: scopes
|
|
246
|
-
)
|
|
247
|
-
if dc[:error]
|
|
248
|
-
log.error("Device code request failed: #{dc[:error]} - #{dc[:description]}")
|
|
249
|
-
return { error: dc[:error], description: dc[:description] }
|
|
250
|
-
end
|
|
251
|
-
|
|
252
|
-
body = dc[:result]
|
|
253
|
-
|
|
254
|
-
log.info("Go to: #{body['verification_uri']}")
|
|
255
|
-
log.info("Code: #{body['user_code']}")
|
|
256
|
-
|
|
257
|
-
open_browser(body['verification_uri']) if gui_available?
|
|
258
|
-
|
|
259
|
-
log.debug('Polling for device code authorization...')
|
|
260
|
-
@auth.poll_device_code(
|
|
261
|
-
tenant_id: tenant_id,
|
|
262
|
-
client_id: client_id,
|
|
263
|
-
device_code: body['device_code']
|
|
264
|
-
)
|
|
265
|
-
end
|
|
266
|
-
end
|
|
267
|
-
end
|
|
268
|
-
end
|
|
269
|
-
end
|
|
270
|
-
end
|