From b80350a9bf52d063c4df3fe5fb8575f25314658a Mon Sep 17 00:00:00 2001 From: Matthew Chiaravalloti Date: Mon, 21 Jul 2025 17:27:50 -0400 Subject: [PATCH 01/10] RUST-2236: Implement basic structure for e2e GSSAPI auth tests --- .evergreen/config.yml | 7 +++++++ .evergreen/run-gssapi-tests.sh | 8 ++++++++ src/test/auth.rs | 2 ++ src/test/auth/gssapi.rs | 37 ++++++++++++++++++++++++++++++++++ 4 files changed, 54 insertions(+) create mode 100644 src/test/auth/gssapi.rs diff --git a/.evergreen/config.yml b/.evergreen/config.yml index e40e29c64..a407b7250 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -1389,6 +1389,9 @@ functions: AWS_AUTH_TYPE: web-identity "run gssapi auth test": + - command: ec2.assume_role + params: + role_arn: ${aws_test_secrets_role} - command: subprocess.exec type: test params: @@ -1397,6 +1400,10 @@ functions: args: - .evergreen/run-gssapi-tests.sh include_expansions_in_env: + - AWS_ACCESS_KEY_ID + - AWS_SECRET_ACCESS_KEY + - AWS_SESSION_TOKEN + - DRIVERS_TOOLS - PROJECT_DIRECTORY "run x509 tests": diff --git a/.evergreen/run-gssapi-tests.sh b/.evergreen/run-gssapi-tests.sh index 63478b563..87674a31f 100644 --- a/.evergreen/run-gssapi-tests.sh +++ b/.evergreen/run-gssapi-tests.sh @@ -9,10 +9,18 @@ cd ${PROJECT_DIRECTORY} source .evergreen/env.sh source .evergreen/cargo-test.sh +# Source the drivers/atlas_connect secrets, where GSSAPI test values are held +source "${DRIVERS_TOOLS}/.evergreen/secrets_handling/setup-secrets.sh" drivers/atlas_connect + +# Authenticate the user principal in the KDC before running the integration test +echo "$SASL_PASS" | kinit -p $PRINCIPAL +klist + FEATURE_FLAGS+=("gssapi-auth") set +o errexit +cargo_test test::auth::gssapi cargo_test spec::auth cargo_test uri_options cargo_test connection_string diff --git a/src/test/auth.rs b/src/test/auth.rs index c6f4ca430..113675a8b 100644 --- a/src/test/auth.rs +++ b/src/test/auth.rs @@ -1,5 +1,7 @@ #[cfg(feature = "aws-auth")] mod aws; +#[cfg(feature = "gssapi-auth")] +mod gssapi; use serde::Deserialize; diff --git a/src/test/auth/gssapi.rs b/src/test/auth/gssapi.rs new file mode 100644 index 000000000..88c035932 --- /dev/null +++ b/src/test/auth/gssapi.rs @@ -0,0 +1,37 @@ +use crate::{ + bson::{doc, Document}, + Client, +}; + +#[tokio::test] +async fn auth_gssapi() { + let host = std::env::var("SASL_HOST").expect("SASL_HOST not set"); + let user_principal = std::env::var("PRINCIPAL") + .expect("PRINCIPAL not set") + .replace("@", "%40"); + let gssapi_db = std::env::var("GSSAPI_DB").expect("GSSAPI_DB not set"); + + let uri = + format!("mongodb://{user_principal}@{host}/?authSource=%24external&authMechanism=GSSAPI"); + let client = Client::with_uri_str(uri) + .await + .expect("failed to create MongoDB Client"); + + let coll = client.database(&gssapi_db).collection::("test"); + let doc = coll.find_one(doc! {}).await; + match doc { + Ok(Some(doc)) => { + assert!( + doc.get_bool("kerberos").unwrap(), + "expected 'kerberos' field to exist and be 'true'" + ); + assert_eq!( + doc.get_str("authenticated").unwrap(), + "yeah", + "unexpected 'authenticated' value" + ); + } + Ok(None) => panic!("expected `find_one` to return a document, but it did not"), + Err(e) => panic!("expected `find_one` to return a document, but it failed: {e:?}"), + } +} From b4812ffa756bc172609782a5f5ee36fa03fb0a40 Mon Sep 17 00:00:00 2001 From: Matthew Chiaravalloti Date: Wed, 23 Jul 2025 09:36:07 -0400 Subject: [PATCH 02/10] RUST-2236: Update test suite --- .evergreen/run-gssapi-tests.sh | 52 ++++++++++++++-- src/test/auth/gssapi.rs | 105 +++++++++++++++++++++++---------- 2 files changed, 123 insertions(+), 34 deletions(-) diff --git a/.evergreen/run-gssapi-tests.sh b/.evergreen/run-gssapi-tests.sh index 87674a31f..3dd07db66 100644 --- a/.evergreen/run-gssapi-tests.sh +++ b/.evergreen/run-gssapi-tests.sh @@ -12,15 +12,59 @@ source .evergreen/cargo-test.sh # Source the drivers/atlas_connect secrets, where GSSAPI test values are held source "${DRIVERS_TOOLS}/.evergreen/secrets_handling/setup-secrets.sh" drivers/atlas_connect -# Authenticate the user principal in the KDC before running the integration test -echo "$SASL_PASS" | kinit -p $PRINCIPAL -klist - FEATURE_FLAGS+=("gssapi-auth") set +o errexit +# Update hosts with relevant data +echo "`host $SASL_HOST | awk '/has address/ { print $4 ; exit }'` $SASL_HOST" | sudo tee -a /etc/hosts + +# Create a krb5 config file with relevant +touch krb5.conf +echo "[realms] + $SASL_REALM = { + kdc = $SASL_HOST + admin_server = $SASL_HOST + } + + $SASL_REALM_CROSS = { + kdc = $SASL_HOST + admin_server = $SASL_HOST + } + +[domain_realm] + .$SASL_DOMAIN = $SASL_REALM + $SASL_DOMAIN = $SASL_REALM +" > krb5.conf + +export KRB5_CONFIG=krb5.conf + +# Authenticate the user principal in the KDC before running the e2e test +echo "Authenticating $PRINCIPAL" +echo "$SASL_PASS" | kinit -p $PRINCIPAL +klist + +# Run end-to-end auth tests for "$PRINCIPAL" user +TEST_OPTIONS+=("--skip with_service_realm_and_host_options") cargo_test test::auth::gssapi + +# Unauthenticate +echo "Unauthenticating $PRINCIPAL" +kdestroy + +# Authenticate the alternative user principal in the KDC and run other e2e test +echo "Authenticating $PRINCIPAL_CROSS" +echo "$SASL_PASS_CROSS" | kinit -p $PRINCIPAL_CROSS +klist + +TEST_OPTIONS=() +cargo_test test::auth::gssapi::with_service_realm_and_host_options + +# Unauthenticate +echo "Unuthenticating $PRINCIPAL_CROSS" +kdestroy + +# Run remaining tests cargo_test spec::auth cargo_test uri_options cargo_test connection_string diff --git a/src/test/auth/gssapi.rs b/src/test/auth/gssapi.rs index 88c035932..8a4351b5a 100644 --- a/src/test/auth/gssapi.rs +++ b/src/test/auth/gssapi.rs @@ -3,35 +3,80 @@ use crate::{ Client, }; -#[tokio::test] -async fn auth_gssapi() { - let host = std::env::var("SASL_HOST").expect("SASL_HOST not set"); - let user_principal = std::env::var("PRINCIPAL") - .expect("PRINCIPAL not set") - .replace("@", "%40"); - let gssapi_db = std::env::var("GSSAPI_DB").expect("GSSAPI_DB not set"); - - let uri = - format!("mongodb://{user_principal}@{host}/?authSource=%24external&authMechanism=GSSAPI"); - let client = Client::with_uri_str(uri) - .await - .expect("failed to create MongoDB Client"); - - let coll = client.database(&gssapi_db).collection::("test"); - let doc = coll.find_one(doc! {}).await; - match doc { - Ok(Some(doc)) => { - assert!( - doc.get_bool("kerberos").unwrap(), - "expected 'kerberos' field to exist and be 'true'" - ); - assert_eq!( - doc.get_str("authenticated").unwrap(), - "yeah", - "unexpected 'authenticated' value" - ); +/// Create a GSSAPI e2e test. +/// - test_name is the name of the test. +/// - user_principal is the name of the environment variable that stores the user principal +/// - auth_mechanism_properties is an optional set of authMechanismProperties to append to the uri +macro_rules! test_gssapi_auth { + ($test_name:ident, user_principal = $user_principal:expr, $(auth_mechanism_properties = $auth_mechanism_properties:expr,)?) => { + #[tokio::test] + async fn $test_name() { + // Get env variables + let host = std::env::var("SASL_HOST").expect("SASL_HOST not set"); + let user_principal = std::env::var($user_principal) + .expect(format!("{} not set", $user_principal).as_str()) + .replace("@", "%40"); + let gssapi_db = std::env::var("GSSAPI_DB").expect("GSSAPI_DB not set"); + + // Optionally create authMechanismProperties + #[allow(unused_mut, unused_assignments)] + let mut auth_mechanism_properties = "".to_string(); + $(auth_mechanism_properties = format!("&authMechanismProperties={}", $auth_mechanism_properties);)? + + // Create client + let uri = format!("mongodb://{user_principal}@{host}/?authSource=%24external&authMechanism=GSSAPI{auth_mechanism_properties}"); + let client = Client::with_uri_str(uri) + .await + .expect("failed to create MongoDB Client"); + + // Check that auth worked by qurying the test collection + let coll = client.database(&gssapi_db).collection::("test"); + let doc = coll.find_one(doc! {}).await; + match doc { + Ok(Some(doc)) => { + assert!( + doc.get_bool("kerberos").unwrap(), + "expected 'kerberos' field to exist and be 'true'" + ); + assert_eq!( + doc.get_str("authenticated").unwrap(), + "yeah", + "unexpected 'authenticated' value" + ); + } + Ok(None) => panic!("expected `find_one` to return a document, but it did not"), + Err(e) => panic!("expected `find_one` to return a document, but it failed: {e:?}"), + } } - Ok(None) => panic!("expected `find_one` to return a document, but it did not"), - Err(e) => panic!("expected `find_one` to return a document, but it failed: {e:?}"), - } + }; } + +test_gssapi_auth!(no_options, user_principal = "PRINCIPAL",); + +test_gssapi_auth!( + explicit_canonicalize_host_name_false, + user_principal = "PRINCIPAL", + auth_mechanism_properties = "CANONICALIZE_HOST_NAME:false", +); + +test_gssapi_auth!( + canonicalize_host_name_forward, + user_principal = "PRINCIPAL", + auth_mechanism_properties = "CANONICALIZE_HOST_NAME:forward", +); + +test_gssapi_auth!( + canonicalize_host_name_forward_and_reverse, + user_principal = "PRINCIPAL", + auth_mechanism_properties = "CANONICALIZE_HOST_NAME:forwardAndReverse", +); + +test_gssapi_auth!( + with_service_realm_and_host_options, + user_principal = "PRINCIPAL_CROSS", + auth_mechanism_properties = { + let service_realm = std::env::var("SASL_REALM_CROSS").expect("SASL_REALM_CROSS not set"); + let service_host = std::env::var("SASL_HOST").expect("SASL_HOST not set"); + format!("SERVICE_REALM:{service_realm},SERVICE_HOST:{service_host}").as_str() + }, +); From bd5d7237f6dc54993b067fa4df349886ff76e055 Mon Sep 17 00:00:00 2001 From: Matthew Chiaravalloti Date: Wed, 23 Jul 2025 09:36:46 -0400 Subject: [PATCH 03/10] RUST-2236: Make canonicalize_hostname ignore canonical names in forward lookup --- src/client/auth/gssapi.rs | 41 ++++++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/src/client/auth/gssapi.rs b/src/client/auth/gssapi.rs index b554dd353..835ceaec3 100644 --- a/src/client/auth/gssapi.rs +++ b/src/client/auth/gssapi.rs @@ -1,5 +1,4 @@ use cross_krb5::{ClientCtx, InitiateFlags, K5Ctx, PendingClientCtx, Step}; -use hickory_resolver::proto::rr::RData; use crate::{ bson::Bson, @@ -324,21 +323,24 @@ async fn canonicalize_hostname( let resolver = crate::runtime::AsyncResolver::new(resolver_config.map(|c| c.inner.clone())).await?; - match mode { + let hostname = match mode { CanonicalizeHostName::Forward => { let lookup_records = resolver.cname_lookup(hostname).await?; - if let Some(first_record) = lookup_records.records().first() { - if let Some(RData::CNAME(cname)) = first_record.data() { - Ok(cname.to_lowercase().to_string()) - } else { - Ok(hostname.to_string()) - } + if !lookup_records.records().is_empty() { + // As long as there is a record, we can return the original hostname. + // Although the spec says to return the canonical name, this is not + // done by any drivers in practice since the majority of them use + // libraries that do not follow CNAME chains. Also, we do not want to + // use the canonical name since it will likely differ from the input + // name, and the use of the input name is required for the service + // principal to be accepted by the GSSAPI auth flow. + hostname.to_lowercase().to_string() } else { - Err(Error::authentication_error( + return Err(Error::authentication_error( GSSAPI_STR, &format!("No addresses found for hostname: {hostname}"), - )) + )); } } CanonicalizeHostName::ForwardAndReverse => { @@ -350,20 +352,27 @@ async fn canonicalize_hostname( match resolver.reverse_lookup(first_address).await { Ok(reverse_lookup) => { if let Some(name) = reverse_lookup.iter().next() { - Ok(name.to_lowercase().to_string()) + name.to_lowercase().to_string() } else { - Ok(hostname.to_lowercase()) + hostname.to_lowercase() } } - Err(_) => Ok(hostname.to_lowercase()), + Err(_) => hostname.to_lowercase(), } } else { - Err(Error::authentication_error( + return Err(Error::authentication_error( GSSAPI_STR, &format!("No addresses found for hostname: {hostname}"), - )) + )); } } CanonicalizeHostName::None => unreachable!(), - } + }; + + // Sometimes reverse lookup results in a trailing "." since that is the correct + // way to present a FQDN. However, GSSAPI rejects the trailing "." so we remove + // it here manually. + let hostname = hostname.trim_end_matches("."); + + Ok(hostname.to_string()) } From 84d1eba6aaf815ab99b0e3cba1cb3c821b2e6a0f Mon Sep 17 00:00:00 2001 From: Matthew Chiaravalloti Date: Wed, 23 Jul 2025 09:52:08 -0400 Subject: [PATCH 04/10] RUST-2236: Update cross-realm test --- src/test/auth/gssapi.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/test/auth/gssapi.rs b/src/test/auth/gssapi.rs index 8a4351b5a..bca25c10f 100644 --- a/src/test/auth/gssapi.rs +++ b/src/test/auth/gssapi.rs @@ -73,9 +73,11 @@ test_gssapi_auth!( test_gssapi_auth!( with_service_realm_and_host_options, - user_principal = "PRINCIPAL_CROSS", + user_principal = "PRINCIPAL_CROSS", // Use the cross-realm USER principal auth_mechanism_properties = { - let service_realm = std::env::var("SASL_REALM_CROSS").expect("SASL_REALM_CROSS not set"); + // However, the SERVICE principal is not cross-realm, hence the use of + // SASL_REALM instead of SASL_REALM_CROSS. + let service_realm = std::env::var("SASL_REALM").expect("SASL_REALM not set"); let service_host = std::env::var("SASL_HOST").expect("SASL_HOST not set"); format!("SERVICE_REALM:{service_realm},SERVICE_HOST:{service_host}").as_str() }, From c9f3bb1a7e53df0fcf495614ea6d62944e62b8b6 Mon Sep 17 00:00:00 2001 From: Matthew Chiaravalloti Date: Wed, 23 Jul 2025 09:53:54 -0400 Subject: [PATCH 05/10] RUST-2236: Separate Linux and Mac tests --- .evergreen/config.yml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/.evergreen/config.yml b/.evergreen/config.yml index a407b7250..aa91efcf6 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -258,10 +258,18 @@ buildvariants: batchtime: 20160 - name: gssapi-auth - display_name: "GSSAPI Authentication" + display_name: "GSSAPI Authentication - Linux" patchable: true run_on: - - ubuntu2004-small + - ubuntu2204-small + tasks: + - test-gssapi-auth + + - name: gssapi-auth + display_name: "GSSAPI Authentication - macOS" + patchable: true + run_on: + - macos-14 tasks: - test-gssapi-auth From f072e6f26262f427c674b52e8b99ff2a06d46c74 Mon Sep 17 00:00:00 2001 From: Matthew Chiaravalloti Date: Wed, 23 Jul 2025 09:55:10 -0400 Subject: [PATCH 06/10] RUST-2236: Fix buildvariant names --- .evergreen/config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.evergreen/config.yml b/.evergreen/config.yml index aa91efcf6..0d56940da 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -257,7 +257,7 @@ buildvariants: # Limit the test to only schedule every 14 days to reduce external resource usage. batchtime: 20160 - - name: gssapi-auth + - name: gssapi-auth-linux display_name: "GSSAPI Authentication - Linux" patchable: true run_on: @@ -265,7 +265,7 @@ buildvariants: tasks: - test-gssapi-auth - - name: gssapi-auth + - name: gssapi-auth-macos display_name: "GSSAPI Authentication - macOS" patchable: true run_on: From f8b0f096caca1cc943271012ec87f13ca68be008 Mon Sep 17 00:00:00 2001 From: Matthew Chiaravalloti Date: Wed, 23 Jul 2025 11:03:29 -0400 Subject: [PATCH 07/10] RUST-2236: Update macos to be disabled given the current hold-ups --- .evergreen/config.yml | 1 + src/test/auth/gssapi.rs | 19 +++++++++++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/.evergreen/config.yml b/.evergreen/config.yml index 0d56940da..c7a03538c 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -268,6 +268,7 @@ buildvariants: - name: gssapi-auth-macos display_name: "GSSAPI Authentication - macOS" patchable: true + disable: true run_on: - macos-14 tasks: diff --git a/src/test/auth/gssapi.rs b/src/test/auth/gssapi.rs index bca25c10f..017ec3513 100644 --- a/src/test/auth/gssapi.rs +++ b/src/test/auth/gssapi.rs @@ -8,7 +8,7 @@ use crate::{ /// - user_principal is the name of the environment variable that stores the user principal /// - auth_mechanism_properties is an optional set of authMechanismProperties to append to the uri macro_rules! test_gssapi_auth { - ($test_name:ident, user_principal = $user_principal:expr, $(auth_mechanism_properties = $auth_mechanism_properties:expr,)?) => { + ($test_name:ident, user_principal = $user_principal:expr, gssapi_db = $gssapi_db:expr, $(auth_mechanism_properties = $auth_mechanism_properties:expr,)?) => { #[tokio::test] async fn $test_name() { // Get env variables @@ -16,7 +16,8 @@ macro_rules! test_gssapi_auth { let user_principal = std::env::var($user_principal) .expect(format!("{} not set", $user_principal).as_str()) .replace("@", "%40"); - let gssapi_db = std::env::var("GSSAPI_DB").expect("GSSAPI_DB not set"); + let gssapi_db = std::env::var($gssapi_db) + .expect(format!("{} not set", $gssapi_db).as_str()); // Optionally create authMechanismProperties #[allow(unused_mut, unused_assignments)] @@ -51,29 +52,39 @@ macro_rules! test_gssapi_auth { }; } -test_gssapi_auth!(no_options, user_principal = "PRINCIPAL",); +test_gssapi_auth!( + no_options, + user_principal = "PRINCIPAL", + gssapi_db = "GSSAPI_DB", +); test_gssapi_auth!( explicit_canonicalize_host_name_false, user_principal = "PRINCIPAL", + gssapi_db = "GSSAPI_DB", auth_mechanism_properties = "CANONICALIZE_HOST_NAME:false", ); test_gssapi_auth!( canonicalize_host_name_forward, user_principal = "PRINCIPAL", + gssapi_db = "GSSAPI_DB", auth_mechanism_properties = "CANONICALIZE_HOST_NAME:forward", ); test_gssapi_auth!( canonicalize_host_name_forward_and_reverse, user_principal = "PRINCIPAL", + gssapi_db = "GSSAPI_DB", auth_mechanism_properties = "CANONICALIZE_HOST_NAME:forwardAndReverse", ); test_gssapi_auth!( with_service_realm_and_host_options, - user_principal = "PRINCIPAL_CROSS", // Use the cross-realm USER principal + // Use the cross-realm USER principal + user_principal = "PRINCIPAL_CROSS", + // The cross-realm user is only authorized on the cross-realm db + gssapi_db = "GSSAPI_DB_CROSS", auth_mechanism_properties = { // However, the SERVICE principal is not cross-realm, hence the use of // SASL_REALM instead of SASL_REALM_CROSS. From 96e1deb51c6ef4a6f78d9dce2d21e162ac7cc0f0 Mon Sep 17 00:00:00 2001 From: Matthew Chiaravalloti Date: Wed, 23 Jul 2025 14:03:22 -0400 Subject: [PATCH 08/10] RUST-2236: Update test to expect correct values --- src/test/auth/gssapi.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/auth/gssapi.rs b/src/test/auth/gssapi.rs index 017ec3513..ce24e599e 100644 --- a/src/test/auth/gssapi.rs +++ b/src/test/auth/gssapi.rs @@ -36,8 +36,8 @@ macro_rules! test_gssapi_auth { match doc { Ok(Some(doc)) => { assert!( - doc.get_bool("kerberos").unwrap(), - "expected 'kerberos' field to exist and be 'true'" + doc.get_bool(gssapi_db).unwrap(), + "expected '{gssapi_db}' field to exist and be 'true'" ); assert_eq!( doc.get_str("authenticated").unwrap(), From 4043959c35b4f6df905cc13360d1a228bf2f7ba7 Mon Sep 17 00:00:00 2001 From: Matthew Chiaravalloti Date: Wed, 23 Jul 2025 14:17:46 -0400 Subject: [PATCH 09/10] RUST-2236: Fix compiler failure --- src/test/auth/gssapi.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/auth/gssapi.rs b/src/test/auth/gssapi.rs index ce24e599e..3bafe4cfb 100644 --- a/src/test/auth/gssapi.rs +++ b/src/test/auth/gssapi.rs @@ -36,7 +36,7 @@ macro_rules! test_gssapi_auth { match doc { Ok(Some(doc)) => { assert!( - doc.get_bool(gssapi_db).unwrap(), + doc.get_bool(&gssapi_db).unwrap(), "expected '{gssapi_db}' field to exist and be 'true'" ); assert_eq!( From 75c8479e574df52762b81ed38f6e33ed01b9c2d7 Mon Sep 17 00:00:00 2001 From: Matthew Chiaravalloti Date: Fri, 25 Jul 2025 17:13:11 -0400 Subject: [PATCH 10/10] RUST-2236: Make code review changes --- .evergreen/run-gssapi-tests.sh | 7 +- src/test/auth.rs | 3 +- src/test/auth/gssapi.rs | 170 ++++++++++++++++++--------------- 3 files changed, 95 insertions(+), 85 deletions(-) diff --git a/.evergreen/run-gssapi-tests.sh b/.evergreen/run-gssapi-tests.sh index 3dd07db66..6957a705d 100644 --- a/.evergreen/run-gssapi-tests.sh +++ b/.evergreen/run-gssapi-tests.sh @@ -16,9 +16,6 @@ FEATURE_FLAGS+=("gssapi-auth") set +o errexit -# Update hosts with relevant data -echo "`host $SASL_HOST | awk '/has address/ { print $4 ; exit }'` $SASL_HOST" | sudo tee -a /etc/hosts - # Create a krb5 config file with relevant touch krb5.conf echo "[realms] @@ -46,7 +43,7 @@ klist # Run end-to-end auth tests for "$PRINCIPAL" user TEST_OPTIONS+=("--skip with_service_realm_and_host_options") -cargo_test test::auth::gssapi +cargo_test test::auth::gssapi_skip_local # Unauthenticate echo "Unauthenticating $PRINCIPAL" @@ -58,7 +55,7 @@ echo "$SASL_PASS_CROSS" | kinit -p $PRINCIPAL_CROSS klist TEST_OPTIONS=() -cargo_test test::auth::gssapi::with_service_realm_and_host_options +cargo_test test::auth::gssapi_skip_local::with_service_realm_and_host_options # Unauthenticate echo "Unuthenticating $PRINCIPAL_CROSS" diff --git a/src/test/auth.rs b/src/test/auth.rs index 113675a8b..3790e07b4 100644 --- a/src/test/auth.rs +++ b/src/test/auth.rs @@ -1,7 +1,8 @@ #[cfg(feature = "aws-auth")] mod aws; #[cfg(feature = "gssapi-auth")] -mod gssapi; +#[path = "auth/gssapi.rs"] +mod gssapi_skip_local; use serde::Deserialize; diff --git a/src/test/auth/gssapi.rs b/src/test/auth/gssapi.rs index 3bafe4cfb..45154d8b2 100644 --- a/src/test/auth/gssapi.rs +++ b/src/test/auth/gssapi.rs @@ -3,93 +3,105 @@ use crate::{ Client, }; -/// Create a GSSAPI e2e test. -/// - test_name is the name of the test. -/// - user_principal is the name of the environment variable that stores the user principal +/// Run a GSSAPI e2e test. +/// - user_principal_var is the name of the environment variable that stores the user principal +/// - gssapi_db_var is the name tof the environment variable that stores the db name to query /// - auth_mechanism_properties is an optional set of authMechanismProperties to append to the uri -macro_rules! test_gssapi_auth { - ($test_name:ident, user_principal = $user_principal:expr, gssapi_db = $gssapi_db:expr, $(auth_mechanism_properties = $auth_mechanism_properties:expr,)?) => { - #[tokio::test] - async fn $test_name() { - // Get env variables - let host = std::env::var("SASL_HOST").expect("SASL_HOST not set"); - let user_principal = std::env::var($user_principal) - .expect(format!("{} not set", $user_principal).as_str()) - .replace("@", "%40"); - let gssapi_db = std::env::var($gssapi_db) - .expect(format!("{} not set", $gssapi_db).as_str()); +async fn run_gssapi_auth_test( + user_principal_var: &str, + gssapi_db_var: &str, + auth_mechanism_properties: Option<&str>, +) { + // Get env variables + let host = std::env::var("SASL_HOST").expect("SASL_HOST not set"); + let user_principal = std::env::var(user_principal_var) + .unwrap_or_else(|_| panic!("{user_principal_var} not set")) + .replace("@", "%40"); + let gssapi_db = + std::env::var(gssapi_db_var).unwrap_or_else(|_| panic!("{gssapi_db_var} not set")); - // Optionally create authMechanismProperties - #[allow(unused_mut, unused_assignments)] - let mut auth_mechanism_properties = "".to_string(); - $(auth_mechanism_properties = format!("&authMechanismProperties={}", $auth_mechanism_properties);)? + // Optionally create authMechanismProperties + let props = if let Some(auth_mech_props) = auth_mechanism_properties { + format!("&authMechanismProperties={auth_mech_props}") + } else { + String::new() + }; - // Create client - let uri = format!("mongodb://{user_principal}@{host}/?authSource=%24external&authMechanism=GSSAPI{auth_mechanism_properties}"); - let client = Client::with_uri_str(uri) - .await - .expect("failed to create MongoDB Client"); + // Create client + let uri = format!( + "mongodb://{user_principal}@{host}/?authSource=%24external&authMechanism=GSSAPI{props}" + ); + let client = Client::with_uri_str(uri) + .await + .expect("failed to create MongoDB Client"); - // Check that auth worked by qurying the test collection - let coll = client.database(&gssapi_db).collection::("test"); - let doc = coll.find_one(doc! {}).await; - match doc { - Ok(Some(doc)) => { - assert!( - doc.get_bool(&gssapi_db).unwrap(), - "expected '{gssapi_db}' field to exist and be 'true'" - ); - assert_eq!( - doc.get_str("authenticated").unwrap(), - "yeah", - "unexpected 'authenticated' value" - ); - } - Ok(None) => panic!("expected `find_one` to return a document, but it did not"), - Err(e) => panic!("expected `find_one` to return a document, but it failed: {e:?}"), - } + // Check that auth worked by qurying the test collection + let coll = client.database(&gssapi_db).collection::("test"); + let doc = coll.find_one(doc! {}).await; + match doc { + Ok(Some(doc)) => { + assert!( + doc.get_bool(&gssapi_db).unwrap(), + "expected '{gssapi_db}' field to exist and be 'true'" + ); + assert_eq!( + doc.get_str("authenticated").unwrap(), + "yeah", + "unexpected 'authenticated' value" + ); } - }; + Ok(None) => panic!("expected `find_one` to return a document, but it did not"), + Err(e) => panic!("expected `find_one` to return a document, but it failed: {e:?}"), + } +} + +#[tokio::test] +async fn no_options() { + run_gssapi_auth_test("PRINCIPAL", "GSSAPI_DB", None).await } -test_gssapi_auth!( - no_options, - user_principal = "PRINCIPAL", - gssapi_db = "GSSAPI_DB", -); +#[tokio::test] +async fn explicit_canonicalize_host_name_false() { + run_gssapi_auth_test( + "PRINCIPAL", + "GSSAPI_DB", + Some("CANONICALIZE_HOST_NAME:false"), + ) + .await +} -test_gssapi_auth!( - explicit_canonicalize_host_name_false, - user_principal = "PRINCIPAL", - gssapi_db = "GSSAPI_DB", - auth_mechanism_properties = "CANONICALIZE_HOST_NAME:false", -); +#[tokio::test] +async fn canonicalize_host_name_forward() { + run_gssapi_auth_test( + "PRINCIPAL", + "GSSAPI_DB", + Some("CANONICALIZE_HOST_NAME:forward"), + ) + .await +} -test_gssapi_auth!( - canonicalize_host_name_forward, - user_principal = "PRINCIPAL", - gssapi_db = "GSSAPI_DB", - auth_mechanism_properties = "CANONICALIZE_HOST_NAME:forward", -); +#[tokio::test] +async fn canonicalize_host_name_forward_and_reverse() { + run_gssapi_auth_test( + "PRINCIPAL", + "GSSAPI_DB", + Some("CANONICALIZE_HOST_NAME:forwardAndReverse"), + ) + .await +} -test_gssapi_auth!( - canonicalize_host_name_forward_and_reverse, - user_principal = "PRINCIPAL", - gssapi_db = "GSSAPI_DB", - auth_mechanism_properties = "CANONICALIZE_HOST_NAME:forwardAndReverse", -); +#[tokio::test] +async fn with_service_realm_and_host_options() { + // This test uses a "cross-realm" user principal, however the service principal is not + // cross-realm. This is why we use SASL_REALM and SASL_HOST instead of SASL_REALM_CROSS + // and SASL_HOST_CROSS. + let service_realm = std::env::var("SASL_REALM").expect("SASL_REALM not set"); + let service_host = std::env::var("SASL_HOST").expect("SASL_HOST not set"); -test_gssapi_auth!( - with_service_realm_and_host_options, - // Use the cross-realm USER principal - user_principal = "PRINCIPAL_CROSS", - // The cross-realm user is only authorized on the cross-realm db - gssapi_db = "GSSAPI_DB_CROSS", - auth_mechanism_properties = { - // However, the SERVICE principal is not cross-realm, hence the use of - // SASL_REALM instead of SASL_REALM_CROSS. - let service_realm = std::env::var("SASL_REALM").expect("SASL_REALM not set"); - let service_host = std::env::var("SASL_HOST").expect("SASL_HOST not set"); - format!("SERVICE_REALM:{service_realm},SERVICE_HOST:{service_host}").as_str() - }, -); + run_gssapi_auth_test( + "PRINCIPAL_CROSS", + "GSSAPI_DB_CROSS", + Some(format!("SERVICE_REALM:{service_realm},SERVICE_HOST:{service_host}").as_str()), + ) + .await +}