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