Repository navigation
fix(catalog): vended credentials follow catalog grants - #760
Merged
Merged
Conversation
Move `grants_write`, `catalog_grant_covers_by` and `CatalogNameEq` from the catalog guard in egret-bin to `egret_catalog_engine::grant_match`, so lakekeeper's authorizer can decide by the same rule. `CatalogNameEq::load` takes the requested names instead of the guard's needs. No behaviour change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A catalog client with only a read grant on a table (or its namespace) loaded the table and was handed STS credentials with Put and Delete on the table's prefix, with or without the delegation header, and could overwrite and delete its data and metadata files. Lakekeeper picks the credentials' scope from its authorizer's ReadData/WriteData answer, and egret ran it on AllowAllAuthorizer behind a mount that marked every request LakekeeperInternal, which skips the authorizer altogether. EgretAuthorizer answers ReadData and WriteData on a table from catalog.catalog_grants, by the guard's own rule (grant_match): a read grant vends read-only credentials, a write or manage grant covering the table read-write-delete. Every other question it answers as AllowAll did, since the guard has already decided the request. egret's system principal keeps full access; an unknown principal gets no data access. The mount now hands lakekeeper a principal that names the catalog client (warehouse, role, subject from the verified token) instead of the internal actor, except on GET /config, which vends nothing and would otherwise register every client as a lakekeeper user. Tests drive the real guard and lakekeeper router over an in-process S3 and STS that records each session policy: read grants (table, namespace, catalog), no header, /credentials, a reader after a writer (the STS cache is keyed by permission), write and manage grants, egret's own principal, and create/stage-create/register on another table's location. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A config call registers no lakekeeper user (the mount keeps the internal actor there), and a grant spelled in another case still vends credentials of its scope, as the guard's collation decides. Both were mutants of the fix that no test killed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The design (an egret authorizer for lakekeeper, the caller principal the mount hands it, the per-method table), the measurements before and after, the red run, the mutation check, the local gate and the live check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lakekeeper passes a user in an authorization question only for a DEFINER view chain or a for-user permission check. egret's grants belong to the caller, so the authorizer now refuses ReadData/WriteData asked about another user instead of answering with the caller's grants. Neither path is reachable today (no trusted engines, no management API), so a unit test pins the classification; another pins that a grant store error fails the decision (C12). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…by subject A role with namespace read and write on one table gets RWD on that table and read-only on its sibling; a view load vends read-only credentials even to a namespace writer (C9). The config test now looks only for its own token's principal instead of counting every catalog client in the shared database. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With the request metadata set directly, /credentials on an in-memory warehouse shows the data decision: a client of the warehouse may read, a client whose token names a warehouse bound elsewhere may not (the authorizer's own warehouse check, C5), and an unknown principal may not (C4). The order-preserving merge of answers is now a function with a unit test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Narrow C8 to one warehouse and record the cross-warehouse register as a follow-up with its precondition, correct the per-load query count (C6), explain the rebased commit ids, add the post-review tests and mutants, and a section on each review finding. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A catalog client holding only a
readgrant on a table (or on its namespace) loaded the table and was handed STS credentials with Put and Delete on the table's prefix, with or without theX-Iceberg-Access-Delegationheader. Measured on an own stack: the reader overwrote a data file, deleted a data file and deleted the metadata file, and the table could no longer be scanned by anyone.Two causes, both fixed here:
WriteData→ read-write-delete,ReadData→ read), and egret ran it onAllowAllAuthorizer.LakekeeperInternal, which skips the authorizer for every table action, so even a real authorizer would never have been asked.Spec:
docs/en/specs/2026-10-27-vended-credentials-follow-grants-design.md(decisions D1–D4, controller defaults C1–C14, per-method authorizer table, mutation, gate and live check recorded). Plan:docs/en/plans/2026-10-27-vended-credentials-follow-grants.md.On main
827da4bb, after F-A (#758).No migration. No API shape change. Behaviour change: a
readgrant now vends read-only storage credentials, and an engine attached with a read-only role can no longer write.Changes (
egret-catalog-engine,egret-bin)EgretAuthorizer(authorizer.rs) replacesAllowAllAuthorizer. It answersReadDataandWriteDataon a table fromcatalog.catalog_grants, by the guard's own rule. Every other question it answers asAllowAlldid, because the guard has already decided that request (the spec lists each method and why).grants_write,catalog_grant_covers_byandCatalogNameEqmoved from the guard toegret_catalog_engine::grant_match, so the guard's decision and the vended scope cannot disagree. Names compare by Lakekeeper's collation in both (F-A D12).CatalogCallergainswarehouseandrole. For an allowed request the mount hands Lakekeeper a principal (egret-catalog~<client>) with a claimegret_catalog_client {warehouse, role, subject}, built from public Lakekeeper APIs.GET /configkeeps the internal actor: it vends nothing, and with a principal Lakekeeper would register every catalog client as a Lakekeeper user.Decisions
The user decided (2026-10-27): approach (a), an egret authorizer plus a real caller principal; read grants get read-only credentials; an engine with a read-only role becomes read-only (egret-provisioned Trino and StarRocks hold
catalogmanageand are unchanged); the credential lifetime is a separate follow-up. Controller defaults C1–C14 are in the spec, each with its reason.Test plan
New tests in
crates/egret-bin/src/catalog_vended_credentials_tests.rsrun the real guard, the table hook and Lakekeeper's router (mount_catalog_engine) on a warehouse with egret's S3 profile overMockS3: an in-process S3 and STS that records the session policy of every key Lakekeeper mints, and enforces it as RustFS does. Lakekeeper's own storage validation of the warehouse runs against it./credentials, and a reader handed the writer's cached key). The 5 controls passed.Full local gate on the pre-review code (
4599c6c5; before the rebase this was12ba90c1, with identical code), against the real test DB with no DB skips:cargo fmt --check, clippy over the workspace with-D warnings,just test-shipping-resolutioncargo test --workspace --no-fail-fastsql_starrocks_live, which needs a live StarRocks, as on earlier PRs. 2 are timing bounds that failed at a load average of 20–40 caused by other sessions:a_rename_with_nothing_bound_moves_at_oncepassed 3/3 alone, andoverflow_records_never_hold_more_connections_than_the_cappassed 6/7 alonejust test-ranger-gatesnpm ci,gen:api(no diff), lint,tsc -b, build, all 10test:bundle*After the review, on
bd80dbe5(the last code commit):cargo fmt --checkpass, workspace clippy-D warningspass, egret-bincatalog_tests 58 passed,egret-catalog-engine155 passed / 0 failed, no DB skips. The full workspace gate and the live check were not repeated: the only change to the vending decision refuses questions egret cannot reach today.Live check
On an own stack (
egret-f4creds), with RustFS, pyiceberg 0.12 and a Trino 483 engine provisioned by egret. Findings M1–M4 were repeated on the fixed code./credentials, now gets a session policy ofs3:GetObject, s3:GetObjectVersiononly.ACCESS_DENIED.catalogmanage) ran CREATE, INSERT, UPDATE, DELETE, RENAME and DROP, and also INSERT into a pyiceberg-made table. Everything succeeded.Review
An independent review found no HIGH. Each finding and how it was handled is in spec §11:
client-managedones.ActionOnTable.userwas ignored. A data question about another user is now refused (red-first unit test).Follow-ups
Docs
🤖 Generated with Claude Code