feat(api): expose caller's effective app role via my_role
GET /api/v1/admin/apps/{id_or_slug} now returns an `AppRole`-typed
`my_role` alongside the existing app fields, computed server-side from
the Principal: `Owner → app_admin` and `Admin → editor` (both
implicit per blueprint §11.6), `Member → app_members.role` (looked up
via the existing `AuthzRepo::membership` already in `AppsState`).
The dashboard uses this single field to decide whether to render
admin-only surfaces (Members tab, etc.) instead of duplicating the
implicit-grant rules on the client side — keeps API and UI gate logic
identical with one round-trip.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -645,3 +645,58 @@ async fn list_active_owners_drives_the_multi_owner_warning(pool: PgPool) {
|
||||
.expect("count");
|
||||
assert_eq!(remaining, 1, "one other owner should remain (owner2)");
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// 12. `my_role` on GET /apps/{id_or_slug} reflects the caller's effective role
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
|
||||
#[sqlx::test(migrations = "../manager-core/migrations")]
|
||||
async fn my_role_field_matches_caller_role(pool: PgPool) {
|
||||
let s = boot(pool).await;
|
||||
|
||||
// Owner → implicit app_admin everywhere.
|
||||
let owner_token = login_token(&s.server, "owner", "owner-pw").await;
|
||||
let r = s
|
||||
.server
|
||||
.get("/api/v1/admin/apps/default")
|
||||
.add_header("authorization", format!("Bearer {owner_token}"))
|
||||
.await;
|
||||
r.assert_status_ok();
|
||||
assert_eq!(
|
||||
r.json::<Value>()["my_role"].as_str(),
|
||||
Some("app_admin"),
|
||||
"owner reports app_admin"
|
||||
);
|
||||
|
||||
// Admin → implicit editor everywhere.
|
||||
seed_user(&s.pool, "alice", "alice-pw", InstanceRole::Admin).await;
|
||||
let admin_token = login_token(&s.server, "alice", "alice-pw").await;
|
||||
let r = s
|
||||
.server
|
||||
.get("/api/v1/admin/apps/default")
|
||||
.add_header("authorization", format!("Bearer {admin_token}"))
|
||||
.await;
|
||||
r.assert_status_ok();
|
||||
assert_eq!(
|
||||
r.json::<Value>()["my_role"].as_str(),
|
||||
Some("editor"),
|
||||
"admin reports editor"
|
||||
);
|
||||
|
||||
// Member with explicit `viewer` membership → viewer.
|
||||
let bob = seed_user(&s.pool, "bob", "bob-pw", InstanceRole::Member).await;
|
||||
grant_membership(&s.pool, bob, s.default_app, AppRole::Viewer).await;
|
||||
let bob_token = login_token(&s.server, "bob", "bob-pw").await;
|
||||
let r = s
|
||||
.server
|
||||
.get("/api/v1/admin/apps/default")
|
||||
.add_header("authorization", format!("Bearer {bob_token}"))
|
||||
.await;
|
||||
r.assert_status_ok();
|
||||
assert_eq!(
|
||||
r.json::<Value>()["my_role"].as_str(),
|
||||
Some("viewer"),
|
||||
"member with viewer row reports viewer"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user