Files
seaweedfs/weed/s3api/policy_engine/conditions_set_test.go
fb3fb9dcb8 s3api: support s3:RequestObjectTagKeys in bucket policies (#11993)
* s3api: support s3:RequestObjectTagKeys in bucket policies

Condition operators accept the ForAllValues: and ForAnyValue: qualifiers,
and s3:RequestObjectTagKeys holds the keys of the tags taken from
X-Amz-Tagging wherever s3:RequestObjectTag/<key> is set.

CopyObject no longer stores an empty-key tag for an empty segment in
X-Amz-Tagging, so it keeps the key set a policy has checked.

Fixes #7679

* s3: parse tagging header like the tag handlers, not url.ParseQuery

url.ParseQuery stops at 10,000 parameters, so an X-Amz-Tagging header
padded with empty & segments made requestObjectTags return nil: the copy
handler still stored the denied tag while every s3:RequestObjectTag*
condition saw nothing. Scan non-empty segments the way parseTagsHeader
does so the policy sees the same tags the object gets.

* test: gofmt s3tables test files after #11997

The format check fails on these files on master as merged, so every
PR's S3 Tables Format Check goes red until they are re-formatted.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: user.email <chrislusf@users.noreply.github.com>
Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-24 02:04:39 -07:00

103 lines
4.4 KiB
Go

package policy_engine
import (
"testing"
)
func TestConditionSetOperators(t *testing.T) {
allowed := []string{"department", "project"}
tests := []struct {
name string
operator string
conditionValue interface{}
contextValues []string
expected bool
}{
{"ForAllValues all match", "ForAllValues:StringEquals", allowed, []string{"department", "project"}, true},
{"ForAllValues subset matches", "ForAllValues:StringEquals", allowed, []string{"project"}, true},
{"ForAllValues one outsider fails", "ForAllValues:StringEquals", allowed, []string{"department", "owner"}, false},
{"ForAllValues empty set matches", "ForAllValues:StringEquals", allowed, []string{}, true},
{"ForAllValues nil set matches", "ForAllValues:StringEquals", allowed, nil, true},
{"ForAllValues is case sensitive", "ForAllValues:StringEquals", allowed, []string{"Department"}, false},
{"ForAllValues StringLike all match", "ForAllValues:StringLike", "proj-*", []string{"proj-a", "proj-b"}, true},
{"ForAllValues StringLike one outsider fails", "ForAllValues:StringLike", "proj-*", []string{"proj-a", "team"}, false},
{"ForAllValues StringNotEquals none listed", "ForAllValues:StringNotEquals", allowed, []string{"owner", "team"}, true},
{"ForAllValues StringNotEquals one listed fails", "ForAllValues:StringNotEquals", allowed, []string{"owner", "project"}, false},
{"ForAnyValue one match", "ForAnyValue:StringEquals", allowed, []string{"owner", "project"}, true},
{"ForAnyValue no match", "ForAnyValue:StringEquals", allowed, []string{"owner", "team"}, false},
{"ForAnyValue empty set fails", "ForAnyValue:StringEquals", allowed, []string{}, false},
{"ForAnyValue nil set fails", "ForAnyValue:StringEquals", allowed, nil, false},
{"ForAnyValue StringLike one match", "ForAnyValue:StringLike", []string{"secret*"}, []string{"owner", "secret-level"}, true},
{"ForAnyValue StringLike no match", "ForAnyValue:StringLike", []string{"secret*"}, []string{"owner"}, false},
{"ForAnyValue StringNotEquals one unlisted", "ForAnyValue:StringNotEquals", allowed, []string{"department", "owner"}, true},
{"ForAnyValue StringNotEquals all listed fails", "ForAnyValue:StringNotEquals", allowed, []string{"department", "project"}, false},
{"ForAnyValue StringEqualsIgnoreCase", "ForAnyValue:StringEqualsIgnoreCase", allowed, []string{"PROJECT"}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
evaluator, err := GetConditionEvaluator(tt.operator)
if err != nil {
t.Fatalf("Failed to get condition evaluator: %v", err)
}
if got := evaluator.Evaluate(tt.conditionValue, tt.contextValues); got != tt.expected {
t.Errorf("Expected %v, got %v", tt.expected, got)
}
})
}
}
func TestGetConditionEvaluatorRejectsUnknownSetOperators(t *testing.T) {
for _, op := range []string{
"ForAllValues:StringBogus",
"ForAnyValue:",
"ForEachValue:StringEquals",
"ForAllValues:ForAnyValue:StringEquals",
} {
if _, err := GetConditionEvaluator(op); err == nil {
t.Errorf("GetConditionEvaluator accepted %q; expected error", op)
}
}
}
func TestValidateBucketPolicyAcceptsSetOperators(t *testing.T) {
policy := `{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Deny",
"Principal": "*",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::test-bucket/*",
"Condition": {
"ForAllValues:StringNotEquals": {"s3:RequestObjectTagKeys": ["department", "project"]},
"ForAnyValue:StringLike": {"s3:RequestObjectTagKeys": ["secret*"]}
}
}]
}`
engine := NewPolicyEngine()
if err := engine.SetBucketPolicy("test-bucket", policy); err != nil {
t.Fatalf("SetBucketPolicy rejected set operators: %v", err)
}
}
func TestOriginalGrantsSupplementOnlyForAnyValue(t *testing.T) {
const key = "s3:x-amz-grant-read"
contextValues := map[string][]string{key: {"id=canonical"}}
originalGrants := map[string][]string{key: {"uri=original"}}
anyValue := PolicyConditions{
"ForAnyValue:StringEquals": {key: NewStringOrStringSlice("uri=original")},
}
if !evaluateConditions(anyValue, contextValues, nil, nil, originalGrants) {
t.Error("ForAnyValue should see the original grant values")
}
allValues := PolicyConditions{
"ForAllValues:StringEquals": {key: NewStringOrStringSlice("id=canonical")},
}
if !evaluateConditions(allValues, contextValues, nil, nil, originalGrants) {
t.Error("ForAllValues should be evaluated on the canonical values only")
}
}