diff --git a/weed/s3api/s3tables/handler_sts_legacy_owner_test.go b/weed/s3api/s3tables/handler_sts_legacy_owner_test.go new file mode 100644 index 0000000000..9c1486fa56 --- /dev/null +++ b/weed/s3api/s3tables/handler_sts_legacy_owner_test.go @@ -0,0 +1,82 @@ +package s3tables + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/seaweedfs/seaweedfs/weed/iam/sts" + "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" + "github.com/seaweedfs/seaweedfs/weed/s3api/s3tables/s3tablestest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// A table bucket a web-identity role session created before owners were +// issuer-scoped is owned by the bare OIDC subject. The same subject's later +// sessions, signed with SigV4 session credentials, carry the scoped aws:userid +// and no asserted issuers, and must still own the bucket. +func TestSTSSessionKeepsLegacySubjectOwnedBucket(t *testing.T) { + const ( + bucket = "lake-norm" + sub = "project_path:dev/infra/datalake/normalisation:ref_type:branch:ref:main" + issuer = "https://gitlab.example.com" + ) + arn, err := BuildBucketARN(DefaultRegion, DefaultAccountID, bucket) + require.NoError(t, err) + body, err := json.Marshal(&CreateNamespaceRequest{TableBucketARN: arn, Namespace: []string{"some_ns"}}) + require.NoError(t, err) + + createNamespace := func(t *testing.T, sessionIssuer string, configured ...string) int { + fs := s3tablestest.Start(t) + metadata, err := json.Marshal(&tableBucketMetadata{Name: bucket, CreatedAt: time.Now(), OwnerAccountID: sub}) + require.NoError(t, err) + fs.Put(TablesPath, bucket, map[string][]byte{ExtendedKeyTableBucket: []byte("true"), ExtendedKeyMetadata: metadata}) + m := NewManager() + m.SetDefaultAllow(false) + if configured != nil { + m.SetTrustedIssuerSource(func(context.Context) []string { return configured }) + } + identity := &testIdentity{ + Name: sub, + Account: &testIdentityAccount{Id: sub}, + Claims: map[string]interface{}{ + "sub": sub, + "aws:userid": sts.ComputeParentUser(sub, sessionIssuer), + }, + } + req, err := newManagerRequest(s3_constants.SetIdentityInContext(context.Background(), identity), "CreateNamespace", body, sub) + require.NoError(t, err) + recorder := httptest.NewRecorder() + m.handler.HandleRequest(recorder, req, NewManagerClient(fs.Client)) + return recorder.Code + } + + assert.Equal(t, http.StatusOK, createNamespace(t, issuer, issuer), "the subject's own session lost its bucket") + assert.Equal(t, http.StatusForbidden, createNamespace(t, issuer), "no configured issuer set must not re-anchor") + assert.Equal(t, http.StatusForbidden, createNamespace(t, issuer, issuer, "https://other.example.com"), + "a legacy subject is ambiguous once two issuers are configured") + assert.Equal(t, http.StatusForbidden, createNamespace(t, "https://other.example.com", issuer), + "another issuer's user with the same subject must not take the bucket") +} + +// Issuers asserted by the bearer path win, even when empty; an identity that +// is not a federated session never reads the configured set. +func TestTrustedIssuersSourceOnlyForFederatedSessions(t *testing.T) { + h := NewS3TablesHandler() + h.SetTrustedIssuerSource(func(context.Context) []string { return []string{"configured"} }) + issuersFor := func(claims map[string]interface{}) []string { + req := httptest.NewRequest(http.MethodGet, "/", nil) + req = req.WithContext(s3_constants.SetIdentityInContext(req.Context(), &testIdentity{Claims: claims})) + return h.trustedIssuers(req) + } + scoped := sts.ComputeParentUser("alice", "configured") + assert.Equal(t, []string{"configured"}, issuersFor(map[string]interface{}{"aws:userid": scoped})) + assert.Equal(t, []string{"asserted"}, issuersFor(map[string]interface{}{"aws:userid": scoped, "seaweed:oidc-issuers": "asserted"})) + assert.Empty(t, issuersFor(map[string]interface{}{"aws:userid": scoped, "seaweed:oidc-issuers": ""})) + assert.Empty(t, issuersFor(map[string]interface{}{"aws:userid": "alice"})) + assert.Empty(t, issuersFor(map[string]interface{}{"sub": "alice"})) +}