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
+58
View File
@@ -27,6 +27,7 @@ import (
"net/url"
"os"
"reflect"
"strings"
"testing"
"github.com/minio/minio/internal/config"
@@ -194,6 +195,63 @@ func TestExtractMetadataHeaders(t *testing.T) {
}
}
func TestExtractMetadataFromRequestUsesHeaderPrecedence(t *testing.T) {
query := make(url.Values)
query.Set(strings.ToLower(xhttp.AmzStorageClass), "QUERY-CLASS")
query.Set(strings.ToLower(xhttp.AmzObjectTagging), "source=query")
req, err := http.NewRequest(http.MethodGet, "http://localhost/test?"+query.Encode(), nil)
if err != nil {
t.Fatal(err)
}
req.Header.Set(xhttp.AmzStorageClass, "HEADER-CLASS")
req.Header.Set(xhttp.AmzObjectTagging, "source=header")
if err = req.ParseForm(); err != nil {
t.Fatal(err)
}
metadata, err := extractMetadataFromReq(t.Context(), req)
if err != nil {
t.Fatal(err)
}
if got := metadata[xhttp.AmzStorageClass]; got != "HEADER-CLASS" {
t.Fatalf("storage class: expected header, got %q", got)
}
if got := metadata[xhttp.AmzObjectTagging]; got != "source=header" {
t.Fatalf("tagging: expected header, got %q", got)
}
// Presence, rather than a non-empty value, establishes precedence. This
// prevents a query value from taking over when a signed header is empty.
req.Header[xhttp.AmzObjectTagging] = []string{""}
if got, ok := getRequestHeaderOrQueryValue(req, xhttp.AmzObjectTagging); !ok || got != "" {
t.Fatalf("empty header did not override query: value=%q present=%v", got, ok)
}
}
func TestExtractMetadataFromRequestKeepsQueryCompatibility(t *testing.T) {
query := make(url.Values)
query.Set(strings.ToLower(xhttp.AmzStorageClass), "REDUCED_REDUNDANCY")
query.Set(strings.ToLower(xhttp.AmzObjectTagging), "security=public")
req, err := http.NewRequest(http.MethodGet, "http://localhost/test?"+query.Encode(), nil)
if err != nil {
t.Fatal(err)
}
if err = req.ParseForm(); err != nil {
t.Fatal(err)
}
metadata, err := extractMetadataFromReq(t.Context(), req)
if err != nil {
t.Fatal(err)
}
if got := metadata[xhttp.AmzStorageClass]; got != "REDUCED_REDUNDANCY" {
t.Fatalf("storage class query value lost: %q", got)
}
if got := metadata[xhttp.AmzObjectTagging]; got != "security=public" {
t.Fatalf("tagging query value lost: %q", got)
}
}
func TestExtractReplicationMetadataHeaders(t *testing.T) {
header := http.Header{
"X-Minio-Replication-Server-Side-Encryption-Sealed-Key": []string{"sealed-key"},