diff --git a/backend/src/db/migrations/20251031044512_add-certificate-sync-external-identifier.ts b/backend/src/db/migrations/20251031044512_add-certificate-sync-external-identifier.ts new file mode 100644 index 000000000..69e15795a --- /dev/null +++ b/backend/src/db/migrations/20251031044512_add-certificate-sync-external-identifier.ts @@ -0,0 +1,21 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasColumn(TableName.CertificateSync, "externalIdentifier"))) { + await knex.schema.alterTable(TableName.CertificateSync, (t) => { + t.text("externalIdentifier").nullable(); + t.index("externalIdentifier"); + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasColumn(TableName.CertificateSync, "externalIdentifier")) { + await knex.schema.alterTable(TableName.CertificateSync, (t) => { + t.dropIndex("externalIdentifier"); + t.dropColumn("externalIdentifier"); + }); + } +} diff --git a/backend/src/db/schemas/certificate-syncs.ts b/backend/src/db/schemas/certificate-syncs.ts index 6f10e4d79..78a8a6e5f 100644 --- a/backend/src/db/schemas/certificate-syncs.ts +++ b/backend/src/db/schemas/certificate-syncs.ts @@ -14,6 +14,7 @@ export const CertificateSyncsSchema = z.object({ syncStatus: z.string().default("pending").nullable().optional(), lastSyncMessage: z.string().nullable().optional(), lastSyncedAt: z.date().nullable().optional(), + externalIdentifier: z.string().nullable().optional(), createdAt: z.date(), updatedAt: z.date() }); diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index b58503110..c6ac6ff3b 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -426,6 +426,7 @@ export enum EventType { SECRET_SYNC_REMOVE_SECRETS = "secret-sync-remove-secrets", GET_PKI_SYNCS = "get-pki-syncs", GET_PKI_SYNC = "get-pki-sync", + GET_PKI_SYNC_CERTIFICATES = "get-pki-sync-certificates", CREATE_PKI_SYNC = "create-pki-sync", UPDATE_PKI_SYNC = "update-pki-sync", DELETE_PKI_SYNC = "delete-pki-sync", @@ -3161,6 +3162,16 @@ interface GetPkiSyncEvent { }; } +interface GetPkiSyncCertificatesEvent { + type: EventType.GET_PKI_SYNC_CERTIFICATES; + metadata: { + syncId: string; + count: number; + certificateIds: string[]; + destination: string; + }; +} + interface CreatePkiSyncEvent { type: EventType.CREATE_PKI_SYNC; metadata: { @@ -4329,6 +4340,7 @@ export type Event = | SecretSyncRemoveSecretsEvent | GetPkiSyncsEvent | GetPkiSyncEvent + | GetPkiSyncCertificatesEvent | CreatePkiSyncEvent | UpdatePkiSyncEvent | DeletePkiSyncEvent diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 386db0e8f..4375e1cac 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -2193,12 +2193,13 @@ export const registerRoutes = async ( const pkiSyncService = pkiSyncServiceFactory({ pkiSyncDAL, + certificateDAL, + certificateSyncDAL, pkiSubscriberDAL, appConnectionService, permissionService, licenseService, - pkiSyncQueue, - certificateSyncDAL + pkiSyncQueue }); const pkiTemplateService = pkiTemplatesServiceFactory({ 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 45e5464f2..0710a91fc 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 @@ -93,8 +93,8 @@ const PkiSyncCertificateSchema = z.object({ certificateStatus: z.string().optional(), certificateNotBefore: z.date().optional(), certificateNotAfter: z.date().optional(), - certificateRenewBeforeDays: z.number().optional(), - certificateRenewalError: z.string().optional(), + certificateRenewBeforeDays: z.number().nullish(), + certificateRenewalError: z.string().nullish(), pkiSyncName: z.string().optional(), pkiSyncDestination: z.string().optional() }); @@ -233,26 +233,26 @@ export const registerPkiSyncRouter = async (server: FastifyZodProvider) => { const { pkiSyncId } = req.params; const { offset, limit } = req.query; - const result = await server.services.pkiSync.listPkiSyncCertificates( + const { certificates, totalCount, pkiSyncInfo } = await server.services.pkiSync.listPkiSyncCertificates( { pkiSyncId, offset, limit }, req.permission ); - const pkiSync = await server.services.pkiSync.findPkiSyncById({ id: pkiSyncId }, req.permission); - await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - projectId: pkiSync.projectId, + projectId: pkiSyncInfo.projectId, event: { - type: EventType.GET_PKI_SYNC, + type: EventType.GET_PKI_SYNC_CERTIFICATES, metadata: { syncId: pkiSyncId, - destination: pkiSync.destination + destination: pkiSyncInfo.destination, + count: certificates.length, + certificateIds: certificates.map((c) => c.certificateId) } } }); - return result; + return { certificates, totalCount }; } }); @@ -294,21 +294,19 @@ export const registerPkiSyncRouter = async (server: FastifyZodProvider) => { const { pkiSyncId } = req.params; const { certificateIds } = req.body; - const addedCertificates = await server.services.pkiSync.addCertificatesToPkiSync( + const { addedCertificates, pkiSyncInfo } = await server.services.pkiSync.addCertificatesToPkiSync( { pkiSyncId, certificateIds }, req.permission ); - const pkiSync = await server.services.pkiSync.findPkiSyncById({ id: pkiSyncId }, req.permission); - await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - projectId: pkiSync.projectId, + projectId: pkiSyncInfo.projectId, event: { type: EventType.UPDATE_PKI_SYNC, metadata: { pkiSyncId, - name: pkiSync.name + name: pkiSyncInfo.name } } }); @@ -344,26 +342,24 @@ export const registerPkiSyncRouter = async (server: FastifyZodProvider) => { const { pkiSyncId } = req.params; const { certificateIds } = req.body; - const result = await server.services.pkiSync.removeCertificatesFromPkiSync( + const { removedCount, pkiSyncInfo } = await server.services.pkiSync.removeCertificatesFromPkiSync( { pkiSyncId, certificateIds }, req.permission ); - const pkiSync = await server.services.pkiSync.findPkiSyncById({ id: pkiSyncId }, req.permission); - await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - projectId: pkiSync.projectId, + projectId: pkiSyncInfo.projectId, event: { type: EventType.UPDATE_PKI_SYNC, metadata: { pkiSyncId, - name: pkiSync.name + name: pkiSyncInfo.name } } }); - return result; + return { removedCount }; } }); }; diff --git a/backend/src/services/certificate-authority/azure-ad-cs/azure-ad-cs-certificate-authority-fns.ts b/backend/src/services/certificate-authority/azure-ad-cs/azure-ad-cs-certificate-authority-fns.ts index 4937a5331..26f59a402 100644 --- a/backend/src/services/certificate-authority/azure-ad-cs/azure-ad-cs-certificate-authority-fns.ts +++ b/backend/src/services/certificate-authority/azure-ad-cs/azure-ad-cs-certificate-authority-fns.ts @@ -192,7 +192,7 @@ export const castDbEntryToAzureAdCsCertificateAuthority = ( ca: Awaited> ): TAzureAdCsCertificateAuthority & { credentials: unknown } => { if (!ca.externalCa?.id) { - throw new BadRequestError({ message: "Malformed Azure AD Certificate Service certificate authority" }); + throw new BadRequestError({ message: "Malformed Active Directory Certificate Service certificate authority" }); } if (!ca.externalCa.dnsAppConnectionId) { @@ -776,7 +776,7 @@ export const AzureAdCsCertificateAuthorityFns = ({ const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(subscriber.caId); if (!ca.externalCa || ca.externalCa.type !== CaType.AZURE_AD_CS) { - throw new BadRequestError({ message: "CA is not an Azure AD Certificate Service CA" }); + throw new BadRequestError({ message: "CA is not an Active Directory Certificate Service CA" }); } const azureCa = castDbEntryToAzureAdCsCertificateAuthority(ca); diff --git a/backend/src/services/certificate-authority/certificate-authority-maps.ts b/backend/src/services/certificate-authority/certificate-authority-maps.ts index ef844a1ed..bae444cae 100644 --- a/backend/src/services/certificate-authority/certificate-authority-maps.ts +++ b/backend/src/services/certificate-authority/certificate-authority-maps.ts @@ -2,8 +2,8 @@ import { CaCapability, CaType } from "./certificate-authority-enums"; export const CERTIFICATE_AUTHORITIES_TYPE_MAP: Record = { [CaType.INTERNAL]: "Internal", - [CaType.ACME]: "ACME", - [CaType.AZURE_AD_CS]: "Azure AD Certificate Service" + [CaType.ACME]: "ACME-compatible CA", + [CaType.AZURE_AD_CS]: "Active Directory Certificate Service" }; export const CERTIFICATE_AUTHORITIES_CAPABILITIES_MAP: Record = { diff --git a/backend/src/services/certificate-sync/certificate-sync-dal.ts b/backend/src/services/certificate-sync/certificate-sync-dal.ts index 904a18a75..a46ae8e5a 100644 --- a/backend/src/services/certificate-sync/certificate-sync-dal.ts +++ b/backend/src/services/certificate-sync/certificate-sync-dal.ts @@ -72,14 +72,15 @@ export const certificateSyncDALFactory = (db: TDbClient) => { const addCertificates = async ( pkiSyncId: string, - certificateIds: string[], + certificateData: Array<{ certificateId: string; externalIdentifier?: string }>, tx?: Knex ): Promise => { try { - const insertData = certificateIds.map((certificateId) => ({ + const insertData = certificateData.map(({ certificateId, externalIdentifier }) => ({ pkiSyncId, certificateId, - syncStatus: CertificateSyncStatus.Pending + syncStatus: CertificateSyncStatus.Pending, + externalIdentifier })); const docs = await (tx || db)(TableName.CertificateSync).insert(insertData).returning("*"); @@ -184,7 +185,7 @@ export const certificateSyncDALFactory = (db: TDbClient) => { certificateStatus?: string; certificateNotBefore?: Date; certificateNotAfter?: Date; - certificateRenewBeforeDays?: number; + certificateRenewBeforeDays?: number | null; certificateRenewedByCertificateId?: string; certificateRenewalError?: string; pkiSyncName?: string; diff --git a/backend/src/services/certificate-v3/certificate-v3-service.test.ts b/backend/src/services/certificate-v3/certificate-v3-service.test.ts index 11b51ecc7..d11cce056 100644 --- a/backend/src/services/certificate-v3/certificate-v3-service.test.ts +++ b/backend/src/services/certificate-v3/certificate-v3-service.test.ts @@ -137,7 +137,7 @@ describe("CertificateV3Service", () => { certificateSyncDAL: { findPkiSyncIdsByCertificateId: vi.fn().mockResolvedValue([]), addCertificates: vi.fn().mockResolvedValue([]), - removeCertificates: vi.fn().mockResolvedValue(0) + findByPkiSyncAndCertificate: vi.fn().mockResolvedValue(null) }, pkiSyncDAL: { find: vi.fn().mockResolvedValue([]) diff --git a/backend/src/services/certificate-v3/certificate-v3-service.ts b/backend/src/services/certificate-v3/certificate-v3-service.ts index 1cff10f3c..51c79f135 100644 --- a/backend/src/services/certificate-v3/certificate-v3-service.ts +++ b/backend/src/services/certificate-v3/certificate-v3-service.ts @@ -78,7 +78,7 @@ type TCertificateV3ServiceFactoryDep = { permissionService: Pick; certificateSyncDAL: Pick< TCertificateSyncDALFactory, - "findPkiSyncIdsByCertificateId" | "removeCertificates" | "addCertificates" + "findPkiSyncIdsByCertificateId" | "addCertificates" | "findByPkiSyncAndCertificate" >; pkiSyncDAL: Pick; pkiSyncQueue: Pick; diff --git a/backend/src/services/pki-sync/aws-certificate-manager/aws-certificate-manager-pki-sync-fns.ts b/backend/src/services/pki-sync/aws-certificate-manager/aws-certificate-manager-pki-sync-fns.ts index 0e416f016..ae4cdcf55 100644 --- a/backend/src/services/pki-sync/aws-certificate-manager/aws-certificate-manager-pki-sync-fns.ts +++ b/backend/src/services/pki-sync/aws-certificate-manager/aws-certificate-manager-pki-sync-fns.ts @@ -92,43 +92,6 @@ const shouldSkipCertificateExport = (certificate: AWS.ACM.CertificateSummary): b return isAwsIssuedCertificate(certificate); }; -const findTagByKey = (tags: AWS.ACM.TagList | undefined, key: string): AWS.ACM.Tag | undefined => { - if (!tags || !Array.isArray(tags)) { - return undefined; - } - return tags.find((tag: AWS.ACM.Tag) => tag.Key === key && tag.Value); -}; - -const findInfisicalCertificateTag = (tags: AWS.ACM.TagList | undefined): AWS.ACM.Tag | undefined => { - return findTagByKey(tags, INFISICAL_CERTIFICATE_TAG); -}; - -const validateCertificateIdentification = ( - certName: string, - existingCert: { arn?: string; Tags?: AWS.ACM.TagList; cert?: string; privateKey?: string; certificateChain?: string }, - alternativeCertNames?: string[] -): boolean => { - if (!existingCert?.arn || !existingCert?.Tags) { - return false; - } - - const certNameTag = findInfisicalCertificateTag(existingCert.Tags); - - if (!certNameTag || !certNameTag.Value) { - return false; - } - - if (certNameTag.Value === certName) { - return true; - } - - if (alternativeCertNames && alternativeCertNames.includes(certNameTag.Value)) { - return true; - } - - return false; -}; - const validateCertificateNameSchema = (schema: string): void => { if (!schema.includes("{{certificateId}}")) { throw new Error( @@ -185,7 +148,15 @@ const generateCertificateName = (certificateName: string, pkiSync: TPkiSyncWithC type TAwsCertificateManagerPkiSyncFactoryDeps = { appConnectionDAL: Pick; kmsService: Pick; - certificateSyncDAL: Pick; + certificateSyncDAL: Pick< + TCertificateSyncDALFactory, + | "removeCertificates" + | "addCertificates" + | "findByPkiSyncAndCertificate" + | "updateSyncStatus" + | "updateById" + | "findByPkiSyncId" + >; certificateDAL: Pick; }; @@ -418,16 +389,38 @@ export const awsCertificateManagerPkiSyncFactory = ({ >; } = await $getAwsAcmCertificates(acm, pkiSync.id); + const acmCertificatesByArn = new Map(); + Object.values(acmCertificates).forEach((acmCert) => { + if (acmCert.arn) { + acmCertificatesByArn.set(acmCert.arn, acmCert); + } + }); + + const existingSyncRecords = await certificateSyncDAL.findByPkiSyncId(pkiSync.id); + type SyncRecord = (typeof existingSyncRecords)[0]; + const syncRecordsByCertId = new Map(); + const syncRecordsByExternalId = new Map(); + + existingSyncRecords.forEach((record: SyncRecord) => { + if (record.certificateId) { + syncRecordsByCertId.set(record.certificateId, record); + } + if (record.externalIdentifier) { + syncRecordsByExternalId.set(record.externalIdentifier, record); + } + }); + const setCertificates: CertificateImportRequest[] = []; const validationErrors: Array<{ name: string; error: string }> = []; - const activeCertificateNames = Object.keys(certificateMap); const syncOptions = pkiSync.syncOptions as { preserveArn?: boolean; canRemoveCertificates?: boolean } | undefined; const preserveArn = syncOptions?.preserveArn ?? true; const canRemoveCertificates = syncOptions?.canRemoveCertificates ?? true; + const activeExternalIdentifiers = new Set(); + for (const [certName, certData] of Object.entries(certificateMap)) { - const { cert, privateKey, certificateChain, alternativeNames, certificateId } = certData; + const { cert, privateKey, certificateChain, certificateId } = certData; try { validateCertificateContent(cert, privateKey); @@ -441,7 +434,7 @@ export const awsCertificateManagerPkiSyncFactory = ({ continue; } - if (preserveArn && certificateId) { + if (preserveArn && certificateId && typeof certificateId === "string") { const certificate = await certificateDAL.findById(certificateId); if (certificate?.renewedByCertificateId) { // eslint-disable-next-line no-continue @@ -451,60 +444,120 @@ export const awsCertificateManagerPkiSyncFactory = ({ const certificateName = generateCertificateName(certName, pkiSync); - let existingArn: string | undefined; + let targetArn: string | undefined; + let shouldCreateNew = false; - const existingCert = Object.values(acmCertificates).find((acmCert) => { - return validateCertificateIdentification(certName, acmCert, alternativeNames); - }); + if (!certificateId || typeof certificateId !== "string") { + shouldCreateNew = true; + } else { + const currentCertificate = await certificateDAL.findById(certificateId); + const isRenewal = !!currentCertificate?.renewedFromCertificateId; - if (existingCert?.arn && preserveArn) { - // When preserveArn is true, reuse the existing ARN - existingArn = existingCert.arn; + if (isRenewal) { + const currentSyncRecord = syncRecordsByCertId.get(certificateId); + const oldCertificateId = currentCertificate.renewedFromCertificateId; + const oldSyncRecord = oldCertificateId ? syncRecordsByCertId.get(oldCertificateId) : undefined; + + if (currentSyncRecord?.externalIdentifier) { + const existingAcmCert = acmCertificatesByArn.get(currentSyncRecord.externalIdentifier); + + if (existingAcmCert) { + if (!preserveArn && oldSyncRecord?.externalIdentifier === currentSyncRecord.externalIdentifier) { + shouldCreateNew = true; + } else if (preserveArn && oldSyncRecord?.externalIdentifier === currentSyncRecord.externalIdentifier) { + targetArn = currentSyncRecord.externalIdentifier; + shouldCreateNew = true; + activeExternalIdentifiers.add(targetArn); + + if (oldCertificateId && oldSyncRecord) { + await certificateSyncDAL.removeCertificates(pkiSync.id, [oldCertificateId]); + } + } else { + targetArn = currentSyncRecord.externalIdentifier; + activeExternalIdentifiers.add(targetArn); + shouldCreateNew = false; + } + } else { + shouldCreateNew = true; + } + } else if (preserveArn && oldSyncRecord?.externalIdentifier) { + const existingAcmCert = acmCertificatesByArn.get(oldSyncRecord.externalIdentifier); + + if (existingAcmCert) { + targetArn = oldSyncRecord.externalIdentifier; + shouldCreateNew = true; + activeExternalIdentifiers.add(targetArn); + if (oldCertificateId) { + await certificateSyncDAL.removeCertificates(pkiSync.id, [oldCertificateId]); + } + } else { + shouldCreateNew = true; + } + } else { + shouldCreateNew = true; + } + } else { + const existingSyncRecord = syncRecordsByCertId.get(certificateId); + if (existingSyncRecord?.externalIdentifier) { + const existingAcmCert = acmCertificatesByArn.get(existingSyncRecord.externalIdentifier); + if (existingAcmCert) { + targetArn = existingSyncRecord.externalIdentifier; + activeExternalIdentifiers.add(targetArn); + shouldCreateNew = false; + } else { + shouldCreateNew = true; + } + } else { + shouldCreateNew = true; + } + } } - setCertificates.push({ - key: certName, - name: certificateName, - cert, - privateKey, - certificateChain, - existingArn, - certificateId - }); + if (shouldCreateNew) { + setCertificates.push({ + key: certName, + name: certificateName, + cert, + privateKey, + certificateChain, + existingArn: targetArn, + certificateId: certificateId as string + }); + } + + if (targetArn) { + activeExternalIdentifiers.add(targetArn); + } } - const certificatesToRemove = canRemoveCertificates - ? Object.values(acmCertificates) - .filter((acmCert) => { - if (!acmCert.arn || !acmCert.Tags) { - return false; + const certificatesToRemove: string[] = []; + + if (canRemoveCertificates) { + existingSyncRecords.forEach((syncRecord) => { + if (syncRecord.externalIdentifier && !activeExternalIdentifiers.has(syncRecord.externalIdentifier)) { + const acmCert = acmCertificatesByArn.get(syncRecord.externalIdentifier); + if (acmCert?.arn) { + certificatesToRemove.push(acmCert.arn); + } + } + }); + + Object.values(acmCertificates).forEach((acmCert) => { + if (acmCert.arn && acmCert.Tags) { + const hasInfisicalTag = acmCert.Tags.some((tag) => tag.Key === INFISICAL_CERTIFICATE_TAG && tag.Value); + + if (hasInfisicalTag) { + const isTrackedInSyncRecords = existingSyncRecords.some( + (record) => record.externalIdentifier === acmCert.arn + ); + const isInActiveSet = activeExternalIdentifiers.has(acmCert.arn); + if (!isTrackedInSyncRecords && !isInActiveSet && !certificatesToRemove.includes(acmCert.arn)) { + certificatesToRemove.push(acmCert.arn); } - - const certNameTag = findInfisicalCertificateTag(acmCert.Tags); - if (!certNameTag || !certNameTag.Value) { - return false; - } - - const isActive = activeCertificateNames.some((activeCertName) => { - const certData = certificateMap[activeCertName]; - if (!certData) return false; - - return validateCertificateIdentification(activeCertName, acmCert, certData.alternativeNames); - }); - - if (!isActive) { - return true; - } - - if (!preserveArn && isActive) { - return true; - } - - return false; - }) - .map((acmCert) => acmCert.arn!) - .filter((arn) => arn) - : []; + } + } + }); + } const uploadResults = await executeWithConcurrencyLimit( setCertificates, @@ -569,10 +622,21 @@ export const awsCertificateManagerPkiSyncFactory = ({ } } - if (existingArn && preserveArn && certificateId) { - const currentCertificate = await certificateDAL.findById(certificateId); - if (currentCertificate?.renewedFromCertificateId) { - await certificateSyncDAL.removeCertificates(pkiSync.id, [currentCertificate.renewedFromCertificateId]); + if (response.CertificateArn && certificateId) { + const existingCertSync = await certificateSyncDAL.findByPkiSyncAndCertificate(pkiSync.id, certificateId); + if (existingCertSync) { + await certificateSyncDAL.updateById(existingCertSync.id, { + externalIdentifier: response.CertificateArn, + syncStatus: CertificateSyncStatus.Succeeded, + lastSyncedAt: new Date() + }); + } else { + await certificateSyncDAL.addCertificates(pkiSync.id, [ + { + certificateId, + externalIdentifier: response.CertificateArn + } + ]); } } @@ -682,29 +746,33 @@ export const awsCertificateManagerPkiSyncFactory = ({ kmsService ); - const { - acmCertificates - }: { - acmCertificates: Record< - string, - { cert: string; privateKey: string; certificateChain?: string; arn?: string; Tags?: AWS.ACM.TagList } - >; - } = await $getAwsAcmCertificates(acm, pkiSync.id); - + const existingSyncRecords = await certificateSyncDAL.findByPkiSyncId(pkiSync.id); const certificateArnsToRemove: string[] = []; - + const certificateIdToArnMap = new Map(); for (const certName of certificateNames) { - const matchingCerts = Object.values(acmCertificates).filter((acmCert) => - validateCertificateIdentification(certName, acmCert) - ); + const certificateData = deps?.certificateMap?.[certName]; + if (certificateData?.certificateId) { + const { certificateId } = certificateData; - for (const acmCert of matchingCerts) { - if (acmCert.arn) { - certificateArnsToRemove.push(acmCert.arn); + if (typeof certificateId === "string") { + const syncRecord = existingSyncRecords.find((record) => record.certificateId === certificateId); + + if (syncRecord?.externalIdentifier) { + certificateArnsToRemove.push(syncRecord.externalIdentifier); + certificateIdToArnMap.set(certificateId, syncRecord.externalIdentifier); + } } } } + if (certificateArnsToRemove.length === 0) { + return { + removed: 0, + failed: 0, + skipped: certificateNames.length + }; + } + const results = await executeWithConcurrencyLimit( certificateArnsToRemove, async (certificateArn) => @@ -714,43 +782,38 @@ export const awsCertificateManagerPkiSyncFactory = ({ const failedRemovals = results.filter((result) => result.status === "rejected"); - if (failedRemovals.length > 0 && deps?.certificateSyncDAL && deps?.certificateMap) { - const certificateNameToArnMap = new Map(); - for (const certName of certificateNames) { - const matchingCerts = Object.values(acmCertificates).filter((acmCert) => - validateCertificateIdentification(certName, acmCert) - ); - for (const acmCert of matchingCerts) { - if (acmCert.arn) { - certificateNameToArnMap.set(acmCert.arn, certName); - } - } - } - + if (failedRemovals.length > 0 && deps?.certificateSyncDAL) { for (const failure of failedRemovals) { if (failure.status === "rejected") { const failedArn = certificateArnsToRemove[results.indexOf(failure)]; - const certificateName = certificateNameToArnMap.get(failedArn); - if (certificateName && deps.certificateMap[certificateName]?.certificateId) { - const { certificateId } = deps.certificateMap[certificateName]; - if (certificateId) { - const errorMessage = failure.reason instanceof Error ? failure.reason.message : "Unknown error"; - try { - await deps.certificateSyncDAL.updateSyncStatus( - pkiSync.id, - certificateId, - CertificateSyncStatus.Failed, - `Failed to remove from AWS: ${errorMessage}` - ); - } catch (updateError) { - logger.warn(`Failed to update sync status for certificate ${certificateId}:`, String(updateError)); - } - } + const certificateId = Array.from(certificateIdToArnMap.entries()).find(([, arn]) => arn === failedArn)?.[0]; + + if (certificateId) { + const errorMessage = failure.reason instanceof Error ? failure.reason.message : "Unknown error"; + await deps.certificateSyncDAL.updateSyncStatus( + pkiSync.id, + certificateId, + CertificateSyncStatus.Failed, + `Failed to remove from AWS: ${errorMessage}` + ); } } } } + const successfulRemovals = results.filter((result) => result.status === "fulfilled"); + if (successfulRemovals.length > 0) { + const successfulArns = new Set(successfulRemovals.map((_, index) => certificateArnsToRemove[index])); + + const certificateIdsToRemove = Array.from(certificateIdToArnMap.entries()) + .filter(([, arn]) => successfulArns.has(arn)) + .map(([certificateId]) => certificateId); + + if (certificateIdsToRemove.length > 0) { + await certificateSyncDAL.removeCertificates(pkiSync.id, certificateIdsToRemove); + } + } + if (failedRemovals.length > 0) { const failedReasons = failedRemovals.map((failure) => { if (failure.status === "rejected") { 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 f2ae23400..0bd9e201d 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 @@ -6,6 +6,7 @@ import { request } from "@app/lib/config/request"; import { logger } from "@app/lib/logger"; import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; import { getAzureConnectionAccessToken } from "@app/services/app-connection/azure-key-vault"; +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"; @@ -50,6 +51,16 @@ const isInfisicalManagedCertificate = (certificateName: string, pkiSync: TPkiSyn type TAzureKeyVaultPkiSyncFactoryDeps = { appConnectionDAL: Pick; kmsService: Pick; + certificateSyncDAL: Pick< + TCertificateSyncDALFactory, + | "removeCertificates" + | "addCertificates" + | "findByPkiSyncAndCertificate" + | "updateById" + | "findByPkiSyncId" + | "updateSyncStatus" + >; + certificateDAL: Pick; }; const parseCertificateX509Props = (certPem: string) => { @@ -192,7 +203,12 @@ const parseCertificateKeyProps = (certPem: string) => { } }; -export const azureKeyVaultPkiSyncFactory = ({ kmsService, appConnectionDAL }: TAzureKeyVaultPkiSyncFactoryDeps) => { +export const azureKeyVaultPkiSyncFactory = ({ + kmsService, + appConnectionDAL, + certificateSyncDAL, + certificateDAL +}: TAzureKeyVaultPkiSyncFactoryDeps) => { const $getAzureKeyVaultCertificates = async (accessToken: string, vaultBaseUrl: string, syncId = "unknown") => { const paginateAzureKeyVaultCertificates = async () => { let result: GetAzureKeyVaultCertificate[] = []; @@ -329,6 +345,20 @@ export const azureKeyVaultPkiSyncFactory = ({ kmsService, appConnectionDAL }: TA pkiSync.id ); + const existingSyncRecords = await certificateSyncDAL.findByPkiSyncId(pkiSync.id); + type SyncRecord = (typeof existingSyncRecords)[0]; + const syncRecordsByCertId = new Map(); + const syncRecordsByExternalId = new Map(); + + existingSyncRecords.forEach((record: SyncRecord) => { + if (record.certificateId) { + syncRecordsByCertId.set(record.certificateId, record); + } + if (record.externalIdentifier) { + syncRecordsByExternalId.set(record.externalIdentifier, record); + } + }); + const setCertificates: { key: string; cert: string; @@ -338,48 +368,104 @@ export const azureKeyVaultPkiSyncFactory = ({ kmsService, appConnectionDAL }: TA }[] = []; const syncOptions = pkiSync.syncOptions as - | { certificateNameSchema?: string; canRemoveCertificates?: boolean } + | { certificateNameSchema?: string; canRemoveCertificates?: boolean; preserveVersion?: boolean } | undefined; const canRemoveCertificates = syncOptions?.canRemoveCertificates ?? true; + const preserveVersion = syncOptions?.preserveVersion ?? true; - // Track which certificates should exist in Azure Key Vault - const activeCertificateNames = Object.keys(certificateMap); + const activeExternalIdentifiers = new Set(); // Iterate through certificates to sync to Azure Key Vault - Object.entries(certificateMap).forEach(([certName, { cert, privateKey, certificateChain, certificateId }]) => { + for (const [certName, { cert, privateKey, certificateChain, certificateId }] of Object.entries(certificateMap)) { if (disabledAzureKeyVaultCertificateKeys.includes(certName)) { - return; + // eslint-disable-next-line no-continue + continue; } - const existingCert = vaultCertificates[certName]; - const shouldUpdateCert = !existingCert || existingCert.cert !== cert; + if (preserveVersion && typeof certificateId === "string") { + const certificate = await certificateDAL.findById(certificateId); + if (certificate?.renewedByCertificateId) { + // eslint-disable-next-line no-continue + continue; + } + } - if (shouldUpdateCert) { + let targetCertName = certName; + let shouldCreateNew = false; + + if (typeof certificateId === "string") { + const existingSyncRecord = syncRecordsByCertId.get(certificateId); + + if (existingSyncRecord?.externalIdentifier) { + const existingAzureCert = vaultCertificates[existingSyncRecord.externalIdentifier]; + + if (existingAzureCert && preserveVersion) { + targetCertName = existingSyncRecord.externalIdentifier; + activeExternalIdentifiers.add(targetCertName); + + const shouldUpdateCert = existingAzureCert.cert !== cert; + if (shouldUpdateCert) { + shouldCreateNew = true; + } + } else if (!existingAzureCert) { + shouldCreateNew = true; + } else if (!preserveVersion) { + shouldCreateNew = true; + } + } else { + shouldCreateNew = true; + } + } else { + shouldCreateNew = true; + } + + if (shouldCreateNew || !vaultCertificates[targetCertName] || vaultCertificates[targetCertName].cert !== cert) { setCertificates.push({ - key: certName, + key: targetCertName, cert, privateKey, certificateChain, certificateId }); } - }); - // Identify expired/removed certificates that need to be cleaned up from Azure Key Vault - // Only remove certificates that were managed by Infisical (match naming schema) - const certificatesToRemove = canRemoveCertificates - ? Object.keys(vaultCertificates).filter( - (vaultCertName) => - isInfisicalManagedCertificate(vaultCertName, pkiSync) && - !activeCertificateNames.includes(vaultCertName) && - !disabledAzureKeyVaultCertificateKeys.includes(vaultCertName) - ) - : []; + if (targetCertName) { + activeExternalIdentifiers.add(targetCertName); + } + } + + const certificatesToRemove: string[] = []; + + if (canRemoveCertificates) { + existingSyncRecords.forEach((syncRecord) => { + if (syncRecord.externalIdentifier && !activeExternalIdentifiers.has(syncRecord.externalIdentifier)) { + if (vaultCertificates[syncRecord.externalIdentifier]) { + certificatesToRemove.push(syncRecord.externalIdentifier); + } + } + }); + + Object.keys(vaultCertificates).forEach((certificateName) => { + const isInfisicalManaged = isInfisicalManagedCertificate(certificateName, pkiSync); + + if (isInfisicalManaged) { + const isTrackedInSyncRecords = existingSyncRecords.some( + (record) => record.externalIdentifier === certificateName + ); + + const isInActiveSet = activeExternalIdentifiers.has(certificateName); + + if (!isTrackedInSyncRecords && !isInActiveSet && !certificatesToRemove.includes(certificateName)) { + certificatesToRemove.push(certificateName); + } + } + }); + } // Upload certificates to Azure Key Vault with rate limiting const uploadResults = await executeWithConcurrencyLimit( setCertificates, - async ({ key, cert, privateKey, certificateChain }) => { + async ({ key, cert, privateKey, certificateChain, certificateId }) => { try { // Combine private key, certificate, and certificate chain in PEM format for Azure Key Vault let combinedPem = ""; @@ -441,6 +527,31 @@ export const azureKeyVaultPkiSyncFactory = ({ kmsService, appConnectionDAL }: TA } ); + if (certificateId) { + const existingCertSync = await certificateSyncDAL.findByPkiSyncAndCertificate(pkiSync.id, certificateId); + if (existingCertSync) { + await certificateSyncDAL.updateById(existingCertSync.id, { + externalIdentifier: key, + syncStatus: CertificateSyncStatus.Succeeded, + lastSyncedAt: new Date() + }); + } else { + await certificateSyncDAL.addCertificates(pkiSync.id, [ + { + certificateId, + externalIdentifier: key + } + ]); + } + + if (preserveVersion) { + const currentCertificate = await certificateDAL.findById(certificateId); + if (currentCertificate?.renewedFromCertificateId) { + await certificateSyncDAL.removeCertificates(pkiSync.id, [currentCertificate.renewedFromCertificateId]); + } + } + } + return { key, success: true, response: response.data as unknown }; } catch (error) { if (error instanceof AxiosError) { @@ -622,13 +733,33 @@ export const azureKeyVaultPkiSyncFactory = ({ kmsService, appConnectionDAL }: TA // Cast destination config to Azure Key Vault config const destinationConfig = pkiSync.destinationConfig as TAzureKeyVaultPkiSyncConfig; - // Only remove certificates that are managed by Infisical (match naming schema) - const infisicalManagedCertNames = certificateNames.filter((certName) => - isInfisicalManagedCertificate(certName, pkiSync) - ); + const existingSyncRecords = await certificateSyncDAL.findByPkiSyncId(pkiSync.id); + const certificateNamesToRemove: string[] = []; + const certificateIdToNameMap = new Map(); + + for (const certName of certificateNames) { + if (deps?.certificateMap?.[certName]?.certificateId) { + const { certificateId } = deps.certificateMap[certName]; + + const syncRecord = existingSyncRecords.find((record) => record.certificateId === certificateId); + + if (syncRecord?.externalIdentifier && typeof certificateId === "string") { + certificateNamesToRemove.push(syncRecord.externalIdentifier); + certificateIdToNameMap.set(certificateId, syncRecord.externalIdentifier); + } + } + } + + if (certificateNamesToRemove.length === 0) { + return { + removed: 0, + failed: 0, + skipped: certificateNames.length + }; + } const results = await executeWithConcurrencyLimit( - infisicalManagedCertNames, + certificateNamesToRemove, async (certName) => { try { const response = await request.delete( @@ -663,29 +794,44 @@ export const azureKeyVaultPkiSyncFactory = ({ kmsService, appConnectionDAL }: TA }, { operation: "remove-specific-certificates", syncId: pkiSync.id } ); + const failedRemovals = results.filter((result) => result.status === "rejected"); - if (failedRemovals.length > 0 && deps?.certificateSyncDAL && deps?.certificateMap) { + if (failedRemovals.length > 0 && deps?.certificateSyncDAL) { for (const failure of failedRemovals) { if (failure.status === "rejected") { - const failedIndex = results.indexOf(failure); - const failedCertName = infisicalManagedCertNames[failedIndex]; - if (failedCertName && deps.certificateMap[failedCertName]?.certificateId) { - const { certificateId } = deps.certificateMap[failedCertName]; - if (certificateId) { - const errorMessage = (failure.reason as Error)?.message || "Unknown error"; - await deps.certificateSyncDAL.updateSyncStatus( - pkiSync.id, - certificateId, - CertificateSyncStatus.Failed, - `Failed to remove from Azure: ${errorMessage}` - ); - } + const failedCertName = certificateNamesToRemove[results.indexOf(failure)]; + + const certificateId = Array.from(certificateIdToNameMap.entries()).find( + ([, name]) => name === failedCertName + )?.[0]; + + if (certificateId) { + const errorMessage = (failure.reason as Error)?.message || "Unknown error"; + await deps.certificateSyncDAL.updateSyncStatus( + pkiSync.id, + certificateId, + CertificateSyncStatus.Failed, + `Failed to remove from Azure: ${errorMessage}` + ); } } } } + const successfulRemovals = results.filter((result) => result.status === "fulfilled"); + if (successfulRemovals.length > 0) { + const successfulCertNames = new Set(successfulRemovals.map((_, index) => certificateNamesToRemove[index])); + + const certificateIdsToRemove = Array.from(certificateIdToNameMap.entries()) + .filter(([, name]) => successfulCertNames.has(name)) + .map(([certificateId]) => certificateId); + + if (certificateIdsToRemove.length > 0) { + await certificateSyncDAL.removeCertificates(pkiSync.id, certificateIdsToRemove); + } + } + if (failedRemovals.length > 0) { const failedReasons = failedRemovals.map((failure) => { if (failure.status === "rejected") { @@ -698,16 +844,16 @@ export const azureKeyVaultPkiSyncFactory = ({ kmsService, appConnectionDAL }: TA message: `Failed to remove ${failedRemovals.length} certificate(s) from Azure Key Vault`, context: { failedReasons, - totalCertificates: infisicalManagedCertNames.length, + totalCertificates: certificateNamesToRemove.length, failedCount: failedRemovals.length } }); } return { - removed: infisicalManagedCertNames.length - failedRemovals.length, + removed: certificateNamesToRemove.length - failedRemovals.length, failed: failedRemovals.length, - skipped: certificateNames.length - infisicalManagedCertNames.length + skipped: certificateNames.length - certificateNamesToRemove.length }; }; diff --git a/backend/src/services/pki-sync/azure-key-vault/azure-key-vault-pki-sync-schemas.ts b/backend/src/services/pki-sync/azure-key-vault/azure-key-vault-pki-sync-schemas.ts index 7e2c875da..b7fcbfb6d 100644 --- a/backend/src/services/pki-sync/azure-key-vault/azure-key-vault-pki-sync-schemas.ts +++ b/backend/src/services/pki-sync/azure-key-vault/azure-key-vault-pki-sync-schemas.ts @@ -14,6 +14,7 @@ export const AzureKeyVaultPkiSyncConfigSchema = z.object({ const AzureKeyVaultPkiSyncOptionsSchema = z.object({ canImportCertificates: z.boolean().default(false), canRemoveCertificates: z.boolean().default(true), + preserveVersion: z.boolean().default(true), certificateNameSchema: z .string() .optional() diff --git a/backend/src/services/pki-sync/pki-sync-fns.ts b/backend/src/services/pki-sync/pki-sync-fns.ts index be424a37d..961687f85 100644 --- a/backend/src/services/pki-sync/pki-sync-fns.ts +++ b/backend/src/services/pki-sync/pki-sync-fns.ts @@ -204,7 +204,12 @@ export const PkiSyncFns = { switch (pkiSync.destination) { case PkiSync.AzureKeyVault: { checkPkiSyncDestination(pkiSync, PkiSync.AzureKeyVault); - const azureKeyVaultPkiSync = azureKeyVaultPkiSyncFactory(dependencies); + const azureKeyVaultPkiSync = azureKeyVaultPkiSyncFactory({ + appConnectionDAL: dependencies.appConnectionDAL, + kmsService: dependencies.kmsService, + certificateDAL: dependencies.certificateDAL, + certificateSyncDAL: dependencies.certificateSyncDAL + }); return azureKeyVaultPkiSync.syncCertificates(pkiSync, certificateMap); } case PkiSync.AwsCertificateManager: { @@ -236,7 +241,12 @@ export const PkiSyncFns = { switch (pkiSync.destination) { case PkiSync.AzureKeyVault: { checkPkiSyncDestination(pkiSync, PkiSync.AzureKeyVault); - const azureKeyVaultPkiSync = azureKeyVaultPkiSyncFactory(dependencies); + const azureKeyVaultPkiSync = azureKeyVaultPkiSyncFactory({ + appConnectionDAL: dependencies.appConnectionDAL, + kmsService: dependencies.kmsService, + certificateDAL: dependencies.certificateDAL, + certificateSyncDAL: dependencies.certificateSyncDAL + }); await azureKeyVaultPkiSync.removeCertificates(pkiSync, certificateNames, { certificateSyncDAL: dependencies.certificateSyncDAL, certificateMap: dependencies.certificateMap diff --git a/backend/src/services/pki-sync/pki-sync-service.ts b/backend/src/services/pki-sync/pki-sync-service.ts index 920b7b866..0c385ce9c 100644 --- a/backend/src/services/pki-sync/pki-sync-service.ts +++ b/backend/src/services/pki-sync/pki-sync-service.ts @@ -1,6 +1,6 @@ import { ForbiddenError, subject } from "@casl/ability"; -import { ActionProjectType } from "@app/db/schemas"; +import { ActionProjectType, TCertificateSyncs } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionPkiSyncActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; @@ -10,6 +10,7 @@ import { AppConnection } from "@app/services/app-connection/app-connection-enums import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service"; import { TPkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal"; +import { TCertificateDALFactory } from "../certificate/certificate-dal"; import { TCertificateSyncDALFactory } from "../certificate-sync/certificate-sync-dal"; import { CertificateSyncStatus } from "../certificate-sync/certificate-sync-enums"; import { TPkiSyncDALFactory } from "./pki-sync-dal"; @@ -48,6 +49,7 @@ type TPkiSyncServiceFactoryDep = { TPkiSyncDALFactory, "findById" | "findByProjectIdWithSubscribers" | "findByNameAndProjectId" | "create" | "updateById" | "deleteById" >; + certificateDAL: Pick; certificateSyncDAL: Pick< TCertificateSyncDALFactory, | "findByPkiSyncId" @@ -72,6 +74,7 @@ export type TPkiSyncServiceFactory = ReturnType; export const pkiSyncServiceFactory = ({ pkiSyncDAL, + certificateDAL, certificateSyncDAL, pkiSubscriberDAL, appConnectionService, @@ -79,6 +82,26 @@ export const pkiSyncServiceFactory = ({ licenseService, pkiSyncQueue }: TPkiSyncServiceFactoryDep) => { + const validateCertificatesProjectOwnership = async (certificateIds: string[], expectedProjectId: string) => { + if (certificateIds.length === 0) return; + + const certificates = await certificateDAL.findActiveCertificatesByIds(certificateIds); + + if (certificates.length !== certificateIds.length) { + const foundIds = certificates.map((cert) => cert.id); + const missingIds = certificateIds.filter((id) => !foundIds.includes(id)); + throw new NotFoundError({ + message: `Certificates not found or not active: ${missingIds.join(", ")}` + }); + } + + const invalidCertificates = certificates.filter((cert) => cert.projectId !== expectedProjectId); + if (invalidCertificates.length > 0) { + throw new BadRequestError({ + message: `Certificates do not belong to the same project: ${invalidCertificates.map((cert) => cert.id).join(", ")}` + }); + } + }; const createPkiSync = async ( { name, @@ -132,6 +155,10 @@ export const pkiSyncServiceFactory = ({ ...syncOptions }; + if (certificateIds.length > 0) { + await validateCertificatesProjectOwnership(certificateIds, projectId); + } + try { const pkiSync = await pkiSyncDAL.create({ name, @@ -147,7 +174,10 @@ export const pkiSyncServiceFactory = ({ }); if (certificateIds.length > 0) { - await certificateSyncDAL.addCertificates(pkiSync.id, certificateIds); + await certificateSyncDAL.addCertificates( + pkiSync.id, + certificateIds.map((id) => ({ certificateId: id })) + ); } if (pkiSync.isAutoSyncEnabled) { @@ -245,9 +275,16 @@ export const pkiSyncServiceFactory = ({ } if (certificateIds !== undefined) { + if (certificateIds.length > 0) { + await validateCertificatesProjectOwnership(certificateIds, pkiSync.projectId); + } + await certificateSyncDAL.removeAllCertificatesFromSync(id); if (certificateIds.length > 0) { - await certificateSyncDAL.addCertificates(id, certificateIds); + await certificateSyncDAL.addCertificates( + id, + certificateIds.map((certId) => ({ certificateId: certId })) + ); } } @@ -489,7 +526,10 @@ export const pkiSyncServiceFactory = ({ const addCertificatesToPkiSync = async ( { pkiSyncId, certificateIds }: Omit, actor: OrgServiceActor - ) => { + ): Promise<{ + addedCertificates: TCertificateSyncs[]; + pkiSyncInfo: { projectId: string; destination: string; name: string }; + }> => { const pkiSync = await pkiSyncDAL.findById(pkiSyncId); if (!pkiSync) throw new NotFoundError({ message: "PKI sync not found" }); @@ -502,31 +542,33 @@ export const pkiSyncServiceFactory = ({ projectId: pkiSync.projectId }); - let subscriber; - if (pkiSync.subscriberId) { - subscriber = await pkiSubscriberDAL.findById(pkiSync.subscriberId); - } + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionPkiSyncActions.Edit, ProjectPermissionSub.PkiSyncs); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionPkiSyncActions.Edit, - subscriber - ? subject(ProjectPermissionSub.PkiSyncs, { subscriberName: subscriber.name }) - : ProjectPermissionSub.PkiSyncs + await validateCertificatesProjectOwnership(certificateIds, pkiSync.projectId); + + const addedCertificates = await certificateSyncDAL.addCertificates( + pkiSyncId, + certificateIds.map((id) => ({ certificateId: id })) ); - const addedCertificates = await certificateSyncDAL.addCertificates(pkiSyncId, certificateIds); - if (pkiSync.isAutoSyncEnabled) { await pkiSyncQueue.queuePkiSyncSyncCertificatesById({ syncId: pkiSyncId }); } - return addedCertificates; + return { + addedCertificates, + pkiSyncInfo: { + projectId: pkiSync.projectId, + destination: pkiSync.destination, + name: pkiSync.name + } + }; }; const removeCertificatesFromPkiSync = async ( { pkiSyncId, certificateIds }: Omit, actor: OrgServiceActor - ) => { + ): Promise<{ removedCount: number; pkiSyncInfo: { projectId: string; destination: string; name: string } }> => { const pkiSync = await pkiSyncDAL.findById(pkiSyncId); if (!pkiSync) throw new NotFoundError({ message: "PKI sync not found" }); @@ -539,17 +581,7 @@ export const pkiSyncServiceFactory = ({ projectId: pkiSync.projectId }); - let subscriber; - if (pkiSync.subscriberId) { - subscriber = await pkiSubscriberDAL.findById(pkiSync.subscriberId); - } - - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionPkiSyncActions.Edit, - subscriber - ? subject(ProjectPermissionSub.PkiSyncs, { subscriberName: subscriber.name }) - : ProjectPermissionSub.PkiSyncs - ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionPkiSyncActions.Edit, ProjectPermissionSub.PkiSyncs); const removedCount = await certificateSyncDAL.removeCertificates(pkiSyncId, certificateIds); @@ -557,13 +589,24 @@ export const pkiSyncServiceFactory = ({ await pkiSyncQueue.queuePkiSyncSyncCertificatesById({ syncId: pkiSyncId }); } - return { removedCount }; + return { + removedCount, + pkiSyncInfo: { + projectId: pkiSync.projectId, + destination: pkiSync.destination, + name: pkiSync.name + } + }; }; const listPkiSyncCertificates = async ( { pkiSyncId, offset = 0, limit = 20 }: Omit, actor: OrgServiceActor - ): Promise<{ certificates: TPkiSyncCertificate[]; totalCount: number }> => { + ): Promise<{ + certificates: TPkiSyncCertificate[]; + totalCount: number; + pkiSyncInfo: { projectId: string; destination: string; name: string }; + }> => { const pkiSync = await pkiSyncDAL.findById(pkiSyncId); if (!pkiSync) throw new NotFoundError({ message: "PKI sync not found" }); @@ -576,17 +619,7 @@ export const pkiSyncServiceFactory = ({ projectId: pkiSync.projectId }); - let subscriber; - if (pkiSync.subscriberId) { - subscriber = await pkiSubscriberDAL.findById(pkiSync.subscriberId); - } - - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionPkiSyncActions.Read, - subscriber - ? subject(ProjectPermissionSub.PkiSyncs, { subscriberName: subscriber.name }) - : ProjectPermissionSub.PkiSyncs - ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionPkiSyncActions.Read, ProjectPermissionSub.PkiSyncs); const result = await certificateSyncDAL.findWithDetails({ pkiSyncId, @@ -611,14 +644,22 @@ export const pkiSyncServiceFactory = ({ certificateNotBefore: detail.certificateNotBefore || undefined, certificateNotAfter: detail.certificateNotAfter || undefined, certificateRenewBeforeDays: !detail.certificateRenewedByCertificateId - ? detail.certificateRenewBeforeDays + ? detail.certificateRenewBeforeDays || undefined : undefined, certificateRenewalError: detail.certificateRenewalError || undefined, pkiSyncName: detail.pkiSyncName || undefined, pkiSyncDestination: detail.pkiSyncDestination || undefined })); - return { certificates, totalCount }; + return { + certificates, + totalCount, + pkiSyncInfo: { + projectId: pkiSync.projectId, + destination: pkiSync.destination, + name: pkiSync.name + } + }; }; return { diff --git a/backend/src/services/pki-sync/pki-sync-utils.ts b/backend/src/services/pki-sync/pki-sync-utils.ts index 3e278de48..d216ddd5a 100644 --- a/backend/src/services/pki-sync/pki-sync-utils.ts +++ b/backend/src/services/pki-sync/pki-sync-utils.ts @@ -45,12 +45,13 @@ export const triggerAutoSyncForCertificate = async ( } const allPkiSyncs = await dependencies.pkiSyncDAL.find({ - isAutoSyncEnabled: true + isAutoSyncEnabled: true, + $in: { + id: pkiSyncIds + } }); - const pkiSyncs = allPkiSyncs.filter((sync) => pkiSyncIds.includes(sync.id)); - - const syncPromises = pkiSyncs.map((pkiSync) => + const syncPromises = allPkiSyncs.map((pkiSync) => dependencies.pkiSyncQueue.queuePkiSyncSyncCertificatesById({ syncId: pkiSync.id }) ); await Promise.all(syncPromises); @@ -65,7 +66,7 @@ export const addRenewedCertificateToSyncs = async ( dependencies: { certificateSyncDAL: Pick< TCertificateSyncDALFactory, - "findPkiSyncIdsByCertificateId" | "removeCertificates" | "addCertificates" + "findPkiSyncIdsByCertificateId" | "addCertificates" | "findByPkiSyncAndCertificate" >; }, tx?: Knex @@ -78,12 +79,25 @@ export const addRenewedCertificateToSyncs = async ( } const addPromises = pkiSyncIds.map(async (pkiSyncId) => { - await dependencies.certificateSyncDAL.addCertificates(pkiSyncId, [newCertificateId], tx); - }); + const oldCertificateRecord = await dependencies.certificateSyncDAL.findByPkiSyncAndCertificate( + pkiSyncId, + oldCertificateId + ); + await dependencies.certificateSyncDAL.addCertificates( + pkiSyncId, + [ + { + certificateId: newCertificateId, + externalIdentifier: oldCertificateRecord?.externalIdentifier || undefined + } + ], + tx + ); + }); await Promise.all(addPromises); - logger.info(`Successfully added renewed certificate ${newCertificateId} to ${pkiSyncIds.length} PKI sync(s)`); + logger.info(`Successfully added renewed certificate ${newCertificateId} to PKI sync(s)`); } catch (error) { logger.error(error, `Failed to add renewed certificate ${newCertificateId} to syncs:`); throw error; diff --git a/docs/documentation/platform/pki/azure-adcs.mdx b/docs/documentation/platform/pki/azure-adcs.mdx index b0cea565e..23df3ed3b 100644 --- a/docs/documentation/platform/pki/azure-adcs.mdx +++ b/docs/documentation/platform/pki/azure-adcs.mdx @@ -31,7 +31,7 @@ This section walks you through the complete end-to-end process of setting up Azu Click **Create CA** and configure: - - **Type**: Choose **Azure AD Certificate Service** + - **Type**: Choose **Active Directory Certificate Services (AD CS)** - **Name**: Friendly name for this CA (e.g., "Production ADCS CA") - **App Connection**: Choose your ADCS connection from the dropdown diff --git a/frontend/src/components/pki-syncs/CreatePkiSyncModal.tsx b/frontend/src/components/pki-syncs/CreatePkiSyncModal.tsx index 5b5e81ba3..0169b2296 100644 --- a/frontend/src/components/pki-syncs/CreatePkiSyncModal.tsx +++ b/frontend/src/components/pki-syncs/CreatePkiSyncModal.tsx @@ -11,21 +11,24 @@ type Props = { isOpen: boolean; onOpenChange: (isOpen: boolean) => void; selectSync?: PkiSync | null; + initialData?: any; }; type ContentProps = { onComplete: (pkiSync: TPkiSync) => void; selectedSync: PkiSync | null; setSelectedSync: (selectedSync: PkiSync | null) => void; + initialData?: any; }; -const Content = ({ onComplete, setSelectedSync, selectedSync }: ContentProps) => { +const Content = ({ onComplete, setSelectedSync, selectedSync, initialData }: ContentProps) => { if (selectedSync) { return ( setSelectedSync(null)} destination={selectedSync} + initialData={initialData} /> ); } @@ -33,7 +36,12 @@ const Content = ({ onComplete, setSelectedSync, selectedSync }: ContentProps) => return ; }; -export const CreatePkiSyncModal = ({ onOpenChange, selectSync = null, ...props }: Props) => { +export const CreatePkiSyncModal = ({ + onOpenChange, + selectSync = null, + initialData, + ...props +}: Props) => { const [selectedSync, setSelectedSync] = useState(selectSync); useEffect(() => { @@ -69,6 +77,7 @@ export const CreatePkiSyncModal = ({ onOpenChange, selectSync = null, ...props } }} selectedSync={selectedSync} setSelectedSync={setSelectedSync} + initialData={initialData} /> diff --git a/frontend/src/components/pki-syncs/forms/CreatePkiSyncForm.tsx b/frontend/src/components/pki-syncs/forms/CreatePkiSyncForm.tsx index 305fda516..6ffb2f859 100644 --- a/frontend/src/components/pki-syncs/forms/CreatePkiSyncForm.tsx +++ b/frontend/src/components/pki-syncs/forms/CreatePkiSyncForm.tsx @@ -23,6 +23,7 @@ type Props = { onComplete: (pkiSync: TPkiSync) => void; destination: PkiSync; onCancel: () => void; + initialData?: any; }; const FORM_TABS: { name: string; key: string; fields: (keyof TPkiSyncForm)[] }[] = [ @@ -33,7 +34,7 @@ const FORM_TABS: { name: string; key: string; fields: (keyof TPkiSyncForm)[] }[] { name: "Review", key: "review", fields: [] } ]; -export const CreatePkiSyncForm = ({ destination, onComplete, onCancel }: Props) => { +export const CreatePkiSyncForm = ({ destination, onComplete, onCancel, initialData }: Props) => { const createPkiSync = useCreatePkiSync(); const { currentProject } = useProject(); const { name: destinationName } = PKI_SYNC_MAP[destination]; @@ -55,7 +56,8 @@ export const CreatePkiSyncForm = ({ destination, onComplete, onCancel }: Props) canRemoveCertificates: false, preserveArn: true, certificateNameSchema: syncOption?.defaultCertificateNameSchema - } + }, + ...initialData } as Partial, reValidateMode: "onChange" }); diff --git a/frontend/src/components/pki-syncs/forms/EditPkiSyncForm.tsx b/frontend/src/components/pki-syncs/forms/EditPkiSyncForm.tsx index 73f5bc2bc..137e0d6de 100644 --- a/frontend/src/components/pki-syncs/forms/EditPkiSyncForm.tsx +++ b/frontend/src/components/pki-syncs/forms/EditPkiSyncForm.tsx @@ -27,12 +27,16 @@ export const EditPkiSyncForm = ({ pkiSync, fields, onComplete }: Props) => { const formMethods = useForm({ resolver: zodResolver(UpdatePkiSyncFormSchema), defaultValues: { - ...pkiSync, + name: pkiSync.name, + destination: pkiSync.destination, description: pkiSync.description ?? "", connection: { id: pkiSync.connectionId, name: pkiSync.appConnectionName - } + }, + syncOptions: pkiSync.syncOptions, + destinationConfig: pkiSync.destinationConfig, + isAutoSyncEnabled: pkiSync.isAutoSyncEnabled } as Partial, reValidateMode: "onChange" }); diff --git a/frontend/src/components/pki-syncs/forms/PkiSyncConnectionField.tsx b/frontend/src/components/pki-syncs/forms/PkiSyncConnectionField.tsx index 445f9169d..90af04203 100644 --- a/frontend/src/components/pki-syncs/forms/PkiSyncConnectionField.tsx +++ b/frontend/src/components/pki-syncs/forms/PkiSyncConnectionField.tsx @@ -1,14 +1,18 @@ import { Controller, useFormContext } from "react-hook-form"; +import { SingleValue } from "react-select"; import { faInfoCircle } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { Link } from "@tanstack/react-router"; +import { useRouterState } from "@tanstack/react-router"; +import { AppConnectionOption } from "@app/components/app-connections"; import { FilterableSelect, FormControl } from "@app/components/v2"; import { ProjectPermissionSub, useProject, useProjectPermission } from "@app/context"; import { ProjectPermissionAppConnectionActions } from "@app/context/ProjectPermissionContext/types"; import { APP_CONNECTION_MAP } from "@app/helpers/appConnections"; import { PKI_SYNC_CONNECTION_MAP } from "@app/helpers/pkiSyncs"; +import { usePopUp } from "@app/hooks"; import { useListAvailableAppConnections } from "@app/hooks/api/appConnections"; +import { AddAppConnectionModal } from "@app/pages/organization/AppConnections/AppConnectionsPage/components"; import { TPkiSyncForm } from "./schemas/pki-sync-schema"; @@ -18,12 +22,30 @@ type Props = { export const PkiSyncConnectionField = ({ onChange: callback }: Props) => { const { permission } = useProjectPermission(); - const { control, watch } = useFormContext(); - const { currentProject } = useProject(); + const { control, watch, setValue } = useFormContext(); + + const { popUp, handlePopUpToggle, handlePopUpOpen } = usePopUp(["addConnection"] as const); const destination = watch("destination"); const app = PKI_SYNC_CONNECTION_MAP[destination]; + const { currentProject } = useProject(); + + const { + location: { pathname } + } = useRouterState(); + + const getPkiSyncReturnUrl = () => { + if (pathname.includes("selectedTab=secret-syncs")) { + return pathname.replace("selectedTab=secret-syncs", "selectedTab=pki-syncs"); + } + if (!pathname.includes("selectedTab=")) { + const separator = pathname.includes("?") ? "&" : "?"; + return `${pathname}${separator}selectedTab=pki-syncs`; + } + return pathname; + }; + const { data: availableConnections, isPending } = useListAvailableAppConnections( app, currentProject.id @@ -47,6 +69,7 @@ export const PkiSyncConnectionField = ({ onChange: callback }: Props) => { ( { { + if ((newValue as SingleValue<{ id: string; name: string }>)?.id === "_create") { + handlePopUpOpen("addConnection"); + onChange(null); + const formData = { ...watch(), returnUrl: getPkiSyncReturnUrl() }; + localStorage.setItem("pkiSyncFormData", JSON.stringify(formData)); + if (callback) callback(); + return; + } + onChange(newValue); if (callback) callback(); }} isLoading={isPending} - options={availableConnections} + options={[ + ...(canCreateConnection ? [{ id: "_create", name: "Create Connection" }] : []), + ...(availableConnections ?? []) + ]} placeholder="Select connection..." getOptionLabel={(option) => option.name} getOptionValue={(option) => option.id} + components={{ Option: AppConnectionOption }} /> )} control={control} name="connection" /> - {availableConnections?.length === 0 && ( + {!isPending && !availableConnections?.length && !canCreateConnection && (

- {canCreateConnection ? ( - <> - You do not have access to any {appName} Connections. Create one from the{" "} - - App Connections - {" "} - page. - - ) : ( - `You do not have access to any ${appName} Connections. Contact an admin to create one.` - )} + You do not have access to any {appName} Connections. Contact an admin to create one.

)} + { + localStorage.removeItem("pkiSyncFormData"); + handlePopUpToggle("addConnection", isOpen); + }} + projectType={currentProject.type} + projectId={currentProject.id} + app={app} + onComplete={(connection) => { + if (connection) { + setValue("connection", connection); + } + }} + /> ); }; diff --git a/frontend/src/components/pki-syncs/forms/PkiSyncOptionsFields/PkiSyncOptionsFields.tsx b/frontend/src/components/pki-syncs/forms/PkiSyncOptionsFields/PkiSyncOptionsFields.tsx index 82b11fa33..3febcd683 100644 --- a/frontend/src/components/pki-syncs/forms/PkiSyncOptionsFields/PkiSyncOptionsFields.tsx +++ b/frontend/src/components/pki-syncs/forms/PkiSyncOptionsFields/PkiSyncOptionsFields.tsx @@ -71,7 +71,7 @@ export const PkiSyncOptionsFields = ({ destination }: Props) => { isChecked={value} >

- Enable Inactive Certificate Removal{" "} + Enable Removal of Active/Revoked Certificates{" "} { /> )} + {currentDestination === PkiSync.AzureKeyVault && ( + ( + + +

+ Preserve Version on Renewal{" "} + +

+ When enabled, Infisical will create a new version of the existing + certificate in Azure Key Vault during certificate renewal syncs, + preserving the certificate name. +

+

+ This allows consuming services to continue using the same certificate name + while automatically using the latest version without requiring manual + updates. +

+

+ When disabled, new certificates will be created with new names, and old + certificates will be removed. +

+ + } + > + + +

+ + + )} + /> + )} + { + if (!val) return true; + + const allowedOptionalPlaceholders = ["{{environment}}"]; + + 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 name schema must include exactly one {{certificateId}} placeholder. It can also include {{environment}} placeholders. Only alphanumeric characters (a-z, A-Z, 0-9), dashes (-), underscores (_), and slashes (/) are allowed besides the placeholders." + } + ) +}); + +export const AzureKeyVaultPkiSyncDestinationSchema = BasePkiSyncSchema( + AzureKeyVaultSyncOptionsSchema +).merge( z.object({ destination: z.literal(PkiSync.AzureKeyVault), destinationConfig: z.object({ diff --git a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/ExternalCaModal.tsx b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/ExternalCaModal.tsx index 121c32e5f..a1fdcfc1f 100644 --- a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/ExternalCaModal.tsx +++ b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/ExternalCaModal.tsx @@ -124,7 +124,7 @@ type Props = { const caTypes = [ { label: "ACME", value: CaType.ACME }, - { label: "Azure AD Certificate Service", value: CaType.AZURE_AD_CS } + { label: "Active Directory Certificate Services (AD CS)", value: CaType.AZURE_AD_CS } ]; export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => { diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateManagePkiSyncsModal.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateManagePkiSyncsModal.tsx index f366ac4c6..db3e6c197 100644 --- a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateManagePkiSyncsModal.tsx +++ b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateManagePkiSyncsModal.tsx @@ -1,5 +1,6 @@ import { useEffect, useMemo, useState } from "react"; import { faPlus, faSearch } from "@fortawesome/free-solid-svg-icons"; +import { useNavigate } from "@tanstack/react-router"; import { createNotification } from "@app/components/notifications"; import { @@ -18,12 +19,15 @@ import { THead, Tr } from "@app/components/v2"; +import { ROUTE_PATHS } from "@app/const/routes"; import { useProject } from "@app/context"; import { + PkiSync, useAddCertificatesToPkiSync, useListPkiSyncsWithCertificate, useRemoveCertificatesFromPkiSync } from "@app/hooks/api/pkiSyncs"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; type Props = { popUp: { @@ -46,6 +50,7 @@ export const CertificateManagePkiSyncsModal = ({ popUp, handlePopUpToggle }: Pro const [searchTerm, setSearchTerm] = useState(""); const { currentProject } = useProject(); + const navigate = useNavigate(); const { certificateId, commonName } = popUp.data || {}; const { data: pkiSyncs = [], isPending } = useListPkiSyncsWithCertificate( @@ -81,6 +86,32 @@ export const CertificateManagePkiSyncsModal = ({ popUp, handlePopUpToggle }: Pro setCurrentPage(1); }; + const handleNavigateToPkiSyncs = () => { + if (!currentProject?.id) return; + + navigate({ + to: ROUTE_PATHS.CertManager.IntegrationsListPage.path, + params: { + projectId: currentProject.id + }, + search: { + selectedTab: IntegrationsListPageTabs.PkiSyncs + } + }); + handleClose(); + }; + + const getDestinationDisplayName = (destination: string) => { + switch (destination) { + case PkiSync.AzureKeyVault: + return "Azure Key Vault"; + case PkiSync.AwsCertificateManager: + return "AWS Certificate Manager"; + default: + return destination; + } + }; + useEffect(() => { if (!certificateId || !pkiSyncs || pkiSyncs.length === 0) return; @@ -161,7 +192,7 @@ export const CertificateManagePkiSyncsModal = ({ popUp, handlePopUpToggle }: Pro placeholder="Search PKI syncs by name..." /> -
+
{isPending && (
Loading PKI syncs...
@@ -169,12 +200,24 @@ export const CertificateManagePkiSyncsModal = ({ popUp, handlePopUpToggle }: Pro )} {!isPending && pkiSyncs.length === 0 && ( - Create a PKI sync first to manage certificate syncing. +
+ Create a{" "} + {" "} + first to manage certificate syncing. +
)} {!isPending && pkiSyncs.length > 0 && filteredSyncs.length === 0 && searchTerm && ( - No PKI syncs match your search criteria. Try a different search term. +
+ No PKI syncs match your search criteria. Try a different search term. +
)} {!isPending && filteredSyncs.length > 0 && ( @@ -209,9 +252,9 @@ export const CertificateManagePkiSyncsModal = ({ popUp, handlePopUpToggle }: Pro
- {sync.destination.replace(/-/g, " ")} + {getDestinationDisplayName(sync.destination)}
diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTable.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTable.tsx index 2fd23dd40..115526f5a 100644 --- a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTable.tsx +++ b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificatesTable.tsx @@ -478,7 +478,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => { disabled={!isAllowed} icon={} > - PKI Syncs + Manage PKI Syncs )} 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 1c1141382..da0bc180f 100644 --- a/frontend/src/pages/cert-manager/IntegrationsListPage/components/PkiSyncsTab/PkiSyncsTab.tsx +++ b/frontend/src/pages/cert-manager/IntegrationsListPage/components/PkiSyncsTab/PkiSyncsTab.tsx @@ -12,13 +12,14 @@ import { ProjectPermissionSub, useProject } from "@app/context"; import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionContext/types"; import { usePopUp } from "@app/hooks"; import { useListPkiSyncs } from "@app/hooks/api/pkiSyncs"; +import { IntegrationsListPageTabs } from "@app/types/integrations"; import { PkiSyncsTable } from "./PkiSyncTable"; export const PkiSyncsTab = () => { const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["addSync"] as const); - const { addSync, ...search } = useSearch({ + const { addSync, connectionId, connectionName, ...search } = useSearch({ from: ROUTE_PATHS.CertManager.IntegrationsListPage.id }); @@ -45,6 +46,42 @@ export const PkiSyncsTab = () => { navigateToBase(); }, [addSync, handlePopUpOpen, navigateToBase]); + useEffect(() => { + const storedFormData = localStorage.getItem("pkiSyncFormData"); + if (storedFormData && !popUp.addSync.isOpen) { + try { + const parsedData = JSON.parse(storedFormData); + if (connectionId && connectionName) { + const initialData = { + ...parsedData, + connection: { id: connectionId, name: connectionName } + }; + handlePopUpOpen("addSync", { destination: parsedData.destination, initialData }); + navigate({ + to: ROUTE_PATHS.CertManager.IntegrationsListPage.path, + params: { projectId: currentProject?.id }, + search: { selectedTab: IntegrationsListPageTabs.PkiSyncs }, + replace: true + }); + } else { + handlePopUpOpen("addSync", { destination: parsedData.destination }); + } + localStorage.removeItem("pkiSyncFormData"); + } catch (error) { + console.error("Failed to parse stored PKI sync form data:", error); + localStorage.removeItem("pkiSyncFormData"); + handlePopUpOpen("addSync"); + } + } + }, [ + handlePopUpOpen, + popUp.addSync.isOpen, + connectionId, + connectionName, + navigate, + currentProject?.id + ]); + const { data: pkiSyncs = [], isPending: isPkiSyncsPending } = useListPkiSyncs( currentProject?.id || "", { @@ -94,7 +131,8 @@ export const PkiSyncsTab = () => {
handlePopUpToggle("addSync", isOpen)} /> diff --git a/frontend/src/pages/cert-manager/IntegrationsListPage/route.tsx b/frontend/src/pages/cert-manager/IntegrationsListPage/route.tsx index 0a0868a07..a996a23b5 100644 --- a/frontend/src/pages/cert-manager/IntegrationsListPage/route.tsx +++ b/frontend/src/pages/cert-manager/IntegrationsListPage/route.tsx @@ -9,7 +9,9 @@ import { IntegrationsListPage } from "./IntegrationsListPage"; const IntegrationsListPageQuerySchema = z.object({ selectedTab: z.nativeEnum(IntegrationsListPageTabs).optional(), - addSync: z.nativeEnum(PkiSync).optional() + addSync: z.nativeEnum(PkiSync).optional(), + connectionId: z.string().optional(), + connectionName: z.string().optional() }); export const Route = createFileRoute( diff --git a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx index ea82fd843..cbd6f9ada 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx +++ b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx @@ -418,7 +418,7 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" } name="enrollmentType" render={({ field: { onChange, ...field }, fieldState: { error } }) => ( { connectionName: data.connection.name, ...(data.returnUrl.includes("integrations") ? { - selectedTab: IntegrationsListPageTabs.SecretSyncs + selectedTab: localStorage.getItem("pkiSyncFormData") + ? IntegrationsListPageTabs.PkiSyncs + : IntegrationsListPageTabs.SecretSyncs } : {}) }