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.
109 lines
2.9 KiB
TypeScript
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));
|
|
}
|