feat(manager-core,picloud): bearer pic_ keys land in Principal
* auth_middleware: split into resolve_principal → verify_session OR verify_api_key (selected by the pic_ prefix). Both paths converge on Principal as the request extension; require_admin keeps working as a #[deprecated] alias for require_authenticated. AuthState gains an api_keys repo; the cookie path is unchanged. * api-key path takes the first 8 chars after pic_ as the indexed lookup key, Argon2-verifies each candidate, soft-rejects deactivated users, and updates last_used_at inline. * auth_api: /auth/me now consumes Extension<Principal> and re-fetches the user row so username updates surface immediately. * picloud: AuthDeps + AuthState wired with PostgresApiKeyRepository; the layer call switches to require_authenticated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,12 +11,12 @@ use axum::{routing::get, Json, Router};
|
||||
use picloud_executor_core::{Engine, Limits};
|
||||
use picloud_manager_core::{
|
||||
admin_router, admins_router, apps_api, apps_router, auth_router, compile_routes, migrations,
|
||||
require_admin, route_admin_router, AdminSessionRepository, AdminState, AdminUserRepository,
|
||||
AdminsState, AppDomainRepository, AppRepository, AppsState, AuthState,
|
||||
PostgresAdminSessionRepository, PostgresAdminUserRepository, PostgresAppDomainRepository,
|
||||
PostgresAppRepository, PostgresExecutionLogRepository, PostgresExecutionLogSink,
|
||||
PostgresRouteRepository, PostgresScriptRepository, RepoResolver, RouteAdminState,
|
||||
RouteRepository, SandboxCeiling,
|
||||
require_authenticated, route_admin_router, AdminSessionRepository, AdminState,
|
||||
AdminUserRepository, AdminsState, ApiKeyRepository, AppDomainRepository, AppRepository,
|
||||
AppsState, AuthState, PostgresAdminSessionRepository, PostgresAdminUserRepository,
|
||||
PostgresApiKeyRepository, PostgresAppDomainRepository, PostgresAppRepository,
|
||||
PostgresExecutionLogRepository, PostgresExecutionLogSink, PostgresRouteRepository,
|
||||
PostgresScriptRepository, RepoResolver, RouteAdminState, RouteRepository, SandboxCeiling,
|
||||
};
|
||||
use picloud_orchestrator_core::routing::{AppDomainTable, RouteTable};
|
||||
use picloud_orchestrator_core::{
|
||||
@@ -37,6 +37,7 @@ const DEFAULT_SESSION_TTL_HOURS: u64 = 24;
|
||||
pub struct AuthDeps {
|
||||
pub users: Arc<dyn AdminUserRepository>,
|
||||
pub sessions: Arc<dyn AdminSessionRepository>,
|
||||
pub keys: Arc<dyn ApiKeyRepository>,
|
||||
pub ttl: Duration,
|
||||
}
|
||||
|
||||
@@ -46,7 +47,8 @@ impl AuthDeps {
|
||||
pub fn from_pool(pool: PgPool) -> Self {
|
||||
Self {
|
||||
users: Arc::new(PostgresAdminUserRepository::new(pool.clone())),
|
||||
sessions: Arc::new(PostgresAdminSessionRepository::new(pool)),
|
||||
sessions: Arc::new(PostgresAdminSessionRepository::new(pool.clone())),
|
||||
keys: Arc::new(PostgresApiKeyRepository::new(pool)),
|
||||
ttl: read_session_ttl(),
|
||||
}
|
||||
}
|
||||
@@ -146,6 +148,7 @@ pub async fn build_app(pool: PgPool, auth: AuthDeps) -> anyhow::Result<Router> {
|
||||
let auth_state = AuthState {
|
||||
users: auth.users.clone(),
|
||||
sessions: auth.sessions.clone(),
|
||||
keys: auth.keys,
|
||||
ttl: auth.ttl,
|
||||
};
|
||||
let admins_state = AdminsState {
|
||||
@@ -156,13 +159,18 @@ pub async fn build_app(pool: PgPool, auth: AuthDeps) -> anyhow::Result<Router> {
|
||||
// /admin/auth/login + /logout are unguarded by design (login is how
|
||||
// you get in). /admin/auth/me applies the middleware internally so
|
||||
// the same Router::with_state machinery composes cleanly. Everything
|
||||
// else under /admin gets the require_admin layer.
|
||||
// else under /admin gets the require_authenticated layer; capability
|
||||
// checks live in each handler (after the resource is loaded so the
|
||||
// capability binds to the resource's actual app_id).
|
||||
let guarded_admin = Router::new()
|
||||
.merge(admin_router(admin))
|
||||
.merge(route_admin_router(route_admin))
|
||||
.merge(admins_router(admins_state))
|
||||
.merge(apps_router(apps_state))
|
||||
.layer(from_fn_with_state(auth_state.clone(), require_admin));
|
||||
.layer(from_fn_with_state(
|
||||
auth_state.clone(),
|
||||
require_authenticated,
|
||||
));
|
||||
|
||||
// Silence "unused import" lint on `apps_api` — we re-export via the
|
||||
// facade above; the bare module path is retained so it's discoverable.
|
||||
|
||||
Reference in New Issue
Block a user