Harden admin ACL on critical write paths.
Local Build and Deploy / deploy (push) Successful in 54s

Gate translations, RCON, and user mutations on SETTINGS_EDIT, RCON_EXECUTE, and USERS_EDIT instead of dashboard/rank checks; redirect the legacy user-edit URL to the guarded canonical page.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
SimoandCursor committed 2026-07-15 20:07:15 +02:00
1 parent bae543baf6
commit f2427b3483
8 files changed
+127 -284

No files matched your search

+15
View File
@@ -17,6 +17,8 @@ const ROUTES: Array<[string, string]> = [
["devops", "PERMS.DEVOPS_VIEW"],
["devops/errors", "PERMS.DEVOPS_VIEW"],
["online", "PERMS.USERS_VIEW"],
["commandocentrum", "PERMS.RCON_EXECUTE"],
["users/edit/[id]", "PERMS.USERS_EDIT"],
];
describe("admin operations route contract", () => {
@@ -29,6 +31,7 @@ describe("admin operations route contract", () => {
it.each([
["analytics/export", "PERMS.ANALYTICS_EXPORT"],
["devops/health", "PERMS.DEVOPS_VIEW"],
["users/actions", "PERMS.USERS_EDIT"],
])("provides and guards /api/admin/%s", (route, permission) => {
const path = `src/app/api/admin/${route}/route.ts`;
expect(existsSync(path), path).toBe(true);
@@ -40,4 +43,16 @@ describe("admin operations route contract", () => {
expect(source).toContain("PERMS.MODERATION_EDIT");
expect(source).toContain("adminAction");
});
it("guards translation writes with SETTINGS_EDIT", () => {
const source = readFileSync("src/actions/translations.ts", "utf8");
expect(source).toContain("PERMS.SETTINGS_EDIT");
expect(source).not.toContain("rank < 7");
});
it("guards commandocentrum mutations with RCON_EXECUTE", () => {
const source = readFileSync("src/actions/commandocentrum.ts", "utf8");
expect(source).toContain("PERMS.RCON_EXECUTE");
expect(source).not.toContain("requireStaff()");
});
});
+17
View File
@@ -22,6 +22,23 @@ export async function requireStaff(): Promise<StaffUser> {
};
}
/**
* Staff gate plus a specific ACL slug. Use for FormData server actions that
* must not stop at admin.dashboard alone (RCON, user edits, settings writes).
*/
export async function requirePermission(permission: string): Promise<StaffUser> {
const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.ADMIN_DASHBOARD, session.user.rank))
redirectSafe("/", "/");
if (!canAccess(permissions, permission, session.user.rank))
redirectSafe("/admin", "/admin");
return {
id: session.user.id,
rank: session.user.rank,
username: session.user.username,
};
}
export async function requireStaffRateLimited(): Promise<StaffUser> {
const staff = await requireStaff();
const ip = await clientIp();