Files
seaweedfs/weed/s3api/s3api_acl_header_parser_test.go
T
zhao-ycandChris Lu 483dd4b12e s3api: persist ACLs on PutObject uploads (#11592)
* s3api: persist ACLs on PutObject uploads

Signed-off-by: zhaoyuchen <43179751+zhao-yc@users.noreply.github.com>

* s3api: fix PutObject ACL edge cases found in review

- Only enforce BucketOwnerEnforced when explicitly configured; buckets
  without a stored ownership control keep accepting upload ACLs
- Ignore ACL query parameters on SigV2 requests, which do not sign them
- Mirror signed-query ACL values into headers after authentication so
  grant parsing and resolveFileMode agree on presigned uploads
- Validate only caller-supplied grantees against the account registry;
  default grants now work for accounts outside the local registry
- Reject unknown grantee keys and accept comma-separated grantee lists
  without spaces in ParseCustomAclHeader
- Guard against identities without an account

* s3api: harden upload ACL parsing and authorization

Signed-off-by: zhaoyuchen <43179751+zhao-yc@users.noreply.github.com>

* s3api: evaluate upload ACL grantees individually in policies

A comma-joined grant header or a signed query parameter reached policy
conditions as one value, so a deny on a later grantee did not fire. Split
grant headers into per-grantee values for policy evaluation and share the
grantee pair parser with ParseCustomAclHeader.

* s3api: keep raw grant header values visible to policy conditions

Exact-match conditions written against the signed header value stopped
matching once grantees were split for evaluation. Preserve the original
wire values alongside the per-grantee values so deny policies fire on
either granularity.

* s3api: evaluate upload ACL grants as one canonical list in policies

Conditions on s3:x-amz-grant-* now see a single comma-separated canonical
grant list identical for a single line, repeated header lines, or a signed
query parameter. This keeps StringEquals allows and exact-list or
allowlist (StringNotEquals) denies accurate regardless of wire encoding.

* s3api: preserve upload ACL denies and align policy checks

Signed-off-by: zhaoyuchen <43179751+zhao-yc@users.noreply.github.com>

* s3api: retain upload owner grants and literal policy values

Signed-off-by: zhaoyuchen <43179751+zhao-yc@users.noreply.github.com>

---------

Signed-off-by: zhaoyuchen <43179751+zhao-yc@users.noreply.github.com>
Co-authored-by: Chris Lu <chris.lu@gmail.com>
2026-10-05 09:13:19 +08:00

62 lines
2.8 KiB
Go

package s3api
import (
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/s3"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
"github.com/stretchr/testify/require"
)
// TestParseCustomAclHeaderList covers the wire syntax separately from account
// resolution, including quoted delimiters and rejection without partial grants.
func TestParseCustomAclHeaderList(t *testing.T) {
tests := []struct {
name, input string
values []string
invalid bool
}{
{name: "absent"},
{name: "single", input: `id="alice"`, values: []string{"alice"}},
{name: "comma without space", input: `id="alice",id="bob"`, values: []string{"alice", "bob"}},
{name: "comma with space", input: `id="alice", id="bob"`, values: []string{"alice", "bob"}},
{name: "optional whitespace", input: " id = \"alice\" ,\t id=\"bob\" ", values: []string{"alice", "bob"}},
{name: "quoted comma and equals", input: `id="a,b=c",id="bob"`, values: []string{"a,b=c", "bob"}},
{name: "escaped quote", input: `id="a\"b",id="bob"`, values: []string{`a"b`, "bob"}},
{name: "email", input: `emailAddress="a=b@example.com"`, values: []string{"a=b@example.com"}},
{name: "group", input: `uri="http://acs.amazonaws.com/groups/global/AllUsers"`, values: []string{s3_constants.GranteeGroupAllUsers}},
{name: "unknown type", input: `account="alice"`, invalid: true},
{name: "mixed unknown type", input: `id="alice",principal="bob"`, invalid: true},
{name: "empty grantee", input: `id=""`, invalid: true},
{name: "unquoted", input: `id=alice`, invalid: true},
{name: "unterminated", input: `id="alice`, invalid: true},
{name: "trailing comma", input: `id="alice",`, invalid: true},
{name: "empty element", input: `id="alice",,id="bob"`, invalid: true},
{name: "missing comma", input: `id="alice" id="bob"`, invalid: true},
{name: "invalid escape", input: `id="a\q"`, invalid: true},
{name: "whitespace only", input: " ", invalid: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
original := &s3.Grant{Permission: aws.String(s3_constants.PermissionFullControl)}
grants := []*s3.Grant{original}
code := ParseCustomAclHeader(tt.input, s3_constants.PermissionRead, &grants)
if tt.invalid {
require.Equal(t, s3err.ErrInvalidRequest, code)
require.Equal(t, []*s3.Grant{original}, grants, "invalid lists must not leave partial grants")
return
}
require.Equal(t, s3err.ErrNone, code)
require.Len(t, grants, 1+len(tt.values))
for i, value := range tt.values {
grant := grants[i+1]
actual := aws.StringValue(grant.Grantee.ID) + aws.StringValue(grant.Grantee.EmailAddress) + aws.StringValue(grant.Grantee.URI)
require.Equal(t, value, actual)
require.Equal(t, s3_constants.PermissionRead, aws.StringValue(grant.Permission))
}
})
}
}