fix(iam): bind policy conditions to effective request values

Policy evaluation mixed server-derived identity and transport values with raw headers and query parameters. A client could therefore shadow internal condition keys, synthesize LDAP or JWT resource variables, substitute request tags for stored tags, or make a condition observe a value different from the one the handler actually used.

Partition condition sources, reserve internal names, adopt exact-name lookup from silo-pkg, and bind authorization to the effective request state. Preserve compatible query forms for storage class and upload tags with explicit header precedence, while restricting signature age and existing-object tags to authenticated or server-resolved values.

Tests sweep every supported key across header and query routes and exercise LDAP/OIDC variables, object-lock spelling, STS tags, metadata extraction, and end-to-end policy decisions.

Co-authored-by: ChatGPT <noreply@openai.com>
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Feng Ruohang
2026-08-02 18:21:56 +08:00
parent 97b7d28040
commit 2f55347f78
10 changed files with 962 additions and 86 deletions
+56 -1
View File
@@ -142,7 +142,62 @@ var userMetadataKeyPrefixes = []string{
// extractMetadataFromReq extracts metadata from HTTP header and HTTP queryString.
func extractMetadataFromReq(ctx context.Context, r *http.Request) (metadata map[string]string, err error) {
return extractMetadata(ctx, textproto.MIMEHeader(r.Form), textproto.MIMEHeader(r.Header))
metadata, err = extractMetadata(ctx, textproto.MIMEHeader(r.Form), textproto.MIMEHeader(r.Header))
if err != nil {
return nil, err
}
// Keep the metadata consumed by object operations in lock-step with policy
// conditions: an explicitly present header wins, otherwise use the query
// value accepted by the existing S3-compatible request path.
for _, name := range []string{xhttp.AmzStorageClass, xhttp.AmzObjectTagging} {
if value, ok := getRequestHeaderOrQueryValue(r, name); ok {
metadata[name] = value
}
}
return metadata, nil
}
// getRequestHeaderOrQueryValue returns the effective value of a request field.
// Header presence takes precedence even when its value is empty. Query lookup
// remains case-insensitive for compatibility with extractMetadataFromReq.
func getRequestHeaderOrQueryValue(r *http.Request, name string) (string, bool) {
if values, ok := getRequestValues(r.Header, name, http.CanonicalHeaderKey(name)); ok {
return strings.Join(values, ","), true
}
if values, ok := getRequestValues(http.Header(r.Form), name, strings.ToLower(name)); ok {
return strings.Join(values, ","), true
}
return "", false
}
func getRequestValues(values http.Header, name, preferred string) ([]string, bool) {
if value, ok := values[preferred]; ok {
return value, true
}
canonical, lower := http.CanonicalHeaderKey(name), strings.ToLower(name)
for _, key := range []string{canonical, lower} {
if key == preferred {
continue
}
if value, ok := values[key]; ok {
return value, true
}
}
// Multiple differently-cased spellings are malformed but were previously
// accepted. Pick one deterministically instead of depending on map order.
match := ""
for key := range values {
if strings.EqualFold(key, name) && (match == "" || key < match) {
match = key
}
}
if match != "" {
return values[match], true
}
return nil, false
}
func extractMetadata(ctx context.Context, mimesHeader ...textproto.MIMEHeader) (metadata map[string]string, err error) {