Merge pull request #4286 from Infisical/ENG-3376

feat(app-connections, PKI): Cloudflare as DNS provider
This commit is contained in:
x032205
2025-07-31 13:34:31 -04:00
committed by GitHub
19 changed files with 580 additions and 238 deletions
@@ -3,14 +3,16 @@ import { useQuery, UseQueryOptions } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { appConnectionKeys } from "../queries";
import { TCloudflarePagesProject, TCloudflareWorkersScript } from "./types";
import { TCloudflarePagesProject, TCloudflareWorkersScript, TCloudflareZone } from "./types";
const cloudflareConnectionKeys = {
all: [...appConnectionKeys.all, "cloudflare"] as const,
listPagesProjects: (connectionId: string) =>
[...cloudflareConnectionKeys.all, "pages-projects", connectionId] as const,
listWorkersScripts: (connectionId: string) =>
[...cloudflareConnectionKeys.all, "workers-scripts", connectionId] as const
[...cloudflareConnectionKeys.all, "workers-scripts", connectionId] as const,
listZones: (connectionId: string) =>
[...cloudflareConnectionKeys.all, "zones", connectionId] as const
};
export const useCloudflareConnectionListPagesProjects = (
@@ -62,3 +64,28 @@ export const useCloudflareConnectionListWorkersScripts = (
...options
});
};
export const useCloudflareConnectionListZones = (
connectionId: string,
options?: Omit<
UseQueryOptions<
TCloudflareZone[],
unknown,
TCloudflareZone[],
ReturnType<typeof cloudflareConnectionKeys.listZones>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: cloudflareConnectionKeys.listZones(connectionId),
queryFn: async () => {
const { data } = await apiRequest.get<TCloudflareZone[]>(
`/api/v1/app-connections/cloudflare/${connectionId}/cloudflare-zones`
);
return data;
},
...options
});
};
@@ -6,3 +6,8 @@ export type TCloudflarePagesProject = {
export type TCloudflareWorkersScript = {
id: string;
};
export type TCloudflareZone = {
id: string;
name: string;
};
+12 -1
View File
@@ -1,6 +1,7 @@
import { AppConnection } from "../appConnections/enums";
import { SshCaStatus } from "../sshCa";
import { SshCertTemplateStatus } from "../sshCertificateTemplates";
import { CaStatus, InternalCaType } from "./enums";
import { AcmeDnsProvider, CaStatus, InternalCaType } from "./enums";
export const caTypeToNameMap: { [K in InternalCaType]: string } = {
[InternalCaType.ROOT]: "Root",
@@ -13,6 +14,16 @@ export const caStatusToNameMap: { [K in CaStatus]: string } = {
[CaStatus.PENDING_CERTIFICATE]: "Pending Certificate"
};
export const ACME_DNS_PROVIDER_NAME_MAP: Record<AcmeDnsProvider, string> = {
[AcmeDnsProvider.ROUTE53]: "Route53",
[AcmeDnsProvider.Cloudflare]: "Cloudflare"
};
export const ACME_DNS_PROVIDER_APP_CONNECTION_MAP: Record<AcmeDnsProvider, AppConnection> = {
[AcmeDnsProvider.ROUTE53]: AppConnection.AWS,
[AcmeDnsProvider.Cloudflare]: AppConnection.Cloudflare
};
export const getCaStatusBadgeVariant = (status: CaStatus | SshCaStatus | SshCertTemplateStatus) => {
switch (status) {
case CaStatus.ACTIVE:
+2 -1
View File
@@ -19,5 +19,6 @@ export enum CaRenewalType {
}
export enum AcmeDnsProvider {
ROUTE53 = "route53"
ROUTE53 = "route53",
Cloudflare = "cloudflare"
}
+1 -1
View File
@@ -11,7 +11,7 @@ export type TAcmeCertificateAuthority = {
configuration: {
dnsAppConnectionId: string;
dnsProviderConfig: {
provider: AcmeDnsProvider.ROUTE53;
provider: AcmeDnsProvider;
hostedZoneId: string;
};
directoryUrl: string;
@@ -1,5 +1,6 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { SingleValue } from "react-select";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
@@ -16,7 +17,15 @@ import {
Switch
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useListAvailableAppConnections } from "@app/hooks/api/appConnections";
import { APP_CONNECTION_MAP } from "@app/helpers/appConnections";
import {
TAvailableAppConnection,
useListAvailableAppConnections
} from "@app/hooks/api/appConnections";
import {
TCloudflareZone,
useCloudflareConnectionListZones
} from "@app/hooks/api/appConnections/cloudflare";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import {
AcmeDnsProvider,
@@ -26,6 +35,10 @@ import {
useGetCa,
useUpdateCa
} from "@app/hooks/api/ca";
import {
ACME_DNS_PROVIDER_APP_CONNECTION_MAP,
ACME_DNS_PROVIDER_NAME_MAP
} from "@app/hooks/api/ca/constants";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { slugSchema } from "@app/lib/schemas";
@@ -42,7 +55,7 @@ const schema = z
id: z.string(),
name: z.string()
}),
// currently specific to Route53 but can be extended to others by differentiating via the provider property
// currently specific to Route53 & Cloudflare but can be extended to others by differentiating via the provider property
dnsProviderConfig: z.object({
provider: z.nativeEnum(AcmeDnsProvider),
hostedZoneId: z.string()
@@ -105,12 +118,29 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
const caType = watch("type");
const dnsProvider = watch("configuration.dnsProviderConfig.provider");
const { data: availableConnections, isPending } = useListAvailableAppConnections(
AppConnection.AWS,
{
enabled: dnsProvider === AcmeDnsProvider.ROUTE53
}
);
const { data: availableRoute53Connections, isPending: isRoute53Pending } =
useListAvailableAppConnections(AppConnection.AWS, {
enabled: caType === CaType.ACME
});
const { data: availableCloudflareConnections, isPending: isCloudflarePending } =
useListAvailableAppConnections(AppConnection.Cloudflare, {
enabled: caType === CaType.ACME
});
const availableConnections: TAvailableAppConnection[] = [
...(availableRoute53Connections || []),
...(availableCloudflareConnections || [])
];
const isPending = isRoute53Pending || isCloudflarePending;
const dnsAppConnection = watch("configuration.dnsAppConnection");
const { data: cloudflareZones = [], isPending: isZonesPending } =
useCloudflareConnectionListZones(dnsAppConnection.id, {
enabled: dnsProvider === AcmeDnsProvider.Cloudflare && !!dnsAppConnection.id
});
useEffect(() => {
if (ca) {
@@ -138,25 +168,6 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
}
});
}
} else {
reset({
type: CaType.ACME,
name: "",
status: CaStatus.ACTIVE,
enableDirectIssuance: true,
configuration: {
dnsAppConnection: {
id: "",
name: ""
},
dnsProviderConfig: {
provider: AcmeDnsProvider.ROUTE53,
hostedZoneId: ""
},
directoryUrl: "",
accountEmail: ""
}
});
}
}, [ca, availableConnections]);
@@ -284,12 +295,11 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
className="w-full"
isDisabled={Boolean(ca)}
>
<SelectItem
value={String(AcmeDnsProvider.ROUTE53)}
key={AcmeDnsProvider.ROUTE53}
>
Route53
</SelectItem>
{Object.values(AcmeDnsProvider).map((provider) => (
<SelectItem value={String(provider)} key={provider}>
{ACME_DNS_PROVIDER_NAME_MAP[provider]}
</SelectItem>
))}
</Select>
</FormControl>
)}
@@ -297,7 +307,7 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
<Controller
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
tooltipText={`${dnsProvider === AcmeDnsProvider.ROUTE53 ? "Route53" : ""} requires an AWS App Connection. This can be created from the Organization Settings page.`}
tooltipText={`${ACME_DNS_PROVIDER_NAME_MAP[dnsProvider]} uses the ${APP_CONNECTION_MAP[ACME_DNS_PROVIDER_APP_CONNECTION_MAP[dnsProvider]].name} App Connection. You can create one in the Organization Settings page.`}
isError={Boolean(error)}
errorText={error?.message}
label="DNS App Connection"
@@ -318,21 +328,49 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
control={control}
name="configuration.dnsAppConnection"
/>
<Controller
control={control}
defaultValue=""
name="configuration.dnsProviderConfig.hostedZoneId"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Hosted Zone ID"
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<Input {...field} placeholder="Z040441124N1GOOMCQYX1" />
</FormControl>
)}
/>
{dnsProvider === AcmeDnsProvider.ROUTE53 && (
<Controller
control={control}
defaultValue=""
name="configuration.dnsProviderConfig.hostedZoneId"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Hosted Zone ID"
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<Input {...field} placeholder="Z040441124N1GOOMCQYX1" />
</FormControl>
)}
/>
)}
{dnsProvider === AcmeDnsProvider.Cloudflare && (
<Controller
name="configuration.dnsProviderConfig.hostedZoneId"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Zone"
>
<FilterableSelect
isLoading={isZonesPending && !!dnsAppConnection.id}
isDisabled={!dnsAppConnection.id}
value={cloudflareZones.find((zone) => zone.id === value)}
onChange={(option) => {
onChange((option as SingleValue<TCloudflareZone>)?.id ?? null);
}}
options={cloudflareZones}
placeholder="Select a zone..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id}
/>
</FormControl>
)}
/>
)}
<Controller
control={control}
defaultValue=""
@@ -82,7 +82,11 @@ export const SecretDetectionIgnoreValuesSection = () => {
<div className="flex w-full items-center justify-between">
<p className="text-xl font-semibold">Secret Detection</p>
</div>
<p className="mb-4 mt-2 max-w-2xl text-sm text-gray-400">Define secret values to ignore when scanning designated parameter folders. Add values here to prevent false positives or allow approved sensitive data. These ignored values will not trigger policy violation alerts.</p>
<p className="mb-4 mt-2 max-w-2xl text-sm text-gray-400">
Define secret values to ignore when scanning designated parameter folders. Add values here
to prevent false positives or allow approved sensitive data. These ignored values will not
trigger policy violation alerts.
</p>
<form onSubmit={handleSubmit(handleIgnoreValuesSubmit)} autoComplete="off">
<div className="mb-4">