Repository navigation
api: serve native file access RPCs on the activation listener - #1365
Conversation
nikhilbirmiwal-pplx
left a comment
There was a problem hiding this comment.
Checked authorization and refusal parity: both RPCs run a body-less request through the same native_router layers as the public routes (request id, error normalization, require_native_auth incl. external console/federated owner, hidden-op gate, owner_scope::authorize with key namespacing, data-plane failover scope), decide is reachable only with the RPC-set FileAccessCall extension, and prepare mirrors each handler's key check → bind → guest key → runtime grant with matching metric names, Access activity and unprotected_mutation_error vs controller_error mapping. The SpacedRuntimeGrant extraction and RuntimeClient endpoint plumbing are behavior-preserving (initial vs mid-stream charge flag kept). Non-blocking perf note: every file op now pays two full credential/owner-scope resolutions (Authorize + Prepare) plus RPC round trips; fine given the stated no-cache revocation goal, but worth watching on the hot path.
0c7cba6 to
1be4b2a
Compare
5f36148 to
1640ffb
Compare
1be4b2a to
41953a9
Compare
Add NativeFileAccessApi beside IngressActivationApi on space-api's
activation listener (Cluster bearer auth), for a data-plane edge that
carries a Native files:* request's bytes and leaves every decision to
space-api. Nothing calls it yet.
- AuthorizeNativeFileRequest takes the operation, the {sandbox_id} path
segment, the client's headers and the edge's request id, and answers
with the effective org and owner, or with the response the public
route renders for the refusal (status, headers, Native envelope).
- PrepareNativeFileCall authorizes again, binds the Idempotency-Key to the
request's fingerprint for write, write_batch, mkdir, remove and rename,
and runs the drain gate, admission charge and inline wake. It answers
with a route grant (the runtime endpoint the API connected to, the
bridge route token over the sandbox's authoritative route identity, and
for write, write_batch and rename the guest key), or the rendered
refusal.
Both run the request through the public route's own layers: v1_router is
split into native_router, and native_file_access_router mounts the files
operations behind it with a capture handler that stops where each
handler would call the sandbox. Credentials, the external console
authenticator, federated owners and grants, the owner-less key
allowlist, owner scope and key namespacing are therefore the public
route's, unchanged and uncached.
The spaced client's connect, authoritative route reload and token mint
become SpacedRuntimeGrant, which the spaced client, its reconnect and
the RPC share; RuntimeClient records the endpoint it dialed. The
ingress path keeps calling connect_spaced_runtime_client alone, since it
needs no token. FileOperation in pplx-space-native-files names the
routes and what each does with its key.
1640ffb to
3975a74
Compare
Summary
Adds
NativeFileAccessApi(proto/node/api/v1/native_file_access.proto) to space-api's existing activation listener, which uses Cluster bearer auth. The service is for a data-plane edge that carries a Nativefiles:*request's bytes and leaves every decision to space-api. Nothing calls it yet. The data-plane identity's allowlist entry is the next PR.AuthorizeNativeFileRequesttakes the operation, the{sandbox_id}path segment as sent, the client's headers and the edge's request id. It answers with the effective org and owner, or with the exact response the public route renders for the refusal: status, headers and Native envelope, carrying the edge's request id.PrepareNativeFileCallauthorizes again, with no cache, so revocation stays immediate. For write, write_batch, mkdir, remove and rename it binds theIdempotency-Keyto the request's fingerprint. It then runs the drain gate, the admission charge and the inline wake. It answers with a route grant, or with the rendered refusal. The grant carries:How the decisions stay the public route's own. Both RPCs run a body-less copy of the request through the public route's own layers: request id, error normalization,
require_native_auth, the hidden-operation gate, owner scope, and the native data-plane failover scope.v1_routeris split intonative_router, andnative_file_access_routermounts the 11 files operations behind it with a capture handler (routes/public/files/access.rs). The handler stops where each route's handler would call the sandbox, and keeps that handler's order:unprotected_mutation_errorfor mkdir, remove and change tracking).This reuses everything unchanged: session tokens, the injected external console authenticator, federated owners and service-grant upserts, the owner-less key allowlist, owner scope, and federated key namespacing.
Supporting changes:
SpacedRuntimeGrant(controllers/sandbox/spaced/runtime_grant.rs). The spaced client, its mid-stream reconnect and the RPC all use it.connect_spaced_runtime_clientitself is unchanged, because the ingress caller needs no token and would otherwise mint one on every preparation.RuntimeClientrecords the URI it dialed.FileOperationinpplx-space-native-filesnames each route and what it does with its key.Validation
At this head (
1640ffba), onmainat0271415a:git range-diffreports it identical).cargo fmt --all -- --check,scripts/generate-env-contract.sh --check,scripts/check-rust-error-policy.shandscripts/source_guards/env_test_hook_boundary.pypass.cargo clippy --locked --all-targets --all-features -p pplx-space-native-files -p pplx-space-api -p pplx-space-node-api-client -- -D warningspasses.cargo check --workspace --all-targets --lockedpasses.cargo archagainstmain's baseline, renames andARCHITECTURE.toml: 0 required findings and 112 advisory, the same findings asmain.cargo nextest run --config 'profile.test.package.pplx-space-api.opt-level=0' --all-features --all-targets, one package at a time, with Postgres and Redis from the nextest setup scripts andSPACE_CI_REQUIRE_TEST_DEPS=1:pplx-space-api2189 passed, 1 skipped;pplx-space-native-files2 passed;pplx-space-node-api-client28 passed.cargo checkbuilds the whole workspace against the new proto. No checked-in generated code comes fromnode/api/v1protos (the Go shim vendors only the sandbox-manager, gateway, policy, execution and operations protos), so there is nothing to regenerate.New tests. Each was mutation-checked: the named change was applied, the test failed, and the change was reverted.
native_file_access::tests::a_request_without_valid_credentials_is_refused_as_its_route_refuses_itfiles:statroute returns, carrying the edge's request id in the envelope andx-request-idnative_router's layers…::a_credential_without_an_owner_is_authorized_for_reads_onlyfiles:modified. A key acting for a user reports that ownerservice_credential_may_callrefusal inrequire_native_auth…::a_federated_owners_key_is_bound_in_its_own_namespacenative-owner-<sha256(subject, key)>, scoped to the sandbox, and nothing is bound under the raw keynamespace_idempotency_keyfrom owner scope…::a_changed_request_is_refused_before_the_runtime_is_reacheda_refused_write_never_acquires_the_runtime: withsnapshot_storage_locationslocked, a changed fingerprint still gets 409idempotency_conflictwhile the bound request waits on runtime acquisition…::a_request_is_bound_before_any_grantINVALID_ARGUMENTingress_activation::tests::remote_activation_authenticates_and_preserves_terminal_status(extended)NativeFileAccessApibehind the same bearer check: no token or a wrong one isUNAUTHENTICATED, and the right one reaches the servicepplx-space-native-files::tests::every_files_route_is_one_operationfiles:route is exactly oneFileOperation, named by its actionModifiedtoLIST_FILESNot validated here: a successful grant against a live runtime. The tests' sandboxes are stopped, so preparation ends at the wake. The Kind run at the top of this stack shows only that Native behaviour is unchanged; it does not exercise the RPCs.
Severity and risk
Severity: does not apply (new internal RPC, no caller).
Risk:
native_router(the same layers in the same order, whichevery_control_plane_operation_is_mountedcovers),SpacedRuntimeGrantin the spaced client and its reconnect (the same three steps), and the newRuntimeClientfield.To roll back, revert this PR.
Rollout
The listener is configured only where
SPACE_INGRESS_ACTIVATION_LISTENis set, as before. A data plane calling an older space-api getsUNIMPLEMENTED. The edge must then keep traffic on space-api.Stack
This is PR 4 of 5 and depends on #1364 (
native-error-rendering-shared). Merge in order:native-files-shared-request)native-files-shared-handling)native-error-rendering-shared)data-plane-native-file-access-authz)Not covered
Idempotency-Keyis reported after JSON extraction and before field checks. A caller that validates the body between the two RPCs must check the key first to keep the same message precedence.AI assistance: Claude Code drafted the code, tests, and this description; the author reviewed them.