diff --git a/crates/manager-core/src/authz.rs b/crates/manager-core/src/authz.rs index 6eb14b0..45b7b1c 100644 --- a/crates/manager-core/src/authz.rs +++ b/crates/manager-core/src/authz.rs @@ -458,6 +458,30 @@ pub enum AuthzDenied { Repo(#[from] AuthzError), } +/// Run [`require`] for a known principal and map the `AuthzDenied` verdict onto +/// the caller's service-specific error via `forbidden`/`backend`. The single +/// home for the `Denied → forbidden() / Repo(e) → backend(e)` mapping that +/// [`script_gate`], [`script_gate_require_principal`], and the stateful services +/// share. +/// +/// # Errors +/// +/// `forbidden()` on `AuthzDenied::Denied`; `backend(repo_err.to_string())` on +/// `AuthzDenied::Repo`. +pub async fn require_mapped( + repo: &dyn AuthzRepo, + principal: &Principal, + cap: Capability, + forbidden: impl FnOnce() -> E, + backend: impl FnOnce(String) -> E, +) -> Result<(), E> { + match require(repo, principal, cap).await { + Ok(()) => Ok(()), + Err(AuthzDenied::Denied) => Err(forbidden()), + Err(AuthzDenied::Repo(e)) => Err(backend(e.to_string())), + } +} + /// Script-as-gate authz: anonymous public-HTTP scripts skip the check /// (`cx.principal` is `None`); authenticated callers must hold `cap`. /// @@ -482,11 +506,7 @@ pub async fn script_gate( let Some(principal) = cx.principal.as_ref() else { return Ok(()); }; - match require(repo, principal, cap).await { - Ok(()) => Ok(()), - Err(AuthzDenied::Denied) => Err(forbidden()), - Err(AuthzDenied::Repo(e)) => Err(backend(e.to_string())), - } + require_mapped(repo, principal, cap, forbidden, backend).await } /// Like [`script_gate`], but **fails closed on an anonymous principal**: a @@ -510,11 +530,7 @@ pub async fn script_gate_require_principal( let Some(principal) = cx.principal.as_ref() else { return Err(forbidden()); }; - match require(repo, principal, cap).await { - Ok(()) => Ok(()), - Err(AuthzDenied::Denied) => Err(forbidden()), - Err(AuthzDenied::Repo(e)) => Err(backend(e.to_string())), - } + require_mapped(repo, principal, cap, forbidden, backend).await } // ---------------------------------------------------------------------------- diff --git a/crates/manager-core/src/users_service.rs b/crates/manager-core/src/users_service.rs index 6916f25..51846a7 100644 --- a/crates/manager-core/src/users_service.rs +++ b/crates/manager-core/src/users_service.rs @@ -46,7 +46,7 @@ use crate::app_user_verification_repo::{AppUserVerificationRepo, AppUserVerifica use crate::auth::{ generate_session_token, hash_password, hash_token, verify_password, TIMING_FLAT_DUMMY_HASH, }; -use crate::authz::{self, AuthzDenied, AuthzRepo, Capability}; +use crate::authz::{self, AuthzRepo, Capability}; /// Runtime configuration. Defaults match the v1.1.8 brief — overridable /// via env vars wired in the picloud binary. @@ -268,17 +268,18 @@ impl UsersServiceImpl { // Public HTTP execution — script-as-gate; not denied here. return Ok(()); }; - match authz::require(&*self.authz, p, cap).await { - Ok(()) => Ok(()), - Err(AuthzDenied::Denied) => Err(UsersError::Forbidden), - Err(AuthzDenied::Repo(e)) => Err(UsersError::Backend(e.to_string())), - } + authz::require_mapped( + &*self.authz, + p, + cap, + || UsersError::Forbidden, + UsersError::Backend, + ) + .await } - /// Compose the public `AppUser` shape from a `AppUserRow`. Roles - /// are always empty in commit 4 of v1.1.8; commit 8 (per-app - /// roles) replaces this stub with an actual `app_user_roles` - /// query. + /// Compose the public `AppUser` shape from an `AppUserRow`, with the caller's + /// already-resolved per-app `roles` (see [`Self::fetch_roles`]). fn to_app_user(row: AppUserRow, roles: Vec) -> AppUser { AppUser { id: row.id,