Skip to content

RUST-1417 Add support for GCP attached service accounts when using GCP KMS #877

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

Merged
merged 2 commits into from
May 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
138 changes: 128 additions & 10 deletions .evergreen/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,61 @@ functions:
-v \
--fault revoked

"build and upload gcp kms test":
- command: shell.exec
params:
shell: bash
working_dir: "src"
script: |
${PREPARE_SHELL}

set +o xtrace
export GCPKMS_GCLOUD=${GCPKMS_GCLOUD}
export GCPKMS_PROJECT=${GCPKMS_PROJECT}
export GCPKMS_ZONE=${GCPKMS_ZONE}
export GCPKMS_INSTANCENAME=${GCPKMS_INSTANCENAME}
set -o xtrace

mkdir test-contents
cp -r $MONGOCRYPT_LIB_DIR test-contents

echo "Building test ... begin"
. ${PROJECT_DIRECTORY}/.evergreen/configure-rust.sh
cargo test get_exe_name --features in-use-encryption-unstable,gcp-kms -- --ignored
cp $(cat exe_name.txt) test-contents/test-exe
echo "Building test ... end"

echo "Copying test contents ... begin"
tar czf test-contents.tgz test-contents
GCPKMS_SRC=test-contents.tgz GCPKMS_DST=$GCPKMS_INSTANCENAME: $DRIVERS_TOOLS/.evergreen/csfle/gcpkms/copy-file.sh
echo "Copying test contents ... end"

echo "Untarring test contents ... begin"
GCPKMS_CMD="tar xf test-contents.tgz" $DRIVERS_TOOLS/.evergreen/csfle/gcpkms/run-command.sh
echo "Untarring test contents ... end"

"run gcp kms test":
- command: shell.exec
type: test
params:
shell: bash
working_dir: "src"
script: |
${PREPARE_SHELL}

set +o xtrace
export GCPKMS_GCLOUD=${GCPKMS_GCLOUD}
export GCPKMS_PROJECT=${GCPKMS_PROJECT}
export GCPKMS_ZONE=${GCPKMS_ZONE}
export GCPKMS_INSTANCENAME=${GCPKMS_INSTANCENAME}
set -o xtrace

export GCPKMS_CMD="ON_DEMAND_GCP_CREDS_SHOULD_SUCCEED=1 \
RUST_BACKTRACE=1 LD_LIBRARY_PATH=./test-contents/lib \
./test-contents/test-exe on_demand_gcp_credentials --nocapture"
$DRIVERS_TOOLS/.evergreen/csfle/gcpkms/run-command.sh


"compile only":
- command: shell.exec
type: test
Expand Down Expand Up @@ -1274,6 +1329,11 @@ tasks:
commands:
- func: "run plain tests"

- name: "test-gcp-kms"
commands:
- func: "build and upload gcp kms test"
- func: "run gcp kms test"

- name: test-ocsp-rsa-valid-cert-server-staples
tags: ["ocsp", "ocsp-rsa", "ocsp-staple"]
commands:
Expand Down Expand Up @@ -1796,6 +1856,11 @@ axes:
variables:
VENV_BIN_DIR: "Scripts"
LIBMONGOCRYPT_OS: "windows-test"
- id: debian-11
display_name: "Debian 11"
run_on: debian11-small
variables:
LIBMONGOCRYPT_OS: "debian11"

- id: "versioned-api"
display_name: "Versioned API"
Expand Down Expand Up @@ -1926,6 +1991,49 @@ task_groups:
tasks:
- test-azure-kms

- name: testgcpkms_task_group
setup_group_can_fail_task: true
setup_group_timeout_secs: 1800 # 30 minutes
setup_group:
- func: "fetch source"
- func: "prepare resources"
- func: "windows fix"
- func: "fix absolute paths"
- func: "init test-results"
- func: "make files executable"
- func: "install rust"
- func: "install libmongocrypt"
- command: shell.exec
params:
shell: "bash"
script: |
${PREPARE_SHELL}
set +o xtrace
echo '${testgcpkms_key_file}' > /tmp/testgcpkms_key_file.json
export GCPKMS_KEYFILE=/tmp/testgcpkms_key_file.json
export GCPKMS_DRIVERS_TOOLS=$DRIVERS_TOOLS
export GCPKMS_SERVICEACCOUNT="${testgcpkms_service_account}"
set -o xtrace
$DRIVERS_TOOLS/.evergreen/csfle/gcpkms/create-and-setup-instance.sh
- command: expansions.update
params:
file: testgcpkms-expansions.yml
teardown_group:
- command: shell.exec
params:
shell: "bash"
script: |
${PREPARE_SHELL}
set +o xtrace
export GCPKMS_GCLOUD=${GCPKMS_GCLOUD}
export GCPKMS_PROJECT=${GCPKMS_PROJECT}
export GCPKMS_ZONE=${GCPKMS_ZONE}
export GCPKMS_INSTANCENAME=${GCPKMS_INSTANCENAME}
set -o xtrace
$DRIVERS_TOOLS/.evergreen/csfle/gcpkms/delete-instance.sh
tasks:
- test-gcp-kms

