//! `PrincipalResolver` — turns a `registered_by_principal` user id from //! a trigger row into the `Principal` the dispatcher passes through to //! the executor. Per design notes §4, a trigger execution runs as the //! user that registered the trigger; the original event's caller is //! recorded elsewhere (on the outbox row, for forensics) and does not //! become the execution principal. use async_trait::async_trait; use picloud_shared::{AdminUserId, Principal}; use crate::admin_user_repo::{AdminUserRepository, AdminUserRepositoryError}; #[derive(Debug, thiserror::Error)] pub enum PrincipalResolverError { #[error("user not found: {0}")] NotFound(AdminUserId), #[error("user is inactive: {0}")] Inactive(AdminUserId), #[error("admin user repo error: {0}")] Backend(String), } #[async_trait] pub trait PrincipalResolver: Send + Sync { async fn resolve(&self, user_id: AdminUserId) -> Result; } pub struct AdminPrincipalResolver { users: std::sync::Arc, } impl AdminPrincipalResolver { #[must_use] pub fn new(users: std::sync::Arc) -> Self { Self { users } } } #[async_trait] impl PrincipalResolver for AdminPrincipalResolver { async fn resolve(&self, user_id: AdminUserId) -> Result { let row = self .users .get(user_id) .await .map_err(|e: AdminUserRepositoryError| PrincipalResolverError::Backend(e.to_string()))? .ok_or(PrincipalResolverError::NotFound(user_id))?; if !row.is_active { return Err(PrincipalResolverError::Inactive(user_id)); } Ok(Principal { user_id, instance_role: row.instance_role, // Trigger executions are cookie-session-style (no API key // scope restriction). Per-app permissions are evaluated // via `authz::can` against the `app_id` of the resource // the script touches, exactly like an admin invocation. scopes: None, app_binding: None, }) } }