Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 116 additions & 0 deletions apps/dokploy/__test__/git-provider/clone-token-header.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { describe, expect, it, vi } from "vitest";

vi.mock("@dokploy/server/services/github", () => ({
findGithubById: vi.fn(async () => ({
githubId: "gh-1",
githubUrl: "https://github.com",
githubAppId: 1,
githubPrivateKey: "key",
githubInstallationId: "42",
})),
}));

vi.mock("@octokit/auth-app", () => ({
createAppAuth: vi.fn(),
}));

vi.mock("octokit", () => ({
Octokit: class {
auth = async () => ({ token: "gh-token" });
},
}));

vi.mock("@dokploy/server/services/gitlab", () => ({
findGitlabById: vi.fn(async () => ({
gitlabId: "gl-1",
gitlabUrl: "https://gitlab.example.com",
gitlabInternalUrl: null,
accessToken: "gl-token",
expiresAt: Math.floor(Date.now() / 1000) + 3600,
})),
updateGitlab: vi.fn(),
}));

vi.mock("@dokploy/server/services/gitea", () => ({
findGiteaById: vi.fn(async () => ({
giteaId: "gt-1",
giteaUrl: "https://gitea.example.com:3000",
giteaInternalUrl: null,
accessToken: "gt-token",
})),
updateGitea: vi.fn(),
}));

const { cloneGithubRepository } = await import(
"@dokploy/server/utils/providers/github"
);
const { cloneGitlabRepository } = await import(
"@dokploy/server/utils/providers/gitlab"
);
const { cloneGiteaRepository } = await import(
"@dokploy/server/utils/providers/gitea"
);

const basic = (token: string) =>
Buffer.from(`oauth2:${token}`).toString("base64");

const base = { appName: "my-app", enableSubmodules: true, serverId: null };

describe("clone commands keep the token out of the URL (#5618)", () => {
it.each([
{
provider: "github",
token: "gh-token",
origin: "https://github.com",
url: "https://github.com/acme/web.git",
build: () =>
cloneGithubRepository({
...base,
owner: "acme",
repository: "web",
branch: "main",
githubId: "gh-1",
}),
},
{
provider: "gitlab",
token: "gl-token",
origin: "https://gitlab.example.com",
url: "https://gitlab.example.com/acme/web.git",
build: () =>
cloneGitlabRepository({
...base,
gitlabBranch: "main",
gitlabId: "gl-1",
gitlabPathNamespace: "acme/web",
gitlabOwner: "acme",
gitlabRepository: "web",
} as Parameters<typeof cloneGitlabRepository>[0]),
},
{
provider: "gitea",
token: "gt-token",
origin: "https://gitea.example.com:3000",
url: "https://gitea.example.com:3000/acme/web.git",
build: () =>
cloneGiteaRepository({
...base,
giteaBranch: "main",
giteaId: "gt-1",
giteaOwner: "acme",
giteaRepository: "web",
}),
},
])(
"$provider sends the token as a host-scoped header",
async ({ token, origin, url, build }) => {
const command = (await build()).replace(/\\/g, "");

expect(command).not.toContain(`${token}@`);
expect(command).toContain(
`http.${origin}/.extraHeader=Authorization: Basic ${basic(token)}`,
);
expect(command).toContain(`--recurse-submodules ${url} `);
},
);
});
20 changes: 5 additions & 15 deletions apps/dokploy/__test__/git-provider/github-clone-host.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,7 @@ describe("cloneGithubRepository host", () => {

const command = await clone();

expect(command).toContain(
"https://oauth2:gh-token@github.com/acme/web.git",
);
expect(command).toContain("https://github.com/acme/web.git");
expect(command).not.toContain("ghe.com");
});

Expand All @@ -64,9 +62,7 @@ describe("cloneGithubRepository host", () => {

const command = await clone();

expect(command).toContain(
"https://oauth2:gh-token@acme.ghe.com/acme/web.git",
);
expect(command).toContain("https://acme.ghe.com/acme/web.git");
expect(command).not.toContain("github.com");
});

Expand All @@ -77,9 +73,7 @@ describe("cloneGithubRepository host", () => {

const command = await clone();

expect(command).toContain(
"https://oauth2:gh-token@github.corp.acme.com/acme/web.git",
);
expect(command).toContain("https://github.corp.acme.com/acme/web.git");
});

it("keeps an explicit port in the clone host", async () => {
Expand All @@ -89,18 +83,14 @@ describe("cloneGithubRepository host", () => {

const command = await clone();

expect(command).toContain(
"https://oauth2:gh-token@github.acme.com:8443/acme/web.git",
);
expect(command).toContain("https://github.acme.com:8443/acme/web.git");
});

it("falls back to github.com for a provider stored before this feature", async () => {
mockFindGithubById.mockResolvedValue(provider(""));

const command = await clone();

expect(command).toContain(
"https://oauth2:gh-token@github.com/acme/web.git",
);
expect(command).toContain("https://github.com/acme/web.git");
});
});
6 changes: 6 additions & 0 deletions packages/server/src/utils/providers/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ interface CloneGitRepository {
outputPathOverride?: string;
}

// Header auth keeps the token out of build logs.
export const getGitTokenAuthConfig = (cloneUrl: string, token: string) => {
const credentials = Buffer.from(`oauth2:${token}`).toString("base64");
return `-c ${quote([`http.${new URL(cloneUrl).origin}/.extraHeader=Authorization: Basic ${credentials}`])}`;
};

