feat(auth): AWS sourced external accounts#4790
feat(auth): AWS sourced external accounts#4790alvarowolfx wants to merge 2 commits intogoogleapis:mainfrom
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #4790 +/- ##
==========================================
- Coverage 95.03% 94.90% -0.14%
==========================================
Files 199 200 +1
Lines 7772 7961 +189
==========================================
+ Hits 7386 7555 +169
- Misses 386 406 +20 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
Successfully tested using our internal go/3pi-sdk-testing process. use google_cloud_auth::credentials::Builder;
use google_cloud_storage::client::StorageControl;
const BUCKET_ID: &str = "REDACTED";
#[tokio::main]
async fn main() -> anyhow::Result<()> {
println!("Building Credentials...");
// Will use ADC with the environment variable set
let creds = Builder::default().build()?;
println!("Credentials built: {:?}", creds);
let client = StorageControl::builder()
.with_credentials(creds)
.build()
.await?;
let bucket = client
.get_bucket()
.set_name(format!("projects/_/buckets/{BUCKET_ID}"))
.send()
.await?;
println!("successfully obtained bucket metadata {bucket:?}");
Ok(())
}output: |
coryan
left a comment
There was a problem hiding this comment.
Pretty good, a few nits and suggestions. Try to refactor the code making HTTP requests so the error handling is all in one place.
| value: String, | ||
| } | ||
|
|
||
| const MSG: &str = "failed to fetch AWS credentials for subject token"; |
There was a problem hiding this comment.
nit: move above with the other constants?
| use serde::{Deserialize, Serialize}; | ||
| use sha2::{Digest, Sha256}; | ||
| use std::collections::BTreeMap; | ||
|
|
There was a problem hiding this comment.
nit: remove the blank line.
| let sts_url = if sts_url.starts_with("http") { | ||
| sts_url | ||
| } else { | ||
| format!("https://{sts_url}") | ||
| }; | ||
| let url = url::Url::parse(&sts_url) |
There was a problem hiding this comment.
If you are already going to use url::Url::parse() shouldn't that be able to handle the missing scheme?
| fn parse_region_from_zone(zone: &str) -> Option<String> { | ||
| let zone = zone.trim(); | ||
| if zone.is_empty() { | ||
| return None; | ||
| } | ||
| if let Some(last_char) = zone.chars().last() { | ||
| if last_char.is_ascii_alphabetic() && zone.len() > 1 { | ||
| let potential_region = &zone[..zone.len() - 1]; | ||
| if potential_region | ||
| .chars() | ||
| .last() | ||
| .is_some_and(|c| c.is_ascii_digit()) | ||
| { | ||
| return Some(potential_region.to_string()); | ||
| } | ||
| } | ||
| } | ||
| Some(zone.to_string()) | ||
| } |
There was a problem hiding this comment.
Consider:
| fn parse_region_from_zone(zone: &str) -> Option<String> { | |
| let zone = zone.trim(); | |
| if zone.is_empty() { | |
| return None; | |
| } | |
| if let Some(last_char) = zone.chars().last() { | |
| if last_char.is_ascii_alphabetic() && zone.len() > 1 { | |
| let potential_region = &zone[..zone.len() - 1]; | |
| if potential_region | |
| .chars() | |
| .last() | |
| .is_some_and(|c| c.is_ascii_digit()) | |
| { | |
| return Some(potential_region.to_string()); | |
| } | |
| } | |
| } | |
| Some(zone.to_string()) | |
| } | |
| fn parse_region_from_zone(zone: &str) -> Option<&str> { | |
| let zone = zone.trim(); | |
| let parts: Vec<&str> = id.split('-').collect(); | |
| match &parts[..] { | |
| [geo, region, letter] if !letter.is_empty() && !geo.is_empty() => Some(region), | |
| _ => None, | |
| } | |
| } |
If you push me, we can save the memory allocation using:
fn parse_region_from_zone(zone: &str) -> Option<&str> {
let zone = zone.trim();
let parts = id.split('-');
match (parts.next(), parts.next(), parts.next(), parts.next()) {
(Some(geo), Some(region), Some(z), None] if !z.is_empty() && !geo.is_empty() => Some(region),
_ => None,
}
}
There was a problem hiding this comment.
Ah, I see in the tests that AWS zones can have four parts: just add a branch to the match case.
| if !response.status().is_success() { | ||
| return Err(errors::from_http_response( | ||
| response, | ||
| "could not resolve AWS role name", | ||
| ) | ||
| .await); | ||
| } |
There was a problem hiding this comment.
This block repeats at least 3 times. Time to refactor to a function?
| if !response.status().is_success() { | ||
| return Err( | ||
| errors::from_http_response(response, "could not resolve AWS region").await, | ||
| ); | ||
| } |
| if !response.status().is_success() { | ||
| return Err(errors::from_http_response( | ||
| response, | ||
| "could not resolve AWS credentials", | ||
| ) | ||
| .await); | ||
| } |
| hmac.workspace = true | ||
| hex = { workspace = true, features = ["std"] } | ||
| url.workspace = true | ||
| sha2.workspace = true |
There was a problem hiding this comment.
nit: insert these in alphabetical order.
| @@ -0,0 +1,770 @@ | |||
| // Copyright 2025 Google LLC | |||
Towards #3644