feat(manager-core): admin is implicit app_admin; delete-script needs AppAdmin

Aligns the canonical capability rules with how the dashboard now shadows
its UI. Instance admins become implicit app_admin on every app (only
InstanceManageSettings stays owner-only), and the script-delete handler
moves from AppWriteScript to AppAdmin so editors can save but not delete.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-05-28 19:27:32 +02:00
parent ec3c768262
commit 4644ea4919
5 changed files with 94 additions and 67 deletions

View File

@@ -293,7 +293,7 @@ async fn owner_access_matrix(pool: PgPool) {
#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
#[sqlx::test(migrations = "../manager-core/migrations")]
async fn admin_can_manage_users_but_not_app_admin_settings(pool: PgPool) {
async fn admin_is_implicit_app_admin_on_every_app(pool: PgPool) {
let s = boot(pool.clone()).await;
seed_user(&s.pool, "alice", "alice-pw", InstanceRole::Admin).await;
let token = login_token(&s.server, "alice", "alice-pw").await;
@@ -305,24 +305,32 @@ async fn admin_can_manage_users_but_not_app_admin_settings(pool: PgPool) {
.await
.assert_status_ok();
// Allowed: read default app (admin is implicit editor everywhere).
// Allowed: read default app admin is implicit app_admin
// everywhere (per blueprint §11.6).
s.server
.get("/api/v1/admin/apps/default")
.add_header("authorization", format!("Bearer {token}"))
.await
.assert_status_ok();
// Allowed: write scripts (implicit editor).
// Allowed: write scripts.
let script = create_script_via_api(&s.server, &token, s.default_app, "admin-write").await;
assert!(script["id"].is_string());
// Denied: delete the default app (AppAdmin only).
let denied = s
.server
// Allowed: list app members (AppAdmin gate). Pre-3.5.x this
// 403'd; now it's the same allow as the owner sees.
s.server
.get("/api/v1/admin/apps/default/members")
.add_header("authorization", format!("Bearer {token}"))
.await
.assert_status_ok();
// Allowed: delete the default app (AppAdmin).
s.server
.delete("/api/v1/admin/apps/default")
.add_header("authorization", format!("Bearer {token}"))
.await;
assert_eq!(denied.status_code(), axum::http::StatusCode::FORBIDDEN);
.await
.assert_status(axum::http::StatusCode::NO_CONTENT);
}
#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
@@ -735,7 +743,7 @@ async fn my_role_field_matches_caller_role(pool: PgPool) {
"owner reports app_admin"
);
// Admin → implicit editor everywhere.
// Admin → implicit app_admin everywhere (post-§11.6 update).
seed_user(&s.pool, "alice", "alice-pw", InstanceRole::Admin).await;
let admin_token = login_token(&s.server, "alice", "alice-pw").await;
let r = s
@@ -746,8 +754,8 @@ async fn my_role_field_matches_caller_role(pool: PgPool) {
r.assert_status_ok();
assert_eq!(
r.json::<Value>()["my_role"].as_str(),
Some("editor"),
"admin reports editor"
Some("app_admin"),
"admin reports app_admin"
);
// Member with explicit `viewer` membership → viewer.