* 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.
* iceberg: withhold the S3 endpoint from credential-vending clients
A client that sends X-Iceberg-Access-Delegation: vended-credentials builds
its storage credential out of the LoadTable config and drops the one it was
configured with. We vend no credentials, so the endpoint we advertised left
DuckDB signing nothing: every metadata and data file came back 403, and its
attempt to refresh the empty credential 404ed on stage-created tables.
Answer those clients with no config at all so they keep their own
credentials. Clients that do not ask for delegation still get the endpoint.
* iceberg: mark load responses as varying on the delegation header
The FileIO config in a table or view load response now depends on whether
the client asked for vended credentials, so a cache between us and the
client must key on that header rather than on the URL alone.
* test: cover the DuckDB vended-credentials access pattern
Runs weed mini with -s3.externalUrl, which is what makes the catalog
advertise an endpoint at all, and checks both halves: a plain LoadTable
still gets the endpoint, while one asking for vended credentials never gets
an endpoint without the credentials to sign with. The DuckDB round trip
creates a table from a query and reads it back, which is the flow that
failed with 403 on every data file.
* iceberg: accept bare bucket names and ARNs as the catalog warehouse
Only s3://<bucket>/ was recognized. A warehouse spelled as a bare table
bucket name or as the s3tables bucket ARN -- the two forms users reach for
first, the latter being what AWS S3 Tables itself takes -- was silently
dropped, so every call landed on the default "warehouse" bucket and failed
with "table bucket warehouse not found".
* iceberg: report a missing table bucket as 404, not 500
Pointing a client at a table bucket that does not exist -- which every
client with no warehouse set does, since the default bucket "warehouse"
rarely exists -- returned InternalServerError with a message naming a
bucket the client never asked for. Answer 404 and say how to select one.
* admin: show the warehouse in the PyIceberg example
The example connected without one, so it always resolved to the default
table bucket and every client that copied it failed on the first call.
* test: pin bearer auth against a table bucket that exists
The subtest called the catalog with no warehouse and accepted 500 as proof
that auth had passed, since the default bucket does not exist. A missing
table bucket now answers 404, which the test read as an auth failure. Give
it a real table bucket so only 200 passes.
* test: assert the missing-bucket guidance reaches the client
The status and error type were checked but not the message, which is the
part of the mapping that tells a user how to select a table bucket.
* test: encode the warehouse query value
The ARN case pasted raw colons and slashes into the query string. Go's
parser tolerates them, so the test passed without modelling how a client
actually sends the request.
* fix(iceberg): route catalog clients to the right bucket and vend S3 endpoint
DuckDB ATTACH 's3://<bucket>/' AS cat (TYPE 'ICEBERG', ...) was failing
with "schema does not exist" because GET /v1/config ignored the warehouse
query parameter and returned no overrides.prefix, so subsequent requests
fell through to the hard-coded "warehouse" default bucket instead of the
one the client attached. LoadTable also returned an empty config, forcing
clients to discover the S3 endpoint out-of-band and producing 403s on
direct iceberg_scan calls.
- handleConfig now echoes overrides.prefix = bucket and defaults.warehouse
when ?warehouse=s3://<bucket>/ is supplied.
- getBucketFromPrefix honors a warehouse query parameter as a fallback for
clients that skip the /v1/config handshake.
- LoadTable responses advertise s3.endpoint and s3.path-style-access so
clients can reach data files without separate configuration.
Refs #9103
* address review feedback on iceberg S3 endpoint vending
- deriveS3AdvertisedEndpoint is now a method on S3Options; honors
externalUrl / S3_EXTERNAL_URL, switches to https when -s3.key.file is
set, uses the https port when configured, and brackets IPv6 literals
via util.JoinHostPort.
- handleCreateTable returns s.buildFileIOConfig() in both its staged and
final LoadTableResult branches so create and load flows see the same
FileIO hints.
- Add unit test coverage for the endpoint derivation scenarios.
* address CI and review feedback for #9109
- DuckDB integration test now runs under its own newOAuthTestEnv (with a
valid IAM config) so the OAuth2 client_credentials flow DuckDB requires
actually works; the shared env has no registered credentials, which was
the cause of the CI failure. Helper createTableWithToken was added to
create tables via Bearer auth.
- Tighten TestIssue9103_LoadTableDoesNotVendS3FileIOCredentials to also
assert s3.path-style-access = "true", so a partial regression where the
endpoint is vended but path-style is dropped still fails.
- deriveS3AdvertisedEndpoint now logs a startup hint when it infers the
host from os.Hostname because the bind IP is a wildcard, pointing
operators at -s3.externalUrl / S3_EXTERNAL_URL for reverse-proxy
deployments where the inferred name is not externally reachable.
- handleConfig has a comment explaining that any sub-path in the
warehouse URL is dropped because catalog routing is bucket-scoped.
* fix(iceberg): make advertised S3 endpoint strictly opt-in; add region
The wildcard-bind fallback to os.Hostname() in deriveS3AdvertisedEndpoint
was hijacking correctly-configured clients: on the CI runner it produced
http://runnervmrc6n4:<port>, which Spark (running in Docker) could not
resolve, so Spark iceberg tests began failing after the endpoint started
being vended in LoadTable responses.
Change the rule so advertising is opt-in and never guesses a host that
might not be routable:
- -s3.externalUrl / S3_EXTERNAL_URL wins (covers reverse-proxy).
- Otherwise, only an explicit, non-wildcard -s3.bindIp is used.
- Wildcard / empty bind returns "" so no FileIO endpoint is vended and
existing clients keep using their own configuration.
buildFileIOConfig additionally vends s3.region (defaulting to the same
value baked into table bucket ARNs) whenever it vends an endpoint, so
DuckDB's attach does not fail with "No region was provided via the
vended credentials" when the operator has opted in.
The DuckDB issue-9103 integration test runs under an env with a
wildcard bind, so it explicitly sets AWS_REGION in the docker run to
pick up the same default. The HTTP-level LoadTable-vending test was
dropped because its expectation is now conditional and already covered
by unit tests in iceberg_issue_9103_test.go.