mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-29 02:55:22 +02:00
Compare commits
51 Commits
main
...
fix/docker
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
29f30e3386 | ||
|
|
4900204107 | ||
|
|
0f76d8f385 | ||
|
|
c968a2755e | ||
|
|
f35f3064e9 | ||
|
|
c377be0a14 | ||
|
|
e944603f99 | ||
|
|
e6fc3db08f | ||
|
|
57ef96a458 | ||
|
|
b29a87aaa8 | ||
|
|
705ca54ccc | ||
|
|
aa545ec71c | ||
|
|
51b5af55d0 | ||
|
|
28673a6166 | ||
|
|
f886010acc | ||
|
|
238bb2f6f9 | ||
|
|
1df6774ee8 | ||
|
|
35f452d25f | ||
|
|
931203a310 | ||
|
|
a3c8b3bd42 | ||
|
|
4f6e57cc9c | ||
|
|
41c09cd86b | ||
|
|
6ff2ca0173 | ||
|
|
d56a17c8ae | ||
|
|
85211afd41 | ||
|
|
9bd44512f0 | ||
|
|
ad680ae108 | ||
|
|
d7d642230c | ||
|
|
4ba0f71220 | ||
|
|
8018027330 | ||
|
|
6675aa6f37 | ||
|
|
2f43f605f3 | ||
|
|
103e2f70a8 | ||
|
|
34d38cf90e | ||
|
|
f6e6e5cc00 | ||
|
|
b06138b230 | ||
|
|
af8072d7ad | ||
|
|
6e342ee2f2 | ||
|
|
ef0cf9bd02 | ||
|
|
8d88a34a64 | ||
|
|
a50f958a6f | ||
|
|
1fdbe87d84 | ||
|
|
67278d8783 | ||
|
|
aff200f84f | ||
|
|
558d809871 | ||
|
|
f8fcf68909 | ||
|
|
7a568aadac | ||
|
|
63e33a29cc | ||
|
|
754774ea02 | ||
|
|
a714e0f83f | ||
|
|
9f10f0f4e9 |
@@ -1,369 +0,0 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
||||||
import {
|
|
||||||
canEditDeployGitSource,
|
|
||||||
getAccessibleGitProviderIds,
|
|
||||||
} from "@dokploy/server/services/git-provider";
|
|
||||||
|
|
||||||
const mockDb = vi.hoisted(() => ({
|
|
||||||
query: {
|
|
||||||
gitProvider: {
|
|
||||||
findMany: vi.fn(),
|
|
||||||
findFirst: vi.fn(),
|
|
||||||
},
|
|
||||||
member: {
|
|
||||||
findFirst: vi.fn(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("@dokploy/server/db", () => ({ db: mockDb }));
|
|
||||||
|
|
||||||
const mockHasValidLicense = vi.hoisted(() => vi.fn());
|
|
||||||
vi.mock("@dokploy/server/services/proprietary/license-key", () => ({
|
|
||||||
hasValidLicense: mockHasValidLicense,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const ORG_ID = "org-1";
|
|
||||||
const USER_OWNER = "user-owner";
|
|
||||||
const USER_ADMIN = "user-admin";
|
|
||||||
const USER_MEMBER = "user-member";
|
|
||||||
const USER_MEMBER_2 = "user-member-2";
|
|
||||||
|
|
||||||
const providerOwned = {
|
|
||||||
gitProviderId: "gp-owned",
|
|
||||||
userId: USER_MEMBER,
|
|
||||||
sharedWithOrganization: false,
|
|
||||||
};
|
|
||||||
const providerShared = {
|
|
||||||
gitProviderId: "gp-shared",
|
|
||||||
userId: USER_OWNER,
|
|
||||||
sharedWithOrganization: true,
|
|
||||||
};
|
|
||||||
const providerPrivate = {
|
|
||||||
gitProviderId: "gp-private",
|
|
||||||
userId: USER_OWNER,
|
|
||||||
sharedWithOrganization: false,
|
|
||||||
};
|
|
||||||
const providerOtherMember = {
|
|
||||||
gitProviderId: "gp-other",
|
|
||||||
userId: USER_MEMBER_2,
|
|
||||||
sharedWithOrganization: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const allProviders = [
|
|
||||||
providerOwned,
|
|
||||||
providerShared,
|
|
||||||
providerPrivate,
|
|
||||||
providerOtherMember,
|
|
||||||
];
|
|
||||||
|
|
||||||
function session(userId: string) {
|
|
||||||
return { userId, activeOrganizationId: ORG_ID };
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
vi.clearAllMocks();
|
|
||||||
mockDb.query.gitProvider.findMany.mockResolvedValue(allProviders);
|
|
||||||
mockHasValidLicense.mockResolvedValue(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("getAccessibleGitProviderIds", () => {
|
|
||||||
describe("owner", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
mockDb.query.member.findFirst.mockResolvedValue({
|
|
||||||
role: "owner",
|
|
||||||
accessedGitProviders: [],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns all org providers", async () => {
|
|
||||||
const ids = await getAccessibleGitProviderIds(session(USER_OWNER));
|
|
||||||
expect(ids).toEqual(new Set(allProviders.map((p) => p.gitProviderId)));
|
|
||||||
});
|
|
||||||
|
|
||||||
it("includes providers owned by other members", async () => {
|
|
||||||
const ids = await getAccessibleGitProviderIds(session(USER_OWNER));
|
|
||||||
expect(ids.has(providerOwned.gitProviderId)).toBe(true);
|
|
||||||
expect(ids.has(providerOtherMember.gitProviderId)).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("admin", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
mockDb.query.member.findFirst.mockResolvedValue({
|
|
||||||
role: "admin",
|
|
||||||
accessedGitProviders: [],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns all org providers", async () => {
|
|
||||||
const ids = await getAccessibleGitProviderIds(session(USER_ADMIN));
|
|
||||||
expect(ids).toEqual(new Set(allProviders.map((p) => p.gitProviderId)));
|
|
||||||
});
|
|
||||||
|
|
||||||
it("includes providers owned by other members — fixes issue #4469", async () => {
|
|
||||||
const ids = await getAccessibleGitProviderIds(session(USER_ADMIN));
|
|
||||||
expect(ids.has(providerPrivate.gitProviderId)).toBe(true);
|
|
||||||
expect(ids.has(providerOtherMember.gitProviderId)).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("member without enterprise license", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
mockDb.query.member.findFirst.mockResolvedValue({
|
|
||||||
role: "member",
|
|
||||||
accessedGitProviders: [providerPrivate.gitProviderId],
|
|
||||||
});
|
|
||||||
mockHasValidLicense.mockResolvedValue(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("can access their own provider", async () => {
|
|
||||||
const ids = await getAccessibleGitProviderIds(session(USER_MEMBER));
|
|
||||||
expect(ids.has(providerOwned.gitProviderId)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("can access shared providers", async () => {
|
|
||||||
const ids = await getAccessibleGitProviderIds(session(USER_MEMBER));
|
|
||||||
expect(ids.has(providerShared.gitProviderId)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cannot access private providers of other users even if assigned (no license)", async () => {
|
|
||||||
const ids = await getAccessibleGitProviderIds(session(USER_MEMBER));
|
|
||||||
expect(ids.has(providerPrivate.gitProviderId)).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cannot access providers of other members", async () => {
|
|
||||||
const ids = await getAccessibleGitProviderIds(session(USER_MEMBER));
|
|
||||||
expect(ids.has(providerOtherMember.gitProviderId)).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("member with enterprise license", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
mockHasValidLicense.mockResolvedValue(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("can access provider explicitly assigned to them", async () => {
|
|
||||||
mockDb.query.member.findFirst.mockResolvedValue({
|
|
||||||
role: "member",
|
|
||||||
accessedGitProviders: [providerPrivate.gitProviderId],
|
|
||||||
});
|
|
||||||
const ids = await getAccessibleGitProviderIds(session(USER_MEMBER));
|
|
||||||
expect(ids.has(providerPrivate.gitProviderId)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cannot access provider not assigned and not shared", async () => {
|
|
||||||
mockDb.query.member.findFirst.mockResolvedValue({
|
|
||||||
role: "member",
|
|
||||||
accessedGitProviders: [],
|
|
||||||
});
|
|
||||||
const ids = await getAccessibleGitProviderIds(session(USER_MEMBER));
|
|
||||||
expect(ids.has(providerPrivate.gitProviderId)).toBe(false);
|
|
||||||
expect(ids.has(providerOtherMember.gitProviderId)).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("can access shared provider even without explicit assignment", async () => {
|
|
||||||
mockDb.query.member.findFirst.mockResolvedValue({
|
|
||||||
role: "member",
|
|
||||||
accessedGitProviders: [],
|
|
||||||
});
|
|
||||||
const ids = await getAccessibleGitProviderIds(session(USER_MEMBER));
|
|
||||||
expect(ids.has(providerShared.gitProviderId)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("can access own provider regardless of assignments", async () => {
|
|
||||||
mockDb.query.member.findFirst.mockResolvedValue({
|
|
||||||
role: "member",
|
|
||||||
accessedGitProviders: [],
|
|
||||||
});
|
|
||||||
const ids = await getAccessibleGitProviderIds(session(USER_MEMBER));
|
|
||||||
expect(ids.has(providerOwned.gitProviderId)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cannot access provider of other member even with license but no assignment", async () => {
|
|
||||||
mockDb.query.member.findFirst.mockResolvedValue({
|
|
||||||
role: "member",
|
|
||||||
accessedGitProviders: [],
|
|
||||||
});
|
|
||||||
const ids = await getAccessibleGitProviderIds(session(USER_MEMBER));
|
|
||||||
expect(ids.has(providerOtherMember.gitProviderId)).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("member with no member record", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
mockDb.query.member.findFirst.mockResolvedValue(null);
|
|
||||||
mockHasValidLicense.mockResolvedValue(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("only returns own providers and shared ones", async () => {
|
|
||||||
const ids = await getAccessibleGitProviderIds(session(USER_MEMBER));
|
|
||||||
expect(ids.has(providerOwned.gitProviderId)).toBe(true);
|
|
||||||
expect(ids.has(providerShared.gitProviderId)).toBe(true);
|
|
||||||
expect(ids.has(providerPrivate.gitProviderId)).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("enterprise license — member assigned to a provider they do not own", () => {
|
|
||||||
// getAccessibleGitProviderIds still returns the provider (member can connect NEW deploys)
|
|
||||||
it("member assigned to owner's private provider can USE the provider for new deploys", async () => {
|
|
||||||
mockHasValidLicense.mockResolvedValue(true);
|
|
||||||
mockDb.query.member.findFirst.mockResolvedValue({
|
|
||||||
role: "member",
|
|
||||||
accessedGitProviders: [providerPrivate.gitProviderId],
|
|
||||||
});
|
|
||||||
const ids = await getAccessibleGitProviderIds(session(USER_MEMBER));
|
|
||||||
expect(ids.has(providerPrivate.gitProviderId)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("member NOT assigned to owner's private provider cannot use it at all", async () => {
|
|
||||||
mockHasValidLicense.mockResolvedValue(true);
|
|
||||||
mockDb.query.member.findFirst.mockResolvedValue({
|
|
||||||
role: "member",
|
|
||||||
accessedGitProviders: [],
|
|
||||||
});
|
|
||||||
const ids = await getAccessibleGitProviderIds(session(USER_MEMBER));
|
|
||||||
expect(ids.has(providerPrivate.gitProviderId)).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("empty org", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
mockDb.query.gitProvider.findMany.mockResolvedValue([]);
|
|
||||||
mockDb.query.member.findFirst.mockResolvedValue({
|
|
||||||
role: "admin",
|
|
||||||
accessedGitProviders: [],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns empty set when org has no providers", async () => {
|
|
||||||
const ids = await getAccessibleGitProviderIds(session(USER_ADMIN));
|
|
||||||
expect(ids.size).toBe(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("canEditDeployGitSource", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
vi.clearAllMocks();
|
|
||||||
mockHasValidLicense.mockResolvedValue(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("owner", () => {
|
|
||||||
it("can edit deploy using any provider", async () => {
|
|
||||||
mockDb.query.member.findFirst.mockResolvedValue({ role: "owner" });
|
|
||||||
const result = await canEditDeployGitSource(
|
|
||||||
providerPrivate.gitProviderId,
|
|
||||||
session(USER_OWNER),
|
|
||||||
);
|
|
||||||
expect(result).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("admin", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
mockDb.query.member.findFirst.mockResolvedValue({ role: "admin" });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cannot edit deploy using owner's private provider (not shared)", async () => {
|
|
||||||
mockDb.query.gitProvider.findFirst.mockResolvedValue({
|
|
||||||
userId: USER_OWNER,
|
|
||||||
sharedWithOrganization: false,
|
|
||||||
});
|
|
||||||
const result = await canEditDeployGitSource(
|
|
||||||
providerPrivate.gitProviderId,
|
|
||||||
session(USER_ADMIN),
|
|
||||||
);
|
|
||||||
expect(result).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("can edit deploy using a provider shared with the org", async () => {
|
|
||||||
mockDb.query.gitProvider.findFirst.mockResolvedValue({
|
|
||||||
userId: USER_OWNER,
|
|
||||||
sharedWithOrganization: true,
|
|
||||||
});
|
|
||||||
const result = await canEditDeployGitSource(
|
|
||||||
providerShared.gitProviderId,
|
|
||||||
session(USER_ADMIN),
|
|
||||||
);
|
|
||||||
expect(result).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("can edit deploy using their own provider", async () => {
|
|
||||||
mockDb.query.gitProvider.findFirst.mockResolvedValue({
|
|
||||||
userId: USER_ADMIN,
|
|
||||||
sharedWithOrganization: false,
|
|
||||||
});
|
|
||||||
const result = await canEditDeployGitSource(
|
|
||||||
"gp-admin-owned",
|
|
||||||
session(USER_ADMIN),
|
|
||||||
);
|
|
||||||
expect(result).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("member", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
mockDb.query.member.findFirst.mockResolvedValue({ role: "member" });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("can edit deploy using their own provider", async () => {
|
|
||||||
mockDb.query.gitProvider.findFirst.mockResolvedValue({
|
|
||||||
userId: USER_MEMBER,
|
|
||||||
sharedWithOrganization: false,
|
|
||||||
});
|
|
||||||
const result = await canEditDeployGitSource(
|
|
||||||
providerOwned.gitProviderId,
|
|
||||||
session(USER_MEMBER),
|
|
||||||
);
|
|
||||||
expect(result).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("can edit deploy using a provider shared with the org", async () => {
|
|
||||||
mockDb.query.gitProvider.findFirst.mockResolvedValue({
|
|
||||||
userId: USER_OWNER,
|
|
||||||
sharedWithOrganization: true,
|
|
||||||
});
|
|
||||||
const result = await canEditDeployGitSource(
|
|
||||||
providerShared.gitProviderId,
|
|
||||||
session(USER_MEMBER),
|
|
||||||
);
|
|
||||||
expect(result).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cannot edit deploy using owner's private provider even with enterprise license and assignment", async () => {
|
|
||||||
// This is the key case: enterprise, provider del owner, no compartido,
|
|
||||||
// member tiene accessedGitProviders asignado — pero NO puede cambiar la branch del deploy del owner
|
|
||||||
mockDb.query.gitProvider.findFirst.mockResolvedValue({
|
|
||||||
userId: USER_OWNER,
|
|
||||||
sharedWithOrganization: false,
|
|
||||||
});
|
|
||||||
const result = await canEditDeployGitSource(
|
|
||||||
providerPrivate.gitProviderId,
|
|
||||||
session(USER_MEMBER),
|
|
||||||
);
|
|
||||||
expect(result).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cannot edit deploy using another member's private provider", async () => {
|
|
||||||
mockDb.query.gitProvider.findFirst.mockResolvedValue({
|
|
||||||
userId: USER_MEMBER_2,
|
|
||||||
sharedWithOrganization: false,
|
|
||||||
});
|
|
||||||
const result = await canEditDeployGitSource(
|
|
||||||
providerOtherMember.gitProviderId,
|
|
||||||
session(USER_MEMBER),
|
|
||||||
);
|
|
||||||
expect(result).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns false if provider does not exist", async () => {
|
|
||||||
mockDb.query.gitProvider.findFirst.mockResolvedValue(null);
|
|
||||||
const result = await canEditDeployGitSource(
|
|
||||||
"nonexistent-id",
|
|
||||||
session(USER_MEMBER),
|
|
||||||
);
|
|
||||||
expect(result).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -226,8 +226,8 @@ describe("deriveCookieSecret", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("produces a 16-byte hex secret (oauth2-proxy requirement)", () => {
|
test("produces a 32-byte base64 secret (oauth2-proxy requirement)", () => {
|
||||||
const secret = deriveCookieSecret(".acme.com");
|
const secret = deriveCookieSecret(".acme.com");
|
||||||
expect(Buffer.from(secret, "hex")).toHaveLength(16);
|
expect(Buffer.from(secret, "base64")).toHaveLength(32);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -134,10 +134,15 @@ export const ShowGitProviders = () => {
|
|||||||
const canManage = gitProvider.isOwner || isOrgAdmin;
|
const canManage = gitProvider.isOwner || isOrgAdmin;
|
||||||
|
|
||||||
const haveGithubRequirements =
|
const haveGithubRequirements =
|
||||||
isGithub && gitProvider.github?.isConfigured;
|
isGithub &&
|
||||||
|
gitProvider.github?.githubPrivateKey &&
|
||||||
|
gitProvider.github?.githubAppId &&
|
||||||
|
gitProvider.github?.githubInstallationId;
|
||||||
|
|
||||||
const haveGitlabRequirements =
|
const haveGitlabRequirements =
|
||||||
isGitlab && gitProvider.gitlab?.isConfigured;
|
isGitlab &&
|
||||||
|
gitProvider.gitlab?.accessToken &&
|
||||||
|
gitProvider.gitlab?.refreshToken;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -225,7 +230,8 @@ export const ShowGitProviders = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{isBitbucket &&
|
{isBitbucket &&
|
||||||
gitProvider.bitbucket?.isDeprecated ? (
|
gitProvider.bitbucket?.appPassword &&
|
||||||
|
!gitProvider.bitbucket?.apiToken ? (
|
||||||
<Badge variant="yellow">Deprecated</Badge>
|
<Badge variant="yellow">Deprecated</Badge>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
@@ -238,7 +244,7 @@ export const ShowGitProviders = () => {
|
|||||||
Action Required
|
Action Required
|
||||||
</Badge>
|
</Badge>
|
||||||
<Link
|
<Link
|
||||||
href={`${gitProvider?.github?.githubAppName}/installations/new?state=gh_setup:${gitProvider?.github?.githubId}`}
|
href={`${gitProvider?.github?.githubAppName}/installations/new?state=gh_setup:${gitProvider?.github.githubId}`}
|
||||||
className={buttonVariants({
|
className={buttonVariants({
|
||||||
size: "icon",
|
size: "icon",
|
||||||
variant: "ghost",
|
variant: "ghost",
|
||||||
@@ -274,7 +280,7 @@ export const ShowGitProviders = () => {
|
|||||||
href={getGitlabUrl(
|
href={getGitlabUrl(
|
||||||
gitProvider.gitlab?.applicationId || "",
|
gitProvider.gitlab?.applicationId || "",
|
||||||
gitProvider.gitlab?.gitlabId || "",
|
gitProvider.gitlab?.gitlabId || "",
|
||||||
gitProvider.gitlab?.gitlabUrl || "",
|
gitProvider.gitlab?.gitlabUrl,
|
||||||
)}
|
)}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className={buttonVariants({
|
className={buttonVariants({
|
||||||
@@ -289,33 +295,29 @@ export const ShowGitProviders = () => {
|
|||||||
|
|
||||||
{canManage && (
|
{canManage && (
|
||||||
<>
|
<>
|
||||||
{isGithub &&
|
{isGithub && haveGithubRequirements && (
|
||||||
haveGithubRequirements &&
|
<EditGithubProvider
|
||||||
gitProvider.github?.githubId && (
|
githubId={gitProvider.github?.githubId}
|
||||||
<EditGithubProvider
|
/>
|
||||||
githubId={gitProvider.github.githubId}
|
)}
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isGitlab &&
|
{isGitlab && (
|
||||||
gitProvider.gitlab?.gitlabId && (
|
<EditGitlabProvider
|
||||||
<EditGitlabProvider
|
gitlabId={gitProvider.gitlab?.gitlabId}
|
||||||
gitlabId={gitProvider.gitlab.gitlabId}
|
/>
|
||||||
/>
|
)}
|
||||||
)}
|
|
||||||
|
|
||||||
{isBitbucket &&
|
{isBitbucket && (
|
||||||
gitProvider.bitbucket?.bitbucketId && (
|
<EditBitbucketProvider
|
||||||
<EditBitbucketProvider
|
bitbucketId={
|
||||||
bitbucketId={
|
gitProvider.bitbucket?.bitbucketId
|
||||||
gitProvider.bitbucket.bitbucketId
|
}
|
||||||
}
|
/>
|
||||||
/>
|
)}
|
||||||
)}
|
|
||||||
|
|
||||||
{isGitea && gitProvider.gitea?.giteaId && (
|
{isGitea && (
|
||||||
<EditGiteaProvider
|
<EditGiteaProvider
|
||||||
giteaId={gitProvider.gitea.giteaId}
|
giteaId={gitProvider.gitea?.giteaId}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
deleteAllMiddlewares,
|
deleteAllMiddlewares,
|
||||||
findApplicationById,
|
findApplicationById,
|
||||||
findEnvironmentById,
|
findEnvironmentById,
|
||||||
|
findGitProviderById,
|
||||||
findProjectById,
|
findProjectById,
|
||||||
getAccessibleServerIds,
|
getAccessibleServerIds,
|
||||||
getApplicationStats,
|
getApplicationStats,
|
||||||
@@ -30,7 +31,6 @@ import {
|
|||||||
writeConfigRemote,
|
writeConfigRemote,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { db } from "@dokploy/server/db";
|
import { db } from "@dokploy/server/db";
|
||||||
import { canEditDeployGitSource } from "@dokploy/server/services/git-provider";
|
|
||||||
import {
|
import {
|
||||||
addNewService,
|
addNewService,
|
||||||
checkServiceAccess,
|
checkServiceAccess,
|
||||||
@@ -174,11 +174,13 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
const gitProviderId = getGitProviderId();
|
const gitProviderId = getGitProviderId();
|
||||||
|
|
||||||
if (gitProviderId) {
|
if (gitProviderId) {
|
||||||
const canEdit = await canEditDeployGitSource(
|
try {
|
||||||
gitProviderId,
|
const gitProvider = await findGitProviderById(gitProviderId);
|
||||||
ctx.session,
|
if (gitProvider.userId !== ctx.session.userId) {
|
||||||
);
|
hasGitProviderAccess = false;
|
||||||
if (!canEdit) {
|
unauthorizedProvider = application.sourceType;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
hasGitProviderAccess = false;
|
hasGitProviderAccess = false;
|
||||||
unauthorizedProvider = application.sourceType;
|
unauthorizedProvider = application.sourceType;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
findComposeById,
|
findComposeById,
|
||||||
findDomainsByComposeId,
|
findDomainsByComposeId,
|
||||||
findEnvironmentById,
|
findEnvironmentById,
|
||||||
|
findGitProviderById,
|
||||||
findProjectById,
|
findProjectById,
|
||||||
findServerById,
|
findServerById,
|
||||||
getAccessibleServerIds,
|
getAccessibleServerIds,
|
||||||
@@ -33,7 +34,6 @@ import {
|
|||||||
updateDeploymentStatus,
|
updateDeploymentStatus,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { db } from "@dokploy/server/db";
|
import { db } from "@dokploy/server/db";
|
||||||
import { canEditDeployGitSource } from "@dokploy/server/services/git-provider";
|
|
||||||
import {
|
import {
|
||||||
addNewService,
|
addNewService,
|
||||||
checkServiceAccess,
|
checkServiceAccess,
|
||||||
@@ -173,11 +173,13 @@ export const composeRouter = createTRPCRouter({
|
|||||||
const gitProviderId = getGitProviderId();
|
const gitProviderId = getGitProviderId();
|
||||||
|
|
||||||
if (gitProviderId) {
|
if (gitProviderId) {
|
||||||
const canEdit = await canEditDeployGitSource(
|
try {
|
||||||
gitProviderId,
|
const gitProvider = await findGitProviderById(gitProviderId);
|
||||||
ctx.session,
|
if (gitProvider.userId !== ctx.session.userId) {
|
||||||
);
|
hasGitProviderAccess = false;
|
||||||
if (!canEdit) {
|
unauthorizedProvider = compose.sourceType;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
hasGitProviderAccess = false;
|
hasGitProviderAccess = false;
|
||||||
unauthorizedProvider = compose.sourceType;
|
unauthorizedProvider = compose.sourceType;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,43 +42,6 @@ export const gitProviderRouter = createTRPCRouter({
|
|||||||
return results.map((r) => ({
|
return results.map((r) => ({
|
||||||
...r,
|
...r,
|
||||||
isOwner: r.userId === ctx.session.userId,
|
isOwner: r.userId === ctx.session.userId,
|
||||||
github: r.github
|
|
||||||
? {
|
|
||||||
githubId: r.github.githubId,
|
|
||||||
githubAppName: r.github.githubAppName,
|
|
||||||
githubAppId: r.github.githubAppId,
|
|
||||||
githubInstallationId: r.github.githubInstallationId,
|
|
||||||
isConfigured: !!(
|
|
||||||
r.github.githubPrivateKey &&
|
|
||||||
r.github.githubAppId &&
|
|
||||||
r.github.githubInstallationId
|
|
||||||
),
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
gitlab: r.gitlab
|
|
||||||
? {
|
|
||||||
gitlabId: r.gitlab.gitlabId,
|
|
||||||
applicationId: r.gitlab.applicationId,
|
|
||||||
gitlabUrl: r.gitlab.gitlabUrl,
|
|
||||||
isConfigured: !!(r.gitlab.accessToken && r.gitlab.refreshToken),
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
bitbucket: r.bitbucket
|
|
||||||
? {
|
|
||||||
bitbucketId: r.bitbucket.bitbucketId,
|
|
||||||
bitbucketUsername: r.bitbucket.bitbucketUsername,
|
|
||||||
isConfigured: false,
|
|
||||||
isDeprecated: !!(r.bitbucket.appPassword && !r.bitbucket.apiToken),
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
gitea: r.gitea
|
|
||||||
? {
|
|
||||||
giteaId: r.gitea.giteaId,
|
|
||||||
giteaUrl: r.gitea.giteaUrl,
|
|
||||||
clientId: r.gitea.clientId,
|
|
||||||
isConfigured: !!(r.gitea.accessToken && r.gitea.refreshToken),
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
}));
|
}));
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|||||||
@@ -43,38 +43,6 @@ export const updateGitProvider = async (
|
|||||||
.then((response) => response[0]);
|
.then((response) => response[0]);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Returns true if the user can edit the git source configuration of an existing
|
|
||||||
// deploy that is connected to the given provider.
|
|
||||||
// Owner/admin: always yes.
|
|
||||||
// Member: only if they own the provider or it's shared with the org.
|
|
||||||
// Being in accessedGitProviders only grants permission to connect NEW deploys,
|
|
||||||
// not to modify the git config of an existing deploy owned by someone else.
|
|
||||||
export const canEditDeployGitSource = async (
|
|
||||||
gitProviderId: string,
|
|
||||||
session: { userId: string; activeOrganizationId: string },
|
|
||||||
): Promise<boolean> => {
|
|
||||||
const { userId, activeOrganizationId } = session;
|
|
||||||
|
|
||||||
const memberRecord = await db.query.member.findFirst({
|
|
||||||
where: and(
|
|
||||||
eq(member.userId, userId),
|
|
||||||
eq(member.organizationId, activeOrganizationId),
|
|
||||||
),
|
|
||||||
columns: { role: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (memberRecord?.role === "owner") return true;
|
|
||||||
|
|
||||||
const provider = await db.query.gitProvider.findFirst({
|
|
||||||
where: eq(gitProvider.gitProviderId, gitProviderId),
|
|
||||||
columns: { userId: true, sharedWithOrganization: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!provider) return false;
|
|
||||||
|
|
||||||
return provider.userId === userId || provider.sharedWithOrganization;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getAccessibleGitProviderIds = async (session: {
|
export const getAccessibleGitProviderIds = async (session: {
|
||||||
userId: string;
|
userId: string;
|
||||||
activeOrganizationId: string;
|
activeOrganizationId: string;
|
||||||
|
|||||||
@@ -27,16 +27,6 @@ export function safeDockerLoginCommand(
|
|||||||
return `printf %s ${escapedPassword} | docker login ${escapedRegistry} -u ${escapedUser} --password-stdin`;
|
return `printf %s ${escapedPassword} | docker login ${escapedRegistry} -u ${escapedUser} --password-stdin`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function sanitizeRegistryError(
|
|
||||||
error: unknown,
|
|
||||||
password: string | null | undefined,
|
|
||||||
): string {
|
|
||||||
const message =
|
|
||||||
error instanceof Error ? error.message : "Error with registry login";
|
|
||||||
if (!password) return message;
|
|
||||||
return message.split(password).join("***");
|
|
||||||
}
|
|
||||||
|
|
||||||
export const createRegistry = async (
|
export const createRegistry = async (
|
||||||
input: z.infer<typeof apiCreateRegistry>,
|
input: z.infer<typeof apiCreateRegistry>,
|
||||||
organizationId: string,
|
organizationId: string,
|
||||||
@@ -69,15 +59,10 @@ export const createRegistry = async (
|
|||||||
input.username,
|
input.username,
|
||||||
input.password,
|
input.password,
|
||||||
);
|
);
|
||||||
try {
|
if (input.serverId && input.serverId !== "none") {
|
||||||
if (input.serverId && input.serverId !== "none") {
|
await execAsyncRemote(input.serverId, loginCommand);
|
||||||
await execAsyncRemote(input.serverId, loginCommand);
|
} else if (newRegistry.registryType === "cloud") {
|
||||||
} else if (newRegistry.registryType === "cloud") {
|
await execAsync(loginCommand);
|
||||||
await execAsync(loginCommand);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
const sanitized = sanitizeRegistryError(error, input.password);
|
|
||||||
throw new TRPCError({ code: "BAD_REQUEST", message: sanitized });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return newRegistry;
|
return newRegistry;
|
||||||
@@ -144,24 +129,16 @@ export const updateRegistry = async (
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
if (registryData?.serverId && registryData?.serverId !== "none") {
|
||||||
if (registryData?.serverId && registryData?.serverId !== "none") {
|
await execAsyncRemote(registryData.serverId, loginCommand);
|
||||||
await execAsyncRemote(registryData.serverId, loginCommand);
|
} else if (response?.registryType === "cloud") {
|
||||||
} else if (response?.registryType === "cloud") {
|
await execAsync(loginCommand);
|
||||||
await execAsync(loginCommand);
|
|
||||||
}
|
|
||||||
} catch (execError) {
|
|
||||||
throw new Error(sanitizeRegistryError(execError, response?.password));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message =
|
const message =
|
||||||
error instanceof TRPCError
|
error instanceof Error ? error.message : "Error updating this registry";
|
||||||
? error.message
|
|
||||||
: error instanceof Error
|
|
||||||
? error.message
|
|
||||||
: "Error updating this registry";
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
message,
|
message,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { createHmac } from "node:crypto";
|
import { createHmac } from "node:crypto";
|
||||||
import type { CreateServiceOptions } from "dockerode";
|
import type { CreateServiceOptions } from "dockerode";
|
||||||
import { betterAuthSecret } from "../lib/auth-secret";
|
|
||||||
import { getRemoteDocker } from "../utils/servers/remote-docker";
|
import { getRemoteDocker } from "../utils/servers/remote-docker";
|
||||||
|
|
||||||
export const FORWARD_AUTH_SERVICE_NAME = "dokploy-forward-auth";
|
export const FORWARD_AUTH_SERVICE_NAME = "dokploy-forward-auth";
|
||||||
@@ -38,12 +37,15 @@ export const forwardAuthCallbackUrl = (
|
|||||||
): string => `${https ? "https" : "http"}://${authDomain}/oauth2/callback`;
|
): string => `${https ? "https" : "http"}://${authDomain}/oauth2/callback`;
|
||||||
|
|
||||||
export const deriveCookieSecret = (salt: string): string => {
|
export const deriveCookieSecret = (salt: string): string => {
|
||||||
// oauth2-proxy requires cookie_secret to be exactly 16, 24, or 32 bytes.
|
const rootSecret = process.env.BETTER_AUTH_SECRET;
|
||||||
// Take the first 32 hex chars (= 16 bytes) to satisfy that constraint.
|
if (!rootSecret) {
|
||||||
return createHmac("sha256", betterAuthSecret)
|
throw new Error(
|
||||||
|
"BETTER_AUTH_SECRET is required to derive the forward-auth cookie secret",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return createHmac("sha256", rootSecret)
|
||||||
.update(`forward-auth:${salt}`)
|
.update(`forward-auth:${salt}`)
|
||||||
.digest("hex")
|
.digest("base64");
|
||||||
.slice(0, 32);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const buildForwardAuthEnv = (
|
export const buildForwardAuthEnv = (
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { findAllDeploymentsByApplicationId } from "@dokploy/server/services/deployment";
|
import { findAllDeploymentsByApplicationId } from "@dokploy/server/services/deployment";
|
||||||
import {
|
import {
|
||||||
findRegistryByIdWithCredentials,
|
findRegistryByIdWithCredentials,
|
||||||
safeDockerLoginCommand,
|
|
||||||
type Registry,
|
type Registry,
|
||||||
} from "@dokploy/server/services/registry";
|
} from "@dokploy/server/services/registry";
|
||||||
import { createRollback } from "@dokploy/server/services/rollbacks";
|
import { createRollback } from "@dokploy/server/services/rollbacks";
|
||||||
@@ -118,14 +117,9 @@ const getRegistryCommands = (
|
|||||||
imageName: string,
|
imageName: string,
|
||||||
registryTag: string,
|
registryTag: string,
|
||||||
): string => {
|
): string => {
|
||||||
const loginCmd = safeDockerLoginCommand(
|
|
||||||
registry.registryUrl,
|
|
||||||
registry.username,
|
|
||||||
registry.password,
|
|
||||||
);
|
|
||||||
return `
|
return `
|
||||||
echo "📦 [Enabled Registry] Uploading image to '${registry.registryType}' | '${registryTag}'" ;
|
echo "📦 [Enabled Registry] Uploading image to '${registry.registryType}' | '${registryTag}'" ;
|
||||||
${loginCmd} || {
|
echo "${registry.password}" | docker login ${registry.registryUrl} -u '${registry.username}' --password-stdin || {
|
||||||
echo "❌ DockerHub Failed" ;
|
echo "❌ DockerHub Failed" ;
|
||||||
exit 1;
|
exit 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { safeDockerLoginCommand } from "@dokploy/server/services/registry";
|
|
||||||
import type { ApplicationNested } from "../builders";
|
import type { ApplicationNested } from "../builders";
|
||||||
|
|
||||||
export const buildRemoteDocker = async (application: ApplicationNested) => {
|
export const buildRemoteDocker = async (application: ApplicationNested) => {
|
||||||
@@ -14,7 +13,7 @@ echo "Pulling ${dockerImage}";
|
|||||||
|
|
||||||
if (username && password) {
|
if (username && password) {
|
||||||
command += `
|
command += `
|
||||||
if ! ${safeDockerLoginCommand(registryUrl || "", username, password)} 2>&1; then
|
if ! echo "${password}" | docker login --username "${username}" --password-stdin "${registryUrl || ""}" 2>&1; then
|
||||||
echo "❌ Login failed";
|
echo "❌ Login failed";
|
||||||
exit 1;
|
exit 1;
|
||||||
fi
|
fi
|
||||||
|
|||||||
Reference in New Issue
Block a user