mirror of
https://github.com/andwati/dokploy.git
synced 2026-10-18 19:22:56 +00:00
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
149 lines
4.3 KiB
TypeScript
149 lines
4.3 KiB
TypeScript
import { db } from "@dokploy/server/db";
|
|
import { gitProvider, member } from "@dokploy/server/db/schema";
|
|
import { hasValidLicense } from "@dokploy/server/services/proprietary/license-key";
|
|
import { TRPCError } from "@trpc/server";
|
|
import { and, eq } from "drizzle-orm";
|
|
|
|
export type GitProvider = typeof gitProvider.$inferSelect;
|
|
|
|
export const removeGitProvider = async (gitProviderId: string) => {
|
|
const result = await db
|
|
.delete(gitProvider)
|
|
.where(eq(gitProvider.gitProviderId, gitProviderId))
|
|
.returning();
|
|
|
|
return result[0];
|
|
};
|
|
|
|
export const findGitProviderById = async (gitProviderId: string) => {
|
|
const result = await db.query.gitProvider.findFirst({
|
|
where: eq(gitProvider.gitProviderId, gitProviderId),
|
|
});
|
|
|
|
if (!result) {
|
|
throw new TRPCError({
|
|
code: "NOT_FOUND",
|
|
message: "Git Provider not found",
|
|
});
|
|
}
|
|
return result;
|
|
};
|
|
|
|
export const updateGitProvider = async (
|
|
gitProviderId: string,
|
|
input: Partial<GitProvider>,
|
|
) => {
|
|
return await db
|
|
.update(gitProvider)
|
|
.set({
|
|
...input,
|
|
})
|
|
.where(eq(gitProvider.gitProviderId, gitProviderId))
|
|
.returning()
|
|
.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: {
|
|
userId: string;
|
|
activeOrganizationId: string;
|
|
}): Promise<Set<string>> => {
|
|
const { userId, activeOrganizationId } = session;
|
|
|
|
const allOrgProviders = await db.query.gitProvider.findMany({
|
|
where: eq(gitProvider.organizationId, activeOrganizationId),
|
|
columns: {
|
|
gitProviderId: true,
|
|
userId: true,
|
|
sharedWithOrganization: true,
|
|
},
|
|
});
|
|
|
|
const memberRecord = await db.query.member.findFirst({
|
|
where: and(
|
|
eq(member.userId, userId),
|
|
eq(member.organizationId, activeOrganizationId),
|
|
),
|
|
columns: { accessedGitProviders: true, role: true },
|
|
});
|
|
|
|
if (memberRecord?.role === "owner" || memberRecord?.role === "admin") {
|
|
return new Set(allOrgProviders.map((p) => p.gitProviderId));
|
|
}
|
|
|
|
const licensed = await hasValidLicense(activeOrganizationId);
|
|
const assignedSet = licensed
|
|
? new Set(memberRecord?.accessedGitProviders ?? [])
|
|
: new Set<string>();
|
|
|
|
const result = new Set<string>();
|
|
for (const p of allOrgProviders) {
|
|
if (
|
|
p.userId === userId ||
|
|
p.sharedWithOrganization ||
|
|
assignedSet.has(p.gitProviderId)
|
|
) {
|
|
result.add(p.gitProviderId);
|
|
}
|
|
}
|
|
return result;
|
|
};
|
|
|
|
/**
|
|
* Authorizes read access to a specific git provider for the current session.
|
|
* Throws if the provider belongs to a different organization (cross-org IDOR)
|
|
* or if the caller is not entitled to it within the active organization.
|
|
* Must be called before returning any git-provider record that carries secrets
|
|
* (OAuth tokens, app private keys, webhook 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",
|
|
});
|
|
}
|
|
};
|