export const cloneGitRepository = async ({
type = "application",
...entity
Expand Down
7 changes: 3 additions & 4 deletions packages/server/src/utils/providers/gitea.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
import type { InferResultType } from "@dokploy/server/types/with";
import { TRPCError } from "@trpc/server";
import { quote } from "shell-quote";
import { getGitTokenAuthConfig } from "./git";

export const getErrorCloneRequirements = (entity: {
giteaRepository?: string | null;
Expand Down Expand Up @@ -99,14 +100,13 @@ export const refreshGiteaToken = async (giteaProviderId: string) => {

const buildGiteaCloneUrl = (
giteaUrl: string,
accessToken: string,
owner: string,
repository: string,
) => {
const protocol = giteaUrl.startsWith("http://") ? "http" : "https";
const baseUrl = giteaUrl.replace(/^https?:\/\//, "");
const repoClone = `${owner}/${repository}.git`;
const cloneUrl = `${protocol}://oauth2:${accessToken}@${baseUrl}/${repoClone}`;
const cloneUrl = `${protocol}://${baseUrl}/${repoClone}`;
return cloneUrl;
};

Expand Down Expand Up @@ -172,13 +172,12 @@ export const cloneGiteaRepository = async ({
const repoClone = `${giteaOwner}/${giteaRepository}.git`;
const cloneUrl = buildGiteaCloneUrl(
giteaProvider.giteaInternalUrl || giteaProvider.giteaUrl,
giteaProvider.accessToken!,
giteaOwner!,
giteaRepository!,
);

command += `echo ${quote([`Cloning Repo ${repoClone} to ${outputPath}: ✅`])};`;
command += `git clone --branch ${quote([String(giteaBranch ?? "")])} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${quote([String(cloneUrl ?? "")])} ${quote([String(outputPath ?? "")])} --progress;`;
command += `git ${getGitTokenAuthConfig(cloneUrl, giteaProvider.accessToken!)} clone --branch ${quote([String(giteaBranch ?? "")])} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${quote([String(cloneUrl ?? "")])} ${quote([String(outputPath ?? "")])} --progress;`;
return command;
};

Expand Down
5 changes: 3 additions & 2 deletions packages/server/src/utils/providers/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { TRPCError } from "@trpc/server";
import { Octokit } from "octokit";
import { quote } from "shell-quote";
import type { z } from "zod";
import { getGitTokenAuthConfig } from "./git";

export const DEFAULT_GITHUB_URL = "https://github.com";
export const DEFAULT_GITHUB_API_URL = "https://api.github.com";
Expand Down Expand Up @@ -229,10 +230,10 @@ export const cloneGithubRepository = async ({
const repoclone = `${cloneBase.host}/${owner}/${repository}.git`;
command += `rm -rf ${outputPath};`;
command += `mkdir -p ${outputPath};`;
const cloneUrl = `${cloneBase.protocol}//oauth2:${token}@${repoclone}`;
const cloneUrl = `${cloneBase.protocol}//${repoclone}`;

command += `echo ${quote([`Cloning Repo ${repoclone} to ${outputPath}: ✅`])};`;
command += `git clone --branch ${quote([String(branch ?? "")])} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${quote([String(cloneUrl ?? "")])} ${quote([String(outputPath ?? "")])} --progress;`;
command += `git ${getGitTokenAuthConfig(cloneUrl, token)} clone --branch ${quote([String(branch ?? "")])} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${quote([String(cloneUrl ?? "")])} ${quote([String(outputPath ?? "")])} --progress;`;

return command;
};
Expand Down
5 changes: 3 additions & 2 deletions packages/server/src/utils/providers/gitlab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type { InferResultType } from "@dokploy/server/types/with";
import { TRPCError } from "@trpc/server";
import { quote } from "shell-quote";
import type { z } from "zod";
import { getGitTokenAuthConfig } from "./git";

export const refreshGitlabToken = async (gitlabProviderId: string) => {
const gitlabProvider = await findGitlabById(gitlabProviderId);
Expand Down Expand Up @@ -101,7 +102,7 @@ const getGitlabRepoClone = (
const getGitlabCloneUrl = (gitlab: GitlabInfo, repoClone: string) => {
const url = gitlab?.gitlabInternalUrl || gitlab?.gitlabUrl;
const isSecure = url?.startsWith("https://");
const cloneUrl = `http${isSecure ? "s" : ""}://oauth2:${gitlab?.accessToken}@${repoClone}`;
const cloneUrl = `http${isSecure ? "s" : ""}://${repoClone}`;
return cloneUrl;
};

Expand Down Expand Up @@ -155,7 +156,7 @@ export const cloneGitlabRepository = async ({
const repoClone = getGitlabRepoClone(gitlab, gitlabPathNamespace);
const cloneUrl = getGitlabCloneUrl(gitlab, repoClone);
command += `echo ${quote([`Cloning Repo ${repoClone} to ${outputPath}: ✅`])};`;
command += `git clone --branch ${quote([String(gitlabBranch ?? "")])} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${quote([String(cloneUrl ?? "")])} ${quote([String(outputPath ?? "")])} --progress;`;
command += `git ${getGitTokenAuthConfig(cloneUrl, String(gitlab?.accessToken))} clone --branch ${quote([String(gitlabBranch ?? "")])} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${quote([String(cloneUrl ?? "")])} ${quote([String(outputPath ?? "")])} --progress;`;
return command;
};

Expand Down
Loading