mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-13 16:46:55 +00:00
c5264f9703
* improve code for metrics and switch tokio-tar to astral-tokio-tar * remove log * fix
464 lines
16 KiB
Rust
464 lines
16 KiB
Rust
// Copyright 2024 RustFS Team
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
//! End-to-end tests for Vault KMS backend
|
|
//!
|
|
//! These tests mirror the local KMS coverage but target the Vault backend.
|
|
//! They validate Vault bootstrap, admin API flows, encryption modes, and
|
|
//! multipart upload behaviour.
|
|
|
|
use crate::common::{TEST_BUCKET, init_logging};
|
|
use md5::compute;
|
|
use serial_test::serial;
|
|
use tokio::time::{Duration, sleep};
|
|
use tracing::{error, info};
|
|
|
|
use super::common::{
|
|
VAULT_KEY_NAME, VaultTestEnvironment, get_kms_status, start_kms, test_all_multipart_encryption_types, test_error_scenarios,
|
|
test_kms_key_management, test_sse_c_encryption, test_sse_kms_encryption, test_sse_s3_encryption,
|
|
};
|
|
|
|
/// Helper that brings up Vault, configures RustFS, and starts the KMS service.
|
|
struct VaultKmsTestContext {
|
|
env: VaultTestEnvironment,
|
|
}
|
|
|
|
impl VaultKmsTestContext {
|
|
async fn new() -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
|
|
let mut env = VaultTestEnvironment::new().await?;
|
|
|
|
env.start_vault().await?;
|
|
env.setup_vault_transit().await?;
|
|
|
|
env.start_rustfs_for_vault().await?;
|
|
env.configure_vault_kms().await?;
|
|
|
|
start_kms(&env.base_env.url, &env.base_env.access_key, &env.base_env.secret_key).await?;
|
|
|
|
// Allow Vault to finish initialising token auth and transit engine.
|
|
sleep(Duration::from_secs(2)).await;
|
|
|
|
Ok(Self { env })
|
|
}
|
|
|
|
fn base_env(&self) -> &crate::common::RustFSTestEnvironment {
|
|
&self.env.base_env
|
|
}
|
|
|
|
fn s3_client(&self) -> aws_sdk_s3::Client {
|
|
self.env.base_env.create_s3_client()
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn test_vault_kms_end_to_end() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|
init_logging();
|
|
info!("Starting Vault KMS End-to-End Test with default key {}", VAULT_KEY_NAME);
|
|
|
|
let context = VaultKmsTestContext::new().await?;
|
|
|
|
match get_kms_status(&context.base_env().url, &context.base_env().access_key, &context.base_env().secret_key).await {
|
|
Ok(status) => info!("Vault KMS status after startup: {}", status),
|
|
Err(err) => {
|
|
error!("Failed to query Vault KMS status: {}", err);
|
|
return Err(err);
|
|
}
|
|
}
|
|
|
|
let s3_client = context.s3_client();
|
|
context
|
|
.base_env()
|
|
.create_test_bucket(TEST_BUCKET)
|
|
.await
|
|
.expect("Failed to create test bucket");
|
|
|
|
test_kms_key_management(&context.base_env().url, &context.base_env().access_key, &context.base_env().secret_key)
|
|
.await
|
|
.expect("Vault KMS key management test failed");
|
|
|
|
test_sse_c_encryption(&s3_client, TEST_BUCKET)
|
|
.await
|
|
.expect("Vault SSE-C encryption test failed");
|
|
|
|
test_sse_s3_encryption(&s3_client, TEST_BUCKET)
|
|
.await
|
|
.expect("Vault SSE-S3 encryption test failed");
|
|
|
|
test_sse_kms_encryption(&s3_client, TEST_BUCKET)
|
|
.await
|
|
.expect("Vault SSE-KMS encryption test failed");
|
|
|
|
test_error_scenarios(&s3_client, TEST_BUCKET)
|
|
.await
|
|
.expect("Vault KMS error scenario test failed");
|
|
|
|
context
|
|
.base_env()
|
|
.delete_test_bucket(TEST_BUCKET)
|
|
.await
|
|
.expect("Failed to delete test bucket");
|
|
|
|
info!("Vault KMS End-to-End Test completed successfully");
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn test_vault_kms_key_isolation() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|
init_logging();
|
|
info!("Starting Vault KMS SSE-C key isolation test");
|
|
|
|
let context = VaultKmsTestContext::new().await?;
|
|
|
|
let s3_client = context.s3_client();
|
|
context
|
|
.base_env()
|
|
.create_test_bucket(TEST_BUCKET)
|
|
.await
|
|
.expect("Failed to create test bucket");
|
|
|
|
let key1 = "01234567890123456789012345678901";
|
|
let key2 = "98765432109876543210987654321098";
|
|
let key1_b64 = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, key1);
|
|
let key2_b64 = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, key2);
|
|
let key1_md5 = format!("{:x}", compute(key1));
|
|
let key2_md5 = format!("{:x}", compute(key2));
|
|
|
|
let data1 = b"Vault data encrypted with key 1";
|
|
let data2 = b"Vault data encrypted with key 2";
|
|
|
|
s3_client
|
|
.put_object()
|
|
.bucket(TEST_BUCKET)
|
|
.key("vault-object1")
|
|
.body(aws_sdk_s3::primitives::ByteStream::from(data1.to_vec()))
|
|
.sse_customer_algorithm("AES256")
|
|
.sse_customer_key(&key1_b64)
|
|
.sse_customer_key_md5(&key1_md5)
|
|
.send()
|
|
.await
|
|
.expect("Failed to upload object1 with key1");
|
|
|
|
s3_client
|
|
.put_object()
|
|
.bucket(TEST_BUCKET)
|
|
.key("vault-object2")
|
|
.body(aws_sdk_s3::primitives::ByteStream::from(data2.to_vec()))
|
|
.sse_customer_algorithm("AES256")
|
|
.sse_customer_key(&key2_b64)
|
|
.sse_customer_key_md5(&key2_md5)
|
|
.send()
|
|
.await
|
|
.expect("Failed to upload object2 with key2");
|
|
|
|
let object1 = s3_client
|
|
.get_object()
|
|
.bucket(TEST_BUCKET)
|
|
.key("vault-object1")
|
|
.sse_customer_algorithm("AES256")
|
|
.sse_customer_key(&key1_b64)
|
|
.sse_customer_key_md5(&key1_md5)
|
|
.send()
|
|
.await
|
|
.expect("Failed to download object1 with key1");
|
|
|
|
let downloaded1 = object1.body.collect().await.expect("Failed to read object1").into_bytes();
|
|
assert_eq!(downloaded1.as_ref(), data1);
|
|
|
|
let wrong_key = s3_client
|
|
.get_object()
|
|
.bucket(TEST_BUCKET)
|
|
.key("vault-object1")
|
|
.sse_customer_algorithm("AES256")
|
|
.sse_customer_key(&key2_b64)
|
|
.sse_customer_key_md5(&key2_md5)
|
|
.send()
|
|
.await;
|
|
assert!(wrong_key.is_err(), "Object1 should not decrypt with key2");
|
|
|
|
context
|
|
.base_env()
|
|
.delete_test_bucket(TEST_BUCKET)
|
|
.await
|
|
.expect("Failed to delete test bucket");
|
|
|
|
info!("Vault KMS SSE-C key isolation test completed successfully");
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn test_vault_kms_large_file() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|
init_logging();
|
|
info!("Starting Vault KMS large file SSE-S3 test");
|
|
|
|
let context = VaultKmsTestContext::new().await?;
|
|
let s3_client = context.s3_client();
|
|
context
|
|
.base_env()
|
|
.create_test_bucket(TEST_BUCKET)
|
|
.await
|
|
.expect("Failed to create test bucket");
|
|
|
|
let large_data = vec![0xCDu8; 1024 * 1024];
|
|
let object_key = "vault-large-encrypted-file";
|
|
|
|
let put_response = s3_client
|
|
.put_object()
|
|
.bucket(TEST_BUCKET)
|
|
.key(object_key)
|
|
.body(aws_sdk_s3::primitives::ByteStream::from(large_data.clone()))
|
|
.server_side_encryption(aws_sdk_s3::types::ServerSideEncryption::Aes256)
|
|
.send()
|
|
.await
|
|
.expect("Failed to upload large SSE-S3 object");
|
|
assert_eq!(
|
|
put_response.server_side_encryption(),
|
|
Some(&aws_sdk_s3::types::ServerSideEncryption::Aes256)
|
|
);
|
|
|
|
let get_response = s3_client
|
|
.get_object()
|
|
.bucket(TEST_BUCKET)
|
|
.key(object_key)
|
|
.send()
|
|
.await
|
|
.expect("Failed to download large SSE-S3 object");
|
|
assert_eq!(
|
|
get_response.server_side_encryption(),
|
|
Some(&aws_sdk_s3::types::ServerSideEncryption::Aes256)
|
|
);
|
|
|
|
let downloaded = get_response
|
|
.body
|
|
.collect()
|
|
.await
|
|
.expect("Failed to read large object body")
|
|
.into_bytes();
|
|
assert_eq!(downloaded.len(), large_data.len());
|
|
assert_eq!(downloaded.as_ref(), large_data.as_slice());
|
|
|
|
context
|
|
.base_env()
|
|
.delete_test_bucket(TEST_BUCKET)
|
|
.await
|
|
.expect("Failed to delete test bucket");
|
|
|
|
info!("Vault KMS large file test completed successfully");
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn test_vault_kms_multipart_upload() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|
init_logging();
|
|
info!("Starting Vault KMS multipart upload encryption suite");
|
|
|
|
let context = VaultKmsTestContext::new().await?;
|
|
let s3_client = context.s3_client();
|
|
context
|
|
.base_env()
|
|
.create_test_bucket(TEST_BUCKET)
|
|
.await
|
|
.expect("Failed to create test bucket");
|
|
|
|
test_all_multipart_encryption_types(&s3_client, TEST_BUCKET, "vault-multipart")
|
|
.await
|
|
.expect("Vault multipart encryption test suite failed");
|
|
|
|
context
|
|
.base_env()
|
|
.delete_test_bucket(TEST_BUCKET)
|
|
.await
|
|
.expect("Failed to delete test bucket");
|
|
|
|
info!("Vault KMS multipart upload tests completed successfully");
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn test_vault_kms_key_operations() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|
init_logging();
|
|
info!("Starting Vault KMS key operations test (CRUD)");
|
|
|
|
let context = VaultKmsTestContext::new().await?;
|
|
test_vault_kms_key_crud(&context.base_env().url, &context.base_env().access_key, &context.base_env().secret_key).await?;
|
|
|
|
info!("Vault KMS key operations test completed successfully");
|
|
Ok(())
|
|
}
|
|
|
|
async fn test_vault_kms_key_crud(
|
|
base_url: &str,
|
|
access_key: &str,
|
|
secret_key: &str,
|
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|
info!("Testing Vault KMS key CRUD operations");
|
|
|
|
// Create with key name in tags
|
|
let test_key_name = "test-vault-key-crud";
|
|
let create_key_body = serde_json::json!({
|
|
"key_usage": "EncryptDecrypt",
|
|
"description": "Test key for CRUD operations",
|
|
"tags": {
|
|
"name": test_key_name,
|
|
"algorithm": "AES-256",
|
|
"created_by": "e2e_test",
|
|
"test_type": "crud"
|
|
}
|
|
})
|
|
.to_string();
|
|
|
|
let create_response =
|
|
crate::common::awscurl_post(&format!("{base_url}/rustfs/admin/v3/kms/keys"), &create_key_body, access_key, secret_key)
|
|
.await?;
|
|
|
|
let create_result: serde_json::Value = serde_json::from_str(&create_response)?;
|
|
let key_id = create_result["key_id"]
|
|
.as_str()
|
|
.ok_or("Failed to get key_id from create response")?;
|
|
info!("✅ Create: Created key with ID: {}", key_id);
|
|
|
|
// Read
|
|
let describe_response =
|
|
crate::common::awscurl_get(&format!("{base_url}/rustfs/admin/v3/kms/keys/{key_id}"), access_key, secret_key).await?;
|
|
|
|
let describe_result: serde_json::Value = serde_json::from_str(&describe_response)?;
|
|
assert_eq!(describe_result["key_metadata"]["key_id"], key_id);
|
|
assert_eq!(describe_result["key_metadata"]["key_usage"], "EncryptDecrypt");
|
|
assert_eq!(describe_result["key_metadata"]["key_state"], "Enabled");
|
|
|
|
// Verify that the key name was properly stored - MUST be present
|
|
let tags = describe_result["key_metadata"]["tags"]
|
|
.as_object()
|
|
.expect("Tags field must be present in key metadata");
|
|
|
|
let stored_name = tags
|
|
.get("name")
|
|
.and_then(|v| v.as_str())
|
|
.expect("Key name must be preserved in tags");
|
|
|
|
assert_eq!(stored_name, test_key_name, "Key name must match the name provided during creation");
|
|
|
|
// Verify other tags are also preserved
|
|
assert_eq!(
|
|
tags.get("algorithm")
|
|
.and_then(|v| v.as_str())
|
|
.expect("Algorithm tag must be present"),
|
|
"AES-256"
|
|
);
|
|
assert_eq!(
|
|
tags.get("created_by")
|
|
.and_then(|v| v.as_str())
|
|
.expect("Created_by tag must be present"),
|
|
"e2e_test"
|
|
);
|
|
assert_eq!(
|
|
tags.get("test_type")
|
|
.and_then(|v| v.as_str())
|
|
.expect("Test_type tag must be present"),
|
|
"crud"
|
|
);
|
|
|
|
info!("✅ Read: Successfully described key: {}", key_id);
|
|
|
|
// Read
|
|
let list_response =
|
|
crate::common::awscurl_get(&format!("{base_url}/rustfs/admin/v3/kms/keys"), access_key, secret_key).await?;
|
|
|
|
let list_result: serde_json::Value = serde_json::from_str(&list_response)?;
|
|
let keys = list_result["keys"]
|
|
.as_array()
|
|
.ok_or("Failed to get keys array from list response")?;
|
|
let found_key = keys.iter().find(|k| k["key_id"].as_str() == Some(key_id));
|
|
assert!(found_key.is_some(), "Created key not found in list");
|
|
|
|
// Verify key name in list response - MUST be present
|
|
let key = found_key.expect("Created key must be found in list");
|
|
let list_tags = key["tags"].as_object().expect("Tags field must be present in list response");
|
|
|
|
let listed_name = list_tags
|
|
.get("name")
|
|
.and_then(|v| v.as_str())
|
|
.expect("Key name must be preserved in list response");
|
|
|
|
assert_eq!(
|
|
listed_name, test_key_name,
|
|
"Key name in list must match the name provided during creation"
|
|
);
|
|
|
|
info!("✅ Read: Successfully listed keys, found test key");
|
|
|
|
// Delete
|
|
let delete_response = crate::common::execute_awscurl(
|
|
&format!("{base_url}/rustfs/admin/v3/kms/keys/delete?keyId={key_id}"),
|
|
"DELETE",
|
|
None,
|
|
access_key,
|
|
secret_key,
|
|
)
|
|
.await?;
|
|
|
|
// Parse and validate the delete response
|
|
let delete_result: serde_json::Value = serde_json::from_str(&delete_response)?;
|
|
assert_eq!(delete_result["success"], true, "Delete operation must return success=true");
|
|
info!("✅ Delete: Successfully deleted key: {}", key_id);
|
|
|
|
// Verify key state after deletion
|
|
let describe_deleted_response =
|
|
crate::common::awscurl_get(&format!("{base_url}/rustfs/admin/v3/kms/keys/{key_id}"), access_key, secret_key).await?;
|
|
|
|
let describe_result: serde_json::Value = serde_json::from_str(&describe_deleted_response)?;
|
|
let key_state = describe_result["key_metadata"]["key_state"]
|
|
.as_str()
|
|
.expect("Key state must be present after deletion");
|
|
|
|
// After deletion, key must not be in Enabled state
|
|
assert_ne!(key_state, "Enabled", "Deleted key must not remain in Enabled state");
|
|
|
|
// Key should be in PendingDeletion state after deletion
|
|
assert_eq!(key_state, "PendingDeletion", "Deleted key must be in PendingDeletion state");
|
|
|
|
info!("✅ Delete verification: Key state correctly changed to: {}", key_state);
|
|
|
|
// Force Delete - Force immediate deletion for PendingDeletion key
|
|
let force_delete_response = crate::common::execute_awscurl(
|
|
&format!("{base_url}/rustfs/admin/v3/kms/keys/delete?keyId={key_id}&force_immediate=true"),
|
|
"DELETE",
|
|
None,
|
|
access_key,
|
|
secret_key,
|
|
)
|
|
.await?;
|
|
|
|
// Parse and validate the force delete response
|
|
let force_delete_result: serde_json::Value = serde_json::from_str(&force_delete_response)?;
|
|
assert_eq!(force_delete_result["success"], true, "Force delete operation must return success=true");
|
|
info!("✅ Force Delete: Successfully force deleted key: {}", key_id);
|
|
|
|
// Verify key no longer exists after force deletion (should return error)
|
|
let describe_force_deleted_result =
|
|
crate::common::awscurl_get(&format!("{base_url}/rustfs/admin/v3/kms/keys/{key_id}"), access_key, secret_key).await;
|
|
|
|
// After force deletion, key should not be found (GET should fail)
|
|
assert!(describe_force_deleted_result.is_err(), "Force deleted key should not be found");
|
|
|
|
info!("✅ Force Delete verification: Key was permanently deleted and is no longer accessible");
|
|
|
|
info!("Vault KMS key CRUD operations completed successfully");
|
|
Ok(())
|
|
}
|