mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-20 13:30:46 +02:00
* iceberg: vend table-scoped credentials to clients that ask for delegation The catalog recognised X-Iceberg-Access-Delegation: vended-credentials and then deliberately said nothing, because it had nothing to vend: it withheld even the S3 endpoint so the client would keep the credentials it was configured with. That left every engine expecting the catalog to hand out access - Snowflake, Databricks, Trino with vending, any multi-tenant setup - needing static S3 keys distributed out of band. Mint an STS session per request instead, scoped by a session policy to the table's own prefix plus the bucket listing needed to resolve it, and return it in the load response config and storage-credentials. The role to assume is named by -s3.iceberg.credentialRole; its trust policy is what decides whether a caller may assume it, and vending stays off until it is set. A failed mint falls back to the old silence rather than handing back an endpoint the client cannot sign for. * iceberg: keep vended credentials inside the table prefix Review follow-ups on credential vending: Listing was granted on the bucket ARN with no condition, so a credential vended for one table could enumerate every other table's object names. Constrain s3:prefix to the table's own prefix, which the S3 gateway already populates for list requests. A table location carrying * or ? would have gone into the policy's resource pattern unescaped and widened the session to sibling prefixes. Refuse to vend for such a location rather than escaping it; nothing the catalog generates contains those characters. DurationSeconds skipped the 900..43200 bounds the other assume-role paths enforce, so -s3.iceberg.credentialDurationSeconds could ask for a session outside them. The check is now shared by all three entry points. * iceberg: return the vended credentials from buildFileIOConfig itself buildStorageConfig was a second name for what buildFileIOConfig already did; it now returns the storage credentials alongside the properties, and callers that only want the properties drop them. * iceberg: split the vended bucket grants, and refuse a whole-bucket scope The prefix condition sat on a statement that also granted GetBucketLocation and ListBucketMultipartUploads, neither of which carries an s3:prefix to satisfy it, so both were denied for every vended credential. GetBucketLocation moves to its own unconditioned statement. ListBucketMultipartUploads is dropped: Iceberg writers complete and abort by upload id, and granting it either leaks in-flight keys bucket-wide or breaks on the same missing prefix. A table whose location has no prefix - one registered at the bucket root - would have been vended read and write over every other table in the bucket. Refuse, the way a location with wildcards is refused.
112 lines
4.1 KiB
Go
112 lines
4.1 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
|
|
}
|
|
|
|
func (v *stubVendor) VendTableCredentials(_ context.Context, principal, bucket, prefix string) (*VendedCredentials, error) {
|
|
v.gotcaller, v.gotBucket, v.gotPrefix = principal, bucket, prefix
|
|
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")
|
|
|
|
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")
|
|
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")
|
|
if len(config) != 0 || storageCredentials != nil {
|
|
t.Errorf("config = %v, storage-credentials = %v, want both empty", config, storageCredentials)
|
|
}
|
|
}
|
|
|
|
// 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")
|
|
|
|
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")
|
|
}
|
|
}
|