Files
seaweedfs/weed/s3api/s3tables/handler_ownerless_test.go
T
Chris Lu 03a75cbc52 s3tables: only the admin repairs a table bucket that has no owner metadata (#11873)
* s3tables: only the admin repairs a table bucket that has no owner metadata

CreateNamespace wrote bucket metadata naming the caller as owner before any
permission check ran, so an ordinary caller that found the marker without
metadata was written in as the bucket's owner and passed every check after.
A bucket in that state is answered with the operation's own denial; the
admin or a trusted internal handler may still repair it.

* s3tables: repair the marker-only bucket under the entry lock

A creator finishing its own create between the missing-metadata check and
the repair write would have its owner overwritten. The mutation now adopts
whatever metadata is there and writes only while it is still absent.

* s3tables: gate the repair on the Admin action, not an operation grant

hasAdminAction also answers s3tables:*, so a non-admin identity holding
the wildcard operation grant could pass the repair check and write itself
in as the bucket's owner.
2026-10-20 18:58:54 -07:00

89 lines
3.6 KiB
Go

package s3tables
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"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 with the marker but no metadata is damaged, not ownerless for
// the taking: an ordinary caller is answered with the operation's own denial,
// and the admin or a trusted handler may repair it.
func TestCreateNamespaceOnAnOwnerlessBucket(t *testing.T) {
const bucket = "broken"
plant := func(fs *s3tablestest.MemFiler) {
fs.Put(TablesPath, bucket, map[string][]byte{ExtendedKeyTableBucket: []byte("true")})
}
arn, err := BuildBucketARN(DefaultRegion, DefaultAccountID, bucket)
require.NoError(t, err)
request := &CreateNamespaceRequest{TableBucketARN: arn, Namespace: []string{"ns"}}
t.Run("an outsider gets the denial and writes nothing", func(t *testing.T) {
fs := s3tablestest.Start(t)
plant(fs)
m := NewManager()
body, err := json.Marshal(request)
require.NoError(t, err)
identity := &testIdentity{Name: "outsider", Account: &testIdentityAccount{Id: "outsider"}, Actions: []string{"Read"}}
req, err := newManagerRequest(s3_constants.SetIdentityInContext(context.Background(), identity), "CreateNamespace", body, "outsider")
require.NoError(t, err)
recorder := httptest.NewRecorder()
m.handler.HandleRequest(recorder, req, NewManagerClient(fs.Client))
assert.Equal(t, http.StatusForbidden, recorder.Code, recorder.Body.String())
entry := fs.Get(TablesPath, bucket)
require.NotNil(t, entry)
assert.Empty(t, entry.Extended[ExtendedKeyMetadata], "the denied caller wrote itself in as the bucket owner")
assert.Nil(t, fs.Get(GetTableBucketPath(bucket), "ns"))
})
t.Run("a trusted handler repairs it and owns it", func(t *testing.T) {
fs := s3tablestest.Start(t)
plant(fs)
m := NewManager()
m.SetTrusted(true)
body, err := json.Marshal(request)
require.NoError(t, err)
identity := &testIdentity{Name: "svc", Account: &testIdentityAccount{Id: "svc"}, Actions: []string{"Read"}}
req, err := newManagerRequest(s3_constants.SetIdentityInContext(context.Background(), identity), "CreateNamespace", body, "svc")
require.NoError(t, err)
recorder := httptest.NewRecorder()
m.handler.HandleRequest(recorder, req, NewManagerClient(fs.Client))
assert.Equal(t, http.StatusOK, recorder.Code, recorder.Body.String())
entry := fs.Get(TablesPath, bucket)
require.NotNil(t, entry)
var metadata tableBucketMetadata
require.NoError(t, json.Unmarshal(entry.Extended[ExtendedKeyMetadata], &metadata))
assert.Equal(t, "svc", metadata.OwnerAccountID)
require.NotNil(t, fs.Get(GetTableBucketPath(bucket), "ns"))
})
t.Run("an untrusted admin repairs it too", func(t *testing.T) {
fs := s3tablestest.Start(t)
plant(fs)
m := NewManager()
body, err := json.Marshal(request)
require.NoError(t, err)
identity := &testIdentity{Name: "admin", Account: &testIdentityAccount{Id: s3_constants.AccountAdminId}, Actions: []string{"Admin"}}
req, err := newManagerRequest(s3_constants.SetIdentityInContext(context.Background(), identity), "CreateNamespace", body, "admin")
require.NoError(t, err)
recorder := httptest.NewRecorder()
m.handler.HandleRequest(recorder, req, NewManagerClient(fs.Client))
assert.Equal(t, http.StatusOK, recorder.Code, recorder.Body.String())
entry := fs.Get(TablesPath, bucket)
require.NotNil(t, entry)
var metadata tableBucketMetadata
require.NoError(t, json.Unmarshal(entry.Extended[ExtendedKeyMetadata], &metadata))
assert.Equal(t, s3_constants.AccountAdminId, metadata.OwnerAccountID)
})
}