refactor(app): add application layer module entry (#1907)

This commit is contained in:
安正超
2026-02-22 22:15:37 +08:00
committed by GitHub
parent 4a6e81d427
commit 4211652991
15 changed files with 3009 additions and 1044 deletions
+16
View File
@@ -0,0 +1,16 @@
// 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.
//! Application layer module entry.
//! Concrete use-case modules will be introduced incrementally in Phase 3.
+1
View File
@@ -13,6 +13,7 @@
// limitations under the License.
mod admin;
mod app;
mod auth;
mod config;
mod error;
+4 -1
View File
@@ -228,7 +228,6 @@ where
if ConditionalCorsLayer::is_s3_path(&path)
&& !bucket.is_empty()
&& cors_origins.is_some()
&& let Some(cors_headers) = apply_cors_headers(&bucket, &method_clone, &request_headers_clone).await
{
for (key, value) in cors_headers.iter() {
@@ -246,6 +245,10 @@ where
});
}
if method == Method::OPTIONS && ConditionalCorsLayer::is_s3_path(&path) {
return Box::pin(async move { Ok(Response::builder().status(StatusCode::OK).body(ResBody::default()).unwrap()) });
}
let mut inner = self.inner.clone();
Box::pin(async move {
let mut response = inner.call(req).await.map_err(Into::into)?;
+246 -15
View File
@@ -13,10 +13,21 @@
// limitations under the License.
use super::ecfs::FS;
use super::ecfs::{
ACL_GROUP_ALL_USERS, ACL_GROUP_AUTHENTICATED_USERS, INTERNAL_ACL_METADATA_KEY, StoredAcl, default_owner,
parse_acl_json_or_canned_bucket, parse_acl_json_or_canned_object, stored_acl_from_canned_bucket,
stored_acl_from_canned_object,
};
use super::options::get_opts;
use crate::auth::{check_key_valid, get_condition_values, get_session_token};
use crate::error::ApiError;
use crate::license::license_check;
use crate::server::RemoteAddr;
use rustfs_ecstore::StorageAPI;
use rustfs_ecstore::bucket::metadata_sys;
use rustfs_ecstore::bucket::policy_sys::PolicySys;
use rustfs_ecstore::error::StorageError;
use rustfs_ecstore::new_object_layer_fn;
use rustfs_iam::error::Error as IamError;
use rustfs_policy::policy::action::{Action, S3Action};
use rustfs_policy::policy::{Args, BucketPolicyArgs};
@@ -36,11 +47,164 @@ pub(crate) struct ReqInfo {
pub region: Option<String>,
}
#[derive(Clone, Copy)]
enum AclTarget {
Bucket,
Object,
}
#[derive(Clone, Copy)]
enum AclPermission {
Read,
Write,
ReadAcp,
WriteAcp,
}
fn acl_permission_for_action(action: &Action) -> Option<(AclTarget, AclPermission)> {
match action {
Action::S3Action(S3Action::ListBucketAction) => Some((AclTarget::Bucket, AclPermission::Read)),
Action::S3Action(S3Action::PutObjectAction) => Some((AclTarget::Bucket, AclPermission::Write)),
Action::S3Action(S3Action::GetBucketAclAction) => Some((AclTarget::Bucket, AclPermission::ReadAcp)),
Action::S3Action(S3Action::PutBucketAclAction) => Some((AclTarget::Bucket, AclPermission::WriteAcp)),
Action::S3Action(S3Action::GetObjectAction) => Some((AclTarget::Object, AclPermission::Read)),
Action::S3Action(S3Action::GetObjectAclAction) => Some((AclTarget::Object, AclPermission::ReadAcp)),
Action::S3Action(S3Action::PutObjectAclAction) => Some((AclTarget::Object, AclPermission::WriteAcp)),
_ => None,
}
}
fn permission_matches(grant_perm: &str, required: AclPermission) -> bool {
if grant_perm == Permission::FULL_CONTROL {
return true;
}
match required {
AclPermission::Read => grant_perm == Permission::READ,
AclPermission::Write => grant_perm == Permission::WRITE,
AclPermission::ReadAcp => grant_perm == Permission::READ_ACP,
AclPermission::WriteAcp => grant_perm == Permission::WRITE_ACP,
}
}
fn acl_allows(
acl: &StoredAcl,
user_id: Option<&str>,
is_authenticated: bool,
required: AclPermission,
ignore_public_acls: bool,
) -> bool {
for grant in &acl.grants {
if !permission_matches(grant.permission.as_str(), required) {
continue;
}
match grant.grantee.grantee_type.as_str() {
"CanonicalUser" => {
if user_id.is_some_and(|id| grant.grantee.id.as_deref() == Some(id)) {
return true;
}
}
"Group" => {
if ignore_public_acls {
continue;
}
if let Some(uri) = grant.grantee.uri.as_deref() {
if uri == ACL_GROUP_ALL_USERS {
return true;
}
if uri == ACL_GROUP_AUTHENTICATED_USERS && is_authenticated {
return true;
}
}
}
_ => {}
}
}
false
}
async fn load_bucket_acl(bucket: &str) -> S3Result<StoredAcl> {
let owner = default_owner();
match metadata_sys::get_bucket_acl_config(bucket).await {
Ok((acl, _)) => Ok(parse_acl_json_or_canned_bucket(&acl, &owner)),
Err(err) => {
if err == StorageError::ConfigNotFound {
Ok(stored_acl_from_canned_bucket(BucketCannedACL::PRIVATE, &owner))
} else {
Err(S3Error::with_message(S3ErrorCode::InternalError, err.to_string()))
}
}
}
}
async fn load_object_acl(bucket: &str, object: &str, version_id: Option<&str>, headers: &http::HeaderMap) -> S3Result<StoredAcl> {
let Some(store) = new_object_layer_fn() else {
return Err(S3Error::with_message(S3ErrorCode::InternalError, "Not init".to_string()));
};
let opts = get_opts(bucket, object, version_id.map(|v| v.to_string()), None, headers)
.await
.map_err(ApiError::from)?;
let info = store.get_object_info(bucket, object, &opts).await.map_err(ApiError::from)?;
let bucket_owner = default_owner();
let object_owner = info
.user_defined
.get(INTERNAL_ACL_METADATA_KEY)
.and_then(|acl| serde_json::from_str::<StoredAcl>(acl).ok())
.map(|acl| acl.owner)
.unwrap_or_else(default_owner);
Ok(info
.user_defined
.get(INTERNAL_ACL_METADATA_KEY)
.map(|acl| parse_acl_json_or_canned_object(acl, &bucket_owner, &object_owner))
.unwrap_or_else(|| stored_acl_from_canned_object(ObjectCannedACL::PRIVATE, &bucket_owner, &object_owner)))
}
async fn check_acl_access<T>(req: &S3Request<T>, req_info: &ReqInfo, action: &Action, policy_allowed: bool) -> S3Result<bool> {
if req_info.is_owner || policy_allowed {
return Ok(true);
}
let Some((target, permission)) = acl_permission_for_action(action) else {
return Ok(true);
};
let bucket = req_info.bucket.as_deref().unwrap_or("");
if bucket.is_empty() {
return Ok(true);
}
let ignore_public_acls = match metadata_sys::get_public_access_block_config(bucket).await {
Ok((config, _)) => config.ignore_public_acls.unwrap_or(false),
Err(_) => false,
};
let user_id = req_info.cred.as_ref().map(|cred| cred.access_key.as_str());
let is_authenticated = user_id.is_some();
let acl = match target {
AclTarget::Bucket => load_bucket_acl(bucket).await?,
AclTarget::Object => {
let object = req_info.object.as_deref().unwrap_or("");
if object.is_empty() {
return Ok(true);
}
load_object_acl(bucket, object, req_info.version_id.as_deref(), &req.headers).await?
}
};
Ok(acl_allows(&acl, user_id, is_authenticated, permission, ignore_public_acls))
}
/// Authorizes the request based on the action and credentials.
pub async fn authorize_request<T>(req: &mut S3Request<T>, action: Action) -> S3Result<()> {
let remote_addr = req.extensions.get::<Option<RemoteAddr>>().and_then(|opt| opt.map(|a| a.0));
let req_info = req.extensions.get_mut::<ReqInfo>().expect("ReqInfo not found");
let req_info = req.extensions.get::<ReqInfo>().expect("ReqInfo not found");
if let Some(cred) = &req_info.cred {
let Ok(iam_store) = rustfs_iam::get() else {
@@ -53,6 +217,23 @@ pub async fn authorize_request<T>(req: &mut S3Request<T>, action: Action) -> S3R
let default_claims = HashMap::new();
let claims = cred.claims.as_ref().unwrap_or(&default_claims);
let conditions = get_condition_values(&req.headers, cred, req_info.version_id.as_deref(), None, remote_addr);
let bucket_name = req_info.bucket.as_deref().unwrap_or("");
if !bucket_name.is_empty()
&& !PolicySys::is_allowed(&BucketPolicyArgs {
bucket: bucket_name,
action,
// Run this early check in deny-only mode so ACL/IAM fallbacks can still grant access.
is_owner: true,
account: &cred.access_key,
groups: &cred.groups,
conditions: &conditions,
object: req_info.object.as_deref().unwrap_or(""),
})
.await
{
return Err(s3_error!(AccessDenied, "Access Denied"));
}
if action == Action::S3Action(S3Action::DeleteObjectAction)
&& req_info.version_id.is_some()
@@ -83,7 +264,7 @@ pub async fn authorize_request<T>(req: &mut S3Request<T>, action: Action) -> S3R
return Err(s3_error!(AccessDenied, "Access Denied"));
}
if iam_store
let iam_allowed = iam_store
.is_allowed(&Args {
account: &cred.access_key,
groups: &cred.groups,
@@ -95,9 +276,29 @@ pub async fn authorize_request<T>(req: &mut S3Request<T>, action: Action) -> S3R
claims,
deny_only: false,
})
.await
{
return Ok(());
.await;
if iam_allowed {
let policy_allowed = if !bucket_name.is_empty() {
PolicySys::is_allowed(&BucketPolicyArgs {
bucket: bucket_name,
action,
is_owner: false,
account: &cred.access_key,
groups: &cred.groups,
conditions: &conditions,
object: req_info.object.as_deref().unwrap_or(""),
})
.await
} else {
false
};
if check_acl_access(req, req_info, &action, policy_allowed).await? {
return Ok(());
}
return Err(s3_error!(AccessDenied, "Access Denied"));
}
if PolicySys::is_allowed(&BucketPolicyArgs {
@@ -154,6 +355,23 @@ pub async fn authorize_request<T>(req: &mut S3Request<T>, action: Action) -> S3R
req.region.as_deref(),
remote_addr,
);
let bucket_name = req_info.bucket.as_deref().unwrap_or("");
if !bucket_name.is_empty()
&& !PolicySys::is_allowed(&BucketPolicyArgs {
bucket: bucket_name,
action,
// Run this early check in deny-only mode so ACL checks are not bypassed.
is_owner: true,
account: "",
groups: &None,
conditions: &conditions,
object: req_info.object.as_deref().unwrap_or(""),
})
.await
{
return Err(s3_error!(AccessDenied, "Access Denied"));
}
if action != Action::S3Action(S3Action::ListAllMyBucketsAction) {
if PolicySys::is_allowed(&BucketPolicyArgs {
@@ -184,6 +402,10 @@ pub async fn authorize_request<T>(req: &mut S3Request<T>, action: Action) -> S3R
{
return Ok(());
}
if acl_permission_for_action(&action).is_some() && check_acl_access(req, req_info, &action, false).await? {
return Ok(());
}
}
}
@@ -502,8 +724,11 @@ impl S3Access for FS {
/// Checks whether the DeletePublicAccessBlock request has accesses to the resources.
///
/// This method returns `Ok(())` by default.
async fn delete_public_access_block(&self, _req: &mut S3Request<DeletePublicAccessBlockInput>) -> S3Result<()> {
Ok(())
async fn delete_public_access_block(&self, req: &mut S3Request<DeletePublicAccessBlockInput>) -> S3Result<()> {
let req_info = req.extensions.get_mut::<ReqInfo>().expect("ReqInfo not found");
req_info.bucket = Some(req.input.bucket.clone());
authorize_request(req, Action::S3Action(S3Action::DeleteBucketPublicAccessBlockAction)).await
}
/// Checks whether the GetBucketAccelerateConfiguration request has accesses to the resources.
@@ -523,7 +748,7 @@ impl S3Access for FS {
let req_info = req.extensions.get_mut::<ReqInfo>().expect("ReqInfo not found");
req_info.bucket = Some(req.input.bucket.clone());
authorize_request(req, Action::S3Action(S3Action::GetBucketPolicyAction)).await
authorize_request(req, Action::S3Action(S3Action::GetBucketAclAction)).await
}
/// Checks whether the GetBucketAnalyticsConfiguration request has accesses to the resources.
@@ -640,7 +865,7 @@ impl S3Access for FS {
let req_info = req.extensions.get_mut::<ReqInfo>().expect("ReqInfo not found");
req_info.bucket = Some(req.input.bucket.clone());
authorize_request(req, Action::S3Action(S3Action::GetBucketPolicyAction)).await
authorize_request(req, Action::S3Action(S3Action::GetObjectAclAction)).await
}
/// Checks whether the GetBucketPolicyStatus request has accesses to the resources.
@@ -797,8 +1022,11 @@ impl S3Access for FS {
/// Checks whether the GetPublicAccessBlock request has accesses to the resources.
///
/// This method returns `Ok(())` by default.
async fn get_public_access_block(&self, _req: &mut S3Request<GetPublicAccessBlockInput>) -> S3Result<()> {
Ok(())
async fn get_public_access_block(&self, req: &mut S3Request<GetPublicAccessBlockInput>) -> S3Result<()> {
let req_info = req.extensions.get_mut::<ReqInfo>().expect("ReqInfo not found");
req_info.bucket = Some(req.input.bucket.clone());
authorize_request(req, Action::S3Action(S3Action::GetBucketPublicAccessBlockAction)).await
}
/// Checks whether the HeadBucket request has accesses to the resources.
@@ -935,7 +1163,7 @@ impl S3Access for FS {
let req_info = req.extensions.get_mut::<ReqInfo>().expect("ReqInfo not found");
req_info.bucket = Some(req.input.bucket.clone());
authorize_request(req, Action::S3Action(S3Action::PutBucketPolicyAction)).await
authorize_request(req, Action::S3Action(S3Action::PutBucketAclAction)).await
}
/// Checks whether the PutBucketAnalyticsConfiguration request has accesses to the resources.
@@ -1042,7 +1270,7 @@ impl S3Access for FS {
let req_info = req.extensions.get_mut::<ReqInfo>().expect("ReqInfo not found");
req_info.bucket = Some(req.input.bucket.clone());
authorize_request(req, Action::S3Action(S3Action::PutBucketPolicyAction)).await
authorize_request(req, Action::S3Action(S3Action::PutObjectAclAction)).await
}
/// Checks whether the PutBucketReplication request has accesses to the resources.
@@ -1169,8 +1397,11 @@ impl S3Access for FS {
/// Checks whether the PutPublicAccessBlock request has accesses to the resources.
///
/// This method returns `Ok(())` by default.
async fn put_public_access_block(&self, _req: &mut S3Request<PutPublicAccessBlockInput>) -> S3Result<()> {
Ok(())
async fn put_public_access_block(&self, req: &mut S3Request<PutPublicAccessBlockInput>) -> S3Result<()> {
let req_info = req.extensions.get_mut::<ReqInfo>().expect("ReqInfo not found");
req_info.bucket = Some(req.input.bucket.clone());
authorize_request(req, Action::S3Action(S3Action::PutBucketPublicAccessBlockAction)).await
}
/// Checks whether the RestoreObject request has accesses to the resources.
+2314 -614
View File
File diff suppressed because it is too large Load Diff
+169 -2
View File
@@ -17,7 +17,7 @@ use crate::config::workload_profiles::{
};
use crate::error::ApiError;
use crate::server::cors;
use crate::storage::ecfs::ListObjectUnorderedQuery;
use crate::storage::ecfs::{InMemoryAsyncReader, ListObjectUnorderedQuery};
use http::{HeaderMap, HeaderValue, StatusCode};
use metrics::counter;
use rustfs_ecstore::bucket::metadata_sys;
@@ -27,6 +27,9 @@ use rustfs_ecstore::bucket::replication::ReplicationConfigurationExt;
use rustfs_ecstore::error::StorageError;
use rustfs_ecstore::store_api::{BucketOptions, ObjectInfo, ObjectToDelete};
use rustfs_ecstore::{StorageAPI, new_object_layer_fn};
use rustfs_filemeta::ObjectPartInfo;
use rustfs_kms::{EncryptionMetadata, ObjectEncryptionContext, get_global_encryption_service};
use rustfs_rio::{DecryptReader, Reader, WarpReader};
use rustfs_targets::EventName;
use rustfs_targets::arn::{TargetID, TargetIDError};
use rustfs_utils::http::{
@@ -36,7 +39,7 @@ use rustfs_utils::http::{
use s3s::dto::{
Delimiter, LambdaFunctionConfiguration, NotificationConfigurationFilter, ObjectLockConfiguration, ObjectLockEnabled,
ObjectLockLegalHold, ObjectLockLegalHoldStatus, ObjectLockRetention, ObjectLockRetentionMode, QueueConfiguration,
TopicConfiguration,
ServerSideEncryption, TopicConfiguration,
};
use s3s::{S3Error, S3ErrorCode, S3Response, S3Result};
use serde_urlencoded::from_bytes;
@@ -46,6 +49,7 @@ use std::sync::Arc;
use time::OffsetDateTime;
use time::format_description::well_known::Rfc3339;
use time::{format_description::FormatItem, macros::format_description};
use tokio::io::AsyncRead;
use tracing::{debug, warn};
pub const RFC1123: &[FormatItem<'_>] =
@@ -63,6 +67,7 @@ pub const RFC1123: &[FormatItem<'_>] =
/// # Arguments
/// * `object_lock_config` - Optional bucket Object Lock configuration. If None, no retention is applied.
/// * `metadata` - Mutable reference to object metadata HashMap. Retention headers are inserted here.
#[allow(dead_code)]
pub(crate) fn apply_lock_retention(object_lock_config: Option<ObjectLockConfiguration>, metadata: &mut HashMap<String, String>) {
if metadata.contains_key(AMZ_OBJECT_LOCK_MODE_LOWER) || metadata.contains_key(AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE_LOWER) {
return;
@@ -187,6 +192,168 @@ pub(crate) fn get_buffer_size_opt_in(file_size: i64) -> usize {
buffer_size
}
pub(crate) async fn create_managed_encryption_material(
bucket: &str,
key: &str,
algorithm: &ServerSideEncryption,
kms_key_id: Option<String>,
original_size: i64,
) -> Result<crate::storage::ecfs::ManagedEncryptionMaterial, ApiError> {
let Some(service) = get_global_encryption_service().await else {
return Err(ApiError::from(StorageError::other("KMS encryption service is not initialized")));
};
if !is_managed_sse(algorithm) {
return Err(ApiError::from(StorageError::other(format!(
"Unsupported server-side encryption algorithm: {}",
algorithm.as_str()
))));
}
let algorithm_str = algorithm.as_str();
let mut context = ObjectEncryptionContext::new(bucket.to_string(), key.to_string());
if original_size >= 0 {
context = context.with_size(original_size as u64);
}
let mut kms_key_candidate = kms_key_id;
if kms_key_candidate.is_none() {
kms_key_candidate = service.get_default_key_id().cloned();
}
let kms_key_to_use = kms_key_candidate
.clone()
.ok_or_else(|| ApiError::from(StorageError::other("No KMS key available for managed server-side encryption")))?;
let (data_key, encrypted_data_key) = service
.create_data_key(&kms_key_candidate, &context)
.await
.map_err(|e| ApiError::from(StorageError::other(format!("Failed to create data key: {e}"))))?;
let metadata = EncryptionMetadata {
algorithm: algorithm_str.to_string(),
key_id: kms_key_to_use.clone(),
key_version: 1,
iv: data_key.nonce.to_vec(),
tag: None,
encryption_context: context.encryption_context.clone(),
encrypted_at: jiff::Zoned::now(),
original_size: if original_size >= 0 { original_size as u64 } else { 0 },
encrypted_data_key,
};
let mut headers = service.metadata_to_headers(&metadata);
headers.insert("x-rustfs-encryption-original-size".to_string(), metadata.original_size.to_string());
Ok(crate::storage::ecfs::ManagedEncryptionMaterial {
data_key,
headers,
kms_key_id: kms_key_to_use,
})
}
pub(crate) async fn decrypt_managed_encryption_key(
bucket: &str,
key: &str,
metadata: &HashMap<String, String>,
) -> Result<Option<([u8; 32], [u8; 12], Option<i64>)>, ApiError> {
if !metadata.contains_key("x-rustfs-encryption-key") {
return Ok(None);
}
let Some(service) = get_global_encryption_service().await else {
return Err(ApiError::from(StorageError::other("KMS encryption service is not initialized")));
};
let parsed = service
.headers_to_metadata(metadata)
.map_err(|e| ApiError::from(StorageError::other(format!("Failed to parse encryption metadata: {e}"))))?;
if parsed.iv.len() != 12 {
return Err(ApiError::from(StorageError::other("Invalid encryption nonce length; expected 12 bytes")));
}
let context = ObjectEncryptionContext::new(bucket.to_string(), key.to_string());
let data_key = service
.decrypt_data_key(&parsed.encrypted_data_key, &context)
.await
.map_err(|e| ApiError::from(StorageError::other(format!("Failed to decrypt data key: {e}"))))?;
let key_bytes = data_key.plaintext_key;
let mut nonce = [0u8; 12];
nonce.copy_from_slice(&parsed.iv[..12]);
let original_size = metadata
.get("x-rustfs-encryption-original-size")
.and_then(|s| s.parse::<i64>().ok());
Ok(Some((key_bytes, nonce, original_size)))
}
pub(crate) fn derive_part_nonce(base: [u8; 12], part_number: usize) -> [u8; 12] {
let mut nonce = base;
let current = u32::from_be_bytes([nonce[8], nonce[9], nonce[10], nonce[11]]);
let incremented = current.wrapping_add(part_number as u32);
nonce[8..12].copy_from_slice(&incremented.to_be_bytes());
nonce
}
pub(crate) async fn decrypt_multipart_managed_stream(
mut encrypted_stream: Box<dyn AsyncRead + Unpin + Send + Sync>,
parts: &[ObjectPartInfo],
key_bytes: [u8; 32],
base_nonce: [u8; 12],
) -> Result<(Box<dyn Reader>, i64), StorageError> {
let total_plain_capacity: usize = parts.iter().map(|part| part.actual_size.max(0) as usize).sum();
let mut plaintext = Vec::with_capacity(total_plain_capacity);
for part in parts {
if part.size == 0 {
continue;
}
let mut encrypted_part = vec![0u8; part.size];
tokio::io::AsyncReadExt::read_exact(&mut encrypted_stream, &mut encrypted_part)
.await
.map_err(|e| StorageError::other(format!("failed to read encrypted multipart segment {}: {}", part.number, e)))?;
let part_nonce = derive_part_nonce(base_nonce, part.number);
let cursor = std::io::Cursor::new(encrypted_part);
let mut decrypt_reader = DecryptReader::new(WarpReader::new(cursor), key_bytes, part_nonce);
tokio::io::AsyncReadExt::read_to_end(&mut decrypt_reader, &mut plaintext)
.await
.map_err(|e| StorageError::other(format!("failed to decrypt multipart segment {}: {}", part.number, e)))?;
}
let total_plain_size = plaintext.len() as i64;
let reader = Box::new(WarpReader::new(InMemoryAsyncReader::new(plaintext))) as Box<dyn Reader>;
Ok((reader, total_plain_size))
}
pub(crate) fn strip_managed_encryption_metadata(metadata: &mut HashMap<String, String>) {
const KEYS: [&str; 7] = [
"x-amz-server-side-encryption",
"x-amz-server-side-encryption-aws-kms-key-id",
"x-rustfs-encryption-iv",
"x-rustfs-encryption-tag",
"x-rustfs-encryption-key",
"x-rustfs-encryption-context",
"x-rustfs-encryption-original-size",
];
for key in KEYS.iter() {
metadata.remove(*key);
}
}
pub(crate) fn is_managed_sse(algorithm: &ServerSideEncryption) -> bool {
matches!(algorithm.as_str(), "AES256" | "aws:kms")
}
/// Validate object key for control characters and log special characters
///
/// This function:
+31 -157
View File
@@ -16,7 +16,7 @@
mod tests {
use crate::config::workload_profiles::WorkloadProfile;
use crate::storage::ecfs::FS;
use crate::storage::s3_api::common::rustfs_owner;
use crate::storage::ecfs::RUSTFS_OWNER;
use crate::storage::{
apply_cors_headers, check_preconditions, get_adaptive_buffer_size_with_profile, get_buffer_size_opt_in, is_etag_equal,
matches_origin_pattern, parse_etag, parse_object_lock_legal_hold, parse_object_lock_retention,
@@ -27,7 +27,6 @@ mod tests {
use rustfs_config::MI_B;
use rustfs_ecstore::set_disk::DEFAULT_READ_BUFFER_SIZE;
use rustfs_ecstore::store_api::ObjectInfo;
use rustfs_policy::policy::{BucketPolicy, Validator};
use rustfs_utils::http::{AMZ_OBJECT_LOCK_LEGAL_HOLD_LOWER, RESERVED_METADATA_PREFIX_LOWER};
use rustfs_zip::CompressionFormat;
use s3s::dto::{
@@ -67,12 +66,11 @@ mod tests {
}
#[test]
fn test_rustfs_owner_helper() {
// Test that rustfs owner metadata remains stable for S3 compatibility.
let owner = rustfs_owner();
assert!(!owner.display_name.as_ref().unwrap().is_empty());
assert!(!owner.id.as_ref().unwrap().is_empty());
assert_eq!(owner.display_name.as_ref().unwrap(), "rustfs");
fn test_rustfs_owner_constant() {
// Test that RUSTFS_OWNER constant is properly defined
assert!(!RUSTFS_OWNER.display_name.as_ref().unwrap().is_empty());
assert!(!RUSTFS_OWNER.id.as_ref().unwrap().is_empty());
assert_eq!(RUSTFS_OWNER.display_name.as_ref().unwrap(), "RustFS Tester");
}
// Note: Most S3 API methods require complex setup with global state, storage backend,
@@ -800,127 +798,6 @@ mod tests {
assert_eq!(result13.unwrap_err().code(), &S3ErrorCode::InvalidArgument);
}
#[test]
fn test_apply_lock_retention() {
use crate::storage::ecfs_extend::apply_lock_retention;
use s3s::dto::{DefaultRetention, ObjectLockConfiguration, ObjectLockEnabled, ObjectLockRetentionMode, ObjectLockRule};
use std::collections::HashMap;
// [1] Normal case: Apply default retention with COMPLIANCE mode and days
let mut metadata = HashMap::new();
let config = Some(ObjectLockConfiguration {
object_lock_enabled: Some(ObjectLockEnabled::from_static(ObjectLockEnabled::ENABLED)),
rule: Some(ObjectLockRule {
default_retention: Some(DefaultRetention {
mode: Some(ObjectLockRetentionMode::from_static(ObjectLockRetentionMode::COMPLIANCE)),
days: Some(30),
years: None,
}),
}),
});
apply_lock_retention(config, &mut metadata);
assert_eq!(metadata.get("x-amz-object-lock-mode"), Some(&"COMPLIANCE".to_string()));
assert!(metadata.contains_key("x-amz-object-lock-retain-until-date"));
// [2] Normal case: Apply default retention with GOVERNANCE mode and years
let mut metadata = HashMap::new();
let config = Some(ObjectLockConfiguration {
object_lock_enabled: Some(ObjectLockEnabled::from_static(ObjectLockEnabled::ENABLED)),
rule: Some(ObjectLockRule {
default_retention: Some(DefaultRetention {
mode: Some(ObjectLockRetentionMode::from_static(ObjectLockRetentionMode::GOVERNANCE)),
days: None,
years: Some(1),
}),
}),
});
apply_lock_retention(config, &mut metadata);
assert_eq!(metadata.get("x-amz-object-lock-mode"), Some(&"GOVERNANCE".to_string()));
assert!(metadata.contains_key("x-amz-object-lock-retain-until-date"));
// [3] Skip case: No configuration provided
let mut metadata = HashMap::new();
apply_lock_retention(None, &mut metadata);
assert!(!metadata.contains_key("x-amz-object-lock-mode"));
assert!(!metadata.contains_key("x-amz-object-lock-retain-until-date"));
// [4] Skip case: Object Lock not enabled
let mut metadata = HashMap::new();
let config = Some(ObjectLockConfiguration {
object_lock_enabled: None,
rule: Some(ObjectLockRule {
default_retention: Some(DefaultRetention {
mode: Some(ObjectLockRetentionMode::from_static(ObjectLockRetentionMode::COMPLIANCE)),
days: Some(30),
years: None,
}),
}),
});
apply_lock_retention(config, &mut metadata);
assert!(!metadata.contains_key("x-amz-object-lock-mode"));
// [5] Skip case: Explicit retention already set (explicit takes precedence)
let mut metadata = HashMap::new();
metadata.insert("x-amz-object-lock-mode".to_string(), "GOVERNANCE".to_string());
metadata.insert("x-amz-object-lock-retain-until-date".to_string(), "2030-01-01T00:00:00Z".to_string());
let config = Some(ObjectLockConfiguration {
object_lock_enabled: Some(ObjectLockEnabled::from_static(ObjectLockEnabled::ENABLED)),
rule: Some(ObjectLockRule {
default_retention: Some(DefaultRetention {
mode: Some(ObjectLockRetentionMode::from_static(ObjectLockRetentionMode::COMPLIANCE)),
days: Some(30),
years: None,
}),
}),
});
apply_lock_retention(config, &mut metadata);
// Explicit retention should remain unchanged
assert_eq!(metadata.get("x-amz-object-lock-mode"), Some(&"GOVERNANCE".to_string()));
assert_eq!(
metadata.get("x-amz-object-lock-retain-until-date"),
Some(&"2030-01-01T00:00:00Z".to_string())
);
// [6] Skip case: No default retention configured
let mut metadata = HashMap::new();
let config = Some(ObjectLockConfiguration {
object_lock_enabled: Some(ObjectLockEnabled::from_static(ObjectLockEnabled::ENABLED)),
rule: Some(ObjectLockRule { default_retention: None }),
});
apply_lock_retention(config, &mut metadata);
assert!(!metadata.contains_key("x-amz-object-lock-mode"));
// [7] Skip case: No retention mode specified
let mut metadata = HashMap::new();
let config = Some(ObjectLockConfiguration {
object_lock_enabled: Some(ObjectLockEnabled::from_static(ObjectLockEnabled::ENABLED)),
rule: Some(ObjectLockRule {
default_retention: Some(DefaultRetention {
mode: None,
days: Some(30),
years: None,
}),
}),
});
apply_lock_retention(config, &mut metadata);
assert!(!metadata.contains_key("x-amz-object-lock-mode"));
// [8] Skip case: No retention period specified (neither days nor years)
let mut metadata = HashMap::new();
let config = Some(ObjectLockConfiguration {
object_lock_enabled: Some(ObjectLockEnabled::from_static(ObjectLockEnabled::ENABLED)),
rule: Some(ObjectLockRule {
default_retention: Some(DefaultRetention {
mode: Some(ObjectLockRetentionMode::from_static(ObjectLockRetentionMode::COMPLIANCE)),
days: None,
years: None,
}),
}),
});
apply_lock_retention(config, &mut metadata);
assert!(!metadata.contains_key("x-amz-object-lock-mode"));
}
// Note: S3Request structure is complex and requires many fields.
// For real testing, we would need proper integration test setup.
// Removing this test as it requires too much S3 infrastructure setup.
@@ -976,6 +853,31 @@ mod tests {
assert_eq!(formatted, "550e8400-e29b-41d4-a716-446655440000");
}
#[test]
fn test_delete_objects_version_id_normalization() {
use uuid::Uuid;
let fs = FS::new();
let (raw, uuid) = fs.normalize_delete_objects_version_id(Some("null".to_string())).unwrap();
assert_eq!(raw.as_deref(), Some("null"));
assert_eq!(uuid, Some(Uuid::nil()));
let valid = "550e8400-e29b-41d4-a716-446655440000".to_string();
let (raw, uuid) = fs.normalize_delete_objects_version_id(Some(valid.clone())).unwrap();
assert_eq!(raw.as_deref(), Some(valid.as_str()));
assert_eq!(uuid, Some(Uuid::parse_str(&valid).unwrap()));
let err = fs
.normalize_delete_objects_version_id(Some("not-a-uuid".to_string()))
.unwrap_err();
assert!(!err.is_empty());
let (raw, uuid) = fs.normalize_delete_objects_version_id(None).unwrap();
assert!(raw.is_none());
assert!(uuid.is_none());
}
/// Test that ListObjectVersionsOutput markers are correctly set
/// This verifies the fix for boto3 ParamValidationError
#[test]
@@ -1014,34 +916,6 @@ mod tests {
assert_eq!(filtered_version_marker.unwrap(), "null");
}
#[test]
fn test_bucket_policy_round_trip_preserves_original_json_text() {
let policy = r#"{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"AWS": "*"},
"Action": "s3:ListBucket",
"Resource": [
"arn:aws:s3:::example-bucket",
"arn:aws:s3:::example-bucket/*"
]
}]
}"#;
let parsed: BucketPolicy = serde_json::from_str(policy).unwrap();
assert!(parsed.is_valid().is_ok());
// Normalized serialization can differ (for example, Action becomes an array).
let normalized = serde_json::to_string(&parsed).unwrap();
assert_ne!(normalized, policy);
// Stored raw policy bytes must preserve exact text for GetBucketPolicy round trip.
let stored = policy.as_bytes().to_vec();
let round_trip = String::from_utf8(stored).unwrap();
assert_eq!(round_trip, policy);
}
#[test]
fn test_matches_origin_pattern_exact_match() {
// Test exact match
+6 -10
View File
@@ -14,21 +14,17 @@
pub mod access;
pub mod concurrency;
#[cfg(test)]
mod concurrent_get_object_test;
pub mod ecfs;
mod ecfs_extend;
pub(crate) mod entity;
pub(crate) mod helper;
pub mod options;
pub(crate) mod readers;
pub(crate) mod s3_api;
pub mod tonic_service;
pub(crate) use ecfs_extend::*;
#[cfg(test)]
mod concurrent_get_object_test;
mod ecfs_extend;
#[cfg(test)]
mod ecfs_test;
pub(crate) mod head_prefix;
mod objects;
mod sse;
#[cfg(test)]
mod sse_test;
pub(crate) use ecfs_extend::*;