diff --git a/crates/manager-core/src/users_admin_api.rs b/crates/manager-core/src/users_admin_api.rs index a26e506..ea9fd16 100644 --- a/crates/manager-core/src/users_admin_api.rs +++ b/crates/manager-core/src/users_admin_api.rs @@ -29,7 +29,7 @@ use serde_json::json; use uuid::Uuid; use crate::app_repo::AppRepository; -use crate::authz::{AuthzDenied, AuthzRepo}; +use crate::authz::{self, AuthzDenied, AuthzRepo, Capability}; use crate::repo::ScriptRepositoryError; #[derive(Clone)] @@ -253,15 +253,16 @@ async fn delete_user( Path((id_or_slug, user_id)): Path<(String, Uuid)>, ) -> Result { let app = resolve_app(&*s.apps, &id_or_slug).await?; - // Per brief: DELETE is gated on AppUsersAdmin (more senior than - // the SDK's delete which is gated on AppUsersWrite — the dashboard - // delete button is an irreversible administrative action). The - // service's `delete` checks AppUsersWrite, so we apply the - // stronger Admin check here and bypass the service. Actually we - // can keep this clean by going via service.delete which gates on - // AppUsersWrite — admins always satisfy Write — and adding the - // explicit Admin gate as a precondition would be redundant since - // anyone with Admin satisfies Write. Documented in HANDBACK §7. + // F-S-005: gate on the stronger AppUsersAdmin per the original brief + // — the dashboard delete is irreversible. The service-layer + // `delete` only checks AppUsersWrite; without this explicit gate, + // any editor could delete app users via the admin HTTP API. + authz::require( + s.authz.as_ref(), + &principal, + Capability::AppUsersAdmin(app.id), + ) + .await?; let cx = synth_cx(app.id, &principal); let removed = s.users.delete(&cx, AppUserId::from(user_id)).await?; if removed {