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

auth: Support URL type external account source #239

Merged
merged 5 commits into from
Mar 12, 2024
Merged
Show file tree
Hide file tree
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
22 changes: 11 additions & 11 deletions foundation/auth/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,40 +4,40 @@ version = "0.13.1"
authors = ["yoshidan <[email protected]>"]
edition = "2021"
repository = "https://github.com/yoshidan/google-cloud-rust/tree/main/foundation/auth"
keywords = ["gcp","auth","googleapis","google-cloud-rust"]
keywords = ["gcp", "auth", "googleapis", "google-cloud-rust"]
license = "MIT"
readme = "README.md"
description = "Google Cloud Platform server application authentication library."

[dependencies]
tracing = "0.1"
reqwest = { version="0.11", features = ["json"], default-features = false }
reqwest = { version = "0.11", features = ["json"], default-features = false }
serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0" }
serde_json = { version = "1.0" }
jsonwebtoken = { version = "9.2.0" }
thiserror = "1.0"
async-trait = "0.1"
home = "0.5"
urlencoding = "2.1"
tokio = { version = "1.32", features = ["fs"]}
tokio = { version = "1.32", features = ["fs"] }
google-cloud-metadata = { version = "0.4.0", path = "../metadata" }
google-cloud-token = { version = "0.1.1", path = "../token" }
base64 = "0.21"
time = "0.3"

url = { version="2.4", optional = true }
path-clean = { version="1.0", optional = true }
sha2 = {version = "0.10", optional = true}
percent-encoding = { version="2.3", optional = true }
url = { version = "2.4", optional = true }
path-clean = { version = "1.0", optional = true }
sha2 = { version = "0.10", optional = true }
percent-encoding = { version = "2.3", optional = true }
hmac = { version = "0.12", optional = true }
hex = { version = "0.4", optional = true }

[dev-dependencies]
tokio = { version = "1.32", features = ["test-util", "rt-multi-thread", "macros"]}
tracing-subscriber = {version="0.3", features=["env-filter","std"]}
tokio = { version = "1.32", features = ["test-util", "rt-multi-thread", "macros"] }
tracing-subscriber = { version = "0.3", features = ["env-filter", "std"] }
ctor = "0.1"
tempfile = "3.8.0"
temp-env = {version="0.3.6", features = ["async_closure",]}
temp-env = { version = "0.3.6", features = ["async_closure"] }

