From aa720913165ffb924da26ef1177ea407ab6cc55f Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Tue, 30 Jun 2026 15:57:50 -0600 Subject: [PATCH] fix: allow members with git providers permission to create and delete their own (#4713) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The canAccessToGitProviders legacy override only granted read access, so members with the Git Providers toggle enabled could not add providers — the create/delete endpoints require gitProviders.create / gitProviders.delete. This mirrors how the SSH Keys toggle already grants read/create/delete. The git-provider remove endpoint now restricts non owner/admin roles to deleting only their own providers (matching the ownership model used for visibility and sharing), while owner/admin can still delete any provider in the organization. Closes #4695 --- .../permissions/check-permission.test.ts | 25 +++++++++++++++++++ .../permissions/resolve-permissions.test.ts | 18 +++++++++++++ .../server/api/routers/git-provider.ts | 14 +++++++++++ packages/server/src/services/permission.ts | 2 ++ 4 files changed, 59 insertions(+) diff --git a/apps/dokploy/__test__/permissions/check-permission.test.ts b/apps/dokploy/__test__/permissions/check-permission.test.ts index 03f47d050..b9f19d984 100644 --- a/apps/dokploy/__test__/permissions/check-permission.test.ts +++ b/apps/dokploy/__test__/permissions/check-permission.test.ts @@ -183,4 +183,29 @@ describe("legacy boolean overrides for member", () => { memberToReturn = mockMemberData("member"); await expect(checkPermission(ctx, { docker: ["read"] })).rejects.toThrow(); }); + + it("member passes gitProviders.create with canAccessToGitProviders=true", async () => { + memberToReturn = mockMemberData("member", { + canAccessToGitProviders: true, + }); + await expect( + checkPermission(ctx, { gitProviders: ["create"] }), + ).resolves.toBeUndefined(); + }); + + it("member passes gitProviders.delete with canAccessToGitProviders=true", async () => { + memberToReturn = mockMemberData("member", { + canAccessToGitProviders: true, + }); + await expect( + checkPermission(ctx, { gitProviders: ["delete"] }), + ).resolves.toBeUndefined(); + }); + + it("member fails gitProviders.create with canAccessToGitProviders=false", async () => { + memberToReturn = mockMemberData("member"); + await expect( + checkPermission(ctx, { gitProviders: ["create"] }), + ).rejects.toThrow(); + }); }); diff --git a/apps/dokploy/__test__/permissions/resolve-permissions.test.ts b/apps/dokploy/__test__/permissions/resolve-permissions.test.ts index 759c8dad8..c85bde189 100644 --- a/apps/dokploy/__test__/permissions/resolve-permissions.test.ts +++ b/apps/dokploy/__test__/permissions/resolve-permissions.test.ts @@ -143,6 +143,24 @@ describe("free-tier resources for member", () => { const perms = await resolvePermissions(ctx); expect(perms.docker.read).toBe(true); }); + + it("member gets gitProviders create/delete=false without legacy override", async () => { + memberToReturn = mockMemberData("member"); + const perms = await resolvePermissions(ctx); + expect(perms.gitProviders.read).toBe(false); + expect(perms.gitProviders.create).toBe(false); + expect(perms.gitProviders.delete).toBe(false); + }); + + it("member gets gitProviders read/create/delete=true with canAccessToGitProviders", async () => { + memberToReturn = mockMemberData("member", { + canAccessToGitProviders: true, + }); + const perms = await resolvePermissions(ctx); + expect(perms.gitProviders.read).toBe(true); + expect(perms.gitProviders.create).toBe(true); + expect(perms.gitProviders.delete).toBe(true); + }); }); describe("free-tier resources for owner", () => { diff --git a/apps/dokploy/server/api/routers/git-provider.ts b/apps/dokploy/server/api/routers/git-provider.ts index 7e2aed9b3..376b803c9 100644 --- a/apps/dokploy/server/api/routers/git-provider.ts +++ b/apps/dokploy/server/api/routers/git-provider.ts @@ -5,6 +5,7 @@ import { updateGitProvider, } from "@dokploy/server"; import { db } from "@dokploy/server/db"; +import { findMemberByUserId } from "@dokploy/server/services/permission"; import { hasValidLicense } from "@dokploy/server/services/proprietary/license-key"; import { TRPCError } from "@trpc/server"; import { desc, eq, inArray } from "drizzle-orm"; @@ -144,6 +145,19 @@ export const gitProviderRouter = createTRPCRouter({ message: "You are not allowed to delete this Git provider", }); } + + const memberRecord = await findMemberByUserId( + ctx.user.id, + ctx.session.activeOrganizationId, + ); + const isPrivileged = + memberRecord.role === "owner" || memberRecord.role === "admin"; + if (!isPrivileged && gitProvider.userId !== ctx.session.userId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You can only delete your own Git providers", + }); + } await audit(ctx, { action: "delete", resourceType: "gitProvider", diff --git a/packages/server/src/services/permission.ts b/packages/server/src/services/permission.ts index 3ce61f6ed..9de4152f5 100644 --- a/packages/server/src/services/permission.ts +++ b/packages/server/src/services/permission.ts @@ -170,6 +170,8 @@ const getLegacyOverrides = ( }, gitProviders: { read: !!memberRecord.canAccessToGitProviders, + create: !!memberRecord.canAccessToGitProviders, + delete: !!memberRecord.canAccessToGitProviders, }, }; };