Skip to content

feat(auth): add external account url sourced credentials #2217

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

Open
wants to merge 21 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
57b4264
feat(auth): add external account url sourced credentials
alvarowolfx May 19, 2025
f11ee98
fix: license header year
alvarowolfx May 19, 2025
b2ba552
feat: add external account builder
alvarowolfx May 20, 2025
055990d
Merge branch 'main' into feat-auth-url-sourced-creds
alvarowolfx May 20, 2025
84b81e0
fix: update CredentialsProvider impl to match cacheable resource trait
alvarowolfx May 20, 2025
42e989f
feat: change CredentialSource to an enum
alvarowolfx May 20, 2025
fca8675
fix: handle err for url sourced creds
alvarowolfx May 21, 2025
ad56efd
fix: lint issues
alvarowolfx May 21, 2025
041003b
test: move tests closer to the external account mod
alvarowolfx May 21, 2025
39c0e18
fix: get rid of Box<dyn SubjectTokenProvider>
alvarowolfx May 21, 2025
06534a4
docs: improve builder docs
alvarowolfx May 21, 2025
3b84492
fix: builder example should be compilable by rustc
alvarowolfx May 21, 2025
c859658
fix: lint issues on builder docs
alvarowolfx May 22, 2025
e8d2371
fix: handle scopes properly for external account
alvarowolfx May 23, 2025
99c140b
test: add integration tests for workload identity url sourced
alvarowolfx May 23, 2025
6917c63
fix: typo and toml formatting
alvarowolfx May 23, 2025
e059453
fix: lint auth readme
alvarowolfx May 23, 2025
9b434fe
chore: rename UrlSorucedCredentials to SubjectTokenProvider
alvarowolfx May 23, 2025
80afb38
fix: lint issues
alvarowolfx May 23, 2025
1d8ea19
feat: add option to override scopes
alvarowolfx May 23, 2025
e4601c2
fix: change visibility on internal structs
alvarowolfx May 23, 2025
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
5 changes: 5 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 11 additions & 5 deletions src/auth/integration-tests/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,22 @@ edition.workspace = true
publish = false

[features]
run-integration-tests = []
run-integration-tests = []
run-byoid-integration-tests = []

[dependencies]
scoped-env.workspace = true
tempfile.workspace = true
http.workspace = true
serde_json.workspace = true
httptest.workspace = true
# Local dependencies
auth = { path = "../../../src/auth", package = "google-cloud-auth" }
gax = { path = "../../../src/gax", package = "google-cloud-gax" }
language = { path = "../../../src/generated/cloud/language/v2", package = "google-cloud-language-v2" }
secretmanager = { path = "../../../src/generated/cloud/secretmanager/v1", package = "google-cloud-secretmanager-v1" }
auth = { path = "../../../src/auth", package = "google-cloud-auth" }
gax = { path = "../../../src/gax", package = "google-cloud-gax" }
language = { path = "../../../src/generated/cloud/language/v2", package = "google-cloud-language-v2" }
secretmanager = { path = "../../../src/generated/cloud/secretmanager/v1", package = "google-cloud-secretmanager-v1" }
iamcredentials = { path = "../../../src/generated/iam/credentials/v1", package = "google-cloud-iam-credentials-v1" }
bigquery = { path = "../../../src/generated/cloud/bigquery/v2", package = "google-cloud-bigquery-v2" }

