stack_auth/
auto_strategy.rs1use cts_common::Crn;
2
3use crate::access_key_strategy::AccessKeyStrategy;
4use crate::device_session_strategy::DeviceSessionStrategy;
5#[cfg(not(target_arch = "wasm32"))]
6use stack_profile::ProfileStore;
7
8#[cfg(not(target_arch = "wasm32"))]
9use crate::Token;
10use crate::{AuthError, AuthStrategy, ServiceToken};
11
12pub enum AutoStrategy {
53 AccessKey(AccessKeyStrategy),
55 DeviceSession(DeviceSessionStrategy),
57}
58
59impl AutoStrategy {
60 pub fn builder() -> AutoStrategyBuilder {
81 AutoStrategyBuilder {
82 access_key: None,
83 crn: None,
84 }
85 }
86
87 pub fn detect() -> Result<Self, AuthError> {
96 Self::builder().detect()
97 }
98
99 #[cfg(not(target_arch = "wasm32"))]
105 fn detect_inner(
106 access_key: Option<String>,
107 crn: Option<Crn>,
108 store: Option<ProfileStore>,
109 ) -> Result<Self, AuthError> {
110 if let Some(access_key) = access_key {
112 let workspace_crn = crn.ok_or(AuthError::MissingWorkspaceCrn)?;
113 let key: crate::AccessKey = access_key.parse()?;
114 let strategy = AccessKeyStrategy::new(workspace_crn, key)?;
115 return Ok(Self::AccessKey(strategy));
116 }
117
118 if let Some(store) = store {
120 let has_token = store
121 .current_workspace_store()
122 .map(|ws| ws.exists_profile::<Token>())
123 .unwrap_or(false);
124 if has_token {
125 let strategy = DeviceSessionStrategy::with_profile(store).build()?;
126 return Ok(Self::DeviceSession(strategy));
127 }
128 }
129
130 Err(AuthError::NotAuthenticated)
132 }
133
134 #[cfg(target_arch = "wasm32")]
135 fn detect_inner(access_key: Option<String>, crn: Option<Crn>) -> Result<Self, AuthError> {
136 if let Some(access_key) = access_key {
137 let workspace_crn = crn.ok_or(AuthError::MissingWorkspaceCrn)?;
138 let key: crate::AccessKey = access_key.parse()?;
139 let strategy = AccessKeyStrategy::new(workspace_crn, key)?;
140 return Ok(Self::AccessKey(strategy));
141 }
142 Err(AuthError::NotAuthenticated)
143 }
144}
145
146pub struct AutoStrategyBuilder {
165 access_key: Option<String>,
166 crn: Option<Crn>,
167}
168
169impl AutoStrategyBuilder {
170 pub fn with_access_key(mut self, access_key: impl Into<String>) -> Self {
172 self.access_key = Some(access_key.into());
173 self
174 }
175
176 pub fn with_workspace_crn(mut self, crn: Crn) -> Self {
178 self.crn = Some(crn);
179 self
180 }
181
182 pub fn detect(self) -> Result<AutoStrategy, AuthError> {
190 let access_key = self
192 .access_key
193 .or_else(|| std::env::var("CS_CLIENT_ACCESS_KEY").ok());
194
195 let crn = match self.crn {
196 Some(crn) => Some(crn),
197 None => std::env::var("CS_WORKSPACE_CRN")
198 .ok()
199 .map(|s| s.parse::<Crn>().map_err(AuthError::InvalidCrn))
200 .transpose()?,
201 };
202
203 #[cfg(not(target_arch = "wasm32"))]
204 {
205 let store = match ProfileStore::resolve(None) {
209 Ok(s) => Some(s),
210 Err(e) => {
211 tracing::info!(error = %e, "could not resolve profile store; continuing without it");
212 None
213 }
214 };
215 AutoStrategy::detect_inner(access_key, crn, store)
216 }
217 #[cfg(target_arch = "wasm32")]
218 {
219 AutoStrategy::detect_inner(access_key, crn)
220 }
221 }
222}
223
224impl AuthStrategy for &AutoStrategy {
225 async fn get_token(self) -> Result<ServiceToken, AuthError> {
226 match self {
227 AutoStrategy::AccessKey(inner) => inner.get_token().await,
228 AutoStrategy::DeviceSession(inner) => inner.get_token().await,
229 }
230 }
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236 use crate::{SecretToken, Token};
237 use std::time::{SystemTime, UNIX_EPOCH};
238
239 const VALID_CRN: &str = "crn:ap-southeast-2.aws:ZVATKW3VHMFG27DY";
240
241 fn valid_crn() -> Crn {
242 VALID_CRN.parse().unwrap()
243 }
244
245 fn make_oauth_token() -> Token {
246 let now = SystemTime::now()
247 .duration_since(UNIX_EPOCH)
248 .unwrap()
249 .as_secs();
250
251 let claims = serde_json::json!({
252 "iss": "https://cts.example.com/",
253 "sub": "CS|test-user",
254 "aud": "test-audience",
255 "iat": now,
256 "exp": now + 3600,
257 "workspace": "ZVATKW3VHMFG27DY",
258 "scope": "",
259 });
260
261 let key = jsonwebtoken::EncodingKey::from_secret(b"test-secret");
262 let jwt = jsonwebtoken::encode(&jsonwebtoken::Header::default(), &claims, &key).unwrap();
263
264 Token {
265 access_token: SecretToken::new(jwt),
266 token_type: "Bearer".to_string(),
267 expires_at: now + 3600,
268 refresh_token: Some(SecretToken::new("test-refresh-token")),
269 region: Some("ap-southeast-2.aws".to_string()),
270 client_id: Some("test-client-id".to_string()),
271 device_instance_id: None,
272 }
273 }
274
275 fn write_token_store(dir: &std::path::Path) -> ProfileStore {
276 let store = ProfileStore::new(dir);
277 store.init_workspace("ZVATKW3VHMFG27DY").unwrap();
278 let ws_store = store.current_workspace_store().unwrap();
279 ws_store.save_profile(&make_oauth_token()).unwrap();
280 store
281 }
282
283 mod detect_inner {
284 use super::*;
285
286 #[test]
287 fn access_key_with_valid_crn() {
288 let result = AutoStrategy::detect_inner(
289 Some("CSAKtestKeyId.testKeySecret".into()),
290 Some(valid_crn()),
291 None,
292 );
293
294 assert!(result.is_ok());
295 assert!(matches!(result.unwrap(), AutoStrategy::AccessKey(_)));
296 }
297
298 #[test]
299 fn access_key_without_crn_returns_missing_workspace_crn() {
300 let result =
301 AutoStrategy::detect_inner(Some("CSAKtestKeyId.testKeySecret".into()), None, None);
302
303 assert!(matches!(result, Err(AuthError::MissingWorkspaceCrn)));
304 }
305
306 #[test]
307 fn invalid_access_key_format_returns_invalid_access_key() {
308 let result =
309 AutoStrategy::detect_inner(Some("not-a-valid-key".into()), Some(valid_crn()), None);
310
311 assert!(matches!(result, Err(AuthError::InvalidAccessKey(_))));
312 }
313
314 #[test]
315 fn oauth_store_with_valid_token() {
316 let dir = tempfile::tempdir().unwrap();
317 let store = write_token_store(dir.path());
318
319 let result = AutoStrategy::detect_inner(None, None, Some(store));
320
321 assert!(result.is_ok());
322 assert!(matches!(result.unwrap(), AutoStrategy::DeviceSession(_)));
323 }
324
325 #[test]
326 fn oauth_store_without_token_file_returns_not_authenticated() {
327 let dir = tempfile::tempdir().unwrap();
328 let store = ProfileStore::new(dir.path());
329
330 let result = AutoStrategy::detect_inner(None, None, Some(store));
331
332 assert!(matches!(result, Err(AuthError::NotAuthenticated)));
333 }
334
335 #[test]
336 fn no_credentials_returns_not_authenticated() {
337 let result = AutoStrategy::detect_inner(None, None, None);
338
339 assert!(matches!(result, Err(AuthError::NotAuthenticated)));
340 }
341
342 #[test]
343 fn access_key_takes_priority_over_oauth_store() {
344 let dir = tempfile::tempdir().unwrap();
345 let store = write_token_store(dir.path());
346
347 let result = AutoStrategy::detect_inner(
348 Some("CSAKtestKeyId.testKeySecret".into()),
349 Some(valid_crn()),
350 Some(store),
351 );
352
353 assert!(result.is_ok());
354 assert!(matches!(result.unwrap(), AutoStrategy::AccessKey(_)));
355 }
356 }
357
358 mod builder {
359 use super::*;
360
361 #[test]
362 fn explicit_access_key_and_crn() {
363 let result = AutoStrategy::builder()
364 .with_access_key("CSAKtestKeyId.testKeySecret")
365 .with_workspace_crn(valid_crn())
366 .detect();
367
368 assert!(result.is_ok());
369 assert!(matches!(result.unwrap(), AutoStrategy::AccessKey(_)));
370 }
371
372 #[test]
373 fn explicit_access_key_without_crn_and_no_env_returns_missing_workspace_crn() {
374 let saved_crn = std::env::var("CS_WORKSPACE_CRN").ok();
376 std::env::remove_var("CS_WORKSPACE_CRN");
377
378 let result = AutoStrategy::builder()
379 .with_access_key("CSAKtestKeyId.testKeySecret")
380 .detect();
381
382 if let Some(val) = saved_crn {
384 std::env::set_var("CS_WORKSPACE_CRN", val);
385 }
386
387 assert!(matches!(result, Err(AuthError::MissingWorkspaceCrn)));
388 }
389
390 #[test]
391 fn invalid_crn_env_var_returns_invalid_crn() {
392 let saved_crn = std::env::var("CS_WORKSPACE_CRN").ok();
393 std::env::set_var("CS_WORKSPACE_CRN", "not-a-crn");
394
395 let result = AutoStrategy::builder()
396 .with_access_key("CSAKtestKeyId.testKeySecret")
397 .detect();
398
399 match saved_crn {
401 Some(val) => std::env::set_var("CS_WORKSPACE_CRN", val),
402 None => std::env::remove_var("CS_WORKSPACE_CRN"),
403 }
404
405 assert!(matches!(result, Err(AuthError::InvalidCrn(_))));
406 }
407
408 #[test]
409 fn invalid_explicit_access_key_returns_invalid_access_key() {
410 let result = AutoStrategy::builder()
411 .with_access_key("not-a-valid-key")
412 .with_workspace_crn(valid_crn())
413 .detect();
414
415 assert!(matches!(result, Err(AuthError::InvalidAccessKey(_))));
416 }
417 }
418}