s3api: tolerate session tokens on statically configured credentials (#11694)

* s3api: consolidate session token extraction into extractSessionToken

Three call sites duplicated the same header/header/query lookup; share
one helper named after the existing s3tables equivalent.

* s3api: tolerate session tokens on statically configured credentials

Credential vendors like Unity Catalog emit a session token with every vended credential, including static ones (UC's StaticAwsCredentialGenerator only engages when s3.sessionToken is set). Requests signed by a configured access key were routed to STS validation and rejected, so static credential vending never worked against SeaweedFS.

Resolve the access key first: when it maps to a configured credential the signature alone authenticates the request, and the attached token is marked ignored so authorization does not route it into the STS session-policy path. STS-issued access keys are never in the static map, so temporary credentials still validate their token exactly as before.

* s3api: cover static credentials carrying a session token

* test: exercise UC static credential vending against SeaweedFS

s3.sessionToken.0 selects UC's StaticAwsCredentialGenerator, which vends the configured keys verbatim. The vended session token is foreign to SeaweedFS and previously failed SigV4; now it round-trips through temporary-table-credentials into real S3 I/O.

* s3api: reject temporary credentials in GetFederationToken after auth

Token presence alone cannot distinguish a temporary credential from a
statically configured one carrying a vended token. Move the check behind
verifyV4Signature and key it on the operative session token so tolerated
tokens keep the caller eligible.

* s3api: test GetFederationToken with authenticated temporary credentials

Sign the rejection cases with real session credentials so they reach the
post-auth check, and cover a vended static credential being accepted.

* test: check vended-credential delete error in UC integration test
This commit is contained in:
Chris Lu authored and GitHub committed 2026-10-10 10:11:24 +08:00
1 parent 924a683397
commit c023493cf0
9 files changed
+297 -109

No files matched your search

@@ -26,6 +26,10 @@ const (
ucAPIBase = "/api/2.1/unity-catalog"
ucWarehouse = "lakehouse"
ucWarehouseKey = "warehouse"
// A non-empty s3.sessionToken.N puts UC's credential vendor on its
// StaticAwsCredentialGenerator path: it vends the configured access key,
// secret key and this token verbatim instead of calling AWS STS.
ucVendedSessionToken = "unity-catalog-vended-session-token"
// Role used by the master-role STS-vended variant of the test. The trust
// policy is wide open so any caller can assume it; in production UC
@@ -65,6 +69,9 @@ type ucServerOpts struct {
// MasterRoleArn populates aws.masterRoleArn. Empty means UC falls back to
// static aws.accessKey / aws.secretKey for storage operations.
MasterRoleArn string
// SessionToken populates s3.sessionToken.0, selecting UC's static
// credential generator for the per-bucket config.
SessionToken string
// ExtraEnv adds environment variables to the UC container, useful for
// AWS_ENDPOINT_URL_STS-style overrides.
ExtraEnv map[string]string
@@ -220,6 +227,7 @@ func (env *testEnv) startUnityCatalog(t *testing.T, ctx context.Context, opts uc
fmt.Sprintf("s3.awsRoleArn.0=%s", opts.MasterRoleArn),
fmt.Sprintf("s3.accessKey.0=%s", env.accessKey),
fmt.Sprintf("s3.secretKey.0=%s", env.secretKey),
fmt.Sprintf("s3.sessionToken.0=%s", opts.SessionToken),
fmt.Sprintf("s3.endpoint.0=%s", s3EndpointForContainer),
"",
}, "\n")
@@ -8,6 +8,7 @@ package unity_catalog
import (
"context"
"fmt"
"strings"
"testing"
"time"
@@ -46,7 +47,7 @@ func TestUnityCatalogDeltaIntegration(t *testing.T) {
}
t.Log(">>> starting Unity Catalog server (static keys)...")
env.startUnityCatalog(t, ctx, ucServerOpts{})
env.startUnityCatalog(t, ctx, ucServerOpts{SessionToken: ucVendedSessionToken})
t.Log(">>> Unity Catalog ready")
uc := newUCClient(fmt.Sprintf("http://127.0.0.1:%d", env.ucHostPort))
@@ -145,28 +146,41 @@ func TestUnityCatalogDeltaIntegration(t *testing.T) {
}
})
t.Run("TemporaryTableCredentialsRejected", func(t *testing.T) {
// With aws.masterRoleArn empty AND no s3.sessionToken.0 set, UC OSS
// always tries to AssumeRole via its internal StsClient (see
// AwsCredentialVendor.createPerBucketCredentialGenerator). Against a
// non-AWS endpoint, that call doesn't reach a real STS, so UC returns
// "S3 bucket configuration not found." or an STS-side error. This is
// the gap users hit at <https://github.com/data-engineering-helpers/mds-in-a-box/blob/main/unitycatalog-playground/etc/conf/server.properties#L45>:
// "with simple S3 access and secret keys, Unity Catalog does not seem
// to work."
//
// The assertion is therefore inverted: we expect a non-nil error from
// /temporary-table-credentials with this configuration. A future
// variant can pin s3.sessionToken.0 (UC's StaticAwsCredentialGenerator
// path) once SeaweedFS' SigV4 path tolerates the vended session token.
t.Run("TemporaryTableCredentials", func(t *testing.T) {
// s3.sessionToken.0 is set, so UC's AwsCredentialVendor takes the
// StaticAwsCredentialGenerator path and vends the configured keys
// verbatim, token included — no STS call at all. SeaweedFS authenticates
// the request on the static access key and tolerates the foreign token.
if createdTable.TableID == "" {
t.Fatalf("created table has empty table_id; cannot request temporary credentials")
}
_, err := uc.generateTemporaryTableCredentials(ctx, createdTable.TableID, "READ_WRITE")
if err == nil {
t.Fatalf("expected /temporary-table-credentials to fail with the static-key playground configuration; it succeeded unexpectedly")
creds, err := uc.generateTemporaryTableCredentials(ctx, createdTable.TableID, "READ_WRITE")
if err != nil {
t.Fatalf("temporary-table-credentials: %v", err)
}
awsCreds := creds.AwsTempCredentials
if awsCreds == nil || awsCreds.AccessKeyID == "" || awsCreds.SessionToken == "" {
t.Fatalf("expected aws_temp_credentials with a session_token, got %+v", creds)
}
if awsCreds.AccessKeyID != env.accessKey || awsCreds.SessionToken != ucVendedSessionToken {
t.Fatalf("expected UC to vend the configured static credential, got %+v", awsCreds)
}
s3v := env.newHostS3ClientWithCreds(t, ctx, awsCreds.AccessKeyID, awsCreds.SecretAccessKey, awsCreds.SessionToken)
probeKey := fmt.Sprintf("%s/%s/%s/vended.txt", ucWarehouseKey, schemaName, tableName)
if _, err := s3v.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(ucWarehouse),
Key: aws.String(probeKey),
Body: strings.NewReader("ok"),
}); err != nil {
t.Fatalf("PutObject with UC-vended credentials: %v", err)
}
if _, err := s3v.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: aws.String(ucWarehouse),
Key: aws.String(probeKey),
}); err != nil {
t.Fatalf("DeleteObject with UC-vended credentials: %v", err)
}
t.Logf("expected failure (UC static-key path requires AWS STS): %v", err)
})
t.Run("DeleteTableSchemaCatalog", func(t *testing.T) {