fix(manager-core): F-S-005 gate dashboard delete_user on AppUsersAdmin

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) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-07 20:40:12 +02:00
parent 9247680ab6
commit 7d045b2a0b

View File

@@ -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<StatusCode, AppUsersApiError> {
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 {