import { db } from "@dokploy/server/db"; import { member, organizationRole } from "@dokploy/server/db/schema"; import { hasValidLicense } from "@dokploy/server/services/proprietary/license-key"; import { TRPCError } from "@trpc/server"; import { and, eq } from "drizzle-orm"; import { ac, adminRole, enterpriseOnlyResources, memberRole, ownerRole, statements, } from "../lib/access-control"; type Statements = typeof statements; type Resource = keyof Statements; type Action = Statements[R][number]; type Permissions = { [R in Resource]?: Action[]; }; export type PermissionCtx = { user: { id: string }; session: { activeOrganizationId: string }; }; export type ResolvedPermissions = { [R in Resource]: { [A in Statements[R][number]]: boolean; }; }; const staticRoles: Record> = { owner: ownerRole, admin: adminRole, member: memberRole, }; const resolveRole = async ( roleName: string, organizationId: string, ): Promise | null> => { if (staticRoles[roleName]) { return staticRoles[roleName]; } const licensed = await hasValidLicense(organizationId); if (!licensed) { return null; } const customRoles = await db.query.organizationRole.findMany({ where: and( eq(organizationRole.organizationId, organizationId), eq(organizationRole.role, roleName), ), }); if (customRoles.length === 0) { return null; } const merged: Record = {}; for (const entry of customRoles) { const parsed = JSON.parse(entry.permission) as Record; for (const [resource, actions] of Object.entries(parsed)) { merged[resource] = [ ...new Set([...(merged[resource] ?? []), ...actions]), ]; } } return ac.newRole(merged as any); }; export const checkPermission = async ( ctx: PermissionCtx, permissions: Permissions, ) => { const { id: userId } = ctx.user; const { activeOrganizationId: organizationId } = ctx.session; const memberRecord = await findMemberByUserId(userId, organizationId); const isStaticRole = memberRecord.role in staticRoles; if (isStaticRole) { const allEnterprise = Object.keys(permissions).every((r) => enterpriseOnlyResources.has(r), ); if (allEnterprise) return; } const role = await resolveRole(memberRecord.role, organizationId); if (!role) { throw new TRPCError({ code: "UNAUTHORIZED", message: "Invalid role", }); } const result = role.authorize(permissions); if (result.success) { return; } if (memberRecord.role === "member") { const overrides = getLegacyOverrides(memberRecord); const allGranted = Object.entries(permissions).every( ([resource, actions]) => (actions as string[]).every( (action) => !!(overrides[resource] as Record | undefined)?.[ action ], ), ); if (allGranted) { return; } } throw new TRPCError({ code: "UNAUTHORIZED", message: result.error || "Permission denied", }); }; export const hasPermission = async ( ctx: PermissionCtx, permissions: Permissions, ): Promise => { try { await checkPermission(ctx, permissions); return true; } catch { return false; } }; const getLegacyOverrides = ( memberRecord: Awaited>, ): Partial>> => { return { project: { create: !!memberRecord.canCreateProjects, delete: !!memberRecord.canDeleteProjects, }, service: { create: !!memberRecord.canCreateServices, delete: !!memberRecord.canDeleteServices, }, environment: { create: !!memberRecord.canCreateEnvironments, delete: !!memberRecord.canDeleteEnvironments, }, traefikFiles: { read: !!memberRecord.canAccessToTraefikFiles, }, docker: { read: !!memberRecord.canAccessToDocker, }, api: { read: !!memberRecord.canAccessToAPI, }, sshKeys: { read: !!memberRecord.canAccessToSSHKeys, create: !!memberRecord.canAccessToSSHKeys, delete: !!memberRecord.canAccessToSSHKeys, }, gitProviders: { read: !!memberRecord.canAccessToGitProviders, }, }; }; export const resolvePermissions = async ( ctx: PermissionCtx, ): Promise => { const userId = ctx.user.id; const organizationId = ctx.session.activeOrganizationId; const memberRecord = await findMemberByUserId(userId, organizationId); const role = await resolveRole(memberRecord.role, organizationId); const legacyOverrides = memberRecord.role === "member" ? getLegacyOverrides(memberRecord) : {}; const isPrivilegedRole = memberRecord.role === "owner" || memberRecord.role === "admin"; const result = {} as ResolvedPermissions; for (const [resource, actions] of Object.entries(statements)) { const resourcePerms = {} as Record; for (const action of actions) { if (isPrivilegedRole && enterpriseOnlyResources.has(resource)) { resourcePerms[action] = true; continue; } if (!role) { resourcePerms[action] = false; continue; } const check = role.authorize({ [resource]: [action] }); resourcePerms[action] = check.success || !!(legacyOverrides[resource] as Record | undefined)?.[ action ]; } (result as any)[resource] = resourcePerms; } return result; }; export const checkProjectAccess = async ( ctx: PermissionCtx, action: "create" | "delete", projectId?: string, ) => { const userId = ctx.user.id; const organizationId = ctx.session.activeOrganizationId; const memberRecord = await findMemberByUserId(userId, organizationId); await checkPermission(ctx, { project: [action] }); if ( action !== "create" && projectId && memberRecord.role !== "owner" && memberRecord.role !== "admin" ) { if (!memberRecord.accessedProjects.includes(projectId)) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You don't have access to this project", }); } } }; export const checkServicePermissionAndAccess = async ( ctx: PermissionCtx, serviceId: string, permissions: Permissions, ) => { const userId = ctx.user.id; const organizationId = ctx.session.activeOrganizationId; const memberRecord = await findMemberByUserId(userId, organizationId); await checkPermission(ctx, permissions); if (memberRecord.role !== "owner" && memberRecord.role !== "admin") { if (!memberRecord.accessedServices.includes(serviceId)) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You don't have access to this service", }); } } }; export const checkServiceAccess = async ( ctx: PermissionCtx, serviceId: string, action: "create" | "read" | "delete" = "read", ) => { const userId = ctx.user.id; const organizationId = ctx.session.activeOrganizationId; const memberRecord = await findMemberByUserId(userId, organizationId); await checkPermission(ctx, { service: [action] }); if (memberRecord.role !== "owner" && memberRecord.role !== "admin") { if (action === "create") { if (!memberRecord.accessedProjects.includes(serviceId)) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You don't have access to this project", }); } } else { if (!memberRecord.accessedServices.includes(serviceId)) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You don't have access to this service", }); } } } }; export const checkEnvironmentAccess = async ( ctx: PermissionCtx, environmentId: string, action: "read" | "create" | "delete" = "read", ) => { const userId = ctx.user.id; const organizationId = ctx.session.activeOrganizationId; const memberRecord = await findMemberByUserId(userId, organizationId); await checkPermission(ctx, { environment: [action] }); if ( action !== "create" && memberRecord.role !== "owner" && memberRecord.role !== "admin" ) { if (!memberRecord.accessedEnvironments.includes(environmentId)) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You don't have access to this environment", }); } } }; export const checkEnvironmentCreationPermission = async ( ctx: PermissionCtx, projectId: string, ) => { const userId = ctx.user.id; const organizationId = ctx.session.activeOrganizationId; const memberRecord = await findMemberByUserId(userId, organizationId); await checkPermission(ctx, { environment: ["create"] }); if (memberRecord.role !== "owner" && memberRecord.role !== "admin") { if (!memberRecord.accessedProjects.includes(projectId)) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You don't have access to this project", }); } } }; export const checkEnvironmentDeletionPermission = async ( ctx: PermissionCtx, projectId: string, ) => { const userId = ctx.user.id; const organizationId = ctx.session.activeOrganizationId; const memberRecord = await findMemberByUserId(userId, organizationId); await checkPermission(ctx, { environment: ["delete"] }); if (memberRecord.role !== "owner" && memberRecord.role !== "admin") { if (!memberRecord.accessedProjects.includes(projectId)) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You don't have access to this project", }); } } }; export const addNewProject = async (ctx: PermissionCtx, projectId: string) => { const userId = ctx.user.id; const organizationId = ctx.session.activeOrganizationId; const memberRecord = await findMemberByUserId(userId, organizationId); await db .update(member) .set({ accessedProjects: [...memberRecord.accessedProjects, projectId], }) .where( and( eq(member.id, memberRecord.id), eq(member.organizationId, organizationId), ), ); }; export const addNewEnvironment = async ( ctx: PermissionCtx, environmentId: string, ) => { const userId = ctx.user.id; const organizationId = ctx.session.activeOrganizationId; const memberRecord = await findMemberByUserId(userId, organizationId); await db .update(member) .set({ accessedEnvironments: [ ...memberRecord.accessedEnvironments, environmentId, ], }) .where( and( eq(member.id, memberRecord.id), eq(member.organizationId, organizationId), ), ); }; export const addNewService = async (ctx: PermissionCtx, serviceId: string) => { const userId = ctx.user.id; const organizationId = ctx.session.activeOrganizationId; const memberRecord = await findMemberByUserId(userId, organizationId); await db .update(member) .set({ accessedServices: [...memberRecord.accessedServices, serviceId], }) .where( and( eq(member.id, memberRecord.id), eq(member.organizationId, organizationId), ), ); }; export const findMemberByUserId = async ( userId: string, organizationId: string, ) => { const result = await db.query.member.findFirst({ where: and( eq(member.userId, userId), eq(member.organizationId, organizationId), ), with: { user: true, }, }); if (!result) { throw new TRPCError({ code: "UNAUTHORIZED", message: "Permission denied", }); } return result; };