From 3587fbf98bf6c105d13d05bc3f8f1327094137f3 Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Mon, 22 Sep 2025 11:45:45 -0300 Subject: [PATCH] Improvements to cert-syncs --- .../v1/pki-sync-routers/pki-sync-endpoints.ts | 2 +- .../acme/acme-certificate-authority-fns.ts | 12 ++- .../azure-ad-cs-certificate-authority-fns.ts | 12 ++- .../certificate-authority-queue.ts | 10 +-- .../certificate-authority-service.ts | 10 +-- .../internal-certificate-authority-fns.ts | 12 ++- .../certificate/certificate-service.ts | 2 +- .../pki-subscriber/pki-subscriber-service.ts | 2 +- .../azure-key-vault-pki-sync-constants.ts | 52 +++++++++++ .../azure-key-vault-pki-sync-fns.ts | 15 ---- .../azure-key-vault-pki-sync-schemas.ts | 10 ++- .../pki-sync/azure-key-vault/index.ts | 1 + backend/src/services/pki-sync/pki-sync-fns.ts | 37 +------- .../src/services/pki-sync/pki-sync-schemas.ts | 8 +- .../src/services/pki-sync/pki-sync-service.ts | 88 +++++-------------- .../src/services/pki-sync/pki-sync-utils.ts | 27 ++++++ .../azure-key-vault/remove-certificates.mdx | 4 +- .../endpoints/pki/syncs/options.mdx | 2 +- docs/docs.json | 4 +- .../pki/certificate-syncs/overview.mdx | 5 +- .../pki-syncs/forms/CreatePkiSyncForm.tsx | 9 +- .../PkiSyncOptionsFields.tsx | 15 +--- frontend/src/hooks/api/pkiSyncs/mutations.tsx | 4 +- .../GeneralPermissionConditions.tsx | 3 +- .../PkiSyncPermissionConditions.tsx | 19 ++++ .../components/RolePermissionsSection.tsx | 5 ++ 26 files changed, 180 insertions(+), 190 deletions(-) create mode 100644 backend/src/services/pki-sync/azure-key-vault/azure-key-vault-pki-sync-constants.ts create mode 100644 backend/src/services/pki-sync/pki-sync-utils.ts create mode 100644 frontend/src/pages/project/RoleDetailsBySlugPage/components/PkiSyncPermissionConditions.tsx diff --git a/backend/src/server/routes/v1/pki-sync-routers/pki-sync-endpoints.ts b/backend/src/server/routes/v1/pki-sync-routers/pki-sync-endpoints.ts index e70080e53..3f803a1e0 100644 --- a/backend/src/server/routes/v1/pki-sync-routers/pki-sync-endpoints.ts +++ b/backend/src/server/routes/v1/pki-sync-routers/pki-sync-endpoints.ts @@ -309,7 +309,7 @@ export const registerSyncPkiEndpoints = ({ server.route({ method: "POST", - url: "/:pkiSyncId/remove", + url: "/:pkiSyncId/remove-certificates", config: { rateLimit: writeLimit }, diff --git a/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts b/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts index c0a2fdf86..521092497 100644 --- a/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts +++ b/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts @@ -23,7 +23,9 @@ import { } from "@app/services/certificate/certificate-types"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TPkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal"; -import { triggerAutoSyncForSubscriber } from "@app/services/pki-sync/pki-sync-fns"; +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 { TProjectDALFactory } from "@app/services/project/project-dal"; import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns"; @@ -57,12 +59,8 @@ type TAcmeCertificateAuthorityFnsDeps = { "encryptWithKmsKey" | "generateKmsKey" | "createCipherPairWithDataKey" | "decryptWithKmsKey" >; pkiSubscriberDAL: Pick; - pkiSyncDAL: { - find: (filter: { subscriberId: string; isAutoSyncEnabled: boolean }) => Promise>; - }; - pkiSyncQueue: { - queuePkiSyncSyncCertificatesById: (params: { syncId: string }) => Promise; - }; + pkiSyncDAL: Pick; + pkiSyncQueue: Pick; projectDAL: Pick; }; 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 00a4fbf3e..69feed40e 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 @@ -26,7 +26,9 @@ import { import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TPkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal"; import { TPkiSubscriberProperties } from "@app/services/pki-subscriber/pki-subscriber-types"; -import { triggerAutoSyncForSubscriber } from "@app/services/pki-sync/pki-sync-fns"; +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 { TProjectDALFactory } from "@app/services/project/project-dal"; import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns"; @@ -56,12 +58,8 @@ type TAzureAdCsCertificateAuthorityFnsDeps = { "encryptWithKmsKey" | "generateKmsKey" | "createCipherPairWithDataKey" | "decryptWithKmsKey" >; pkiSubscriberDAL: Pick; - pkiSyncDAL: { - find: (filter: { subscriberId: string; isAutoSyncEnabled: boolean }) => Promise>; - }; - pkiSyncQueue: { - queuePkiSyncSyncCertificatesById: (params: { syncId: string }) => Promise; - }; + pkiSyncDAL: Pick; + pkiSyncQueue: Pick; projectDAL: Pick; }; diff --git a/backend/src/services/certificate-authority/certificate-authority-queue.ts b/backend/src/services/certificate-authority/certificate-authority-queue.ts index 6efd995a7..81aab337d 100644 --- a/backend/src/services/certificate-authority/certificate-authority-queue.ts +++ b/backend/src/services/certificate-authority/certificate-authority-queue.ts @@ -20,6 +20,8 @@ import { TCertificateBodyDALFactory } from "../certificate/certificate-body-dal" import { TCertificateSecretDALFactory } from "../certificate/certificate-secret-dal"; import { TPkiSubscriberDALFactory } from "../pki-subscriber/pki-subscriber-dal"; import { SubscriberOperationStatus } from "../pki-subscriber/pki-subscriber-types"; +import { TPkiSyncDALFactory } from "../pki-sync/pki-sync-dal"; +import { TPkiSyncQueueFactory } from "../pki-sync/pki-sync-queue"; import { AcmeCertificateAuthorityFns } from "./acme/acme-certificate-authority-fns"; import { AzureAdCsCertificateAuthorityFns } from "./azure-ad-cs/azure-ad-cs-certificate-authority-fns"; import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal"; @@ -50,12 +52,8 @@ type TCertificateAuthorityQueueFactoryDep = { certificateSecretDAL: Pick; queueService: TQueueServiceFactory; pkiSubscriberDAL: Pick; - pkiSyncDAL: { - find: (filter: { subscriberId: string; isAutoSyncEnabled: boolean }) => Promise>; - }; - pkiSyncQueue: { - queuePkiSyncSyncCertificatesById: (params: { syncId: string }) => Promise; - }; + pkiSyncDAL: Pick; + pkiSyncQueue: Pick; }; export type TCertificateAuthorityQueueFactory = ReturnType; diff --git a/backend/src/services/certificate-authority/certificate-authority-service.ts b/backend/src/services/certificate-authority/certificate-authority-service.ts index 197143d73..ed27f7571 100644 --- a/backend/src/services/certificate-authority/certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/certificate-authority-service.ts @@ -13,6 +13,8 @@ import { TCertificateDALFactory } from "../certificate/certificate-dal"; import { TCertificateSecretDALFactory } from "../certificate/certificate-secret-dal"; import { TKmsServiceFactory } from "../kms/kms-service"; import { TPkiSubscriberDALFactory } from "../pki-subscriber/pki-subscriber-dal"; +import { TPkiSyncDALFactory } from "../pki-sync/pki-sync-dal"; +import { TPkiSyncQueueFactory } from "../pki-sync/pki-sync-queue"; import { TProjectDALFactory } from "../project/project-dal"; import { AcmeCertificateAuthorityFns, @@ -68,12 +70,8 @@ type TCertificateAuthorityServiceFactoryDep = { "encryptWithKmsKey" | "generateKmsKey" | "createCipherPairWithDataKey" | "decryptWithKmsKey" >; pkiSubscriberDAL: Pick; - pkiSyncDAL: { - find: (filter: { subscriberId: string; isAutoSyncEnabled: boolean }) => Promise>; - }; - pkiSyncQueue: { - queuePkiSyncSyncCertificatesById: (params: { syncId: string }) => Promise; - }; + pkiSyncDAL: Pick; + pkiSyncQueue: Pick; }; export type TCertificateAuthorityServiceFactory = ReturnType; diff --git a/backend/src/services/certificate-authority/internal/internal-certificate-authority-fns.ts b/backend/src/services/certificate-authority/internal/internal-certificate-authority-fns.ts index 8dc17b687..f1dc372b1 100644 --- a/backend/src/services/certificate-authority/internal/internal-certificate-authority-fns.ts +++ b/backend/src/services/certificate-authority/internal/internal-certificate-authority-fns.ts @@ -19,7 +19,9 @@ import { TAltNameMapping } from "@app/services/certificate/certificate-types"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; -import { triggerAutoSyncForSubscriber } from "@app/services/pki-sync/pki-sync-fns"; +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 { TProjectDALFactory } from "@app/services/project/project-dal"; import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns"; @@ -52,12 +54,8 @@ type TInternalCertificateAuthorityFnsDeps = { certificateDAL: Pick; certificateBodyDAL: Pick; certificateSecretDAL: Pick; - pkiSyncDAL: { - find: (filter: { subscriberId: string; isAutoSyncEnabled: boolean }) => Promise>; - }; - pkiSyncQueue: { - queuePkiSyncSyncCertificatesById: (params: { syncId: string }) => Promise; - }; + pkiSyncDAL: Pick; + pkiSyncQueue: Pick; }; export const InternalCertificateAuthorityFns = ({ diff --git a/backend/src/services/certificate/certificate-service.ts b/backend/src/services/certificate/certificate-service.ts index c79ffd8f8..c9da3f946 100644 --- a/backend/src/services/certificate/certificate-service.ts +++ b/backend/src/services/certificate/certificate-service.ts @@ -22,7 +22,7 @@ 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-fns"; +import { triggerAutoSyncForSubscriber } from "@app/services/pki-sync/pki-sync-utils"; import { TPkiSyncQueueFactory } from "@app/services/pki-sync/pki-sync-queue"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns"; diff --git a/backend/src/services/pki-subscriber/pki-subscriber-service.ts b/backend/src/services/pki-subscriber/pki-subscriber-service.ts index 3fc5e841a..4e00acec6 100644 --- a/backend/src/services/pki-subscriber/pki-subscriber-service.ts +++ b/backend/src/services/pki-subscriber/pki-subscriber-service.ts @@ -38,7 +38,7 @@ import { TCertificateAuthoritySecretDALFactory } from "@app/services/certificate import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TPkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal"; import { TPkiSyncDALFactory } from "@app/services/pki-sync/pki-sync-dal"; -import { triggerAutoSyncForSubscriber } from "@app/services/pki-sync/pki-sync-fns"; +import { triggerAutoSyncForSubscriber } from "@app/services/pki-sync/pki-sync-utils"; import { TPkiSyncQueueFactory } from "@app/services/pki-sync/pki-sync-queue"; 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 new file mode 100644 index 000000000..3c64bc86d --- /dev/null +++ b/backend/src/services/pki-sync/azure-key-vault/azure-key-vault-pki-sync-constants.ts @@ -0,0 +1,52 @@ +import RE2 from "re2"; + +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { PkiSync } from "@app/services/pki-sync/pki-sync-enums"; + +/** + * Azure Key Vault naming constraints for certificates + */ +export const AZURE_KEY_VAULT_CERTIFICATE_NAMING = { + /** + * Regular expression pattern for valid Azure Key Vault certificate names + * Must contain only alphanumeric characters and hyphens (a-z, A-Z, 0-9, -) + * Must be 1-127 characters long + */ + NAME_PATTERN: new RE2("^[a-zA-Z0-9-]{1,127}$"), + + /** + * String of characters that are forbidden in Azure Key Vault certificate names + */ + FORBIDDEN_CHARACTERS: "!@#$%^&*()+=[]{}|\\:;\"'<>,.?/~` _", + + /** + * Maximum length for certificate names in Azure Key Vault + */ + MAX_NAME_LENGTH: 127, + + /** + * Minimum length for certificate names in Azure Key Vault + */ + MIN_NAME_LENGTH: 1, + + /** + * String representation of the allowed character pattern (for UI display) + */ + ALLOWED_CHARACTER_PATTERN: "^[a-zA-Z0-9-]{1,127}$" +} as const; + +/** + * Azure Key Vault PKI Sync list option configuration + */ +export const AZURE_KEY_VAULT_PKI_SYNC_LIST_OPTION = { + name: "Azure Key Vault" as const, + connection: AppConnection.AzureKeyVault, + destination: PkiSync.AzureKeyVault, + canImportCertificates: 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, + maxCertificateNameLength: AZURE_KEY_VAULT_CERTIFICATE_NAMING.MAX_NAME_LENGTH, + minCertificateNameLength: AZURE_KEY_VAULT_CERTIFICATE_NAMING.MIN_NAME_LENGTH +} as const; 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 a2da09924..2b9521bb0 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 @@ -5,14 +5,12 @@ import * as crypto from "crypto"; import { request } from "@app/lib/config/request"; import { logger } from "@app/lib/logger"; import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; -import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { getAzureConnectionAccessToken } from "@app/services/app-connection/azure-key-vault"; import { createConnectionQueue, RateLimitConfig } from "@app/services/connection-queue"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { matchesCertificateNameSchema } from "@app/services/pki-sync/pki-sync-fns"; import { TCertificateMap } from "@app/services/pki-sync/pki-sync-types"; -import { PkiSync } from "../pki-sync-enums"; import { PkiSyncError } from "../pki-sync-errors"; import { TPkiSyncWithCredentials } from "../pki-sync-types"; import { GetAzureKeyVaultCertificate, TAzureKeyVaultPkiSyncConfig } from "./azure-key-vault-pki-sync-types"; @@ -45,19 +43,6 @@ const isInfisicalManagedCertificate = (certificateName: string, pkiSync: TPkiSyn return certificateName.startsWith("Infisical-PKI-Sync-"); }; -export const AZURE_KEY_VAULT_PKI_SYNC_LIST_OPTION = { - name: "Azure Key Vault" as const, - connection: AppConnection.AzureKeyVault, - destination: PkiSync.AzureKeyVault, - canImportCertificates: false, - canRemoveCertificates: true, - defaultCertificateNameSchema: "Infisical-PKI-Sync-{{certificateId}}", - forbiddenCharacters: "!@#$%^&*()+=[]{}|\\:;\"'<>,.?/~` _", - allowedCharacterPattern: "^[a-zA-Z0-9-]{1,127}$", - maxCertificateNameLength: 127, - minCertificateNameLength: 1 -}; - type TAzureKeyVaultPkiSyncFactoryDeps = { appConnectionDAL: Pick; kmsService: Pick; 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 2701556e7..b27cc2f42 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 @@ -5,6 +5,8 @@ import { AppConnection } from "@app/services/app-connection/app-connection-enums import { PkiSync } from "@app/services/pki-sync/pki-sync-enums"; import { PkiSyncSchema } from "@app/services/pki-sync/pki-sync-schemas"; +import { AZURE_KEY_VAULT_CERTIFICATE_NAMING } from "./azure-key-vault-pki-sync-constants"; + export const AzureKeyVaultPkiSyncConfigSchema = z.object({ vaultBaseUrl: z.string().url() }); @@ -22,12 +24,12 @@ const AzureKeyVaultPkiSyncOptionsSchema = z.object({ const testName = schema .replace(new RE2("\\{\\{certificateId\\}\\}", "g"), "") .replace(new RE2("\\{\\{environment\\}\\}", "g"), ""); - const azureNamePattern = new RE2("^[a-zA-Z0-9-]{1,127}$"); - const forbiddenChars = "!@#$%^&*()+=[]{}|\\:;\"'<>,.?/~` _"; - const hasForbiddenChars = forbiddenChars.split("").some((char) => testName.includes(char)); + const hasForbiddenChars = AZURE_KEY_VAULT_CERTIFICATE_NAMING.FORBIDDEN_CHARACTERS.split("").some((char) => + testName.includes(char) + ); - return azureNamePattern.test(testName) && !hasForbiddenChars; + return AZURE_KEY_VAULT_CERTIFICATE_NAMING.NAME_PATTERN.test(testName) && !hasForbiddenChars; }, { message: diff --git a/backend/src/services/pki-sync/azure-key-vault/index.ts b/backend/src/services/pki-sync/azure-key-vault/index.ts index 463a3a5d8..cc9c64f9a 100644 --- a/backend/src/services/pki-sync/azure-key-vault/index.ts +++ b/backend/src/services/pki-sync/azure-key-vault/index.ts @@ -1,3 +1,4 @@ +export * from "./azure-key-vault-pki-sync-constants"; export * from "./azure-key-vault-pki-sync-fns"; export * from "./azure-key-vault-pki-sync-schemas"; export * from "./azure-key-vault-pki-sync-types"; diff --git a/backend/src/services/pki-sync/pki-sync-fns.ts b/backend/src/services/pki-sync/pki-sync-fns.ts index 24f3039ca..343afa626 100644 --- a/backend/src/services/pki-sync/pki-sync-fns.ts +++ b/backend/src/services/pki-sync/pki-sync-fns.ts @@ -3,14 +3,11 @@ import { z, ZodSchema } from "zod"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { BadRequestError } from "@app/lib/errors"; -import { logger } from "@app/lib/logger"; import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; -import { - AZURE_KEY_VAULT_PKI_SYNC_LIST_OPTION, - azureKeyVaultPkiSyncFactory -} from "./azure-key-vault/azure-key-vault-pki-sync-fns"; +import { AZURE_KEY_VAULT_PKI_SYNC_LIST_OPTION } from "./azure-key-vault/azure-key-vault-pki-sync-constants"; +import { azureKeyVaultPkiSyncFactory } from "./azure-key-vault/azure-key-vault-pki-sync-fns"; import { PkiSync } from "./pki-sync-enums"; import { TCertificateMap, TPkiSyncWithCredentials } from "./pki-sync-types"; @@ -151,36 +148,6 @@ const isAzureKeyVaultPkiSync = (pkiSync: TPkiSyncWithCredentials): boolean => { return pkiSync.destination === PkiSync.AzureKeyVault; }; -/** - * Trigger auto sync for PKI syncs connected to a PKI subscriber when certificates are issued/revoked/deleted - */ -export const triggerAutoSyncForSubscriber = async ( - subscriberId: string, - dependencies: { - pkiSyncDAL: { - find: (filter: { subscriberId: string; isAutoSyncEnabled: boolean }) => Promise>; - }; - pkiSyncQueue: { - queuePkiSyncSyncCertificatesById: (params: { syncId: string }) => Promise; - }; - } -) => { - try { - const pkiSyncs = await dependencies.pkiSyncDAL.find({ - subscriberId, - isAutoSyncEnabled: true - }); - - // Queue sync jobs for each auto sync enabled PKI sync - const syncPromises = pkiSyncs.map((pkiSync) => - dependencies.pkiSyncQueue.queuePkiSyncSyncCertificatesById({ syncId: pkiSync.id }) - ); - await Promise.all(syncPromises); - } catch (error) { - logger.error(error, `Failed to trigger auto sync for subscriber ${subscriberId}:`); - } -}; - export const PkiSyncFns = { getCertificates: async ( pkiSync: TPkiSyncWithCredentials, diff --git a/backend/src/services/pki-sync/pki-sync-schemas.ts b/backend/src/services/pki-sync/pki-sync-schemas.ts index 339b1eba1..95023002e 100644 --- a/backend/src/services/pki-sync/pki-sync-schemas.ts +++ b/backend/src/services/pki-sync/pki-sync-schemas.ts @@ -14,10 +14,8 @@ export const PkiSyncOptionsSchema = z.object({ (val) => { if (!val) return true; - const allowedOptionalPlaceholders = ["{{environment}}"]; - - const allowedPlaceholdersRegexPart = ["{{certificateId}}", ...allowedOptionalPlaceholders] - .map((p) => p.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&")) // Escape regex special characters + const allowedPlaceholdersRegexPart = ["{{certificateId}}"] + .map((p) => p.replace(new RE2(/[-/\\^$*+?.()|[\]{}]/g), "\\$&")) // Escape regex special characters .join("|"); const allowedContentRegex = new RE2(`^([a-zA-Z0-9_\\-/]|${allowedPlaceholdersRegexPart})*$`); @@ -33,7 +31,7 @@ export const PkiSyncOptionsSchema = z.object({ }, { 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." + "Certificate name schema must include exactly one {{certificateId}} placeholder. Only alphanumeric characters (a-z, A-Z, 0-9), dashes (-), underscores (_), and slashes (/) are allowed besides the placeholders." } ) }); diff --git a/backend/src/services/pki-sync/pki-sync-service.ts b/backend/src/services/pki-sync/pki-sync-service.ts index f0040a98d..d2a1607eb 100644 --- a/backend/src/services/pki-sync/pki-sync-service.ts +++ b/backend/src/services/pki-sync/pki-sync-service.ts @@ -153,8 +153,8 @@ export const pkiSyncServiceFactory = ({ }: Omit, actor: OrgServiceActor ): Promise => { - const existingSync = await pkiSyncDAL.findById(id); - if (!existingSync) throw new NotFoundError({ message: "PKI sync not found" }); + const pkiSync = await pkiSyncDAL.findById(id); + if (!pkiSync) throw new NotFoundError({ message: "PKI sync not found" }); const { permission } = await permissionService.getProjectPermission({ actor: actor.type, @@ -162,12 +162,9 @@ export const pkiSyncServiceFactory = ({ actorAuthMethod: actor.authMethod, actorOrgId: actor.orgId, actionProjectType: ActionProjectType.CertificateManager, - projectId: existingSync.projectId + projectId: pkiSync.projectId }); - const pkiSync = await pkiSyncDAL.findByIdAndProjectId(id, existingSync.projectId); - if (!pkiSync) throw new NotFoundError({ message: "PKI sync not found" }); - let currentSubscriber; if (pkiSync.subscriberId) { currentSubscriber = await pkiSubscriberDAL.findById(pkiSync.subscriberId); @@ -181,7 +178,7 @@ export const pkiSyncServiceFactory = ({ ); if (name && name !== pkiSync.name) { - const existingPkiSync = await pkiSyncDAL.findByNameAndProjectId(name, existingSync.projectId); + const existingPkiSync = await pkiSyncDAL.findByNameAndProjectId(name, pkiSync.projectId); if (existingPkiSync) { throw new BadRequestError({ message: "PKI sync with this name already exists" }); } @@ -189,7 +186,7 @@ export const pkiSyncServiceFactory = ({ if (subscriberId) { const subscriber = await pkiSubscriberDAL.findById(subscriberId); - if (!subscriber || subscriber.projectId !== existingSync.projectId) { + if (!subscriber || subscriber.projectId !== pkiSync.projectId) { throw new NotFoundError({ message: "PKI subscriber not found" }); } } @@ -209,9 +206,9 @@ export const pkiSyncServiceFactory = ({ }); } - if (syncOptions.canRemoveCertificates === false && providerCapabilities.canRemoveCertificates) { + if (syncOptions.canRemoveCertificates && !providerCapabilities.canRemoveCertificates) { throw new BadRequestError({ - message: `Certificate removal cannot be disabled for ${PKI_SYNC_NAME_MAP[pkiSync.destination]} PKI sync destination` + message: `Certificate removal cannot be enabled for ${PKI_SYNC_NAME_MAP[pkiSync.destination]} PKI sync destination` }); } @@ -237,9 +234,9 @@ export const pkiSyncServiceFactory = ({ const deletePkiSync = async ( { id }: Omit, actor: OrgServiceActor - ): Promise => { - const existingSync = await pkiSyncDAL.findById(id); - if (!existingSync) throw new NotFoundError({ message: "PKI sync not found" }); + ) => { + const pkiSync = await pkiSyncDAL.findById(id); + if (!pkiSync) throw new NotFoundError({ message: "PKI sync not found" }); const { permission } = await permissionService.getProjectPermission({ actor: actor.type, @@ -247,12 +244,9 @@ export const pkiSyncServiceFactory = ({ actorAuthMethod: actor.authMethod, actorOrgId: actor.orgId, actionProjectType: ActionProjectType.CertificateManager, - projectId: existingSync.projectId + projectId: pkiSync.projectId }); - const pkiSync = await pkiSyncDAL.findByIdAndProjectId(id, existingSync.projectId); - if (!pkiSync) throw new NotFoundError({ message: "PKI sync not found" }); - let pkiSyncSubscriber; if (pkiSync.subscriberId) { pkiSyncSubscriber = await pkiSubscriberDAL.findById(pkiSync.subscriberId); @@ -265,31 +259,7 @@ export const pkiSyncServiceFactory = ({ : ProjectPermissionSub.PkiSyncs ); - await pkiSyncDAL.deleteById(id); - return { - ...pkiSync, - description: pkiSync.description || undefined, - subscriberId: pkiSync.subscriberId || undefined, - syncStatus: pkiSync.syncStatus || undefined, - lastSyncedAt: pkiSync.lastSyncedAt || undefined, - lastSyncJobId: pkiSync.lastSyncJobId || undefined, - lastSyncMessage: pkiSync.lastSyncMessage || undefined, - importStatus: pkiSync.importStatus || undefined, - lastImportJobId: pkiSync.lastImportJobId || undefined, - lastImportMessage: pkiSync.lastImportMessage || undefined, - lastImportedAt: pkiSync.lastImportedAt || undefined, - removeStatus: pkiSync.removeStatus || undefined, - lastRemoveJobId: pkiSync.lastRemoveJobId || undefined, - lastRemoveMessage: pkiSync.lastRemoveMessage || undefined, - lastRemovedAt: pkiSync.lastRemovedAt || undefined, - connection: { - ...pkiSync.connection, - description: pkiSync.connection.description || undefined, - gatewayId: pkiSync.connection.gatewayId || undefined, - projectId: pkiSync.connection.projectId || undefined, - isPlatformManagedCredentials: pkiSync.connection.isPlatformManagedCredentials || undefined - } - }; + return pkiSyncDAL.deleteById(id); }; const listPkiSyncsByProjectId = async ({ projectId }: TListPkiSyncsByProjectId, actor: OrgServiceActor) => { @@ -310,15 +280,10 @@ export const pkiSyncServiceFactory = ({ const findPkiSyncById = async ({ id, projectId }: TFindPkiSyncByIdDTO, actor: OrgServiceActor) => { const pkiSync = await pkiSyncDAL.findById(id); - if (!pkiSync) + if (!pkiSync || (projectId && pkiSync.projectId !== projectId)) { throw new NotFoundError({ message: `Could not find PKI Sync with ID "${id}"` }); - - if (projectId && pkiSync.projectId !== projectId) { - throw new NotFoundError({ - message: `Could not find PKI Sync with ID "${id}" in project "${projectId}"` - }); } const { permission } = await permissionService.getProjectPermission({ @@ -349,8 +314,8 @@ export const pkiSyncServiceFactory = ({ { id }: Omit, actor: OrgServiceActor ) => { - const existingSync = await pkiSyncDAL.findById(id); - if (!existingSync) throw new NotFoundError({ message: "PKI sync not found" }); + const pkiSync = await pkiSyncDAL.findById(id); + if (!pkiSync) throw new NotFoundError({ message: "PKI sync not found" }); const { permission } = await permissionService.getProjectPermission({ actor: actor.type, @@ -358,12 +323,9 @@ export const pkiSyncServiceFactory = ({ actorAuthMethod: actor.authMethod, actorOrgId: actor.orgId, actionProjectType: ActionProjectType.CertificateManager, - projectId: existingSync.projectId + projectId: pkiSync.projectId }); - const pkiSync = await pkiSyncDAL.findByIdAndProjectId(id, existingSync.projectId); - if (!pkiSync) throw new NotFoundError({ message: "PKI sync not found" }); - let syncSubscriber; if (pkiSync.subscriberId) { syncSubscriber = await pkiSubscriberDAL.findById(pkiSync.subscriberId); @@ -385,8 +347,8 @@ export const pkiSyncServiceFactory = ({ { id }: Omit, actor: OrgServiceActor ) => { - const existingSync = await pkiSyncDAL.findById(id); - if (!existingSync) throw new NotFoundError({ message: "PKI sync not found" }); + const pkiSync = await pkiSyncDAL.findById(id); + if (!pkiSync) throw new NotFoundError({ message: "PKI sync not found" }); const { permission } = await permissionService.getProjectPermission({ actor: actor.type, @@ -394,12 +356,9 @@ export const pkiSyncServiceFactory = ({ actorAuthMethod: actor.authMethod, actorOrgId: actor.orgId, actionProjectType: ActionProjectType.CertificateManager, - projectId: existingSync.projectId + projectId: pkiSync.projectId }); - const pkiSync = await pkiSyncDAL.findByIdAndProjectId(id, existingSync.projectId); - if (!pkiSync) throw new NotFoundError({ message: "PKI sync not found" }); - // Check if the PKI sync destination supports importing certificates const syncOptions = listPkiSyncOptions().find((option) => option.destination === pkiSync.destination); if (!syncOptions?.canImportCertificates) { @@ -429,8 +388,8 @@ export const pkiSyncServiceFactory = ({ { id }: Omit, actor: OrgServiceActor ) => { - const existingSync = await pkiSyncDAL.findById(id); - if (!existingSync) throw new NotFoundError({ message: "PKI sync not found" }); + const pkiSync = await pkiSyncDAL.findById(id); + if (!pkiSync) throw new NotFoundError({ message: "PKI sync not found" }); const { permission } = await permissionService.getProjectPermission({ actor: actor.type, @@ -438,12 +397,9 @@ export const pkiSyncServiceFactory = ({ actorAuthMethod: actor.authMethod, actorOrgId: actor.orgId, actionProjectType: ActionProjectType.CertificateManager, - projectId: existingSync.projectId + projectId: pkiSync.projectId }); - const pkiSync = await pkiSyncDAL.findByIdAndProjectId(id, existingSync.projectId); - if (!pkiSync) throw new NotFoundError({ message: "PKI sync not found" }); - let removeSubscriber; if (pkiSync.subscriberId) { removeSubscriber = await pkiSubscriberDAL.findById(pkiSync.subscriberId); diff --git a/backend/src/services/pki-sync/pki-sync-utils.ts b/backend/src/services/pki-sync/pki-sync-utils.ts new file mode 100644 index 000000000..a81864a17 --- /dev/null +++ b/backend/src/services/pki-sync/pki-sync-utils.ts @@ -0,0 +1,27 @@ +import { logger } from "@app/lib/logger"; + +import { TPkiSyncDALFactory } from "./pki-sync-dal"; +import { TPkiSyncQueueFactory } from "./pki-sync-queue"; + +export const triggerAutoSyncForSubscriber = async ( + subscriberId: string, + dependencies: { + pkiSyncDAL: Pick; + pkiSyncQueue: Pick; + } +) => { + try { + const pkiSyncs = await dependencies.pkiSyncDAL.find({ + subscriberId, + isAutoSyncEnabled: true + }); + + // Queue sync jobs for each auto sync enabled PKI sync + const syncPromises = pkiSyncs.map((pkiSync) => + dependencies.pkiSyncQueue.queuePkiSyncSyncCertificatesById({ syncId: pkiSync.id }) + ); + await Promise.all(syncPromises); + } catch (error) { + logger.error(error, `Failed to trigger auto sync for subscriber ${subscriberId}:`); + } +}; diff --git a/docs/api-reference/endpoints/pki/syncs/azure-key-vault/remove-certificates.mdx b/docs/api-reference/endpoints/pki/syncs/azure-key-vault/remove-certificates.mdx index 0c73dba63..ebd9bff6b 100644 --- a/docs/api-reference/endpoints/pki/syncs/azure-key-vault/remove-certificates.mdx +++ b/docs/api-reference/endpoints/pki/syncs/azure-key-vault/remove-certificates.mdx @@ -1,6 +1,6 @@ --- title: "Remove Certificates from Azure Key Vault" -openapi: "POST /api/v1/pki/syncs/azure-key-vault/{pkiSyncId}/remove" +openapi: "POST /api/v1/pki/syncs/azure-key-vault/{pkiSyncId}/remove-certificates" --- @@ -29,7 +29,7 @@ This endpoint removes certificates from the specified Azure Key Vault that are n ```bash cURL -curl -X POST "https://app.infisical.com/api/v1/pki/syncs/azure-key-vault/ps_12345/remove" \ +curl -X POST "https://app.infisical.com/api/v1/pki/syncs/azure-key-vault/ps_12345/remove-certificates" \ -H "Authorization: Bearer " ``` diff --git a/docs/api-reference/endpoints/pki/syncs/options.mdx b/docs/api-reference/endpoints/pki/syncs/options.mdx index e9bf0e57e..6b7d3e2dd 100644 --- a/docs/api-reference/endpoints/pki/syncs/options.mdx +++ b/docs/api-reference/endpoints/pki/syncs/options.mdx @@ -10,7 +10,7 @@ This endpoint lists all available PKI sync destination options and their capabil ## Request - Project ID (for authorization purposes, but the options are global) + Project ID ## Response diff --git a/docs/docs.json b/docs/docs.json index b1af69970..b61099f9f 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -2646,9 +2646,9 @@ "href": "https://infisical.com" }, "api": { - "openapi": "https://api.infisical.com/api/docs/json", + "openapi": "https://app.infisical.com/api/docs/json", "mdx": { - "server": ["https://api.infisical.com"] + "server": ["https://app.infisical.com"] } }, "appearance": { diff --git a/docs/documentation/platform/pki/certificate-syncs/overview.mdx b/docs/documentation/platform/pki/certificate-syncs/overview.mdx index 085729416..db4844a92 100644 --- a/docs/documentation/platform/pki/certificate-syncs/overview.mdx +++ b/docs/documentation/platform/pki/certificate-syncs/overview.mdx @@ -80,8 +80,7 @@ via the UI or API for the third-party service you intend to sync certificates to - Certificate naming schema to control how certificate names are generated in the destination - Certificate Syncs manage certificates that are prefixed with "Infisical-" in the destination. Only - certificates managed by Infisical will be affected during sync operations. Certificates not created or + Only certificates managed by Infisical will be affected during sync operations. Certificates not created or managed by Infisical will remain untouched, and changes made to Infisical-managed certificates directly in the destination service may be overwritten by future syncs. @@ -110,11 +109,9 @@ By default, certificates are named using the pattern `Infisical-{certificateId}` You can customize certificate naming by providing a **Certificate Name Schema** when creating or updating a Certificate Sync. The schema supports the following placeholders: - `{{certificateId}}` - The unique certificate identifier (required) -- `{{environment}}` - The environment context (always "global" for PKI syncs) **Examples:** - `myapp-{{certificateId}}` → `myapp-abc123def456` -- `{{environment}}-cert-{{certificateId}}` → `global-cert-abc123def456` - `ssl/{{certificateId}}` → `ssl/abc123def456` **Rules:** diff --git a/frontend/src/components/pki-syncs/forms/CreatePkiSyncForm.tsx b/frontend/src/components/pki-syncs/forms/CreatePkiSyncForm.tsx index 2fb7913bc..1688bf3a7 100644 --- a/frontend/src/components/pki-syncs/forms/CreatePkiSyncForm.tsx +++ b/frontend/src/components/pki-syncs/forms/CreatePkiSyncForm.tsx @@ -130,11 +130,10 @@ export const CreatePkiSyncForm = ({ destination, onComplete, onCancel }: Props) Certificate Sync Behavior

- Certificate Syncs manage certificates that are prefixed with "Infisical-" in - the destination. Only certificates managed by Infisical will be affected during sync - operations. Certificates not created or managed by Infisical will remain untouched, and - changes made to Infisical-managed certificates directly in the destination service may - be overwritten by future syncs. + Only certificates managed by Infisical will be affected during sync operations. + Certificates not created or managed by Infisical will remain untouched, and changes made + to Infisical-managed certificates directly in the destination service may be overwritten + by future syncs.

diff --git a/frontend/src/components/pki-syncs/forms/PkiSyncOptionsFields/PkiSyncOptionsFields.tsx b/frontend/src/components/pki-syncs/forms/PkiSyncOptionsFields/PkiSyncOptionsFields.tsx index 87bfdf69a..9043b36a3 100644 --- a/frontend/src/components/pki-syncs/forms/PkiSyncOptionsFields/PkiSyncOptionsFields.tsx +++ b/frontend/src/components/pki-syncs/forms/PkiSyncOptionsFields/PkiSyncOptionsFields.tsx @@ -104,8 +104,8 @@ export const PkiSyncOptionsFields = ({ destination }: Props) => { tooltipText={
- When a certificate is synced, values will be injected into the certificate name - schema before it reaches the destination. This is useful for organization. + When a certificate is synced, the certificate name schema will be applied before + it reaches the destination.
@@ -114,10 +114,6 @@ export const PkiSyncOptionsFields = ({ destination }: Props) => {
  • {"{{certificateId}}"} - The unique ID of the certificate
  • -
  • - {"{{environment}}"} - The environment which the certificate is in - (e.g. dev, staging, prod) -
  • {syncOption?.forbiddenCharacters && syncOption.forbiddenCharacters.length > 0 && ( @@ -129,11 +125,6 @@ export const PkiSyncOptionsFields = ({ destination }: Props) => { The following characters are not allowed:{" "} {syncOption.forbiddenCharacters.split("").join(" ")}
    - {syncOption.allowedCharacterPattern && ( -
    - Only alphanumeric characters and hyphens are allowed (a-z, A-Z, 0-9, -) -
    - )}
    )} @@ -142,7 +133,7 @@ export const PkiSyncOptionsFields = ({ destination }: Props) => { isOptional errorText={error?.message} label="Certificate Name Schema" - helperText="Infisical strongly advises setting a Certificate Name Schema to ensure that Infisical only manages the specific certificates you intend, keeping everything else untouched." + helperText="Infisical strongly advises setting a Certificate Name Schema to ensure that Infisical only manages the specific certificates you intend to manage, keeping everything else untouched." > { const queryClient = useQueryClient(); return useMutation({ mutationFn: async ({ syncId, destination }: TTriggerPkiSyncRemoveCertificatesDTO) => { - const { data } = await apiRequest.post(`/api/v1/pki/syncs/${destination}/${syncId}/remove`); + const { data } = await apiRequest.post( + `/api/v1/pki/syncs/${destination}/${syncId}/remove-certificates` + ); return data; }, diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionConditions.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionConditions.tsx index 8156ee15c..e87e2f3a4 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionConditions.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionConditions.tsx @@ -8,8 +8,7 @@ type Props = { type: | ProjectPermissionSub.SecretFolders | ProjectPermissionSub.SecretImports - | ProjectPermissionSub.SecretRotation - | ProjectPermissionSub.PkiSyncs; + | ProjectPermissionSub.SecretRotation; }; export const GeneralPermissionConditions = ({ position = 0, isDisabled, type }: Props) => { diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/PkiSyncPermissionConditions.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/PkiSyncPermissionConditions.tsx new file mode 100644 index 000000000..a045ca258 --- /dev/null +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/PkiSyncPermissionConditions.tsx @@ -0,0 +1,19 @@ +import { ProjectPermissionSub } from "@app/context/ProjectPermissionContext/types"; + +import { ConditionsFields } from "./ConditionsFields"; + +type Props = { + position?: number; + isDisabled?: boolean; +}; + +export const PkiSyncPermissionConditions = ({ position = 0, isDisabled }: Props) => { + return ( + + ); +}; diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx index 347ebaf99..a7f2d07ae 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx @@ -23,6 +23,7 @@ import { GeneralPermissionPolicies } from "./GeneralPermissionPolicies"; import { IdentityManagementPermissionConditions } from "./IdentityManagementPermissionConditions"; import { PermissionEmptyState } from "./PermissionEmptyState"; import { PkiSubscriberPermissionConditions } from "./PkiSubscriberPermissionConditions"; +import { PkiSyncPermissionConditions } from "./PkiSyncPermissionConditions"; import { PkiTemplatePermissionConditions } from "./PkiTemplatePermissionConditions"; import { EXCLUDED_PERMISSION_SUBS, @@ -74,6 +75,10 @@ export const renderConditionalComponents = ( return ; } + if (subject === ProjectPermissionSub.PkiSyncs) { + return ; + } + if (subject === ProjectPermissionSub.SecretEvents) { return ; }