Files
dokploy/packages/server/src/services/permission.ts
Mauricio Siu 95633b4122 fix: refine permission check for privileged static roles in permission service
Updated the permission check logic to specifically identify "owner" and "admin" roles as privileged static roles, enhancing clarity and accuracy in permission validation. This change ensures that only users with these roles are granted access to enterprise-only resources.
2026-06-02 02:17:50 -06:00

435 lines
11 KiB
TypeScript

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<R extends Resource> = Statements[R][number];
type Permissions = {
[R in Resource]?: Action<R>[];
};
export type PermissionCtx = {
user: { id: string };
session: { activeOrganizationId: string };
};
export type ResolvedPermissions = {
[R in Resource]: {
[A in Statements[R][number]]: boolean;
};
};
const staticRoles: Record<string, ReturnType<typeof ac.newRole>> = {
owner: ownerRole,
admin: adminRole,
member: memberRole,
};
const resolveRole = async (
roleName: string,
organizationId: string,
): Promise<ReturnType<typeof ac.newRole> | 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<string, string[]> = {};
for (const entry of customRoles) {
const parsed = JSON.parse(entry.permission) as Record<string, string[]>;
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 isPrivilegedStaticRole =
memberRecord.role === "owner" || memberRecord.role === "admin";
if (isPrivilegedStaticRole) {
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<string, boolean> | undefined)?.[
action
],
),
);
if (allGranted) {
return;
}
}
throw new TRPCError({
code: "UNAUTHORIZED",
message: result.error || "Permission denied",
});
};
export const hasPermission = async (
ctx: PermissionCtx,
permissions: Permissions,
): Promise<boolean> => {
try {
await checkPermission(ctx, permissions);
return true;
} catch {
return false;
}
};
const getLegacyOverrides = (
memberRecord: Awaited<ReturnType<typeof findMemberByUserId>>,
): Partial<Record<string, Record<string, boolean>>> => {
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<ResolvedPermissions> => {
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<string, boolean>;
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<string, boolean> | 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;
};