Add Chef PKI sync
@@ -1,7 +1,12 @@
|
||||
import { seedData1 } from "@app/db/seed-data";
|
||||
import { ApproverType } from "@app/ee/services/access-approval-policy/access-approval-policy-types";
|
||||
|
||||
const createPolicy = async (dto: { name: string; secretPath: string; approvers: {type: ApproverType.User, id: string}[]; approvals: number }) => {
|
||||
const createPolicy = async (dto: {
|
||||
name: string;
|
||||
secretPath: string;
|
||||
approvers: { type: ApproverType.User; id: string }[];
|
||||
approvals: number;
|
||||
}) => {
|
||||
const res = await testServer.inject({
|
||||
method: "POST",
|
||||
url: `/api/v1/secret-approvals`,
|
||||
@@ -27,7 +32,7 @@ describe("Secret approval policy router", async () => {
|
||||
const policy = await createPolicy({
|
||||
secretPath: "/",
|
||||
approvals: 1,
|
||||
approvers: [{id:seedData1.id, type: ApproverType.User}],
|
||||
approvers: [{ id: seedData1.id, type: ApproverType.User }],
|
||||
name: "test-policy"
|
||||
});
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { execSync } from "child_process";
|
||||
import path from "path";
|
||||
import promptSync from "prompt-sync";
|
||||
import slugify from "@sindresorhus/slugify"
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
|
||||
const prompt = promptSync({ sigint: true });
|
||||
|
||||
|
||||
@@ -255,6 +255,37 @@ export const getChefDataBagItem = async ({
|
||||
}
|
||||
};
|
||||
|
||||
export const createChefDataBagItem = async ({
|
||||
serverUrl,
|
||||
userName,
|
||||
privateKey,
|
||||
orgName,
|
||||
dataBagName,
|
||||
data
|
||||
}: Omit<TUpdateChefDataBagItem, "dataBagItemName">): Promise<void> => {
|
||||
try {
|
||||
const path = `/organizations/${orgName}/data/${dataBagName}`;
|
||||
const body = JSON.stringify(data);
|
||||
|
||||
const hostServerUrl = await getChefServerUrl(serverUrl);
|
||||
|
||||
const headers = getChefAuthHeaders("POST", path, body, userName, privateKey);
|
||||
|
||||
await request.post(`${hostServerUrl}${path}`, data, {
|
||||
headers
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to create Chef data bag item: ${error.message || "Unknown error"}`
|
||||
});
|
||||
}
|
||||
throw new BadRequestError({
|
||||
message: "Unable to create Chef data bag item"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const updateChefDataBagItem = async ({
|
||||
serverUrl,
|
||||
userName,
|
||||
@@ -286,3 +317,34 @@ export const updateChefDataBagItem = async ({
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const removeChefDataBagItem = async ({
|
||||
serverUrl,
|
||||
userName,
|
||||
privateKey,
|
||||
orgName,
|
||||
dataBagName,
|
||||
dataBagItemName
|
||||
}: Omit<TUpdateChefDataBagItem, "data">): Promise<void> => {
|
||||
try {
|
||||
const path = `/organizations/${orgName}/data/${dataBagName}/${dataBagItemName}`;
|
||||
const body = "";
|
||||
|
||||
const hostServerUrl = await getChefServerUrl(serverUrl);
|
||||
|
||||
const headers = getChefAuthHeaders("DELETE", path, body, userName, privateKey);
|
||||
|
||||
await request.delete(`${hostServerUrl}${path}`, {
|
||||
headers
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to remove Chef data bag item: ${error.message || "Unknown error"}`
|
||||
});
|
||||
}
|
||||
throw new BadRequestError({
|
||||
message: "Unable to remove Chef data bag item"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { ChefPkiSyncSchema, CreateChefPkiSyncSchema, UpdateChefPkiSyncSchema } from "@app/services/pki-sync/chef";
|
||||
import { PkiSync } from "@app/services/pki-sync/pki-sync-enums";
|
||||
|
||||
import { registerSyncPkiEndpoints } from "./pki-sync-endpoints";
|
||||
|
||||
export const registerChefPkiSyncRouter = async (server: FastifyZodProvider) =>
|
||||
registerSyncPkiEndpoints({
|
||||
destination: PkiSync.Chef,
|
||||
server,
|
||||
responseSchema: ChefPkiSyncSchema,
|
||||
createSchema: CreateChefPkiSyncSchema,
|
||||
updateSchema: UpdateChefPkiSyncSchema,
|
||||
syncOptions: {
|
||||
canImportCertificates: false,
|
||||
canRemoveCertificates: true
|
||||
}
|
||||
});
|
||||
@@ -2,10 +2,12 @@ import { PkiSync } from "@app/services/pki-sync/pki-sync-enums";
|
||||
|
||||
import { registerAwsCertificateManagerPkiSyncRouter } from "./aws-certificate-manager-pki-sync-router";
|
||||
import { registerAzureKeyVaultPkiSyncRouter } from "./azure-key-vault-pki-sync-router";
|
||||
import { registerChefPkiSyncRouter } from "./chef-pki-sync-router";
|
||||
|
||||
export * from "./pki-sync-router";
|
||||
|
||||
export const PKI_SYNC_REGISTER_ROUTER_MAP: Record<PkiSync, (server: FastifyZodProvider) => Promise<void>> = {
|
||||
[PkiSync.AzureKeyVault]: registerAzureKeyVaultPkiSyncRouter,
|
||||
[PkiSync.AwsCertificateManager]: registerAwsCertificateManagerPkiSyncRouter
|
||||
[PkiSync.AwsCertificateManager]: registerAwsCertificateManagerPkiSyncRouter,
|
||||
[PkiSync.Chef]: registerChefPkiSyncRouter
|
||||
};
|
||||
|
||||
@@ -170,7 +170,8 @@ const PKI_APP_CONNECTIONS = [
|
||||
AppConnection.AWS,
|
||||
AppConnection.Cloudflare,
|
||||
AppConnection.AzureADCS,
|
||||
AppConnection.AzureKeyVault
|
||||
AppConnection.AzureKeyVault,
|
||||
AppConnection.Chef
|
||||
];
|
||||
|
||||
export const listAppConnectionOptions = (projectType?: ProjectType) => {
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
export const CHEF_PKI_SYNC_CERTIFICATE_NAMING = {
|
||||
NAME_PATTERN: /^[a-zA-Z0-9_-]+$/,
|
||||
FORBIDDEN_CHARACTERS: "[]{}()<>|\\:;\"'=+*&^%$#@!~`?/",
|
||||
MIN_LENGTH: 1,
|
||||
MAX_LENGTH: 255,
|
||||
DEFAULT_SCHEMA: "{{certificateId}}"
|
||||
};
|
||||
|
||||
export const CHEF_PKI_SYNC_DATA_BAG_NAMING = {
|
||||
NAME_PATTERN: /^[a-zA-Z0-9_-]+$/,
|
||||
FORBIDDEN_CHARACTERS: "[]{}()<>|\\:;\"'=+*&^%$#@!~`?/.",
|
||||
MIN_LENGTH: 1,
|
||||
MAX_LENGTH: 255
|
||||
};
|
||||
|
||||
export const CHEF_PKI_SYNC_DEFAULTS = {
|
||||
CERTIFICATE_DATA_BAG: "ssl_certificates",
|
||||
ITEM_NAME_TEMPLATE: "{{certificateId}}",
|
||||
INFISICAL_PREFIX: "Infisical-",
|
||||
DEFAULT_ENVIRONMENT: "global"
|
||||
} as const;
|
||||
577
backend/src/services/pki-sync/chef/chef-pki-sync-fns.ts
Normal file
@@ -0,0 +1,577 @@
|
||||
/* eslint-disable no-continue */
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import { TCertificateSyncs } from "@app/db/schemas";
|
||||
import {
|
||||
createChefDataBagItem,
|
||||
listChefDataBagItems,
|
||||
removeChefDataBagItem,
|
||||
updateChefDataBagItem
|
||||
} from "@app/ee/services/app-connections/chef";
|
||||
import { TChefDataBagItemContent } from "@app/ee/services/secret-sync/chef";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||
import { TCertificateSyncDALFactory } from "@app/services/certificate-sync/certificate-sync-dal";
|
||||
import { CertificateSyncStatus } from "@app/services/certificate-sync/certificate-sync-enums";
|
||||
import { createConnectionQueue, RateLimitConfig } from "@app/services/connection-queue";
|
||||
import { matchesCertificateNameSchema } from "@app/services/pki-sync/pki-sync-fns";
|
||||
import { TCertificateMap, TPkiSyncWithCredentials } from "@app/services/pki-sync/pki-sync-types";
|
||||
|
||||
import { CHEF_PKI_SYNC_DEFAULTS } from "./chef-pki-sync-constants";
|
||||
import { ChefCertificateDataBagItem, SyncCertificatesResult, TChefPkiSyncWithCredentials } from "./chef-pki-sync-types";
|
||||
|
||||
const CHEF_RATE_LIMIT_CONFIG: RateLimitConfig = {
|
||||
MAX_CONCURRENT_REQUESTS: 5, // Chef servers generally have lower rate limits
|
||||
BASE_DELAY: 1500,
|
||||
MAX_DELAY: 30000,
|
||||
MAX_RETRIES: 3,
|
||||
RATE_LIMIT_STATUS_CODES: [429, 503]
|
||||
};
|
||||
|
||||
const chefConnectionQueue = createConnectionQueue(CHEF_RATE_LIMIT_CONFIG);
|
||||
const { withRateLimitRetry } = chefConnectionQueue;
|
||||
|
||||
const isInfisicalManagedCertificate = (certificateName: string, pkiSync: TPkiSyncWithCredentials): boolean => {
|
||||
const syncOptions = pkiSync.syncOptions as { certificateNameSchema?: string } | undefined;
|
||||
const certificateNameSchema = syncOptions?.certificateNameSchema;
|
||||
|
||||
if (certificateNameSchema) {
|
||||
const environment = CHEF_PKI_SYNC_DEFAULTS.DEFAULT_ENVIRONMENT;
|
||||
return matchesCertificateNameSchema(certificateName, environment, certificateNameSchema);
|
||||
}
|
||||
|
||||
return certificateName.startsWith(CHEF_PKI_SYNC_DEFAULTS.INFISICAL_PREFIX);
|
||||
};
|
||||
|
||||
const parseErrorMessage = (error: unknown): string => {
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
|
||||
if (typeof error === "string") {
|
||||
return error;
|
||||
}
|
||||
|
||||
if (error && typeof error === "object" && "message" in error) {
|
||||
const { message } = error as { message: unknown };
|
||||
if (typeof message === "string") {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
return "Unknown error occurred";
|
||||
};
|
||||
|
||||
type TChefPkiSyncFactoryDeps = {
|
||||
certificateDAL: Pick<TCertificateDALFactory, "findById">;
|
||||
certificateSyncDAL: Pick<
|
||||
TCertificateSyncDALFactory,
|
||||
| "removeCertificates"
|
||||
| "addCertificates"
|
||||
| "findByPkiSyncAndCertificate"
|
||||
| "updateById"
|
||||
| "findByPkiSyncId"
|
||||
| "updateSyncStatus"
|
||||
>;
|
||||
};
|
||||
|
||||
export const chefPkiSyncFactory = ({ certificateDAL, certificateSyncDAL }: TChefPkiSyncFactoryDeps) => {
|
||||
const $getChefDataBagItems = async (
|
||||
pkiSync: TChefPkiSyncWithCredentials,
|
||||
syncId = "unknown"
|
||||
): Promise<Record<string, boolean>> => {
|
||||
const {
|
||||
connection,
|
||||
destinationConfig: { dataBagName }
|
||||
} = pkiSync;
|
||||
const { serverUrl, userName, privateKey, orgName } = connection.credentials;
|
||||
|
||||
const dataBagItems = await withRateLimitRetry(
|
||||
() =>
|
||||
listChefDataBagItems(
|
||||
{
|
||||
credentials: { serverUrl, userName, privateKey, orgName }
|
||||
} as Parameters<typeof listChefDataBagItems>[0],
|
||||
dataBagName
|
||||
),
|
||||
{
|
||||
operation: "list-chef-data-bag-items",
|
||||
syncId
|
||||
}
|
||||
);
|
||||
|
||||
const chefDataBagItems: Record<string, boolean> = {};
|
||||
dataBagItems.forEach((item) => {
|
||||
chefDataBagItems[item.name] = true;
|
||||
});
|
||||
|
||||
return chefDataBagItems;
|
||||
};
|
||||
|
||||
const syncCertificates = async (
|
||||
pkiSync: TPkiSyncWithCredentials,
|
||||
certificateMap: TCertificateMap
|
||||
): Promise<SyncCertificatesResult> => {
|
||||
const chefPkiSync = pkiSync as unknown as TChefPkiSyncWithCredentials;
|
||||
const {
|
||||
connection,
|
||||
destinationConfig: { dataBagName }
|
||||
} = chefPkiSync;
|
||||
const { serverUrl, userName, privateKey, orgName } = connection.credentials;
|
||||
|
||||
const chefDataBagItems = await $getChefDataBagItems(chefPkiSync, pkiSync.id);
|
||||
|
||||
const existingSyncRecords = await certificateSyncDAL.findByPkiSyncId(pkiSync.id);
|
||||
const syncRecordsByCertId = new Map<string, TCertificateSyncs>();
|
||||
const syncRecordsByExternalId = new Map<string, TCertificateSyncs>();
|
||||
|
||||
existingSyncRecords.forEach((record: TCertificateSyncs) => {
|
||||
if (record.certificateId) {
|
||||
syncRecordsByCertId.set(record.certificateId, record);
|
||||
}
|
||||
if (record.externalIdentifier) {
|
||||
syncRecordsByExternalId.set(record.externalIdentifier, record);
|
||||
}
|
||||
});
|
||||
|
||||
type CertificateUploadData = {
|
||||
key: string;
|
||||
name: string;
|
||||
cert: string;
|
||||
privateKey: string;
|
||||
certificateChain?: string;
|
||||
certificateId: string;
|
||||
isUpdate: boolean;
|
||||
targetItemName: string;
|
||||
oldCertificateIdToRemove?: string;
|
||||
};
|
||||
|
||||
const setCertificates: CertificateUploadData[] = [];
|
||||
|
||||
const validationErrors: Array<{ name: string; error: string }> = [];
|
||||
|
||||
const syncOptions = pkiSync.syncOptions as
|
||||
| { canRemoveCertificates?: boolean; preserveItemOnRenewal?: boolean }
|
||||
| undefined;
|
||||
const canRemoveCertificates = syncOptions?.canRemoveCertificates ?? true;
|
||||
const preserveItemOnRenewal = syncOptions?.preserveItemOnRenewal ?? true;
|
||||
|
||||
const activeExternalIdentifiers = new Set<string>();
|
||||
|
||||
for (const [certName, certData] of Object.entries(certificateMap)) {
|
||||
const { cert, privateKey: certPrivateKey, certificateChain, certificateId } = certData;
|
||||
|
||||
if (!cert || cert.trim().length === 0) {
|
||||
validationErrors.push({
|
||||
name: certName,
|
||||
error: "Certificate content is empty or missing"
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!certPrivateKey || certPrivateKey.trim().length === 0) {
|
||||
validationErrors.push({
|
||||
name: certName,
|
||||
error: "Private key content is empty or missing"
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!certificateId || typeof certificateId !== "string") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const targetCertificateName = certName;
|
||||
|
||||
const certificate = await certificateDAL.findById(certificateId);
|
||||
|
||||
if (certificate?.renewedByCertificateId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const syncRecordLookupId = certificate?.renewedFromCertificateId || certificateId;
|
||||
const existingSyncRecord = syncRecordsByCertId.get(syncRecordLookupId);
|
||||
|
||||
let shouldProcess = true;
|
||||
let isUpdate = false;
|
||||
let targetItemName = targetCertificateName;
|
||||
|
||||
if (existingSyncRecord?.externalIdentifier) {
|
||||
const existingChefItem = chefDataBagItems[existingSyncRecord.externalIdentifier];
|
||||
|
||||
if (existingChefItem) {
|
||||
if (certificate?.renewedFromCertificateId && preserveItemOnRenewal) {
|
||||
targetItemName = existingSyncRecord.externalIdentifier;
|
||||
isUpdate = true;
|
||||
} else if (!certificate?.renewedFromCertificateId) {
|
||||
shouldProcess = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!shouldProcess) {
|
||||
continue;
|
||||
}
|
||||
|
||||
setCertificates.push({
|
||||
key: certName,
|
||||
name: certName,
|
||||
cert,
|
||||
privateKey: certPrivateKey,
|
||||
certificateChain,
|
||||
certificateId,
|
||||
isUpdate,
|
||||
targetItemName,
|
||||
oldCertificateIdToRemove:
|
||||
certificate?.renewedFromCertificateId && preserveItemOnRenewal
|
||||
? certificate.renewedFromCertificateId
|
||||
: undefined
|
||||
});
|
||||
|
||||
activeExternalIdentifiers.add(targetItemName);
|
||||
}
|
||||
|
||||
type UploadResult =
|
||||
| { status: "fulfilled"; certificate: CertificateUploadData }
|
||||
| { status: "rejected"; certificate: CertificateUploadData; error: unknown };
|
||||
|
||||
const uploadPromises = setCertificates.map(async (certificateData): Promise<UploadResult> => {
|
||||
const {
|
||||
targetItemName,
|
||||
cert,
|
||||
privateKey: certPrivateKey,
|
||||
certificateChain,
|
||||
certificateId,
|
||||
isUpdate
|
||||
} = 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()
|
||||
};
|
||||
|
||||
const itemExists = chefDataBagItems[targetItemName] === true;
|
||||
|
||||
if (itemExists) {
|
||||
await withRateLimitRetry(
|
||||
() =>
|
||||
updateChefDataBagItem({
|
||||
serverUrl,
|
||||
userName,
|
||||
privateKey,
|
||||
orgName,
|
||||
dataBagName,
|
||||
dataBagItemName: targetItemName,
|
||||
data: chefDataBagItem as unknown as TChefDataBagItemContent
|
||||
}),
|
||||
{
|
||||
operation: "update-chef-data-bag-item",
|
||||
syncId: pkiSync.id
|
||||
}
|
||||
);
|
||||
} else {
|
||||
await withRateLimitRetry(
|
||||
() =>
|
||||
createChefDataBagItem({
|
||||
serverUrl,
|
||||
userName,
|
||||
privateKey,
|
||||
orgName,
|
||||
dataBagName,
|
||||
data: chefDataBagItem as unknown as TChefDataBagItemContent
|
||||
}),
|
||||
{
|
||||
operation: "create-chef-data-bag-item",
|
||||
syncId: pkiSync.id
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return { status: "fulfilled" as const, certificate: certificateData };
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
{
|
||||
syncId: pkiSync.id,
|
||||
certificateId,
|
||||
targetItemName,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
},
|
||||
"Failed to sync certificate to Chef"
|
||||
);
|
||||
return { status: "rejected" as const, certificate: certificateData, error };
|
||||
}
|
||||
});
|
||||
|
||||
const uploadResults = await Promise.allSettled(uploadPromises);
|
||||
|
||||
const successfulUploads = uploadResults.filter(
|
||||
(result): result is PromiseFulfilledResult<UploadResult> =>
|
||||
result.status === "fulfilled" && result.value.status === "fulfilled"
|
||||
);
|
||||
const failedUploads = uploadResults.filter(
|
||||
(
|
||||
result
|
||||
): result is
|
||||
| PromiseRejectedResult
|
||||
| PromiseFulfilledResult<{ status: "rejected"; certificate: CertificateUploadData; error: unknown }> =>
|
||||
result.status === "rejected" || (result.status === "fulfilled" && result.value.status === "rejected")
|
||||
);
|
||||
|
||||
let removedCount = 0;
|
||||
let failedRemovals: Array<{ name: string; error: string }> = [];
|
||||
|
||||
if (canRemoveCertificates) {
|
||||
const itemsToRemove: string[] = [];
|
||||
|
||||
Object.keys(chefDataBagItems).forEach((itemName) => {
|
||||
if (!activeExternalIdentifiers.has(itemName) && isInfisicalManagedCertificate(itemName, pkiSync)) {
|
||||
itemsToRemove.push(itemName);
|
||||
}
|
||||
});
|
||||
|
||||
if (itemsToRemove.length > 0) {
|
||||
const removalPromises = itemsToRemove.map(async (itemName) => {
|
||||
try {
|
||||
await withRateLimitRetry(
|
||||
() =>
|
||||
removeChefDataBagItem({
|
||||
serverUrl,
|
||||
userName,
|
||||
privateKey,
|
||||
orgName,
|
||||
dataBagName,
|
||||
dataBagItemName: itemName
|
||||
}),
|
||||
{
|
||||
operation: "remove-chef-data-bag-item",
|
||||
syncId: pkiSync.id
|
||||
}
|
||||
);
|
||||
|
||||
const syncRecord = syncRecordsByExternalId.get(itemName);
|
||||
if (syncRecord?.certificateId) {
|
||||
await certificateSyncDAL.removeCertificates(pkiSync.id, [syncRecord.certificateId]);
|
||||
}
|
||||
|
||||
return { status: "fulfilled" as const, itemName };
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
{
|
||||
syncId: pkiSync.id,
|
||||
itemName,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
},
|
||||
"Failed to remove Chef data bag item"
|
||||
);
|
||||
return { status: "rejected" as const, itemName, error };
|
||||
}
|
||||
});
|
||||
|
||||
const removalResults = await Promise.allSettled(removalPromises);
|
||||
|
||||
const successfulRemovals = removalResults.filter(
|
||||
(result): result is PromiseFulfilledResult<{ status: "fulfilled"; itemName: string }> =>
|
||||
result.status === "fulfilled" && result.value.status === "fulfilled"
|
||||
);
|
||||
removedCount = successfulRemovals.length;
|
||||
|
||||
const failedRemovalPromises = removalResults.filter(
|
||||
(
|
||||
result
|
||||
): result is
|
||||
| PromiseRejectedResult
|
||||
| PromiseFulfilledResult<{ status: "rejected"; itemName: string; error: unknown }> =>
|
||||
result.status === "rejected" || (result.status === "fulfilled" && result.value.status === "rejected")
|
||||
);
|
||||
|
||||
failedRemovals = failedRemovalPromises.map((result) => {
|
||||
if (result.status === "rejected") {
|
||||
return {
|
||||
name: "unknown",
|
||||
error: parseErrorMessage(result.reason)
|
||||
};
|
||||
}
|
||||
const { itemName, error } = result.value;
|
||||
return {
|
||||
name: String(itemName),
|
||||
error: parseErrorMessage(error)
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const result of successfulUploads) {
|
||||
const { certificateId, targetItemName, oldCertificateIdToRemove } = result.value.certificate;
|
||||
|
||||
if (certificateId && typeof certificateId === "string") {
|
||||
const existingCertSync = await certificateSyncDAL.findByPkiSyncAndCertificate(pkiSync.id, certificateId);
|
||||
if (existingCertSync) {
|
||||
await certificateSyncDAL.updateById(existingCertSync.id, {
|
||||
externalIdentifier: targetItemName,
|
||||
syncStatus: CertificateSyncStatus.Succeeded,
|
||||
lastSyncedAt: new Date(),
|
||||
lastSyncMessage: "Certificate successfully synced to destination"
|
||||
});
|
||||
} else {
|
||||
await certificateSyncDAL.addCertificates(pkiSync.id, [
|
||||
{
|
||||
certificateId,
|
||||
externalIdentifier: targetItemName
|
||||
}
|
||||
]);
|
||||
|
||||
const newCertSync = await certificateSyncDAL.findByPkiSyncAndCertificate(pkiSync.id, certificateId);
|
||||
if (newCertSync) {
|
||||
await certificateSyncDAL.updateById(newCertSync.id, {
|
||||
syncStatus: CertificateSyncStatus.Succeeded,
|
||||
lastSyncedAt: new Date(),
|
||||
lastSyncMessage: "Certificate successfully synced to destination"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (oldCertificateIdToRemove) {
|
||||
await certificateSyncDAL.removeCertificates(pkiSync.id, [oldCertificateIdToRemove]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
failedUploads.map(async (result) => {
|
||||
let certificateId: string;
|
||||
let errorMessage: string;
|
||||
|
||||
if (result.status === "rejected") {
|
||||
certificateId = "unknown";
|
||||
errorMessage = result.reason instanceof Error ? result.reason.message : String(result.reason);
|
||||
return;
|
||||
}
|
||||
|
||||
const { certificate, error } = result.value;
|
||||
certificateId = certificate.certificateId;
|
||||
errorMessage = error instanceof Error ? error.message : String(error);
|
||||
|
||||
const existingSyncRecord = syncRecordsByCertId.get(certificateId);
|
||||
if (existingSyncRecord) {
|
||||
await certificateSyncDAL.updateSyncStatus(
|
||||
pkiSync.id,
|
||||
certificateId,
|
||||
CertificateSyncStatus.Failed,
|
||||
errorMessage
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
uploaded: successfulUploads.filter((result) => !result.value.certificate.isUpdate).length,
|
||||
updated: successfulUploads.filter((result) => result.value.certificate.isUpdate).length,
|
||||
removed: removedCount,
|
||||
failedRemovals: failedRemovals.length,
|
||||
skipped: validationErrors.length,
|
||||
details: {
|
||||
failedUploads: failedUploads.map((result) => {
|
||||
if (result.status === "rejected") {
|
||||
return {
|
||||
name: "unknown",
|
||||
error: result.reason instanceof Error ? result.reason.message : String(result.reason)
|
||||
};
|
||||
}
|
||||
const { certificate, error } = result.value;
|
||||
return {
|
||||
name: certificate.name,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
};
|
||||
}),
|
||||
failedRemovals,
|
||||
validationErrors
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const importCertificates = async (): Promise<SyncCertificatesResult> => {
|
||||
throw new Error("Chef PKI Sync does not support importing certificates from Chef data bags");
|
||||
};
|
||||
|
||||
const removeCertificates = async (
|
||||
sync: TPkiSyncWithCredentials,
|
||||
certificateNames: string[],
|
||||
deps?: { certificateSyncDAL?: TCertificateSyncDALFactory; certificateMap?: TCertificateMap }
|
||||
): Promise<void> => {
|
||||
const chefPkiSync = sync as unknown as TChefPkiSyncWithCredentials;
|
||||
const {
|
||||
connection,
|
||||
destinationConfig: { dataBagName }
|
||||
} = chefPkiSync;
|
||||
const { serverUrl, userName, privateKey, orgName } = connection.credentials;
|
||||
|
||||
const existingSyncRecords = await certificateSyncDAL.findByPkiSyncId(sync.id);
|
||||
const certificateIdsToRemove: string[] = [];
|
||||
const itemsToRemove: string[] = [];
|
||||
|
||||
for (const certName of certificateNames) {
|
||||
const certificateData = deps?.certificateMap?.[certName];
|
||||
if (certificateData?.certificateId && typeof certificateData.certificateId === "string") {
|
||||
const syncRecord = existingSyncRecords.find((record) => record.certificateId === certificateData.certificateId);
|
||||
if (syncRecord) {
|
||||
certificateIdsToRemove.push(certificateData.certificateId);
|
||||
if (syncRecord.externalIdentifier) {
|
||||
itemsToRemove.push(syncRecord.externalIdentifier);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const targetName = certName;
|
||||
const syncRecord = existingSyncRecords.find((record) => record.externalIdentifier === targetName);
|
||||
if (syncRecord && syncRecord.certificateId) {
|
||||
certificateIdsToRemove.push(syncRecord.certificateId);
|
||||
itemsToRemove.push(targetName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const removalPromises = itemsToRemove.map(async (itemName) => {
|
||||
try {
|
||||
await withRateLimitRetry(
|
||||
() =>
|
||||
removeChefDataBagItem({
|
||||
serverUrl,
|
||||
userName,
|
||||
privateKey,
|
||||
orgName,
|
||||
dataBagName,
|
||||
dataBagItemName: itemName
|
||||
}),
|
||||
{
|
||||
operation: "remove-chef-data-bag-item",
|
||||
syncId: sync.id
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
{
|
||||
syncId: sync.id,
|
||||
itemName,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
},
|
||||
"Failed to remove Chef data bag item during certificate removal"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.allSettled(removalPromises);
|
||||
|
||||
if (certificateIdsToRemove.length > 0) {
|
||||
await certificateSyncDAL.removeCertificates(sync.id, certificateIdsToRemove);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
syncCertificates,
|
||||
importCertificates,
|
||||
removeCertificates
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { PkiSync } from "@app/services/pki-sync/pki-sync-enums";
|
||||
|
||||
export const CHEF_PKI_SYNC_LIST_OPTION = {
|
||||
name: "Chef" as const,
|
||||
connection: AppConnection.Chef,
|
||||
destination: PkiSync.Chef,
|
||||
canImportCertificates: false,
|
||||
canRemoveCertificates: true
|
||||
} as const;
|
||||
97
backend/src/services/pki-sync/chef/chef-pki-sync-schemas.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import RE2 from "re2";
|
||||
import { z } from "zod";
|
||||
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { PkiSync } from "@app/services/pki-sync/pki-sync-enums";
|
||||
import { PkiSyncSchema } from "@app/services/pki-sync/pki-sync-schemas";
|
||||
|
||||
import { CHEF_PKI_SYNC_CERTIFICATE_NAMING, CHEF_PKI_SYNC_DATA_BAG_NAMING } from "./chef-pki-sync-constants";
|
||||
|
||||
export const ChefPkiSyncConfigSchema = z.object({
|
||||
dataBagName: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Data bag name required")
|
||||
.max(255, "Data bag name cannot exceed 255 characters")
|
||||
.refine(
|
||||
(name) => CHEF_PKI_SYNC_DATA_BAG_NAMING.NAME_PATTERN.test(name),
|
||||
"Data bag name can only contain alphanumeric characters, underscores, and hyphens"
|
||||
)
|
||||
});
|
||||
|
||||
const ChefPkiSyncOptionsSchema = z.object({
|
||||
canImportCertificates: z.boolean().default(false),
|
||||
canRemoveCertificates: z.boolean().default(true),
|
||||
preserveItemOnRenewal: z.boolean().default(true),
|
||||
updateExistingCertificates: z.boolean().default(true),
|
||||
certificateNameSchema: z
|
||||
.string()
|
||||
.optional()
|
||||
.refine(
|
||||
(schema) => {
|
||||
if (!schema) return true;
|
||||
|
||||
if (!schema.includes("{{certificateId}}")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const testName = schema
|
||||
.replace(new RE2("\\{\\{certificateId\\}\\}", "g"), "test-cert-id")
|
||||
.replace(new RE2("\\{\\{profileId\\}\\}", "g"), "test-profile-id")
|
||||
.replace(new RE2("\\{\\{commonName\\}\\}", "g"), "test-common-name")
|
||||
.replace(new RE2("\\{\\{friendlyName\\}\\}", "g"), "test-friendly-name")
|
||||
.replace(new RE2("\\{\\{environment\\}\\}", "g"), "test-env");
|
||||
|
||||
const hasForbiddenChars = CHEF_PKI_SYNC_CERTIFICATE_NAMING.FORBIDDEN_CHARACTERS.split("").some((char) =>
|
||||
testName.includes(char)
|
||||
);
|
||||
|
||||
return (
|
||||
CHEF_PKI_SYNC_CERTIFICATE_NAMING.NAME_PATTERN.test(testName) &&
|
||||
!hasForbiddenChars &&
|
||||
testName.length >= CHEF_PKI_SYNC_CERTIFICATE_NAMING.MIN_LENGTH &&
|
||||
testName.length <= CHEF_PKI_SYNC_CERTIFICATE_NAMING.MAX_LENGTH
|
||||
);
|
||||
},
|
||||
{
|
||||
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. Available placeholders: {{certificateId}}, {{profileId}}, {{commonName}}, {{friendlyName}}, {{environment}}"
|
||||
}
|
||||
)
|
||||
});
|
||||
|
||||
export const ChefPkiSyncSchema = PkiSyncSchema.extend({
|
||||
destination: z.literal(PkiSync.Chef),
|
||||
destinationConfig: ChefPkiSyncConfigSchema,
|
||||
syncOptions: ChefPkiSyncOptionsSchema
|
||||
});
|
||||
|
||||
export const CreateChefPkiSyncSchema = z.object({
|
||||
name: z.string().trim().min(1).max(64),
|
||||
description: z.string().optional(),
|
||||
isAutoSyncEnabled: z.boolean().default(true),
|
||||
destinationConfig: ChefPkiSyncConfigSchema,
|
||||
syncOptions: ChefPkiSyncOptionsSchema.optional().default({}),
|
||||
subscriberId: z.string().nullish(),
|
||||
connectionId: z.string(),
|
||||
projectId: z.string().trim().min(1),
|
||||
certificateIds: z.array(z.string().uuid()).optional()
|
||||
});
|
||||
|
||||
export const UpdateChefPkiSyncSchema = z.object({
|
||||
name: z.string().trim().min(1).max(64).optional(),
|
||||
description: z.string().optional(),
|
||||
isAutoSyncEnabled: z.boolean().optional(),
|
||||
destinationConfig: ChefPkiSyncConfigSchema.optional(),
|
||||
syncOptions: ChefPkiSyncOptionsSchema.optional(),
|
||||
subscriberId: z.string().nullish(),
|
||||
connectionId: z.string().optional()
|
||||
});
|
||||
|
||||
export const ChefPkiSyncListItemSchema = z.object({
|
||||
name: z.literal("Chef"),
|
||||
connection: z.literal(AppConnection.Chef),
|
||||
destination: z.literal(PkiSync.Chef),
|
||||
canImportCertificates: z.literal(false),
|
||||
canRemoveCertificates: z.literal(true)
|
||||
});
|
||||
65
backend/src/services/pki-sync/chef/chef-pki-sync-types.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { TChefConnection } from "@app/ee/services/app-connections/chef/chef-connection-types";
|
||||
|
||||
import {
|
||||
ChefPkiSyncConfigSchema,
|
||||
ChefPkiSyncSchema,
|
||||
CreateChefPkiSyncSchema,
|
||||
UpdateChefPkiSyncSchema
|
||||
} from "./chef-pki-sync-schemas";
|
||||
|
||||
export type TChefPkiSyncConfig = z.infer<typeof ChefPkiSyncConfigSchema>;
|
||||
|
||||
export type TChefPkiSync = z.infer<typeof ChefPkiSyncSchema>;
|
||||
|
||||
export type TChefPkiSyncInput = z.infer<typeof CreateChefPkiSyncSchema>;
|
||||
|
||||
export type TChefPkiSyncUpdate = z.infer<typeof UpdateChefPkiSyncSchema>;
|
||||
|
||||
export type TChefPkiSyncWithCredentials = TChefPkiSync & {
|
||||
connection: TChefConnection;
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export interface SyncCertificatesResult {
|
||||
uploaded: number;
|
||||
updated: number;
|
||||
removed: number;
|
||||
failedRemovals: number;
|
||||
skipped: number;
|
||||
details?: {
|
||||
failedUploads?: Array<{ name: string; error: string }>;
|
||||
failedRemovals?: Array<{ name: string; error: string }>;
|
||||
validationErrors?: Array<{ name: string; error: string }>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface RemoveCertificatesResult {
|
||||
removed: number;
|
||||
failed: number;
|
||||
skipped: number;
|
||||
}
|
||||
|
||||
export interface CertificateImportRequest {
|
||||
id: string;
|
||||
name: string;
|
||||
certificate: string;
|
||||
privateKey: string;
|
||||
certificateChain?: string;
|
||||
alternativeNames?: string[];
|
||||
certificateId?: string;
|
||||
}
|
||||
4
backend/src/services/pki-sync/chef/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from "./chef-pki-sync-constants";
|
||||
export * from "./chef-pki-sync-fns";
|
||||
export * from "./chef-pki-sync-schemas";
|
||||
export * from "./chef-pki-sync-types";
|
||||
@@ -1,6 +1,7 @@
|
||||
export enum PkiSync {
|
||||
AzureKeyVault = "azure-key-vault",
|
||||
AwsCertificateManager = "aws-certificate-manager"
|
||||
AwsCertificateManager = "aws-certificate-manager",
|
||||
Chef = "chef"
|
||||
}
|
||||
|
||||
export enum PkiSyncStatus {
|
||||
|
||||
@@ -12,6 +12,8 @@ import { AWS_CERTIFICATE_MANAGER_PKI_SYNC_LIST_OPTION } from "./aws-certificate-
|
||||
import { awsCertificateManagerPkiSyncFactory } from "./aws-certificate-manager/aws-certificate-manager-pki-sync-fns";
|
||||
import { AZURE_KEY_VAULT_PKI_SYNC_LIST_OPTION } from "./azure-key-vault/azure-key-vault-pki-sync-constants";
|
||||
import { azureKeyVaultPkiSyncFactory } from "./azure-key-vault/azure-key-vault-pki-sync-fns";
|
||||
import { chefPkiSyncFactory } from "./chef/chef-pki-sync-fns";
|
||||
import { CHEF_PKI_SYNC_LIST_OPTION } from "./chef/chef-pki-sync-list-constants";
|
||||
import { PkiSync } from "./pki-sync-enums";
|
||||
import { TCertificateMap, TPkiSyncWithCredentials } from "./pki-sync-types";
|
||||
|
||||
@@ -19,7 +21,8 @@ const ENTERPRISE_PKI_SYNCS: PkiSync[] = [];
|
||||
|
||||
const PKI_SYNC_LIST_OPTIONS = {
|
||||
[PkiSync.AzureKeyVault]: AZURE_KEY_VAULT_PKI_SYNC_LIST_OPTION,
|
||||
[PkiSync.AwsCertificateManager]: AWS_CERTIFICATE_MANAGER_PKI_SYNC_LIST_OPTION
|
||||
[PkiSync.AwsCertificateManager]: AWS_CERTIFICATE_MANAGER_PKI_SYNC_LIST_OPTION,
|
||||
[PkiSync.Chef]: CHEF_PKI_SYNC_LIST_OPTION
|
||||
};
|
||||
|
||||
export const enterprisePkiSyncCheck = async (
|
||||
@@ -175,6 +178,11 @@ export const PkiSyncFns = {
|
||||
"AWS Certificate Manager does not support importing certificates into Infisical (private keys cannot be extracted)"
|
||||
);
|
||||
}
|
||||
case PkiSync.Chef: {
|
||||
throw new Error(
|
||||
"Chef does not support importing certificates into Infisical (private keys cannot be extracted securely)"
|
||||
);
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unsupported PKI sync destination: ${String(pkiSync.destination)}`);
|
||||
}
|
||||
@@ -222,6 +230,14 @@ export const PkiSyncFns = {
|
||||
});
|
||||
return awsCertificateManagerPkiSync.syncCertificates(pkiSync, certificateMap);
|
||||
}
|
||||
case PkiSync.Chef: {
|
||||
checkPkiSyncDestination(pkiSync, PkiSync.Chef);
|
||||
const chefPkiSync = chefPkiSyncFactory({
|
||||
certificateDAL: dependencies.certificateDAL,
|
||||
certificateSyncDAL: dependencies.certificateSyncDAL
|
||||
});
|
||||
return chefPkiSync.syncCertificates(pkiSync, certificateMap);
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unsupported PKI sync destination: ${String(pkiSync.destination)}`);
|
||||
}
|
||||
@@ -267,6 +283,18 @@ export const PkiSyncFns = {
|
||||
});
|
||||
break;
|
||||
}
|
||||
case PkiSync.Chef: {
|
||||
checkPkiSyncDestination(pkiSync, PkiSync.Chef);
|
||||
const chefPkiSync = chefPkiSyncFactory({
|
||||
certificateDAL: dependencies.certificateDAL,
|
||||
certificateSyncDAL: dependencies.certificateSyncDAL
|
||||
});
|
||||
await chefPkiSync.removeCertificates(pkiSync, certificateNames, {
|
||||
certificateSyncDAL: dependencies.certificateSyncDAL,
|
||||
certificateMap: dependencies.certificateMap
|
||||
});
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unsupported PKI sync destination: ${String(pkiSync.destination)}`);
|
||||
}
|
||||
|
||||
@@ -4,10 +4,12 @@ import { PkiSync } from "./pki-sync-enums";
|
||||
|
||||
export const PKI_SYNC_NAME_MAP: Record<PkiSync, string> = {
|
||||
[PkiSync.AzureKeyVault]: "Azure Key Vault",
|
||||
[PkiSync.AwsCertificateManager]: "AWS Certificate Manager"
|
||||
[PkiSync.AwsCertificateManager]: "AWS Certificate Manager",
|
||||
[PkiSync.Chef]: "Chef"
|
||||
};
|
||||
|
||||
export const PKI_SYNC_CONNECTION_MAP: Record<PkiSync, AppConnection> = {
|
||||
[PkiSync.AzureKeyVault]: AppConnection.AzureKeyVault,
|
||||
[PkiSync.AwsCertificateManager]: AppConnection.AWS
|
||||
[PkiSync.AwsCertificateManager]: AppConnection.AWS,
|
||||
[PkiSync.Chef]: AppConnection.Chef
|
||||
};
|
||||
|
||||
@@ -180,11 +180,16 @@ export const pkiSyncQueueFactory = ({
|
||||
(cert, index, self) => self.findIndex((c) => c.id === cert.id) === index
|
||||
);
|
||||
|
||||
if (uniqueCertificates.length === 0) {
|
||||
const activeCertificates = uniqueCertificates.filter((cert) => {
|
||||
const typedCert = cert as TCertificates;
|
||||
return !typedCert.renewedByCertificateId;
|
||||
});
|
||||
|
||||
if (activeCertificates.length === 0) {
|
||||
return { certificateMap, certificateMetadata };
|
||||
}
|
||||
|
||||
certificates = uniqueCertificates;
|
||||
certificates = activeCertificates;
|
||||
|
||||
for (const certificate of certificates) {
|
||||
const cert = certificate as TCertificates;
|
||||
|
||||
4
docs/api-reference/endpoints/pki/syncs/chef/create.mdx
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Create Chef PKI Sync"
|
||||
openapi: "POST /api/v1/pki/syncs/chef"
|
||||
---
|
||||
4
docs/api-reference/endpoints/pki/syncs/chef/delete.mdx
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Delete Chef PKI Sync"
|
||||
openapi: "DELETE /api/v1/pki/syncs/chef/{pkiSyncId}"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get Chef PKI Sync by ID"
|
||||
openapi: "GET /api/v1/pki/syncs/chef/{pkiSyncId}"
|
||||
---
|
||||
4
docs/api-reference/endpoints/pki/syncs/chef/list.mdx
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "List Chef PKI Syncs"
|
||||
openapi: "GET /api/v1/pki/syncs/chef"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Remove Certificates from Chef"
|
||||
openapi: "POST /api/v1/pki/syncs/chef/{pkiSyncId}/remove-certificates"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Sync Certificates to Chef"
|
||||
openapi: "POST /api/v1/pki/syncs/chef/{pkiSyncId}/sync"
|
||||
---
|
||||
4
docs/api-reference/endpoints/pki/syncs/chef/update.mdx
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Update Chef PKI Sync"
|
||||
openapi: "PATCH /api/v1/pki/syncs/chef/{pkiSyncId}"
|
||||
---
|
||||
@@ -765,7 +765,8 @@
|
||||
"pages": [
|
||||
"documentation/platform/pki/certificate-syncs/overview",
|
||||
"documentation/platform/pki/certificate-syncs/aws-certificate-manager",
|
||||
"documentation/platform/pki/certificate-syncs/azure-key-vault"
|
||||
"documentation/platform/pki/certificate-syncs/azure-key-vault",
|
||||
"documentation/platform/pki/certificate-syncs/chef"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -2685,6 +2686,18 @@
|
||||
"api-reference/endpoints/pki/syncs/azure-key-vault/sync-certificates",
|
||||
"api-reference/endpoints/pki/syncs/azure-key-vault/remove-certificates"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Chef",
|
||||
"pages": [
|
||||
"api-reference/endpoints/pki/syncs/chef/list",
|
||||
"api-reference/endpoints/pki/syncs/chef/get-by-id",
|
||||
"api-reference/endpoints/pki/syncs/chef/create",
|
||||
"api-reference/endpoints/pki/syncs/chef/update",
|
||||
"api-reference/endpoints/pki/syncs/chef/delete",
|
||||
"api-reference/endpoints/pki/syncs/chef/sync-certificates",
|
||||
"api-reference/endpoints/pki/syncs/chef/remove-certificates"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
183
docs/documentation/platform/pki/certificate-syncs/chef.mdx
Normal file
@@ -0,0 +1,183 @@
|
||||
---
|
||||
title: "Chef"
|
||||
description: "Learn how to configure a Chef Certificate Sync for Infisical PKI."
|
||||
---
|
||||
|
||||
**Prerequisites:**
|
||||
|
||||
- Create a [Chef Connection](/integrations/app-connections/chef)
|
||||
- Ensure your network security policies allow incoming requests from Infisical to this certificate sync provider, if network restrictions apply.
|
||||
|
||||
<Note>
|
||||
The Chef Certificate Sync requires the following permissions to be set on the Chef user
|
||||
for Infisical to sync certificates to Chef: `data bag read`, `data bag create`, `data bag update`, `data bag delete`.
|
||||
|
||||
Any role with these permissions would work such as a custom role with **Data Bag** permissions.
|
||||
|
||||
</Note>
|
||||
|
||||
<Note>
|
||||
Certificates synced to Chef will be stored as data bag items within the specified data bag,
|
||||
preserving both the certificate and private key components as separate fields.
|
||||
</Note>
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Infisical UI">
|
||||
1. Navigate to **Project** > **Integrations** > **Certificate Syncs** and press **Add Sync**.
|
||||

|
||||
|
||||
2. Select the **Chef** option.
|
||||

|
||||
|
||||
3. Configure the **Destination** to where certificates should be deployed, then click **Next**.
|
||||

|
||||
|
||||
- **Chef Connection**: The Chef Connection to authenticate with.
|
||||
- **Data Bag Name**: The name of the Chef data bag where certificates will be stored.
|
||||
|
||||
4. Configure the **Sync Options** to specify how certificates should be synced, then click **Next**.
|
||||

|
||||
|
||||
- **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}}`.
|
||||
- **Auto-Sync Enabled**: If enabled, certificates will automatically be synced when changes occur. Disable to enforce manual syncing only.
|
||||
|
||||
<Tip>
|
||||
**Chef Data Bag Item Structure**: Certificates are stored in Chef data bags as items with the following structure:
|
||||
```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"
|
||||
}
|
||||
}
|
||||
```
|
||||
</Tip>
|
||||
|
||||
5. Configure the **Details** of your Chef Certificate Sync, then click **Next**.
|
||||

|
||||
|
||||
- **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. Review your Chef Certificate Sync configuration, then click **Create Sync**.
|
||||

|
||||
|
||||
8. If enabled, your Chef Certificate Sync will begin syncing your certificates to the destination endpoint.
|
||||

|
||||
</Tab>
|
||||
<Tab title="API">
|
||||
To create a **Chef Certificate Sync**, make an API request to the [Create Chef Certificate Sync](/api-reference/endpoints/pki/syncs/chef/create) API endpoint.
|
||||
|
||||
### Sample request
|
||||
|
||||
<Note>
|
||||
You can optionally specify `certificateIds` during sync creation to immediately add certificates to the sync.
|
||||
If not provided, you can add certificates later using the certificate management endpoints.
|
||||
</Note>
|
||||
|
||||
```bash Request
|
||||
curl --request POST \
|
||||
--url https://app.infisical.com/api/v1/pki/syncs/chef \
|
||||
--header 'Authorization: Bearer <access-token>' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"name": "my-chef-cert-sync",
|
||||
"projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
|
||||
"description": "an example certificate sync",
|
||||
"connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
|
||||
"destination": "chef",
|
||||
"isAutoSyncEnabled": true,
|
||||
"certificateIds": [
|
||||
"550e8400-e29b-41d4-a716-446655440000",
|
||||
"660f1234-e29b-41d4-a716-446655440001"
|
||||
],
|
||||
"syncOptions": {
|
||||
"canRemoveCertificates": true,
|
||||
"preserveItemOnRenewal": true,
|
||||
"updateExistingCertificates": true,
|
||||
"certificateNameSchema": "myapp-{{certificateId}}",
|
||||
"includeMetadata": true,
|
||||
"encryptDataBag": true
|
||||
},
|
||||
"destinationConfig": {
|
||||
"dataBagName": "ssl_certificates"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
### Sample response
|
||||
|
||||
```json Response
|
||||
{
|
||||
"pkiSync": {
|
||||
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
|
||||
"name": "my-chef-cert-sync",
|
||||
"description": "an example certificate sync",
|
||||
"destination": "chef",
|
||||
"isAutoSyncEnabled": true,
|
||||
"destinationConfig": {
|
||||
"dataBagName": "ssl_certificates"
|
||||
},
|
||||
"syncOptions": {
|
||||
"canRemoveCertificates": true,
|
||||
"preserveItemOnRenewal": true,
|
||||
"updateExistingCertificates": true,
|
||||
"certificateNameSchema": "myapp-{{certificateId}}",
|
||||
"includeMetadata": true,
|
||||
"encryptDataBag": true
|
||||
},
|
||||
"projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
|
||||
"connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
|
||||
"createdAt": "2023-01-01T00:00:00.000Z",
|
||||
"updatedAt": "2023-01-01T00:00:00.000Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
</Tab>
|
||||
|
||||
</Tabs>
|
||||
|
||||
## Certificate Management
|
||||
|
||||
Your Chef Certificate Sync will:
|
||||
|
||||
- **Automatic Deployment**: Deploy certificates in Infisical to Chef data bags.
|
||||
- **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
|
||||
|
||||
<Note>
|
||||
Chef Certificate Syncs support both automatic and manual
|
||||
synchronization modes. When auto-sync is enabled, certificates are
|
||||
automatically deployed as they are issued or renewed.
|
||||
</Note>
|
||||
|
||||
## Manual Certificate Sync
|
||||
|
||||
You can manually trigger certificate synchronization to Chef using the sync certificates functionality. This is useful for:
|
||||
|
||||
- Initial setup when you have existing certificates to deploy
|
||||
- One-time sync of specific certificates
|
||||
- Testing certificate sync configurations
|
||||
- Force sync after making changes
|
||||
|
||||
To manually sync certificates, use the [Sync Certificates](/api-reference/endpoints/pki/syncs/chef/sync-certificates) API endpoint or the manual sync option in the Infisical UI.
|
||||
|
||||
<Note>
|
||||
Chef does not support importing certificates back into Infisical
|
||||
due to the nature of Chef data bags where certificates are stored as data
|
||||
rather than managed certificate objects.
|
||||
</Note>
|
||||
|
After Width: | Height: | Size: 311 KiB |
|
After Width: | Height: | Size: 287 KiB |
BIN
docs/images/platform/pki/certificate-syncs/chef/chef-details.png
Normal file
|
After Width: | Height: | Size: 296 KiB |
BIN
docs/images/platform/pki/certificate-syncs/chef/chef-options.png
Normal file
|
After Width: | Height: | Size: 362 KiB |
BIN
docs/images/platform/pki/certificate-syncs/chef/chef-review.png
Normal file
|
After Width: | Height: | Size: 311 KiB |
BIN
docs/images/platform/pki/certificate-syncs/chef/chef-synced.png
Normal file
|
After Width: | Height: | Size: 255 KiB |
|
After Width: | Height: | Size: 269 KiB |
@@ -0,0 +1,35 @@
|
||||
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";
|
||||
import { PkiSyncConnectionField } from "./PkiSyncConnectionField";
|
||||
|
||||
export const ChefPkiSyncFields = () => {
|
||||
const { control, setValue } = useFormContext<TPkiSyncForm & { destination: PkiSync.Chef }>();
|
||||
|
||||
return (
|
||||
<>
|
||||
<PkiSyncConnectionField
|
||||
onChange={() => {
|
||||
setValue("destinationConfig.dataBagName", "");
|
||||
}}
|
||||
/>
|
||||
<Controller
|
||||
name="destinationConfig.dataBagName"
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="Data Bag Name"
|
||||
tooltipText="Enter your Chef data bag name where certificates will be stored. This data bag will be used to store SSL/TLS certificates, private keys, and certificate chains. Data bag names must contain only alphanumeric characters, underscores, and hyphens."
|
||||
>
|
||||
<Input {...field} placeholder="ssl_certificates" maxLength={255} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -5,6 +5,7 @@ import { PkiSync } from "@app/hooks/api/pkiSyncs";
|
||||
import { TPkiSyncForm } from "./schemas/pki-sync-schema";
|
||||
import { AwsCertificateManagerPkiSyncFields } from "./AwsCertificateManagerPkiSyncFields";
|
||||
import { AzureKeyVaultPkiSyncFields } from "./AzureKeyVaultPkiSyncFields";
|
||||
import { ChefPkiSyncFields } from "./ChefPkiSyncFields";
|
||||
|
||||
export const PkiSyncDestinationFields = () => {
|
||||
const { watch } = useFormContext<TPkiSyncForm>();
|
||||
@@ -16,6 +17,8 @@ export const PkiSyncDestinationFields = () => {
|
||||
return <AzureKeyVaultPkiSyncFields />;
|
||||
case PkiSync.AwsCertificateManager:
|
||||
return <AwsCertificateManagerPkiSyncFields />;
|
||||
case PkiSync.Chef:
|
||||
return <ChefPkiSyncFields />;
|
||||
default:
|
||||
return (
|
||||
<div className="flex items-center justify-center rounded-md border border-red-500 bg-red-100 p-4 text-red-700">
|
||||
|
||||
@@ -183,6 +183,52 @@ export const PkiSyncOptionsFields = ({ destination }: Props) => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentDestination === PkiSync.Chef && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="syncOptions.preserveItemOnRenewal"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||
<Switch
|
||||
className="bg-mineshaft-400/80 shadow-inner data-[state=checked]:bg-green/80"
|
||||
id="preserve-item-on-renewal"
|
||||
thumbClassName="bg-mineshaft-800"
|
||||
onCheckedChange={onChange}
|
||||
isChecked={value}
|
||||
>
|
||||
<p>
|
||||
Preserve Data Bag Item on Renewal{" "}
|
||||
<Tooltip
|
||||
className="max-w-md"
|
||||
content={
|
||||
<>
|
||||
<p>
|
||||
<strong>Only applies to certificate renewals:</strong> When a certificate
|
||||
is renewed in Infisical, this option controls how the renewed certificate
|
||||
is handled in Chef.
|
||||
</p>
|
||||
<p className="mt-4">
|
||||
When enabled, the renewed certificate will update the existing data bag
|
||||
item, preserving the same item name. This allows consuming services to
|
||||
continue using the same data bag item without requiring updates to Chef
|
||||
cookbooks or recipes.
|
||||
</p>
|
||||
<p className="mt-4">
|
||||
When disabled, the renewed certificate will be created as a new data bag
|
||||
item with a new name, and the old item will be removed.
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={faQuestionCircle} size="sm" className="ml-1" />
|
||||
</Tooltip>
|
||||
</p>
|
||||
</Switch>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="syncOptions.certificateNameSchema"
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { PkiSync } from "@app/hooks/api/pkiSyncs";
|
||||
|
||||
import { BasePkiSyncSchema } from "./base-pki-sync-schema";
|
||||
|
||||
const ChefSyncOptionsSchema = z.object({
|
||||
canImportCertificates: z.boolean().default(false),
|
||||
canRemoveCertificates: z.boolean().default(true),
|
||||
preserveItemOnRenewal: z.boolean().default(true),
|
||||
updateExistingCertificates: z.boolean().default(true),
|
||||
certificateNameSchema: z
|
||||
.string()
|
||||
.optional()
|
||||
.refine(
|
||||
(val) => {
|
||||
if (!val) return true;
|
||||
|
||||
const allowedOptionalPlaceholders = [
|
||||
"{{environment}}",
|
||||
"{{profileId}}",
|
||||
"{{commonName}}",
|
||||
"{{friendlyName}}"
|
||||
];
|
||||
|
||||
const allowedPlaceholdersRegexPart = ["{{certificateId}}", ...allowedOptionalPlaceholders]
|
||||
.map((p) => p.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&"))
|
||||
.join("|");
|
||||
|
||||
const allowedContentRegex = new RegExp(
|
||||
`^([a-zA-Z0-9_\\-]|${allowedPlaceholdersRegexPart})*$`
|
||||
);
|
||||
const contentIsValid = allowedContentRegex.test(val);
|
||||
|
||||
if (val.trim()) {
|
||||
const certificateIdRegex = /\{\{certificateId\}\}/;
|
||||
const certificateIdIsPresent = certificateIdRegex.test(val);
|
||||
return contentIsValid && certificateIdIsPresent;
|
||||
}
|
||||
|
||||
return contentIsValid;
|
||||
},
|
||||
{
|
||||
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."
|
||||
}
|
||||
)
|
||||
});
|
||||
|
||||
export const ChefPkiSyncDestinationSchema = BasePkiSyncSchema(ChefSyncOptionsSchema).merge(
|
||||
z.object({
|
||||
destination: z.literal(PkiSync.Chef),
|
||||
destinationConfig: z.object({
|
||||
dataBagName: z
|
||||
.string()
|
||||
.min(1, "Data bag name is required")
|
||||
.max(255, "Data bag name must be less than 255 characters")
|
||||
.regex(
|
||||
/^[a-zA-Z0-9_-]+$/,
|
||||
"Data bag name can only contain alphanumeric characters, underscores, and hyphens"
|
||||
)
|
||||
})
|
||||
})
|
||||
);
|
||||
|
||||
export const UpdateChefPkiSyncDestinationSchema = ChefPkiSyncDestinationSchema.partial().merge(
|
||||
z.object({
|
||||
name: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Name is required")
|
||||
.max(255, "Name must be less than 255 characters"),
|
||||
destination: z.literal(PkiSync.Chef),
|
||||
connection: z.object({
|
||||
id: z.string().uuid("Invalid connection ID format"),
|
||||
name: z
|
||||
.string()
|
||||
.min(1, "Connection name is required")
|
||||
.max(255, "Connection name must be less than 255 characters")
|
||||
})
|
||||
})
|
||||
);
|
||||
@@ -8,15 +8,21 @@ import {
|
||||
AzureKeyVaultPkiSyncDestinationSchema,
|
||||
UpdateAzureKeyVaultPkiSyncDestinationSchema
|
||||
} from "./azure-key-vault-pki-sync-destination-schema";
|
||||
import {
|
||||
ChefPkiSyncDestinationSchema,
|
||||
UpdateChefPkiSyncDestinationSchema
|
||||
} from "./chef-pki-sync-destination-schema";
|
||||
|
||||
const PkiSyncUnionSchema = z.discriminatedUnion("destination", [
|
||||
AzureKeyVaultPkiSyncDestinationSchema,
|
||||
AwsCertificateManagerPkiSyncDestinationSchema
|
||||
AwsCertificateManagerPkiSyncDestinationSchema,
|
||||
ChefPkiSyncDestinationSchema
|
||||
]);
|
||||
|
||||
const UpdatePkiSyncUnionSchema = z.discriminatedUnion("destination", [
|
||||
UpdateAzureKeyVaultPkiSyncDestinationSchema,
|
||||
UpdateAwsCertificateManagerPkiSyncDestinationSchema
|
||||
UpdateAwsCertificateManagerPkiSyncDestinationSchema,
|
||||
UpdateChefPkiSyncDestinationSchema
|
||||
]);
|
||||
|
||||
export const PkiSyncFormSchema = PkiSyncUnionSchema;
|
||||
|
||||
@@ -15,10 +15,15 @@ export const PKI_SYNC_MAP: Record<
|
||||
[PkiSync.AwsCertificateManager]: {
|
||||
name: "AWS Certificate Manager",
|
||||
image: "Amazon Web Services.png"
|
||||
},
|
||||
[PkiSync.Chef]: {
|
||||
name: "Chef",
|
||||
image: "Chef.png"
|
||||
}
|
||||
};
|
||||
|
||||
export const PKI_SYNC_CONNECTION_MAP: Record<PkiSync, AppConnection> = {
|
||||
[PkiSync.AzureKeyVault]: AppConnection.AzureKeyVault,
|
||||
[PkiSync.AwsCertificateManager]: AppConnection.AWS
|
||||
[PkiSync.AwsCertificateManager]: AppConnection.AWS,
|
||||
[PkiSync.Chef]: AppConnection.Chef
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export enum PkiSync {
|
||||
AzureKeyVault = "azure-key-vault",
|
||||
AwsCertificateManager = "aws-certificate-manager"
|
||||
AwsCertificateManager = "aws-certificate-manager",
|
||||
Chef = "chef"
|
||||
}
|
||||
|
||||
export enum PkiSyncStatus {
|
||||
|
||||
16
frontend/src/hooks/api/pkiSyncs/types/chef-sync.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
|
||||
import { PkiSync } from "../enums";
|
||||
import { TRootPkiSync } from "./common";
|
||||
|
||||
export type TChefPkiSync = TRootPkiSync & {
|
||||
destination: PkiSync.Chef;
|
||||
destinationConfig: {
|
||||
dataBagName: string;
|
||||
};
|
||||
connection: {
|
||||
app: AppConnection.Chef;
|
||||
name: string;
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
@@ -7,6 +7,10 @@ export type RootPkiSyncOptions = {
|
||||
canRemoveCertificates: boolean;
|
||||
certificateNamePrefix?: string;
|
||||
certificateNameSchema?: string;
|
||||
preserveArn?: boolean;
|
||||
enableVersioning?: boolean;
|
||||
preserveItemOnRenewal?: boolean;
|
||||
updateExistingCertificates?: boolean;
|
||||
};
|
||||
|
||||
export type TRootPkiSync = {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { PkiSync } from "@app/hooks/api/pkiSyncs";
|
||||
|
||||
import { TAwsCertificateManagerPkiSync } from "./aws-certificate-manager-sync";
|
||||
import { TAzureKeyVaultPkiSync } from "./azure-key-vault-sync";
|
||||
import { TChefPkiSync } from "./chef-sync";
|
||||
|
||||
export type TPkiSyncOption = {
|
||||
name: string;
|
||||
@@ -16,7 +17,7 @@ export type TPkiSyncOption = {
|
||||
minCertificateNameLength?: number;
|
||||
};
|
||||
|
||||
export type TPkiSync = TAzureKeyVaultPkiSync | TAwsCertificateManagerPkiSync;
|
||||
export type TPkiSync = TAzureKeyVaultPkiSync | TAwsCertificateManagerPkiSync | TChefPkiSync;
|
||||
|
||||
export type TListPkiSyncs = { pkiSyncs: TPkiSync[] };
|
||||
|
||||
@@ -31,6 +32,10 @@ type TCreatePkiSyncDTOBase = {
|
||||
canRemoveCertificates: boolean;
|
||||
certificateNamePrefix?: string;
|
||||
certificateNameSchema?: string;
|
||||
preserveArn?: boolean;
|
||||
enableVersioning?: boolean;
|
||||
preserveItemOnRenewal?: boolean;
|
||||
updateExistingCertificates?: boolean;
|
||||
};
|
||||
isAutoSyncEnabled: boolean;
|
||||
subscriberId?: string | null;
|
||||
@@ -43,6 +48,7 @@ export type TCreatePkiSyncDTO = TCreatePkiSyncDTOBase & {
|
||||
destinationConfig: {
|
||||
vaultBaseUrl?: string;
|
||||
region?: string;
|
||||
dataBagName?: string;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -77,4 +83,5 @@ export type TTriggerPkiSyncRemoveCertificatesDTO = {
|
||||
|
||||
export * from "./aws-certificate-manager-sync";
|
||||
export * from "./azure-key-vault-sync";
|
||||
export * from "./chef-sync";
|
||||
export * from "./common";
|
||||
|
||||
@@ -13,7 +13,8 @@ import { PkiSync, TPkiSync } from "@app/hooks/api/pkiSyncs";
|
||||
|
||||
import {
|
||||
AwsCertificateManagerPkiSyncDestinationSection,
|
||||
AzureKeyVaultPkiSyncDestinationSection
|
||||
AzureKeyVaultPkiSyncDestinationSection,
|
||||
ChefPkiSyncDestinationSection
|
||||
} from "./PkiSyncDestinationSection/index";
|
||||
|
||||
const GenericFieldLabel = ({ label, children }: { label: string; children: React.ReactNode }) => (
|
||||
@@ -41,6 +42,9 @@ export const PkiSyncDestinationSection = ({ pkiSync, onEditDestination }: Props)
|
||||
case PkiSync.AzureKeyVault:
|
||||
DestinationComponents = <AzureKeyVaultPkiSyncDestinationSection pkiSync={pkiSync} />;
|
||||
break;
|
||||
case PkiSync.Chef:
|
||||
DestinationComponents = <ChefPkiSyncDestinationSection pkiSync={pkiSync} />;
|
||||
break;
|
||||
default:
|
||||
// For future destinations, return null (no additional fields to show)
|
||||
DestinationComponents = null;
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { TPkiSync } from "@app/hooks/api/pkiSyncs";
|
||||
|
||||
const GenericFieldLabel = ({ label, children }: { label: string; children: React.ReactNode }) => (
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-medium text-mineshaft-300">{label}</p>
|
||||
<div className="text-sm text-mineshaft-300">{children}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
type Props = {
|
||||
pkiSync: TPkiSync;
|
||||
};
|
||||
|
||||
export const ChefPkiSyncDestinationSection = ({ pkiSync }: Props) => {
|
||||
const dataBagName =
|
||||
pkiSync.destinationConfig && "dataBagName" in pkiSync.destinationConfig
|
||||
? pkiSync.destinationConfig.dataBagName
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<GenericFieldLabel label="Chef Data Bag Name">
|
||||
{dataBagName || "Not specified"}
|
||||
</GenericFieldLabel>
|
||||
);
|
||||
};
|
||||
@@ -1,2 +1,3 @@
|
||||
export { AwsCertificateManagerPkiSyncDestinationSection } from "./AwsCertificateManagerPkiSyncDestinationSection";
|
||||
export { AzureKeyVaultPkiSyncDestinationSection } from "./AzureKeyVaultPkiSyncDestinationSection";
|
||||
export { ChefPkiSyncDestinationSection } from "./ChefPkiSyncDestinationSection";
|
||||
|
||||