Files
seaweedfs/weed/s3api/s3tables/handler_principal_test.go
T
Chris LuandClaude Opus 5.5 d6c0d728e4 s3vectors: resolve the caller like table buckets and honor bucket-scoped identity actions (#12319)
* s3vectors: resolve the caller the way table buckets do

The vector checks used the identity name as principal, while the catalog
records a bucket's owner through getAccountID: the account id, or the
issuer-scoped user id of a federated identity. An identity with an account
or a federated identity did not own the vector bucket it created, a Deny
naming its account did not bind it, and an Admin identity in the admin
account did not get the admin bypass it has on table buckets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011UpvafvK8JpagF94MHWqwK

* s3vectors: honor identity actions scoped to a vector bucket

An identity action such as s3vectors:QueryVectors:team-a granted nothing,
because the vector policy context never named the bucket. Table buckets
match <action>:<bucket> through the same context field, so pass the bucket
the check is about. Creating a bucket stays unscoped, as for table buckets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011UpvafvK8JpagF94MHWqwK

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 23:50:19 -08:00

62 lines
2.6 KiB
Go

package s3tables
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
"github.com/stretchr/testify/assert"
)
func TestGetAccountIDRejectsHeaderAdmin(t *testing.T) {
h := NewS3TablesHandler()
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set(s3_constants.AmzAccountId, s3_constants.AccountAdminId)
assert.NotEqual(t, s3_constants.AccountAdminId, h.getAccountID(req),
"a client-supplied account header must not resolve to the admin principal")
}
func TestGetAccountIDHeaderBranchResolvesCaller(t *testing.T) {
h := NewS3TablesHandler()
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set(s3_constants.AmzAccountId, "alice")
assert.Equal(t, "alice", h.getAccountID(req))
}
func TestPrincipalForMirrorsGetAccountID(t *testing.T) {
m := NewManager()
m.SetAccountID("000000000000")
named := func(name string) *http.Request {
req := httptest.NewRequest(http.MethodGet, "/", nil)
return req.WithContext(s3_constants.SetIdentityNameInContext(req.Context(), name))
}
withIdentity := func(id *testIdentity) *http.Request {
req := named(id.Name)
return req.WithContext(s3_constants.SetIdentityInContext(req.Context(), id))
}
assert.Equal(t, "000000000000", m.PrincipalFor(nil))
assert.Equal(t, "000000000000", m.PrincipalFor(named("")))
assert.Equal(t, "alice", m.PrincipalFor(named("alice")))
assert.Equal(t, s3_constants.AccountAdminId, m.PrincipalFor(named(s3_constants.AccountAdminId)),
"the name branch of getAccountID keeps the admin name")
assert.Equal(t, "000000000000", m.PrincipalFor(named(" ")))
assert.Equal(t, "bob", m.PrincipalFor(named("arn:aws:iam::123456789012:user/bob")),
"ARN identities collapse to the trailing segment like getAccountID")
assert.Equal(t, "team-acct", m.PrincipalFor(withIdentity(&testIdentity{Name: "carol", Account: &testIdentityAccount{Id: "team-acct"}})),
"an identity with an account is its account, not its name")
assert.Equal(t, "uid-alice", m.PrincipalFor(withIdentity(&testIdentity{Name: "alice-sub", Account: &testIdentityAccount{Id: "alice-sub"},
Claims: map[string]interface{}{"aws:userid": "uid-alice"}})),
"a federated identity is its issuer-scoped user id")
}
func TestCheckPermissionDenyPolicyBindsNonAdmin(t *testing.T) {
denyAll := `{"Statement":[{"Effect":"Deny","Principal":"*","Action":"s3tables:DeleteTableBucket"}]}`
assert.False(t, CheckPermissionWithContext("s3tables:DeleteTableBucket", "mallory", "owner123", denyAll, "arn:aws:s3tables:us-east-1:000000000000:bucket/victim",
&PolicyContext{DefaultAllow: true}))
}