[dev-dependencies]
serial_test = { workspace = true }
Expand Down
15 changes: 15 additions & 0 deletions src/auth/integration-tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,21 @@ env GOOGLE_CLOUD_PROJECT=rust-auth-testing \
cargo test --features run-integration-tests -p auth-integration-tests
```

### Workload Identity integration tests

Those integration tests requires more complex set up to run, like running from
an Azure/AWS VM and having Workload Identity Pools set up. For now we are only
run those tests locally and under a feature (`run-byoid-integration-tests`).
Some extra environment variables with the workload identity pool configuration
are required to run the tests.

```sh
env GOOGLE_CLOUD_PROJECT=cloud-sdk-auth-test-project \
env GOOGLE_WORKLOAD_IDENTITY_SERVICE_ACCOUNT=<path-to-service-account> \
env GOOGLE_WORKLOAD_IDENTITY_OIDC_AUDIENCE=//iam.googleapis.com/projects/<PROJECT_ID>/locations/global/workloadIdentityPools/<WORKLOAD_IDENTITY_POOL_ID>/providers/<WORKLOAD_IDENTITY_PROVIDER_ID> \
cargo test run_workload_ --features run-integration-tests --features run-byoid-integration-tests -p auth-integration-tests
```

#### Rotating the service account key

Service account keys expire after 90 days, due to our org policy.
Expand Down
120 changes: 120 additions & 0 deletions src/auth/integration-tests/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,17 @@
use auth::credentials::{
Builder as AccessTokenCredentialBuilder,
api_key_credentials::Builder as ApiKeyCredentialsBuilder,
external_account::Builder as ExternalAccountCredentialsBuilder,
};
use bigquery::client::DatasetService;
use gax::error::Error;
use httptest::{Expectation, Server, matchers::*, responders::*};
use iamcredentials::client::IAMCredentials;
use language::client::LanguageService;
use language::model::Document;
use scoped_env::ScopedEnv;
use secretmanager::client::SecretManagerService;
use std::collections::HashMap;

pub type Result<T> = std::result::Result<T, gax::error::Error>;

Expand Down Expand Up @@ -120,3 +125,118 @@ pub async fn api_key() -> Result<()> {

Ok(())
}

pub async fn workload_identity_provider_url_sourced() -> Result<()> {
let project = std::env::var("GOOGLE_CLOUD_PROJECT").expect("GOOGLE_CLOUD_PROJECT not set");
let audience = get_oidc_audience();
let service_account = get_byoid_service_account();
let service_account_data =
serde_json::from_value::<HashMap<String, String>>(service_account.clone())
.expect("failed to read service account data as map");
let client_email = service_account_data
.get("client_email")
.expect("missing client_email");

let id_token =
generate_id_token(audience.clone(), client_email.to_string(), service_account).await?;

let source_token_response_body = serde_json::json!({
"id_token": id_token,
})
.to_string();

let server = Server::run();
server.expect(
Expectation::matching(all_of![
request::method_path("GET", "/source_token"),
request::headers(contains(("metadata", "True",))),
])
.respond_with(status_code(200).body(source_token_response_body)),
);

let contents = serde_json::json!({
"type": "external_account",
"audience": audience,
"subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
"token_url": "https://sts.googleapis.com/v1/token",
"credential_source": {
"url": server.url("/source_token").to_string(),
"headers": {
"Metadata": "True"
},
"format": {
"type": "json",
"subject_token_field_name": "id_token"
}
}
})
.to_string();

// Create external account with Url sourced creds
let creds =
ExternalAccountCredentialsBuilder::new(serde_json::from_str(contents.as_str()).unwrap())
.build()
.unwrap();

// Construct a BigQuery client using the credentials.
// Using BigQuery as it is enabled by default.
let client = DatasetService::builder()
.with_credentials(creds)
.build()
.await?;

// Make a request using the external account credentials
client
.list_datasets()
.set_project_id(project)
.send()
.await?;

Ok(())
}

/// Generates a Google ID token using the iamcredentials generateIdToken API.
/// https://cloud.google.com/iam/docs/creating-short-lived-service-account-credentials#sa-credentials-oidc
async fn generate_id_token(
audience: String,
client_email: String,
service_account: serde_json::Value,
) -> Result<String> {
let creds = AccessTokenCredentialBuilder::new(service_account.clone())
.with_scopes(["https://www.googleapis.com/auth/cloud-platform"])
.build()
.expect("failed to setup service account credentials for IAM");

let client = IAMCredentials::builder()
.with_credentials(creds)
.build()
.await
.expect("failed to setup IAM client");

let res = client
.generate_id_token()
.set_audience(audience)
.set_include_email(true)
.set_name(format!("projects/-/serviceAccounts/{client_email}"))
.send()
.await?;

Ok(res.token)
}

fn get_oidc_audience() -> String {
std::env::var("GOOGLE_WORKLOAD_IDENTITY_OIDC_AUDIENCE")
.expect("GOOGLE_WORKLOAD_IDENTITY_OIDC_AUDIENCE not set")
}

fn get_byoid_service_account() -> serde_json::Value {
let path = std::env::var("GOOGLE_WORKLOAD_IDENTITY_CREDENTIALS")
.expect("GOOGLE_WORKLOAD_IDENTITY_CREDENTIALS not set");

let service_account_content =
std::fs::read_to_string(path).expect("unable to read service account");
let service_account: serde_json::Value = serde_json::from_str(service_account_content.as_str())
.expect("unable to parse service account");

service_account
}
6 changes: 6 additions & 0 deletions src/auth/integration-tests/tests/driver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,10 @@ mod driver {
async fn run_api_key() -> Result<()> {
auth_integration_tests::api_key().await
}

#[cfg(all(test, feature = "run-byoid-integration-tests"))]
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn run_workload_identity_provider_url_sourced() -> Result<()> {
auth_integration_tests::workload_identity_provider_url_sourced().await
}
}
9 changes: 9 additions & 0 deletions src/auth/src/credentials.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ pub mod mds;
pub mod service_account;
pub mod user_account;

pub mod external_account;
Copy link
Contributor

Choose a reason for hiding this comment

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

Lets keep this private until we figure out twosigma's requirements regarding openssl and custom request client.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The problem is that by doing that, I can't use the module on the auth integration tests.

pub(crate) mod external_account_sources;

pub(crate) mod internal;

use crate::Result;
Expand Down Expand Up @@ -534,6 +537,12 @@ fn build_credentials(
|b: service_account::Builder, s: Vec<String>| b
.with_access_specifier(service_account::AccessSpecifier::from_scopes(s))
),
"external_account" => config_builder!(
external_account::Builder::new(json),
quota_project_id,
scopes,
|b: external_account::Builder, s: Vec<String>| b.with_scopes(s)
),
_ => Err(errors::non_retryable_from_str(format!(
"Invalid or unsupported credentials type found in JSON: {cred_type}"
))),
Expand Down
Loading
Loading