buildvariants:
-
matrix_name: "tests"
Expand Down Expand Up @@ -2210,6 +2318,7 @@ buildvariants:
# tasks:
# # Windows MongoDB servers do not staple OCSP responses and only support RSA.
# - name: ".ocsp-rsa !.ocsp-staple"

- matrix_name: "compile-only"
matrix_spec:
os:
Expand All @@ -2232,6 +2341,24 @@ buildvariants:
- ".6.0 .standalone"
- ".5.0 .standalone"

- matrix_name: "azure-kms"
display_name: "Azure KMS"
matrix_spec:
os:
- ubuntu-20.04
tasks:
- name: "azurekms_task_group"
batchtime: 20160

- matrix_name: "gcp-kms"
display_name: "GCP KMS"
matrix_spec:
os:
- debian-11
tasks:
- name: testgcpkms_task_group
batchtime: 20160

- name: "lint"
display_name: "! Lint"
run_on:
Expand All @@ -2241,13 +2368,4 @@ buildvariants:
- name: "check-rustfmt"
- name: "check-rustdoc"
- name: "check-manual"
- name: "check-cargo-deny"

- matrix_name: "azure-kms"
display_name: "Azure KMS"
matrix_spec:
os:
- ubuntu-20.04
tasks:
- name: "azurekms_task_group"
batchtime: 20160
- name: "check-cargo-deny"
1 change: 1 addition & 0 deletions .evergreen/env.sh
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ else
# Turn off tracing for the very-spammy nvm script.
set +o xtrace
[ -s "$NVM_DIR/nvm.sh" ] && source "$NVM_DIR/nvm.sh"
set -o xtrace
fi
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ aws-auth = ["reqwest"]
# This can only be used with the tokio-runtime feature flag.
azure-kms = ["reqwest"]

# Enable support for on-demand GCP KMS credentials.
# This can only be used with the tokio-runtime feature flag.
gcp-kms = ["reqwest"]

