-
Notifications
You must be signed in to change notification settings - Fork 115
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
support url-sourced external account #222
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,6 +11,7 @@ use http::Uri; | |
use hyper::client::connect::Connection; | ||
use hyper::header; | ||
use serde::{Deserialize, Serialize}; | ||
use std::collections::HashMap; | ||
use std::error::Error as StdError; | ||
use tokio::io::{AsyncRead, AsyncWrite}; | ||
use tower_service::Service; | ||
|
@@ -44,14 +45,42 @@ pub struct ExternalAccountSecret { | |
pub enum CredentialSource { | ||
/// file-sourced credentials | ||
File { | ||
/// file | ||
/// File name of a file containing a subject token. | ||
file: String, | ||
}, | ||
// TODO: Microsoft Azure and URL-sourced credentials | ||
|
||
//// [Microsoft Azure and URL-sourced | ||
///credentials](https://google.aip.dev/auth/4117#determining-the-subject-token-in-microsoft-azure-and-url-sourced-credentials) | ||
Url { | ||
/// This defines the local metadata server to retrieve the external credentials from. For | ||
/// Azure, this should be the Azure Instance Metadata Service (IMDS) URL used to retrieve | ||
/// the Azure AD access token. | ||
url: String, | ||
/// This defines the headers to append to the GET request to credential_source.url. | ||
headers: Option<HashMap<String, String>>, | ||
/// See struct documentation. | ||
format: UrlCredentialSourceFormat, | ||
}, | ||
// TODO: executable-sourced credentials | ||
} | ||
|
||
/// ExternalAccountFlow can fetch oauth tokens using an external account secret. | ||
/// JSON schema of URL-sourced credentials' format. | ||
/// This indicates the format of the URL response. This can be either "text" or "json". The default should be "text". | ||
#[derive(Serialize, Deserialize, Debug, Clone)] | ||
#[serde(tag = "type")] | ||
pub enum UrlCredentialSourceFormat { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This enum represents the format of document that the url returns. I avoid There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using modules is another option pub mod credential_source {
pub mod url {
pub enum Format { ... }
}
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. no I think it's fine the way you've done it |
||
/// Response is text. | ||
#[serde(rename = "text")] | ||
Text, | ||
/// Response is JSON. | ||
#[serde(rename = "json")] | ||
Json { | ||
/// Required for JSON URL responses. This indicates the JSON field name where the subject_token should be stored. | ||
subject_token_field_name: String, | ||
}, | ||
} | ||
|
||
/// An ExternalAccountFlow can fetch OAuth tokens using an external account secret. | ||
pub struct ExternalAccountFlow { | ||
pub(crate) secret: ExternalAccountSecret, | ||
} | ||
|
@@ -72,6 +101,39 @@ impl ExternalAccountFlow { | |
{ | ||
let subject_token = match &self.secret.credential_source { | ||
CredentialSource::File { file } => tokio::fs::read_to_string(file).await?, | ||
CredentialSource::Url { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this looks good to me, have you tested it in a real deployment? Unfortunately I don't have the means to do it myself, lacking an appropriate cloud environment. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I confirmed this code works for my case (workload identity federation in github actions). |
||
url, | ||
headers, | ||
format, | ||
} => { | ||
let request = headers | ||
.iter() | ||
.flatten() | ||
.fold(hyper::Request::get(url), |builder, (name, value)| { | ||
builder.header(name, value) | ||
}) | ||
.body(hyper::Body::empty()) | ||
.unwrap(); | ||
|
||
log::debug!("requesting credential from url: {:?}", request); | ||
let (head, body) = hyper_client.request(request).await?.into_parts(); | ||
let body = hyper::body::to_bytes(body).await?; | ||
log::debug!("received response; head: {:?}, body: {:?}", head, body); | ||
|
||
match format { | ||
UrlCredentialSourceFormat::Text => { | ||
String::from_utf8(body.to_vec()).map_err(anyhow::Error::from)? | ||
} | ||
UrlCredentialSourceFormat::Json { | ||
subject_token_field_name, | ||
} => serde_json::from_slice::<HashMap<String, serde_json::Value>>(&body)? | ||
.remove(subject_token_field_name) | ||
.ok_or_else(|| anyhow::format_err!("missing {subject_token_field_name}"))? | ||
.as_str() | ||
.ok_or_else(|| anyhow::format_err!("invalid type"))? | ||
.to_string(), | ||
} | ||
} | ||
}; | ||
|
||
let req = form_urlencoded::Serializer::new(String::new()) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice!