mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-03 04:55:23 +02:00
- Introduced a new SQL migration file `0171_lucky_echo.sql` to modify the foreign key constraint on the `sso_provider` table, changing the `ON DELETE` behavior from `cascade` to `set null`. - Updated the journal to include the new migration version and its associated tag. - Added a snapshot file for version 7 of the database schema, reflecting the current state of the `sso_provider` and other related tables. These changes enhance the integrity of the database by ensuring that user references are set to null instead of being deleted when the referenced user is removed.
383 lines
10 KiB
TypeScript
383 lines
10 KiB
TypeScript
import { IS_CLOUD } from "@dokploy/server/constants";
|
|
import { db } from "@dokploy/server/db";
|
|
import {
|
|
forwardAuthSettings,
|
|
server,
|
|
ssoProvider,
|
|
} from "@dokploy/server/db/schema";
|
|
import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission";
|
|
import {
|
|
deriveBaseDomain,
|
|
deriveCookieSecret,
|
|
type ForwardAuthOidcConfig,
|
|
forwardAuthCallbackUrl,
|
|
isForwardAuthRunning,
|
|
removeForwardAuth,
|
|
setupForwardAuth,
|
|
} from "@dokploy/server/setup/forward-auth-setup";
|
|
import { manageDomain } from "@dokploy/server/utils/traefik/domain";
|
|
import {
|
|
manageForwardAuthDomain,
|
|
removeForwardAuthDomain,
|
|
removeForwardAuthMiddleware,
|
|
} from "@dokploy/server/utils/traefik/forward-auth";
|
|
import { TRPCError } from "@trpc/server";
|
|
import { and, asc, desc, eq, isNotNull, isNull } from "drizzle-orm";
|
|
import { findApplicationById } from "../application";
|
|
import { findDomainById, updateDomainById } from "../domain";
|
|
|
|
const resolveOidcConfig = (provider: {
|
|
issuer: string;
|
|
oidcConfig: string | null;
|
|
}): ForwardAuthOidcConfig => {
|
|
if (!provider.oidcConfig) {
|
|
throw new TRPCError({
|
|
code: "BAD_REQUEST",
|
|
message:
|
|
"Forward-auth requires an OIDC provider — SAML is not supported.",
|
|
});
|
|
}
|
|
|
|
let parsed: any;
|
|
try {
|
|
parsed = JSON.parse(provider.oidcConfig);
|
|
} catch {
|
|
throw new TRPCError({
|
|
code: "INTERNAL_SERVER_ERROR",
|
|
message: "Failed to parse the SSO provider OIDC configuration",
|
|
});
|
|
}
|
|
|
|
if (!parsed?.clientId || !parsed?.clientSecret) {
|
|
throw new TRPCError({
|
|
code: "BAD_REQUEST",
|
|
message: "SSO provider OIDC config is missing clientId/clientSecret",
|
|
});
|
|
}
|
|
|
|
return {
|
|
clientId: parsed.clientId,
|
|
clientSecret: parsed.clientSecret,
|
|
issuer: provider.issuer,
|
|
scopes: parsed.scopes,
|
|
skipDiscovery: parsed.skipDiscovery,
|
|
};
|
|
};
|
|
|
|
const findProviderForOrg = async (
|
|
providerId: string,
|
|
organizationId: string,
|
|
) => {
|
|
const provider = await db.query.ssoProvider.findFirst({
|
|
where: and(
|
|
eq(ssoProvider.providerId, providerId),
|
|
eq(ssoProvider.organizationId, organizationId),
|
|
),
|
|
columns: { providerId: true, issuer: true, oidcConfig: true },
|
|
});
|
|
if (!provider) {
|
|
throw new TRPCError({
|
|
code: "NOT_FOUND",
|
|
message: "SSO provider not found",
|
|
});
|
|
}
|
|
return provider;
|
|
};
|
|
|
|
export const listSsoProvidersForOrg = async (organizationId: string) => {
|
|
return db.query.ssoProvider.findMany({
|
|
where: and(
|
|
eq(ssoProvider.organizationId, organizationId),
|
|
isNotNull(ssoProvider.oidcConfig),
|
|
),
|
|
columns: { providerId: true, issuer: true, domain: true },
|
|
orderBy: [asc(ssoProvider.createdAt)],
|
|
});
|
|
};
|
|
|
|
export const getDomainSsoStatus = async (
|
|
ctx: { session: { activeOrganizationId: string } },
|
|
domainId: string,
|
|
) => {
|
|
const domain = await findDomainById(domainId);
|
|
if (domain.applicationId) {
|
|
await checkServicePermissionAndAccess(ctx as any, domain.applicationId, {
|
|
domain: ["read"],
|
|
});
|
|
}
|
|
return { enabled: !!domain.forwardAuthEnabled };
|
|
};
|
|
|
|
const settingsWhere = (serverId: string | null) =>
|
|
serverId
|
|
? eq(forwardAuthSettings.serverId, serverId)
|
|
: isNull(forwardAuthSettings.serverId);
|
|
|
|
export const getForwardAuthSettings = async (serverId: string | null) => {
|
|
return db.query.forwardAuthSettings.findFirst({
|
|
where: settingsWhere(serverId),
|
|
});
|
|
};
|
|
|
|
export const setForwardAuthSettings = async (input: {
|
|
organizationId: string;
|
|
serverId: string | null;
|
|
authDomain: string;
|
|
https: boolean;
|
|
certificateType: "none" | "letsencrypt" | "custom";
|
|
customCertResolver?: string | null;
|
|
}) => {
|
|
const baseDomain = deriveBaseDomain(input.authDomain);
|
|
const existing = await getForwardAuthSettings(input.serverId);
|
|
|
|
const values = {
|
|
authDomain: input.authDomain,
|
|
baseDomain,
|
|
https: input.https,
|
|
certificateType: input.certificateType,
|
|
customCertResolver: input.customCertResolver ?? null,
|
|
};
|
|
|
|
if (existing) {
|
|
await db
|
|
.update(forwardAuthSettings)
|
|
.set(values)
|
|
.where(settingsWhere(input.serverId));
|
|
} else {
|
|
await db.insert(forwardAuthSettings).values({
|
|
...values,
|
|
serverId: input.serverId,
|
|
});
|
|
}
|
|
|
|
await manageForwardAuthDomain(input.serverId, {
|
|
authDomain: input.authDomain,
|
|
https: input.https,
|
|
certificateType: input.certificateType,
|
|
customCertResolver: input.customCertResolver,
|
|
});
|
|
|
|
if (existing?.providerId) {
|
|
const proxyRunning = await isForwardAuthRunning(
|
|
input.serverId ?? undefined,
|
|
);
|
|
if (proxyRunning) {
|
|
await deployForwardAuthOnServer({
|
|
serverId: input.serverId ?? undefined,
|
|
providerId: existing.providerId,
|
|
organizationId: input.organizationId,
|
|
});
|
|
}
|
|
}
|
|
|
|
return { callbackUrl: forwardAuthCallbackUrl(input.authDomain, input.https) };
|
|
};
|
|
|
|
export const removeForwardAuthSettings = async (serverId: string | null) => {
|
|
const existing = await getForwardAuthSettings(serverId);
|
|
if (!existing) return { ok: true } as const;
|
|
await removeForwardAuthDomain(serverId);
|
|
await db.delete(forwardAuthSettings).where(settingsWhere(serverId));
|
|
return { ok: true } as const;
|
|
};
|
|
|
|
export const deployForwardAuthOnServer = async (input: {
|
|
serverId?: string;
|
|
providerId: string;
|
|
organizationId: string;
|
|
}) => {
|
|
const settings = await getForwardAuthSettings(input.serverId ?? null);
|
|
if (!settings) {
|
|
throw new TRPCError({
|
|
code: "PRECONDITION_FAILED",
|
|
message:
|
|
"Set the authentication domain for this server before deploying the proxy.",
|
|
});
|
|
}
|
|
|
|
const provider = await findProviderForOrg(
|
|
input.providerId,
|
|
input.organizationId,
|
|
);
|
|
const oidc = resolveOidcConfig(provider);
|
|
|
|
await setupForwardAuth({
|
|
serverId: input.serverId,
|
|
oidc,
|
|
cookieSecret: deriveCookieSecret(
|
|
`${input.serverId ?? "host"}:${settings.baseDomain}`,
|
|
),
|
|
authDomain: settings.authDomain,
|
|
baseDomain: settings.baseDomain,
|
|
authDomainHttps: settings.https,
|
|
});
|
|
|
|
if (settings.providerId !== input.providerId) {
|
|
await db
|
|
.update(forwardAuthSettings)
|
|
.set({ providerId: input.providerId })
|
|
.where(settingsWhere(input.serverId ?? null));
|
|
}
|
|
|
|
return { ok: true } as const;
|
|
};
|
|
|
|
const FORWARD_AUTH_CHECK_TIMEOUT_MS = 4000;
|
|
|
|
const proxyStatus = async (
|
|
serverId: string | null,
|
|
): Promise<"running" | "stopped" | "unknown"> => {
|
|
try {
|
|
const running = await Promise.race([
|
|
isForwardAuthRunning(serverId ?? undefined),
|
|
new Promise<never>((_, reject) =>
|
|
setTimeout(
|
|
() => reject(new Error("timeout")),
|
|
FORWARD_AUTH_CHECK_TIMEOUT_MS,
|
|
),
|
|
),
|
|
]);
|
|
return running ? "running" : "stopped";
|
|
} catch {
|
|
return "unknown";
|
|
}
|
|
};
|
|
|
|
export const getForwardAuthServerStatus = async (organizationId: string) => {
|
|
const servers = await db.query.server.findMany({
|
|
where: and(
|
|
eq(server.organizationId, organizationId),
|
|
isNotNull(server.sshKeyId),
|
|
eq(server.serverType, "deploy"),
|
|
),
|
|
columns: { serverId: true, name: true, ipAddress: true },
|
|
orderBy: [desc(server.createdAt)],
|
|
});
|
|
|
|
const targets: {
|
|
serverId: string | null;
|
|
name: string;
|
|
ipAddress: string | null;
|
|
}[] = [
|
|
...(IS_CLOUD
|
|
? []
|
|
: [
|
|
{
|
|
serverId: null,
|
|
name: "Dokploy Server (local)",
|
|
ipAddress: null,
|
|
},
|
|
]),
|
|
...servers.map((s) => ({
|
|
serverId: s.serverId,
|
|
name: s.name,
|
|
ipAddress: s.ipAddress,
|
|
})),
|
|
];
|
|
|
|
return Promise.all(
|
|
targets.map(async (t) => {
|
|
const settings = await getForwardAuthSettings(t.serverId);
|
|
return {
|
|
...t,
|
|
status: await proxyStatus(t.serverId),
|
|
authDomain: settings?.authDomain ?? null,
|
|
https: settings?.https ?? true,
|
|
certificateType: settings?.certificateType ?? "none",
|
|
customCertResolver: settings?.customCertResolver ?? null,
|
|
callbackUrl: settings
|
|
? forwardAuthCallbackUrl(settings.authDomain, settings.https)
|
|
: null,
|
|
};
|
|
}),
|
|
);
|
|
};
|
|
|
|
export const removeForwardAuthProxy = async (serverId: string | null) => {
|
|
await removeForwardAuth(serverId ?? undefined);
|
|
await db
|
|
.update(forwardAuthSettings)
|
|
.set({ providerId: null })
|
|
.where(settingsWhere(serverId));
|
|
return { ok: true } as const;
|
|
};
|
|
|
|
const resolveApplicationDomain = async (domainId: string) => {
|
|
const domain = await findDomainById(domainId);
|
|
if (!domain.applicationId) {
|
|
throw new TRPCError({
|
|
code: "BAD_REQUEST",
|
|
message:
|
|
"SSO forward-auth is currently only supported on application domains",
|
|
});
|
|
}
|
|
const application = await findApplicationById(domain.applicationId);
|
|
return { domain, application };
|
|
};
|
|
|
|
export const assertApplicationDomainAccess = async (
|
|
ctx: { session: { activeOrganizationId: string } },
|
|
domainId: string,
|
|
action: "create" | "delete",
|
|
) => {
|
|
const domain = await findDomainById(domainId);
|
|
if (!domain.applicationId) {
|
|
throw new TRPCError({
|
|
code: "BAD_REQUEST",
|
|
message:
|
|
"SSO forward-auth is currently only supported on application domains",
|
|
});
|
|
}
|
|
await checkServicePermissionAndAccess(ctx as any, domain.applicationId, {
|
|
domain: [action],
|
|
});
|
|
return domain;
|
|
};
|
|
|
|
export const enableForwardAuthOnDomain = async (input: {
|
|
domainId: string;
|
|
}) => {
|
|
const { application } = await resolveApplicationDomain(input.domainId);
|
|
const serverId = application.serverId ?? undefined;
|
|
|
|
const settings = await getForwardAuthSettings(serverId ?? null);
|
|
if (!settings?.providerId) {
|
|
throw new TRPCError({
|
|
code: "PRECONDITION_FAILED",
|
|
message:
|
|
"Deploy the authentication proxy for this server in SSO settings first.",
|
|
});
|
|
}
|
|
|
|
const proxyRunning = await isForwardAuthRunning(serverId);
|
|
if (!proxyRunning) {
|
|
throw new TRPCError({
|
|
code: "PRECONDITION_FAILED",
|
|
message:
|
|
"The authentication proxy is not deployed on this server. Deploy it in SSO settings first.",
|
|
});
|
|
}
|
|
|
|
await updateDomainById(input.domainId, { forwardAuthEnabled: true });
|
|
const domain = await findDomainById(input.domainId);
|
|
await manageDomain(application, domain);
|
|
|
|
return { ok: true } as const;
|
|
};
|
|
|
|
export const disableForwardAuthOnDomain = async (input: {
|
|
domainId: string;
|
|
}) => {
|
|
const { application, domain } = await resolveApplicationDomain(
|
|
input.domainId,
|
|
);
|
|
const uniqueConfigKey = domain.uniqueConfigKey;
|
|
|
|
await updateDomainById(input.domainId, { forwardAuthEnabled: false });
|
|
const updated = await findDomainById(input.domainId);
|
|
await manageDomain(application, updated);
|
|
await removeForwardAuthMiddleware(application, uniqueConfigKey);
|
|
|
|
return { ok: true } as const;
|
|
};
|