package s3tables import ( "context" "errors" "testing" "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) var errFilerReached = errors.New("filer reached") // recordingFilerClient reports whether the handler reached the filer. The // CreateTableBucket handler authorizes before touching the filer, so "filer // reached" is a proxy for "authorization passed". type recordingFilerClient struct { called bool } func (c *recordingFilerClient) WithFilerClient(streamingMode bool, fn func(client filer_pb.SeaweedFilerClient) error) error { c.called = true return errFilerReached } // The Manager path (used by the Iceberg catalog) enforces authorization for // authenticated callers and only falls open for trusted/zero-config access. func TestManagerCreateTableBucketAuthorization(t *testing.T) { lowPriv := &testIdentity{ Name: "alice", Account: &testIdentityAccount{Id: s3_constants.AccountAdminId}, Actions: []string{"Read"}, } cases := []struct { name string defaultAllow bool trusted bool ctx context.Context identity string wantFiler bool // true => authorization passed (filer reached) }{ { name: "authenticated identity struct is enforced", defaultAllow: true, ctx: s3_constants.SetIdentityInContext(context.Background(), lowPriv), identity: "alice", wantFiler: false, }, { name: "secured manager denies a name without struct", defaultAllow: false, ctx: context.Background(), identity: "alice", wantFiler: false, }, { name: "untrusted name without struct is enforced", defaultAllow: true, ctx: context.Background(), identity: "alice", wantFiler: false, }, { name: "trusted manager allows a name without struct", defaultAllow: true, trusted: true, ctx: context.Background(), identity: "alice", wantFiler: true, }, { name: "admin principal is allowed", defaultAllow: false, ctx: context.Background(), identity: s3_constants.AccountAdminId, wantFiler: true, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { m := NewManager() m.SetDefaultAllow(tc.defaultAllow) m.SetTrusted(tc.trusted) fc := &recordingFilerClient{} err := m.Execute(tc.ctx, fc, "CreateTableBucket", &CreateTableBucketRequest{Name: "testbucket"}, nil, tc.identity) require.Error(t, err) // denied (403) or the filer sentinel (reached); never nil here assert.Equal(t, tc.wantFiler, fc.called, "filer reached should equal authorization passed") if !tc.wantFiler { var s3Err *S3TablesError require.ErrorAs(t, err, &s3Err) assert.Equal(t, ErrCodeAccessDenied, s3Err.Type) } }) } } // The bucket stores the declared configuration, but nothing downstream reads a // caller-chosen key — accepting one records a promise the data never gets. func TestManagerCreateTableBucketRejectsCustomerKeys(t *testing.T) { for _, tc := range []struct { name string cfg map[string]interface{} reaches bool }{ {"no configuration", nil, true}, {"tables default sse", map[string]interface{}{"sseAlgorithm": "AES256"}, true}, {"vector wire name", map[string]interface{}{"sseType": "AES256"}, false}, {"unknown sse", map[string]interface{}{"sseType": "aws:fsx"}, false}, {"tables kms", map[string]interface{}{"sseAlgorithm": "aws:kms"}, false}, {"conflicting types", map[string]interface{}{"sseAlgorithm": "AES256", "sseType": "aws:kms"}, false}, {"missing algorithm", map[string]interface{}{"other": "x"}, false}, {"customer key", map[string]interface{}{"sseType": "aws:kms", "kmsKeyArn": "arn:aws:kms:us-east-1:123456789012:key/x"}, false}, {"key without a type", map[string]interface{}{"kmsKeyArn": "arn:aws:kms:us-east-1:123456789012:key/x"}, false}, {"key on default sse", map[string]interface{}{"sseType": "AES256", "kmsKeyArn": "arn:aws:kms:us-east-1:123456789012:key/x"}, false}, } { t.Run(tc.name, func(t *testing.T) { m := NewManager() fc := &recordingFilerClient{} err := m.Execute(context.Background(), fc, "CreateTableBucket", &CreateTableBucketRequest{ Name: "testbucket", EncryptionConfiguration: tc.cfg, }, nil, s3_constants.AccountAdminId) require.Error(t, err) assert.Equal(t, tc.reaches, fc.called) if !tc.reaches { var s3Err *S3TablesError require.ErrorAs(t, err, &s3Err) assert.Equal(t, ErrCodeInvalidRequest, s3Err.Type) } }) } }