diff --git a/backend/src/db/migrations/20250910193000_pki-sync.ts b/backend/src/db/migrations/20250910193000_pki-sync.ts index 73dc620d9..3c4fdd4eb 100644 --- a/backend/src/db/migrations/20250910193000_pki-sync.ts +++ b/backend/src/db/migrations/20250910193000_pki-sync.ts @@ -19,7 +19,7 @@ export async function up(knex: Knex): Promise { t.uuid("subscriberId"); t.foreign("subscriberId").references("id").inTable(TableName.PkiSubscriber).onDelete("SET NULL"); t.uuid("connectionId").notNullable(); - t.foreign("connectionId").references("id").inTable(TableName.AppConnection); + t.foreign("connectionId").references("id").inTable(TableName.AppConnection).onDelete("CASCADE"); t.timestamps(true, true, true); t.string("syncStatus"); t.string("lastSyncJobId"); diff --git a/backend/src/ee/services/license/__mocks__/license-fns.ts b/backend/src/ee/services/license/__mocks__/license-fns.ts index 729696baa..f139ff2c1 100644 --- a/backend/src/ee/services/license/__mocks__/license-fns.ts +++ b/backend/src/ee/services/license/__mocks__/license-fns.ts @@ -31,6 +31,7 @@ export const getDefaultOnPremFeatures = () => { caCrl: false, sshHostGroups: false, enterpriseSecretSyncs: false, + enterpriseCertificateSyncs: false, enterpriseAppConnections: true, machineIdentityAuthTemplates: false }; diff --git a/backend/src/ee/services/license/license-fns.ts b/backend/src/ee/services/license/license-fns.ts index a302e956a..cbe194f31 100644 --- a/backend/src/ee/services/license/license-fns.ts +++ b/backend/src/ee/services/license/license-fns.ts @@ -62,6 +62,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ sshHostGroups: false, secretScanning: false, enterpriseSecretSyncs: false, + enterpriseCertificateSyncs: false, enterpriseAppConnections: false, fips: false, eventSubscriptions: false, diff --git a/backend/src/ee/services/license/license-types.ts b/backend/src/ee/services/license/license-types.ts index 2ccd3ac8f..d5d1143d4 100644 --- a/backend/src/ee/services/license/license-types.ts +++ b/backend/src/ee/services/license/license-types.ts @@ -75,6 +75,7 @@ export type TFeatureSet = { sshHostGroups: false; secretScanning: false; enterpriseSecretSyncs: false; + enterpriseCertificateSyncs: false; enterpriseAppConnections: false; machineIdentityAuthTemplates: false; fips: false; diff --git a/backend/src/services/pki-sync/pki-sync-dal.ts b/backend/src/services/pki-sync/pki-sync-dal.ts index 151d93f77..639a6a788 100644 --- a/backend/src/services/pki-sync/pki-sync-dal.ts +++ b/backend/src/services/pki-sync/pki-sync-dal.ts @@ -30,8 +30,7 @@ const basePkiSyncQuery = ({ filter, db, tx }: { db: TDbClient; filter?: PkiSyncF db .ref("isPlatformManagedCredentials") .withSchema(TableName.AppConnection) - .as("appConnectionIsPlatformManagedCredentials"), - db.ref("encryptedCredentials").withSchema(TableName.AppConnection).as("appConnectionEncryptedCredentials") + .as("appConnectionIsPlatformManagedCredentials") ); if (filter) { diff --git a/backend/src/services/pki-sync/pki-sync-fns.ts b/backend/src/services/pki-sync/pki-sync-fns.ts index a6027177b..e518e4622 100644 --- a/backend/src/services/pki-sync/pki-sync-fns.ts +++ b/backend/src/services/pki-sync/pki-sync-fns.ts @@ -18,7 +18,7 @@ export const enterprisePkiSyncCheck = async ( ) => { const plan = await licenseService.getPlan(orgId); - if (!plan.enterpriseSecretSyncs && ENTERPRISE_PKI_SYNCS.includes(pkiSyncDestination)) { + if (!plan.enterpriseCertificateSyncs && ENTERPRISE_PKI_SYNCS.includes(pkiSyncDestination)) { throw new BadRequestError({ message: errorMessage || "Failed to create PKI sync due to plan restriction. Upgrade plan to create PKI sync." }); diff --git a/backend/src/services/pki-sync/pki-sync-queue.ts b/backend/src/services/pki-sync/pki-sync-queue.ts index caec59f48..c0b938107 100644 --- a/backend/src/services/pki-sync/pki-sync-queue.ts +++ b/backend/src/services/pki-sync/pki-sync-queue.ts @@ -172,7 +172,8 @@ export const pkiSyncQueueFactory = ({ // Validate certificate data if (!certData.certificate || certData.certificate.trim() === "") { logger.error(`Skipping certificate ${certData.name}: empty certificate data`); - return; + // eslint-disable-next-line no-continue + continue; } // Parse certificate to extract metadata diff --git a/backend/src/services/pki-sync/pki-sync-schemas.ts b/backend/src/services/pki-sync/pki-sync-schemas.ts index c7b7656b6..b831d1f0c 100644 --- a/backend/src/services/pki-sync/pki-sync-schemas.ts +++ b/backend/src/services/pki-sync/pki-sync-schemas.ts @@ -1,19 +1,33 @@ import { z } from "zod"; +import { AzureKeyVaultPkiSyncConfigSchema } from "./azure-key-vault/azure-key-vault-pki-sync-types"; import { PkiSync } from "./pki-sync-enums"; +// Schema for PKI sync options configuration +export const PkiSyncOptionsSchema = z.object({ + canImportCertificates: z.boolean() +}); + +// Schema for destination-specific configurations +export const PkiSyncDestinationConfigSchema = z.discriminatedUnion("destination", [ + z.object({ + destination: z.literal(PkiSync.AzureKeyVault), + config: AzureKeyVaultPkiSyncConfigSchema + }) +]); + // Base PKI sync schema for API responses export const PkiSyncSchema = z.object({ - id: z.string(), - name: z.string(), + id: z.string().uuid(), + name: z.string().max(255), description: z.string().nullable().optional(), destination: z.nativeEnum(PkiSync), isAutoSyncEnabled: z.boolean(), destinationConfig: z.record(z.unknown()), syncOptions: z.record(z.unknown()), - projectId: z.string(), - subscriberId: z.string().nullable().optional(), - connectionId: z.string(), + projectId: z.string().uuid(), + subscriberId: z.string().uuid().nullable().optional(), + connectionId: z.string().uuid(), createdAt: z.date(), updatedAt: z.date(), syncStatus: z.string().nullable().optional(), @@ -22,14 +36,14 @@ export const PkiSyncSchema = z.object({ // Schema for PKI sync list items (includes app connection info) export const PkiSyncListItemSchema = PkiSyncSchema.extend({ - appConnectionName: z.string(), - appConnectionApp: z.string() + appConnectionName: z.string().max(255), + appConnectionApp: z.string().max(255) }); // Schema for PKI sync details (includes app connection info) export const PkiSyncDetailsSchema = PkiSyncSchema.extend({ - appConnectionName: z.string(), - appConnectionApp: z.string() + appConnectionName: z.string().max(255), + appConnectionApp: z.string().max(255) }); export type TPkiSyncSchema = z.infer; diff --git a/backend/src/services/pki-sync/pki-sync-service.ts b/backend/src/services/pki-sync/pki-sync-service.ts index fdb1e2361..27af42c9f 100644 --- a/backend/src/services/pki-sync/pki-sync-service.ts +++ b/backend/src/services/pki-sync/pki-sync-service.ts @@ -29,6 +29,15 @@ import { TUpdatePkiSyncDTO } from "./pki-sync-types"; +const getDestinationAppType = (destination: PkiSync): AppConnection => { + switch (destination) { + case PkiSync.AzureKeyVault: + return AppConnection.AzureKeyVault; + default: + throw new BadRequestError({ message: "Unsupported PKI sync destination" }); + } +}; + type TPkiSyncServiceFactoryDep = { pkiSyncDAL: TPkiSyncDALFactory; pkiSubscriberDAL: Pick; @@ -89,7 +98,7 @@ export const pkiSyncServiceFactory = ({ } // Get the destination app type based on PKI sync destination - const destinationApp = destination === PkiSync.AzureKeyVault ? AppConnection.AzureKeyVault : destination; + const destinationApp = getDestinationAppType(destination); // Validates permission to connect and app is valid for sync destination await appConnectionService.connectAppConnectionById(destinationApp, connectionId, actor); diff --git a/frontend/src/components/pki-syncs/PkiSyncSelect.tsx b/frontend/src/components/pki-syncs/PkiSyncSelect.tsx index 5a004823d..cf1dd1764 100644 --- a/frontend/src/components/pki-syncs/PkiSyncSelect.tsx +++ b/frontend/src/components/pki-syncs/PkiSyncSelect.tsx @@ -65,10 +65,10 @@ export const PkiSyncSelect = ({ onSelect }: Props) => { const { image, name } = PKI_SYNC_MAP[destination]; return (