fix: honor bucket-scoped ListBucket policies with s3:prefix (#2707)

Co-authored-by: houseme <housemecn@gmail.com>
This commit is contained in:
安正超
2026-04-27 22:13:22 +08:00
committed by GitHub
parent a68fe1601f
commit 159ddd5bac
4 changed files with 293 additions and 25 deletions
+67 -1
View File
@@ -35,7 +35,7 @@ use s3s::access::{S3Access, S3AccessContext};
use s3s::{S3Error, S3ErrorCode, S3Request, S3Result, dto::*, s3_error};
use std::collections::HashMap;
use std::sync::OnceLock;
use url::Url;
use url::{Url, form_urlencoded};
#[derive(Default, Clone, Debug)]
pub(crate) struct ReqInfo {
@@ -220,6 +220,30 @@ fn action_tag_metric_label(action: &Action) -> &'static str {
}
}
fn merge_list_bucket_query_conditions(action: Action, query: Option<&str>, conditions: &mut HashMap<String, Vec<String>>) {
if !matches!(
action,
Action::S3Action(
S3Action::ListBucketAction | S3Action::ListBucketVersionsAction | S3Action::ListBucketMultipartUploadsAction
)
) {
return;
}
let Some(query) = query else {
return;
};
for (key, value) in form_urlencoded::parse(query.as_bytes()) {
match key.as_ref() {
"prefix" | "delimiter" | "max-keys" => {
conditions.entry(key.into_owned()).or_default().push(value.into_owned());
}
_ => {}
}
}
}
fn auth_fs() -> &'static FS {
static AUTH_FS: OnceLock<FS> = OnceLock::new();
AUTH_FS.get_or_init(FS::new)
@@ -312,6 +336,7 @@ pub async fn authorize_request<T>(req: &mut S3Request<T>, action: Action) -> S3R
let claims = cred.claims.as_ref().unwrap_or(&default_claims);
let mut conditions =
get_condition_values_with_query(&req.headers, cred, version_id.as_deref(), None, remote_addr, req.uri.query());
merge_list_bucket_query_conditions(action, req.uri.query(), &mut conditions);
let action_args = Args {
account: &cred.access_key,
@@ -526,6 +551,7 @@ pub async fn authorize_request<T>(req: &mut S3Request<T>, action: Action) -> S3R
remote_addr,
req.uri.query(),
);
merge_list_bucket_query_conditions(action, req.uri.query(), &mut conditions);
let no_groups: Option<Vec<String>> = None;
let bucket_tag_hint = if !bucket.is_empty() && !object.is_empty() {
@@ -1999,6 +2025,46 @@ mod tests {
/// Object tag conditions must use keys like ExistingObjectTag/<tag-key> so that
/// bucket policy conditions (e.g. s3:ExistingObjectTag/security) are evaluated correctly.
#[test]
fn test_merge_list_bucket_query_conditions_extracts_supported_keys() {
let mut conditions = HashMap::new();
merge_list_bucket_query_conditions(
Action::S3Action(S3Action::ListBucketAction),
Some("prefix=photos%2F2024%2F&delimiter=%2F&max-keys=10&encoding-type=url"),
&mut conditions,
);
assert_eq!(conditions.get("prefix"), Some(&vec!["photos/2024/".to_string()]));
assert_eq!(conditions.get("delimiter"), Some(&vec!["/".to_string()]));
assert_eq!(conditions.get("max-keys"), Some(&vec!["10".to_string()]));
assert!(!conditions.contains_key("encoding-type"));
}
#[test]
fn test_merge_list_bucket_query_conditions_preserves_empty_prefix_signal() {
let mut conditions = HashMap::new();
merge_list_bucket_query_conditions(
Action::S3Action(S3Action::ListBucketVersionsAction),
Some("prefix=&delimiter=%2F"),
&mut conditions,
);
assert_eq!(conditions.get("prefix"), Some(&vec![String::new()]));
assert_eq!(conditions.get("delimiter"), Some(&vec!["/".to_string()]));
}
#[test]
fn test_merge_list_bucket_query_conditions_ignores_non_list_actions() {
let mut conditions = HashMap::new();
merge_list_bucket_query_conditions(
Action::S3Action(S3Action::GetObjectAction),
Some("prefix=photos%2F2024%2F&delimiter=%2F&max-keys=10"),
&mut conditions,
);
assert!(conditions.is_empty());
}
#[test]
fn test_object_tag_conditions_key_format() {
let mut tags = HashMap::new();