[features]
default = ["default-tls"]
Expand Down
12 changes: 12 additions & 0 deletions foundation/auth/src/token_source/external_account_source/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ pub enum Error {
#[error("Unsupported Subject Token Source")]
UnsupportedSubjectTokenSource,

#[error("Unsupported Format Type")]
UnsupportedFormatType,

#[error(transparent)]
HttpError(#[from] reqwest::Error),

Expand Down Expand Up @@ -47,6 +50,15 @@ pub enum Error {
#[error("Missing Subject Token Type")]
MissingSubjectTokenType,

#[error("Missing Headers")]
MissingHeaders,

#[error("Missing Format")]
MissingFormat,

#[error("Missing Subject Token Field Name")]
MissingSubjectTokenFieldName,

#[error(transparent)]
InvalidHashLength(#[from] sha2::digest::InvalidLength),

Expand Down
10 changes: 7 additions & 3 deletions foundation/auth/src/token_source/external_account_source/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use crate::token_source::{default_http_client, InternalToken, TokenSource};
mod aws_subject_token_source;
pub mod error;
mod subject_token_source;
mod url_subject_token_source;

pub struct ExternalAccountTokenSource {
source: CredentialSource,
Expand Down Expand Up @@ -95,16 +96,19 @@ impl TokenSource for ExternalAccountTokenSource {
async fn subject_token_source(
audience: Option<String>,
source: CredentialSource,
) -> Result<impl SubjectTokenSource, Error> {
) -> Result<Box<dyn SubjectTokenSource>, Error> {
Copy link
Owner

Choose a reason for hiding this comment

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

Is there any particular reason for changing from impl SubjectTokenSource to Box<dyn SubjectTokenSource>?
There seems to be no need for the change since the function return is limited to AWSSubjectTokenSource and UrlSubjectTokenSource.

(I was looking at this PR and noticed that I can remove the async_trait of the SubjectTokenSource in rust 1.75. Thank you very much.)

Copy link
Contributor Author

@moricho moricho Mar 10, 2024

Choose a reason for hiding this comment

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

@yoshidan
impl SubjectTokenSource can return either AWSSubjectTokenSource or UrlSubjectTokenSource. However, if the type returned changes depending on the CredentialSource within the same function, it becomes impossible to specify the type at compile time, so dynamic dispatch may be required
(I might be misunderstanding something 🙏 )

Actually, the following error occurs when I specify impl SubjectTokenSource at the return position

error[E0308]: mismatched types
   --> foundation/auth/src/token_source/external_account_source/mod.rs:109:12
    |
109 |         Ok(ts)
    |         -- ^^ expected `AWSSubjectTokenSource`, found `UrlSubjectTokenSource`
    |         |
    |         arguments to this enum variant are incorrect
    |

Copy link
Owner

Choose a reason for hiding this comment

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

Thanks. you are right.

let environment_id = &source.environment_id.unwrap_or_empty();
if environment_id.len() > 3 && environment_id.starts_with("aws") {
if environment_id != "aws1" {
return Err(Error::UnsupportedAWSVersion(environment_id.clone()));
}
let ts = aws_subject_token_source::AWSSubjectTokenSource::new(audience, source).await?;
Ok(ts)
Ok(Box::new(ts))
} else if let Some(_) = source.url {
let ts = url_subject_token_source::UrlSubjectTokenSource::new(source).await?;
Ok(Box::new(ts))
} else {
//TODO support file, url and executable
// TODO: support file and executable type
Err(Error::UnsupportedSubjectTokenSource)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
use async_trait::async_trait;
use serde_json::Value;
use std::collections::HashMap;
use url::Url;

use crate::credentials::{CredentialSource, Format};
use crate::token_source::default_http_client;
use crate::token_source::external_account_source::error::Error;
use crate::token_source::external_account_source::subject_token_source::SubjectTokenSource;

pub struct UrlSubjectTokenSource {
url: Url,
headers: HashMap<String, String>,
format: Format,
}

impl UrlSubjectTokenSource {
pub async fn new(value: CredentialSource) -> Result<Self, Error> {
let url = value.url.ok_or(Error::MissingTokenURL)?;
let url = Url::parse(&url).map_err(Error::URLError)?;
let headers = value.headers.ok_or(Error::MissingHeaders)?;
let format = value.format.ok_or(Error::MissingFormat)?;

Ok(Self { url, headers, format })
}

async fn create_subject_token(&self) -> Result<String, Error> {
let client = default_http_client();
let mut request = client.get(self.url.clone());

for (key, val) in &self.headers {
request = request.header(key, val);
}

let response = request.send().await.map_err(Error::HttpError)?;

if !response.status().is_success() {
return Err(Error::UnexpectedStatusOnGetSessionToken(response.status().as_u16()));
}

let body = response.text_with_charset("utf-8").await?;
let body = body.chars().take(1 << 20).collect::<String>(); // Limiting the response body to 1MB

let format_type = self.format.tp.as_str();
match format_type {
"json" => {
let data: Value = serde_json::from_str(&body).map_err(Error::JsonError)?;
if let Some(token) = data[&self.format.subject_token_field_name].as_str() {
Ok(token.to_string())
} else {
Err(Error::MissingSubjectTokenFieldName)
}
}
"text" | "" => Ok(body),
_ => Err(Error::UnsupportedFormatType),
}
}
}

#[async_trait]
impl SubjectTokenSource for UrlSubjectTokenSource {
async fn subject_token(&self) -> Result<String, Error> {
self.create_subject_token().await
}
}
Loading