stack_auth/
auto_strategy.rs1use cts_common::Crn;
2
3use crate::access_key_strategy::AccessKeyStrategy;
4use crate::oauth_strategy::OAuthStrategy;
5use stack_profile::ProfileStore;
6
7use crate::{AuthError, AuthStrategy, ServiceToken, Token};
8
9pub enum AutoStrategy {
34 AccessKey(AccessKeyStrategy),
36 OAuth(OAuthStrategy),
38}
39
40impl AutoStrategy {
41 pub fn new() -> Result<Self, AuthError> {
45 let access_key = std::env::var("CS_CLIENT_ACCESS_KEY").ok();
46 let crn = std::env::var("CS_WORKSPACE_CRN").ok();
47 let store = Some(ProfileStore::resolve(None)?);
48 Self::detect(access_key, crn, store)
49 }
50
51 fn detect(
56 access_key: Option<String>,
57 crn: Option<String>,
58 store: Option<ProfileStore>,
59 ) -> Result<Self, AuthError> {
60 if let Some(access_key) = access_key {
62 let crn_str = crn.ok_or(AuthError::NotAuthenticated)?;
63 let crn: Crn = crn_str.parse().map_err(AuthError::InvalidCrn)?;
64 let key: crate::AccessKey = access_key.parse()?;
65 let strategy = AccessKeyStrategy::new_with_crn(crn, key)?;
66 return Ok(Self::AccessKey(strategy));
67 }
68
69 if let Some(store) = store {
71 if store.exists_profile::<Token>() {
72 let strategy = OAuthStrategy::with_profile(store).build()?;
73 return Ok(Self::OAuth(strategy));
74 }
75 }
76
77 Err(AuthError::NotAuthenticated)
79 }
80}
81
82impl AutoStrategy {
83 pub fn workspace_crn(&self) -> Result<Crn, AuthError> {
89 match self {
90 AutoStrategy::AccessKey(inner) => inner
91 .workspace_crn()
92 .cloned()
93 .ok_or(AuthError::NotAuthenticated),
94 AutoStrategy::OAuth(inner) => inner
95 .workspace_crn()
96 .cloned()
97 .ok_or(AuthError::NotAuthenticated),
98 }
99 }
100}
101
102impl AuthStrategy for &AutoStrategy {
103 async fn get_token(self) -> Result<ServiceToken, AuthError> {
104 match self {
105 AutoStrategy::AccessKey(inner) => inner.get_token().await,
106 AutoStrategy::OAuth(inner) => inner.get_token().await,
107 }
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114 use crate::{SecretToken, Token};
115 use std::time::{SystemTime, UNIX_EPOCH};
116
117 const VALID_CRN: &str = "crn:ap-southeast-2.aws:ZVATKW3VHMFG27DY";
118
119 fn make_oauth_token() -> Token {
120 let now = SystemTime::now()
121 .duration_since(UNIX_EPOCH)
122 .unwrap()
123 .as_secs();
124
125 let claims = serde_json::json!({
126 "iss": "https://cts.example.com/",
127 "sub": "CS|test-user",
128 "aud": "test-audience",
129 "iat": now,
130 "exp": now + 3600,
131 "workspace": "ZVATKW3VHMFG27DY",
132 "scope": "",
133 });
134
135 let key = jsonwebtoken::EncodingKey::from_secret(b"test-secret");
136 let jwt = jsonwebtoken::encode(&jsonwebtoken::Header::default(), &claims, &key).unwrap();
137
138 Token {
139 access_token: SecretToken::new(jwt),
140 token_type: "Bearer".to_string(),
141 expires_at: now + 3600,
142 refresh_token: Some(SecretToken::new("test-refresh-token")),
143 region: Some("ap-southeast-2.aws".to_string()),
144 client_id: Some("test-client-id".to_string()),
145 device_instance_id: None,
146 }
147 }
148
149 fn write_token_store(dir: &std::path::Path) -> ProfileStore {
150 let store = ProfileStore::new(dir);
151 store.save_profile(&make_oauth_token()).unwrap();
152 store
153 }
154
155 #[test]
156 fn access_key_with_valid_crn() {
157 let result = AutoStrategy::detect(
158 Some("CSAKtestKeyId.testKeySecret".into()),
159 Some(VALID_CRN.into()),
160 None,
161 );
162
163 assert!(result.is_ok());
164 assert!(matches!(result.unwrap(), AutoStrategy::AccessKey(_)));
165 }
166
167 #[test]
168 fn access_key_without_crn_returns_not_authenticated() {
169 let result = AutoStrategy::detect(Some("CSAKtestKeyId.testKeySecret".into()), None, None);
170
171 assert!(matches!(result, Err(AuthError::NotAuthenticated)));
172 }
173
174 #[test]
175 fn invalid_access_key_format_returns_invalid_access_key() {
176 let result =
177 AutoStrategy::detect(Some("not-a-valid-key".into()), Some(VALID_CRN.into()), None);
178
179 assert!(matches!(result, Err(AuthError::InvalidAccessKey(_))));
180 }
181
182 #[test]
183 fn access_key_with_invalid_crn_returns_invalid_crn() {
184 let result = AutoStrategy::detect(
185 Some("CSAKtestKeyId.testKeySecret".into()),
186 Some("not-a-crn".into()),
187 None,
188 );
189
190 assert!(matches!(result, Err(AuthError::InvalidCrn(_))));
191 }
192
193 #[test]
194 fn oauth_store_with_valid_token() {
195 let dir = tempfile::tempdir().unwrap();
196 let store = write_token_store(dir.path());
197
198 let result = AutoStrategy::detect(None, None, Some(store));
199
200 assert!(result.is_ok());
201 assert!(matches!(result.unwrap(), AutoStrategy::OAuth(_)));
202 }
203
204 #[test]
205 fn oauth_store_without_token_file_returns_not_authenticated() {
206 let dir = tempfile::tempdir().unwrap();
207 let store = ProfileStore::new(dir.path());
208
209 let result = AutoStrategy::detect(None, None, Some(store));
210
211 assert!(matches!(result, Err(AuthError::NotAuthenticated)));
212 }
213
214 #[test]
215 fn no_credentials_returns_not_authenticated() {
216 let result = AutoStrategy::detect(None, None, None);
217
218 assert!(matches!(result, Err(AuthError::NotAuthenticated)));
219 }
220
221 #[test]
222 fn access_key_takes_priority_over_oauth_store() {
223 let dir = tempfile::tempdir().unwrap();
224 let store = write_token_store(dir.path());
225
226 let result = AutoStrategy::detect(
227 Some("CSAKtestKeyId.testKeySecret".into()),
228 Some(VALID_CRN.into()),
229 Some(store),
230 );
231
232 assert!(result.is_ok());
233 assert!(matches!(result.unwrap(), AutoStrategy::AccessKey(_)));
234 }
235}