feat: add comprehensive permission tests and enhance permission checks in components

- Introduced new test files for permission checks, including `check-permission.test.ts`, `enterprise-only-resources.test.ts`, `resolve-permissions.test.ts`, and `service-access.test.ts`.
- Implemented permission checks in various components to ensure actions are gated by user permissions, including `ShowTraefikConfig`, `UpdateTraefikConfig`, `ShowVolumes`, `ShowDomains`, and others.
- Enhanced the logic for displaying UI elements based on user permissions, ensuring that only authorized users can access or modify resources.
This commit is contained in:
Mauricio Siu
2026-03-15 16:42:48 -06:00
parent 8eace173b9
commit 8127dc4536
118 changed files with 25020 additions and 5430 deletions

View File

@@ -1,13 +1,10 @@
import {
addNewService,
checkPortInUse,
checkServiceAccess,
createMount,
createPostgres,
deployPostgres,
findBackupsByDbId,
findEnvironmentById,
findMemberById,
findPostgresById,
findProjectById,
getMountPath,
@@ -21,10 +18,17 @@ import {
stopServiceRemote,
updatePostgresById,
} from "@dokploy/server";
import {
addNewService,
checkServiceAccess,
checkServicePermissionAndAccess,
findMemberByUserId,
} from "@dokploy/server/services/permission";
import { db } from "@dokploy/server/db";
import { TRPCError } from "@trpc/server";
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
import { z } from "zod";
import { audit } from "@/server/api/utils/audit";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import {
apiChangePostgresStatus,
@@ -46,18 +50,10 @@ export const postgresRouter = createTRPCRouter({
.input(apiCreatePostgres)
.mutation(async ({ input, ctx }) => {
try {
// Get project from environment
const environment = await findEnvironmentById(input.environmentId);
const project = await findProjectById(environment.projectId);
if (ctx.user.role === "member") {
await checkServiceAccess(
ctx.user.id,
project.projectId,
ctx.session.activeOrganizationId,
"create",
);
}
await checkServiceAccess(ctx, project.projectId, "create");
if (IS_CLOUD && !input.serverId) {
throw new TRPCError({
@@ -75,13 +71,7 @@ export const postgresRouter = createTRPCRouter({
const newPostgres = await createPostgres({
...input,
});
if (ctx.user.role === "member") {
await addNewService(
ctx.user.id,
newPostgres.postgresId,
project.organizationId,
);
}
await addNewService(ctx, newPostgres.postgresId);
const mountPath = getMountPath(input.dockerImage);
@@ -93,6 +83,12 @@ export const postgresRouter = createTRPCRouter({
type: "volume",
});
await audit(ctx, {
action: "create",
resourceType: "service",
resourceId: newPostgres.postgresId,
resourceName: newPostgres.appName,
});
return newPostgres;
} catch (error) {
if (error instanceof TRPCError) {
@@ -108,14 +104,7 @@ export const postgresRouter = createTRPCRouter({
one: protectedProcedure
.input(apiFindOnePostgres)
.query(async ({ input, ctx }) => {
if (ctx.user.role === "member") {
await checkServiceAccess(
ctx.user.id,
input.postgresId,
ctx.session.activeOrganizationId,
"access",
);
}
await checkServiceAccess(ctx, input.postgresId, "read");
const postgres = await findPostgresById(input.postgresId);
if (
@@ -133,18 +122,11 @@ export const postgresRouter = createTRPCRouter({
start: protectedProcedure
.input(apiFindOnePostgres)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.postgresId, {
deployment: ["create"],
});
const service = await findPostgresById(input.postgresId);
if (
service.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to start this Postgres",
});
}
if (service.serverId) {
await startServiceRemote(service.serverId, service.appName);
} else {
@@ -154,21 +136,21 @@ export const postgresRouter = createTRPCRouter({
applicationStatus: "done",
});
await audit(ctx, {
action: "start",
resourceType: "service",
resourceId: service.postgresId,
resourceName: service.appName,
});
return service;
}),
stop: protectedProcedure
.input(apiFindOnePostgres)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.postgresId, {
deployment: ["create"],
});
const postgres = await findPostgresById(input.postgresId);
if (
postgres.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to stop this Postgres",
});
}
if (postgres.serverId) {
await stopServiceRemote(postgres.serverId, postgres.appName);
} else {
@@ -178,23 +160,22 @@ export const postgresRouter = createTRPCRouter({
applicationStatus: "idle",
});
await audit(ctx, {
action: "stop",
resourceType: "service",
resourceId: postgres.postgresId,
resourceName: postgres.appName,
});
return postgres;
}),
saveExternalPort: protectedProcedure
.input(apiSaveExternalPortPostgres)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.postgresId, {
service: ["create"],
});
const postgres = await findPostgresById(input.postgresId);
if (
postgres.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to save this external port",
});
}
if (input.externalPort) {
const portCheck = await checkPortInUse(
input.externalPort,
@@ -212,21 +193,27 @@ export const postgresRouter = createTRPCRouter({
externalPort: input.externalPort,
});
await deployPostgres(input.postgresId);
await audit(ctx, {
action: "update",
resourceType: "service",
resourceId: postgres.postgresId,
resourceName: postgres.appName,
});
return postgres;
}),
deploy: protectedProcedure
.input(apiDeployPostgres)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.postgresId, {
deployment: ["create"],
});
const postgres = await findPostgresById(input.postgresId);
if (
postgres.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to deploy this Postgres",
});
}
await audit(ctx, {
action: "deploy",
resourceType: "service",
resourceId: postgres.postgresId,
resourceName: postgres.appName,
});
return deployPostgres(input.postgresId);
}),
@@ -241,17 +228,9 @@ export const postgresRouter = createTRPCRouter({
})
.input(apiDeployPostgres)
.subscription(async function* ({ input, ctx, signal }) {
const postgres = await findPostgresById(input.postgresId);
if (
postgres.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to deploy this Postgres",
});
}
await checkServicePermissionAndAccess(ctx, input.postgresId, {
deployment: ["create"],
});
const queue: string[] = [];
const done = false;
@@ -276,32 +255,25 @@ export const postgresRouter = createTRPCRouter({
changeStatus: protectedProcedure
.input(apiChangePostgresStatus)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.postgresId, {
deployment: ["create"],
});
const postgres = await findPostgresById(input.postgresId);
if (
postgres.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to change this Postgres status",
});
}
await updatePostgresById(input.postgresId, {
applicationStatus: input.applicationStatus,
});
await audit(ctx, {
action: "update",
resourceType: "service",
resourceId: postgres.postgresId,
resourceName: postgres.appName,
});
return postgres;
}),
remove: protectedProcedure
.input(apiFindOnePostgres)
.mutation(async ({ input, ctx }) => {
if (ctx.user.role === "member") {
await checkServiceAccess(
ctx.user.id,
input.postgresId,
ctx.session.activeOrganizationId,
"delete",
);
}
await checkServiceAccess(ctx, input.postgresId, "delete");
const postgres = await findPostgresById(input.postgresId);
if (
@@ -314,6 +286,12 @@ export const postgresRouter = createTRPCRouter({
});
}
await audit(ctx, {
action: "delete",
resourceType: "service",
resourceId: postgres.postgresId,
resourceName: postgres.appName,
});
const backups = await findBackupsByDbId(input.postgresId, "postgres");
const cleanupOperations = [
@@ -333,16 +311,9 @@ export const postgresRouter = createTRPCRouter({
saveEnvironment: protectedProcedure
.input(apiSaveEnvironmentVariablesPostgres)
.mutation(async ({ input, ctx }) => {
const postgres = await findPostgresById(input.postgresId);
if (
postgres.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to save this environment",
});
}
await checkServicePermissionAndAccess(ctx, input.postgresId, {
envVars: ["write"],
});
const service = await updatePostgresById(input.postgresId, {
env: input.env,
});
@@ -354,21 +325,20 @@ export const postgresRouter = createTRPCRouter({
});
}
await audit(ctx, {
action: "update",
resourceType: "service",
resourceId: input.postgresId,
});
return true;
}),
reload: protectedProcedure
.input(apiResetPostgres)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.postgresId, {
deployment: ["create"],
});
const postgres = await findPostgresById(input.postgresId);
if (
postgres.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to reload this Postgres",
});
}
if (postgres.serverId) {
await stopServiceRemote(postgres.serverId, postgres.appName);
} else {
@@ -386,22 +356,21 @@ export const postgresRouter = createTRPCRouter({
await updatePostgresById(input.postgresId, {
applicationStatus: "done",
});
await audit(ctx, {
action: "reload",
resourceType: "service",
resourceId: postgres.postgresId,
resourceName: postgres.appName,
});
return true;
}),
update: protectedProcedure
.input(apiUpdatePostgres)
.mutation(async ({ input, ctx }) => {
const { postgresId, ...rest } = input;
const postgres = await findPostgresById(postgresId);
if (
postgres.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to update this Postgres",
});
}
await checkServicePermissionAndAccess(ctx, postgresId, {
service: ["create"],
});
const service = await updatePostgresById(postgresId, {
...rest,
@@ -414,6 +383,12 @@ export const postgresRouter = createTRPCRouter({
});
}
await audit(ctx, {
action: "update",
resourceType: "service",
resourceId: postgresId,
resourceName: service.appName,
});
return true;
}),
move: protectedProcedure
@@ -424,31 +399,10 @@ export const postgresRouter = createTRPCRouter({
}),
)
.mutation(async ({ input, ctx }) => {
const postgres = await findPostgresById(input.postgresId);
if (
postgres.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move this postgres",
});
}
await checkServicePermissionAndAccess(ctx, input.postgresId, {
service: ["create"],
});
const targetEnvironment = await findEnvironmentById(
input.targetEnvironmentId,
);
if (
targetEnvironment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move to this environment",
});
}
// Update the postgres's projectId
const updatedPostgres = await db
.update(postgresTable)
.set({
@@ -465,24 +419,28 @@ export const postgresRouter = createTRPCRouter({
});
}
await audit(ctx, {
action: "move",
resourceType: "service",
resourceId: updatedPostgres.postgresId,
resourceName: updatedPostgres.appName,
});
return updatedPostgres;
}),
rebuild: protectedProcedure
.input(apiRebuildPostgres)
.mutation(async ({ input, ctx }) => {
const postgres = await findPostgresById(input.postgresId);
if (
postgres.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to rebuild this Postgres database",
});
}
await checkServicePermissionAndAccess(ctx, input.postgresId, {
deployment: ["create"],
});
await rebuildDatabase(postgres.postgresId, "postgres");
await rebuildDatabase(input.postgresId, "postgres");
await audit(ctx, {
action: "rebuild",
resourceType: "service",
resourceId: input.postgresId,
});
return true;
}),
search: protectedProcedure
@@ -538,19 +496,18 @@ export const postgresRouter = createTRPCRouter({
),
);
}
if (ctx.user.role === "member") {
const { accessedServices } = await findMemberById(
ctx.user.id,
ctx.session.activeOrganizationId,
);
if (accessedServices.length === 0) return { items: [], total: 0 };
baseConditions.push(
sql`${postgresTable.postgresId} IN (${sql.join(
accessedServices.map((id) => sql`${id}`),
sql`, `,
)})`,
);
}
const { accessedServices } = await findMemberByUserId(
ctx.user.id,
ctx.session.activeOrganizationId,
);
if (accessedServices.length === 0) return { items: [], total: 0 };
baseConditions.push(
sql`${postgresTable.postgresId} IN (${sql.join(
accessedServices.map((id) => sql`${id}`),
sql`, `,
)})`,
);
const where = and(...baseConditions);
const [items, countResult] = await Promise.all([
db