Support for project-scoped tokens over the REAPI gRPC surface is now available in Kura 0.9.0. Update to kura@0.9.0 to use this feature.
Hive
feat(kura): support project-scoped tokens over the REAPI gRPC surface
GitHub issue · Closed
What changed
The REAPI surface (Bazel/Buck2, gRPC) now authorizes at project scope using the namespace the request already carries, so a project-scoped token (e.g. a Tuist user token) works over gRPC the same way it does over the HTTP CAS protocol.
Three changes, all in kura/src/reapi/mod.rs (+ a #[cfg(test)] constructor in kura/src/extension/mod.rs):
ctx.tenant_idfrom metadata headers.grpc_extension_contextnow readsx-kura-tenant-id(falling back tox-tuist-account-handle) intoctx.tenant_id, mirroring the HTTPtenant_id/account_handlequery aliases. This lets the extension enforce the same request-account-matches-server-tenant guard the HTTP path already has.- GetCapabilities namespace. It now derives its namespace from
instance_nameinstead of dropping it (None). - ByteStream Write (deferred, single authorize per stream). Write captures the request metadata up front, then authorizes once per stream — on the first chunk, as soon as the namespace is known from its
resource_name(authorize_requestwas split into a metadata-basedauthorize_metadata). Later chunks are not re-authorized; they’re only checked for a consistentresource_name, and a mid-stream change is rejected. Authorization runs before any blob data is written, and the temp file is removed on denial.
Why
Over gRPC, the extension previously authorized two RPCs — GetCapabilities and ByteStream Write — at account scope, because Kura handed them no namespace:
- GetCapabilities hardcoded
namespace_id: None(the request’sinstance_namewas dropped). - ByteStream Write authorized before reading the stream, but its namespace lives inside the first chunk’s
resource_name, so it had to fall back toNone→ account scope.
GetCapabilities is Bazel’s first call and ByteStream Write handles large-blob uploads, so a project-scoped token was rejected on both and couldn’t drive a Bazel session at all — even though the same token works over HTTP CAS.
Why this approach
The namespace always comes from the REAPI instance_name / resource_name, never a client-supplied namespace header. That means the value authorized is exactly the value the blob is stored under (artifact_storage_id), so authorization and storage can’t diverge — no confused-deputy where a token authorized for project A writes into project B. The account half of the identifier is always the node’s KURA_TENANT_ID, never client-supplied, so a token granting a/ios can never satisfy a b/ios target.
tuist.lua needs no change: every gRPC RPC now hands the hook a non-nil project namespace, and the existing project-scope grant check + tenant guard do the rest. The tenant header is opt-in/non-breaking — clients that omit it keep the prior behavior.
Impact
- Project-scoped (user) tokens now authorize the full Bazel REAPI surface: GetCapabilities, FindMissingBlobs, Batch{Update,Read}Blobs, action cache, ByteStream Read, and ByteStream Write.
- Behavioral note: GetCapabilities with an empty
instance_nameresolves to namespacedefaultand is denied unless adefaultproject exists — Bazel sets--remote_instance_namewhen configured.
Validation
cargo build,cargo clippy --all-targets -- -D warnings, and the fullreapitest suite pass.- New tests: header-extraction unit tests for
ctx.tenant_id, plus extension-enabled end-to-end tests asserting GetCapabilities and ByteStream Write authorize a granted namespace and deny a non-granted one (the latter returnsPermissionDeniedbefore the blob is persisted).
🤖 Generated with Claude Code