Skip to content

Commit be56c79

Browse files
RUST-2236 Add e2e testing for GSSAPI auth on Linux and macOS (#1431)
1 parent 5452060 commit be56c79

File tree

5 files changed

+203
-19
lines changed

5 files changed

+203
-19
lines changed

.evergreen/config.yml

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -257,11 +257,20 @@ buildvariants:
257257
# Limit the test to only schedule every 14 days to reduce external resource usage.
258258
batchtime: 20160
259259

260-
- name: gssapi-auth
261-
display_name: "GSSAPI Authentication"
260+
- name: gssapi-auth-linux
261+
display_name: "GSSAPI Authentication - Linux"
262262
patchable: true
263263
run_on:
264-
- ubuntu2004-small
264+
- ubuntu2204-small
265+
tasks:
266+
- test-gssapi-auth
267+
268+
- name: gssapi-auth-macos
269+
display_name: "GSSAPI Authentication - macOS"
270+
patchable: true
271+
disable: true
272+
run_on:
273+
- macos-14
265274
tasks:
266275
- test-gssapi-auth
267276

@@ -1389,6 +1398,9 @@ functions:
13891398
AWS_AUTH_TYPE: web-identity
13901399

13911400
"run gssapi auth test":
1401+
- command: ec2.assume_role
1402+
params:
1403+
role_arn: ${aws_test_secrets_role}
13921404
- command: subprocess.exec
13931405
type: test
13941406
params:
@@ -1397,6 +1409,10 @@ functions:
13971409
args:
13981410
- .evergreen/run-gssapi-tests.sh
13991411
include_expansions_in_env:
1412+
- AWS_ACCESS_KEY_ID
1413+
- AWS_SECRET_ACCESS_KEY
1414+
- AWS_SESSION_TOKEN
1415+
- DRIVERS_TOOLS
14001416
- PROJECT_DIRECTORY
14011417

14021418
"run x509 tests":

.evergreen/run-gssapi-tests.sh

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,59 @@ cd ${PROJECT_DIRECTORY}
99
source .evergreen/env.sh
1010
source .evergreen/cargo-test.sh
1111

12+
# Source the drivers/atlas_connect secrets, where GSSAPI test values are held
13+
source "${DRIVERS_TOOLS}/.evergreen/secrets_handling/setup-secrets.sh" drivers/atlas_connect
14+
1215
FEATURE_FLAGS+=("gssapi-auth")
1316

1417
set +o errexit
1518

19+
# Create a krb5 config file with relevant
20+
touch krb5.conf
21+
echo "[realms]
22+
$SASL_REALM = {
23+
kdc = $SASL_HOST
24+
admin_server = $SASL_HOST
25+
}
26+
27+
$SASL_REALM_CROSS = {
28+
kdc = $SASL_HOST
29+
admin_server = $SASL_HOST
30+
}
31+
32+
[domain_realm]
33+
.$SASL_DOMAIN = $SASL_REALM
34+
$SASL_DOMAIN = $SASL_REALM
35+
" > krb5.conf
36+
37+
export KRB5_CONFIG=krb5.conf
38+
39+
# Authenticate the user principal in the KDC before running the e2e test
40+
echo "Authenticating $PRINCIPAL"
41+
echo "$SASL_PASS" | kinit -p $PRINCIPAL
42+
klist
43+
44+
# Run end-to-end auth tests for "$PRINCIPAL" user
45+
TEST_OPTIONS+=("--skip with_service_realm_and_host_options")
46+
cargo_test test::auth::gssapi_skip_local
47+
48+
# Unauthenticate
49+
echo "Unauthenticating $PRINCIPAL"
50+
kdestroy
51+
52+
# Authenticate the alternative user principal in the KDC and run other e2e test
53+
echo "Authenticating $PRINCIPAL_CROSS"
54+
echo "$SASL_PASS_CROSS" | kinit -p $PRINCIPAL_CROSS
55+
klist
56+
57+
TEST_OPTIONS=()
58+
cargo_test test::auth::gssapi_skip_local::with_service_realm_and_host_options
59+
60+
# Unauthenticate
61+
echo "Unuthenticating $PRINCIPAL_CROSS"
62+
kdestroy
63+
64+
# Run remaining tests
1665
cargo_test spec::auth
1766
cargo_test uri_options
1867
cargo_test connection_string

src/client/auth/gssapi.rs

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
use cross_krb5::{ClientCtx, InitiateFlags, K5Ctx, PendingClientCtx, Step};
2-
use hickory_resolver::proto::rr::RData;
32

43
use crate::{
54
bson::Bson,
@@ -324,21 +323,24 @@ async fn canonicalize_hostname(
324323
let resolver =
325324
crate::runtime::AsyncResolver::new(resolver_config.map(|c| c.inner.clone())).await?;
326325

327-
match mode {
326+
let hostname = match mode {
328327
CanonicalizeHostName::Forward => {
329328
let lookup_records = resolver.cname_lookup(hostname).await?;
330329

331-
if let Some(first_record) = lookup_records.records().first() {
332-
if let Some(RData::CNAME(cname)) = first_record.data() {
333-
Ok(cname.to_lowercase().to_string())
334-
} else {
335-
Ok(hostname.to_string())
336-
}
330+
if !lookup_records.records().is_empty() {
331+
// As long as there is a record, we can return the original hostname.
332+
// Although the spec says to return the canonical name, this is not
333+
// done by any drivers in practice since the majority of them use
334+
// libraries that do not follow CNAME chains. Also, we do not want to
335+
// use the canonical name since it will likely differ from the input
336+
// name, and the use of the input name is required for the service
337+
// principal to be accepted by the GSSAPI auth flow.
338+
hostname.to_lowercase().to_string()
337339
} else {
338-
Err(Error::authentication_error(
340+
return Err(Error::authentication_error(
339341
GSSAPI_STR,
340342
&format!("No addresses found for hostname: {hostname}"),
341-
))
343+
));
342344
}
343345
}
344346
CanonicalizeHostName::ForwardAndReverse => {
@@ -350,20 +352,27 @@ async fn canonicalize_hostname(
350352
match resolver.reverse_lookup(first_address).await {
351353
Ok(reverse_lookup) => {
352354
if let Some(name) = reverse_lookup.iter().next() {
353-
Ok(name.to_lowercase().to_string())
355+
name.to_lowercase().to_string()
354356
} else {
355-
Ok(hostname.to_lowercase())
357+
hostname.to_lowercase()
356358
}
357359
}
358-
Err(_) => Ok(hostname.to_lowercase()),
360+
Err(_) => hostname.to_lowercase(),
359361
}
360362
} else {
361-
Err(Error::authentication_error(
363+
return Err(Error::authentication_error(
362364
GSSAPI_STR,
363365
&format!("No addresses found for hostname: {hostname}"),
364-
))
366+
));
365367
}
366368
}
367369
CanonicalizeHostName::None => unreachable!(),
368-
}
370+
};
371+
372+
// Sometimes reverse lookup results in a trailing "." since that is the correct
373+
// way to present a FQDN. However, GSSAPI rejects the trailing "." so we remove
374+
// it here manually.
375+
let hostname = hostname.trim_end_matches(".");
376+
377+
Ok(hostname.to_string())
369378
}

src/test/auth.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
#[cfg(feature = "aws-auth")]
22
mod aws;
3+
#[cfg(feature = "gssapi-auth")]
4+
#[path = "auth/gssapi.rs"]
5+
mod gssapi_skip_local;
36

47
use serde::Deserialize;
58

src/test/auth/gssapi.rs

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
use crate::{
2+
bson::{doc, Document},
3+
Client,
4+
};
5+
6+
/// Run a GSSAPI e2e test.
7+
/// - user_principal_var is the name of the environment variable that stores the user principal
8+
/// - gssapi_db_var is the name tof the environment variable that stores the db name to query
9+
/// - auth_mechanism_properties is an optional set of authMechanismProperties to append to the uri
10+
async fn run_gssapi_auth_test(
11+
user_principal_var: &str,
12+
gssapi_db_var: &str,
13+
auth_mechanism_properties: Option<&str>,
14+
) {
15+
// Get env variables
16+
let host = std::env::var("SASL_HOST").expect("SASL_HOST not set");
17+
let user_principal = std::env::var(user_principal_var)
18+
.unwrap_or_else(|_| panic!("{user_principal_var} not set"))
19+
.replace("@", "%40");
20+
let gssapi_db =
21+
std::env::var(gssapi_db_var).unwrap_or_else(|_| panic!("{gssapi_db_var} not set"));
22+
23+
// Optionally create authMechanismProperties
24+
let props = if let Some(auth_mech_props) = auth_mechanism_properties {
25+
format!("&authMechanismProperties={auth_mech_props}")
26+
} else {
27+
String::new()
28+
};
29+
30+
// Create client
31+
let uri = format!(
32+
"mongodb://{user_principal}@{host}/?authSource=%24external&authMechanism=GSSAPI{props}"
33+
);
34+
let client = Client::with_uri_str(uri)
35+
.await
36+
.expect("failed to create MongoDB Client");
37+
38+
// Check that auth worked by qurying the test collection
39+
let coll = client.database(&gssapi_db).collection::<Document>("test");
40+
let doc = coll.find_one(doc! {}).await;
41+
match doc {
42+
Ok(Some(doc)) => {
43+
assert!(
44+
doc.get_bool(&gssapi_db).unwrap(),
45+
"expected '{gssapi_db}' field to exist and be 'true'"
46+
);
47+
assert_eq!(
48+
doc.get_str("authenticated").unwrap(),
49+
"yeah",
50+
"unexpected 'authenticated' value"
51+
);
52+
}
53+
Ok(None) => panic!("expected `find_one` to return a document, but it did not"),
54+
Err(e) => panic!("expected `find_one` to return a document, but it failed: {e:?}"),
55+
}
56+
}
57+
58+
#[tokio::test]
59+
async fn no_options() {
60+
run_gssapi_auth_test("PRINCIPAL", "GSSAPI_DB", None).await
61+
}
62+
63+
#[tokio::test]
64+
async fn explicit_canonicalize_host_name_false() {
65+
run_gssapi_auth_test(
66+
"PRINCIPAL",
67+
"GSSAPI_DB",
68+
Some("CANONICALIZE_HOST_NAME:false"),
69+
)
70+
.await
71+
}
72+
73+
#[tokio::test]
74+
async fn canonicalize_host_name_forward() {
75+
run_gssapi_auth_test(
76+
"PRINCIPAL",
77+
"GSSAPI_DB",
78+
Some("CANONICALIZE_HOST_NAME:forward"),
79+
)
80+
.await
81+
}
82+
83+
#[tokio::test]
84+
async fn canonicalize_host_name_forward_and_reverse() {
85+
run_gssapi_auth_test(
86+
"PRINCIPAL",
87+
"GSSAPI_DB",
88+
Some("CANONICALIZE_HOST_NAME:forwardAndReverse"),
89+
)
90+
.await
91+
}
92+
93+
#[tokio::test]
94+
async fn with_service_realm_and_host_options() {
95+
// This test uses a "cross-realm" user principal, however the service principal is not
96+
// cross-realm. This is why we use SASL_REALM and SASL_HOST instead of SASL_REALM_CROSS
97+
// and SASL_HOST_CROSS.
98+
let service_realm = std::env::var("SASL_REALM").expect("SASL_REALM not set");
99+
let service_host = std::env::var("SASL_HOST").expect("SASL_HOST not set");
100+
101+
run_gssapi_auth_test(
102+
"PRINCIPAL_CROSS",
103+
"GSSAPI_DB_CROSS",
104+
Some(format!("SERVICE_REALM:{service_realm},SERVICE_HOST:{service_host}").as_str()),
105+
)
106+
.await
107+
}

0 commit comments

Comments
 (0)