Hive Hive
Sign in

feat(kura): support project-scoped tokens over the REAPI gRPC surface

GitHub issue · Closed

Metadata
Source
tuist/tuist #11198
Updated
Jun 24, 2026
Domains
Kura
Details

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_id from metadata headers. grpc_extension_context now reads x-kura-tenant-id (falling back to x-tuist-account-handle) into ctx.tenant_id, mirroring the HTTP tenant_id / account_handle query 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_name instead 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_request was split into a metadata-based authorize_metadata). Later chunks are not re-authorized; they’re only checked for a consistent resource_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’s instance_name was 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 to None → 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_name resolves to namespace default and is denied unless a default project exists — Bazel sets --remote_instance_name when configured.

Validation

  • cargo build, cargo clippy --all-targets -- -D warnings, and the full reapi test 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 returns PermissionDenied before the blob is persisted).

🤖 Generated with Claude Code

Comments
TA
tuist-atlas[bot] Jun 11, 2026

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.