From 16169d74659aaf0833b6ba0fb191fd14735b431e Mon Sep 17 00:00:00 2001 From: KingKDot Date: Tue, 19 May 2026 09:57:47 -0500 Subject: [PATCH] rbac standardization --- Overlord-Server/src/rbac.test.ts | 31 ++ Overlord-Server/src/rbac.ts | 4 + .../src/registration-default-groups.test.ts | 136 ++++++++ .../auth-routes.permissions-array.test.ts | 123 +++++++ .../routes/permission-groups-routes.test.ts | 320 ++++++++++++++++++ .../server/routes/permission-groups-routes.ts | 27 +- 6 files changed, 617 insertions(+), 24 deletions(-) create mode 100644 Overlord-Server/src/registration-default-groups.test.ts create mode 100644 Overlord-Server/src/server/routes/auth-routes.permissions-array.test.ts create mode 100644 Overlord-Server/src/server/routes/permission-groups-routes.test.ts diff --git a/Overlord-Server/src/rbac.test.ts b/Overlord-Server/src/rbac.test.ts index 04dcc26..6088836 100644 --- a/Overlord-Server/src/rbac.test.ts +++ b/Overlord-Server/src/rbac.test.ts @@ -9,6 +9,7 @@ import { getPermissionDescription, getRoleDescription, getRolePermissions, + listAllPermissions, type Permission, } from "./rbac"; import type { AuthenticatedUser } from "./auth"; @@ -298,6 +299,36 @@ describe("getRolePermissions", () => { }); }); +describe("listAllPermissions", () => { + test("returns every key declared in the PERMISSIONS registry", () => { + const perms = listAllPermissions(); + // Spot-check a sampling of permissions we know exist — if any are missing, + // the route-layer sanitizer would silently strip them. + expect(perms).toContain("users:manage" as Permission); + expect(perms).toContain("clients:control" as Permission); + expect(perms).toContain("clients:disconnect" as Permission); + expect(perms).toContain("clients:reconnect" as Permission); + expect(perms).toContain("clients:metadata" as Permission); + expect(perms).toContain("clients:uninstall" as Permission); + expect(perms).toContain("system:configure" as Permission); + expect(perms).toContain("network:manage-bans" as Permission); + }); + + test("returns the same length as the description coverage (every entry has a description)", () => { + const perms = listAllPermissions(); + for (const p of perms) { + const desc = getPermissionDescription(p); + expect(desc).not.toBe("Unknown permission"); + expect(desc.length).toBeGreaterThan(0); + } + }); + + test("has no duplicates", () => { + const perms = listAllPermissions(); + expect(new Set(perms).size).toBe(perms.length); + }); +}); + describe("getRoleDescription", () => { test("admin description", () => { expect(getRoleDescription("admin")).toBe( diff --git a/Overlord-Server/src/rbac.ts b/Overlord-Server/src/rbac.ts index e80fbce..9c9afe5 100644 --- a/Overlord-Server/src/rbac.ts +++ b/Overlord-Server/src/rbac.ts @@ -95,6 +95,10 @@ const PERMISSIONS = { export type Permission = keyof typeof PERMISSIONS; +export function listAllPermissions(): Permission[] { + return Object.keys(PERMISSIONS) as Permission[]; +} + function lookupPermission(permission: string): PermissionDef | undefined { return (PERMISSIONS as Record)[permission]; } diff --git a/Overlord-Server/src/registration-default-groups.test.ts b/Overlord-Server/src/registration-default-groups.test.ts new file mode 100644 index 0000000..79f5a2d --- /dev/null +++ b/Overlord-Server/src/registration-default-groups.test.ts @@ -0,0 +1,136 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import { getConfig, updateRegistrationConfig } from "./config"; +import { + approvePendingRegistration, + createPendingRegistration, + createPermissionGroup, + deletePermissionGroup, + deleteUser, + getUserByUsername, + getUserGrantedPermissions, + getUserGroupIds, + registerUser, +} from "./users"; + +const PASSWORD = "Aa1!RegDefaultGroupsTestPass"; + +const createdUserIds: number[] = []; +const createdGroupIds: number[] = []; +let savedDefaultGroupIds: number[] | null = null; + +function tempGroup(name: string, permissions: string[]) { + const result = createPermissionGroup( + `${name}_${Date.now().toString(36)}_${Math.floor(Math.random() * 1e6).toString(36)}`, + null, + permissions, + null, + ); + expect(result.success).toBe(true); + createdGroupIds.push(result.group!.id); + return result.group!; +} + +async function withDefaultGroupIds(ids: number[], fn: () => Promise): Promise { + if (savedDefaultGroupIds === null) { + savedDefaultGroupIds = [...(getConfig().registration.defaultGroupIds || [])]; + } + await updateRegistrationConfig({ defaultGroupIds: ids }); + try { + await fn(); + } finally { + // restored in afterEach + } +} + +afterEach(async () => { + if (savedDefaultGroupIds !== null) { + await updateRegistrationConfig({ defaultGroupIds: savedDefaultGroupIds }); + savedDefaultGroupIds = null; + } + while (createdUserIds.length > 0) { + const id = createdUserIds.pop(); + if (typeof id === "number") deleteUser(id); + } + while (createdGroupIds.length > 0) { + const id = createdGroupIds.pop(); + if (typeof id === "number") deletePermissionGroup(id); + } +}); + +describe("registration default groups", () => { + test("registerUser assigns configured default groups", async () => { + const g1 = tempGroup("default_a", ["clients:metadata"]); + const g2 = tempGroup("default_b", ["clients:disconnect"]); + + await withDefaultGroupIds([g1.id, g2.id], async () => { + const username = `regdg_${Date.now().toString(36)}${Math.floor(Math.random() * 1e6).toString(36)}`; + const result = await registerUser(username, PASSWORD, "open", "operator"); + expect(result.success).toBe(true); + createdUserIds.push(result.userId!); + + const assigned = new Set(getUserGroupIds(result.userId!)); + expect(assigned.has(g1.id)).toBe(true); + expect(assigned.has(g2.id)).toBe(true); + + // The granted-permission union should include the group's perms. + const granted = getUserGrantedPermissions(result.userId!); + expect(granted.has("clients:metadata")).toBe(true); + expect(granted.has("clients:disconnect")).toBe(true); + }); + }); + + test("approvePendingRegistration assigns configured default groups", async () => { + const admin = await registerUser( + `regdg_admin_${Date.now().toString(36)}${Math.floor(Math.random() * 1e6).toString(36)}`, + PASSWORD, + "open", + "operator", + ); + expect(admin.success).toBe(true); + createdUserIds.push(admin.userId!); + + const g = tempGroup("approval_default", ["clients:metadata"]); + + await withDefaultGroupIds([g.id], async () => { + const username = `regdg_pending_${Date.now().toString(36)}${Math.floor(Math.random() * 1e6).toString(36)}`; + const passwordHash = await Bun.password.hash(PASSWORD, { algorithm: "bcrypt", cost: 10 }); + const pending = createPendingRegistration(username, passwordHash); + expect(pending.success).toBe(true); + + const approval = await approvePendingRegistration(pending.id!, admin.userId!, "operator"); + expect(approval.success).toBe(true); + createdUserIds.push(approval.userId!); + + const assigned = new Set(getUserGroupIds(approval.userId!)); + expect(assigned.has(g.id)).toBe(true); + }); + }); + + test("stale group ids in config are silently filtered (signup still succeeds)", async () => { + const validGroup = tempGroup("valid_only", ["clients:metadata"]); + + // Use a definitely-nonexistent id alongside the valid one. Signup must NOT + // fail just because one stale id is in the config. + const STALE_ID = 9999999; + await withDefaultGroupIds([STALE_ID, validGroup.id], async () => { + const username = `regdg_stale_${Date.now().toString(36)}${Math.floor(Math.random() * 1e6).toString(36)}`; + const result = await registerUser(username, PASSWORD, "open", "operator"); + expect(result.success).toBe(true); + createdUserIds.push(result.userId!); + + const assigned = getUserGroupIds(result.userId!); + expect(assigned).toContain(validGroup.id); + expect(assigned).not.toContain(STALE_ID); + }); + }); + + test("empty defaultGroupIds leaves the new user with no groups", async () => { + await withDefaultGroupIds([], async () => { + const username = `regdg_empty_${Date.now().toString(36)}${Math.floor(Math.random() * 1e6).toString(36)}`; + const result = await registerUser(username, PASSWORD, "open", "operator"); + expect(result.success).toBe(true); + createdUserIds.push(result.userId!); + expect(getUserGroupIds(result.userId!)).toEqual([]); + }); + }); +}); diff --git a/Overlord-Server/src/server/routes/auth-routes.permissions-array.test.ts b/Overlord-Server/src/server/routes/auth-routes.permissions-array.test.ts new file mode 100644 index 0000000..a979ea8 --- /dev/null +++ b/Overlord-Server/src/server/routes/auth-routes.permissions-array.test.ts @@ -0,0 +1,123 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import { generateToken } from "../../auth"; +import { + createPermissionGroup, + createUser, + deletePermissionGroup, + deleteUser, + getUserById, + setUserExtraPermissions, + setUserGroups, +} from "../../users"; +import { handleAuthRoutes } from "./auth-routes"; + +const PASSWORD = "Aa1!AuthMePermissionsTest"; + +const mockServer = { + requestIP: () => ({ address: "127.0.0.1" }), +}; + +const createdUserIds: number[] = []; +const createdGroupIds: number[] = []; + +async function tempUser(role: "admin" | "operator" | "viewer") { + const username = `mep_${role.slice(0, 3)}_${Date.now().toString(36)}${Math.floor(Math.random() * 1e6).toString(36)}`; + const created = await createUser(username, PASSWORD, role, "test"); + expect(created.success).toBe(true); + const user = getUserById(created.userId!); + expect(user).not.toBeNull(); + createdUserIds.push(user!.id); + const token = await generateToken(user!); + return { user: user!, token }; +} + +function tempGroup(name: string, permissions: string[]) { + const result = createPermissionGroup( + `${name}_${Date.now().toString(36)}_${Math.floor(Math.random() * 1e6).toString(36)}`, + null, + permissions, + null, + ); + expect(result.success).toBe(true); + createdGroupIds.push(result.group!.id); + return result.group!; +} + +async function fetchMe(token: string): Promise { + const url = new URL("https://localhost/api/auth/me"); + const res = await handleAuthRoutes( + new Request(url, { method: "GET", headers: { Authorization: `Bearer ${token}` } }), + url, + mockServer, + ); + expect(res).not.toBeNull(); + expect(res!.status).toBe(200); + return res!.json(); +} + +afterEach(() => { + while (createdGroupIds.length > 0) { + const id = createdGroupIds.pop(); + if (typeof id === "number") deletePermissionGroup(id); + } + while (createdUserIds.length > 0) { + const id = createdUserIds.pop(); + if (typeof id === "number") deleteUser(id); + } +}); + +describe("/api/auth/me permissions array", () => { + test("admin gets every declared permission", async () => { + const admin = await tempUser("admin"); + const body = await fetchMe(admin.token); + expect(Array.isArray(body.permissions)).toBe(true); + const set = new Set(body.permissions); + expect(set.has("users:manage")).toBe(true); + expect(set.has("clients:control")).toBe(true); + expect(set.has("system:configure")).toBe(true); + expect(set.has("clients:disconnect")).toBe(true); + }); + + test("operator gets the role-eligible subset and lacks admin-only perms", async () => { + const op = await tempUser("operator"); + const body = await fetchMe(op.token); + const set = new Set(body.permissions); + expect(set.has("clients:control")).toBe(true); + expect(set.has("clients:disconnect")).toBe(true); + expect(set.has("clients:metadata")).toBe(true); + expect(set.has("users:manage")).toBe(false); + expect(set.has("system:configure")).toBe(false); + expect(set.has("network:manage-bans")).toBe(false); + }); + + test("viewer has no permissions by default", async () => { + const viewer = await tempUser("viewer"); + const body = await fetchMe(viewer.token); + expect(body.permissions).toEqual([]); + }); + + test("viewer with extras gains those permissions in /api/auth/me", async () => { + const viewer = await tempUser("viewer"); + setUserExtraPermissions(viewer.user.id, ["clients:metadata"]); + const body = await fetchMe(viewer.token); + expect(new Set(body.permissions).has("clients:metadata")).toBe(true); + }); + + test("viewer in a group gains the group's permissions in /api/auth/me", async () => { + const viewer = await tempUser("viewer"); + const group = tempGroup("me_group", ["clients:disconnect", "audit:view"]); + setUserGroups(viewer.user.id, [group.id]); + const body = await fetchMe(viewer.token); + const set = new Set(body.permissions); + expect(set.has("clients:disconnect")).toBe(true); + expect(set.has("audit:view")).toBe(true); + }); + + test("operator with can_build=0 loses clients:build (legacy userOverride still revokes via /api/auth/me)", async () => { + const op = await tempUser("operator"); + const { setUserCanBuild } = await import("../../users"); + setUserCanBuild(op.user.id, false); + const body = await fetchMe(op.token); + expect(new Set(body.permissions).has("clients:build")).toBe(false); + }); +}); diff --git a/Overlord-Server/src/server/routes/permission-groups-routes.test.ts b/Overlord-Server/src/server/routes/permission-groups-routes.test.ts new file mode 100644 index 0000000..48ab6b9 --- /dev/null +++ b/Overlord-Server/src/server/routes/permission-groups-routes.test.ts @@ -0,0 +1,320 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import { generateToken } from "../../auth"; +import { AuditAction, flushAuditLogsSync, getAuditLogs } from "../../auditLog"; +import { + createUser, + deleteUser, + deletePermissionGroup, + getUserById, + setUserGroups, +} from "../../users"; +import { handlePermissionGroupsRoutes } from "./permission-groups-routes"; + +const PASSWORD = "Aa1!PgRoutesTestPassword"; + +const mockServer = { + requestIP: () => ({ address: "127.0.0.1" }), +}; + +const createdUserIds: number[] = []; +const createdGroupIds: number[] = []; + +async function tempUser(role: "admin" | "operator" | "viewer") { + const username = `pgr_${role.slice(0, 3)}_${Date.now().toString(36)}${Math.floor(Math.random() * 1e6).toString(36)}`; + const result = await createUser(username, PASSWORD, role, "test"); + expect(result.success).toBe(true); + const user = getUserById(result.userId!); + expect(user).not.toBeNull(); + createdUserIds.push(user!.id); + const token = await generateToken(user!); + return { user: user!, token }; +} + +function call(method: string, path: string, token: string | null, body?: unknown) { + const url = new URL(`https://localhost${path}`); + const headers: Record = { "Content-Type": "application/json" }; + if (token) headers.Authorization = `Bearer ${token}`; + return handlePermissionGroupsRoutes( + new Request(url, { + method, + headers, + body: body === undefined ? undefined : JSON.stringify(body), + }), + url, + mockServer, + ); +} + +afterEach(() => { + while (createdGroupIds.length > 0) { + const id = createdGroupIds.pop(); + if (typeof id === "number") deletePermissionGroup(id); + } + while (createdUserIds.length > 0) { + const id = createdUserIds.pop(); + if (typeof id === "number") deleteUser(id); + } +}); + +describe("permission-groups routes — auth gate", () => { + test("operator gets 403 from GET /api/permissions", async () => { + const op = await tempUser("operator"); + const res = await call("GET", "/api/permissions", op.token); + expect(res!.status).toBe(403); + }); + + test("operator gets 403 from GET /api/permission-groups", async () => { + const op = await tempUser("operator"); + const res = await call("GET", "/api/permission-groups", op.token); + expect(res!.status).toBe(403); + }); + + test("operator gets 403 from POST /api/permission-groups", async () => { + const op = await tempUser("operator"); + const res = await call("POST", "/api/permission-groups", op.token, { + name: "shouldnt-work", + permissions: ["clients:metadata"], + }); + expect(res!.status).toBe(403); + }); + + test("unauthenticated request returns 401", async () => { + const res = await call("GET", "/api/permission-groups", null); + expect(res!.status).toBe(401); + }); +}); + +describe("permission-groups routes — list permissions catalog", () => { + test("admin can list known permissions with descriptions", async () => { + const admin = await tempUser("admin"); + const res = await call("GET", "/api/permissions", admin.token); + expect(res!.status).toBe(200); + const body = await res!.json(); + expect(Array.isArray(body.permissions)).toBe(true); + const ids = new Set(body.permissions.map((p: any) => p.id)); + // Spot-check a few permissions we know exist. + expect(ids.has("clients:disconnect")).toBe(true); + expect(ids.has("clients:metadata")).toBe(true); + expect(ids.has("system:configure")).toBe(true); + // Each entry must have a non-empty description. + for (const p of body.permissions) { + expect(typeof p.description).toBe("string"); + expect(p.description.length).toBeGreaterThan(0); + } + }); +}); + +describe("permission-groups routes — group CRUD", () => { + test("create → list → fetch by id round trip", async () => { + const admin = await tempUser("admin"); + + const createRes = await call("POST", "/api/permission-groups", admin.token, { + name: `crud_${Date.now().toString(36)}`, + description: "test group", + permissions: ["clients:metadata", "clients:disconnect"], + }); + expect(createRes!.status).toBe(200); + const created = await createRes!.json(); + expect(created.group.id).toBeGreaterThan(0); + createdGroupIds.push(created.group.id); + expect(created.group.permissions.sort()).toEqual(["clients:disconnect", "clients:metadata"]); + + const listRes = await call("GET", "/api/permission-groups", admin.token); + const list = await listRes!.json(); + expect(list.groups.some((g: any) => g.id === created.group.id)).toBe(true); + + const getRes = await call("GET", `/api/permission-groups/${created.group.id}`, admin.token); + expect(getRes!.status).toBe(200); + const fetched = await getRes!.json(); + expect(fetched.group.permissions.sort()).toEqual(["clients:disconnect", "clients:metadata"]); + }); + + test("PATCH replaces permissions", async () => { + const admin = await tempUser("admin"); + const createRes = await call("POST", "/api/permission-groups", admin.token, { + name: `patch_${Date.now().toString(36)}`, + permissions: ["clients:metadata"], + }); + const created = await createRes!.json(); + createdGroupIds.push(created.group.id); + + const patchRes = await call("PATCH", `/api/permission-groups/${created.group.id}`, admin.token, { + permissions: ["clients:uninstall"], + }); + expect(patchRes!.status).toBe(200); + const patched = await patchRes!.json(); + expect(patched.group.permissions).toEqual(["clients:uninstall"]); + }); + + test("DELETE removes the group", async () => { + const admin = await tempUser("admin"); + const createRes = await call("POST", "/api/permission-groups", admin.token, { + name: `del_${Date.now().toString(36)}`, + permissions: [], + }); + const created = await createRes!.json(); + + const delRes = await call("DELETE", `/api/permission-groups/${created.group.id}`, admin.token); + expect(delRes!.status).toBe(200); + + const getRes = await call("GET", `/api/permission-groups/${created.group.id}`, admin.token); + expect(getRes!.status).toBe(404); + }); + + test("rejects unknown permissions during creation (silent strip)", async () => { + const admin = await tempUser("admin"); + const res = await call("POST", "/api/permission-groups", admin.token, { + name: `sanitize_${Date.now().toString(36)}`, + permissions: ["clients:metadata", "obviously:not:real", ""], + }); + expect(res!.status).toBe(200); + const data = await res!.json(); + createdGroupIds.push(data.group.id); + expect(data.group.permissions).toEqual(["clients:metadata"]); + }); + + test("duplicate group name returns 400", async () => { + const admin = await tempUser("admin"); + const name = `dup_${Date.now().toString(36)}`; + const first = await call("POST", "/api/permission-groups", admin.token, { name, permissions: [] }); + const firstData = await first!.json(); + createdGroupIds.push(firstData.group.id); + const second = await call("POST", "/api/permission-groups", admin.token, { name, permissions: [] }); + expect(second!.status).toBe(400); + }); +}); + +describe("permission-groups routes — user assignments", () => { + test("PUT /api/users/:id/permission-groups assigns + GET returns the same set", async () => { + const admin = await tempUser("admin"); + const target = await tempUser("viewer"); + + const createRes = await call("POST", "/api/permission-groups", admin.token, { + name: `assign_${Date.now().toString(36)}`, + permissions: ["clients:metadata"], + }); + const group = (await createRes!.json()).group; + createdGroupIds.push(group.id); + + const putRes = await call("PUT", `/api/users/${target.user.id}/permission-groups`, admin.token, { + groupIds: [group.id], + }); + expect(putRes!.status).toBe(200); + + const getRes = await call("GET", `/api/users/${target.user.id}/permission-groups`, admin.token); + const body = await getRes!.json(); + expect(body.groupIds).toEqual([group.id]); + }); + + test("PUT /api/users/:id/extra-permissions sanitizes unknown perms", async () => { + const admin = await tempUser("admin"); + const target = await tempUser("viewer"); + + const putRes = await call("PUT", `/api/users/${target.user.id}/extra-permissions`, admin.token, { + permissions: ["clients:metadata", "not:a:permission", 42], + }); + expect(putRes!.status).toBe(200); + const body = await putRes!.json(); + expect(body.permissions).toEqual(["clients:metadata"]); + }); + + test("returns 404 for unknown user", async () => { + const admin = await tempUser("admin"); + const res = await call("PUT", `/api/users/9999999/permission-groups`, admin.token, { groupIds: [] }); + expect(res!.status).toBe(404); + }); +}); + +describe("permission-groups routes — audit log shape", () => { + test("group create logs PERMISSION_GROUP_CREATE with structured details", async () => { + const admin = await tempUser("admin"); + const name = `audit_create_${Date.now().toString(36)}`; + const createRes = await call("POST", "/api/permission-groups", admin.token, { + name, + description: "describes the group", + permissions: ["clients:metadata"], + }); + const created = (await createRes!.json()).group; + createdGroupIds.push(created.id); + + flushAuditLogsSync(); + const { logs } = getAuditLogs({ + action: AuditAction.PERMISSION_GROUP_CREATE, + username: admin.user.username, + pageSize: 5, + }); + const entry = logs.find((l) => l.details && l.details.includes(`"groupId":${created.id}`)); + expect(entry).toBeDefined(); + const parsed = JSON.parse(entry!.details!); + expect(parsed.groupId).toBe(created.id); + expect(parsed.name).toBe(name); + expect(parsed.description).toBe("describes the group"); + expect(parsed.permissions).toEqual(["clients:metadata"]); + }); + + test("user-groups change logs added/removed deltas, not just the count", async () => { + const admin = await tempUser("admin"); + const target = await tempUser("viewer"); + + const ga = (await (await call("POST", "/api/permission-groups", admin.token, { + name: `delta_a_${Date.now().toString(36)}`, + permissions: [], + }))!.json()).group; + createdGroupIds.push(ga.id); + const gb = (await (await call("POST", "/api/permission-groups", admin.token, { + name: `delta_b_${Date.now().toString(36)}`, + permissions: [], + }))!.json()).group; + createdGroupIds.push(gb.id); + + // Seed: user has only group A + setUserGroups(target.user.id, [ga.id]); + + // Change: replace with group B + await call("PUT", `/api/users/${target.user.id}/permission-groups`, admin.token, { + groupIds: [gb.id], + }); + + flushAuditLogsSync(); + const { logs } = getAuditLogs({ + action: AuditAction.USER_GROUPS_CHANGE, + username: admin.user.username, + pageSize: 5, + }); + const entry = logs.find((l) => l.details && l.details.includes(`"targetUserId":${target.user.id}`)); + expect(entry).toBeDefined(); + const parsed = JSON.parse(entry!.details!); + expect(parsed.groupsAdded).toEqual([gb.id]); + expect(parsed.groupsRemoved).toEqual([ga.id]); + expect(parsed.targetUsername).toBe(target.user.username); + }); + + test("user-extras change logs added/removed deltas", async () => { + const admin = await tempUser("admin"); + const target = await tempUser("viewer"); + + // First set: ["clients:metadata"] + await call("PUT", `/api/users/${target.user.id}/extra-permissions`, admin.token, { + permissions: ["clients:metadata"], + }); + // Then replace with ["clients:disconnect"] + await call("PUT", `/api/users/${target.user.id}/extra-permissions`, admin.token, { + permissions: ["clients:disconnect"], + }); + + flushAuditLogsSync(); + const { logs } = getAuditLogs({ + action: AuditAction.USER_EXTRA_PERMISSIONS_CHANGE, + username: admin.user.username, + pageSize: 10, + }); + // The most recent entry should reflect the second swap. + const entry = logs + .filter((l) => l.details && l.details.includes(`"targetUserId":${target.user.id}`)) + .sort((a, b) => b.timestamp - a.timestamp)[0]; + expect(entry).toBeDefined(); + const parsed = JSON.parse(entry!.details!); + expect(parsed.permissionsAdded).toEqual(["clients:disconnect"]); + expect(parsed.permissionsRemoved).toEqual(["clients:metadata"]); + }); +}); diff --git a/Overlord-Server/src/server/routes/permission-groups-routes.ts b/Overlord-Server/src/server/routes/permission-groups-routes.ts index 2c63272..f1c42c6 100644 --- a/Overlord-Server/src/server/routes/permission-groups-routes.ts +++ b/Overlord-Server/src/server/routes/permission-groups-routes.ts @@ -14,34 +14,13 @@ import { } from "../../users"; import { getPermissionDescription, - type Permission, + listAllPermissions, requirePermission, } from "../../rbac"; -const KNOWN_PERMISSIONS: Permission[] = [ - "users:manage", - "clients:control", - "clients:build", - "clients:enroll", - "clients:silent-exec", - "clients:disconnect", - "clients:reconnect", - "clients:metadata", - "clients:uninstall", - "clients:winre", - "audit:view", - "chat:write", - "scripts:manage", - "deploys:manage", - "plugins:manage", - "plugins:configure", - "network:manage-bans", - "system:configure", -]; - function sanitizePermissions(raw: unknown): string[] { if (!Array.isArray(raw)) return []; - const known = new Set(KNOWN_PERMISSIONS); + const known = new Set(listAllPermissions()); return Array.from( new Set( raw @@ -79,7 +58,7 @@ export async function handlePermissionGroupsRoutes( return new Response("Forbidden", { status: 403 }); } return Response.json({ - permissions: KNOWN_PERMISSIONS.map((p) => ({ + permissions: listAllPermissions().map((p) => ({ id: p, description: getPermissionDescription(p), })),