diff --git a/backend/src/queue/queue-service.ts b/backend/src/queue/queue-service.ts index 42910b97e..335f25d5f 100644 --- a/backend/src/queue/queue-service.ts +++ b/backend/src/queue/queue-service.ts @@ -51,6 +51,7 @@ export enum QueueName { AuditLogPrune = "audit-log-prune", DailyResourceCleanUp = "daily-resource-cleanup", DailyExpiringPkiItemAlert = "daily-expiring-pki-item-alert", + PkiSyncCleanup = "pki-sync-cleanup", PkiSubscriber = "pki-subscriber", TelemetryInstanceStats = "telemtry-self-hosted-stats", IntegrationSync = "sync-integrations", @@ -86,6 +87,7 @@ export enum QueueJobs { AuditLogPrune = "audit-log-prune-job", DailyResourceCleanUp = "daily-resource-cleanup-job", DailyExpiringPkiItemAlert = "daily-expiring-pki-item-alert", + PkiSyncCleanup = "pki-sync-cleanup-job", SecWebhook = "secret-webhook-trigger", TelemetryInstanceStats = "telemetry-self-hosted-stats", IntegrationSync = "secret-integration-pull", @@ -151,6 +153,10 @@ export type TQueueJobTypes = { name: QueueJobs.DailyExpiringPkiItemAlert; payload: undefined; }; + [QueueName.PkiSyncCleanup]: { + name: QueueJobs.PkiSyncCleanup; + payload: undefined; + }; [QueueName.AuditLogPrune]: { name: QueueJobs.AuditLogPrune; payload: undefined; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index cc74ed04e..2fb7e9ff7 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -248,6 +248,7 @@ import { pkiCollectionServiceFactory } from "@app/services/pki-collection/pki-co import { pkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal"; import { pkiSubscriberQueueServiceFactory } from "@app/services/pki-subscriber/pki-subscriber-queue"; import { pkiSubscriberServiceFactory } from "@app/services/pki-subscriber/pki-subscriber-service"; +import { pkiSyncCleanupQueueServiceFactory } from "@app/services/pki-sync/pki-sync-cleanup-queue"; import { pkiSyncDALFactory } from "@app/services/pki-sync/pki-sync-dal"; import { pkiSyncQueueFactory } from "@app/services/pki-sync/pki-sync-queue"; import { pkiSyncServiceFactory } from "@app/services/pki-sync/pki-sync-service"; @@ -1906,7 +1907,15 @@ export const registerRoutes = async ( licenseService, certificateDAL, certificateBodyDAL, - certificateSecretDAL + certificateSecretDAL, + certificateAuthorityDAL, + certificateAuthorityCertDAL + }); + + const pkiSyncCleanup = pkiSyncCleanupQueueServiceFactory({ + queueService, + pkiSyncDAL, + pkiSyncQueue }); const internalCaFns = InternalCertificateAuthorityFns({ @@ -2104,6 +2113,7 @@ export const registerRoutes = async ( await telemetryQueue.startTelemetryCheck(); await telemetryQueue.startAggregatedEventsJob(); await dailyResourceCleanUp.init(); + await pkiSyncCleanup.init(); await dailyReminderQueueService.startDailyRemindersJob(); await dailyReminderQueueService.startSecretReminderMigrationJob(); await dailyExpiringPkiItemAlert.startSendingAlerts(); diff --git a/backend/src/server/routes/v1/pki-sync-routers/pki-sync-router.ts b/backend/src/server/routes/v1/pki-sync-routers/pki-sync-router.ts index 5f1980fbc..158d9e4dc 100644 --- a/backend/src/server/routes/v1/pki-sync-routers/pki-sync-router.ts +++ b/backend/src/server/routes/v1/pki-sync-routers/pki-sync-router.ts @@ -53,7 +53,14 @@ const PkiSyncSchema = z.object({ createdAt: z.date(), updatedAt: z.date(), isPlatformManagedCredentials: z.boolean().nullable().optional() - }) + }), + subscriber: z + .object({ + id: z.string(), + name: z.string() + }) + .nullable() + .optional() }); const PkiSyncOptionsSchema = z.object({ diff --git a/backend/src/services/certificate/certificate-dal.ts b/backend/src/services/certificate/certificate-dal.ts index 377563ced..88808bd63 100644 --- a/backend/src/services/certificate/certificate-dal.ts +++ b/backend/src/services/certificate/certificate-dal.ts @@ -1,5 +1,5 @@ import { TDbClient } from "@app/db"; -import { TableName } from "@app/db/schemas"; +import { TableName, TCertificates } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; import { ormify } from "@app/lib/knex"; @@ -93,11 +93,33 @@ export const certificateDALFactory = (db: TDbClient) => { } }; + const findExpiredSyncedCertificates = async (): Promise => { + try { + const yesterday = new Date(); + yesterday.setDate(yesterday.getDate() - 1); + yesterday.setHours(0, 0, 0, 0); + + const today = new Date(); + today.setHours(0, 0, 0, 0); + + const certs = await db + .replicaNode()(TableName.Certificate) + .where("notAfter", ">=", yesterday) + .where("notAfter", "<", today) + .whereNotNull("pkiSubscriberId"); + + return certs; + } catch (error) { + throw new DatabaseError({ error, name: "Find expired synced certificates" }); + } + }; + return { ...certificateOrm, countCertificatesInProject, countCertificatesForPkiSubscriber, findLatestActiveCertForSubscriber, - findAllActiveCertsForSubscriber + findAllActiveCertsForSubscriber, + findExpiredSyncedCertificates }; }; diff --git a/backend/src/services/certificate/certificate-service.ts b/backend/src/services/certificate/certificate-service.ts index c9da3f946..91731c387 100644 --- a/backend/src/services/certificate/certificate-service.ts +++ b/backend/src/services/certificate/certificate-service.ts @@ -22,8 +22,8 @@ import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TPkiCollectionDALFactory } from "@app/services/pki-collection/pki-collection-dal"; import { TPkiCollectionItemDALFactory } from "@app/services/pki-collection/pki-collection-item-dal"; import { TPkiSyncDALFactory } from "@app/services/pki-sync/pki-sync-dal"; -import { triggerAutoSyncForSubscriber } from "@app/services/pki-sync/pki-sync-utils"; import { TPkiSyncQueueFactory } from "@app/services/pki-sync/pki-sync-queue"; +import { triggerAutoSyncForSubscriber } from "@app/services/pki-sync/pki-sync-utils"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns"; diff --git a/backend/src/services/pki-sync/azure-key-vault/azure-key-vault-pki-sync-constants.ts b/backend/src/services/pki-sync/azure-key-vault/azure-key-vault-pki-sync-constants.ts index 247d72fa4..3c64bc86d 100644 --- a/backend/src/services/pki-sync/azure-key-vault/azure-key-vault-pki-sync-constants.ts +++ b/backend/src/services/pki-sync/azure-key-vault/azure-key-vault-pki-sync-constants.ts @@ -43,7 +43,7 @@ export const AZURE_KEY_VAULT_PKI_SYNC_LIST_OPTION = { connection: AppConnection.AzureKeyVault, destination: PkiSync.AzureKeyVault, canImportCertificates: false, - canRemoveCertificates: false, + canRemoveCertificates: true, defaultCertificateNameSchema: "Infisical-PKI-Sync-{{certificateId}}", forbiddenCharacters: AZURE_KEY_VAULT_CERTIFICATE_NAMING.FORBIDDEN_CHARACTERS, allowedCharacterPattern: AZURE_KEY_VAULT_CERTIFICATE_NAMING.ALLOWED_CHARACTER_PATTERN, diff --git a/backend/src/services/pki-sync/azure-key-vault/azure-key-vault-pki-sync-fns.ts b/backend/src/services/pki-sync/azure-key-vault/azure-key-vault-pki-sync-fns.ts index 2b9521bb0..b423f42db 100644 --- a/backend/src/services/pki-sync/azure-key-vault/azure-key-vault-pki-sync-fns.ts +++ b/backend/src/services/pki-sync/azure-key-vault/azure-key-vault-pki-sync-fns.ts @@ -329,13 +329,14 @@ export const azureKeyVaultPkiSyncFactory = ({ kmsService, appConnectionDAL }: TA key: string; cert: string; privateKey: string; + certificateChain?: string; }[] = []; // Track which certificates should exist in Azure Key Vault const activeCertificateNames = Object.keys(certificateMap); // Iterate through certificates to sync to Azure Key Vault - Object.entries(certificateMap).forEach(([certName, { cert, privateKey }]) => { + Object.entries(certificateMap).forEach(([certName, { cert, privateKey, certificateChain }]) => { if (disabledAzureKeyVaultCertificateKeys.includes(certName)) { return; } @@ -347,7 +348,8 @@ export const azureKeyVaultPkiSyncFactory = ({ kmsService, appConnectionDAL }: TA setCertificates.push({ key: certName, cert, - privateKey + privateKey, + certificateChain }); } }); @@ -364,13 +366,26 @@ export const azureKeyVaultPkiSyncFactory = ({ kmsService, appConnectionDAL }: TA // Upload certificates to Azure Key Vault with rate limiting const uploadResults = await executeWithConcurrencyLimit( setCertificates, - async ({ key, cert, privateKey }) => { + async ({ key, cert, privateKey, certificateChain }) => { try { - // Combine certificate and private key in PEM format for Azure Key Vault - // Azure Key Vault accepts PEM format with both cert and private key - let combinedPem = cert; + // Combine private key, certificate, and certificate chain in PEM format for Azure Key Vault + let combinedPem = ""; + if (privateKey) { - combinedPem = `${privateKey}\n${cert}`; + combinedPem = privateKey.trim(); + } + + if (combinedPem) { + combinedPem = `${combinedPem}\n${cert.trim()}`; + } else { + combinedPem = cert.trim(); + } + + if (certificateChain) { + const trimmedChain = certificateChain.trim(); + if (trimmedChain) { + combinedPem = `${combinedPem}\n${trimmedChain}`; + } } // Convert to base64 for Azure Key Vault import diff --git a/backend/src/services/pki-sync/pki-sync-cleanup-queue.ts b/backend/src/services/pki-sync/pki-sync-cleanup-queue.ts new file mode 100644 index 000000000..8d65f0ceb --- /dev/null +++ b/backend/src/services/pki-sync/pki-sync-cleanup-queue.ts @@ -0,0 +1,94 @@ +import { getConfig } from "@app/lib/config/env"; +import { logger } from "@app/lib/logger"; +import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; + +import { TPkiSyncDALFactory } from "./pki-sync-dal"; +import { TPkiSyncQueueFactory } from "./pki-sync-queue"; + +type TPkiSyncCleanupQueueServiceFactoryDep = { + queueService: TQueueServiceFactory; + pkiSyncDAL: Pick; + pkiSyncQueue: Pick; +}; + +export type TPkiSyncCleanupQueueServiceFactory = ReturnType; + +export const pkiSyncCleanupQueueServiceFactory = ({ + queueService, + pkiSyncDAL, + pkiSyncQueue +}: TPkiSyncCleanupQueueServiceFactoryDep) => { + const appCfg = getConfig(); + + const syncExpiredCertificatesForPkiSyncs = async () => { + try { + const pkiSyncsWithExpiredCerts = await pkiSyncDAL.findPkiSyncsWithExpiredCertificates(); + + if (pkiSyncsWithExpiredCerts.length === 0) { + logger.info("No PKI syncs found with certificates that expired the previous day"); + return; + } + + logger.info( + `Found ${pkiSyncsWithExpiredCerts.length} PKI sync(s) with certificates that expired the previous day` + ); + + // Trigger sync for each PKI sync that has expired certificates + for (const { id: syncId, subscriberId } of pkiSyncsWithExpiredCerts) { + try { + // eslint-disable-next-line no-await-in-loop + await pkiSyncQueue.queuePkiSyncSyncCertificatesById({ + syncId + }); + logger.info( + `Successfully queued PKI sync ${syncId} for subscriber ${subscriberId} due to expired certificates` + ); + } catch (error) { + logger.error(error, `Failed to queue PKI sync ${syncId} for subscriber ${subscriberId}`); + } + } + } catch (error) { + logger.error(error, "Failed to sync expired certificates for PKI syncs"); + throw error; + } + }; + + const init = async () => { + if (appCfg.isSecondaryInstance) { + return; + } + + await queueService.stopRepeatableJob( + QueueName.PkiSyncCleanup, + QueueJobs.PkiSyncCleanup, + { pattern: "0 0 * * *", utc: true }, + QueueName.PkiSyncCleanup // just a job id + ); + + await queueService.startPg( + QueueJobs.PkiSyncCleanup, + async () => { + try { + logger.info(`${QueueName.PkiSyncCleanup}: queue task started`); + await syncExpiredCertificatesForPkiSyncs(); + logger.info(`${QueueName.PkiSyncCleanup}: queue task completed`); + } catch (error) { + logger.error(error, `${QueueName.PkiSyncCleanup}: PKI sync cleanup failed`); + throw error; + } + }, + { + batchSize: 1, + workerCount: 1, + pollingIntervalSeconds: 1 + } + ); + + await queueService.schedulePg(QueueJobs.PkiSyncCleanup, "0 0 * * *", undefined, { tz: "UTC" }); + }; + + return { + init, + syncExpiredCertificatesForPkiSyncs + }; +}; diff --git a/backend/src/services/pki-sync/pki-sync-dal.ts b/backend/src/services/pki-sync/pki-sync-dal.ts index 325121f98..460bbfc42 100644 --- a/backend/src/services/pki-sync/pki-sync-dal.ts +++ b/backend/src/services/pki-sync/pki-sync-dal.ts @@ -42,6 +42,49 @@ const basePkiSyncQuery = ({ filter, db, tx }: { db: TDbClient; filter?: PkiSyncF return query; }; +const basePkiSyncWithSubscriberQuery = ({ + filter, + db, + tx +}: { + db: TDbClient; + filter?: PkiSyncFindFilter; + tx?: Knex; +}) => { + const query = (tx || db.replicaNode())(TableName.PkiSync) + .leftJoin(TableName.AppConnection, `${TableName.PkiSync}.connectionId`, `${TableName.AppConnection}.id`) + .leftJoin(TableName.PkiSubscriber, `${TableName.PkiSync}.subscriberId`, `${TableName.PkiSubscriber}.id`) + .select(selectAllTableCols(TableName.PkiSync)) + .select( + // app connection fields + db.ref("name").withSchema(TableName.AppConnection).as("appConnectionName"), + db.ref("app").withSchema(TableName.AppConnection).as("appConnectionApp"), + db.ref("encryptedCredentials").withSchema(TableName.AppConnection).as("appConnectionEncryptedCredentials"), + db.ref("orgId").withSchema(TableName.AppConnection).as("appConnectionOrgId"), + db.ref("projectId").withSchema(TableName.AppConnection).as("appConnectionProjectId"), + db.ref("method").withSchema(TableName.AppConnection).as("appConnectionMethod"), + db.ref("description").withSchema(TableName.AppConnection).as("appConnectionDescription"), + db.ref("version").withSchema(TableName.AppConnection).as("appConnectionVersion"), + db.ref("gatewayId").withSchema(TableName.AppConnection).as("appConnectionGatewayId"), + db.ref("createdAt").withSchema(TableName.AppConnection).as("appConnectionCreatedAt"), + db.ref("updatedAt").withSchema(TableName.AppConnection).as("appConnectionUpdatedAt"), + db + .ref("isPlatformManagedCredentials") + .withSchema(TableName.AppConnection) + .as("appConnectionIsPlatformManagedCredentials"), + // pki subscriber fields + db.ref("id").withSchema(TableName.PkiSubscriber).as("pkiSubscriberId"), + db.ref("name").withSchema(TableName.PkiSubscriber).as("subscriberName") + ); + + if (filter) { + // eslint-disable-next-line @typescript-eslint/no-misused-promises + void query.where(buildFindFilter(prependTableNameToFindFilter(TableName.PkiSync, filter))); + } + + return query; +}; + const expandPkiSync = (pkiSync: Awaited>[number]) => { const { appConnectionName, @@ -84,6 +127,51 @@ const expandPkiSync = (pkiSync: Awaited>[num }; }; +const expandPkiSyncWithSubscriber = (pkiSync: Awaited>[number]) => { + const { + appConnectionName, + appConnectionApp, + appConnectionEncryptedCredentials, + appConnectionOrgId, + appConnectionProjectId, + appConnectionMethod, + appConnectionDescription, + appConnectionVersion, + appConnectionGatewayId, + appConnectionCreatedAt, + appConnectionUpdatedAt, + appConnectionIsPlatformManagedCredentials, + pkiSubscriberId, + subscriberName, + ...el + } = pkiSync; + + return { + ...el, + destination: el.destination as PkiSync, + destinationConfig: el.destinationConfig as Record, + syncOptions: el.syncOptions as Record, + appConnectionName, + appConnectionApp, + connection: { + id: el.connectionId, + name: appConnectionName, + app: appConnectionApp, + encryptedCredentials: appConnectionEncryptedCredentials, + orgId: appConnectionOrgId, + projectId: appConnectionProjectId, + method: appConnectionMethod, + description: appConnectionDescription, + version: appConnectionVersion, + gatewayId: appConnectionGatewayId, + createdAt: appConnectionCreatedAt, + updatedAt: appConnectionUpdatedAt, + isPlatformManagedCredentials: appConnectionIsPlatformManagedCredentials + }, + subscriber: pkiSubscriberId && subscriberName ? { id: pkiSubscriberId, name: subscriberName } : null + }; +}; + export const pkiSyncDALFactory = (db: TDbClient) => { const pkiSyncOrm = ormify(db, TableName.PkiSync); @@ -96,6 +184,15 @@ export const pkiSyncDALFactory = (db: TDbClient) => { } }; + const findByProjectIdWithSubscribers = async (projectId: string, tx?: Knex) => { + try { + const pkiSyncs = await basePkiSyncWithSubscriberQuery({ filter: { projectId }, db, tx }); + return pkiSyncs.map(expandPkiSyncWithSubscriber); + } catch (error) { + throw new DatabaseError({ error, name: "Find By Project ID With Subscribers - PKI Sync" }); + } + }; + const findBySubscriberId = async (subscriberId: string, tx?: Knex) => { try { const pkiSyncs = await basePkiSyncQuery({ filter: { subscriberId }, db, tx }); @@ -168,9 +265,42 @@ export const pkiSyncDALFactory = (db: TDbClient) => { return expandPkiSync(pkiSync); }; + const findPkiSyncsWithExpiredCertificates = async (): Promise> => { + try { + const yesterday = new Date(); + yesterday.setDate(yesterday.getDate() - 1); + yesterday.setHours(0, 0, 0, 0); + + const today = new Date(); + today.setHours(0, 0, 0, 0); + + const pkiSyncs = (await db + .replicaNode()(TableName.PkiSync) + .select(`${TableName.PkiSync}.id`, `${TableName.PkiSync}.subscriberId`) + .innerJoin( + TableName.Certificate, + `${TableName.PkiSync}.subscriberId`, + `${TableName.Certificate}.pkiSubscriberId` + ) + .where(`${TableName.Certificate}.notAfter`, ">=", yesterday) + .where(`${TableName.Certificate}.notAfter`, "<", today) + .whereNotNull(`${TableName.Certificate}.pkiSubscriberId`) + .whereNotNull(`${TableName.PkiSync}.subscriberId`) + .groupBy(`${TableName.PkiSync}.id`, `${TableName.PkiSync}.subscriberId`)) as Array<{ + id: string; + subscriberId: string; + }>; + + return pkiSyncs; + } catch (error) { + throw new DatabaseError({ error, name: "Find PKI syncs with expired certificates" }); + } + }; + return { ...pkiSyncOrm, findByProjectId, + findByProjectIdWithSubscribers, findBySubscriberId, findByIdAndProjectId, findByNameAndProjectId, @@ -178,6 +308,7 @@ export const pkiSyncDALFactory = (db: TDbClient) => { findOne, find, create, - updateById + updateById, + findPkiSyncsWithExpiredCertificates }; }; diff --git a/backend/src/services/pki-sync/pki-sync-queue.ts b/backend/src/services/pki-sync/pki-sync-queue.ts index 3393aa558..5967a6c97 100644 --- a/backend/src/services/pki-sync/pki-sync-queue.ts +++ b/backend/src/services/pki-sync/pki-sync-queue.ts @@ -22,6 +22,9 @@ import { TCertificateBodyDALFactory } from "../certificate/certificate-body-dal" import { TCertificateDALFactory } from "../certificate/certificate-dal"; import { getCertificateCredentials } from "../certificate/certificate-fns"; import { TCertificateSecretDALFactory } from "../certificate/certificate-secret-dal"; +import { TCertificateAuthorityCertDALFactory } from "../certificate-authority/certificate-authority-cert-dal"; +import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal"; +import { getCaCertChain } from "../certificate-authority/certificate-authority-fns"; import { TPkiSyncDALFactory } from "./pki-sync-dal"; import { PkiSyncStatus } from "./pki-sync-enums"; import { PkiSyncError } from "./pki-sync-errors"; @@ -58,6 +61,8 @@ type TPkiSyncQueueFactoryDep = { >; certificateBodyDAL: Pick; certificateSecretDAL: Pick; + certificateAuthorityDAL: Pick; + certificateAuthorityCertDAL: Pick; }; type PkiSyncActionJob = Job< @@ -86,7 +91,9 @@ export const pkiSyncQueueFactory = ({ licenseService, certificateDAL, certificateBodyDAL, - certificateSecretDAL + certificateSecretDAL, + certificateAuthorityDAL, + certificateAuthorityCertDAL }: TPkiSyncQueueFactoryDep) => { const appCfg = getConfig(); @@ -207,6 +214,32 @@ export const pkiSyncQueueFactory = ({ certPrivateKey = undefined; } + let certificateChain: string | undefined; + try { + if (certBody.encryptedCertificateChain) { + const decryptedCertChain = await kmsDecryptor({ + cipherTextBlob: certBody.encryptedCertificateChain + }); + certificateChain = decryptedCertChain.toString(); + } else if (certificate.caCertId) { + const { caCert, caCertChain } = await getCaCertChain({ + caCertId: certificate.caCertId, + certificateAuthorityDAL, + certificateAuthorityCertDAL, + projectDAL, + kmsService + }); + certificateChain = `${caCert}\n${caCertChain}`.trim(); + } + } catch (chainError) { + logger.warn( + { certificateId: certificate.id, subscriberId, error: chainError }, + "Certificate chain not found or could not be decrypted - certificate may be imported or chain was not stored" + ); + // Continue without certificate chain + certificateChain = undefined; + } + let certificateName: string; const syncOptions = pkiSync.syncOptions as { certificateNameSchema?: string } | undefined; const certificateNameSchema = syncOptions?.certificateNameSchema; @@ -223,7 +256,8 @@ export const pkiSyncQueueFactory = ({ certificateMap[certificateName] = { cert: certificatePem, - privateKey: certPrivateKey || "" + privateKey: certPrivateKey || "", + certificateChain }; } else { logger.warn({ certificateId: certificate.id, subscriberId }, "Certificate body not found for certificate"); @@ -649,8 +683,7 @@ export const pkiSyncQueueFactory = ({ break; } default: - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - throw new Error(`Unhandled PKI Sync Job ${job.name}`); + throw new Error(`Unhandled PKI Sync Job ${String(job.name)}`); } }; @@ -701,8 +734,7 @@ export const pkiSyncQueueFactory = ({ await $handleRemoveCertificatesJob(job as TPkiSyncRemoveCertificatesDTO, pkiSync); break; default: - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - throw new Error(`Unhandled PKI Sync Job ${job.name}`); + throw new Error(`Unhandled PKI Sync Job ${String(job.name)}`); } } finally { if (job.name === QueueJobs.PkiSyncSyncCertificates) { diff --git a/backend/src/services/pki-sync/pki-sync-service.ts b/backend/src/services/pki-sync/pki-sync-service.ts index d2a1607eb..f92c9e19f 100644 --- a/backend/src/services/pki-sync/pki-sync-service.ts +++ b/backend/src/services/pki-sync/pki-sync-service.ts @@ -38,7 +38,10 @@ const getDestinationAppType = (destination: PkiSync): AppConnection => { }; type TPkiSyncServiceFactoryDep = { - pkiSyncDAL: TPkiSyncDALFactory; + pkiSyncDAL: Pick< + TPkiSyncDALFactory, + "findById" | "findByProjectIdWithSubscribers" | "findByNameAndProjectId" | "create" | "updateById" | "deleteById" + >; pkiSubscriberDAL: Pick; appConnectionService: Pick; permissionService: Pick; @@ -262,7 +265,10 @@ export const pkiSyncServiceFactory = ({ return pkiSyncDAL.deleteById(id); }; - const listPkiSyncsByProjectId = async ({ projectId }: TListPkiSyncsByProjectId, actor: OrgServiceActor) => { + const listPkiSyncsByProjectId = async ( + { projectId }: TListPkiSyncsByProjectId, + actor: OrgServiceActor + ): Promise => { const { permission } = await permissionService.getProjectPermission({ actor: actor.type, actorId: actor.id, @@ -274,8 +280,9 @@ export const pkiSyncServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionPkiSyncActions.Read, ProjectPermissionSub.PkiSyncs); - const pkiSyncs = await pkiSyncDAL.findByProjectId(projectId); - return pkiSyncs as TPkiSync[]; + const pkiSyncsWithSubscribers = await pkiSyncDAL.findByProjectIdWithSubscribers(projectId); + + return pkiSyncsWithSubscribers as TPkiSync[]; }; const findPkiSyncById = async ({ id, projectId }: TFindPkiSyncByIdDTO, actor: OrgServiceActor) => { @@ -307,7 +314,12 @@ export const pkiSyncServiceFactory = ({ : ProjectPermissionSub.PkiSyncs ); - return pkiSync as TPkiSync; + const result = { + ...pkiSync, + subscriber: findSubscriber ? { id: findSubscriber.id, name: findSubscriber.name } : null + } as TPkiSync; + + return result; }; const triggerPkiSyncSyncCertificatesById = async ( diff --git a/backend/src/services/pki-sync/pki-sync-types.ts b/backend/src/services/pki-sync/pki-sync-types.ts index b1ca9115e..bf750beee 100644 --- a/backend/src/services/pki-sync/pki-sync-types.ts +++ b/backend/src/services/pki-sync/pki-sync-types.ts @@ -49,6 +49,10 @@ export type TPkiSync = { updatedAt: Date; isPlatformManagedCredentials?: boolean; }; + subscriber?: { + id: string; + name: string; + } | null; }; export type TPkiSyncWithCredentials = TPkiSync & { @@ -66,7 +70,7 @@ export type TPkiSyncListItem = TPkiSync & { appConnectionApp: string; }; -export type TCertificateMap = Record; +export type TCertificateMap = Record; export type TCreatePkiSyncDTO = { name: string; diff --git a/docs/documentation/platform/pki/certificate-syncs/azure-key-vault.mdx b/docs/documentation/platform/pki/certificate-syncs/azure-key-vault.mdx index 55da5232b..6190ab3c3 100644 --- a/docs/documentation/platform/pki/certificate-syncs/azure-key-vault.mdx +++ b/docs/documentation/platform/pki/certificate-syncs/azure-key-vault.mdx @@ -47,6 +47,12 @@ description: "Learn how to configure an Azure Key Vault Certificate Sync for Inf - **Enable Certificate Removal**: If enabled, Infisical will remove expired certificates from the destination during sync operations. Disable this option if you intend to manage certificate cleanup manually. - **Certificate Name Schema** (Optional): Customize how certificate names are generated in Azure Key Vault. Use `{{certificateId}}` as a placeholder for the certificate ID. If not specified, defaults to `Infisical-{{certificateId}}`. + + **Azure Key Vault Soft Delete**: When certificates are removed from Azure Key Vault, they are placed in a soft-deleted state rather than being permanently deleted. This means: + - Subsequent syncs will not re-add these soft-deleted certificates automatically + - To resync removed certificates, you must either manually **purge** them from Azure Key Vault or **recover** them through the Azure portal/CLI + + 6. Configure the **Details** of your Azure Key Vault Certificate Sync, then click **Next**. ![Configure Details](/images/certificate-syncs/azure-key-vault/vault-details.png) diff --git a/frontend/src/components/pki-syncs/DeletePkiSyncModal.tsx b/frontend/src/components/pki-syncs/DeletePkiSyncModal.tsx index c01b70e90..e465d3f34 100644 --- a/frontend/src/components/pki-syncs/DeletePkiSyncModal.tsx +++ b/frontend/src/components/pki-syncs/DeletePkiSyncModal.tsx @@ -51,11 +51,6 @@ export const DeletePkiSyncModal = ({ isOpen, onOpenChange, pkiSync, onComplete } title={`Are you sure you want to delete ${name}?`} deleteKey={name} onDeleteApproved={handleDeletePkiSync} - > -

- This action will also remove all certificates that were synced by this configuration from - the {PKI_SYNC_MAP[destination].name} destination. -

- + /> ); }; diff --git a/frontend/src/components/pki-syncs/PkiSyncImportCertificatesModal.tsx b/frontend/src/components/pki-syncs/PkiSyncImportCertificatesModal.tsx index 26911f139..33b06dbe4 100644 --- a/frontend/src/components/pki-syncs/PkiSyncImportCertificatesModal.tsx +++ b/frontend/src/components/pki-syncs/PkiSyncImportCertificatesModal.tsx @@ -15,7 +15,7 @@ type ContentProps = { }; const Content = ({ pkiSync, onComplete }: ContentProps) => { - const { id: syncId, destination } = pkiSync; + const { id: syncId, destination, projectId } = pkiSync; const destinationName = PKI_SYNC_MAP[destination].name; const triggerImportCertificates = useTriggerPkiSyncImportCertificates(); @@ -24,7 +24,8 @@ const Content = ({ pkiSync, onComplete }: ContentProps) => { try { await triggerImportCertificates.mutateAsync({ syncId, - destination + destination, + projectId }); createNotification({ diff --git a/frontend/src/components/pki-syncs/PkiSyncRemoveCertificatesModal.tsx b/frontend/src/components/pki-syncs/PkiSyncRemoveCertificatesModal.tsx index 2fa019804..f17855289 100644 --- a/frontend/src/components/pki-syncs/PkiSyncRemoveCertificatesModal.tsx +++ b/frontend/src/components/pki-syncs/PkiSyncRemoveCertificatesModal.tsx @@ -15,7 +15,7 @@ type ContentProps = { }; const Content = ({ pkiSync, onComplete }: ContentProps) => { - const { id: syncId, destination } = pkiSync; + const { id: syncId, destination, projectId } = pkiSync; const destinationName = PKI_SYNC_MAP[destination].name; const triggerRemoveCertificates = useTriggerPkiSyncRemoveCertificates(); @@ -24,7 +24,8 @@ const Content = ({ pkiSync, onComplete }: ContentProps) => { try { await triggerRemoveCertificates.mutateAsync({ syncId, - destination + destination, + projectId }); createNotification({ diff --git a/frontend/src/components/pki-syncs/forms/PkiSyncReviewFields.tsx b/frontend/src/components/pki-syncs/forms/PkiSyncReviewFields.tsx index 41d84cb72..7ddf6a514 100644 --- a/frontend/src/components/pki-syncs/forms/PkiSyncReviewFields.tsx +++ b/frontend/src/components/pki-syncs/forms/PkiSyncReviewFields.tsx @@ -67,9 +67,6 @@ export const PkiSyncReviewFields = () => { {isAutoSyncEnabled ? "Enabled" : "Disabled"} - - Always Enabled - {/* Hidden for now - Import certificates functionality disabled {syncOptions?.canImportCertificates !== undefined && ( diff --git a/frontend/src/hooks/api/pkiSyncs/mutations.tsx b/frontend/src/hooks/api/pkiSyncs/mutations.tsx index abae04833..30cf18690 100644 --- a/frontend/src/hooks/api/pkiSyncs/mutations.tsx +++ b/frontend/src/hooks/api/pkiSyncs/mutations.tsx @@ -1,6 +1,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; +import { PkiSyncStatus } from "@app/hooks/api/pkiSyncs/enums"; import { pkiSyncKeys } from "@app/hooks/api/pkiSyncs/queries"; import { TCreatePkiSyncDTO, @@ -69,10 +70,39 @@ export const useTriggerPkiSyncSyncCertificates = () => { return data; }, - onSuccess: (_, { syncId }) => { - // Invalidate all PKI sync queries since we don't have projectId here - queryClient.invalidateQueries({ queryKey: pkiSyncKeys.all }); - queryClient.invalidateQueries({ queryKey: ["pkiSync", syncId] }); + onMutate: async ({ syncId, projectId }) => { + await queryClient.cancelQueries({ queryKey: pkiSyncKeys.byId(syncId, projectId) }); + await queryClient.cancelQueries({ queryKey: pkiSyncKeys.list(projectId) }); + + const previousPkiSync = queryClient.getQueryData(pkiSyncKeys.byId(syncId, projectId)); + + if (previousPkiSync) { + queryClient.setQueryData(pkiSyncKeys.byId(syncId, projectId), { + ...previousPkiSync, + syncStatus: PkiSyncStatus.Pending + }); + } + + return { previousPkiSync }; + }, + onSuccess: (_, { syncId, projectId }) => { + const currentData = queryClient.getQueryData(pkiSyncKeys.byId(syncId, projectId)); + if (currentData) { + queryClient.setQueryData(pkiSyncKeys.byId(syncId, projectId), { + ...currentData, + syncStatus: PkiSyncStatus.Pending + }); + } + + setTimeout(() => { + queryClient.invalidateQueries({ queryKey: pkiSyncKeys.byId(syncId, projectId) }); + queryClient.invalidateQueries({ queryKey: pkiSyncKeys.list(projectId) }); + }, 2000); // Wait 2 seconds before refetching + }, + onError: (_, { syncId, projectId }, context) => { + if (context?.previousPkiSync) { + queryClient.setQueryData(pkiSyncKeys.byId(syncId, projectId), context.previousPkiSync); + } } }); }; @@ -85,10 +115,39 @@ export const useTriggerPkiSyncImportCertificates = () => { return data; }, - onSuccess: (_, { syncId }) => { - // Invalidate all PKI sync queries since we don't have projectId here - queryClient.invalidateQueries({ queryKey: pkiSyncKeys.all }); - queryClient.invalidateQueries({ queryKey: ["pkiSync", syncId] }); + onMutate: async ({ syncId, projectId }) => { + await queryClient.cancelQueries({ queryKey: pkiSyncKeys.byId(syncId, projectId) }); + await queryClient.cancelQueries({ queryKey: pkiSyncKeys.list(projectId) }); + + const previousPkiSync = queryClient.getQueryData(pkiSyncKeys.byId(syncId, projectId)); + + if (previousPkiSync) { + queryClient.setQueryData(pkiSyncKeys.byId(syncId, projectId), { + ...previousPkiSync, + importStatus: PkiSyncStatus.Pending + }); + } + + return { previousPkiSync }; + }, + onSuccess: (_, { syncId, projectId }) => { + const currentData = queryClient.getQueryData(pkiSyncKeys.byId(syncId, projectId)); + if (currentData) { + queryClient.setQueryData(pkiSyncKeys.byId(syncId, projectId), { + ...currentData, + importStatus: PkiSyncStatus.Pending + }); + } + + setTimeout(() => { + queryClient.invalidateQueries({ queryKey: pkiSyncKeys.byId(syncId, projectId) }); + queryClient.invalidateQueries({ queryKey: pkiSyncKeys.list(projectId) }); + }, 2000); // Wait 2 seconds before refetching + }, + onError: (_, { syncId, projectId }, context) => { + if (context?.previousPkiSync) { + queryClient.setQueryData(pkiSyncKeys.byId(syncId, projectId), context.previousPkiSync); + } } }); }; @@ -103,10 +162,39 @@ export const useTriggerPkiSyncRemoveCertificates = () => { return data; }, - onSuccess: (_, { syncId }) => { - // Invalidate all PKI sync queries since we don't have projectId here - queryClient.invalidateQueries({ queryKey: pkiSyncKeys.all }); - queryClient.invalidateQueries({ queryKey: ["pkiSync", syncId] }); + onMutate: async ({ syncId, projectId }) => { + await queryClient.cancelQueries({ queryKey: pkiSyncKeys.byId(syncId, projectId) }); + await queryClient.cancelQueries({ queryKey: pkiSyncKeys.list(projectId) }); + + const previousPkiSync = queryClient.getQueryData(pkiSyncKeys.byId(syncId, projectId)); + + if (previousPkiSync) { + queryClient.setQueryData(pkiSyncKeys.byId(syncId, projectId), { + ...previousPkiSync, + removeStatus: PkiSyncStatus.Pending + }); + } + + return { previousPkiSync }; + }, + onSuccess: (_, { syncId, projectId }) => { + const currentData = queryClient.getQueryData(pkiSyncKeys.byId(syncId, projectId)); + if (currentData) { + queryClient.setQueryData(pkiSyncKeys.byId(syncId, projectId), { + ...currentData, + removeStatus: PkiSyncStatus.Pending + }); + } + + setTimeout(() => { + queryClient.invalidateQueries({ queryKey: pkiSyncKeys.byId(syncId, projectId) }); + queryClient.invalidateQueries({ queryKey: pkiSyncKeys.list(projectId) }); + }, 2000); // Wait 2 seconds before refetching + }, + onError: (_, { syncId, projectId }, context) => { + if (context?.previousPkiSync) { + queryClient.setQueryData(pkiSyncKeys.byId(syncId, projectId), context.previousPkiSync); + } } }); }; diff --git a/frontend/src/hooks/api/pkiSyncs/types/index.ts b/frontend/src/hooks/api/pkiSyncs/types/index.ts index 30c2351e4..baf8a693b 100644 --- a/frontend/src/hooks/api/pkiSyncs/types/index.ts +++ b/frontend/src/hooks/api/pkiSyncs/types/index.ts @@ -48,16 +48,19 @@ export type TDeletePkiSyncDTO = { export type TTriggerPkiSyncSyncCertificatesDTO = { syncId: string; destination: PkiSync; + projectId: string; }; export type TTriggerPkiSyncImportCertificatesDTO = { syncId: string; destination: PkiSync; + projectId: string; }; export type TTriggerPkiSyncRemoveCertificatesDTO = { syncId: string; destination: PkiSync; + projectId: string; }; export * from "./common"; diff --git a/frontend/src/pages/cert-manager/IntegrationsListPage/components/PkiSyncsTab/PkiSyncTable/PkiSyncRow.tsx b/frontend/src/pages/cert-manager/IntegrationsListPage/components/PkiSyncsTab/PkiSyncTable/PkiSyncRow.tsx index ec3f6a11f..bb96e72f0 100644 --- a/frontend/src/pages/cert-manager/IntegrationsListPage/components/PkiSyncsTab/PkiSyncTable/PkiSyncRow.tsx +++ b/frontend/src/pages/cert-manager/IntegrationsListPage/components/PkiSyncsTab/PkiSyncTable/PkiSyncRow.tsx @@ -164,7 +164,7 @@ export const PkiSyncRow = ({ {subscriberId ? ( - + ) : ( diff --git a/frontend/src/pages/cert-manager/IntegrationsListPage/components/PkiSyncsTab/PkiSyncTable/PkiSyncsTable.tsx b/frontend/src/pages/cert-manager/IntegrationsListPage/components/PkiSyncsTab/PkiSyncTable/PkiSyncsTable.tsx index a3c04f14c..9f4694b0b 100644 --- a/frontend/src/pages/cert-manager/IntegrationsListPage/components/PkiSyncsTab/PkiSyncTable/PkiSyncsTable.tsx +++ b/frontend/src/pages/cert-manager/IntegrationsListPage/components/PkiSyncsTab/PkiSyncTable/PkiSyncsTable.tsx @@ -252,7 +252,8 @@ export const PkiSyncsTable = ({ pkiSyncs }: Props) => { try { await triggerSync.mutateAsync({ syncId: pkiSync.id, - destination: pkiSync.destination + destination: pkiSync.destination, + projectId: pkiSync.projectId }); createNotification({ diff --git a/frontend/src/pages/cert-manager/IntegrationsListPage/components/PkiSyncsTab/PkiSyncsTab.tsx b/frontend/src/pages/cert-manager/IntegrationsListPage/components/PkiSyncsTab/PkiSyncsTab.tsx index 68c6e4bea..38a66e12e 100644 --- a/frontend/src/pages/cert-manager/IntegrationsListPage/components/PkiSyncsTab/PkiSyncsTab.tsx +++ b/frontend/src/pages/cert-manager/IntegrationsListPage/components/PkiSyncsTab/PkiSyncsTab.tsx @@ -45,7 +45,10 @@ export const PkiSyncsTab = () => { }, [addSync, handlePopUpOpen, navigateToBase]); const { data: pkiSyncs = [], isPending: isPkiSyncsPending } = useListPkiSyncs( - currentProject?.id || "" + currentProject?.id || "", + { + refetchInterval: 30000 + } ); if (isPkiSyncsPending) diff --git a/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncActionTriggers.tsx b/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncActionTriggers.tsx index 09392b504..46a019d89 100644 --- a/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncActionTriggers.tsx +++ b/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncActionTriggers.tsx @@ -89,7 +89,8 @@ export const PkiSyncActionTriggers = ({ pkiSync }: Props) => { try { await triggerSyncMutation.mutateAsync({ syncId: id, - destination + destination, + projectId }); createNotification({ text: "PKI sync job queued successfully", @@ -102,7 +103,7 @@ export const PkiSyncActionTriggers = ({ pkiSync }: Props) => { type: "error" }); } - }, [triggerSyncMutation, id, destination]); + }, [triggerSyncMutation, id, destination, projectId]); const handleToggleAutoSync = useCallback(async () => { try { diff --git a/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncSourceSection.tsx b/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncSourceSection.tsx index 29df6b746..4a6852903 100644 --- a/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncSourceSection.tsx +++ b/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncSourceSection.tsx @@ -22,7 +22,7 @@ type Props = { }; export const PkiSyncSourceSection = ({ pkiSync, onEditSource }: Props) => { - const { subscriberId } = pkiSync; + const { subscriberId, subscriber } = pkiSync; const permissionSubject = subject(ProjectPermissionSub.PkiSyncs, { subscriberId: subscriberId || "" @@ -65,7 +65,7 @@ export const PkiSyncSourceSection = ({ pkiSync, onEditSource }: Props) => {
- {subscriberId || "Deleted"} + {subscriber ? subscriber.name : "Deleted"}