an uninstalled plugin's grants stop breaking the permissions screen

Uninstalling music left its grants in role_permissions. The keys stop existing
when the plugin unmounts, so the settings page — which loads every grant and
submits them all back — failed with "Unknown permission 'music'" for a row
nobody could see, edit or remove through the UI. Nothing could be saved again
without editing the database.

Two changes, because either alone leaves half the problem.

Uninstall now revokes the plugin's grants, across every role rather than one. A
deliberate exception to "uninstall deletes nothing": a grant is the PLATFORM's
record of who may reach the plugin, not anything the plugin stored. Reinstalling
means granting again, which is the honest behaviour — the alternative is access
silently returning.

And the write DROPS an unknown key instead of refusing the request, so every
server already in this state is unblocked without touching its database. That is
what the resolver already does on read, so the write now agrees with it. The
cost is that a typo in a hand-made API call is ignored rather than reported —
worth less than a settings page that cannot be used. The response returns what
was dropped, so it is not silent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 12:18:49 +00:00
co-authored by Claude Opus 5
parent b9524ee4d5
commit 0fd89082fe
5 changed files with 102 additions and 4 deletions
@@ -1,4 +1,11 @@
export { getAllRoleGrants, getRoleGrants, setRoleGrant, revokeRoleGrant, replaceRoleGrants } from './queries';
export {
getAllRoleGrants,
getRoleGrants,
setRoleGrant,
revokeRoleGrant,
replaceRoleGrants,
revokeGrantsForPermissions,
} from './queries';
export type { RoleGrant } from './queries';
@@ -1,4 +1,4 @@
import { eq, and } from 'drizzle-orm';
import { eq, and, inArray } from 'drizzle-orm';
import { db } from '../db';
import { rolePermissions } from './schema';
import type { UserRole } from '../auth/schema';
@@ -75,3 +75,23 @@ export async function replaceRoleGrants(
}
});
}
/**
* Drop every role's grants for these permission keys.
*
* Called when a plugin is uninstalled: its keys stop existing, and a grant naming one is a row nobody
* can see, edit or remove through the UI. Left in place they made the whole permissions screen
* unsaveable — the page loads every grant, submits them back, and the write refused a key it no longer
* recognised.
*
* Across ALL roles, not one: an uninstall is not per-role, and leaving another role's copy behind would
* reproduce the same dead screen for whoever opened that tab.
*/
export async function revokeGrantsForPermissions(permissions: string[]): Promise<number> {
if (!permissions.length) return 0;
const removed = await db
.delete(rolePermissions)
.where(inArray(rolePermissions.permission, permissions))
.returning({ id: rolePermissions.id });
return removed.length;
}