Skip to content

Commit 50d6c32

Browse files
Merge pull request #8 from jgowdy/fix/exhaustive-defects-awsenc
Fix awsenc auth and profile handling defects
2 parents e40017f + 1af2159 commit 50d6c32

File tree

18 files changed

+1700
-323
lines changed

18 files changed

+1700
-323
lines changed

Cargo.lock

Lines changed: 93 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

awsenc-cli/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ workspace = true
1313

1414
[dependencies]
1515
awsenc-core = { path = "../awsenc-core" }
16+
enclaveapp-core = { workspace = true }
1617
enclaveapp-app-storage = { workspace = true }
1718
enclaveapp-wsl = { workspace = true }
1819
clap = { version = "4", features = ["derive", "env", "string"] }
@@ -36,4 +37,5 @@ winresource = "0.1"
3637
assert_cmd = "2"
3738
predicates = "3"
3839
tempfile = "3"
40+
toml = "0.8"
3941
enclaveapp-app-storage = { workspace = true, features = ["mock"] }

awsenc-cli/src/auth.rs

Lines changed: 87 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use std::io::{IsTerminal, Read};
22
use std::time::Duration;
33

44
use chrono::Utc;
5+
use tracing::warn;
56
use zeroize::Zeroizing;
67

78
use awsenc_core::cache::{
@@ -53,8 +54,15 @@ pub async fn run_auth(
5354
};
5455

5556
let creds = obtain_credentials(&okta, &session_token, &resolved).await?;
57+
let okta_session = match okta.create_session(&session_token).await {
58+
Ok(session) => Some(session),
59+
Err(err) => {
60+
warn!("failed to create reusable Okta session: {err}");
61+
None
62+
}
63+
};
5664

57-
encrypt_and_cache(profile, storage, &creds, &session_token)?;
65+
encrypt_and_cache(profile, storage, &creds, okta_session.as_ref())?;
5866
usage::record_usage(profile);
5967

6068
let remaining = creds
@@ -104,6 +112,7 @@ async fn obtain_credentials(
104112
session_token: &Zeroizing<String>,
105113
resolved: &config::ResolvedConfig,
106114
) -> Result<awsenc_core::credential::AwsCredentials> {
115+
ensure_supported_secondary_role(resolved)?;
107116
eprintln!("Getting SAML assertion...");
108117
let saml_assertion = okta
109118
.get_saml_assertion(session_token, &resolved.okta_application)
@@ -136,33 +145,45 @@ async fn obtain_credentials(
136145
Ok(creds)
137146
}
138147

148+
fn ensure_supported_secondary_role(resolved: &config::ResolvedConfig) -> Result<()> {
149+
if let Some(role) = resolved.secondary_role.as_deref() {
150+
return Err(format!(
151+
"secondary_role '{role}' is configured but chained role assumption is not supported yet"
152+
)
153+
.into());
154+
}
155+
Ok(())
156+
}
157+
139158
#[allow(clippy::cast_sign_loss)]
140159
fn encrypt_and_cache(
141160
profile: &str,
142161
storage: &dyn EncryptionStorage,
143162
creds: &awsenc_core::credential::AwsCredentials,
144-
session_token: &Zeroizing<String>,
163+
okta_session: Option<&OktaSession>,
145164
) -> Result<()> {
146165
let creds_json = serde_json::to_vec(creds)?;
147166
let aws_ciphertext = storage.encrypt(&creds_json)?;
148167

149-
let okta_session = OktaSession {
150-
session_id: session_token.as_str().to_owned(),
151-
expiration: Utc::now() + chrono::Duration::hours(2),
152-
};
153-
let okta_json = serde_json::to_vec(&okta_session)?;
154-
let okta_ciphertext = storage.encrypt(&okta_json)?;
155-
156168
let cache_file = CacheFile {
157169
header: CacheHeader {
158170
magic: MAGIC,
159171
version: FORMAT_VERSION,
160-
flags: FLAG_HAS_OKTA_SESSION,
172+
flags: if okta_session.is_some() {
173+
FLAG_HAS_OKTA_SESSION
174+
} else {
175+
0
176+
},
161177
credential_expiration: creds.expiration.timestamp() as u64,
162-
okta_session_expiration: okta_session.expiration.timestamp() as u64,
178+
okta_session_expiration: okta_session
179+
.map_or(0, |session| session.expiration.timestamp() as u64),
163180
},
164181
aws_ciphertext,
165-
okta_session_ciphertext: Some(okta_ciphertext),
182+
okta_session_ciphertext: okta_session
183+
.map(serde_json::to_vec)
184+
.transpose()?
185+
.map(|json| storage.encrypt(&json))
186+
.transpose()?,
166187
};
167188

168189
cache::write_cache(profile, &cache_file)?;
@@ -290,17 +311,16 @@ mod tests {
290311
session_token: Zeroizing::new("sessiontoken456".to_string()),
291312
expiration: Utc::now() + chrono::Duration::hours(1),
292313
};
293-
let session_token = Zeroizing::new("okta-session-token".to_string());
314+
let okta_session = OktaSession {
315+
session_id: "okta-session-id".to_string(),
316+
expiration: Utc::now() + chrono::Duration::hours(2),
317+
};
294318

295319
// Test encrypt_and_cache by verifying it constructs correct structures
296320
// without relying on file I/O (which depends on HOME env var)
297321
let creds_json = serde_json::to_vec(&creds).unwrap();
298322
let aws_ciphertext = storage.encrypt(&creds_json).unwrap();
299323

300-
let okta_session = OktaSession {
301-
session_id: session_token.as_str().to_owned(),
302-
expiration: Utc::now() + chrono::Duration::hours(2),
303-
};
304324
let okta_json = serde_json::to_vec(&okta_session).unwrap();
305325
let okta_ciphertext = storage.encrypt(&okta_json).unwrap();
306326

@@ -313,7 +333,7 @@ mod tests {
313333
// Verify Okta session can be decrypted
314334
let okta_plaintext = storage.decrypt(&okta_ciphertext).unwrap();
315335
let recovered_session: OktaSession = serde_json::from_slice(&okta_plaintext).unwrap();
316-
assert_eq!(recovered_session.session_id, "okta-session-token");
336+
assert_eq!(recovered_session.session_id, "okta-session-id");
317337

318338
// Verify CacheFile structure
319339
#[allow(clippy::cast_sign_loss)]
@@ -333,6 +353,36 @@ mod tests {
333353
assert!(cache_file.header.has_okta_session());
334354
}
335355

356+
#[test]
357+
fn encrypt_and_cache_without_okta_session_clears_session_flag() {
358+
use enclaveapp_app_storage::mock::MockEncryptionStorage as MockStorage;
359+
360+
let storage = MockStorage::new();
361+
let creds = awsenc_core::credential::AwsCredentials {
362+
access_key_id: "AKIATEST".to_string(),
363+
secret_access_key: Zeroizing::new("secretkey123".to_string()),
364+
session_token: Zeroizing::new("sessiontoken456".to_string()),
365+
expiration: Utc::now() + chrono::Duration::hours(1),
366+
};
367+
368+
let creds_json = serde_json::to_vec(&creds).unwrap();
369+
let aws_ciphertext = storage.encrypt(&creds_json).unwrap();
370+
let cache_file = CacheFile {
371+
header: CacheHeader {
372+
magic: MAGIC,
373+
version: FORMAT_VERSION,
374+
flags: 0,
375+
credential_expiration: creds.expiration.timestamp() as u64,
376+
okta_session_expiration: 0,
377+
},
378+
aws_ciphertext,
379+
okta_session_ciphertext: None,
380+
};
381+
382+
assert_eq!(cache_file.header.flags, 0);
383+
assert!(!cache_file.header.has_okta_session());
384+
}
385+
336386
#[test]
337387
fn resolved_profile_positional_preferred() {
338388
let args = AuthArgs {
@@ -358,4 +408,23 @@ mod tests {
358408
let args = default_auth_args();
359409
assert_eq!(args.resolved_profile(), None);
360410
}
411+
412+
#[test]
413+
fn secondary_role_configuration_fails_fast() {
414+
let resolved = config::ResolvedConfig {
415+
okta_organization: "org.okta.com".into(),
416+
okta_user: "user@example.com".into(),
417+
okta_application: "https://org.okta.com/app".into(),
418+
okta_role: "arn:aws:iam::123:role/Primary".into(),
419+
okta_factor: "push".into(),
420+
okta_duration: 3600,
421+
biometric: false,
422+
refresh_window_seconds: 600,
423+
secondary_role: Some("arn:aws:iam::456:role/Secondary".into()),
424+
region: None,
425+
};
426+
427+
let err = ensure_supported_secondary_role(&resolved).unwrap_err();
428+
assert!(err.to_string().contains("secondary_role"));
429+
}
361430
}

0 commit comments

Comments
 (0)