fix: require ownership to change an existing GitLab source

Assigned provider access can connect new deploys, but it must not let a member reassign someone else's source or register its webhook.
This commit is contained in:
haouarihk 2026-08-20 21:57:05 +01:00
parent d1f684cbe8
commit 50b977eb77
4 changed files with 117 additions and 3 deletions

View File

@ -1,6 +1,8 @@
import {
assertCanEditExistingDeployGitSource,
canEditDeployGitSource,
getAccessibleGitProviderIds,
getConnectedGitProviderId,
} from "@dokploy/server/services/git-provider";
import { beforeEach, describe, expect, it, vi } from "vitest";
@ -367,3 +369,61 @@ describe("canEditDeployGitSource", () => {
});
});
});
describe("getConnectedGitProviderId", () => {
it("returns the gitlab gitProviderId for a gitlab source", () => {
expect(
getConnectedGitProviderId({
sourceType: "gitlab",
gitlab: { gitProviderId: "gp-gitlab" },
}),
).toBe("gp-gitlab");
});
it("returns null when there is no connected git provider", () => {
expect(getConnectedGitProviderId({ sourceType: "docker" })).toBeNull();
});
});
describe("assertCanEditExistingDeployGitSource", () => {
beforeEach(() => {
vi.clearAllMocks();
mockHasValidLicense.mockResolvedValue(true);
});
it("allows first-time connects when no git source is attached", async () => {
await expect(
assertCanEditExistingDeployGitSource(null, session(USER_MEMBER)),
).resolves.toBeUndefined();
});
it("rejects existing-source edits that only have accessedGitProviders", async () => {
mockDb.query.member.findFirst.mockResolvedValue({ role: "member" });
mockDb.query.gitProvider.findFirst.mockResolvedValue({
userId: USER_OWNER,
sharedWithOrganization: false,
});
await expect(
assertCanEditExistingDeployGitSource(
providerPrivate.gitProviderId,
session(USER_MEMBER),
),
).rejects.toMatchObject({ code: "FORBIDDEN" });
});
it("allows existing-source edits when the member owns the provider", async () => {
mockDb.query.member.findFirst.mockResolvedValue({ role: "member" });
mockDb.query.gitProvider.findFirst.mockResolvedValue({
userId: USER_MEMBER,
sharedWithOrganization: false,
});
await expect(
assertCanEditExistingDeployGitSource(
providerOwned.gitProviderId,
session(USER_MEMBER),
),
).resolves.toBeUndefined();
});
});

View File

@ -35,7 +35,11 @@ import {
writeConfigRemote,
} from "@dokploy/server";
import { db } from "@dokploy/server/db";
import { canEditDeployGitSource } from "@dokploy/server/services/git-provider";
import {
assertCanEditExistingDeployGitSource,
canEditDeployGitSource,
getConnectedGitProviderId,
} from "@dokploy/server/services/git-provider";
import {
addNewService,
checkServiceAccess,
@ -461,6 +465,11 @@ export const applicationRouter = createTRPCRouter({
message: "GitLab provider is required",
});
}
const application = await findApplicationById(input.applicationId);
await assertCanEditExistingDeployGitSource(
getConnectedGitProviderId(application),
ctx.session,
);
await assertGitlabProviderAccess(input.gitlabId, ctx.session);
await updateApplication(input.applicationId, {
gitlabRepository: input.gitlabRepository,
@ -475,7 +484,6 @@ export const applicationRouter = createTRPCRouter({
watchPaths: input.watchPaths,
enableSubmodules: input.enableSubmodules,
});
const application = await findApplicationById(input.applicationId);
if (application.autoDeploy) {
const dokployUrl = await getDokployUrl();
await registerGitlabDeployWebhook({

View File

@ -36,7 +36,11 @@ import {
updateDeploymentStatus,
} from "@dokploy/server";
import { db } from "@dokploy/server/db";
import { canEditDeployGitSource } from "@dokploy/server/services/git-provider";
import {
assertCanEditExistingDeployGitSource,
canEditDeployGitSource,
getConnectedGitProviderId,
} from "@dokploy/server/services/git-provider";
import {
addNewService,
checkServiceAccess,
@ -199,6 +203,11 @@ export const composeRouter = createTRPCRouter({
service: ["create"],
});
if (input.gitlabId) {
const compose = await findComposeById(input.composeId);
await assertCanEditExistingDeployGitSource(
getConnectedGitProviderId(compose),
ctx.session,
);
await assertGitlabProviderAccess(input.gitlabId, ctx.session);
}
const updated = await updateCompose(input.composeId, input);

View File

@ -75,6 +75,43 @@ export const canEditDeployGitSource = async (
return provider.userId === userId || provider.sharedWithOrganization;
};
export const getConnectedGitProviderId = (service: {
sourceType: string;
github?: { gitProviderId: string } | null;
gitlab?: { gitProviderId: string } | null;
bitbucket?: { gitProviderId: string } | null;
gitea?: { gitProviderId: string } | null;
}): string | null => {
switch (service.sourceType) {
case "github":
return service.github?.gitProviderId ?? null;
case "gitlab":
return service.gitlab?.gitProviderId ?? null;
case "bitbucket":
return service.bitbucket?.gitProviderId ?? null;
case "gitea":
return service.gitea?.gitProviderId ?? null;
default:
return null;
}
};
export const assertCanEditExistingDeployGitSource = async (
gitProviderId: string | null | undefined,
session: { userId: string; activeOrganizationId: string },
) => {
if (!gitProviderId) return;
const canEdit = await canEditDeployGitSource(gitProviderId, session);
if (!canEdit) {
throw new TRPCError({
code: "FORBIDDEN",
message:
"You are not authorized to change the git source of this service",
});
}
};
export const getAccessibleGitProviderIds = async (session: {
userId: string;
activeOrganizationId: string;