zstd-compression = ["zstd"]
zlib-compression = ["flate2"]
snappy-compression = ["snap"]
Expand Down
71 changes: 67 additions & 4 deletions src/client/csfle/state_machine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ impl CryptExecutor {
State::NeedKmsCredentials => {
let ctx = result_mut(&mut ctx)?;
#[allow(unused_mut)]
let mut out = rawdoc! {};
let mut kms_providers = rawdoc! {};
let credentials = self.kms_providers.credentials();
if credentials
.get(&KmsProvider::Aws)
Expand All @@ -234,7 +234,7 @@ impl CryptExecutor {
if let Some(token) = aws_creds.session_token() {
creds.append("sessionToken", token);
}
out.append("aws", creds);
kms_providers.append("aws", creds);
}
#[cfg(not(feature = "aws-auth"))]
{
Expand All @@ -249,7 +249,7 @@ impl CryptExecutor {
{
#[cfg(feature = "azure-kms")]
{
out.append("azure", self.azure.get_token().await?);
kms_providers.append("azure", self.azure.get_token().await?);
}
#[cfg(not(feature = "azure-kms"))]
{
Expand All @@ -258,7 +258,70 @@ impl CryptExecutor {
));
}
}
ctx.provide_kms_providers(&out)?;
if credentials
.get(&KmsProvider::Gcp)
.map_or(false, Document::is_empty)
{
#[cfg(feature = "gcp-kms")]
{
use crate::runtime::HttpClient;
use reqwest::Method;
use serde::Deserialize;

#[derive(Deserialize)]
struct ResponseBody {
access_token: String,
}

fn kms_error(error: String) -> Error {
let message = format!(
"An error occurred when obtaining GCP credentials: {}",
error
);
let error = mongocrypt::error::Error {
kind: mongocrypt::error::ErrorKind::Kms,
message: Some(message),
code: None,
};
error.into()
}

let http_client = HttpClient::default();
let host = std::env::var("GCE_METADATA_HOST")
.unwrap_or_else(|_| "metadata.google.internal".into());
let uri = format!(
"http://{}/computeMetadata/v1/instance/service-accounts/default/token",
host
);
let headers = vec![("Metadata-Flavor", "Google")];
let response = http_client
.request(Method::GET, &uri, &headers)
.await
.map_err(|e| kms_error(e.to_string()))?;

if response.status().as_u16() != 200 {
let error = match response.text().await {
Ok(text) => text,
Err(e) => format!("could not parse HTTP response: {}", e),
};
return Err(kms_error(error));
}

let body: ResponseBody = response.json().await.map_err(|e| {
let error = format!("could not parse HTTP response: {}", e);
kms_error(error)
})?;
kms_providers
.append("gcp", rawdoc! { "accessToken": body.access_token });
}
#[cfg(not(feature = "gcp-kms"))]
{
return Err(Error::invalid_argument(
"On-demand GCP KMS credentials require the `gcp-kms` feature.",
));
}
}
ctx.provide_kms_providers(&kms_providers)?;
}
State::Ready => {
let (tx, rx) = oneshot::channel();
Expand Down
6 changes: 3 additions & 3 deletions src/runtime/http.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Suppress noisy warnings when a method is not used under a certain feature flag.
#![allow(unused)]

use reqwest::{IntoUrl, Method, Response};
use serde::Deserialize;

Expand Down Expand Up @@ -36,7 +39,6 @@ impl HttpClient {
}

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

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

/// Executes an HTTP request and returns the response body as a string.
#[allow(unused)]
pub(crate) async fn request_and_read_string<'a>(
&self,
method: Method,
Expand Down
9 changes: 1 addition & 8 deletions src/test/atlas_planned_maintenance_testing/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,8 @@ use json_models::{Events, Results};
use super::spec::unified_runner::EntityMap;

#[test]
#[ignore]
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This will require an update to our script in drivers-atlas-testing; I'll make a PR there once this one is merged.

fn get_exe_name() {
if env::var("ATLAS_PLANNED_MAINTENANCE_TESTING").is_err() {
// This test should only be run from the workload-executor script.
log_uncaptured(
"Skipping get_exe_name due to being run outside of planned maintenance testing",
);
return;
}

let mut file = File::create("exe_name.txt").expect("Failed to create file");
let exe_name = env::current_exe()
.expect("Failed to determine name of test executable")
Expand Down
42 changes: 41 additions & 1 deletion src/test/csfle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2856,7 +2856,47 @@ async fn on_demand_aws_success() -> Result<()> {

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

// TODO RUST-1417: implement prose test 17. On-demand GCP Credentials
// Prose test 17. On-demand GCP Credentials
#[cfg(feature = "gcp-kms")]
#[cfg_attr(feature = "tokio-runtime", tokio::test)]
#[cfg_attr(feature = "async-std-runtime", async_std::test)]
async fn on_demand_gcp_credentials() -> Result<()> {
let _guard = LOCK.run_exclusively().await;

let util_client = TestClient::new().await.into_client();
let client_encryption = ClientEncryption::new(
util_client,
KV_NAMESPACE.clone(),
[(KmsProvider::Gcp, doc! {}, None)],
)?;

let result = client_encryption
.create_data_key(MasterKey::Gcp {
project_id: "devprod-drivers".into(),
location: "global".into(),
key_ring: "key-ring-csfle".into(),
key_name: "key-name-csfle".into(),
key_version: None,
endpoint: None,
})
.run()
.await;

if std::env::var("ON_DEMAND_GCP_CREDS_SHOULD_SUCCEED").is_ok() {
result.unwrap();
} else {
let error = result.unwrap_err();
match *error.kind {
ErrorKind::Encryption(e) => {
assert!(matches!(e.kind, mongocrypt::error::ErrorKind::Kms));
assert!(e.message.unwrap().contains("GCP credentials"));
}
other => panic!("Expected encryption error, got {:?}", other),
}
}

Ok(())
}

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