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.
80 lines
3.1 KiB
Go
80 lines
3.1 KiB
Go
package iceberg
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3tables"
|
|
)
|
|
|
|
func TestWantsVendedCredentials(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
values []string
|
|
want bool
|
|
}{
|
|
{name: "no header", want: false},
|
|
{name: "vended credentials", values: []string{"vended-credentials"}, want: true},
|
|
{name: "mechanism list", values: []string{"remote-signing,vended-credentials"}, want: true},
|
|
{name: "spaced and mixed case", values: []string{"Remote-Signing, Vended-Credentials"}, want: true},
|
|
{name: "repeated header", values: []string{"remote-signing", "vended-credentials"}, want: true},
|
|
{name: "remote signing only", values: []string{"remote-signing"}, want: false},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
r := httptest.NewRequest(http.MethodGet, "/v1/namespaces/ns/tables/t", nil)
|
|
for _, v := range tc.values {
|
|
r.Header.Add(accessDelegationHeader, v)
|
|
}
|
|
if got := wantsVendedCredentials(r); got != tc.want {
|
|
t.Fatalf("wantsVendedCredentials(%v) = %v, want %v", tc.values, got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// A client that asked for vended credentials replaces its own storage
|
|
// credentials with whatever the catalog returns. With no vending configured
|
|
// there are none, so an endpoint by itself would leave the client signing
|
|
// nothing and every data file read and write would come back 403.
|
|
func TestBuildFileIOConfigWithholdsEndpointFromCredentialVendingClients(t *testing.T) {
|
|
s := &Server{s3Endpoint: "http://seaweed.example:8333"}
|
|
|
|
r := httptest.NewRequest(http.MethodGet, "/v1/namespaces/ns/tables/t", nil)
|
|
r.Header.Set(accessDelegationHeader, "vended-credentials")
|
|
if got, _ := s.buildFileIOConfig(r, "s3://warehouse/ns/t"); len(got) != 0 {
|
|
t.Fatalf("buildFileIOConfig() = %v, want empty for a vended-credentials request", got)
|
|
}
|
|
|
|
plain := httptest.NewRequest(http.MethodGet, "/v1/namespaces/ns/tables/t", nil)
|
|
if got, _ := s.buildFileIOConfig(plain, "s3://warehouse/ns/t"); got["s3.endpoint"] != s.s3Endpoint {
|
|
t.Fatalf("s3.endpoint = %q, want %q", got["s3.endpoint"], s.s3Endpoint)
|
|
}
|
|
}
|
|
|
|
// The body of a load response depends on the delegation header, so a cache in
|
|
// front of the catalog must not key on the URL alone.
|
|
func TestWriteLoadResultVariesOnTheDelegationHeader(t *testing.T) {
|
|
rec := httptest.NewRecorder()
|
|
writeLoadResult(rec, http.StatusOK, LoadTableResult{})
|
|
if got := rec.Header().Get("Vary"); got != accessDelegationHeader {
|
|
t.Fatalf("Vary = %q, want %q", got, accessDelegationHeader)
|
|
}
|
|
}
|
|
|
|
func TestLoadTableResultOmitsConfigForCredentialVendingClients(t *testing.T) {
|
|
s := &Server{s3Endpoint: "http://seaweed.example:8333"}
|
|
r := httptest.NewRequest(http.MethodGet, "/v1/namespaces/ns/tables/t", nil)
|
|
r.Header.Set(accessDelegationHeader, "vended-credentials")
|
|
|
|
getResp := s3tables.GetTableResponse{MetadataLocation: "s3://bkt/ns/t/metadata/v1.metadata.json"}
|
|
result, err := s.buildLoadTableResult(r, getResp, "bkt", []string{"ns"}, "t")
|
|
if err != nil {
|
|
t.Fatalf("buildLoadTableResult() error = %v", err)
|
|
}
|
|
if len(result.Config) != 0 {
|
|
t.Fatalf("LoadTableResult.Config = %v, want empty", result.Config)
|
|
}
|
|
}
|