Skip to content
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

Closed
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 65 additions & 3 deletions src/external_account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice!


//// [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 {
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 UrlFormat since it sounds "the format of url". I don't think UrlCredentialSourceFormat is good. Suggestion welcome.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 { ... }
    }
}

Copy link
Owner

Choose a reason for hiding this comment

The 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,
}
Expand All @@ -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 {
Copy link
Owner

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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())
Expand Down