wulf-pulse/lib/permissions.ts
lorentz 00f196c115 fix(auth): stop hasPermission crashing for non-admin ("user") roles
hasPermission()'s parameter was named userRole: string, shadowing the
module-level userRole role object exported earlier in the same file.
The internal roles map's `user: userRole` entry therefore bound to the
shadowed string parameter (e.g. "user") instead of the actual role
object — so any permission check for a "user"-role session (the only
non-admin role in the app) hit `"user".statements[resource]`, which is
undefined, and threw instead of returning false.

Net effect: every requirePermission()-gated route in the app returned
a 500 instead of a 403 for non-admin users. This predates phase 14 —
surfaced now because phase 14's PAX8 resolve route is admin-gated and
got exercised by a non-admin account during verification.

Renamed the parameter to roleName to remove the collision. Added
lib/permissions.test.ts (previously zero coverage on this file) to
lock in the "user"/admin/super-admin behavior and prevent regression.
2026-07-12 18:20:36 -04:00

109 lines
2.9 KiB
TypeScript

import { createAccessControl } from "better-auth/plugins/access";
// Define all available permissions for resources
export const statement = {
// Ticket management
tickets: ["create", "read", "update", "delete"],
// Configuration items
configItems: ["create", "read", "update", "delete"],
// Admin panel access
admin: ["access"],
// User management
users: ["create", "read", "update", "delete", "invite", "ban"],
// Role management
roles: ["create", "read", "update", "delete"],
// Audit log access
auditLog: ["read"],
// Settings management
settings: ["read", "update"],
// IT Glue documentation read/write (Phase 4 — asset audit + write-back)
itglue: ["read", "write"],
// Datto RMM Overshell evidence (Phase 4.2 — read jobs / execute scripts)
rmm: ["read", "execute"],
} as const;
// Create access control instance
export const ac = createAccessControl(statement);
// Super Admin role - full access to everything
export const superAdminRole = ac.newRole({
tickets: ["create", "read", "update", "delete"],
configItems: ["create", "read", "update", "delete"],
admin: ["access"],
users: ["create", "read", "update", "delete", "invite", "ban"],
roles: ["create", "read", "update", "delete"],
auditLog: ["read"],
settings: ["read", "update"],
itglue: ["read", "write"],
rmm: ["read", "execute"],
});
// Admin role - access to admin panel and user management, but not role management
export const adminRole = ac.newRole({
tickets: ["create", "read", "update", "delete"],
configItems: ["create", "read", "update", "delete"],
admin: ["access"],
users: ["create", "read", "update", "invite"],
roles: ["read"],
auditLog: ["read"],
settings: ["read"],
itglue: ["read", "write"],
rmm: ["read", "execute"],
});
// User role - basic access
export const userRole = ac.newRole({
tickets: ["create", "read", "update"],
configItems: ["read"],
admin: [],
users: [],
roles: [],
auditLog: [],
settings: [],
itglue: ["read"],
rmm: ["read"],
});
// Helper function to check if a user has a specific permission
export function hasPermission(
roleName: string,
resource: keyof typeof statement,
action: string
): boolean {
const roles: Record<string, ReturnType<typeof ac.newRole>> = {
"super-admin": superAdminRole,
admin: adminRole,
user: userRole,
};
const role = roles[roleName];
if (!role) return false;
// Check if the role has the permission
const permissions = role.statements[resource];
if (!permissions) return false;
return (permissions as readonly string[]).includes(action);
}
// Type for permission check
export type Permission = {
resource: keyof typeof statement;
action: string;
};
// Check multiple permissions
export function hasPermissions(
userRole: string,
permissions: Permission[]
): boolean {
return permissions.every((p) => hasPermission(userRole, p.resource, p.action));
}