diff --git a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts index c41970b5c8d..8d925288971 100644 --- a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts +++ b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts @@ -82,6 +82,10 @@ import { DNSMadeEasyConnectionListItemSchema, SanitizedDNSMadeEasyConnectionSchema } from "@app/services/app-connection/dns-made-easy/dns-made-easy-connection-schema"; +import { + PowerDNSConnectionListItemSchema, + SanitizedPowerDNSConnectionSchema +} from "@app/services/app-connection/powerdns/powerdns-connection-schema"; import { ExternalInfisicalConnectionListItemSchema, SanitizedExternalInfisicalConnectionSchema @@ -228,6 +232,7 @@ const SanitizedAppConnectionSchema = z.union([ ...SanitizedAzureEntraIdConnectionSchema.options, ...SanitizedVenafiConnectionSchema.options, ...SanitizedExternalInfisicalConnectionSchema.options, + ...SanitizedPowerDNSConnectionSchema.options, ...SanitizedNetScalerConnectionSchema.options ]); @@ -286,6 +291,7 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ AzureEntraIdConnectionListItemSchema, VenafiConnectionListItemSchema, ExternalInfisicalConnectionListItemSchema, + PowerDNSConnectionListItemSchema, NetScalerConnectionListItemSchema, AnthropicConnectionListItemSchema ]); diff --git a/backend/src/server/routes/v1/app-connection-routers/index.ts b/backend/src/server/routes/v1/app-connection-routers/index.ts index 0755d814e21..303504347d1 100644 --- a/backend/src/server/routes/v1/app-connection-routers/index.ts +++ b/backend/src/server/routes/v1/app-connection-routers/index.ts @@ -23,6 +23,7 @@ import { registerDatabricksConnectionRouter } from "./databricks-connection-rout import { registerDbtConnectionRouter } from "./dbt-connection-router"; import { registerDigitalOceanConnectionRouter } from "./digital-ocean-connection-router"; import { registerDNSMadeEasyConnectionRouter } from "./dns-made-easy-connection-router"; +import { registerPowerDNSConnectionRouter } from "./powerdns-connection-router"; import { registerExternalInfisicalConnectionRouter } from "./external-infisical-connection-router"; import { registerFlyioConnectionRouter } from "./flyio-connection-router"; import { registerGcpConnectionRouter } from "./gcp-connection-router"; @@ -115,6 +116,7 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record { + registerAppConnectionEndpoints({ + app: AppConnection.PowerDNS, + server, + sanitizedResponseSchema: SanitizedPowerDNSConnectionSchema, + createSchema: CreatePowerDNSConnectionSchema, + updateSchema: UpdatePowerDNSConnectionSchema + }); +}; diff --git a/backend/src/services/app-connection/app-connection-enums.ts b/backend/src/services/app-connection/app-connection-enums.ts index fe4dcf4d06c..ae68ee54c17 100644 --- a/backend/src/services/app-connection/app-connection-enums.ts +++ b/backend/src/services/app-connection/app-connection-enums.ts @@ -53,6 +53,7 @@ export enum AppConnection { AzureEntraId = "azure-entra-id", Venafi = "venafi", ExternalInfisical = "external-infisical", + PowerDNS = "powerdns", NetScaler = "netscaler", Anthropic = "anthropic" } diff --git a/backend/src/services/app-connection/app-connection-fns.ts b/backend/src/services/app-connection/app-connection-fns.ts index 5295ee62c45..0f31819e85d 100644 --- a/backend/src/services/app-connection/app-connection-fns.ts +++ b/backend/src/services/app-connection/app-connection-fns.ts @@ -118,6 +118,11 @@ import { getDNSMadeEasyConnectionListItem, validateDNSMadeEasyConnectionCredentials } from "./dns-made-easy/dns-made-easy-connection-fns"; +import { PowerDNSConnectionMethod } from "./powerdns/powerdns-connection-enum"; +import { + getPowerDNSConnectionListItem, + validatePowerDNSConnectionCredentials +} from "./powerdns/powerdns-connection-fns"; import { ExternalInfisicalConnectionMethod, getExternalInfisicalConnectionListItem, @@ -232,6 +237,7 @@ const PKI_APP_CONNECTIONS = [ AppConnection.DNSMadeEasy, AppConnection.AzureDNS, AppConnection.Venafi, + AppConnection.PowerDNS, AppConnection.NetScaler ]; @@ -292,6 +298,7 @@ export const listAppConnectionOptions = (projectType?: ProjectType) => { getAzureEntraIdConnectionListItem(), getVenafiConnectionListItem(), getExternalInfisicalConnectionListItem(), + getPowerDNSConnectionListItem(), getNetScalerConnectionListItem() ] .filter((option) => { @@ -443,7 +450,8 @@ export const validateAppConnectionCredentials = async ( validateExternalInfisicalConnectionCredentials( config as TExternalInfisicalConnectionConfig, deps.identityUaDAL - )) as TAppConnectionCredentialsValidator + )) as TAppConnectionCredentialsValidator, + [AppConnection.PowerDNS]: validatePowerDNSConnectionCredentials as TAppConnectionCredentialsValidator }; return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection, gatewayService, gatewayV2Service); @@ -538,6 +546,8 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) => return "Basic Auth"; case ExternalInfisicalConnectionMethod.MachineIdentityUniversalAuth: return "Machine Identity - Universal Auth"; + case PowerDNSConnectionMethod.APIKey: + return "API Key"; default: // eslint-disable-next-line @typescript-eslint/restrict-template-expressions throw new Error(`Unhandled App Connection Method: ${method}`); diff --git a/backend/src/services/app-connection/app-connection-maps.ts b/backend/src/services/app-connection/app-connection-maps.ts index b3de1153d2b..9947178fd48 100644 --- a/backend/src/services/app-connection/app-connection-maps.ts +++ b/backend/src/services/app-connection/app-connection-maps.ts @@ -55,6 +55,7 @@ export const APP_CONNECTION_NAME_MAP: Record = { [AppConnection.AzureEntraId]: "Azure Entra ID", [AppConnection.Venafi]: "Venafi TLS Protect Cloud", [AppConnection.ExternalInfisical]: "Infisical", + [AppConnection.PowerDNS]: "PowerDNS", [AppConnection.NetScaler]: "NetScaler", [AppConnection.Anthropic]: "Anthropic" }; @@ -114,6 +115,7 @@ export const APP_CONNECTION_PLAN_MAP: Record { + return { + name: "PowerDNS" as const, + app: AppConnection.PowerDNS as const, + methods: Object.values(PowerDNSConnectionMethod) as [PowerDNSConnectionMethod.APIKey] + }; +}; + +export const validatePowerDNSConnectionCredentials = async (config: TPowerDNSConnectionConfig) => { + if (config.method !== PowerDNSConnectionMethod.APIKey) { + throw new BadRequestError({ message: "Unsupported PowerDNS connection method" }); + } + + const { apiKey, baseUrl } = config.credentials; + + try { + + await blockLocalAndPrivateIpAddresses(baseUrl); + + + // Use /servers/localhost/zones as the validation endpoint — it is supported by both + // direct PowerDNS Server and PowerDNS-Admin proxy configurations. + const resp = await request.get(`${baseUrl}/servers/localhost/zones`, { + headers: { + "x-api-key": apiKey, + Accept: "application/json" + } + }); + + if (resp.status !== 200) { + throw new BadRequestError({ + message: "Unable to validate connection: Invalid API credentials provided." + }); + } + } catch (error: unknown) { + if (error instanceof BadRequestError) { + throw error; + } + if (error instanceof AxiosError) { + throw new BadRequestError({ + message: `Failed to validate credentials: ${ + (error.response?.data as { error?: string })?.error || error.message || "Unknown error" + }` + }); + } + logger.error(error, "Error validating PowerDNS connection credentials"); + throw new BadRequestError({ + message: "Unable to validate connection: verify credentials and base URL" + }); + } + + return config.credentials; +}; diff --git a/backend/src/services/app-connection/powerdns/powerdns-connection-schema.ts b/backend/src/services/app-connection/powerdns/powerdns-connection-schema.ts new file mode 100644 index 00000000000..e0ab21fb051 --- /dev/null +++ b/backend/src/services/app-connection/powerdns/powerdns-connection-schema.ts @@ -0,0 +1,69 @@ +import z from "zod"; + +import { AppConnections } from "@app/lib/api-docs"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + BaseAppConnectionSchema, + GenericCreateAppConnectionFieldsSchema, + GenericUpdateAppConnectionFieldsSchema +} from "@app/services/app-connection/app-connection-schemas"; + +import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps"; +import { PowerDNSConnectionMethod } from "./powerdns-connection-enum"; + +export const PowerDNSConnectionApiKeyCredentialsSchema = z.object({ + apiKey: z.string().trim().min(1, "API key required").max(256, "API key cannot exceed 256 characters"), + baseUrl: z + .string() + .trim() + .url("Base URL must be a valid URL") + .min(1, "Base URL required") + .max(1024, "Base URL cannot exceed 1024 characters") +}); + +const BasePowerDNSConnectionSchema = BaseAppConnectionSchema.extend({ + app: z.literal(AppConnection.PowerDNS) +}); + +export const PowerDNSConnectionSchema = BasePowerDNSConnectionSchema.extend({ + method: z.literal(PowerDNSConnectionMethod.APIKey), + credentials: PowerDNSConnectionApiKeyCredentialsSchema +}); + +export const SanitizedPowerDNSConnectionSchema = z.discriminatedUnion("method", [ + BasePowerDNSConnectionSchema.extend({ + method: z.literal(PowerDNSConnectionMethod.APIKey), + credentials: PowerDNSConnectionApiKeyCredentialsSchema.pick({ baseUrl: true }) + }).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.PowerDNS]} (API Key)` })) +]); + +export const ValidatePowerDNSConnectionCredentialsSchema = z.discriminatedUnion("method", [ + z.object({ + method: z + .literal(PowerDNSConnectionMethod.APIKey) + .describe(AppConnections.CREATE(AppConnection.PowerDNS).method), + credentials: PowerDNSConnectionApiKeyCredentialsSchema.describe( + AppConnections.CREATE(AppConnection.PowerDNS).credentials + ) + }) +]); + +export const CreatePowerDNSConnectionSchema = ValidatePowerDNSConnectionCredentialsSchema.and( + GenericCreateAppConnectionFieldsSchema(AppConnection.PowerDNS) +); + +export const UpdatePowerDNSConnectionSchema = z + .object({ + credentials: PowerDNSConnectionApiKeyCredentialsSchema.optional().describe( + AppConnections.UPDATE(AppConnection.PowerDNS).credentials + ) + }) + .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.PowerDNS)); + +export const PowerDNSConnectionListItemSchema = z + .object({ + name: z.literal("PowerDNS"), + app: z.literal(AppConnection.PowerDNS), + methods: z.nativeEnum(PowerDNSConnectionMethod).array() + }) + .describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.PowerDNS] })); diff --git a/backend/src/services/app-connection/powerdns/powerdns-connection-service.ts b/backend/src/services/app-connection/powerdns/powerdns-connection-service.ts new file mode 100644 index 00000000000..ef6fb148e93 --- /dev/null +++ b/backend/src/services/app-connection/powerdns/powerdns-connection-service.ts @@ -0,0 +1,3 @@ +export const powerDnsConnectionService = () => { + return {}; +}; diff --git a/backend/src/services/app-connection/powerdns/powerdns-connection-types.ts b/backend/src/services/app-connection/powerdns/powerdns-connection-types.ts new file mode 100644 index 00000000000..8f377f1fad9 --- /dev/null +++ b/backend/src/services/app-connection/powerdns/powerdns-connection-types.ts @@ -0,0 +1,25 @@ +import z from "zod"; + +import { DiscriminativePick } from "@app/lib/types"; + +import { AppConnection } from "../app-connection-enums"; +import { + CreatePowerDNSConnectionSchema, + PowerDNSConnectionSchema, + ValidatePowerDNSConnectionCredentialsSchema +} from "./powerdns-connection-schema"; + +export type TPowerDNSConnection = z.infer; + +export type TPowerDNSConnectionInput = z.infer & { + app: AppConnection.PowerDNS; +}; + +export type TValidatePowerDNSConnectionCredentialsSchema = typeof ValidatePowerDNSConnectionCredentialsSchema; + +export type TPowerDNSConnectionConfig = DiscriminativePick< + TPowerDNSConnectionInput, + "method" | "app" | "credentials" +> & { + orgId: string; +}; diff --git a/backend/src/services/certificate-authority/acme/acme-certificate-authority-enums.ts b/backend/src/services/certificate-authority/acme/acme-certificate-authority-enums.ts index 7c1fcedd29b..cb01c2aabac 100644 --- a/backend/src/services/certificate-authority/acme/acme-certificate-authority-enums.ts +++ b/backend/src/services/certificate-authority/acme/acme-certificate-authority-enums.ts @@ -2,5 +2,6 @@ export enum AcmeDnsProvider { Route53 = "route53", Cloudflare = "cloudflare", DNSMadeEasy = "dns-made-easy", - AzureDNS = "azure-dns" + AzureDNS = "azure-dns", + PowerDNS = "powerdns" } diff --git a/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts b/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts index 05047274eef..167052cd36f 100644 --- a/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts +++ b/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts @@ -22,6 +22,7 @@ import { TAwsConnection } from "@app/services/app-connection/aws/aws-connection- import { TAzureDnsConnection } from "@app/services/app-connection/azure-dns/azure-dns-connection-types"; import { TCloudflareConnection } from "@app/services/app-connection/cloudflare/cloudflare-connection-types"; import { TDNSMadeEasyConnection } from "@app/services/app-connection/dns-made-easy/dns-made-easy-connection-types"; +import { TPowerDNSConnection } from "@app/services/app-connection/powerdns/powerdns-connection-types"; import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal"; import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal"; @@ -54,6 +55,7 @@ import { import { azureDnsDeleteTxtRecord, azureDnsInsertTxtRecord } from "./dns-providers/azure-dns"; import { cloudflareDeleteTxtRecord, cloudflareInsertTxtRecord } from "./dns-providers/cloudflare"; import { dnsMadeEasyDeleteTxtRecord, dnsMadeEasyInsertTxtRecord } from "./dns-providers/dns-made-easy"; +import { powerDnsDeleteTxtRecord, powerDnsInsertTxtRecord } from "./dns-providers/powerdns"; import { route53DeleteTxtRecord, route53InsertTxtRecord } from "./dns-providers/route54"; const validateDnsResolver = (resolver: string): void => { @@ -203,7 +205,7 @@ export const castDbEntryToAcmeCertificateAuthority = ( }; }; -const DNS_PROPAGATION_MAX_RETRIES = 5; +const DNS_PROPAGATION_MAX_RETRIES = 30; const DNS_PROPAGATION_INTERVAL_MS = 2000; const CNAME_MAX_DEPTH = 10; @@ -257,6 +259,7 @@ const waitForDnsPropagation = async ( await delay(DNS_PROPAGATION_INTERVAL_MS); // eslint-disable-line no-await-in-loop } } + throw new Error(`DNS record "${lookupName}" with value "${expectedValue}" not found after ${DNS_PROPAGATION_MAX_RETRIES} attempts`); }; const getAcmeChallengeRecord = async ( @@ -457,6 +460,15 @@ export const orderCertificate = async ( ); break; } + case AcmeDnsProvider.PowerDNS: { + await powerDnsInsertTxtRecord( + connection as TPowerDNSConnection, + acmeCa.configuration.dnsProviderConfig.hostedZoneId, + recordName, + recordValue + ); + break; + } default: { throw new Error(`Unsupported DNS provider: ${acmeCa.configuration.dnsProviderConfig.provider as string}`); } @@ -513,6 +525,14 @@ export const orderCertificate = async ( ); break; } + case AcmeDnsProvider.PowerDNS: { + await powerDnsDeleteTxtRecord( + connection as TPowerDNSConnection, + acmeCa.configuration.dnsProviderConfig.hostedZoneId, + recordName, + ); + break; + } default: { throw new Error(`Unsupported DNS provider: ${acmeCa.configuration.dnsProviderConfig.provider as string}`); } @@ -675,6 +695,12 @@ export const AcmeCertificateAuthorityFns = ({ }); } + if (dnsProviderConfig.provider === AcmeDnsProvider.PowerDNS && appConnection.app !== AppConnection.PowerDNS) { + throw new BadRequestError({ + message: `App connection with ID '${dnsAppConnectionId}' is not a PowerDNS connection` + }); + } + if (dnsResolver) { validateDnsResolver(dnsResolver); } @@ -789,6 +815,12 @@ export const AcmeCertificateAuthorityFns = ({ }); } + if (dnsProviderConfig.provider === AcmeDnsProvider.PowerDNS && appConnection.app !== AppConnection.PowerDNS) { + throw new BadRequestError({ + message: `App connection with ID '${dnsAppConnectionId}' is not a PowerDNS connection` + }); + } + if (dnsResolver) { validateDnsResolver(dnsResolver); } diff --git a/backend/src/services/certificate-authority/acme/dns-providers/powerdns.ts b/backend/src/services/certificate-authority/acme/dns-providers/powerdns.ts new file mode 100644 index 00000000000..746260738e6 --- /dev/null +++ b/backend/src/services/certificate-authority/acme/dns-providers/powerdns.ts @@ -0,0 +1,116 @@ +import axios from "axios"; + +import { request } from "@app/lib/config/request"; +import { logger } from "@app/lib/logger"; +import { TPowerDNSConnection } from "@app/services/app-connection/powerdns/powerdns-connection-types"; +import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator"; + +const normalizePowerDNSZone = (zone: string): string => { + return zone.endsWith(".") ? zone : `${zone}.`; +}; + +const normalizePowerDNSRecordName = (name: string): string => { + return name.endsWith(".") ? name : `${name}.`; +}; + +export const powerDnsInsertTxtRecord = async ( + connection: TPowerDNSConnection, + hostedZoneId: string, + domain: string, + value: string +) => { + const { + credentials: { apiKey, baseUrl } + } = connection; + + await blockLocalAndPrivateIpAddresses(baseUrl); + + const zone = normalizePowerDNSZone(hostedZoneId); + // domain is the full FQDN (e.g. "_acme-challenge.example.com"), ensure trailing dot for PowerDNS + const recordName = normalizePowerDNSRecordName(domain); + + logger.info({ zone, recordName, value }, "Inserting TXT record for PowerDNS"); + + try { + await request.patch( + `${baseUrl}/servers/localhost/zones/${encodeURIComponent(zone)}`, + { + rrsets: [ + { + name: recordName, + type: "TXT", + ttl: 60, + changetype: "REPLACE", + records: [ + { + content: value, + disabled: false + } + ] + } + ] + }, + { + headers: { + "x-api-key": apiKey, + "Content-Type": "application/json", + Accept: "application/json" + } + } + ); + } catch (error) { + if (axios.isAxiosError(error)) { + const errorMessage = + (error.response?.data as { error?: string })?.error || error.message || "Unknown error"; + throw new Error(typeof errorMessage === "string" ? errorMessage : String(errorMessage)); + } + throw error; + } +}; + +export const powerDnsDeleteTxtRecord = async ( + connection: TPowerDNSConnection, + hostedZoneId: string, + domain: string, +) => { + const { + credentials: { apiKey, baseUrl } + } = connection; + + await blockLocalAndPrivateIpAddresses(baseUrl); + + const zone = normalizePowerDNSZone(hostedZoneId); + const recordName = normalizePowerDNSRecordName(domain); + + logger.info({ zone, recordName }, "Deleting TXT record for PowerDNS"); + + try { + // PowerDNS returns 204 even if the record does not exist, so no pre-check needed + await request.patch( + `${baseUrl}/servers/localhost/zones/${encodeURIComponent(zone)}`, + { + rrsets: [ + { + name: recordName, + type: "TXT", + changetype: "DELETE" + } + ] + }, + { + headers: { + "x-api-key": apiKey, + "Content-Type": "application/json", + Accept: "application/json" + } + } + ); + } catch (error) { + if (axios.isAxiosError(error)) { + const errorMessage = + (error.response?.data as { error?: string })?.error || error.message || "Unknown error"; + throw new Error(typeof errorMessage === "string" ? errorMessage : String(errorMessage)); + } + throw error; + } +}; diff --git a/docs/docs.json b/docs/docs.json index 879e160217f..bf05ded9758 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -150,6 +150,7 @@ "integrations/app-connections/openrouter", "integrations/app-connections/oracledb", "integrations/app-connections/postgres", + "integrations/app-connections/powerdns", "integrations/app-connections/railway", "integrations/app-connections/redis", "integrations/app-connections/render", diff --git a/docs/integrations/app-connections/powerdns.mdx b/docs/integrations/app-connections/powerdns.mdx new file mode 100644 index 00000000000..6adcac6e57c --- /dev/null +++ b/docs/integrations/app-connections/powerdns.mdx @@ -0,0 +1,57 @@ +--- +title: "PowerDNS" +description: "Learn how to configure a PowerDNS Connection for Infisical." +--- + +Infisical supports connecting to PowerDNS using an API key for secure access to your PowerDNS service. Both a direct PowerDNS Authoritative Server and a [PowerDNS-Admin](https://github.com/PowerDNS-Admin/PowerDNS-Admin) proxy are supported via a configurable base URL. + +## Configure an API Key for Infisical + + + + The PowerDNS Authoritative Server exposes a REST API protected by a static API key. The key is set in your PowerDNS configuration file (typically `/etc/powerdns/pdns.conf`) via the `api-key` directive: + + ```ini + api=yes + api-key=your-secret-api-key + webserver=yes + webserver-address=0.0.0.0 + webserver-port=8081 + ``` + + If you are using **PowerDNS-Admin**, the API key is the value configured under **Settings → API Keys** in the PowerDNS-Admin web interface. + + + Keep your API key secure and do not share it. Anyone with access to this key can read and modify all zones managed by your PowerDNS server. + + + + The base URL depends on how you are accessing PowerDNS: + + - **Direct PowerDNS server:** `http://your-powerdns-server:8081` + - **PowerDNS-Admin proxy:** `https://powerdns-admin.example.com/api/v1` + + Infisical will use this base URL to reach the PowerDNS REST API when creating and removing DNS challenge records during ACME certificate issuance. + + + +## Setup PowerDNS Connection in Infisical + + + + Navigate to the **Integrations** tab in the desired project, then select **App Connections**. ![App + Connections Tab](/images/app-connections/general/add-connection.png) + + + Select the **PowerDNS Connection** option from the connection options modal. + + + Enter the following details and click **Connect to PowerDNS** to establish the connection: + + - **Base URL** — The base URL of your PowerDNS API (e.g. `http://your-powerdns-server:8081` or `https://powerdns-admin.example.com/api/v1`). + - **API Key** — The API key used to authenticate requests to the PowerDNS REST API. + + + Your **PowerDNS Connection** is now available for use in your Infisical projects, for example as the DNS provider when configuring an ACME External CA. + + diff --git a/frontend/public/images/integrations/PowerDNS.png b/frontend/public/images/integrations/PowerDNS.png new file mode 100644 index 00000000000..59c9a83e7d7 Binary files /dev/null and b/frontend/public/images/integrations/PowerDNS.png differ diff --git a/frontend/src/helpers/appConnections.ts b/frontend/src/helpers/appConnections.ts index c262252827b..3ea46a21c47 100644 --- a/frontend/src/helpers/appConnections.ts +++ b/frontend/src/helpers/appConnections.ts @@ -57,6 +57,7 @@ import { ChefConnectionMethod } from "@app/hooks/api/appConnections/types/chef-c import { CircleCIConnectionMethod } from "@app/hooks/api/appConnections/types/circleci-connection"; import { DigitalOceanConnectionMethod } from "@app/hooks/api/appConnections/types/digital-ocean"; import { DNSMadeEasyConnectionMethod } from "@app/hooks/api/appConnections/types/dns-made-easy-connection"; +import { PowerDNSConnectionMethod } from "@app/hooks/api/appConnections/types/powerdns-connection"; import { ExternalInfisicalConnectionMethod } from "@app/hooks/api/appConnections/types/external-infisical-connection"; import { HerokuConnectionMethod } from "@app/hooks/api/appConnections/types/heroku-connection"; import { LaravelForgeConnectionMethod } from "@app/hooks/api/appConnections/types/laravel-forge-connection"; @@ -159,6 +160,7 @@ export const APP_CONNECTION_MAP: Record< [AppConnection.AzureEntraId]: { name: "Azure Entra ID", image: "Microsoft Azure.png" }, [AppConnection.Venafi]: { name: "Venafi TLS Protect Cloud", image: "Venafi.png" }, [AppConnection.ExternalInfisical]: { name: "Infisical", image: "Infisical.png" }, + [AppConnection.PowerDNS]: { name: "PowerDNS", image: "PowerDNS.png" }, [AppConnection.NetScaler]: { name: "NetScaler", image: "NetScaler.png" }, [AppConnection.Anthropic]: { name: "Anthropic", image: "Anthropic.png" } }; @@ -251,6 +253,8 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) return { name: "Certificate", icon: faCertificate }; case DNSMadeEasyConnectionMethod.APIKeySecret: return { name: "API Key & Secret", icon: faKey }; + case PowerDNSConnectionMethod.APIKey: + return { name: "API Key", icon: faKey }; case AzureDNSConnectionMethod.ClientSecret: case AzureEntraIdConnectionMethod.ClientSecret: return { name: "Client Secret", icon: faKey }; diff --git a/frontend/src/hooks/api/appConnections/enums.ts b/frontend/src/hooks/api/appConnections/enums.ts index f57e65e16ce..67513519174 100644 --- a/frontend/src/hooks/api/appConnections/enums.ts +++ b/frontend/src/hooks/api/appConnections/enums.ts @@ -53,6 +53,7 @@ export enum AppConnection { AzureEntraId = "azure-entra-id", Venafi = "venafi", ExternalInfisical = "external-infisical", + PowerDNS = "powerdns", NetScaler = "netscaler", Anthropic = "anthropic" } diff --git a/frontend/src/hooks/api/appConnections/types/index.ts b/frontend/src/hooks/api/appConnections/types/index.ts index 4194265e717..acf0a86ddac 100644 --- a/frontend/src/hooks/api/appConnections/types/index.ts +++ b/frontend/src/hooks/api/appConnections/types/index.ts @@ -21,6 +21,7 @@ import { TDatabricksConnection } from "./databricks-connection"; import { TDbtConnection } from "./dbt-connection"; import { TDigitalOceanConnection } from "./digital-ocean"; import { TDNSMadeEasyConnection } from "./dns-made-easy-connection"; +import { TPowerDNSConnection } from "./powerdns-connection"; import { TExternalInfisicalConnection } from "./external-infisical-connection"; import { TFlyioConnection } from "./flyio-connection"; import { TGcpConnection } from "./gcp-connection"; @@ -110,6 +111,7 @@ export * from "./terraform-cloud-connection"; export * from "./venafi-connection"; export * from "./vercel-connection"; export * from "./windmill-connection"; +export * from "./powerdns-connection"; export * from "./zabbix-connection"; export type TAppConnection = @@ -168,6 +170,7 @@ export type TAppConnection = | TAzureEntraIdConnection | TVenafiConnection | TExternalInfisicalConnection + | TPowerDNSConnection | TNetScalerConnection; export type TAvailableAppConnection = Pick; diff --git a/frontend/src/hooks/api/appConnections/types/powerdns-connection.ts b/frontend/src/hooks/api/appConnections/types/powerdns-connection.ts new file mode 100644 index 00000000000..42ad1fe27cb --- /dev/null +++ b/frontend/src/hooks/api/appConnections/types/powerdns-connection.ts @@ -0,0 +1,13 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection"; + +export enum PowerDNSConnectionMethod { + APIKey = "api-key" +} + +export type TPowerDNSConnection = TRootAppConnection & { app: AppConnection.PowerDNS } & { + method: PowerDNSConnectionMethod.APIKey; + credentials: { + baseUrl: string; + }; +}; diff --git a/frontend/src/hooks/api/ca/constants.tsx b/frontend/src/hooks/api/ca/constants.tsx index 5ab6ccad43e..bdff950e73e 100644 --- a/frontend/src/hooks/api/ca/constants.tsx +++ b/frontend/src/hooks/api/ca/constants.tsx @@ -18,14 +18,16 @@ export const ACME_DNS_PROVIDER_NAME_MAP: Record = { [AcmeDnsProvider.ROUTE53]: "Route53", [AcmeDnsProvider.Cloudflare]: "Cloudflare", [AcmeDnsProvider.DNSMadeEasy]: "DNS Made Easy", - [AcmeDnsProvider.AzureDNS]: "Azure DNS" + [AcmeDnsProvider.AzureDNS]: "Azure DNS", + [AcmeDnsProvider.PowerDNS]: "PowerDNS" }; export const ACME_DNS_PROVIDER_APP_CONNECTION_MAP: Record = { [AcmeDnsProvider.ROUTE53]: AppConnection.AWS, [AcmeDnsProvider.Cloudflare]: AppConnection.Cloudflare, [AcmeDnsProvider.DNSMadeEasy]: AppConnection.DNSMadeEasy, - [AcmeDnsProvider.AzureDNS]: AppConnection.AzureDNS + [AcmeDnsProvider.AzureDNS]: AppConnection.AzureDNS, + [AcmeDnsProvider.PowerDNS]: AppConnection.PowerDNS }; export const CA_TYPE_CAPABILITIES_MAP: Record = { diff --git a/frontend/src/hooks/api/ca/enums.tsx b/frontend/src/hooks/api/ca/enums.tsx index 27dca00c041..bae5d7af72b 100644 --- a/frontend/src/hooks/api/ca/enums.tsx +++ b/frontend/src/hooks/api/ca/enums.tsx @@ -24,7 +24,8 @@ export enum AcmeDnsProvider { ROUTE53 = "route53", Cloudflare = "cloudflare", DNSMadeEasy = "dns-made-easy", - AzureDNS = "azure-dns" + AzureDNS = "azure-dns", + PowerDNS = "powerdns" } export enum CaRenewalStatus { diff --git a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/ExternalCaModal.tsx b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/ExternalCaModal.tsx index 0595322368d..9fb9e6fe462 100644 --- a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/ExternalCaModal.tsx +++ b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/ExternalCaModal.tsx @@ -259,6 +259,11 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => { enabled: caType === CaType.ACME }); + const { data: availablePowerDNSConnections, isPending: isPowerDNSPending } = + useListAvailableAppConnections(AppConnection.PowerDNS, currentProject.id, { + enabled: caType === CaType.ACME + }); + const { data: availableAzureConnections, isPending: isAzurePending } = useListAvailableAppConnections(AppConnection.AzureADCS, currentProject.id, { enabled: caType === CaType.AZURE_AD_CS @@ -283,7 +288,8 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => { ...(availableRoute53Connections || []), ...(availableCloudflareConnections || []), ...(availableDNSMadeEasyConnections || []), - ...(availableAzureDNSConnections || []) + ...(availableAzureDNSConnections || []), + ...(availablePowerDNSConnections || []) ]; }, [ caType, @@ -292,7 +298,8 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => { availableDNSMadeEasyConnections, availableAzureDNSConnections, availableAzureConnections, - availableAwsConnections + availableAwsConnections, + availablePowerDNSConnections ]); const isPending = @@ -300,6 +307,7 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => { isCloudflarePending || isDNSMadeEasyPending || isAzureDNSPending || + isPowerDNSPending || (isAzurePending && caType === CaType.AZURE_AD_CS) || (isAwsPending && caType === CaType.AWS_PCA); @@ -655,6 +663,24 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => { )} /> )} + {dnsProvider === AcmeDnsProvider.PowerDNS && ( + ( + + + + )} + /> + )} { return ; case AppConnection.ExternalInfisical: return ; + case AppConnection.PowerDNS: + return ; case AppConnection.NetScaler: return ; default: @@ -485,6 +488,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => { return ( ); + case AppConnection.PowerDNS: + return ; case AppConnection.NetScaler: return ; default: diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/PowerDNSConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/PowerDNSConnectionForm.tsx new file mode 100644 index 00000000000..2a9cf5098e4 --- /dev/null +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/PowerDNSConnectionForm.tsx @@ -0,0 +1,156 @@ +import { Controller, FormProvider, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { + Button, + FormControl, + Input, + ModalClose, + SecretInput, + Select, + SelectItem +} from "@app/components/v2"; +import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections"; +import { TPowerDNSConnection } from "@app/hooks/api/appConnections"; +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { PowerDNSConnectionMethod } from "@app/hooks/api/appConnections/types/powerdns-connection"; + +import { + genericAppConnectionFieldsSchema, + GenericAppConnectionsFields +} from "./GenericAppConnectionFields"; + +type Props = { + appConnection?: TPowerDNSConnection; + onSubmit: (formData: FormData) => Promise; +}; + +const rootSchema = genericAppConnectionFieldsSchema.extend({ + app: z.literal(AppConnection.PowerDNS) +}); + +const formSchema = z.discriminatedUnion("method", [ + rootSchema.extend({ + method: z.literal(PowerDNSConnectionMethod.APIKey), + credentials: z.object({ + apiKey: z.string().trim().min(1, "API Key required"), + baseUrl: z.string().trim().url("Must be a valid URL").min(1, "Base URL required") + }) + }) +]); + +type FormData = z.infer; + +export const PowerDNSConnectionForm = ({ appConnection, onSubmit }: Props) => { + const isUpdate = Boolean(appConnection); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: appConnection ?? { + app: AppConnection.PowerDNS, + method: PowerDNSConnectionMethod.APIKey, + credentials: { + apiKey: "", + baseUrl: "" + } + } + }); + + const { + handleSubmit, + control, + formState: { isSubmitting, isDirty } + } = form; + + return ( + +
+ {!isUpdate && } + ( + + + + )} + /> + ( + + onChange(e.target.value)} + placeholder="https://powerdns-admin/api/v1" + /> + + )} + /> + ( + + onChange(e.target.value)} + /> + + )} + /> +
+ + + + +
+ +
+ ); +};