-
-
Notifications
You must be signed in to change notification settings - Fork 241
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
9 changed files
with
319 additions
and
31 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
import { prisma, Role, User } from "../prisma"; | ||
import { checkSession } from "./session"; | ||
import { Permission } from "./types/permissions"; | ||
|
||
type UserWithRoles = User & { | ||
roles: Role[]; | ||
}; | ||
|
||
export class InsufficientPermissionsError extends Error { | ||
constructor(message: string = "Insufficient permissions") { | ||
super(message); | ||
this.name = "InsufficientPermissionsError"; | ||
} | ||
} | ||
|
||
/** | ||
* Checks if a user has the required permissions through their roles | ||
* @param user - The user with their roles loaded | ||
* @param requiredPermissions - Single permission or array of permissions to check | ||
* @param requireAll - If true, user must have ALL permissions. If false, only ONE permission is required | ||
* @returns boolean | ||
*/ | ||
export function hasPermission( | ||
user: UserWithRoles, | ||
requiredPermissions: Permission | Permission[], | ||
requireAll: boolean = true | ||
): boolean { | ||
// Convert single permission to array for consistent handling | ||
const permissions = Array.isArray(requiredPermissions) | ||
? requiredPermissions | ||
: [requiredPermissions]; | ||
|
||
// Combine all permissions from user's roles and default role | ||
const userPermissions = new Set<Permission>(); | ||
|
||
// Add permissions from default role if it exists | ||
const defaultRole = user.roles.find((role) => role.isDefault); | ||
if (defaultRole) { | ||
defaultRole.permissions.forEach((perm) => userPermissions.add(perm as Permission)); | ||
} | ||
|
||
// Add permissions from additional roles | ||
user.roles.forEach((role) => { | ||
role.permissions.forEach((perm) => userPermissions.add(perm as Permission)); | ||
}); | ||
|
||
if (requireAll) { | ||
// Check if user has ALL required permissions | ||
return permissions.every((permission) => userPermissions.has(permission)); | ||
} else { | ||
// Check if user has AT LEAST ONE of the required permissions | ||
return permissions.some((permission) => userPermissions.has(permission)); | ||
} | ||
} | ||
|
||
/** | ||
* Authorization middleware that checks for required permissions | ||
* @param requiredPermissions - Single permission or array of permissions to check | ||
* @param requireAll - If true, user must have ALL permissions. If false, only ONE permission is required | ||
*/ | ||
export function requirePermission( | ||
requiredPermissions: Permission | Permission[], | ||
requireAll: boolean = true | ||
) { | ||
return async (req: any, res: any, next: any) => { | ||
try { | ||
const user = await checkSession(req); | ||
const config = await prisma.config.findFirst(); | ||
|
||
if (!config?.roles_active) { | ||
next(); | ||
} | ||
|
||
const userWithRoles = user | ||
? await prisma.user.findUnique({ | ||
where: { id: user.id }, | ||
include: { | ||
roles: true, | ||
}, | ||
}) | ||
: null; | ||
|
||
if (!userWithRoles) { | ||
throw new Error("User not authenticated"); | ||
} | ||
|
||
if (!hasPermission(userWithRoles, requiredPermissions, requireAll)) { | ||
throw new InsufficientPermissionsError(); | ||
} | ||
|
||
next(); | ||
} catch (error) { | ||
next(error); | ||
} | ||
}; | ||
} | ||
|
||
// Usage examples: | ||
/* | ||
// Check single permission | ||
if (hasPermission(user, 'issue::create')) { | ||
// Allow create ticket | ||
} | ||
// Check multiple permissions (all required) | ||
if (hasPermission(user, ['issue:update', 'issue:assign'])) { | ||
// Allow ticket update and assignment | ||
} | ||
// Check multiple permissions (at least one required) | ||
if (hasPermission(user, ['user:manage', 'role:manage'], false)) { | ||
// Allow access if user has either permission | ||
} | ||
// Use as middleware | ||
router.post('/tickets', | ||
requirePermission('issue::create'), | ||
ticketController.create | ||
); | ||
// Use as middleware with multiple permissions | ||
router.put('/tickets/:id/assign', | ||
requirePermission(['issue:update', 'issue:assign']), | ||
ticketController.assign | ||
); | ||
*/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
export type IssuePermission = | ||
| 'issue:create' | ||
| 'issue:read' | ||
| 'issue:write' | ||
| 'issue:update' | ||
| 'issue:delete' | ||
| 'issue:assign' | ||
| 'issue:transfer' | ||
| 'issue:comment'; | ||
|
||
export type UserPermission = | ||
| 'user:create' | ||
| 'user:read' | ||
| 'user:update' | ||
| 'user:delete' | ||
| 'user:manage'; | ||
|
||
export type RolePermission = | ||
| 'role:create' | ||
| 'role:read' | ||
| 'role:update' | ||
| 'role:delete' | ||
| 'role:manage'; | ||
|
||
export type TeamPermission = | ||
| 'team:create' | ||
| 'team:read' | ||
| 'team:update' | ||
| 'team:delete' | ||
| 'team:manage'; | ||
|
||
export type ClientPermission = | ||
| 'client:create' | ||
| 'client:read' | ||
| 'client:update' | ||
| 'client:delete' | ||
| 'client:manage'; | ||
|
||
export type KnowledgeBasePermission = | ||
| 'kb:create' | ||
| 'kb:read' | ||
| 'kb:update' | ||
| 'kb:delete' | ||
| 'kb:manage'; | ||
|
||
export type SystemPermission = | ||
| 'settings:view' | ||
| 'settings:manage' | ||
| 'webhook:manage' | ||
| 'integration:manage' | ||
| 'email_template:manage'; | ||
|
||
export type TimeTrackingPermission = | ||
| 'time_entry:create' | ||
| 'time_entry:read' | ||
| 'time_entry:update' | ||
| 'time_entry:delete'; | ||
|
||
export type ViewPermission = | ||
| 'docs:manage' | ||
| 'admin:panel'; | ||
|
||
export type WebhookPermission = | ||
| 'webhook:create' | ||
| 'webhook:read' | ||
| 'webhook:update' | ||
| 'webhook:delete'; | ||
|
||
export type Permission = | ||
| IssuePermission | ||
| UserPermission | ||
| RolePermission | ||
| TeamPermission | ||
| ClientPermission | ||
| KnowledgeBasePermission | ||
| SystemPermission | ||
| TimeTrackingPermission | ||
| ViewPermission | ||
| WebhookPermission; | ||
|
||
// Useful type for grouping permissions by category | ||
export const PermissionCategories = { | ||
ISSUE: 'Issue Management', | ||
USER: 'User Management', | ||
ROLE: 'Role Management', | ||
TEAM: 'Team Management', | ||
CLIENT: 'Client Management', | ||
KNOWLEDGE_BASE: 'Knowledge Base', | ||
SYSTEM: 'System Settings', | ||
TIME_TRACKING: 'Time Tracking', | ||
VIEW: 'Views', | ||
WEBHOOK: 'Webhook Management', | ||
} as const; | ||
|
||
export type PermissionCategory = typeof PermissionCategories[keyof typeof PermissionCategories]; | ||
|
||
// Helper type for permission groups | ||
export interface PermissionGroup { | ||
category: PermissionCategory; | ||
permissions: Permission[]; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,3 @@ | ||
import { PrismaClient } from "@prisma/client"; | ||
import { Hook, Permission, PrismaClient, Role, User } from "@prisma/client"; | ||
export const prisma: PrismaClient = new PrismaClient(); | ||
export type Hook = "ticket_created" | "ticket_status_changed"; | ||
export { Hook, Permission, Role, User }; |
36 changes: 36 additions & 0 deletions
36
apps/api/src/prisma/migrations/20241113193825_rbac/migration.sql
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
-- CreateEnum | ||
CREATE TYPE "Permission" AS ENUM ('CREATE_TICKET', 'READ_TICKET', 'WRITE_TICKET', 'UPDATE_TICKET', 'DELETE_TICKET', 'ASSIGN_TICKET', 'TRANSFER_TICKET', 'COMMENT_TICKET', 'CREATE_USER', 'READ_USER', 'UPDATE_USER', 'DELETE_USER', 'MANAGE_USERS', 'CREATE_ROLE', 'READ_ROLE', 'UPDATE_ROLE', 'DELETE_ROLE', 'MANAGE_ROLES', 'CREATE_TEAM', 'READ_TEAM', 'UPDATE_TEAM', 'DELETE_TEAM', 'MANAGE_TEAMS', 'CREATE_CLIENT', 'READ_CLIENT', 'UPDATE_CLIENT', 'DELETE_CLIENT', 'MANAGE_CLIENTS', 'CREATE_KB', 'READ_KB', 'UPDATE_KB', 'DELETE_KB', 'MANAGE_KB', 'VIEW_REPORTS', 'MANAGE_SETTINGS', 'MANAGE_WEBHOOKS', 'MANAGE_INTEGRATIONS', 'MANAGE_EMAIL_TEMPLATES', 'CREATE_TIME_ENTRY', 'READ_TIME_ENTRY', 'UPDATE_TIME_ENTRY', 'DELETE_TIME_ENTRY', 'MANAGE_DOCS', 'ADMIN_PANEL'); | ||
|
||
-- CreateTable | ||
CREATE TABLE "Role" ( | ||
"id" TEXT NOT NULL, | ||
"name" TEXT NOT NULL, | ||
"description" TEXT, | ||
"permissions" "Permission"[], | ||
"isDefault" BOOLEAN NOT NULL DEFAULT false, | ||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
|
||
CONSTRAINT "Role_pkey" PRIMARY KEY ("id") | ||
); | ||
|
||
-- CreateTable | ||
CREATE TABLE "_RoleToUser" ( | ||
"A" TEXT NOT NULL, | ||
"B" TEXT NOT NULL | ||
); | ||
|
||
-- CreateIndex | ||
CREATE UNIQUE INDEX "Role_name_key" ON "Role"("name"); | ||
|
||
-- CreateIndex | ||
CREATE UNIQUE INDEX "_RoleToUser_AB_unique" ON "_RoleToUser"("A", "B"); | ||
|
||
-- CreateIndex | ||
CREATE INDEX "_RoleToUser_B_index" ON "_RoleToUser"("B"); | ||
|
||
-- AddForeignKey | ||
ALTER TABLE "_RoleToUser" ADD CONSTRAINT "_RoleToUser_A_fkey" FOREIGN KEY ("A") REFERENCES "Role"("id") ON DELETE CASCADE ON UPDATE CASCADE; | ||
|
||
-- AddForeignKey | ||
ALTER TABLE "_RoleToUser" ADD CONSTRAINT "_RoleToUser_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; |
2 changes: 2 additions & 0 deletions
2
apps/api/src/prisma/migrations/20241113195413_role_active/migration.sql
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
-- AlterTable | ||
ALTER TABLE "Config" ADD COLUMN "roles_active" BOOLEAN NOT NULL DEFAULT false; |
12 changes: 12 additions & 0 deletions
12
apps/api/src/prisma/migrations/20241113200612_update_types/migration.sql
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
/* | ||
Warnings: | ||
- The `permissions` column on the `Role` table would be dropped and recreated. This will lead to data loss if there is data in the column. | ||
*/ | ||
-- AlterTable | ||
ALTER TABLE "Role" DROP COLUMN "permissions", | ||
ADD COLUMN "permissions" JSONB[]; | ||
|
||
-- DropEnum | ||
DROP TYPE "Permission"; |
Oops, something went wrong.