fix(nav): gate Moderation and Support queue on the shard module
Found by the M20 trace, and fixed in this PR by the org lead's decision.
Both rows call /admin/shard/*, module-uo's admin mount, but they carried no
capability. So a moderator on a Rust-only site saw "Moderation" in the drawer
and opened a UO form ("Serial (0x…)", kick or ban by UO account) where every
action failed.
Both rows now declare Capability.SHARD, as the player rows do, so they are UO
screens and their strings belong in strings_uo.xml, where step 1 put them. A
new MenuCapabilityGatingTest case covers it: a moderator on a Rust-only
backend sees neither row but keeps the Dashboard, and on a UO backend sees
both.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
@@ -196,8 +196,21 @@ val APP_MENU: List<MenuEntry> = listOf(
|
||||
// Staff operations (§1, M10) — revealed for staff roles; the backend re-checks every call.
|
||||
MenuEntry(Routes.ADMIN_DASHBOARD, R.string.menu_admin_dashboard, MenuAccess.STAFF),
|
||||
MenuEntry(Routes.ADMIN_CONTENT, R.string.menu_admin_content, MenuAccess.STAFF),
|
||||
MenuEntry(Routes.ADMIN_MODERATION, R.string.menu_admin_moderation, MenuAccess.MODERATOR),
|
||||
MenuEntry(Routes.ADMIN_SUPPORT, R.string.menu_admin_support, MenuAccess.MODERATOR),
|
||||
// Moderation and the support queue act on the shard (`/admin/shard/*`, module-uo's
|
||||
// admin mount), so they need the module installed like the player rows do. On a
|
||||
// Rust-only site they once showed a UO form whose every action failed (M20).
|
||||
MenuEntry(
|
||||
Routes.ADMIN_MODERATION,
|
||||
R.string.menu_admin_moderation,
|
||||
MenuAccess.MODERATOR,
|
||||
capability = Capability.SHARD,
|
||||
),
|
||||
MenuEntry(
|
||||
Routes.ADMIN_SUPPORT,
|
||||
R.string.menu_admin_support,
|
||||
MenuAccess.MODERATOR,
|
||||
capability = Capability.SHARD,
|
||||
),
|
||||
)
|
||||
|
||||
/**
|
||||
|
||||
@@ -168,6 +168,26 @@ class MenuCapabilityGatingTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun moderationAndSupportNeedTheShardModule() {
|
||||
// Both call `/admin/shard/*`, module-uo's admin mount. Before M20 they
|
||||
// carried no capability, so a moderator on a Rust-only site saw a UO form
|
||||
// whose every action failed.
|
||||
val staffRows = APP_MENU.filter { it.route == Routes.ADMIN_MODERATION || it.route == Routes.ADMIN_SUPPORT }
|
||||
assertEquals(2, staffRows.size)
|
||||
assertTrue(staffRows.all { it.capability == Capability.SHARD })
|
||||
|
||||
val moderator = signedIn(Role.MODERATOR)
|
||||
val rustOnly = visibleEntries(APP_MENU, moderator, null, serving(Capability.RUST)).map { it.route }
|
||||
assertFalse(Routes.ADMIN_MODERATION in rustOnly)
|
||||
assertFalse(Routes.ADMIN_SUPPORT in rustOnly)
|
||||
// …while the core staff rows stay.
|
||||
assertTrue(Routes.ADMIN_DASHBOARD in rustOnly)
|
||||
|
||||
val uo = visibleEntries(APP_MENU, moderator, null, serving(Capability.SHARD)).map { it.route }
|
||||
assertTrue(Routes.ADMIN_MODERATION in uo)
|
||||
assertTrue(Routes.ADMIN_SUPPORT in uo)
|
||||
}
|
||||
|
||||
@Test fun bothEventRowsDeclareCoresCapabilityAndNoFeature() {
|
||||
// Events are core's. A `feature` on one of them would gate a core screen on
|
||||
// a module's visibility config, which is the coupling this separation exists
|
||||
|
||||
Reference in New Issue
Block a user