feat(picloud): opportunistic principal middleware on the data plane

The data-plane (POST /execute/{id} + user-route fallback) is
unauthenticated by default — public scripts get hit by anonymous HTTP
traffic. But some calls are authed (dashboard test-runs, API-key
invocations) and v1.1.x services will want to see the caller via
`cx.principal` for audit / authz once those features land.

  - New manager-core::attach_principal_if_present middleware. Always
    inserts Extension<Option<Principal>>: Some on resolved bearer/cookie,
    None on absent or malformed token. Fail-open on DB blip so a
    transient infra failure can't 500 anonymous traffic.
  - Wired in picloud build_app, scoped to the data-plane and user-routes
    routers only. The admin path keeps using require_authenticated; no
    double-resolve on the same token.
  - orchestrator-core handlers (execute_by_id, user_route_handler) now
    extract Extension<Option<Principal>> and pass it to build_exec_request.
    Replaces the temporary `None` placeholders from the previous commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-05-30 18:53:27 +02:00
parent dea776b2a3
commit 902dd78027
4 changed files with 70 additions and 18 deletions

View File

@@ -12,7 +12,7 @@ use axum::{
http::{HeaderMap, HeaderName, HeaderValue, StatusCode},
response::{IntoResponse, Response},
routing::post,
Json, Router,
Extension, Json, Router,
};
use chrono::Utc;
use picloud_executor_core::{ExecError, ExecRequest, ExecResponse, InvocationType};
@@ -55,6 +55,11 @@ impl<E, R> Clone for DataPlaneState<E, R> {
/// Build the data-plane router. Handles `POST /execute/:id` — the
/// always-available ID-based bypass.
///
/// Handlers expect an `Extension<Option<Principal>>` to be attached by
/// upstream middleware (`manager-core::attach_principal_if_present`);
/// requests without that extension panic at extraction time. The
/// picloud binary wires this in `build_app`.
pub fn data_plane_router<E, R>(state: DataPlaneState<E, R>) -> Router
where
E: ExecutorClient + 'static,
@@ -68,6 +73,10 @@ where
/// Build a router that handles ALL paths via the user-defined routing
/// table. Intended to be merged into the picloud app router as a
/// fallback (after the system routes are mounted).
///
/// Same middleware expectation as `data_plane_router` — wrap with
/// `attach_principal_if_present` so handlers can extract
/// `Extension<Option<Principal>>`.
pub fn user_routes_router<E, R>(state: DataPlaneState<E, R>) -> Router
where
E: ExecutorClient + 'static,
@@ -85,6 +94,7 @@ where
async fn execute_by_id<E, R>(
State(state): State<DataPlaneState<E, R>>,
Path(id): Path<ScriptId>,
Extension(principal): Extension<Option<Principal>>,
headers: HeaderMap,
body: Bytes,
) -> Result<Response, ApiError>
@@ -98,10 +108,7 @@ where
.await?
.ok_or(ApiError::NotFound(id))?;
// Principal stays `None` until the data-plane `attach_principal_if_present`
// middleware lands in the picloud-wiring commit. Both shapes are
// valid against `ExecRequest.principal: Option<Principal>`.
let mut req = build_exec_request(id, &script.name, &headers, &body, script.app_id, None)?;
let mut req = build_exec_request(id, &script.name, &headers, &body, script.app_id, principal)?;
req.sandbox_overrides = script.sandbox;
let request_id = req.request_id;
let request_path = req.path.clone();
@@ -137,6 +144,7 @@ where
async fn user_route_handler<E, R>(
State(state): State<DataPlaneState<E, R>>,
Extension(principal): Extension<Option<Principal>>,
request: Request,
) -> Result<Response, ApiError>
where
@@ -200,7 +208,7 @@ where
&headers,
&body_bytes,
app_id,
None,
principal,
)?;
req.path = path;
req.params = matched.params;