import { type AuthenticatedUser } from "./auth"; import { canBuildClients, canChatWrite, canUserAccessClient, canUserAccessFeature, canUserAccessPlugin, getUserGrantedPermissions, type FeatureName, type UserRole, } from "./users"; type PermissionDef = { description: string; roles: readonly UserRole[]; userOverride?: (userId: number, role: UserRole) => boolean; }; const PERMISSIONS = { "users:manage": { description: "Manage users and roles", roles: ["admin"], }, "clients:control": { description: "Control clients (execute commands, desktop, console, files)", roles: ["admin", "operator"], }, "clients:build": { description: "Build client binaries", roles: ["admin", "operator"], userOverride: canBuildClients, }, "clients:enroll": { description: "Manage client enrollment approvals", roles: ["admin", "operator"], }, "clients:silent-exec": { description: "Silently execute arbitrary commands on clients", roles: ["admin"], }, "clients:disconnect": { description: "Force a client to disconnect", roles: ["admin", "operator"], }, "clients:reconnect": { description: "Force a client to drop its session and reconnect", roles: ["admin", "operator"], }, "clients:metadata": { description: "Edit client metadata (nickname, tag, group, bookmark, mute)", roles: ["admin", "operator"], }, "clients:uninstall": { description: "Uninstall the agent on a client (removes persistence and deletes from dashboard)", roles: ["admin", "operator"], }, "audit:view": { description: "View audit logs", roles: ["admin", "operator"], }, "chat:write": { description: "Send messages in team chat", roles: ["admin", "operator"], userOverride: canChatWrite, }, "scripts:manage": { description: "Manage auto-run scripts", roles: ["admin"], }, "deploys:manage": { description: "Manage deploys and auto-deploys", roles: ["admin"], }, "plugins:manage": { description: "Upload, enable, and delete plugins", roles: ["admin", "operator"], }, "plugins:configure": { description: "Configure plugin trust, auto-load, and direct execution", roles: ["admin"], }, "network:manage-bans": { description: "Manage IP bans", roles: ["admin"], }, "system:configure": { description: "Legacy full server settings access (grants all system:* settings permissions)", roles: ["admin"], }, "system:security": { description: "Change security policy settings", roles: ["admin"], }, "system:tls": { description: "Change TLS and certificate settings", roles: ["admin"], }, "system:registration": { description: "Change user registration policy settings", roles: ["admin"], }, "system:notifications": { description: "Change global notification delivery settings", roles: ["admin"], }, "system:chat": { description: "Change team chat settings", roles: ["admin"], }, "system:appearance": { description: "Change custom CSS and appearance settings", roles: ["admin"], }, "system:thumbnails": { description: "Change screenshot thumbnail settings", roles: ["admin"], }, "system:input-archive": { description: "Change input log archive settings", roles: ["admin"], }, "system:build-limits": { description: "Change build rate limit settings", roles: ["admin"], }, "system:export-import": { description: "Export and import server settings", roles: ["admin"], }, "system:health": { description: "View server health diagnostics", roles: ["admin"], }, "system:health:manage": { description: "Run server health maintenance actions", roles: ["admin"], }, "system:profiler": { description: "Run server CPU and memory profiler captures", roles: ["admin"], }, "clients:elevate": { description: "Elevate agent privileges (UAC on Windows, sudo on macOS)", roles: ["admin"], }, "clients:winre": { description: "Install or uninstall WinRE persistence on clients", roles: ["admin"], }, } as const satisfies Record; 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]; } export function hasPermission( role: UserRole, permission: Permission | string, userId?: number, ): boolean { const def = lookupPermission(permission); if (!def) return false; // The legacy role-level gate. Per-permission userOverride (canBuild, // canChatWrite) is authoritative when present + userId is known — it can // both grant AND deny relative to the role default (an operator with // can_build=0 loses clients:build at the role layer). Without a userOverride, // membership in def.roles decides. let grantedByRole: boolean; if (def.userOverride && userId !== undefined) { grantedByRole = def.userOverride(userId, role); } else { grantedByRole = def.roles.includes(role); } if (grantedByRole) return true; // Permission groups + per-user extras are *additive only*: they can grant a // permission the role doesn't have, but they cannot revoke one the role does. if (userId !== undefined) { const granted = getUserGrantedPermissions(userId); if (granted.has(permission)) return true; if ( typeof permission === "string" && permission.startsWith("system:") && permission !== "system:configure" && granted.has("system:configure") ) { return true; } } return false; } export function checkPermission( user: AuthenticatedUser | null, permission: Permission, ): boolean { if (!user) return false; return hasPermission(user.role, permission, user.userId); } export function checkAnyPermission( user: AuthenticatedUser | null, permissions: Permission[], ): boolean { if (!user) return false; return permissions.some((p) => hasPermission(user.role, p, user.userId)); } export function checkAllPermissions( user: AuthenticatedUser | null, permissions: Permission[], ): boolean { if (!user) return false; return permissions.every((p) => hasPermission(user.role, p, user.userId)); } export function requireAuth(user: AuthenticatedUser | null): AuthenticatedUser { if (!user) { throw new Response("Unauthorized", { status: 401 }); } return user; } export function requirePermission( user: AuthenticatedUser | null, permission: Permission, ): AuthenticatedUser { const authedUser = requireAuth(user); if (!checkPermission(authedUser, permission)) { throw new Response("Forbidden: Insufficient permissions", { status: 403 }); } return authedUser; } export function requireAnyPermission( user: AuthenticatedUser | null, permissions: Permission[], ): AuthenticatedUser { const authedUser = requireAuth(user); if (!checkAnyPermission(authedUser, permissions)) { throw new Response("Forbidden: Insufficient permissions", { status: 403 }); } return authedUser; } export function getPermissionDescription(permission: Permission | string): string { return lookupPermission(permission)?.description ?? "Unknown permission"; } export function getRoleDescription(role: UserRole): string { switch (role) { case "admin": return "Full access - can manage users and control all clients"; case "operator": return "Can control clients but cannot manage users"; case "viewer": return "Read-only access to view clients"; default: return "Unknown role"; } } export function requireClientAccess( user: AuthenticatedUser | null, clientId: string, ): AuthenticatedUser { const authed = requireAuth(user); if (!canUserAccessClient(authed.userId, authed.role, clientId)) { throw new Response("Forbidden: You do not have access to this client", { status: 403 }); } return authed; } export function requirePluginAccess( user: AuthenticatedUser | null, pluginId: string, ): AuthenticatedUser { const authed = requireAuth(user); if (!canUserAccessPlugin(authed.userId, authed.role, pluginId)) { throw new Response("Forbidden: You do not have access to this plugin", { status: 403 }); } return authed; } export function requireFeatureAccess( user: AuthenticatedUser | null, feature: FeatureName, ): AuthenticatedUser { const authed = requireAuth(user); if (!canUserAccessFeature(authed.userId, authed.role, feature)) { throw new Response("Forbidden: feature access denied", { status: 403 }); } return authed; } export function getRolePermissions(role: UserRole): Permission[] { const result: Permission[] = []; for (const perm of Object.keys(PERMISSIONS) as Permission[]) { const def = lookupPermission(perm); if (def && def.roles.includes(role)) result.push(perm); } return result; } export function getUserPermissions(userId: number, role: UserRole): Permission[] { const result: Permission[] = []; for (const perm of Object.keys(PERMISSIONS) as Permission[]) { if (hasPermission(role, perm, userId)) result.push(perm); } return result; }