refactor(authz): route users_service through the shared authz-verdict mapper
`users_service::require` open-coded the `AuthzDenied → {Forbidden, Backend}`
match that `authz::script_gate` centralizes for every other stateful service —
the one straggler. Extract the mapping into `authz::require_mapped` (used now by
both `script_gate` variants and `users_service::require`), so the verdict→error
mapping lives in exactly one place. Also refresh the stale `to_app_user` comment
(it referenced an unshipped "commit 8" stub; roles are resolved via
`fetch_roles` and passed in). No behavior change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -458,6 +458,30 @@ pub enum AuthzDenied {
|
|||||||
Repo(#[from] AuthzError),
|
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<E>(
|
||||||
|
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
|
/// Script-as-gate authz: anonymous public-HTTP scripts skip the check
|
||||||
/// (`cx.principal` is `None`); authenticated callers must hold `cap`.
|
/// (`cx.principal` is `None`); authenticated callers must hold `cap`.
|
||||||
///
|
///
|
||||||
@@ -482,11 +506,7 @@ pub async fn script_gate<E>(
|
|||||||
let Some(principal) = cx.principal.as_ref() else {
|
let Some(principal) = cx.principal.as_ref() else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
match require(repo, principal, cap).await {
|
require_mapped(repo, principal, cap, forbidden, backend).await
|
||||||
Ok(()) => Ok(()),
|
|
||||||
Err(AuthzDenied::Denied) => Err(forbidden()),
|
|
||||||
Err(AuthzDenied::Repo(e)) => Err(backend(e.to_string())),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Like [`script_gate`], but **fails closed on an anonymous principal**: a
|
/// Like [`script_gate`], but **fails closed on an anonymous principal**: a
|
||||||
@@ -510,11 +530,7 @@ pub async fn script_gate_require_principal<E>(
|
|||||||
let Some(principal) = cx.principal.as_ref() else {
|
let Some(principal) = cx.principal.as_ref() else {
|
||||||
return Err(forbidden());
|
return Err(forbidden());
|
||||||
};
|
};
|
||||||
match require(repo, principal, cap).await {
|
require_mapped(repo, principal, cap, forbidden, backend).await
|
||||||
Ok(()) => Ok(()),
|
|
||||||
Err(AuthzDenied::Denied) => Err(forbidden()),
|
|
||||||
Err(AuthzDenied::Repo(e)) => Err(backend(e.to_string())),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ use crate::app_user_verification_repo::{AppUserVerificationRepo, AppUserVerifica
|
|||||||
use crate::auth::{
|
use crate::auth::{
|
||||||
generate_session_token, hash_password, hash_token, verify_password, TIMING_FLAT_DUMMY_HASH,
|
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
|
/// Runtime configuration. Defaults match the v1.1.8 brief — overridable
|
||||||
/// via env vars wired in the picloud binary.
|
/// via env vars wired in the picloud binary.
|
||||||
@@ -268,17 +268,18 @@ impl UsersServiceImpl {
|
|||||||
// Public HTTP execution — script-as-gate; not denied here.
|
// Public HTTP execution — script-as-gate; not denied here.
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
match authz::require(&*self.authz, p, cap).await {
|
authz::require_mapped(
|
||||||
Ok(()) => Ok(()),
|
&*self.authz,
|
||||||
Err(AuthzDenied::Denied) => Err(UsersError::Forbidden),
|
p,
|
||||||
Err(AuthzDenied::Repo(e)) => Err(UsersError::Backend(e.to_string())),
|
cap,
|
||||||
}
|
|| UsersError::Forbidden,
|
||||||
|
UsersError::Backend,
|
||||||
|
)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compose the public `AppUser` shape from a `AppUserRow`. Roles
|
/// Compose the public `AppUser` shape from an `AppUserRow`, with the caller's
|
||||||
/// are always empty in commit 4 of v1.1.8; commit 8 (per-app
|
/// already-resolved per-app `roles` (see [`Self::fetch_roles`]).
|
||||||
/// roles) replaces this stub with an actual `app_user_roles`
|
|
||||||
/// query.
|
|
||||||
fn to_app_user(row: AppUserRow, roles: Vec<String>) -> AppUser {
|
fn to_app_user(row: AppUserRow, roles: Vec<String>) -> AppUser {
|
||||||
AppUser {
|
AppUser {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
|
|||||||
Reference in New Issue
Block a user