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

@@ -11,14 +11,14 @@ use axum::{routing::get, Json, Router};
use picloud_executor_core::{Engine, Limits};
use picloud_manager_core::{
admin_router, admins_router, api_keys_router, app_members_router, apps_api, apps_router,
auth_router, compile_routes, migrations, require_authenticated, route_admin_router,
AdminSessionRepository, AdminState, AdminUserRepository, AdminsState, ApiKeyRepository,
ApiKeysState, AppDomainRepository, AppMembersRepository, AppMembersState, AppRepository,
AppsState, AuthState, AuthzRepo, PostgresAdminSessionRepository, PostgresAdminUserRepository,
PostgresApiKeyRepository, PostgresAppDomainRepository, PostgresAppMembersRepository,
PostgresAppRepository, PostgresExecutionLogRepository, PostgresExecutionLogSink,
PostgresRouteRepository, PostgresScriptRepository, RepoResolver, RouteAdminState,
RouteRepository, SandboxCeiling,
attach_principal_if_present, auth_router, compile_routes, migrations, require_authenticated,
route_admin_router, AdminSessionRepository, AdminState, AdminUserRepository, AdminsState,
ApiKeyRepository, ApiKeysState, AppDomainRepository, AppMembersRepository, AppMembersState,
AppRepository, AppsState, AuthState, AuthzRepo, PostgresAdminSessionRepository,
PostgresAdminUserRepository, PostgresApiKeyRepository, PostgresAppDomainRepository,
PostgresAppMembersRepository, PostgresAppRepository, PostgresExecutionLogRepository,
PostgresExecutionLogSink, PostgresRouteRepository, PostgresScriptRepository, RepoResolver,
RouteAdminState, RouteRepository, SandboxCeiling,
};
use picloud_orchestrator_core::routing::{AppDomainTable, RouteTable};
use picloud_orchestrator_core::{
@@ -206,16 +206,31 @@ pub async fn build_app(pool: PgPool, auth: AuthDeps) -> anyhow::Result<Router> {
// facade above; the bare module path is retained so it's discoverable.
let _ = apps_api::AppsState::clone;
// Opportunistic principal extraction on every data-plane request.
// Always inserts `Extension<Option<Principal>>`: Some for authed
// ingress (bearer / cookie), None otherwise. Handlers depend on
// this layer being applied — scoped to the data-plane routers so
// the admin path (which uses `require_authenticated`) doesn't
// double-resolve the same token.
let data_plane_routed = data_plane_router(data_plane.clone()).layer(from_fn_with_state(
auth_state.clone(),
attach_principal_if_present,
));
let user_routes = user_routes_router(data_plane).layer(from_fn_with_state(
auth_state.clone(),
attach_principal_if_present,
));
let api_v1 = Router::new()
.nest("/admin", auth_router(auth_state))
.nest("/admin", guarded_admin)
.merge(data_plane_router(data_plane.clone()));
.merge(data_plane_routed);
Ok(Router::new()
.route("/healthz", get(healthz))
.route("/version", get(version))
.nest(&format!("/api/v{API_VERSION}"), api_v1)
.merge(user_routes_router(data_plane))
.merge(user_routes)
.layer(TraceLayer::new_for_http()))
}