mirror of
https://github.com/rustfs/rustfs.git
synced 2026-03-17 14:24:08 +00:00
fix: support presigned links from query params
This commit is contained in:
@@ -21,7 +21,8 @@ use crate::storage::ecfs::RUSTFS_OWNER;
|
||||
use crate::storage::entity;
|
||||
use crate::storage::helper::OperationHelper;
|
||||
use crate::storage::options::{
|
||||
copy_src_opts, extract_metadata, get_complete_multipart_upload_opts, get_content_sha256, parse_copy_source_range, put_opts,
|
||||
copy_src_opts, extract_metadata, get_complete_multipart_upload_opts, get_content_sha256_with_query, parse_copy_source_range,
|
||||
put_opts,
|
||||
};
|
||||
use crate::storage::*;
|
||||
use bytes::Bytes;
|
||||
@@ -615,7 +616,7 @@ impl DefaultMultipartUsecase {
|
||||
None
|
||||
};
|
||||
|
||||
let mut sha256hex = get_content_sha256(&req.headers);
|
||||
let mut sha256hex = get_content_sha256_with_query(&req.headers, req.uri.query());
|
||||
|
||||
if is_compressible {
|
||||
let mut hrd = HashReader::new(reader, size, actual_size, md5hex, sha256hex, false).map_err(ApiError::from)?;
|
||||
|
||||
@@ -26,7 +26,7 @@ use crate::storage::head_prefix::{head_prefix_not_found_message, probe_prefix_ha
|
||||
use crate::storage::helper::OperationHelper;
|
||||
use crate::storage::options::{
|
||||
copy_dst_opts, copy_src_opts, del_opts, extract_metadata, extract_metadata_from_mime_with_object_name,
|
||||
filter_object_metadata, get_content_sha256, get_opts, put_opts,
|
||||
filter_object_metadata, get_content_sha256_with_query, get_opts, put_opts,
|
||||
};
|
||||
use crate::storage::s3_api::multipart::parse_list_parts_params;
|
||||
use crate::storage::s3_api::{restore, select};
|
||||
@@ -461,7 +461,7 @@ impl DefaultObjectUsecase {
|
||||
None
|
||||
};
|
||||
|
||||
let mut sha256hex = get_content_sha256(&req.headers);
|
||||
let mut sha256hex = get_content_sha256_with_query(&req.headers, req.uri.query());
|
||||
|
||||
if is_compressible(&req.headers, &key) && size > MIN_COMPRESSIBLE_SIZE as i64 {
|
||||
let algorithm = CompressionAlgorithm::default();
|
||||
@@ -3521,7 +3521,7 @@ impl DefaultObjectUsecase {
|
||||
None
|
||||
};
|
||||
|
||||
let sha256hex = get_content_sha256(&req.headers);
|
||||
let sha256hex = get_content_sha256_with_query(&req.headers, req.uri.query());
|
||||
let actual_size = size;
|
||||
|
||||
let reader: Box<dyn Reader> = Box::new(WarpReader::new(body));
|
||||
|
||||
@@ -397,6 +397,29 @@ pub fn get_condition_values(
|
||||
version_id: Option<&str>,
|
||||
region: Option<s3s::region::Region>,
|
||||
remote_addr: Option<std::net::SocketAddr>,
|
||||
) -> HashMap<String, Vec<String>> {
|
||||
get_condition_values_with_query(header, cred, version_id, region, remote_addr, None)
|
||||
}
|
||||
|
||||
/// Get condition values for policy evaluation with optional query-string values.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `header` - HTTP headers of the request
|
||||
/// * `cred` - User credentials
|
||||
/// * `version_id` - Optional version ID of the object
|
||||
/// * `region` - Optional region/location constraint
|
||||
/// * `remote_addr` - Optional remote address of the connection
|
||||
/// * `query` - Optional request query string
|
||||
///
|
||||
/// # Returns
|
||||
/// * `HashMap<String, Vec<String>>` - Condition values for policy evaluation
|
||||
pub fn get_condition_values_with_query(
|
||||
header: &HeaderMap,
|
||||
cred: &Credentials,
|
||||
version_id: Option<&str>,
|
||||
region: Option<s3s::region::Region>,
|
||||
remote_addr: Option<std::net::SocketAddr>,
|
||||
query: Option<&str>,
|
||||
) -> HashMap<String, Vec<String>> {
|
||||
let username = if cred.is_temp() || cred.is_service_account() {
|
||||
cred.parent_user.clone()
|
||||
@@ -427,8 +450,8 @@ pub fn get_condition_values(
|
||||
// Use provided version ID or empty string
|
||||
let vid = version_id.unwrap_or("");
|
||||
|
||||
// Determine auth type and signature version from headers
|
||||
let (auth_type, signature_version) = determine_auth_type_and_version(header);
|
||||
// Determine auth type and signature version from headers and query
|
||||
let (auth_type, signature_version) = determine_auth_type_and_version_with_query(header, query);
|
||||
|
||||
// Get TLS status from header
|
||||
let is_tls = header
|
||||
@@ -582,10 +605,16 @@ pub fn get_condition_values(
|
||||
/// # Returns
|
||||
/// * `AuthType` - The determined authentication type
|
||||
///
|
||||
#[allow(dead_code)]
|
||||
pub fn get_request_auth_type(header: &HeaderMap) -> AuthType {
|
||||
get_request_auth_type_with_query(header, None)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn get_request_auth_type_with_query(header: &HeaderMap, query: Option<&str>) -> AuthType {
|
||||
if is_request_signature_v2(header) {
|
||||
AuthType::SignedV2
|
||||
} else if is_request_presigned_signature_v2(header) {
|
||||
} else if is_request_presigned_signature_v2(header, query) {
|
||||
AuthType::PresignedV2
|
||||
} else if is_request_sign_streaming_v4(header) {
|
||||
AuthType::StreamingSigned
|
||||
@@ -595,7 +624,7 @@ pub fn get_request_auth_type(header: &HeaderMap) -> AuthType {
|
||||
AuthType::StreamingUnsignedTrailer
|
||||
} else if is_request_signature_v4(header) {
|
||||
AuthType::Signed
|
||||
} else if is_request_presigned_signature_v4(header) {
|
||||
} else if is_request_presigned_signature_v4_with_query(header, query) {
|
||||
AuthType::Presigned
|
||||
} else if is_request_jwt(header) {
|
||||
AuthType::JWT
|
||||
@@ -618,8 +647,14 @@ pub fn get_request_auth_type(header: &HeaderMap) -> AuthType {
|
||||
/// # Returns
|
||||
/// * `(String, String)` - Tuple of auth type and signature version
|
||||
///
|
||||
#[allow(dead_code)]
|
||||
fn determine_auth_type_and_version(header: &HeaderMap) -> (String, String) {
|
||||
match get_request_auth_type(header) {
|
||||
determine_auth_type_and_version_with_query(header, None)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn determine_auth_type_and_version_with_query(header: &HeaderMap, query: Option<&str>) -> (String, String) {
|
||||
match get_request_auth_type_with_query(header, query) {
|
||||
AuthType::JWT => ("JWT".to_string(), String::new()),
|
||||
AuthType::SignedV2 => ("REST-HEADER".to_string(), "AWS2".to_string()),
|
||||
AuthType::PresignedV2 => ("REST-QUERY-STRING".to_string(), "AWS2".to_string()),
|
||||
@@ -690,11 +725,18 @@ fn is_request_signature_v2(header: &HeaderMap) -> bool {
|
||||
///
|
||||
/// # Returns
|
||||
/// * `bool` - True if request has AWS PreSign Version '4', false otherwise
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn is_request_presigned_signature_v4(header: &HeaderMap) -> bool {
|
||||
is_request_presigned_signature_v4_with_query(header, None)
|
||||
}
|
||||
|
||||
pub(crate) fn is_request_presigned_signature_v4_with_query(header: &HeaderMap, query: Option<&str>) -> bool {
|
||||
if let Some(credential) = header.get(AMZ_CREDENTIAL) {
|
||||
return !credential.to_str().unwrap_or("").is_empty();
|
||||
}
|
||||
false
|
||||
query
|
||||
.and_then(|query| get_query_param(query, "x-amz-credential"))
|
||||
.is_some_and(|credential| !credential.is_empty())
|
||||
}
|
||||
|
||||
/// Verify request has AWS PreSign Version '2'
|
||||
@@ -704,11 +746,13 @@ pub(crate) fn is_request_presigned_signature_v4(header: &HeaderMap) -> bool {
|
||||
///
|
||||
/// # Returns
|
||||
/// * `bool` - True if request has AWS PreSign Version '2', false otherwise
|
||||
fn is_request_presigned_signature_v2(header: &HeaderMap) -> bool {
|
||||
fn is_request_presigned_signature_v2(header: &HeaderMap, query: Option<&str>) -> bool {
|
||||
if let Some(access_key) = header.get(AMZ_ACCESS_KEY_ID) {
|
||||
return !access_key.to_str().unwrap_or("").is_empty();
|
||||
}
|
||||
false
|
||||
query
|
||||
.and_then(|query| get_query_param(query, "awsaccesskeyid"))
|
||||
.is_some_and(|access_key| !access_key.is_empty())
|
||||
}
|
||||
|
||||
/// Verify if request has AWS Post policy Signature Version '4'
|
||||
@@ -1016,6 +1060,20 @@ mod tests {
|
||||
assert_eq!(conditions.get("principaltype"), Some(&vec!["User".to_string()]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_condition_values_with_presigned_query() {
|
||||
let cred = create_test_credentials();
|
||||
let headers = HeaderMap::new();
|
||||
let uri: Uri = "https://example.com/?X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20130524%2Fus-east-1%2Fs3%2Faws4_request"
|
||||
.parse()
|
||||
.unwrap();
|
||||
|
||||
let conditions = get_condition_values_with_query(&headers, &cred, None, None, None, uri.query());
|
||||
|
||||
assert_eq!(conditions.get("signatureversion"), Some(&vec!["AWS4-HMAC-SHA256".to_string()]));
|
||||
assert_eq!(conditions.get("authType"), Some(&vec!["REST-QUERY-STRING".to_string()]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_condition_values_temp_user() {
|
||||
let cred = create_temp_credentials();
|
||||
@@ -1284,6 +1342,18 @@ mod tests {
|
||||
assert_eq!(auth_type, AuthType::PresignedV2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_request_auth_type_presigned_v2_from_query() {
|
||||
let headers = HeaderMap::new();
|
||||
let uri: Uri = "https://example.com/?AWSAccessKeyId=AKIAIOSFODNN7EXAMPLE&Signature=example&Expires=1672531200"
|
||||
.parse()
|
||||
.unwrap();
|
||||
|
||||
let auth_type = get_request_auth_type_with_query(&headers, uri.query());
|
||||
|
||||
assert_eq!(auth_type, AuthType::PresignedV2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_request_auth_type_presigned_v4() {
|
||||
let mut headers = HeaderMap::new();
|
||||
@@ -1297,6 +1367,18 @@ mod tests {
|
||||
assert_eq!(auth_type, AuthType::Presigned);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_request_auth_type_presigned_v4_from_query() {
|
||||
let headers = HeaderMap::new();
|
||||
let uri: Uri = "https://example.com/?X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20130524%2Fus-east-1%2Fs3%2Faws4_request"
|
||||
.parse()
|
||||
.unwrap();
|
||||
|
||||
let auth_type = get_request_auth_type_with_query(&headers, uri.query());
|
||||
|
||||
assert_eq!(auth_type, AuthType::Presigned);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_request_auth_type_post_policy() {
|
||||
let mut headers = HeaderMap::new();
|
||||
|
||||
@@ -19,7 +19,7 @@ use super::ecfs::{
|
||||
stored_acl_from_canned_object,
|
||||
};
|
||||
use super::options::get_opts;
|
||||
use crate::auth::{check_key_valid, get_condition_values, get_session_token};
|
||||
use crate::auth::{check_key_valid, get_condition_values_with_query, get_session_token};
|
||||
use crate::error::ApiError;
|
||||
use crate::license::license_check;
|
||||
use crate::server::RemoteAddr;
|
||||
@@ -276,7 +276,14 @@ 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 mut conditions = get_condition_values(&req.headers, cred, req_info.version_id.as_deref(), None, remote_addr);
|
||||
let mut conditions = get_condition_values_with_query(
|
||||
&req.headers,
|
||||
cred,
|
||||
req_info.version_id.as_deref(),
|
||||
None,
|
||||
remote_addr,
|
||||
req.uri.query(),
|
||||
);
|
||||
// Merge object tag conditions; extend existing values if the same key exists (e.g. from get_condition_values).
|
||||
if let Some(ref tags) = object_tag_conditions {
|
||||
for (k, v) in &tags.0 {
|
||||
@@ -425,12 +432,13 @@ pub async fn authorize_request<T>(req: &mut S3Request<T>, action: Action) -> S3R
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let mut conditions = get_condition_values(
|
||||
let mut conditions = get_condition_values_with_query(
|
||||
&req.headers,
|
||||
&rustfs_credentials::Credentials::default(),
|
||||
req_info.version_id.as_deref(),
|
||||
req.region.clone(),
|
||||
remote_addr,
|
||||
req.uri.query(),
|
||||
);
|
||||
// Merge object tag conditions; extend existing values if the same key exists.
|
||||
if let Some(ref tags) = object_tag_conditions {
|
||||
|
||||
@@ -44,8 +44,9 @@ use tracing::error;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::auth::AuthType;
|
||||
use crate::auth::get_request_auth_type;
|
||||
use crate::auth::is_request_presigned_signature_v4;
|
||||
use crate::auth::get_query_param;
|
||||
use crate::auth::get_request_auth_type_with_query;
|
||||
use crate::auth::is_request_presigned_signature_v4_with_query;
|
||||
|
||||
const REPLICATION_REQUEST_TRUE: HeaderValue = HeaderValue::from_static("true");
|
||||
|
||||
@@ -580,13 +581,18 @@ pub fn parse_copy_source_range(range_str: &str) -> S3Result<HTTPRangeSpec> {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn get_content_sha256(headers: &HeaderMap<HeaderValue>) -> Option<String> {
|
||||
match get_request_auth_type(headers) {
|
||||
get_content_sha256_with_query(headers, None)
|
||||
}
|
||||
|
||||
pub(crate) fn get_content_sha256_with_query(headers: &HeaderMap<HeaderValue>, query: Option<&str>) -> Option<String> {
|
||||
match get_request_auth_type_with_query(headers, query) {
|
||||
AuthType::Presigned | AuthType::Signed => {
|
||||
if skip_content_sha256_cksum(headers) {
|
||||
if skip_content_sha256_cksum_with_query(headers, query) {
|
||||
None
|
||||
} else {
|
||||
Some(get_content_sha256_cksum(headers, ServiceType::S3))
|
||||
Some(get_content_sha256_cksum_with_query(headers, query, ServiceType::S3))
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
@@ -595,24 +601,20 @@ pub(crate) fn get_content_sha256(headers: &HeaderMap<HeaderValue>) -> Option<Str
|
||||
|
||||
/// skip_content_sha256_cksum returns true if caller needs to skip
|
||||
/// payload checksum, false if not.
|
||||
#[allow(dead_code)]
|
||||
fn skip_content_sha256_cksum(headers: &HeaderMap<HeaderValue>) -> bool {
|
||||
let content_sha256 = if is_request_presigned_signature_v4(headers) {
|
||||
// For presigned requests, check query params first, then headers
|
||||
// Note: In a real implementation, you would need to check query parameters
|
||||
// For now, we'll just check headers
|
||||
headers.get(AMZ_CONTENT_SHA256)
|
||||
} else {
|
||||
headers.get(AMZ_CONTENT_SHA256)
|
||||
};
|
||||
skip_content_sha256_cksum_with_query(headers, None)
|
||||
}
|
||||
|
||||
fn skip_content_sha256_cksum_with_query(headers: &HeaderMap<HeaderValue>, query: Option<&str>) -> bool {
|
||||
let content_sha256 = get_content_sha256_value(headers, query, false);
|
||||
|
||||
// Skip if no header was set
|
||||
let Some(header_value) = content_sha256 else {
|
||||
return true;
|
||||
};
|
||||
|
||||
let Ok(value) = header_value.to_str() else {
|
||||
return true;
|
||||
};
|
||||
let value = header_value;
|
||||
|
||||
// If x-amz-content-sha256 is set and the value is not
|
||||
// 'UNSIGNED-PAYLOAD' we should validate the content sha256.
|
||||
@@ -643,7 +645,11 @@ fn skip_content_sha256_cksum(headers: &HeaderMap<HeaderValue>) -> bool {
|
||||
}
|
||||
|
||||
/// Returns SHA256 for calculating canonical-request.
|
||||
fn get_content_sha256_cksum(headers: &HeaderMap<HeaderValue>, service_type: ServiceType) -> String {
|
||||
fn get_content_sha256_cksum_with_query(
|
||||
headers: &HeaderMap<HeaderValue>,
|
||||
query: Option<&str>,
|
||||
service_type: ServiceType,
|
||||
) -> String {
|
||||
if service_type == ServiceType::STS {
|
||||
// For STS requests, we would need to read the body and calculate SHA256
|
||||
// This is a simplified implementation - in practice you'd need access to the request body
|
||||
@@ -651,28 +657,54 @@ fn get_content_sha256_cksum(headers: &HeaderMap<HeaderValue>, service_type: Serv
|
||||
return "sts-body-sha256-placeholder".to_string();
|
||||
}
|
||||
|
||||
let (default_sha256_cksum, content_sha256) = if is_request_presigned_signature_v4(headers) {
|
||||
let (default_sha256_cksum, content_sha256) = if is_request_presigned_signature_v4_with_query(headers, query) {
|
||||
// For a presigned request we look at the query param for sha256.
|
||||
// X-Amz-Content-Sha256, if not set in presigned requests, checksum
|
||||
// will default to 'UNSIGNED-PAYLOAD'.
|
||||
(UNSIGNED_PAYLOAD.to_string(), headers.get(AMZ_CONTENT_SHA256))
|
||||
(UNSIGNED_PAYLOAD.to_string(), get_content_sha256_value(headers, query, true))
|
||||
} else {
|
||||
// X-Amz-Content-Sha256, if not set in signed requests, checksum
|
||||
// will default to sha256([]byte("")).
|
||||
(EMPTY_STRING_SHA256_HASH.to_string(), headers.get(AMZ_CONTENT_SHA256))
|
||||
(
|
||||
EMPTY_STRING_SHA256_HASH.to_string(),
|
||||
headers
|
||||
.get(AMZ_CONTENT_SHA256)
|
||||
.and_then(|v| v.to_str().ok().map(str::to_owned)),
|
||||
)
|
||||
};
|
||||
|
||||
// We found 'X-Amz-Content-Sha256' return the captured value.
|
||||
if let Some(header_value) = content_sha256
|
||||
&& let Ok(value) = header_value.to_str()
|
||||
{
|
||||
return value.to_string();
|
||||
if let Some(header_value) = content_sha256 {
|
||||
return header_value;
|
||||
}
|
||||
|
||||
// We couldn't find 'X-Amz-Content-Sha256'.
|
||||
default_sha256_cksum
|
||||
}
|
||||
|
||||
fn get_content_sha256_value(
|
||||
headers: &HeaderMap<HeaderValue>,
|
||||
query: Option<&str>,
|
||||
include_query_for_presigned: bool,
|
||||
) -> Option<String> {
|
||||
if include_query_for_presigned && is_request_presigned_signature_v4_with_query(headers, query) {
|
||||
return query
|
||||
.and_then(|q| get_query_param(q, "x-amz-content-sha256"))
|
||||
.or_else(|| headers.get(AMZ_CONTENT_SHA256).and_then(|v| v.to_str().ok()))
|
||||
.map(str::to_owned);
|
||||
}
|
||||
|
||||
headers
|
||||
.get(AMZ_CONTENT_SHA256)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(str::to_owned)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn get_content_sha256_cksum(headers: &HeaderMap<HeaderValue>, service_type: ServiceType) -> String {
|
||||
get_content_sha256_cksum_with_query(headers, None, service_type)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
Reference in New Issue
Block a user