From 7d045b2a0b6a8ec6d0bb86f150c3d7818056e122 Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Sun, 7 Jun 2026 20:40:12 +0200 Subject: [PATCH] fix(manager-core): F-S-005 gate dashboard delete_user on AppUsersAdmin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit users_admin_api::delete_user was a comment-only acknowledgement that "this should require AppUsersAdmin" while actually calling service.delete which only gates on AppUsersWrite. v1.1.8 HANDBACK §7 listed this as known. Effect: any editor could delete app users via the admin HTTP API and dashboard button. Add an explicit `authz::require(... AppUsersAdmin(app_id))` before the service call. Delete the stale comment. AUDIT.md anchor: F-S-005. Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/manager-core/src/users_admin_api.rs | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) 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 {