refactor: enhance access control in environment, mount, port, rollback, and schedule routers to ensure users can only interact with resources belonging to their organization

This commit is contained in:
Mauricio Siu
2025-09-04 23:32:25 -06:00
parent d199a54033
commit 47b66d0dc3
8 changed files with 305 additions and 38 deletions
+56 -5
View File
@@ -39,9 +39,18 @@ export const environmentRouter = createTRPCRouter({
one: protectedProcedure one: protectedProcedure
.input(apiFindOneEnvironment) .input(apiFindOneEnvironment)
.query(async ({ input }) => { .query(async ({ input, ctx }) => {
try { try {
const environment = await findEnvironmentById(input.environmentId); const environment = await findEnvironmentById(input.environmentId);
if (
environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "FORBIDDEN",
message: "You are not allowed to access this environment",
});
}
return environment; return environment;
} catch (error) { } catch (error) {
throw new TRPCError({ throw new TRPCError({
@@ -53,9 +62,21 @@ export const environmentRouter = createTRPCRouter({
byProjectId: protectedProcedure byProjectId: protectedProcedure
.input(z.object({ projectId: z.string() })) .input(z.object({ projectId: z.string() }))
.query(async ({ input }) => { .query(async ({ input, ctx }) => {
try { try {
const environments = await findEnvironmentsByProjectId(input.projectId); const environments = await findEnvironmentsByProjectId(input.projectId);
if (
environments.some(
(environment) =>
environment.project.organizationId !==
ctx.session.activeOrganizationId,
)
) {
throw new TRPCError({
code: "FORBIDDEN",
message: "You are not allowed to access this environment",
});
}
return environments; return environments;
} catch (error) { } catch (error) {
throw new TRPCError({ throw new TRPCError({
@@ -67,8 +88,18 @@ export const environmentRouter = createTRPCRouter({
remove: protectedProcedure remove: protectedProcedure
.input(apiRemoveEnvironment) .input(apiRemoveEnvironment)
.mutation(async ({ input }) => { .mutation(async ({ input, ctx }) => {
try { try {
const environment = await findEnvironmentById(input.environmentId);
if (
environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "FORBIDDEN",
message: "You are not allowed to access this environment",
});
}
const deletedEnvironment = await deleteEnvironment(input.environmentId); const deletedEnvironment = await deleteEnvironment(input.environmentId);
return deletedEnvironment; return deletedEnvironment;
} catch (error) { } catch (error) {
@@ -81,9 +112,19 @@ export const environmentRouter = createTRPCRouter({
update: protectedProcedure update: protectedProcedure
.input(apiUpdateEnvironment) .input(apiUpdateEnvironment)
.mutation(async ({ input }) => { .mutation(async ({ input, ctx }) => {
try { try {
const { environmentId, ...updateData } = input; const { environmentId, ...updateData } = input;
const currentEnvironment = await findEnvironmentById(environmentId);
if (
currentEnvironment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "FORBIDDEN",
message: "You are not allowed to access this environment",
});
}
const environment = await updateEnvironmentById( const environment = await updateEnvironmentById(
environmentId, environmentId,
updateData, updateData,
@@ -99,8 +140,18 @@ export const environmentRouter = createTRPCRouter({
duplicate: protectedProcedure duplicate: protectedProcedure
.input(apiDuplicateEnvironment) .input(apiDuplicateEnvironment)
.mutation(async ({ input }) => { .mutation(async ({ input, ctx }) => {
try { try {
const environment = await findEnvironmentById(input.environmentId);
if (
environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "FORBIDDEN",
message: "You are not allowed to access this environment",
});
}
const duplicatedEnvironment = await duplicateEnvironment(input); const duplicatedEnvironment = await duplicateEnvironment(input);
return duplicatedEnvironment; return duplicatedEnvironment;
} catch (error) { } catch (error) {
+30 -5
View File
@@ -3,9 +3,11 @@ import {
deleteMount, deleteMount,
findApplicationById, findApplicationById,
findMountById, findMountById,
findMountOrganizationId,
getServiceContainer, getServiceContainer,
updateMount, updateMount,
} from "@dokploy/server"; } from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { z } from "zod"; import { z } from "zod";
import { import {
apiCreateMount, apiCreateMount,
@@ -24,16 +26,39 @@ export const mountRouter = createTRPCRouter({
}), }),
remove: protectedProcedure remove: protectedProcedure
.input(apiRemoveMount) .input(apiRemoveMount)
.mutation(async ({ input }) => { .mutation(async ({ input, ctx }) => {
const organizationId = await findMountOrganizationId(input.mountId);
if (organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to delete this mount",
});
}
return await deleteMount(input.mountId); return await deleteMount(input.mountId);
}), }),
one: protectedProcedure.input(apiFindOneMount).query(async ({ input }) => { one: protectedProcedure
return await findMountById(input.mountId); .input(apiFindOneMount)
}), .query(async ({ input, ctx }) => {
const organizationId = await findMountOrganizationId(input.mountId);
if (organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this mount",
});
}
return await findMountById(input.mountId);
}),
update: protectedProcedure update: protectedProcedure
.input(apiUpdateMount) .input(apiUpdateMount)
.mutation(async ({ input }) => { .mutation(async ({ input, ctx }) => {
const organizationId = await findMountOrganizationId(input.mountId);
if (organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to update this mount",
});
}
return await updateMount(input.mountId, input); return await updateMount(input.mountId, input);
}), }),
allNamedByApplicationId: protectedProcedure allNamedByApplicationId: protectedProcedure
+47 -15
View File
@@ -27,22 +27,44 @@ export const portRouter = createTRPCRouter({
}); });
} }
}), }),
one: protectedProcedure.input(apiFindOnePort).query(async ({ input }) => { one: protectedProcedure
try { .input(apiFindOnePort)
return await finPortById(input.portId); .query(async ({ input, ctx }) => {
} catch (error) { try {
throw new TRPCError({ const port = await finPortById(input.portId);
code: "BAD_REQUEST", if (
message: "Port not found", port.application.environment.project.organizationId !==
cause: error, ctx.session.activeOrganizationId
}); ) {
} throw new TRPCError({
}), code: "UNAUTHORIZED",
message: "You are not authorized to access this port",
});
}
return port;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Port not found",
cause: error,
});
}
}),
delete: protectedProcedure delete: protectedProcedure
.input(apiFindOnePort) .input(apiFindOnePort)
.mutation(async ({ input }) => { .mutation(async ({ input, ctx }) => {
const port = await finPortById(input.portId);
if (
port.application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to delete this port",
});
}
try { try {
return removePortById(input.portId); return await removePortById(input.portId);
} catch (error) { } catch (error) {
const message = const message =
error instanceof Error ? error.message : "Error input: Deleting port"; error instanceof Error ? error.message : "Error input: Deleting port";
@@ -54,9 +76,19 @@ export const portRouter = createTRPCRouter({
}), }),
update: protectedProcedure update: protectedProcedure
.input(apiUpdatePort) .input(apiUpdatePort)
.mutation(async ({ input }) => { .mutation(async ({ input, ctx }) => {
const port = await finPortById(input.portId);
if (
port.application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to update this port",
});
}
try { try {
return updatePortById(input.portId, input); return await updatePortById(input.portId, input);
} catch (error) { } catch (error) {
const message = const message =
error instanceof Error ? error.message : "Error updating the port"; error instanceof Error ? error.message : "Error updating the port";
+16 -2
View File
@@ -1,4 +1,8 @@
import { removeRollbackById, rollback } from "@dokploy/server"; import {
findRollbackById,
removeRollbackById,
rollback,
} from "@dokploy/server";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc"; import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import { apiFindOneRollback } from "@/server/db/schema"; import { apiFindOneRollback } from "@/server/db/schema";
@@ -22,8 +26,18 @@ export const rollbackRouter = createTRPCRouter({
}), }),
rollback: protectedProcedure rollback: protectedProcedure
.input(apiFindOneRollback) .input(apiFindOneRollback)
.mutation(async ({ input }) => { .mutation(async ({ input, ctx }) => {
try { try {
const currentRollback = await findRollbackById(input.rollbackId);
if (
currentRollback?.deployment?.application?.environment?.project
.organizationId !== ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to rollback this deployment",
});
}
return await rollback(input.rollbackId); return await rollback(input.rollbackId);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
+91 -7
View File
@@ -105,13 +105,69 @@ export const findMountById = async (mountId: string) => {
const mount = await db.query.mounts.findFirst({ const mount = await db.query.mounts.findFirst({
where: eq(mounts.mountId, mountId), where: eq(mounts.mountId, mountId),
with: { with: {
application: true, application: {
postgres: true, with: {
mariadb: true, environment: {
mongo: true, with: {
mysql: true, project: true,
redis: true, },
compose: true, },
},
},
postgres: {
with: {
environment: {
with: {
project: true,
},
},
},
},
mariadb: {
with: {
environment: {
with: {
project: true,
},
},
},
},
mongo: {
with: {
environment: {
with: {
project: true,
},
},
},
},
mysql: {
with: {
environment: {
with: {
project: true,
},
},
},
},
redis: {
with: {
environment: {
with: {
project: true,
},
},
},
},
compose: {
with: {
environment: {
with: {
project: true,
},
},
},
},
}, },
}); });
if (!mount) { if (!mount) {
@@ -123,6 +179,34 @@ export const findMountById = async (mountId: string) => {
return mount; return mount;
}; };
export const findMountOrganizationId = async (mountId: string) => {
const mount = await findMountById(mountId);
if (mount.application) {
return mount.application.environment.project.organizationId;
}
if (mount.postgres) {
return mount.postgres.environment.project.organizationId;
}
if (mount.mariadb) {
return mount.mariadb.environment.project.organizationId;
}
if (mount.mongo) {
return mount.mongo.environment.project.organizationId;
}
if (mount.mysql) {
return mount.mysql.environment.project.organizationId;
}
if (mount.redis) {
return mount.redis.environment.project.organizationId;
}
if (mount.compose) {
return mount.compose.environment.project.organizationId;
}
return null;
};
export const updateMount = async ( export const updateMount = async (
mountId: string, mountId: string,
mountData: Partial<Mount>, mountData: Partial<Mount>,
+11
View File
@@ -27,6 +27,17 @@ export const createPort = async (input: typeof apiCreatePort._type) => {
export const finPortById = async (portId: string) => { export const finPortById = async (portId: string) => {
const result = await db.query.ports.findFirst({ const result = await db.query.ports.findFirst({
where: eq(ports.portId, portId), where: eq(ports.portId, portId),
with: {
application: {
with: {
environment: {
with: {
project: true,
},
},
},
},
},
}); });
if (!result) { if (!result) {
throw new TRPCError({ throw new TRPCError({
+16 -1
View File
@@ -76,9 +76,24 @@ export const createRollback = async (
}); });
}; };
const findRollbackById = async (rollbackId: string) => { export const findRollbackById = async (rollbackId: string) => {
const result = await db.query.rollbacks.findFirst({ const result = await db.query.rollbacks.findFirst({
where: eq(rollbacks.rollbackId, rollbackId), where: eq(rollbacks.rollbackId, rollbackId),
with: {
deployment: {
with: {
application: {
with: {
environment: {
with: {
project: true,
},
},
},
},
},
},
},
}); });
if (!result) { if (!result) {
+38 -3
View File
@@ -35,9 +35,29 @@ export const findScheduleById = async (scheduleId: string) => {
const schedule = await db.query.schedules.findFirst({ const schedule = await db.query.schedules.findFirst({
where: eq(schedules.scheduleId, scheduleId), where: eq(schedules.scheduleId, scheduleId),
with: { with: {
application: true, application: {
compose: true, with: {
server: true, environment: {
with: {
project: true,
},
},
},
},
compose: {
with: {
environment: {
with: {
project: true,
},
},
},
},
server: {
with: {
organization: true,
},
},
}, },
}); });
@@ -50,6 +70,21 @@ export const findScheduleById = async (scheduleId: string) => {
return schedule; return schedule;
}; };
export const findScheduleOrganizationId = async (scheduleId: string) => {
const schedule = await findScheduleById(scheduleId);
if (schedule?.application) {
return schedule?.application?.environment?.project?.organizationId;
}
if (schedule?.compose) {
return schedule?.compose?.environment?.project?.organizationId;
}
if (schedule?.server) {
return schedule?.server?.organization?.id;
}
return null;
};
export const deleteSchedule = async (scheduleId: string) => { export const deleteSchedule = async (scheduleId: string) => {
const schedule = await findScheduleById(scheduleId); const schedule = await findScheduleById(scheduleId);
const serverId = const serverId =