mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-03 02:32:35 +01:00
* s3tables: bind table bucket policy writes to the authorized bucket PutTableBucketPolicy compared only the policy bytes, and DeleteTableBucketPolicy removed the policy unconditionally. A bucket deleted and created again under the same name between authorization and the write took a policy change nobody authorized for it. Both writes now require the bucket's CreatedAt and the policy they were authorized against, answer 409 otherwise and 404 for a bucket that is gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * s3tables: bind bucket tag writes to the authorized bucket TagResource and UntagResource checked the creation time only for a table ARN. On a bucket ARN the merged tags landed on a bucket deleted and created again since authorization. extractResourceOwnerAndBucket now returns the resource's CreatedAt for both kinds, and the write requires it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * s3tables: bind bucket maintenance writes to the authorized bucket PutTableBucketMaintenanceConfiguration wrote onto whatever bucket held the name at write time. authorizeMaintenanceBucket now returns the bucket's CreatedAt like authorizeMaintenanceTable, and putMaintenanceConfiguration takes the check and creation time from both callers instead of an optional closure. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * s3vectors: bind vector bucket policy writes to the authorized bucket Put/DeleteVectorBucketPolicy authorized inside the compare-and-swap with the owner from an earlier bucket read, so a bucket deleted and created again by another account in between had its policy rewritten by the old owner. The mutation now requires the entry's createdAt to match the record it authorized against. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * s3vectors: bind DeleteVectorBucket to the authorized bucket DeleteVectorBucket authorized one bucket record, then ran the trusted catalog DeleteTableBucket on whatever bucket held the name by then, so an empty bucket deleted and created again by another account in between was deleted by the old owner. The catalog delete now accepts the authorized bucket's createdAt through an in-process context value and treats any other bucket as gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * s3vectors: bind CreateIndex and DeleteIndex to the authorized bucket Both authorized the bucket, then ran trusted CreateNamespace, CreateTable or DeleteTable on whatever bucket held the name by then, so an index could be created in, or deleted from, a bucket another account created again in between. Those catalog operations now honor the bucket binding DeleteTableBucket already takes, and the vector handlers pass the authorized bucket's createdAt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
105 lines
3.8 KiB
Go
105 lines
3.8 KiB
Go
package s3tables
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// recreatedBucket is the record of the test bucket deleted and created again:
|
|
// another owner, and a creation time of its own.
|
|
func recreatedBucket(t *testing.T) map[string][]byte {
|
|
t.Helper()
|
|
meta, err := json.Marshal(tableBucketMetadata{
|
|
Name: renameTestBucket,
|
|
OwnerAccountID: "111122223333",
|
|
CreatedAt: time.Now().Add(time.Hour),
|
|
})
|
|
require.NoError(t, err)
|
|
return map[string][]byte{
|
|
ExtendedKeyTableBucket: []byte("{}"),
|
|
ExtendedKeyMetadata: meta,
|
|
}
|
|
}
|
|
|
|
// bucketWriteOp is a write to the test bucket, with the attributes the bucket
|
|
// carries for it besides its record.
|
|
type bucketWriteOp struct {
|
|
operation string
|
|
input func(t *testing.T) interface{}
|
|
extra map[string][]byte
|
|
}
|
|
|
|
func bucketWriteOps() []bucketWriteOp {
|
|
return []bucketWriteOp{
|
|
{operation: "PutTableBucketPolicy", input: func(t *testing.T) interface{} {
|
|
return &PutTableBucketPolicyRequest{TableBucketARN: mustBucketARN(t), ResourcePolicy: bindTestPolicy}
|
|
}},
|
|
{operation: "DeleteTableBucketPolicy", input: func(t *testing.T) interface{} {
|
|
return &DeleteTableBucketPolicyRequest{TableBucketARN: mustBucketARN(t)}
|
|
}, extra: map[string][]byte{ExtendedKeyPolicy: []byte(bindTestPolicy)}},
|
|
{operation: "TagResource", input: func(t *testing.T) interface{} {
|
|
return &TagResourceRequest{ResourceARN: mustBucketARN(t), Tags: map[string]string{"k": "v"}}
|
|
}},
|
|
{operation: "UntagResource", input: func(t *testing.T) interface{} {
|
|
return &UntagResourceRequest{ResourceARN: mustBucketARN(t), TagKeys: []string{"k"}}
|
|
}, extra: map[string][]byte{ExtendedKeyTags: []byte(`{"k":"v","keep":"1"}`)}},
|
|
{operation: "PutTableBucketMaintenanceConfiguration", input: func(t *testing.T) interface{} {
|
|
return &PutTableBucketMaintenanceConfigurationRequest{
|
|
TableBucketARN: mustBucketARN(t),
|
|
Type: MaintenanceTypeIcebergUnreferencedFileRemoval,
|
|
Value: &MaintenanceConfigurationValue{Status: MaintenanceStatusEnabled},
|
|
}
|
|
}},
|
|
}
|
|
}
|
|
|
|
// A write to a bucket's policy, tags or maintenance settings is authorized
|
|
// against the bucket it read. If that bucket is deleted and another is created under the same name
|
|
// before the write, the values it compares can match the new bucket's just as
|
|
// well. The write checks the bucket by its creation time and is refused, and
|
|
// the new bucket is kept as it was created.
|
|
func TestBucketWritesSpareARecreatedBucket(t *testing.T) {
|
|
for _, op := range bucketWriteOps() {
|
|
t.Run(op.operation, func(t *testing.T) {
|
|
fs, m := startEmptyBucket(t)
|
|
extended := extendedOf(t, fs, TablesPath, renameTestBucket)
|
|
recreated := recreatedBucket(t)
|
|
for key, value := range op.extra {
|
|
extended[key] = value
|
|
recreated[key] = value
|
|
}
|
|
fs.Put(TablesPath, renameTestBucket, extended)
|
|
fs.BeforeUpdate = func() { fs.Put(TablesPath, renameTestBucket, recreated) }
|
|
|
|
code, resp := runHandler(t, m, fs.Client, op.operation, op.input(t))
|
|
|
|
assert.Equal(t, http.StatusConflict, code, "%+v", resp)
|
|
assert.Equal(t, ErrCodeConflict, resp.Type, "%+v", resp)
|
|
assert.Equal(t, recreated, fs.Get(TablesPath, renameTestBucket).Extended, "the new bucket is left as it was created")
|
|
})
|
|
}
|
|
}
|
|
|
|
// The same writes still land on the bucket they were authorized against.
|
|
func TestBucketWritesLandOnTheAuthorizedBucket(t *testing.T) {
|
|
for _, op := range bucketWriteOps() {
|
|
t.Run(op.operation, func(t *testing.T) {
|
|
fs, m := startEmptyBucket(t)
|
|
extended := extendedOf(t, fs, TablesPath, renameTestBucket)
|
|
for key, value := range op.extra {
|
|
extended[key] = value
|
|
}
|
|
fs.Put(TablesPath, renameTestBucket, extended)
|
|
|
|
code, resp := runHandler(t, m, fs.Client, op.operation, op.input(t))
|
|
|
|
assert.Equal(t, http.StatusOK, code, "%+v", resp)
|
|
})
|
|
}
|
|
}
|