diff --git a/backend/src/services/pki-sync/chef/chef-pki-sync-fns.ts b/backend/src/services/pki-sync/chef/chef-pki-sync-fns.ts index 0ee2a1228..bf740c660 100644 --- a/backend/src/services/pki-sync/chef/chef-pki-sync-fns.ts +++ b/backend/src/services/pki-sync/chef/chef-pki-sync-fns.ts @@ -139,6 +139,7 @@ export const chefPkiSyncFactory = ({ certificateDAL, certificateSyncDAL }: TChef cert: string; privateKey: string; certificateChain?: string; + caCertificate?: string; certificateId: string; isUpdate: boolean; targetItemName: string; @@ -150,15 +151,32 @@ export const chefPkiSyncFactory = ({ certificateDAL, certificateSyncDAL }: TChef const validationErrors: Array<{ name: string; error: string }> = []; const syncOptions = pkiSync.syncOptions as - | { canRemoveCertificates?: boolean; preserveItemOnRenewal?: boolean } + | { + canRemoveCertificates?: boolean; + preserveItemOnRenewal?: boolean; + fieldMappings?: { + certificate?: string; + privateKey?: string; + certificateChain?: string; + caCertificate?: string; + metadata?: string; + }; + } | undefined; const canRemoveCertificates = syncOptions?.canRemoveCertificates ?? true; const preserveItemOnRenewal = syncOptions?.preserveItemOnRenewal ?? true; + const fieldMappings = { + certificate: syncOptions?.fieldMappings?.certificate ?? "certificate", + privateKey: syncOptions?.fieldMappings?.privateKey ?? "private_key", + certificateChain: syncOptions?.fieldMappings?.certificateChain ?? "certificate_chain", + caCertificate: syncOptions?.fieldMappings?.caCertificate ?? "ca_certificate" + }; + const activeExternalIdentifiers = new Set(); for (const [certName, certData] of Object.entries(certificateMap)) { - const { cert, privateKey: certPrivateKey, certificateChain, certificateId } = certData; + const { cert, privateKey: certPrivateKey, certificateChain, caCertificate, certificateId } = certData; if (!cert || cert.trim().length === 0) { validationErrors.push({ @@ -218,6 +236,7 @@ export const chefPkiSyncFactory = ({ certificateDAL, certificateSyncDAL }: TChef cert, privateKey: certPrivateKey, certificateChain, + caCertificate, certificateId, isUpdate, targetItemName, @@ -240,18 +259,17 @@ export const chefPkiSyncFactory = ({ certificateDAL, certificateSyncDAL }: TChef cert, privateKey: certPrivateKey, certificateChain, - certificateId, - isUpdate + caCertificate, + certificateId } = certificateData; try { const chefDataBagItem: ChefCertificateDataBagItem = { id: targetItemName, - certificate: cert, - private_key: certPrivateKey, - ...(certificateChain && { certificate_chain: certificateChain }), - ...(isUpdate ? {} : { created_at: new Date().toISOString() }), - updated_at: new Date().toISOString() + [fieldMappings.certificate]: cert, + [fieldMappings.privateKey]: certPrivateKey, + ...(certificateChain && { [fieldMappings.certificateChain]: certificateChain }), + ...(caCertificate && { [fieldMappings.caCertificate]: caCertificate }) }; const itemExists = chefDataBagItems[targetItemName] === true; diff --git a/backend/src/services/pki-sync/chef/chef-pki-sync-schemas.ts b/backend/src/services/pki-sync/chef/chef-pki-sync-schemas.ts index 34e67d652..2d5298974 100644 --- a/backend/src/services/pki-sync/chef/chef-pki-sync-schemas.ts +++ b/backend/src/services/pki-sync/chef/chef-pki-sync-schemas.ts @@ -19,6 +19,13 @@ export const ChefPkiSyncConfigSchema = z.object({ ) }); +const ChefFieldMappingsSchema = z.object({ + certificate: z.string().min(1, "Certificate field name is required").default("certificate"), + privateKey: z.string().min(1, "Private key field name is required").default("private_key"), + certificateChain: z.string().min(1, "Certificate chain field name is required").default("certificate_chain"), + caCertificate: z.string().min(1, "CA certificate field name is required").default("ca_certificate") +}); + const ChefPkiSyncOptionsSchema = z.object({ canImportCertificates: z.boolean().default(false), canRemoveCertificates: z.boolean().default(true), @@ -57,7 +64,13 @@ const ChefPkiSyncOptionsSchema = z.object({ message: "Certificate item name schema must include {{certificateId}} placeholder and result in names that contain only alphanumeric characters, underscores, and hyphens and be 1-255 characters long for Chef data bag items." } - ) + ), + fieldMappings: ChefFieldMappingsSchema.optional().default({ + certificate: "certificate", + privateKey: "private_key", + certificateChain: "certificate_chain", + caCertificate: "ca_certificate" + }) }); export const ChefPkiSyncSchema = PkiSyncSchema.extend({ @@ -95,3 +108,5 @@ export const ChefPkiSyncListItemSchema = z.object({ canImportCertificates: z.literal(false), canRemoveCertificates: z.literal(true) }); + +export { ChefFieldMappingsSchema }; diff --git a/backend/src/services/pki-sync/chef/chef-pki-sync-types.ts b/backend/src/services/pki-sync/chef/chef-pki-sync-types.ts index 7e93c6906..52ea83ea6 100644 --- a/backend/src/services/pki-sync/chef/chef-pki-sync-types.ts +++ b/backend/src/services/pki-sync/chef/chef-pki-sync-types.ts @@ -3,6 +3,7 @@ import { z } from "zod"; import { TChefConnection } from "@app/ee/services/app-connections/chef/chef-connection-types"; import { + ChefFieldMappingsSchema, ChefPkiSyncConfigSchema, ChefPkiSyncSchema, CreateChefPkiSyncSchema, @@ -11,6 +12,8 @@ import { export type TChefPkiSyncConfig = z.infer; +export type TChefFieldMappings = z.infer; + export type TChefPkiSync = z.infer; export type TChefPkiSyncInput = z.infer; @@ -23,16 +26,7 @@ export type TChefPkiSyncWithCredentials = TChefPkiSync & { export interface ChefCertificateDataBagItem { id: string; - certificate: string; - private_key: string; - certificate_chain?: string; - common_name?: string; - alternative_names?: string; - serial_number?: string; - not_before?: string; - not_after?: string; - created_at?: string; - updated_at?: string; + [key: string]: string; } export interface SyncCertificatesResult { diff --git a/backend/src/services/pki-sync/pki-sync-queue.ts b/backend/src/services/pki-sync/pki-sync-queue.ts index 270a2165a..4f713a33f 100644 --- a/backend/src/services/pki-sync/pki-sync-queue.ts +++ b/backend/src/services/pki-sync/pki-sync-queue.ts @@ -236,13 +236,15 @@ export const pkiSyncQueueFactory = ({ } let certificateChain: string | undefined; + let caCertificate: string | undefined; try { if (certBody.encryptedCertificateChain) { const decryptedCertChain = await kmsDecryptor({ cipherTextBlob: certBody.encryptedCertificateChain }); certificateChain = decryptedCertChain.toString(); - } else if (certificate.caCertId) { + } + if (certificate.caCertId) { const { caCert, caCertChain } = await getCaCertChain({ caCertId: certificate.caCertId, certificateAuthorityDAL, @@ -250,7 +252,10 @@ export const pkiSyncQueueFactory = ({ projectDAL, kmsService }); - certificateChain = `${caCert}\n${caCertChain}`.trim(); + if (!certBody.encryptedCertificateChain) { + certificateChain = `${caCert}\n${caCertChain}`.trim(); + } + caCertificate = caCert; } } catch (chainError) { logger.warn( @@ -259,6 +264,7 @@ export const pkiSyncQueueFactory = ({ ); // Continue without certificate chain certificateChain = undefined; + caCertificate = undefined; } let certificateName: string; @@ -298,6 +304,7 @@ export const pkiSyncQueueFactory = ({ cert: certificatePem, privateKey: certPrivateKey || "", certificateChain, + caCertificate, alternativeNames, certificateId: certificate.id }; diff --git a/backend/src/services/pki-sync/pki-sync-types.ts b/backend/src/services/pki-sync/pki-sync-types.ts index f42f64a1b..fa76ddb55 100644 --- a/backend/src/services/pki-sync/pki-sync-types.ts +++ b/backend/src/services/pki-sync/pki-sync-types.ts @@ -73,7 +73,14 @@ export type TPkiSyncListItem = TPkiSync & { export type TCertificateMap = Record< string, - { cert: string; privateKey: string; certificateChain?: string; alternativeNames?: string[]; certificateId?: string } + { + cert: string; + privateKey: string; + certificateChain?: string; + caCertificate?: string; + alternativeNames?: string[]; + certificateId?: string; + } >; export type TCreatePkiSyncDTO = { diff --git a/docs/documentation/platform/pki/certificate-syncs/chef.mdx b/docs/documentation/platform/pki/certificate-syncs/chef.mdx index f2028bca3..c9cb2d675 100644 --- a/docs/documentation/platform/pki/certificate-syncs/chef.mdx +++ b/docs/documentation/platform/pki/certificate-syncs/chef.mdx @@ -41,40 +41,54 @@ Any role with these permissions would work such as a custom role with **Data Bag - **Enable Removal of Expired/Revoked Certificates**: If enabled, Infisical will remove certificates from the destination if they are no longer active in Infisical. - **Preserve Data Bag Item on Renewal**: Only applies to certificate renewals. When a certificate is renewed in Infisical, this option controls how the renewed certificate is handled. If enabled, the renewed certificate will update the existing data bag item, preserving the same item name. If disabled, the renewed certificate will be created as a new data bag item with a new name. - **Update Existing Certificates**: If enabled, Infisical will update existing data bag items when certificate content changes. - - **Certificate Name Schema** (Optional): Customize how certificate item names are generated in Chef data bags. Use `{{certificateId}}` as a placeholder for the certificate ID. Available placeholders: `{{certificateId}}`, `{{profileId}}`, `{{commonName}}`, `{{friendlyName}}`, `{{environment}}`. If not specified, defaults to `{{certificateId}}`. + - **Certificate Name Schema** (Optional): Customize how certificate item names are generated in Chef data bags. Use `{{certificateId}}` as a placeholder for the certificate ID. - **Auto-Sync Enabled**: If enabled, certificates will automatically be synced when changes occur. Disable to enforce manual syncing only. + 5. Configure the **Field Mappings** to customize how certificate data is stored in Chef data bag items, then click **Next**. + ![Configure Field Mappings](/images/platform/pki/certificate-syncs/chef/chef-field-mappings.png) + + - **Certificate Field**: The field name where the certificate will be stored in the data bag item (default: `certificate`) + - **Private Key Field**: The field name where the private key will be stored in the data bag item (default: `private_key`) + - **Certificate Chain Field**: The field name where the full certificate chain will be stored in the data bag item (default: `certificate_chain`) + - **CA Certificate Field**: The field name where the CA certificate will be stored in the data bag item (default: `ca_certificate`) + - **Chef Data Bag Item Structure**: Certificates are stored in Chef data bags as items with the following structure: + **Chef Data Bag Item Structure**: Certificates are stored in Chef data bags as items with the following structure (field names can be customized via field mappings): ```json { "id": "certificate-item-name", "certificate": "-----BEGIN CERTIFICATE-----\n...", "private_key": "-----BEGIN PRIVATE KEY-----\n...", "certificate_chain": "-----BEGIN CERTIFICATE-----\n...", - "metadata": { - "common_name": "example.com", - "serial_number": "1234567890", - "not_before": "2023-01-01T00:00:00Z", - "not_after": "2024-01-01T00:00:00Z" - } + "ca_certificate": "-----BEGIN CERTIFICATE-----\n..." + } + ``` + + **Example with Custom Field Mappings**: + ```json + { + "id": "certificate-item-name", + "ssl_cert": "-----BEGIN CERTIFICATE-----\n...", + "ssl_key": "-----BEGIN PRIVATE KEY-----\n...", + "ssl_chain": "-----BEGIN CERTIFICATE-----\n...", + "ssl_ca": "-----BEGIN CERTIFICATE-----\n..." } ``` - 5. Configure the **Details** of your Chef Certificate Sync, then click **Next**. + 6. Configure the **Details** of your Chef Certificate Sync, then click **Next**. ![Configure Details](/images/platform/pki/certificate-syncs/chef/chef-details.png) - **Name**: The name of your sync. Must be slug-friendly. - **Description**: An optional description for your sync. - 6. Select which certificates should be synced to Chef. + 7. Select which certificates should be synced to Chef. ![Select Certificates](/images/platform/pki/certificate-syncs/chef/chef-certificates.png) - 7. Review your Chef Certificate Sync configuration, then click **Create Sync**. + 8. Review your Chef Certificate Sync configuration, then click **Create Sync**. ![Confirm Configuration](/images/platform/pki/certificate-syncs/chef/chef-review.png) - 8. If enabled, your Chef Certificate Sync will begin syncing your certificates to the destination endpoint. + 9. If enabled, your Chef Certificate Sync will begin syncing your certificates to the destination endpoint. ![Sync Certificates](/images/platform/pki/certificate-syncs/chef/chef-synced.png) @@ -105,11 +119,15 @@ Any role with these permissions would work such as a custom role with **Data Bag ], "syncOptions": { "canRemoveCertificates": true, - "preserveItemOnRenewal": true, - "updateExistingCertificates": true, + "preserveArn": true, + "canImportCertificates": false, "certificateNameSchema": "myapp-{{certificateId}}", - "includeMetadata": true, - "encryptDataBag": true + "fieldMappings": { + "certificate": "ssl_cert", + "privateKey": "ssl_key", + "certificateChain": "ssl_chain", + "caCertificate": "ssl_ca" + } }, "destinationConfig": { "dataBagName": "ssl_certificates" @@ -117,6 +135,38 @@ Any role with these permissions would work such as a custom role with **Data Bag }' ``` + ### Example with Default Field Mappings + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/pki/syncs/chef \ + --header 'Authorization: Bearer ' \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-chef-cert-sync-default", + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "description": "Chef sync with default field mappings", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "destination": "chef", + "isAutoSyncEnabled": true, + "syncOptions": { + "canRemoveCertificates": true, + "preserveArn": true, + "canImportCertificates": false, + "certificateNameSchema": "{{commonName}}-{{certificateId}}", + "fieldMappings": { + "certificate": "certificate", + "privateKey": "private_key", + "certificateChain": "certificate_chain", + "caCertificate": "ca_certificate" + } + }, + "destinationConfig": { + "dataBagName": "certificates" + } + }' + ``` + ### Sample response ```json Response @@ -132,11 +182,15 @@ Any role with these permissions would work such as a custom role with **Data Bag }, "syncOptions": { "canRemoveCertificates": true, - "preserveItemOnRenewal": true, - "updateExistingCertificates": true, + "preserveArn": true, + "canImportCertificates": false, "certificateNameSchema": "myapp-{{certificateId}}", - "includeMetadata": true, - "encryptDataBag": true + "fieldMappings": { + "certificate": "ssl_cert", + "privateKey": "ssl_key", + "certificateChain": "ssl_chain", + "caCertificate": "ssl_ca" + } }, "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", @@ -153,11 +207,12 @@ Any role with these permissions would work such as a custom role with **Data Bag Your Chef Certificate Sync will: -- **Automatic Deployment**: Deploy certificates in Infisical to Chef data bags. +- **Automatic Deployment**: Deploy certificates in Infisical to Chef data bags with customizable field names - **Certificate Updates**: Update certificates in Chef data bags when renewals occur - **Expiration Handling**: Optionally remove expired certificates from Chef data bags (if enabled) -- **Format Preservation**: Maintain certificate format and metadata during sync operations -- **Data Bag Encryption**: Support Chef's encrypted data bag functionality for secure storage +- **Format Preservation**: Maintain certificate format during sync operations +- **Field Customization**: Map certificate data to custom field names that match your Chef cookbook requirements +- **CA Certificate Support**: Include CA certificates in data bag items for complete certificate chain management Chef Certificate Syncs support both automatic and manual diff --git a/docs/images/platform/pki/certificate-syncs/chef/chef-field-mappings.png b/docs/images/platform/pki/certificate-syncs/chef/chef-field-mappings.png new file mode 100644 index 000000000..de580f849 Binary files /dev/null and b/docs/images/platform/pki/certificate-syncs/chef/chef-field-mappings.png differ diff --git a/frontend/src/components/pki-syncs/CreatePkiSyncModal.tsx b/frontend/src/components/pki-syncs/CreatePkiSyncModal.tsx index 0169b2296..5b38415a5 100644 --- a/frontend/src/components/pki-syncs/CreatePkiSyncModal.tsx +++ b/frontend/src/components/pki-syncs/CreatePkiSyncModal.tsx @@ -64,7 +64,7 @@ export const CreatePkiSyncModal = ({ "Add Sync" ) } - className="max-w-2xl" + className="max-w-3xl" bodyClassName="overflow-visible" subTitle={ selectedSync ? undefined : "Select a third-party service to sync certificates to." diff --git a/frontend/src/components/pki-syncs/EditPkiSyncModal.tsx b/frontend/src/components/pki-syncs/EditPkiSyncModal.tsx index db5745aad..0fe3dabe6 100644 --- a/frontend/src/components/pki-syncs/EditPkiSyncModal.tsx +++ b/frontend/src/components/pki-syncs/EditPkiSyncModal.tsx @@ -15,11 +15,13 @@ type Props = { export const EditPkiSyncModal = ({ pkiSync, onOpenChange, fields, ...props }: Props) => { if (!pkiSync) return null; + const modalClassName = fields === PkiSyncEditFields.Mappings ? "max-w-4xl" : "max-w-2xl"; + return ( } - className="max-w-2xl" + className={modalClassName} bodyClassName="overflow-visible" > onOpenChange(false)} fields={fields} pkiSync={pkiSync} /> diff --git a/frontend/src/components/pki-syncs/forms/CreatePkiSyncForm.tsx b/frontend/src/components/pki-syncs/forms/CreatePkiSyncForm.tsx index 1dee6eaa7..e9c8c1f02 100644 --- a/frontend/src/components/pki-syncs/forms/CreatePkiSyncForm.tsx +++ b/frontend/src/components/pki-syncs/forms/CreatePkiSyncForm.tsx @@ -4,7 +4,6 @@ import { faInfoCircle } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Tab } from "@headlessui/react"; import { zodResolver } from "@hookform/resolvers/zod"; -import { twMerge } from "tailwind-merge"; import { createNotification } from "@app/components/notifications"; import { Button, FormControl, Switch } from "@app/components/v2"; @@ -16,6 +15,7 @@ import { PkiSyncFormSchema, TPkiSyncForm } from "./schemas/pki-sync-schema"; import { PkiSyncCertificatesFields } from "./PkiSyncCertificatesFields"; import { PkiSyncDestinationFields } from "./PkiSyncDestinationFields"; import { PkiSyncDetailsFields } from "./PkiSyncDetailsFields"; +import { PkiSyncFieldMappingsFields } from "./PkiSyncFieldMappingsFields"; import { PkiSyncOptionsFields } from "./PkiSyncOptionsFields"; import { PkiSyncReviewFields } from "./PkiSyncReviewFields"; @@ -26,13 +26,38 @@ type Props = { initialData?: any; }; -const FORM_TABS: { name: string; key: string; fields: (keyof TPkiSyncForm)[] }[] = [ - { name: "Destination", key: "destination", fields: ["connection", "destinationConfig"] }, - { name: "Sync Options", key: "options", fields: ["syncOptions"] }, - { name: "Details", key: "details", fields: ["name", "description"] }, - { name: "Certificates", key: "certificates", fields: ["certificateIds"] }, - { name: "Review", key: "review", fields: [] } -]; +const getFormTabs = ( + destination: PkiSync +): { name: string; key: string; fields: (keyof TPkiSyncForm)[] }[] => { + const baseTabs = [ + { + name: "Destination", + key: "destination", + fields: ["connection", "destinationConfig"] as (keyof TPkiSyncForm)[] + }, + { name: "Sync Options", key: "options", fields: ["syncOptions"] as (keyof TPkiSyncForm)[] } + ]; + + if (destination === PkiSync.Chef) { + baseTabs.push({ + name: "Mappings", + key: "mappings", + fields: ["syncOptions"] as (keyof TPkiSyncForm)[] + }); + } + + baseTabs.push( + { name: "Details", key: "details", fields: ["name", "description"] as (keyof TPkiSyncForm)[] }, + { + name: "Certificates", + key: "certificates", + fields: ["certificateIds"] as (keyof TPkiSyncForm)[] + }, + { name: "Review", key: "review", fields: [] as (keyof TPkiSyncForm)[] } + ); + + return baseTabs; +}; export const CreatePkiSyncForm = ({ destination, onComplete, onCancel, initialData }: Props) => { const createPkiSync = useCreatePkiSync(); @@ -42,6 +67,7 @@ export const CreatePkiSyncForm = ({ destination, onComplete, onCancel, initialDa const [showConfirmation, setShowConfirmation] = useState(false); const [selectedTabIndex, setSelectedTabIndex] = useState(0); + const FORM_TABS = getFormTabs(destination); const { syncOption } = usePkiSyncOption(destination); @@ -55,7 +81,15 @@ export const CreatePkiSyncForm = ({ destination, onComplete, onCancel, initialDa canImportCertificates: false, canRemoveCertificates: false, preserveArn: true, - certificateNameSchema: syncOption?.defaultCertificateNameSchema + certificateNameSchema: syncOption?.defaultCertificateNameSchema, + ...(destination === PkiSync.Chef && { + fieldMappings: { + certificate: "certificate", + privateKey: "private_key", + certificateChain: "certificate_chain", + caCertificate: "ca_certificate" + } + }) }, ...initialData } as Partial, @@ -78,11 +112,10 @@ export const CreatePkiSyncForm = ({ destination, onComplete, onCancel, initialDa }); createNotification({ - text: `Successfully created ${destinationName} Certificate Sync${ - certificateIds && certificateIds.length > 0 + text: `Successfully created ${destinationName} Certificate Sync${certificateIds && certificateIds.length > 0 ? ` with ${certificateIds.length} certificate(s)` : "" - }`, + }`, type: "success" }); onComplete(pkiSync); @@ -167,10 +200,10 @@ export const CreatePkiSyncForm = ({ destination, onComplete, onCancel, initialDa ); return ( -
+ - + {FORM_TABS.map((tab, index) => ( { @@ -179,10 +212,9 @@ export const CreatePkiSyncForm = ({ destination, onComplete, onCancel, initialDa setSelectedTabIndex((prev) => (isEnabled ? index : prev)); }} className={({ selected }) => - `-mb-[0.14rem] whitespace-nowrap ${index > selectedTabIndex ? "opacity-30" : ""} px-4 py-2 text-sm font-medium outline-hidden disabled:opacity-60 ${ - selected - ? "border-b-2 border-mineshaft-300 text-mineshaft-200" - : "text-bunker-300" + `-mb-[0.14rem] whitespace-nowrap ${index > selectedTabIndex ? "opacity-30" : ""} px-4 py-2 text-sm font-medium outline-hidden disabled:opacity-60 ${selected + ? "border-b-2 border-mineshaft-300 text-mineshaft-200" + : "text-bunker-300" }` } key={tab.key} @@ -191,11 +223,11 @@ export const CreatePkiSyncForm = ({ destination, onComplete, onCancel, initialDa ))} - - + + - + - + {destination === PkiSync.Chef && ( + + + + )} + - + - + -
+
diff --git a/frontend/src/components/pki-syncs/forms/EditPkiSyncForm.tsx b/frontend/src/components/pki-syncs/forms/EditPkiSyncForm.tsx index 9041d32f2..380ba039e 100644 --- a/frontend/src/components/pki-syncs/forms/EditPkiSyncForm.tsx +++ b/frontend/src/components/pki-syncs/forms/EditPkiSyncForm.tsx @@ -11,6 +11,7 @@ import { TPkiSync, useUpdatePkiSync } from "@app/hooks/api/pkiSyncs"; import { TUpdatePkiSyncForm, UpdatePkiSyncFormSchema } from "./schemas/pki-sync-schema"; import { PkiSyncDestinationFields } from "./PkiSyncDestinationFields"; import { PkiSyncDetailsFields } from "./PkiSyncDetailsFields"; +import { PkiSyncFieldMappingsFields } from "./PkiSyncFieldMappingsFields"; import { PkiSyncOptionsFields } from "./PkiSyncOptionsFields"; import { PkiSyncSourceFields } from "./PkiSyncSourceFields"; @@ -66,6 +67,9 @@ export const EditPkiSyncForm = ({ pkiSync, fields, onComplete }: Props) => { case PkiSyncEditFields.Options: Component = ; break; + case PkiSyncEditFields.Mappings: + Component = ; + break; case PkiSyncEditFields.Source: Component = ; break; diff --git a/frontend/src/components/pki-syncs/forms/PkiSyncFieldMappingsFields.tsx b/frontend/src/components/pki-syncs/forms/PkiSyncFieldMappingsFields.tsx new file mode 100644 index 000000000..fd9a31b5c --- /dev/null +++ b/frontend/src/components/pki-syncs/forms/PkiSyncFieldMappingsFields.tsx @@ -0,0 +1,103 @@ +import { Controller, useFormContext } from "react-hook-form"; + +import { FormControl, Input } from "@app/components/v2"; +import { PkiSync } from "@app/hooks/api/pkiSyncs"; + +import { TPkiSyncForm } from "./schemas/pki-sync-schema"; + +type Props = { + destination?: PkiSync; +}; + +export const PkiSyncFieldMappingsFields = ({ destination }: Props) => { + const { control, watch } = useFormContext(); + const currentDestination = destination || watch("destination"); + + // Only show field mappings for Chef + if (currentDestination !== PkiSync.Chef) { + return null; + } + + return ( + <> +

+ Configure how certificate fields are mapped to your Chef data bag items. +

+ +
+ ( + + + + )} + /> + + ( + + + + )} + /> + + ( + + + + )} + /> + + ( + + + + )} + /> +
+ +
+

Preview JSON Structure

+
+          {`{
+  "id": "certificate-item-name",
+  "${watch("syncOptions.fieldMappings.certificate") || "certificate"}": "",
+  "${watch("syncOptions.fieldMappings.privateKey") || "private_key"}": "",
+  "${watch("syncOptions.fieldMappings.certificateChain") || "certificate_chain"}": "",
+  "${watch("syncOptions.fieldMappings.caCertificate") || "ca_certificate"}": ""
+}`}
+        
+
+ + ); +}; diff --git a/frontend/src/components/pki-syncs/forms/schemas/chef-pki-sync-destination-schema.ts b/frontend/src/components/pki-syncs/forms/schemas/chef-pki-sync-destination-schema.ts index afb7704fb..7fa8a7020 100644 --- a/frontend/src/components/pki-syncs/forms/schemas/chef-pki-sync-destination-schema.ts +++ b/frontend/src/components/pki-syncs/forms/schemas/chef-pki-sync-destination-schema.ts @@ -4,6 +4,19 @@ import { PkiSync } from "@app/hooks/api/pkiSyncs"; import { BasePkiSyncSchema } from "./base-pki-sync-schema"; +const ChefFieldMappingsSchema = z.object({ + certificate: z.string().min(1, "Certificate field name is required").default("certificate"), + privateKey: z.string().min(1, "Private key field name is required").default("private_key"), + certificateChain: z + .string() + .min(1, "Certificate chain field name is required") + .default("certificate_chain"), + caCertificate: z + .string() + .min(1, "CA certificate field name is required") + .default("ca_certificate") +}); + const ChefSyncOptionsSchema = z.object({ canImportCertificates: z.boolean().default(false), canRemoveCertificates: z.boolean().default(true), @@ -44,7 +57,13 @@ const ChefSyncOptionsSchema = z.object({ message: "Certificate item name schema must include exactly one {{certificateId}} placeholder. It can also include {{environment}}, {{profileId}}, {{commonName}}, or {{friendlyName}} placeholders. Only alphanumeric characters (a-z, A-Z, 0-9), hyphens (-), and underscores (_) are allowed besides the placeholders." } - ) + ), + fieldMappings: ChefFieldMappingsSchema.optional().default({ + certificate: "certificate", + privateKey: "private_key", + certificateChain: "certificate_chain", + caCertificate: "ca_certificate" + }) }); export const ChefPkiSyncDestinationSchema = BasePkiSyncSchema(ChefSyncOptionsSchema).merge( diff --git a/frontend/src/components/pki-syncs/types/index.ts b/frontend/src/components/pki-syncs/types/index.ts index 093d065ab..954be4ff2 100644 --- a/frontend/src/components/pki-syncs/types/index.ts +++ b/frontend/src/components/pki-syncs/types/index.ts @@ -1,6 +1,7 @@ export enum PkiSyncEditFields { Details = "details", Options = "options", + Mappings = "mappings", Source = "source", Destination = "destination" } diff --git a/frontend/src/hooks/api/pkiSyncs/types/common.ts b/frontend/src/hooks/api/pkiSyncs/types/common.ts index 7d1853715..711a33a75 100644 --- a/frontend/src/hooks/api/pkiSyncs/types/common.ts +++ b/frontend/src/hooks/api/pkiSyncs/types/common.ts @@ -2,6 +2,13 @@ import { AppConnection } from "@app/hooks/api/appConnections/enums"; import { CertificateSyncStatus, PkiSyncStatus } from "../enums"; +export type TChefFieldMappings = { + certificate: string; + privateKey: string; + certificateChain: string; + caCertificate: string; +}; + export type RootPkiSyncOptions = { canImportCertificates: boolean; canRemoveCertificates: boolean; @@ -11,6 +18,7 @@ export type RootPkiSyncOptions = { enableVersioning?: boolean; preserveItemOnRenewal?: boolean; updateExistingCertificates?: boolean; + fieldMappings?: TChefFieldMappings; }; export type TRootPkiSync = { diff --git a/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/PkiSyncDetailsByIDPage.tsx b/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/PkiSyncDetailsByIDPage.tsx index b1b1b2836..16b105bc6 100644 --- a/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/PkiSyncDetailsByIDPage.tsx +++ b/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/PkiSyncDetailsByIDPage.tsx @@ -21,6 +21,7 @@ import { PkiSyncCertificatesSection, PkiSyncDestinationSection, PkiSyncDetailsSection, + PkiSyncFieldMappingsSection, PkiSyncOptionsSection } from "./components"; @@ -63,6 +64,7 @@ const PageContent = () => { const handleEditDetails = () => handlePopUpOpen("editSync", PkiSyncEditFields.Details); const handleEditOptions = () => handlePopUpOpen("editSync", PkiSyncEditFields.Options); + const handleEditMappings = () => handlePopUpOpen("editSync", PkiSyncEditFields.Mappings); const handleEditDestination = () => handlePopUpOpen("editSync", PkiSyncEditFields.Destination); return ( @@ -103,6 +105,7 @@ const PageContent = () => {
+
( +
+

{label}

+
{children}
+
+); + +type Props = { + pkiSync: TPkiSync; + onEditMappings: VoidFunction; +}; + +export const PkiSyncFieldMappingsSection = ({ pkiSync, onEditMappings }: Props) => { + // Only show for Chef PKI syncs + if (pkiSync.destination !== PkiSync.Chef) { + return null; + } + + const fieldMappings = pkiSync.syncOptions?.fieldMappings; + + const permissionSubject = subject(ProjectPermissionSub.PkiSyncs, { + subscriberId: pkiSync.subscriberId || "" + }); + + return ( +
+
+
+

Field Mappings

+ + {(isAllowed) => ( + + + + )} + +
+
+
+ + + {fieldMappings?.certificate || "certificate"} + + + + + + {fieldMappings?.privateKey || "private_key"} + + + + + + {fieldMappings?.certificateChain || "certificate_chain"} + + + + + + {fieldMappings?.caCertificate || "ca_certificate"} + + +
+
+
+
+ ); +}; diff --git a/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/index.ts b/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/index.ts index 55a877bff..5e6baa973 100644 --- a/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/index.ts +++ b/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/index.ts @@ -3,5 +3,6 @@ export { PkiSyncAuditLogsSection } from "./PkiSyncAuditLogsSection"; export { PkiSyncCertificatesSection } from "./PkiSyncCertificatesSection"; export { PkiSyncDestinationSection } from "./PkiSyncDestinationSection"; export { PkiSyncDetailsSection } from "./PkiSyncDetailsSection"; +export { PkiSyncFieldMappingsSection } from "./PkiSyncFieldMappingsSection"; export { PkiSyncOptionsSection } from "./PkiSyncOptionsSection"; export { PkiSyncSourceSection } from "./PkiSyncSourceSection";