From 6e4126a420276898b2df76cb0034fc53e7e19395 Mon Sep 17 00:00:00 2001 From: Patrick Meredith Date: Tue, 4 Jun 2024 11:52:21 -0400 Subject: [PATCH] RUST-1627: Add automatic token acquisition for GCP (#1097) --- .evergreen/config.yml | 89 +++++++++++++++++++++++++---- .evergreen/run-mongodb-oidc-test.sh | 15 +++-- Cargo.toml | 4 ++ src/client/auth/oidc.rs | 62 +++++++++++++++++--- src/runtime.rs | 6 +- src/test/spec/oidc.rs | 45 +++++++++++++++ 6 files changed, 194 insertions(+), 27 deletions(-) diff --git a/.evergreen/config.yml b/.evergreen/config.yml index d409fd0b9..a0fc7d1be 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -307,6 +307,7 @@ buildvariants: tasks: - testoidc_task_group - testazureoidc_task_group + - testgcpoidc_task_group - name: oidc-macos display_name: "OIDC Macos" @@ -331,6 +332,7 @@ buildvariants: tasks: - testoidc_task_group - testazureoidc_task_group + - testgcpoidc_task_group - name: in-use-encryption display_name: "In-Use Encryption" @@ -653,14 +655,13 @@ task_groups: - func: fix absolute paths - func: init test-results - func: make files executable - - command: shell.exec + - command: subprocess.exec params: - shell: bash + binary: bash env: AZUREOIDC_VMNAME_PREFIX: "RUST_DRIVER" - script: | - ${PREPARE_SHELL} - ${DRIVERS_TOOLS}/.evergreen/auth_oidc/azure/create-and-setup-vm.sh + args: + - ${DRIVERS_TOOLS}/.evergreen/auth_oidc/azure/create-and-setup-vm.sh teardown_task: - command: subprocess.exec params: @@ -672,6 +673,32 @@ task_groups: tasks: - oidc-auth-test-azure-latest + - name: testgcpoidc_task_group + setup_group: + - func: fetch source + - func: create expansions + - func: prepare resources + - func: fix absolute paths + - func: init test-results + - func: make files executable + - command: subprocess.exec + params: + binary: bash + env: + GCPOIDC_VMNAME_PREFIX: "RUST_DRIVER" + args: + - ${DRIVERS_TOOLS}/.evergreen/auth_oidc/gcp/setup.sh + teardown_task: + - command: subprocess.exec + params: + binary: bash + args: + - ${DRIVERS_TOOLS}/.evergreen/auth_oidc/gcp/teardown.sh + setup_group_can_fail_task: true + setup_group_timeout_secs: 1800 + tasks: + - oidc-auth-test-gcp-latest + ######### # Tasks # ######### @@ -1081,15 +1108,53 @@ tasks: script: |- set -o errexit ${PREPARE_SHELL} - git add . - git commit -m "add files" - export AZUREOIDC_DRIVERS_TAR_FILE=/tmp/mongo-rust-driver.tgz - git archive -o $AZUREOIDC_DRIVERS_TAR_FILE HEAD - export AZUREOIDC_TEST_CMD="PROJECT_DIRECTORY='.' ./.evergreen/install-dependencies.sh rust\ - && PROJECT_DIRECTORY='.' .evergreen/install-dependencies.sh junit-dependencies\ - && PROJECT_DIRECTORY='.' OIDC_ENV=azure OIDC=oidc ./.evergreen/run-mongodb-oidc-test.sh" + ./.evergreen/install-dependencies.sh rust + source .cargo/env + export AZUREOIDC_DRIVERS_TAR_FILE=/tmp/mongo-rust-driver.tar + rustup default stable + export RUSTFLAGS="-C target-feature=+crt-static" + cargo test --features azure-oidc --target x86_64-unknown-linux-gnu get_exe_name -- --ignored + export TEST_FILE=$(cat exe_name.txt) + rm "$AZUREOIDC_DRIVERS_TAR_FILE" || true + tar -cf $AZUREOIDC_DRIVERS_TAR_FILE $TEST_FILE + tar -uf $AZUREOIDC_DRIVERS_TAR_FILE ./.evergreen + rm "$AZUREOIDC_DRIVERS_TAR_FILE".gz || true + gzip $AZUREOIDC_DRIVERS_TAR_FILE + export AZUREOIDC_DRIVERS_TAR_FILE=/tmp/mongo-rust-driver.tar.gz + # Define the command to run on the azure VM. + # Ensure that we source the environment file created for us, set up any other variables we need, + # and then run our test suite on the vm. + export AZUREOIDC_TEST_CMD="ls -laR data && PROJECT_DIRECTORY='.' OIDC_ENV=azure OIDC=oidc TEST_FILE=./$TEST_FILE ./.evergreen/run-mongodb-oidc-test.sh" bash $DRIVERS_TOOLS/.evergreen/auth_oidc/azure/run-driver-test.sh + - name: "oidc-auth-test-gcp-latest" + commands: + - command: shell.exec + params: + working_dir: src + shell: bash + script: |- + set -o errexit + ${PREPARE_SHELL} + ./.evergreen/install-dependencies.sh rust + source .cargo/env + export GCPOIDC_DRIVERS_TAR_FILE=/tmp/mongo-rust-driver.tar + rustup default stable + export RUSTFLAGS="-C target-feature=+crt-static" + cargo test --features gcp-oidc --target x86_64-unknown-linux-gnu test::atlas_planned_maintenance_testing::get_exe_name -- --ignored + export TEST_FILE=$(cat exe_name.txt) + rm "$GCPOIDC_DRIVERS_TAR_FILE" || true + tar -cf $GCPOIDC_DRIVERS_TAR_FILE $TEST_FILE + tar -uf $GCPOIDC_DRIVERS_TAR_FILE ./.evergreen + rm "$GCPOIDC_DRIVERS_TAR_FILE".gz || true + gzip $GCPOIDC_DRIVERS_TAR_FILE + export GCPOIDC_DRIVERS_TAR_FILE=/tmp/mongo-rust-driver.tar.gz + # Define the command to run on the gcp VM. + # Ensure that we source the environment file created for us, set up any other variables we need, + # and then run our test suite on the vm. + export GCPOIDC_TEST_CMD="ls -la && PROJECT_DIRECTORY='.' OIDC_ENV=gcp OIDC=oidc TEST_FILE=./$TEST_FILE ./.evergreen/run-mongodb-oidc-test.sh" + bash $DRIVERS_TOOLS/.evergreen/auth_oidc/gcp/run-driver-test.sh + ############# # Functions # ############# diff --git a/.evergreen/run-mongodb-oidc-test.sh b/.evergreen/run-mongodb-oidc-test.sh index 49e0f4d70..d3dcb875b 100755 --- a/.evergreen/run-mongodb-oidc-test.sh +++ b/.evergreen/run-mongodb-oidc-test.sh @@ -3,9 +3,6 @@ set +x # Disable debug trace set -o errexit # Exit the script with error if any of the commands fail -source .evergreen/env.sh -source .evergreen/cargo-test.sh - echo "Running MONGODB-OIDC authentication tests" OIDC_ENV=${OIDC_ENV:-"test"} @@ -15,6 +12,9 @@ export COVERAGE=1 export AUTH="auth" if [ $OIDC_ENV == "test" ]; then + + source .evergreen/env.sh + source .evergreen/cargo-test.sh # Make sure DRIVERS_TOOLS is set. if [ -z "$DRIVERS_TOOLS" ]; then echo "Must specify DRIVERS_TOOLS" @@ -24,15 +24,20 @@ if [ $OIDC_ENV == "test" ]; then cargo nextest run test::spec::oidc::basic --no-capture --profile ci RESULT=$? + cp target/nextest/ci/junit.xml results.xml elif [ $OIDC_ENV == "azure" ]; then source ./env.sh - cargo nextest run test::spec::oidc::azure --no-capture --profile ci --features=azure-oidc + $TEST_FILE test::spec::oidc::azure --nocapture + RESULT=$? +elif [ $OIDC_ENV == "gcp" ]; then + source ./secrets-export.sh + + $TEST_FILE test::spec::oidc::gcp --nocapture RESULT=$? else echo "Unrecognized OIDC_ENV $OIDC_ENV" exit 1 fi -cp target/nextest/ci/junit.xml results.xml exit $RESULT diff --git a/Cargo.toml b/Cargo.toml index c2781cf33..049a546c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ authors = [ "Isabel Atkinson ", "Abraham Egnor ", "Kaitlin Mahar ", + "Patrick Meredith ", ] description = "The official MongoDB driver for Rust" edition = "2021" @@ -45,6 +46,9 @@ azure-kms = ["dep:reqwest"] # Enable support for azure OIDC authentication. azure-oidc = ["dep:reqwest"] +# Enable support for gcp OIDC authentication. +gcp-oidc = ["dep:reqwest"] + # Enable support for on-demand GCP KMS credentials. # This can only be used with the tokio-runtime feature flag. gcp-kms = ["dep:reqwest"] diff --git a/src/client/auth/oidc.rs b/src/client/auth/oidc.rs index ceb6fef80..c98e6812e 100644 --- a/src/client/auth/oidc.rs +++ b/src/client/auth/oidc.rs @@ -7,7 +7,7 @@ use std::{ use tokio::sync::Mutex; use typed_builder::TypedBuilder; -#[cfg(feature = "azure-oidc")] +#[cfg(any(feature = "azure-oidc", feature = "gcp-oidc"))] use crate::client::auth::{ AZURE_ENVIRONMENT_VALUE_STR, ENVIRONMENT_PROP_STR, @@ -214,6 +214,46 @@ impl Callback { cache: Cache::new(), } } + + /// Create gcp callback. + #[cfg(feature = "gcp-oidc")] + fn gcp_callback(resource: &str) -> CallbackInner { + use futures_util::FutureExt; + let url = format!( + "http://metadata/computeMetadata/v1/instance/service-accounts/default/identity?audience={}", + resource + ); + CallbackInner { + function: Self::new_function( + move |_| { + let url = url.clone(); + async move { + let url = url.clone(); + let response = crate::runtime::HttpClient::default() + .get(&url) + .headers(&[("Metadata-Flavor", "Google")]) + .send_and_get_string() + .await + .map_err(|e| { + Error::authentication_error( + MONGODB_OIDC_STR, + &format!("Failed to get access token from GCP IDMS: {}", e), + ) + }); + let access_token = response?; + Ok(IdpServerResponse { + access_token, + expires: None, + refresh_token: None, + }) + } + .boxed() + }, + CallbackKind::Machine, + ), + cache: Cache::new(), + } + } } /// The OIDC state containing the cache of necessary OIDC info as well as the function @@ -467,25 +507,31 @@ pub(crate) async fn reauthenticate_stream( authenticate_stream(conn, credential, server_api, None).await } -#[cfg(feature = "azure-oidc")] -async fn setup_automatic_providers(credential: &Credential, state: &mut Option) { +#[cfg(any(feature = "azure-oidc", feature = "gcp-oidc"))] +async fn setup_automatic_providers(credential: &Credential, callback: &mut Option) { // If there is already a function, there is no need to set up an automatic provider // this could happen in the case of a reauthentication, or if the user has already set up // a function. A situation where the user has set up a function and an automatic provider // would already have caused an InvalidArgument error in `validate_credential`. - if state.is_some() { + if callback.is_some() { return; } if let Some(ref p) = credential.mechanism_properties { let environment = p.get_str(ENVIRONMENT_PROP_STR).unwrap_or(""); - let client_id = credential.username.as_deref(); let resource = p.get_str(TOKEN_RESOURCE_PROP_STR).unwrap_or(""); match environment { AZURE_ENVIRONMENT_VALUE_STR => { - *state = Some(Callback::azure_callback(client_id, resource)) + #[cfg(feature = "azure-oidc")] + { + let client_id = credential.username.as_deref(); + *callback = Some(Callback::azure_callback(client_id, resource)) + } } GCP_ENVIRONMENT_VALUE_STR => { - // TODO RUST-1627: Implement GCP automatic provider + #[cfg(feature = "gcp-oidc")] + { + *callback = Some(Callback::gcp_callback(resource)) + } } _ => {} } @@ -503,7 +549,7 @@ pub(crate) async fn authenticate_stream( // always matches that in the Credential Cache. let mut guard = credential.oidc_callback.inner.lock().await; - #[cfg(feature = "azure-oidc")] + #[cfg(any(feature = "azure-oidc", feature = "gcp-oidc"))] setup_automatic_providers(credential, &mut guard).await; let CallbackInner { cache, diff --git a/src/runtime.rs b/src/runtime.rs index 239f1c90d..185d441cb 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -3,7 +3,8 @@ mod acknowledged_message; feature = "aws-auth", feature = "azure-kms", feature = "gcp-kms", - feature = "azure-oidc" + feature = "azure-oidc", + feature = "gcp-oidc" ))] mod http; mod join_handle; @@ -36,7 +37,8 @@ use crate::{error::Result, options::ServerAddress}; feature = "aws-auth", feature = "azure-kms", feature = "gcp-kms", - feature = "azure-oidc" + feature = "azure-oidc", + feature = "gcp-oidc" ))] pub(crate) use http::HttpClient; #[cfg(feature = "openssl-tls")] diff --git a/src/test/spec/oidc.rs b/src/test/spec/oidc.rs index c86f04649..233c65cc2 100644 --- a/src/test/spec/oidc.rs +++ b/src/test/spec/oidc.rs @@ -1231,3 +1231,48 @@ mod azure { Ok(()) } } + +mod gcp { + use crate::client::{options::ClientOptions, Client}; + use bson::{doc, Document}; + + #[tokio::test] + async fn machine_5_4_gcp_with_no_username() -> anyhow::Result<()> { + get_env_or_skip!("OIDC"); + + let mut opts = ClientOptions::parse(mongodb_uri_single!()).await?; + opts.credential.as_mut().unwrap().source = None; + let client = Client::with_options(opts)?; + client + .database("test") + .collection::("test") + .find_one(doc! {}) + .await?; + Ok(()) + } + + #[tokio::test] + async fn machine_5_5_token_resource_must_be_set_for_gcp() -> anyhow::Result<()> { + get_env_or_skip!("OIDC"); + use crate::client::auth::{ENVIRONMENT_PROP_STR, GCP_ENVIRONMENT_VALUE_STR}; + + let mut opts = ClientOptions::parse(mongodb_uri_single!()).await?; + opts.credential.as_mut().unwrap().source = None; + opts.credential.as_mut().unwrap().mechanism_properties = Some(doc! { + ENVIRONMENT_PROP_STR: GCP_ENVIRONMENT_VALUE_STR, + }); + let client = Client::with_options(opts)?; + let res = client + .database("test") + .collection::("test") + .find_one(doc! {}) + .await; + + assert!(res.is_err()); + assert!(matches!( + *res.unwrap_err().kind, + crate::error::ErrorKind::InvalidArgument { .. }, + )); + Ok(()) + } +}