mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-25 22:40:44 +01:00
* s3tables: share the UpdateTable authorization gate Extract the validation, load, and permission check from handleUpdateTable into authorizeUpdateTable so the table an update was authorized against is the same snapshot the mutation writes to. Manager gains AuthorizeUpdateTable, mirroring AuthorizeCreateTable, so callers can ask the question without updating anything. * s3tables: share the UpdateView authorization gate Extract the lookup, policy load, and permission check from handleUpdateView into authorizeUpdateView, mirroring authorizeUpdateTable, and expose it as Manager.AuthorizeUpdateView for callers that need the answer without the update. * s3api: scope vended table credentials to read-only on request VendTableCredentials takes a readOnly flag and tablePrefixSessionPolicy emits only s3:GetObject for the table prefix when it is set, keeping the conditioned ListBucket and unconditioned GetBucketLocation statements that reads still need. * iceberg: vend read-only credentials when the caller may not write LoadTable and friends vend storage credentials only for the authority the caller holds: the vend now checks UpdateTable/UpdateView permission and a caller that may only read the catalog entry gets a read-only session. Paths that just passed a write gate (stage-create, view commit) keep the full session. * lance: vend read-only credentials when the caller may not write storage_options vends the same read-only session as the Iceberg catalog when the caller lacks UpdateTable on the table. * lance: keep write credentials for callers authorized to declare A Lance commit is a direct S3 write that never revisits the catalog, so a caller that just passed CreateTable still gets a writable session; the UpdateTable gate applies to describe, the read path. * iceberg, lance: log a failed write check instead of only reading it as denied
136 lines
5.2 KiB
Go
136 lines
5.2 KiB
Go
package iceberg
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
type stubVendor struct {
|
|
credentials *VendedCredentials
|
|
err error
|
|
gotBucket string
|
|
gotPrefix string
|
|
gotcaller string
|
|
gotReadOnly bool
|
|
}
|
|
|
|
func (v *stubVendor) VendTableCredentials(_ context.Context, principal, bucket, prefix string, readOnly bool) (*VendedCredentials, error) {
|
|
v.gotcaller, v.gotBucket, v.gotPrefix, v.gotReadOnly = principal, bucket, prefix, readOnly
|
|
return v.credentials, v.err
|
|
}
|
|
|
|
func vendingServer(vendor CredentialVendor) *Server {
|
|
s := &Server{s3Endpoint: "http://s3.example:8333"}
|
|
s.SetCredentialVendor(vendor)
|
|
return s
|
|
}
|
|
|
|
func TestBuildFileIOConfigVendingVendsScopedCredentials(t *testing.T) {
|
|
expiry := time.Now().Add(time.Hour).Truncate(time.Millisecond)
|
|
vendor := &stubVendor{credentials: &VendedCredentials{
|
|
AccessKeyID: "ASIAEXAMPLE",
|
|
SecretAccessKey: "secret",
|
|
SessionToken: "token",
|
|
Expiration: expiry,
|
|
}}
|
|
s := vendingServer(vendor)
|
|
|
|
r := httptest.NewRequest("GET", "/v1/namespaces/ns/tables/t", nil)
|
|
r.Header.Set(accessDelegationHeader, "vended-credentials")
|
|
|
|
config, storageCredentials := s.buildFileIOConfig(r, "s3://warehouse/ns/t", nil)
|
|
|
|
if vendor.gotBucket != "warehouse" || vendor.gotPrefix != "ns/t" {
|
|
t.Errorf("vendor called with bucket=%q prefix=%q, want warehouse and ns/t", vendor.gotBucket, vendor.gotPrefix)
|
|
}
|
|
if config["s3.access-key-id"] != "ASIAEXAMPLE" || config["s3.secret-access-key"] != "secret" || config["s3.session-token"] != "token" {
|
|
t.Errorf("config missing vended credentials: %v", config)
|
|
}
|
|
if config["s3.endpoint"] != "http://s3.example:8333" {
|
|
t.Errorf("config missing the endpoint the credentials are for: %v", config)
|
|
}
|
|
if len(storageCredentials) != 1 {
|
|
t.Fatalf("storage-credentials = %d entries, want 1", len(storageCredentials))
|
|
}
|
|
if storageCredentials[0].Prefix != "s3://warehouse/ns/t" {
|
|
t.Errorf("storage credential prefix = %q", storageCredentials[0].Prefix)
|
|
}
|
|
if storageCredentials[0].Config["s3.session-token"] != "token" {
|
|
t.Errorf("storage credential is missing the session token: %v", storageCredentials[0].Config)
|
|
}
|
|
}
|
|
|
|
// Without a vendor the response must stay silent about the endpoint too: a
|
|
// client that asked for vended credentials would otherwise drop its own and
|
|
// start sending unsigned requests.
|
|
func TestBuildFileIOConfigVendingWithoutVendorSaysNothing(t *testing.T) {
|
|
s := &Server{s3Endpoint: "http://s3.example:8333"}
|
|
r := httptest.NewRequest("GET", "/v1/namespaces/ns/tables/t", nil)
|
|
r.Header.Set(accessDelegationHeader, "vended-credentials")
|
|
|
|
config, storageCredentials := s.buildFileIOConfig(r, "s3://warehouse/ns/t", nil)
|
|
if len(config) != 0 || storageCredentials != nil {
|
|
t.Errorf("config = %v, storage-credentials = %v, want both empty", config, storageCredentials)
|
|
}
|
|
}
|
|
|
|
func TestBuildFileIOConfigVendingFallsBackWhenVendingFails(t *testing.T) {
|
|
s := vendingServer(&stubVendor{err: errors.New("no role configured")})
|
|
r := httptest.NewRequest("GET", "/v1/namespaces/ns/tables/t", nil)
|
|
r.Header.Set(accessDelegationHeader, "vended-credentials")
|
|
|
|
config, storageCredentials := s.buildFileIOConfig(r, "s3://warehouse/ns/t", nil)
|
|
if len(config) != 0 || storageCredentials != nil {
|
|
t.Errorf("config = %v, storage-credentials = %v, want both empty", config, storageCredentials)
|
|
}
|
|
}
|
|
|
|
// The vend stays read-only unless the caller proved write authority: a nil
|
|
// or false canWrite must reach the vendor as readOnly.
|
|
func TestBuildFileIOConfigVendingReadOnlyUnlessCanWrite(t *testing.T) {
|
|
vendor := &stubVendor{credentials: &VendedCredentials{AccessKeyID: "ASIAEXAMPLE"}}
|
|
s := vendingServer(vendor)
|
|
|
|
r := httptest.NewRequest("GET", "/v1/namespaces/ns/tables/t", nil)
|
|
r.Header.Set(accessDelegationHeader, "vended-credentials")
|
|
|
|
s.buildFileIOConfig(r, "s3://warehouse/ns/t", nil)
|
|
if !vendor.gotReadOnly {
|
|
t.Error("vendCredentials() passed readOnly=false for a nil canWrite, want readOnly=true")
|
|
}
|
|
s.buildFileIOConfig(r, "s3://warehouse/ns/t", func() bool { return false })
|
|
if !vendor.gotReadOnly {
|
|
t.Error("vendCredentials() passed readOnly=false for canWrite=false, want readOnly=true")
|
|
}
|
|
s.buildFileIOConfig(r, "s3://warehouse/ns/t", func() bool { return true })
|
|
if vendor.gotReadOnly {
|
|
t.Error("vendCredentials() passed readOnly=true for canWrite=true, want readOnly=false")
|
|
}
|
|
}
|
|
|
|
// A client that did not ask for delegation keeps the plain endpoint config and
|
|
// must never be handed credentials.
|
|
func TestBuildFileIOConfigVendingWithoutDelegationHeader(t *testing.T) {
|
|
vendor := &stubVendor{credentials: &VendedCredentials{AccessKeyID: "ASIAEXAMPLE"}}
|
|
s := vendingServer(vendor)
|
|
|
|
r := httptest.NewRequest("GET", "/v1/namespaces/ns/tables/t", nil)
|
|
config, storageCredentials := s.buildFileIOConfig(r, "s3://warehouse/ns/t", nil)
|
|
|
|
if _, vended := config["s3.access-key-id"]; vended {
|
|
t.Errorf("credentials vended without the delegation header: %v", config)
|
|
}
|
|
if config["s3.endpoint"] != "http://s3.example:8333" {
|
|
t.Errorf("config = %v, want the endpoint", config)
|
|
}
|
|
if storageCredentials != nil {
|
|
t.Errorf("storage-credentials = %v, want none", storageCredentials)
|
|
}
|
|
if vendor.gotBucket != "" {
|
|
t.Errorf("vendor was called for a request that did not ask for delegation")
|
|
}
|
|
}
|