mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-03 18:47:41 +01:00
* s3api: encrypt the objects of a vector bucket declared AES256 CreateVectorBucket recorded encryptionConfiguration sseType AES256 and returned it, but nothing on the write path read it: the index's data files, manifests and transactions were stored without SSE. A LANCE table bucket's record now supplies the bucket's default encryption whenever the bucket has none of its own, so PutObject, CreateMultipartUpload and CopyObject apply SSE-S3 to every object the engine writes. The S3 bucket metadata itself is unchanged. ICEBERG table buckets keep recording the declaration only: their catalog reads the manifests clients upload straight from the filer, which would see ciphertext. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011UpvafvK8JpagF94MHWqwK * s3tables: refuse an AES256 vector bucket on a gateway without an SSE-S3 key A vector bucket's AES256 is now applied to every index write, and SSE-S3 fails each write when the gateway has no KEK. Creating the bucket there succeeded and the first CreateIndex came back as InternalFailure. CreateTableBucket now refuses a LANCE bucket declaring AES256 when the gateway reports no SSE-S3 key, naming s3.sse.kek and s3.sse.key; CreateVectorBucket returns it as ValidationException. ICEBERG buckets, whose declaration is only recorded, and tooling that sets no checker are unaffected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011UpvafvK8JpagF94MHWqwK * s3api: refuse DeleteBucketEncryption on a bucket's declared encryption GetBucketEncryption reports a vector bucket's declared AES256, but DeleteBucketEncryption answered NoSuchBucketEncryptionConfiguration and the declaration kept applying. The declaration is fixed when the bucket is created, so the delete now returns MethodNotAllowed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011UpvafvK8JpagF94MHWqwK * test(s3vectors): keep an inherited WEED_S3_SSE_KEK out of the test cluster The gateway refuses to start SSE-S3 with both s3.sse.kek and s3.sse.key set, so a developer shell exporting WEED_S3_SSE_KEK made the AES256 bucket test fail. 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>
148 lines
5.6 KiB
Go
148 lines
5.6 KiB
Go
package s3tables
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"testing"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3tables/s3tablestest"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
const formatTestBucket = "formats"
|
|
|
|
// bucketWithFormat lays down a table bucket holding one namespace, declared as
|
|
// the given format. An empty format is a bucket from before the declaration
|
|
// existed.
|
|
func bucketWithFormat(t *testing.T, format string) (*s3tablestest.MemFiler, *Manager) {
|
|
t.Helper()
|
|
fs := s3tablestest.Start(t)
|
|
m := NewManager()
|
|
|
|
bucketMeta, _ := json.Marshal(tableBucketMetadata{
|
|
Name: formatTestBucket,
|
|
OwnerAccountID: DefaultAccountID,
|
|
Format: format,
|
|
})
|
|
fs.Put(TablesPath, formatTestBucket, map[string][]byte{
|
|
ExtendedKeyTableBucket: []byte("{}"),
|
|
ExtendedKeyMetadata: bucketMeta,
|
|
})
|
|
|
|
nsMeta, _ := json.Marshal(namespaceMetadata{Namespace: []string{"ns"}, OwnerAccountID: DefaultAccountID})
|
|
fs.Put(GetTableBucketPath(formatTestBucket), "ns", map[string][]byte{ExtendedKeyMetadata: nsMeta})
|
|
|
|
return fs, m
|
|
}
|
|
|
|
func createTableOfFormat(t *testing.T, m *Manager, fs *s3tablestest.MemFiler, name, format string) error {
|
|
t.Helper()
|
|
return m.Execute(context.Background(), NewManagerClient(fs.Client), "CreateTable", &CreateTableRequest{
|
|
TableBucketARN: "arn:aws:s3tables:::bucket/" + formatTestBucket,
|
|
Namespace: []string{"ns"},
|
|
Name: name,
|
|
Format: format,
|
|
}, nil, "")
|
|
}
|
|
|
|
// The declaration is the point: a bucket that says it holds Lance cannot be
|
|
// handed an Iceberg table, because the catalog serving it would never show one.
|
|
func TestCreateTableRefusesAForeignFormat(t *testing.T) {
|
|
fs, m := bucketWithFormat(t, FormatLance)
|
|
|
|
require.NoError(t, createTableOfFormat(t, m, fs, "vectors", FormatLance))
|
|
|
|
err := createTableOfFormat(t, m, fs, "events", FormatIceberg)
|
|
require.Error(t, err, "an Iceberg table in a Lance bucket should be refused")
|
|
require.Contains(t, err.Error(), "holds LANCE")
|
|
}
|
|
|
|
func TestCreateTableRefusesLanceInAnIcebergBucket(t *testing.T) {
|
|
fs, m := bucketWithFormat(t, FormatIceberg)
|
|
|
|
err := createTableOfFormat(t, m, fs, "vectors", FormatLance)
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "holds ICEBERG")
|
|
}
|
|
|
|
// A bucket made before formats were declared keeps taking anything. Nothing is
|
|
// migrated, so nothing that worked stops working.
|
|
func TestUndeclaredBucketAcceptsEitherFormat(t *testing.T) {
|
|
fs, m := bucketWithFormat(t, "")
|
|
|
|
require.NoError(t, createTableOfFormat(t, m, fs, "events", FormatIceberg))
|
|
require.NoError(t, createTableOfFormat(t, m, fs, "vectors", FormatLance))
|
|
}
|
|
|
|
// A view is Iceberg metadata, so it has no meaning in a bucket of another format.
|
|
func TestCreateViewRefusedInALanceBucket(t *testing.T) {
|
|
fs, m := bucketWithFormat(t, FormatLance)
|
|
|
|
err := m.Execute(context.Background(), NewManagerClient(fs.Client), "CreateView", &CreateViewRequest{
|
|
TableBucketARN: "arn:aws:s3tables:::bucket/" + formatTestBucket,
|
|
Namespace: []string{"ns"},
|
|
Name: "v",
|
|
}, nil, "")
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "cannot hold views")
|
|
}
|
|
|
|
func TestNormalizeFormat(t *testing.T) {
|
|
cases := []struct {
|
|
in string
|
|
want string
|
|
ok bool
|
|
}{
|
|
{"ICEBERG", FormatIceberg, true},
|
|
{"iceberg", FormatIceberg, true},
|
|
{" Lance ", FormatLance, true},
|
|
{"delta", "", false},
|
|
{"", "", false},
|
|
}
|
|
for _, c := range cases {
|
|
got, ok := NormalizeFormat(c.in)
|
|
if got != c.want || ok != c.ok {
|
|
t.Errorf("NormalizeFormat(%q) = %q,%v; want %q,%v", c.in, got, ok, c.want, c.ok)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDeclaredBucketEncryption(t *testing.T) {
|
|
entry := func(format string, cfg map[string]interface{}) *filer_pb.Entry {
|
|
meta, _ := json.Marshal(tableBucketMetadata{Name: "b", Format: format, EncryptionConfiguration: cfg})
|
|
return &filer_pb.Entry{Extended: map[string][]byte{ExtendedKeyTableBucket: []byte("true"), ExtendedKeyMetadata: meta}}
|
|
}
|
|
aes := map[string]interface{}{"sseAlgorithm": "AES256"}
|
|
|
|
require.Equal(t, "AES256", DeclaredBucketEncryption(entry(FormatLance, aes)))
|
|
require.Empty(t, DeclaredBucketEncryption(entry(FormatLance, nil)))
|
|
require.Empty(t, DeclaredBucketEncryption(entry(FormatIceberg, aes)), "the Iceberg catalog reads client manifests raw")
|
|
require.Empty(t, DeclaredBucketEncryption(entry("", aes)), "a bucket without a format is Iceberg")
|
|
require.Empty(t, DeclaredBucketEncryption(&filer_pb.Entry{Extended: map[string][]byte{ExtendedKeyMetadata: entry(FormatLance, aes).Extended[ExtendedKeyMetadata]}}), "not a table bucket")
|
|
require.Empty(t, DeclaredBucketEncryption(nil))
|
|
}
|
|
|
|
// Only a format whose files the catalog leaves to clients gets its declared
|
|
// AES256 applied, so only that format needs the gateway's SSE-S3 key.
|
|
func TestCreateTableBucketNeedsSSES3ForADeclaredLanceBucket(t *testing.T) {
|
|
fs := s3tablestest.Start(t)
|
|
m := NewManager()
|
|
m.SetEncryptionChecker(func() bool { return false })
|
|
create := func(name, format string, cfg map[string]interface{}) error {
|
|
return m.Execute(context.Background(), NewManagerClient(fs.Client), "CreateTableBucket",
|
|
&CreateTableBucketRequest{Name: name, Format: format, EncryptionConfiguration: cfg}, nil, s3_constants.AccountAdminId)
|
|
}
|
|
aes := map[string]interface{}{"sseAlgorithm": "AES256"}
|
|
|
|
err := create("lance-sealed", FormatLance, aes)
|
|
var s3Err *S3TablesError
|
|
require.ErrorAs(t, err, &s3Err)
|
|
require.Equal(t, ErrCodeInvalidRequest, s3Err.Type)
|
|
require.Contains(t, s3Err.Message, "s3.sse.kek")
|
|
require.NoError(t, create("lance-plain", FormatLance, nil))
|
|
require.NoError(t, create("iceberg-sealed", FormatIceberg, aes), "an Iceberg bucket only records the declaration")
|
|
}
|