Skip to content

Commit 8cb9096

Browse files
RUST-1417 Add support for GCP attached service accounts when using GCP KMS (#877)
1 parent b18c756 commit 8cb9096

File tree

7 files changed

+245
-26
lines changed

7 files changed

+245
-26
lines changed

.evergreen/config.yml

Lines changed: 128 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -645,6 +645,61 @@ functions:
645645
-v \
646646
--fault revoked
647647
648+
"build and upload gcp kms test":
649+
- command: shell.exec
650+
params:
651+
shell: bash
652+
working_dir: "src"
653+
script: |
654+
${PREPARE_SHELL}
655+
656+
set +o xtrace
657+
export GCPKMS_GCLOUD=${GCPKMS_GCLOUD}
658+
export GCPKMS_PROJECT=${GCPKMS_PROJECT}
659+
export GCPKMS_ZONE=${GCPKMS_ZONE}
660+
export GCPKMS_INSTANCENAME=${GCPKMS_INSTANCENAME}
661+
set -o xtrace
662+
663+
mkdir test-contents
664+
cp -r $MONGOCRYPT_LIB_DIR test-contents
665+
666+
echo "Building test ... begin"
667+
. ${PROJECT_DIRECTORY}/.evergreen/configure-rust.sh
668+
cargo test get_exe_name --features in-use-encryption-unstable,gcp-kms -- --ignored
669+
cp $(cat exe_name.txt) test-contents/test-exe
670+
echo "Building test ... end"
671+
672+
echo "Copying test contents ... begin"
673+
tar czf test-contents.tgz test-contents
674+
GCPKMS_SRC=test-contents.tgz GCPKMS_DST=$GCPKMS_INSTANCENAME: $DRIVERS_TOOLS/.evergreen/csfle/gcpkms/copy-file.sh
675+
echo "Copying test contents ... end"
676+
677+
echo "Untarring test contents ... begin"
678+
GCPKMS_CMD="tar xf test-contents.tgz" $DRIVERS_TOOLS/.evergreen/csfle/gcpkms/run-command.sh
679+
echo "Untarring test contents ... end"
680+
681+
"run gcp kms test":
682+
- command: shell.exec
683+
type: test
684+
params:
685+
shell: bash
686+
working_dir: "src"
687+
script: |
688+
${PREPARE_SHELL}
689+
690+
set +o xtrace
691+
export GCPKMS_GCLOUD=${GCPKMS_GCLOUD}
692+
export GCPKMS_PROJECT=${GCPKMS_PROJECT}
693+
export GCPKMS_ZONE=${GCPKMS_ZONE}
694+
export GCPKMS_INSTANCENAME=${GCPKMS_INSTANCENAME}
695+
set -o xtrace
696+
697+
export GCPKMS_CMD="ON_DEMAND_GCP_CREDS_SHOULD_SUCCEED=1 \
698+
RUST_BACKTRACE=1 LD_LIBRARY_PATH=./test-contents/lib \
699+
./test-contents/test-exe on_demand_gcp_credentials --nocapture"
700+
$DRIVERS_TOOLS/.evergreen/csfle/gcpkms/run-command.sh
701+
702+
648703
"compile only":
649704
- command: shell.exec
650705
type: test
@@ -1274,6 +1329,11 @@ tasks:
12741329
commands:
12751330
- func: "run plain tests"
12761331

1332+
- name: "test-gcp-kms"
1333+
commands:
1334+
- func: "build and upload gcp kms test"
1335+
- func: "run gcp kms test"
1336+
12771337
- name: test-ocsp-rsa-valid-cert-server-staples
12781338
tags: ["ocsp", "ocsp-rsa", "ocsp-staple"]
12791339
commands:
@@ -1796,6 +1856,11 @@ axes:
17961856
variables:
17971857
VENV_BIN_DIR: "Scripts"
17981858
LIBMONGOCRYPT_OS: "windows-test"
1859+
- id: debian-11
1860+
display_name: "Debian 11"
1861+
run_on: debian11-small
1862+
variables:
1863+
LIBMONGOCRYPT_OS: "debian11"
17991864

18001865
- id: "versioned-api"
18011866
display_name: "Versioned API"
@@ -1926,6 +1991,49 @@ task_groups:
19261991
tasks:
19271992
- test-azure-kms
19281993

1994+
- name: testgcpkms_task_group
1995+
setup_group_can_fail_task: true
1996+
setup_group_timeout_secs: 1800 # 30 minutes
1997+
setup_group:
1998+
- func: "fetch source"
1999+
- func: "prepare resources"
2000+
- func: "windows fix"
2001+
- func: "fix absolute paths"
2002+
- func: "init test-results"
2003+
- func: "make files executable"
2004+
- func: "install rust"
2005+
- func: "install libmongocrypt"
2006+
- command: shell.exec
2007+
params:
2008+
shell: "bash"
2009+
script: |
2010+
${PREPARE_SHELL}
2011+
set +o xtrace
2012+
echo '${testgcpkms_key_file}' > /tmp/testgcpkms_key_file.json
2013+
export GCPKMS_KEYFILE=/tmp/testgcpkms_key_file.json
2014+
export GCPKMS_DRIVERS_TOOLS=$DRIVERS_TOOLS
2015+
export GCPKMS_SERVICEACCOUNT="${testgcpkms_service_account}"
2016+
set -o xtrace
2017+
$DRIVERS_TOOLS/.evergreen/csfle/gcpkms/create-and-setup-instance.sh
2018+
- command: expansions.update
2019+
params:
2020+
file: testgcpkms-expansions.yml
2021+
teardown_group:
2022+
- command: shell.exec
2023+
params:
2024+
shell: "bash"
2025+
script: |
2026+
${PREPARE_SHELL}
2027+
set +o xtrace
2028+
export GCPKMS_GCLOUD=${GCPKMS_GCLOUD}
2029+
export GCPKMS_PROJECT=${GCPKMS_PROJECT}
2030+
export GCPKMS_ZONE=${GCPKMS_ZONE}
2031+
export GCPKMS_INSTANCENAME=${GCPKMS_INSTANCENAME}
2032+
set -o xtrace
2033+
$DRIVERS_TOOLS/.evergreen/csfle/gcpkms/delete-instance.sh
2034+
tasks:
2035+
- test-gcp-kms
2036+
19292037
buildvariants:
19302038
-
19312039
matrix_name: "tests"
@@ -2210,6 +2318,7 @@ buildvariants:
22102318
# tasks:
22112319
# # Windows MongoDB servers do not staple OCSP responses and only support RSA.
22122320
# - name: ".ocsp-rsa !.ocsp-staple"
2321+
22132322
- matrix_name: "compile-only"
22142323
matrix_spec:
22152324
os:
@@ -2232,6 +2341,24 @@ buildvariants:
22322341
- ".6.0 .standalone"
22332342
- ".5.0 .standalone"
22342343

2344+
- matrix_name: "azure-kms"
2345+
display_name: "Azure KMS"
2346+
matrix_spec:
2347+
os:
2348+
- ubuntu-20.04
2349+
tasks:
2350+
- name: "azurekms_task_group"
2351+
batchtime: 20160
2352+
2353+
- matrix_name: "gcp-kms"
2354+
display_name: "GCP KMS"
2355+
matrix_spec:
2356+
os:
2357+
- debian-11
2358+
tasks:
2359+
- name: testgcpkms_task_group
2360+
batchtime: 20160
2361+
22352362
- name: "lint"
22362363
display_name: "! Lint"
22372364
run_on:
@@ -2241,13 +2368,4 @@ buildvariants:
22412368
- name: "check-rustfmt"
22422369
- name: "check-rustdoc"
22432370
- name: "check-manual"
2244-
- name: "check-cargo-deny"
2245-
2246-
- matrix_name: "azure-kms"
2247-
display_name: "Azure KMS"
2248-
matrix_spec:
2249-
os:
2250-
- ubuntu-20.04
2251-
tasks:
2252-
- name: "azurekms_task_group"
2253-
batchtime: 20160
2371+
- name: "check-cargo-deny"

.evergreen/env.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,5 @@ else
2222
# Turn off tracing for the very-spammy nvm script.
2323
set +o xtrace
2424
[ -s "$NVM_DIR/nvm.sh" ] && source "$NVM_DIR/nvm.sh"
25+
set -o xtrace
2526
fi

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@ aws-auth = ["reqwest"]
6969
# This can only be used with the tokio-runtime feature flag.
7070
azure-kms = ["reqwest"]
7171

72+
# Enable support for on-demand GCP KMS credentials.
73+
# This can only be used with the tokio-runtime feature flag.
74+
gcp-kms = ["reqwest"]
75+
7276
zstd-compression = ["zstd"]
7377
zlib-compression = ["flate2"]
7478
snappy-compression = ["snap"]

src/client/csfle/state_machine.rs

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ impl CryptExecutor {
214214
State::NeedKmsCredentials => {
215215
let ctx = result_mut(&mut ctx)?;
216216
#[allow(unused_mut)]
217-
let mut out = rawdoc! {};
217+
let mut kms_providers = rawdoc! {};
218218
let credentials = self.kms_providers.credentials();
219219
if credentials
220220
.get(&KmsProvider::Aws)
@@ -234,7 +234,7 @@ impl CryptExecutor {
234234
if let Some(token) = aws_creds.session_token() {
235235
creds.append("sessionToken", token);
236236
}
237-
out.append("aws", creds);
237+
kms_providers.append("aws", creds);
238238
}
239239
#[cfg(not(feature = "aws-auth"))]
240240
{
@@ -249,7 +249,7 @@ impl CryptExecutor {
249249
{
250250
#[cfg(feature = "azure-kms")]
251251
{
252-
out.append("azure", self.azure.get_token().await?);
252+
kms_providers.append("azure", self.azure.get_token().await?);
253253
}
254254
#[cfg(not(feature = "azure-kms"))]
255255
{
@@ -258,7 +258,70 @@ impl CryptExecutor {
258258
));
259259
}
260260
}
261-
ctx.provide_kms_providers(&out)?;
261+
if credentials
262+
.get(&KmsProvider::Gcp)
263+
.map_or(false, Document::is_empty)
264+
{
265+
#[cfg(feature = "gcp-kms")]
266+
{
267+
use crate::runtime::HttpClient;
268+
use reqwest::Method;
269+
use serde::Deserialize;
270+
271+
#[derive(Deserialize)]
272+
struct ResponseBody {
273+
access_token: String,
274+
}
275+
276+
fn kms_error(error: String) -> Error {
277+
let message = format!(
278+
"An error occurred when obtaining GCP credentials: {}",
279+
error
280+
);
281+
let error = mongocrypt::error::Error {
282+
kind: mongocrypt::error::ErrorKind::Kms,
283+
message: Some(message),
284+
code: None,
285+
};
286+
error.into()
287+
}
288+
289+
let http_client = HttpClient::default();
290+
let host = std::env::var("GCE_METADATA_HOST")
291+
.unwrap_or_else(|_| "metadata.google.internal".into());
292+
let uri = format!(
293+
"http://{}/computeMetadata/v1/instance/service-accounts/default/token",
294+
host
295+
);
296+
let headers = vec![("Metadata-Flavor", "Google")];
297+
let response = http_client
298+
.request(Method::GET, &uri, &headers)
299+
.await
300+
.map_err(|e| kms_error(e.to_string()))?;
301+
302+
if response.status().as_u16() != 200 {
303+
let error = match response.text().await {
304+
Ok(text) => text,
305+
Err(e) => format!("could not parse HTTP response: {}", e),
306+
};
307+
return Err(kms_error(error));
308+
}
309+
310+
let body: ResponseBody = response.json().await.map_err(|e| {
311+
let error = format!("could not parse HTTP response: {}", e);
312+
kms_error(error)
313+
})?;
314+
kms_providers
315+
.append("gcp", rawdoc! { "accessToken": body.access_token });
316+
}
317+
#[cfg(not(feature = "gcp-kms"))]
318+
{
319+
return Err(Error::invalid_argument(
320+
"On-demand GCP KMS credentials require the `gcp-kms` feature.",
321+
));
322+
}
323+
}
324+
ctx.provide_kms_providers(&kms_providers)?;
262325
}
263326
State::Ready => {
264327
let (tx, rx) = oneshot::channel();

src/runtime/http.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
// Suppress noisy warnings when a method is not used under a certain feature flag.
2+
#![allow(unused)]
3+
14
use reqwest::{IntoUrl, Method, Response};
25
use serde::Deserialize;
36

@@ -36,7 +39,6 @@ impl HttpClient {
3639
}
3740

3841
/// Executes an HTTP GET request and returns the response body as a string.
39-
#[allow(unused)]
4042
pub(crate) async fn get_and_read_string<'a>(
4143
&self,
4244
uri: &str,
@@ -47,7 +49,6 @@ impl HttpClient {
4749
}
4850

4951
/// Executes an HTTP PUT request and returns the response body as a string.
50-
#[allow(unused)]
5152
pub(crate) async fn put_and_read_string<'a>(
5253
&self,
5354
uri: &str,
@@ -58,7 +59,6 @@ impl HttpClient {
5859
}
5960

6061
/// Executes an HTTP request and returns the response body as a string.
61-
#[allow(unused)]
6262
pub(crate) async fn request_and_read_string<'a>(
6363
&self,
6464
method: Method,

src/test/atlas_planned_maintenance_testing/mod.rs

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,8 @@ use json_models::{Events, Results};
2929
use super::spec::unified_runner::EntityMap;
3030

3131
#[test]
32+
#[ignore]
3233
fn get_exe_name() {
33-
if env::var("ATLAS_PLANNED_MAINTENANCE_TESTING").is_err() {
34-
// This test should only be run from the workload-executor script.
35-
log_uncaptured(
36-
"Skipping get_exe_name due to being run outside of planned maintenance testing",
37-
);
38-
return;
39-
}
40-
4134
let mut file = File::create("exe_name.txt").expect("Failed to create file");
4235
let exe_name = env::current_exe()
4336
.expect("Failed to determine name of test executable")

src/test/csfle.rs

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2856,7 +2856,47 @@ async fn on_demand_aws_success() -> Result<()> {
28562856

28572857
// TODO RUST-1441: implement prose test 16. Rewrap
28582858

2859-
// TODO RUST-1417: implement prose test 17. On-demand GCP Credentials
2859+
// Prose test 17. On-demand GCP Credentials
2860+
#[cfg(feature = "gcp-kms")]
2861+
#[cfg_attr(feature = "tokio-runtime", tokio::test)]
2862+
#[cfg_attr(feature = "async-std-runtime", async_std::test)]
2863+
async fn on_demand_gcp_credentials() -> Result<()> {
2864+
let _guard = LOCK.run_exclusively().await;
2865+
2866+
let util_client = TestClient::new().await.into_client();
2867+
let client_encryption = ClientEncryption::new(
2868+
util_client,
2869+
KV_NAMESPACE.clone(),
2870+
[(KmsProvider::Gcp, doc! {}, None)],
2871+
)?;
2872+
2873+
let result = client_encryption
2874+
.create_data_key(MasterKey::Gcp {
2875+
project_id: "devprod-drivers".into(),
2876+
location: "global".into(),
2877+
key_ring: "key-ring-csfle".into(),
2878+
key_name: "key-name-csfle".into(),
2879+
key_version: None,
2880+
endpoint: None,
2881+
})
2882+
.run()
2883+
.await;
2884+
2885+
if std::env::var("ON_DEMAND_GCP_CREDS_SHOULD_SUCCEED").is_ok() {
2886+
result.unwrap();
2887+
} else {
2888+
let error = result.unwrap_err();
2889+
match *error.kind {
2890+
ErrorKind::Encryption(e) => {
2891+
assert!(matches!(e.kind, mongocrypt::error::ErrorKind::Kms));
2892+
assert!(e.message.unwrap().contains("GCP credentials"));
2893+
}
2894+
other => panic!("Expected encryption error, got {:?}", other),
2895+
}
2896+
}
2897+
2898+
Ok(())
2899+
}
28602900

28612901
// Prose test 18. Azure IMDS Credentials
28622902
#[cfg(feature = "azure-kms")]

0 commit comments

Comments
 (0)