mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-25 06:20:42 +01:00
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>
This commit is contained in:
7 files changed
+353
-15
No files matched your search
@@ -4,6 +4,7 @@ import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
||||
@@ -1051,6 +1052,158 @@ func TestRequestObjectTagConditionDeny(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func evaluateTaggedRequest(engine *PolicyEngine, method, target string, headers map[string]string, tagging string) PolicyEvaluationResult {
|
||||
if method == "" {
|
||||
method = http.MethodPut
|
||||
}
|
||||
req := httptest.NewRequest(method, "/test-bucket/test-object"+target, nil)
|
||||
for k, v := range headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
if tagging != "" {
|
||||
req.Header.Set(s3_constants.AmzObjectTagging, tagging)
|
||||
}
|
||||
return engine.EvaluatePolicy("test-bucket", &PolicyEvaluationArgs{
|
||||
Action: "s3:PutObject",
|
||||
Resource: "arn:aws:s3:::test-bucket/test-object",
|
||||
Principal: "*",
|
||||
Conditions: engine.ExtractConditionValuesFromRequest(req),
|
||||
})
|
||||
}
|
||||
|
||||
func TestRequestObjectTagKeysCondition(t *testing.T) {
|
||||
engine := NewPolicyEngine()
|
||||
|
||||
policyJSON := `{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Principal": "*",
|
||||
"Action": "s3:PutObject",
|
||||
"Resource": "arn:aws:s3:::test-bucket/*",
|
||||
"Condition": {
|
||||
"ForAllValues:StringEquals": {
|
||||
"s3:RequestObjectTagKeys": ["department", "project"]
|
||||
},
|
||||
"Null": {
|
||||
"s3:RequestObjectTagKeys": "false"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"Effect": "Deny",
|
||||
"Principal": "*",
|
||||
"Action": "s3:PutObject",
|
||||
"Resource": "arn:aws:s3:::test-bucket/*",
|
||||
"Condition": {
|
||||
"ForAnyValue:StringLike": {
|
||||
"s3:RequestObjectTagKeys": ["secret*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
||||
if err := engine.SetBucketPolicy("test-bucket", policyJSON); err != nil {
|
||||
t.Fatalf("Failed to set bucket policy: %v", err)
|
||||
}
|
||||
|
||||
copyHeaders := map[string]string{"X-Amz-Copy-Source": "/src-bucket/src-object"}
|
||||
replaceHeaders := map[string]string{
|
||||
"X-Amz-Copy-Source": "/src-bucket/src-object",
|
||||
s3_constants.AmzObjectTaggingDirective: "REPLACE",
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
method string
|
||||
target string
|
||||
headers map[string]string
|
||||
tagging string
|
||||
expected PolicyEvaluationResult
|
||||
}{
|
||||
{"listed key allows", "", "", nil, "department=engineering", PolicyResultAllow},
|
||||
{"all listed keys allow", "", "", nil, "department=engineering&project=alpha", PolicyResultAllow},
|
||||
{"unlisted key is not allowed", "", "", nil, "department=engineering&owner=bob", PolicyResultIndeterminate},
|
||||
{"no tagging header is not allowed", "", "", nil, "", PolicyResultIndeterminate},
|
||||
{"repeated key counts once", "", "", nil, "department=a&department=b", PolicyResultAllow},
|
||||
{"url-encoded key is decoded", "", "", nil, "depart%6Dent=engineering", PolicyResultAllow},
|
||||
{"empty segments add no key", "", "", nil, "&department=engineering&", PolicyResultAllow},
|
||||
{"empty key is not allowed", "", "", nil, "department=engineering&=x", PolicyResultIndeterminate},
|
||||
{"key case is preserved", "", "", nil, "Department=engineering", PolicyResultIndeterminate},
|
||||
{"malformed header is not allowed", "", "", nil, "department=engineering&%zz", PolicyResultIndeterminate},
|
||||
{"secret key is denied", "", "", nil, "secret=1", PolicyResultDeny},
|
||||
{"secret key behind a padded header is denied", "", "", nil, "secret=1" + strings.Repeat("&", 10001), PolicyResultDeny},
|
||||
{"secret key among listed keys is denied", "", "", nil, "department=engineering&secret-level=2", PolicyResultDeny},
|
||||
{"url-encoded secret key is denied", "", "", nil, "secr%65t=1", PolicyResultDeny},
|
||||
{"secret key on a copy that replaces tags is denied", "", "", replaceHeaders, "secret=1", PolicyResultDeny},
|
||||
{"secret key on a multipart create is denied", "POST", "?uploads", nil, "secret=1", PolicyResultDeny},
|
||||
{"header is ignored on a copy that keeps source tags", "", "", copyHeaders, "secret=1", PolicyResultIndeterminate},
|
||||
{"header is ignored on PutObjectTagging", "", "?tagging", nil, "department=engineering", PolicyResultIndeterminate},
|
||||
{"header is ignored on a part upload", "", "?partNumber=1&uploadId=x", nil, "secret=1", PolicyResultIndeterminate},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if result := evaluateTaggedRequest(engine, tt.method, tt.target, tt.headers, tt.tagging); result != tt.expected {
|
||||
t.Errorf("Expected %v, got %v", tt.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestObjectTagKeysRequiredTagDeny(t *testing.T) {
|
||||
engine := NewPolicyEngine()
|
||||
|
||||
policyJSON := `{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Principal": "*",
|
||||
"Action": "s3:PutObject",
|
||||
"Resource": "arn:aws:s3:::test-bucket/*"
|
||||
},
|
||||
{
|
||||
"Effect": "Deny",
|
||||
"Principal": "*",
|
||||
"Action": "s3:PutObject",
|
||||
"Resource": "arn:aws:s3:::test-bucket/*",
|
||||
"Condition": {
|
||||
"ForAllValues:StringNotEquals": {
|
||||
"s3:RequestObjectTagKeys": ["department", "project"]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
||||
if err := engine.SetBucketPolicy("test-bucket", policyJSON); err != nil {
|
||||
t.Fatalf("Failed to set bucket policy: %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
tagging string
|
||||
expected PolicyEvaluationResult
|
||||
}{
|
||||
{"required key passes", "department=engineering", PolicyResultAllow},
|
||||
{"required key with another key passes", "project=alpha&owner=bob", PolicyResultAllow},
|
||||
{"only other keys are denied", "owner=bob", PolicyResultDeny},
|
||||
{"no tagging header is denied", "", PolicyResultDeny},
|
||||
{"malformed header is denied", "department=engineering&%zz", PolicyResultDeny},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if result := evaluateTaggedRequest(engine, "", "", nil, tt.tagging); result != tt.expected {
|
||||
t.Errorf("Expected %v, got %v", tt.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestExistingObjectTagConditionMultipleTags tests policies with multiple tag conditions
|
||||
func TestExistingObjectTagConditionMultipleTags(t *testing.T) {
|
||||
engine := NewPolicyEngine()
|
||||
|
||||
Reference in new issue
Block a user