diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 53a2ca993..0bec59a76 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -50,6 +50,7 @@ export enum ApiDocsTags { IdentitySpecificPrivilegesV2 = "Identity Specific Privileges V2", AppConnections = "App Connections", SecretSyncs = "Secret Syncs", + PkiSyncs = "PKI Syncs", Integrations = "Integrations", ServiceTokens = "Service Tokens", AuditLogs = "Audit Logs", diff --git a/backend/src/server/routes/v1/index.ts b/backend/src/server/routes/v1/index.ts index 54d4b5a01..64d9d511e 100644 --- a/backend/src/server/routes/v1/index.ts +++ b/backend/src/server/routes/v1/index.ts @@ -48,7 +48,7 @@ import { registerPasswordRouter } from "./password-router"; import { registerPkiAlertRouter } from "./pki-alert-router"; import { registerPkiCollectionRouter } from "./pki-collection-router"; import { registerPkiSubscriberRouter } from "./pki-subscriber-router"; -import { registerPkiSyncRouter } from "./pki-sync-router"; +import { PKI_SYNC_REGISTER_ROUTER_MAP, registerPkiSyncRouter } from "./pki-sync-routers"; import { registerProjectEnvRouter } from "./project-env-router"; import { registerProjectKeyRouter } from "./project-key-router"; import { registerProjectMembershipRouter } from "./project-membership-router"; @@ -157,7 +157,16 @@ export const registerV1Routes = async (server: FastifyZodProvider) => { await server.register(registerIntegrationAuthRouter, { prefix: "/integration-auth" }); await server.register(registerWebhookRouter, { prefix: "/webhooks" }); await server.register(registerIdentityRouter, { prefix: "/identities" }); - await server.register(registerPkiSyncRouter, { prefix: "/pki-syncs" }); + await server.register( + async (pkiSyncRouter) => { + // register generic pki sync endpoints + await pkiSyncRouter.register(registerPkiSyncRouter); + for await (const [destination, router] of Object.entries(PKI_SYNC_REGISTER_ROUTER_MAP)) { + await pkiSyncRouter.register(router, { prefix: `/${destination}` }); + } + }, + { prefix: "/pki-syncs" } + ); await server.register( async (secretSharingRouter) => { diff --git a/backend/src/server/routes/v1/pki-sync-router.ts b/backend/src/server/routes/v1/pki-sync-router.ts deleted file mode 100644 index 547acf75d..000000000 --- a/backend/src/server/routes/v1/pki-sync-router.ts +++ /dev/null @@ -1,540 +0,0 @@ -import { z } from "zod"; - -import { EventType } from "@app/ee/services/audit-log/audit-log-types"; -import { logger } from "@app/lib/logger"; -import { readLimit } from "@app/server/config/rateLimiter"; -import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; -import { AuthMode } from "@app/services/auth/auth-type"; -import { AzureKeyVaultPkiSyncConfigSchema } from "@app/services/pki-sync/azure-key-vault/azure-key-vault-pki-sync-types"; -import { PkiSync } from "@app/services/pki-sync/pki-sync-enums"; -import { PkiSyncDetailsSchema, PkiSyncListItemSchema, PkiSyncSchema } from "@app/services/pki-sync/pki-sync-schemas"; -import { TCreatePkiSyncDTO, TUpdatePkiSyncDTO } from "@app/services/pki-sync/pki-sync-types"; - -const CreatePkiSyncRequestBodySchema = z.object({ - name: z.string().trim().min(1).max(64), - description: z.string().optional(), - destination: z.nativeEnum(PkiSync), - isAutoSyncEnabled: z.boolean().default(true), - destinationConfig: z - .discriminatedUnion("destination", [ - z.object({ - destination: z.literal(PkiSync.AzureKeyVault), - config: AzureKeyVaultPkiSyncConfigSchema - }) - ]) - .transform(({ config }) => config), - syncOptions: z.record(z.unknown()).default({}), - subscriberId: z.string().optional(), - connectionId: z.string(), - projectId: z.string().trim().min(1) -}); - -const UpdatePkiSyncRequestBodySchema = z.object({ - name: z.string().trim().min(1).max(64).optional(), - description: z.string().optional(), - isAutoSyncEnabled: z.boolean().optional(), - destinationConfig: z.record(z.unknown()).optional(), - syncOptions: z.record(z.unknown()).optional(), - subscriberId: z.string().optional(), - connectionId: z.string().optional() -}); - -export const registerPkiSyncRouter = async (server: FastifyZodProvider) => { - server.route({ - method: "GET", - url: "/options", - config: { - rateLimit: readLimit - }, - schema: { - description: "Get PKI sync options", - security: [ - { - bearerAuth: [] - } - ], - response: { - 200: { - description: "PKI sync options retrieved successfully", - content: { - "application/json": { - schema: z.object({ - pkiSyncOptions: z.array( - z.object({ - name: z.string(), - destination: z.nativeEnum(PkiSync), - canImportCertificates: z.boolean(), - canRemoveCertificates: z.boolean(), - enterprise: z.boolean().optional() - }) - ) - }) - } - } - } - } - }, - onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]), - handler: async () => { - const pkiSyncOptions = [ - { - name: "Azure Key Vault", - destination: PkiSync.AzureKeyVault, - canImportCertificates: true, - canRemoveCertificates: true, - enterprise: false - } - ]; - - return { pkiSyncOptions }; - } - }); - - server.route({ - method: "POST", - url: "/", - config: { - rateLimit: readLimit - }, - schema: { - description: "Create PKI sync", - security: [ - { - bearerAuth: [] - } - ], - requestBody: { - content: { - "application/json": { - schema: CreatePkiSyncRequestBodySchema - } - } - }, - response: { - 200: { - description: "PKI sync created successfully", - content: { - "application/json": { - schema: z.object({ - pkiSync: PkiSyncSchema - }) - } - } - } - } - }, - onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]), - handler: async (req) => { - const requestBody = CreatePkiSyncRequestBodySchema.parse(req.body); - const createData: Omit = requestBody; - - try { - const pkiSync = await server.services.pkiSync.createPkiSync(createData, req.permission); - - await server.services.auditLog.createAuditLog({ - ...req.auditLogInfo, - projectId: createData.projectId, - event: { - type: EventType.CREATE_PKI_SYNC, - metadata: { - pkiSyncId: pkiSync.id, - name: pkiSync.name, - destination: pkiSync.destination - } - } - }); - - return { pkiSync }; - } catch (error) { - logger.error("Failed to create PKI sync"); - logger.error(error); - throw error; - } - } - }); - - server.route({ - method: "GET", - url: "/", - config: { - rateLimit: readLimit - }, - schema: { - description: "List PKI syncs", - security: [ - { - bearerAuth: [] - } - ], - querystring: z.object({ - projectId: z.string().trim().min(1) - }), - response: { - 200: { - description: "PKI syncs retrieved successfully", - content: { - "application/json": { - schema: z.object({ - pkiSyncs: z.array(PkiSyncListItemSchema) - }) - } - } - } - } - }, - onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]), - handler: async (req) => { - const pkiSyncs = await server.services.pkiSync.listPkiSyncsByProjectId( - { - projectId: req.query.projectId - }, - req.permission - ); - - return { pkiSyncs }; - } - }); - - server.route({ - method: "GET", - url: "/:pkiSyncId", - config: { - rateLimit: readLimit - }, - schema: { - description: "Get PKI sync by ID", - security: [ - { - bearerAuth: [] - } - ], - params: z.object({ - pkiSyncId: z.string() - }), - querystring: z.object({ - projectId: z.string().trim().min(1) - }), - response: { - 200: { - description: "PKI sync retrieved successfully", - content: { - "application/json": { - schema: z.object({ - pkiSync: PkiSyncDetailsSchema - }) - } - } - } - } - }, - onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]), - handler: async (req) => { - const pkiSync = await server.services.pkiSync.findPkiSyncById( - { - id: req.params.pkiSyncId, - projectId: req.query.projectId - }, - req.permission - ); - - return { pkiSync }; - } - }); - - server.route({ - method: "PATCH", - url: "/:pkiSyncId", - config: { - rateLimit: readLimit - }, - schema: { - description: "Update PKI sync", - security: [ - { - bearerAuth: [] - } - ], - params: z.object({ - pkiSyncId: z.string() - }), - querystring: z.object({ - projectId: z.string().trim().min(1) - }), - requestBody: { - content: { - "application/json": { - schema: UpdatePkiSyncRequestBodySchema - } - } - }, - response: { - 200: { - description: "PKI sync updated successfully", - content: { - "application/json": { - schema: z.object({ - pkiSync: PkiSyncSchema - }) - } - } - } - } - }, - onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]), - handler: async (req) => { - const requestBody = UpdatePkiSyncRequestBodySchema.parse(req.body); - const updateData: Omit = { - id: req.params.pkiSyncId, - projectId: req.query.projectId, - ...requestBody - }; - - try { - const pkiSync = await server.services.pkiSync.updatePkiSync(updateData, req.permission); - - await server.services.auditLog.createAuditLog({ - ...req.auditLogInfo, - projectId: req.query.projectId, - event: { - type: EventType.UPDATE_PKI_SYNC, - metadata: { - pkiSyncId: pkiSync.id, - name: pkiSync.name - } - } - }); - - return { pkiSync }; - } catch (error) { - logger.error("Failed to update PKI sync"); - logger.error(error); - throw error; - } - } - }); - - server.route({ - method: "DELETE", - url: "/:pkiSyncId", - config: { - rateLimit: readLimit - }, - schema: { - description: "Delete PKI sync", - security: [ - { - bearerAuth: [] - } - ], - params: z.object({ - pkiSyncId: z.string() - }), - querystring: z.object({ - projectId: z.string().trim().min(1) - }), - response: { - 200: { - description: "PKI sync deleted successfully", - content: { - "application/json": { - schema: z.object({ - pkiSync: z.object({ - id: z.string(), - name: z.string(), - destination: z.nativeEnum(PkiSync) - }) - }) - } - } - } - } - }, - onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]), - handler: async (req) => { - try { - const pkiSync = await server.services.pkiSync.deletePkiSync( - { - id: req.params.pkiSyncId, - projectId: req.query.projectId - }, - req.permission - ); - - await server.services.auditLog.createAuditLog({ - ...req.auditLogInfo, - projectId: req.query.projectId, - event: { - type: EventType.DELETE_PKI_SYNC, - metadata: { - pkiSyncId: pkiSync.id, - name: pkiSync.name, - destination: pkiSync.destination - } - } - }); - - return { pkiSync }; - } catch (error) { - logger.error("Failed to delete PKI sync"); - logger.error(error); - throw error; - } - } - }); - - server.route({ - method: "POST", - url: "/:pkiSyncId/sync", - config: { - rateLimit: readLimit - }, - schema: { - description: "Trigger PKI sync", - security: [ - { - bearerAuth: [] - } - ], - params: z.object({ - pkiSyncId: z.string() - }), - querystring: z.object({ - projectId: z.string().trim().min(1) - }), - response: { - 200: { - description: "PKI sync triggered successfully", - content: { - "application/json": { - schema: z.object({ - message: z.string() - }) - } - } - } - } - }, - onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]), - handler: async (req) => { - try { - const result = await server.services.pkiSync.triggerPkiSyncSyncCertificatesById( - { - id: req.params.pkiSyncId, - projectId: req.query.projectId - }, - req.permission - ); - - return result; - } catch (error) { - logger.error("Failed to trigger PKI sync"); - logger.error(error); - throw error; - } - } - }); - - server.route({ - method: "POST", - url: "/:pkiSyncId/import", - config: { - rateLimit: readLimit - }, - schema: { - description: "Import certificates from PKI sync destination", - security: [ - { - bearerAuth: [] - } - ], - params: z.object({ - pkiSyncId: z.string() - }), - querystring: z.object({ - projectId: z.string().trim().min(1) - }), - response: { - 200: { - description: "PKI sync import triggered successfully", - content: { - "application/json": { - schema: z.object({ - message: z.string() - }) - } - } - } - } - }, - onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]), - handler: async (req) => { - try { - const result = await server.services.pkiSync.triggerPkiSyncImportCertificatesById( - { - id: req.params.pkiSyncId, - projectId: req.query.projectId - }, - req.permission - ); - - return result; - } catch (error) { - logger.error("Failed to trigger PKI sync import certificates"); - logger.error(error); - throw error; - } - } - }); - - server.route({ - method: "POST", - url: "/:pkiSyncId/remove", - config: { - rateLimit: readLimit - }, - schema: { - description: "Remove certificates from PKI sync destination", - security: [ - { - bearerAuth: [] - } - ], - params: z.object({ - pkiSyncId: z.string() - }), - querystring: z.object({ - projectId: z.string().trim().min(1) - }), - response: { - 200: { - description: "PKI sync remove triggered successfully", - content: { - "application/json": { - schema: z.object({ - message: z.string() - }) - } - } - } - } - }, - onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]), - handler: async (req) => { - try { - const result = await server.services.pkiSync.triggerPkiSyncRemoveCertificatesById( - { - id: req.params.pkiSyncId, - projectId: req.query.projectId - }, - req.permission - ); - - return result; - } catch (error) { - logger.error("Failed to trigger PKI sync remove certificates"); - logger.error(error); - throw error; - } - } - }); -}; diff --git a/backend/src/server/routes/v1/pki-sync-routers/azure-key-vault-pki-sync-router.ts b/backend/src/server/routes/v1/pki-sync-routers/azure-key-vault-pki-sync-router.ts new file mode 100644 index 000000000..66e93b2a6 --- /dev/null +++ b/backend/src/server/routes/v1/pki-sync-routers/azure-key-vault-pki-sync-router.ts @@ -0,0 +1,17 @@ +import { + AzureKeyVaultPkiSyncSchema, + CreateAzureKeyVaultPkiSyncSchema, + UpdateAzureKeyVaultPkiSyncSchema +} from "@app/services/pki-sync/azure-key-vault"; +import { PkiSync } from "@app/services/pki-sync/pki-sync-enums"; + +import { registerSyncPkiEndpoints } from "./pki-sync-endpoints"; + +export const registerAzureKeyVaultPkiSyncRouter = async (server: FastifyZodProvider) => + registerSyncPkiEndpoints({ + destination: PkiSync.AzureKeyVault, + server, + responseSchema: AzureKeyVaultPkiSyncSchema, + createSchema: CreateAzureKeyVaultPkiSyncSchema, + updateSchema: UpdateAzureKeyVaultPkiSyncSchema + }); diff --git a/backend/src/server/routes/v1/pki-sync-routers/index.ts b/backend/src/server/routes/v1/pki-sync-routers/index.ts new file mode 100644 index 000000000..326b650e9 --- /dev/null +++ b/backend/src/server/routes/v1/pki-sync-routers/index.ts @@ -0,0 +1,9 @@ +import { PkiSync } from "@app/services/pki-sync/pki-sync-enums"; + +import { registerAzureKeyVaultPkiSyncRouter } from "./azure-key-vault-pki-sync-router"; + +export * from "./pki-sync-router"; + +export const PKI_SYNC_REGISTER_ROUTER_MAP: Record Promise> = { + [PkiSync.AzureKeyVault]: registerAzureKeyVaultPkiSyncRouter +}; 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 new file mode 100644 index 000000000..728a5df0d --- /dev/null +++ b/backend/src/server/routes/v1/pki-sync-routers/pki-sync-endpoints.ts @@ -0,0 +1,363 @@ +import { z } from "zod"; + +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { ApiDocsTags } from "@app/lib/api-docs"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; +import { PkiSync } from "@app/services/pki-sync/pki-sync-enums"; +import { PKI_SYNC_NAME_MAP } from "@app/services/pki-sync/pki-sync-maps"; + +export const registerSyncPkiEndpoints = ({ + server, + destination, + createSchema, + updateSchema, + responseSchema +}: { + destination: PkiSync; + server: FastifyZodProvider; + createSchema: z.ZodType<{ + name: string; + projectId: string; + connectionId: string; + destinationConfig: Record; + syncOptions?: Record; + description?: string; + isAutoSyncEnabled?: boolean; + subscriberId?: string; + }>; + updateSchema: z.ZodType<{ + connectionId?: string; + name?: string; + destinationConfig?: Record; + syncOptions?: Record; + description?: string; + isAutoSyncEnabled?: boolean; + subscriberId?: string; + }>; + responseSchema: z.ZodTypeAny; +}) => { + const destinationName = PKI_SYNC_NAME_MAP[destination]; + + server.route({ + method: "GET", + url: `/`, + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.PkiSyncs], + description: `List the ${destinationName} PKI Syncs for the specified project.`, + querystring: z.object({ + projectId: z.string().trim().min(1, "Project ID required") + }), + response: { + 200: z.object({ pkiSyncs: responseSchema.array() }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]), + handler: async (req) => { + const { + query: { projectId } + } = req; + + const pkiSyncs = await server.services.pkiSync.listPkiSyncsByProjectId({ projectId }, req.permission); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId, + event: { + type: EventType.GET_PKI_SYNCS, + metadata: { + projectId + } + } + }); + + return { pkiSyncs }; + } + }); + + server.route({ + method: "GET", + url: "/:pkiSyncId", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.PkiSyncs], + description: `Get the specified ${destinationName} PKI Sync by ID.`, + params: z.object({ + pkiSyncId: z.string() + }), + querystring: z.object({ + projectId: z.string().trim().min(1) + }), + response: { + 200: z.object({ pkiSync: responseSchema }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]), + handler: async (req) => { + const { pkiSyncId } = req.params; + const { projectId } = req.query; + + const pkiSync = await server.services.pkiSync.findPkiSyncById({ id: pkiSyncId, projectId }, req.permission); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: pkiSync.projectId, + event: { + type: EventType.GET_PKI_SYNC, + metadata: { + syncId: pkiSyncId, + destination + } + } + }); + + return { pkiSync }; + } + }); + + server.route({ + method: "POST", + url: "/", + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.PkiSyncs], + description: `Create a ${destinationName} PKI Sync for the specified project.`, + body: createSchema, + response: { + 200: z.object({ pkiSync: responseSchema }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]), + handler: async (req) => { + const pkiSync = await server.services.pkiSync.createPkiSync({ ...req.body, destination }, req.permission); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: pkiSync.projectId, + event: { + type: EventType.CREATE_PKI_SYNC, + metadata: { + pkiSyncId: pkiSync.id, + name: pkiSync.name, + destination + } + } + }); + + return { pkiSync }; + } + }); + + server.route({ + method: "PATCH", + url: "/:pkiSyncId", + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.PkiSyncs], + description: `Update the specified ${destinationName} PKI Sync.`, + params: z.object({ + pkiSyncId: z.string() + }), + querystring: z.object({ + projectId: z.string().trim().min(1) + }), + body: updateSchema, + response: { + 200: z.object({ pkiSync: responseSchema }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]), + handler: async (req) => { + const { pkiSyncId } = req.params; + const { projectId } = req.query; + + const pkiSync = await server.services.pkiSync.updatePkiSync( + { ...req.body, id: pkiSyncId, projectId }, + req.permission + ); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId, + event: { + type: EventType.UPDATE_PKI_SYNC, + metadata: { + pkiSyncId, + name: pkiSync.name + } + } + }); + + return { pkiSync }; + } + }); + + server.route({ + method: "DELETE", + url: `/:pkiSyncId`, + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.PkiSyncs], + description: `Delete the specified ${destinationName} PKI Sync.`, + params: z.object({ + pkiSyncId: z.string() + }), + querystring: z.object({ + projectId: z.string().trim().min(1) + }), + response: { + 200: z.object({ pkiSync: responseSchema }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]), + handler: async (req) => { + const { pkiSyncId } = req.params; + const { projectId } = req.query; + + const pkiSync = await server.services.pkiSync.deletePkiSync({ id: pkiSyncId, projectId }, req.permission); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId, + event: { + type: EventType.DELETE_PKI_SYNC, + metadata: { + pkiSyncId, + name: pkiSync.name, + destination: pkiSync.destination + } + } + }); + + return { pkiSync }; + } + }); + + server.route({ + method: "POST", + url: "/:pkiSyncId/sync", + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.PkiSyncs], + description: `Trigger a sync for the specified ${destinationName} PKI Sync.`, + params: z.object({ + pkiSyncId: z.string() + }), + querystring: z.object({ + projectId: z.string().trim().min(1) + }), + response: { + 200: z.object({ message: z.string() }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]), + handler: async (req) => { + const { pkiSyncId } = req.params; + const { projectId } = req.query; + + const result = await server.services.pkiSync.triggerPkiSyncSyncCertificatesById( + { + id: pkiSyncId, + projectId + }, + req.permission + ); + + return result; + } + }); + + server.route({ + method: "POST", + url: "/:pkiSyncId/import", + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.PkiSyncs], + description: `Import certificates from the specified ${destinationName} PKI Sync destination.`, + params: z.object({ + pkiSyncId: z.string() + }), + querystring: z.object({ + projectId: z.string().trim().min(1) + }), + response: { + 200: z.object({ message: z.string() }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]), + handler: async (req) => { + const { pkiSyncId } = req.params; + const { projectId } = req.query; + + const result = await server.services.pkiSync.triggerPkiSyncImportCertificatesById( + { + id: pkiSyncId, + projectId + }, + req.permission + ); + + return result; + } + }); + + server.route({ + method: "POST", + url: "/:pkiSyncId/remove", + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.PkiSyncs], + description: `Remove certificates from the specified ${destinationName} PKI Sync destination.`, + params: z.object({ + pkiSyncId: z.string() + }), + querystring: z.object({ + projectId: z.string().trim().min(1) + }), + response: { + 200: z.object({ message: z.string() }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]), + handler: async (req) => { + const { pkiSyncId } = req.params; + const { projectId } = req.query; + + const result = await server.services.pkiSync.triggerPkiSyncRemoveCertificatesById( + { + id: pkiSyncId, + projectId + }, + req.permission + ); + + return result; + } + }); +}; 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 new file mode 100644 index 000000000..c426d7d87 --- /dev/null +++ b/backend/src/server/routes/v1/pki-sync-routers/pki-sync-router.ts @@ -0,0 +1,174 @@ +import { z } from "zod"; + +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { ApiDocsTags } from "@app/lib/api-docs"; +import { readLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { AuthMode } from "@app/services/auth/auth-type"; +import { PkiSync } from "@app/services/pki-sync/pki-sync-enums"; + +const PkiSyncSchema = z.object({ + id: z.string().uuid(), + name: z.string(), + 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().uuid(), + subscriberId: z.string().uuid().nullable().optional(), + connectionId: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date(), + // Sync status fields + syncStatus: z.string().nullable().optional(), + lastSyncJobId: z.string().nullable().optional(), + lastSyncMessage: z.string().nullable().optional(), + lastSyncedAt: z.date().nullable().optional(), + // Import status fields + importStatus: z.string().nullable().optional(), + lastImportJobId: z.string().nullable().optional(), + lastImportMessage: z.string().nullable().optional(), + lastImportedAt: z.date().nullable().optional(), + // Remove status fields + removeStatus: z.string().nullable().optional(), + lastRemoveJobId: z.string().nullable().optional(), + lastRemoveMessage: z.string().nullable().optional(), + lastRemovedAt: z.date().nullable().optional(), + // App connection info + appConnectionName: z.string(), + appConnectionApp: z.string(), + connection: z.object({ + id: z.string(), + name: z.string(), + app: z.string(), + encryptedCredentials: z.unknown().nullable(), + orgId: z.string().uuid(), + projectId: z.string().uuid().nullable().optional(), + method: z.string(), + description: z.string().nullable().optional(), + version: z.number(), + gatewayId: z.string().uuid().nullable().optional(), + createdAt: z.date(), + updatedAt: z.date(), + isPlatformManagedCredentials: z.boolean().nullable().optional() + }) +}); + +const PkiSyncOptionsSchema = z.object({ + name: z.string(), + connection: z.nativeEnum(AppConnection), + destination: z.nativeEnum(PkiSync), + canImportCertificates: z.boolean(), + canRemoveCertificates: z.boolean() +}); + +export const registerPkiSyncRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "GET", + url: "/options", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.PkiSyncs], + description: "List the available PKI Sync Options.", + response: { + 200: z.object({ + pkiSyncOptions: PkiSyncOptionsSchema.array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]), + handler: () => { + const pkiSyncOptions = server.services.pkiSync.getPkiSyncOptions(); + return { pkiSyncOptions }; + } + }); + + server.route({ + method: "GET", + url: "/", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.PkiSyncs], + description: "List all the PKI Syncs for the specified project.", + querystring: z.object({ + projectId: z.string().trim().min(1) + }), + response: { + 200: z.object({ pkiSyncs: PkiSyncSchema.array() }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]), + handler: async (req) => { + const { + query: { projectId }, + permission + } = req; + + const pkiSyncs = await server.services.pkiSync.listPkiSyncsByProjectId({ projectId }, permission); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId, + event: { + type: EventType.GET_PKI_SYNCS, + metadata: { + projectId + } + } + }); + + return { pkiSyncs }; + } + }); + + server.route({ + method: "GET", + url: "/:pkiSyncId", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.PkiSyncs], + description: "Get a PKI Sync by ID.", + params: z.object({ + pkiSyncId: z.string() + }), + querystring: z.object({ + projectId: z.string().trim().min(1) + }), + response: { + 200: z.object({ pkiSync: PkiSyncSchema }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]), + handler: async (req) => { + const { pkiSyncId } = req.params; + const { projectId } = req.query; + + const pkiSync = await server.services.pkiSync.findPkiSyncById({ id: pkiSyncId, projectId }, req.permission); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: pkiSync.projectId, + event: { + type: EventType.GET_PKI_SYNC, + metadata: { + syncId: pkiSyncId, + destination: pkiSync.destination + } + } + }); + + return { pkiSync }; + } + }); +}; 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 da308bcdb..f162cdc44 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 @@ -4,13 +4,23 @@ import { AxiosError } from "axios"; 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 { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TCertificateMap } from "@app/services/pki-sync/pki-sync-types"; +import { PkiSync } from "../pki-sync-enums"; import { PkiSyncError } from "../pki-sync-errors"; import { GetAzureKeyVaultCertificate, TAzureKeyVaultPkiSyncWithCredentials } from "./azure-key-vault-pki-sync-types"; +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 +}; + type TAzureKeyVaultPkiSyncFactoryDeps = { appConnectionDAL: Pick; kmsService: Pick; @@ -56,7 +66,6 @@ export const azureKeyVaultPkiSyncFactory = ({ kmsService, appConnectionDAL }: TA lastSlashIndex = getAzureKeyVaultCertificate.id.lastIndexOf("/"); } - // Get the certificate details const azureKeyVaultCertificate = await request.get( `${getAzureKeyVaultCertificate.id}?api-version=7.4`, { @@ -66,7 +75,6 @@ export const azureKeyVaultPkiSyncFactory = ({ kmsService, appConnectionDAL }: TA } ); - // Convert base64 certificate to PEM format if available let certPem = ""; if (azureKeyVaultCertificate.data.cer) { try { @@ -75,7 +83,6 @@ export const azureKeyVaultPkiSyncFactory = ({ kmsService, appConnectionDAL }: TA const base64Cert = azureKeyVaultCertificate.data.cer; certPem = `-----BEGIN CERTIFICATE-----\n${base64Cert.match(/.{1,64}/g)?.join("\n")}\n-----END CERTIFICATE-----`; } catch (error) { - // If conversion fails, assume it's already in PEM format certPem = azureKeyVaultCertificate.data.cer; } } @@ -259,7 +266,6 @@ export const azureKeyVaultPkiSyncFactory = ({ kmsService, appConnectionDAL }: TA { certificateKey: key, syncId: pkiSync.id }, "Certificate exists in deleted but recoverable state in Azure Key Vault - skipping upload" ); - // Return a successful result to avoid failing the entire sync return { key, success: false, skipped: true, reason: "Certificate in deleted but recoverable state" }; } 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 new file mode 100644 index 000000000..27a481675 --- /dev/null +++ b/backend/src/services/pki-sync/azure-key-vault/azure-key-vault-pki-sync-schemas.ts @@ -0,0 +1,41 @@ +import { z } from "zod"; + +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { PkiSync } from "@app/services/pki-sync/pki-sync-enums"; +import { PkiSyncSchema } from "@app/services/pki-sync/pki-sync-schemas"; + +import { AzureKeyVaultPkiSyncConfigSchema } from "./azure-key-vault-pki-sync-types"; + +export const AzureKeyVaultPkiSyncSchema = PkiSyncSchema.extend({ + destination: z.literal(PkiSync.AzureKeyVault), + destinationConfig: AzureKeyVaultPkiSyncConfigSchema +}); + +export const CreateAzureKeyVaultPkiSyncSchema = z.object({ + name: z.string().trim().min(1).max(64), + description: z.string().optional(), + isAutoSyncEnabled: z.boolean().default(true), + destinationConfig: AzureKeyVaultPkiSyncConfigSchema, + syncOptions: z.record(z.unknown()).optional().default({}), + subscriberId: z.string().optional(), + connectionId: z.string(), + projectId: z.string().trim().min(1) +}); + +export const UpdateAzureKeyVaultPkiSyncSchema = z.object({ + name: z.string().trim().min(1).max(64).optional(), + description: z.string().optional(), + isAutoSyncEnabled: z.boolean().optional(), + destinationConfig: AzureKeyVaultPkiSyncConfigSchema.optional(), + syncOptions: z.record(z.unknown()).optional(), + subscriberId: z.string().optional(), + connectionId: z.string().optional() +}); + +export const AzureKeyVaultPkiSyncListItemSchema = z.object({ + name: z.literal("Azure Key Vault"), + connection: z.literal(AppConnection.AzureKeyVault), + destination: z.literal(PkiSync.AzureKeyVault), + canImportCertificates: z.literal(false), + canRemoveCertificates: z.literal(true) +}); diff --git a/backend/src/services/pki-sync/azure-key-vault/index.ts b/backend/src/services/pki-sync/azure-key-vault/index.ts new file mode 100644 index 000000000..463a3a5d8 --- /dev/null +++ b/backend/src/services/pki-sync/azure-key-vault/index.ts @@ -0,0 +1,3 @@ +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-dal.ts b/backend/src/services/pki-sync/pki-sync-dal.ts index 639a6a788..325121f98 100644 --- a/backend/src/services/pki-sync/pki-sync-dal.ts +++ b/backend/src/services/pki-sync/pki-sync-dal.ts @@ -21,6 +21,7 @@ const basePkiSyncQuery = ({ filter, db, tx }: { db: TDbClient; filter?: PkiSyncF db.ref("app").withSchema(TableName.AppConnection).as("appConnectionApp"), db.ref("encryptedCredentials").withSchema(TableName.AppConnection).as("appConnectionEncryptedCredentials"), db.ref("orgId").withSchema(TableName.AppConnection).as("appConnectionOrgId"), + db.ref("projectId").withSchema(TableName.AppConnection).as("appConnectionProjectId"), db.ref("method").withSchema(TableName.AppConnection).as("appConnectionMethod"), db.ref("description").withSchema(TableName.AppConnection).as("appConnectionDescription"), db.ref("version").withSchema(TableName.AppConnection).as("appConnectionVersion"), @@ -47,6 +48,7 @@ const expandPkiSync = (pkiSync: Awaited>[num appConnectionApp, appConnectionEncryptedCredentials, appConnectionOrgId, + appConnectionProjectId, appConnectionMethod, appConnectionDescription, appConnectionVersion, @@ -70,6 +72,7 @@ const expandPkiSync = (pkiSync: Awaited>[num app: appConnectionApp, encryptedCredentials: appConnectionEncryptedCredentials, orgId: appConnectionOrgId, + projectId: appConnectionProjectId, method: appConnectionMethod, description: appConnectionDescription, version: appConnectionVersion, diff --git a/backend/src/services/pki-sync/pki-sync-fns.ts b/backend/src/services/pki-sync/pki-sync-fns.ts index e518e4622..78535e773 100644 --- a/backend/src/services/pki-sync/pki-sync-fns.ts +++ b/backend/src/services/pki-sync/pki-sync-fns.ts @@ -5,11 +5,16 @@ import { BadRequestError } from "@app/lib/errors"; 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 } from "./azure-key-vault/azure-key-vault-pki-sync-fns"; import { PkiSync } from "./pki-sync-enums"; import { TCertificateMap, TPkiSyncWithCredentials } from "./pki-sync-types"; const ENTERPRISE_PKI_SYNCS: PkiSync[] = []; +const PKI_SYNC_LIST_OPTIONS = { + [PkiSync.AzureKeyVault]: AZURE_KEY_VAULT_PKI_SYNC_LIST_OPTION +}; + export const enterprisePkiSyncCheck = async ( licenseService: Pick, orgId: string, @@ -26,7 +31,7 @@ export const enterprisePkiSyncCheck = async ( }; export const listPkiSyncOptions = () => { - return Object.values(PkiSync); + return Object.values(PKI_SYNC_LIST_OPTIONS).sort((a, b) => a.name.localeCompare(b.name)); }; export const matchesSchema = (schema: T, data: unknown): data is z.infer => { diff --git a/backend/src/services/pki-sync/pki-sync-queue.ts b/backend/src/services/pki-sync/pki-sync-queue.ts index 3747393fc..7740047f5 100644 --- a/backend/src/services/pki-sync/pki-sync-queue.ts +++ b/backend/src/services/pki-sync/pki-sync-queue.ts @@ -216,7 +216,6 @@ export const pkiSyncQueueFactory = ({ encryptedCertificate }); - // Create certificate secret record with encrypted private key (if available) if (certData.privateKey) { const { cipherTextBlob: encryptedPrivateKey } = await kmsEncryptor({ plainText: Buffer.from(certData.privateKey) @@ -231,7 +230,6 @@ export const pkiSyncQueueFactory = ({ logger.info(`Successfully created certificate ${certData.name} with ID ${createdCert.id}`); } catch (error) { logger.error(`Failed to create certificate ${certData.name}: ${String(error)}`); - // Continue with other certificates even if one fails } } }; @@ -264,7 +262,6 @@ export const pkiSyncQueueFactory = ({ for (const certificate of certificates) { try { // Only sync certificates issued by Infisical (not imported ones) - // Imported certificates don't have caId and certificateTemplateId if (!certificate.caId) { logger.debug( { certificateId: certificate.id, subscriberId }, @@ -503,15 +500,14 @@ export const pkiSyncQueueFactory = ({ try { const { - connection: { orgId, encryptedCredentials }, - projectId + connection: { orgId, encryptedCredentials, projectId: appConnectionProjectId } } = pkiSync; const credentials = await decryptAppConnectionCredentials({ orgId, encryptedCredentials, kmsService, - projectId + projectId: appConnectionProjectId }); const pkiSyncWithCredentials = { @@ -564,7 +560,6 @@ export const pkiSyncQueueFactory = ({ if (err instanceof PkiSyncError && !err.shouldRetry) { isFinalAttempt = true; } else { - // re-throw so job fails throw err; } } finally { @@ -630,15 +625,14 @@ export const pkiSyncQueueFactory = ({ try { const { - connection: { orgId, encryptedCredentials }, - projectId + connection: { orgId, encryptedCredentials, projectId: appConnectionProjectId } } = pkiSync; const credentials = await decryptAppConnectionCredentials({ orgId, encryptedCredentials, kmsService, - projectId + projectId: appConnectionProjectId }); await $importCertificates({ @@ -673,7 +667,6 @@ export const pkiSyncQueueFactory = ({ if (err instanceof PkiSyncError && !err.shouldRetry) { isFinalAttempt = true; } else { - // re-throw so job fails throw err; } } finally { @@ -746,14 +739,14 @@ export const pkiSyncQueueFactory = ({ try { const { - connection: { orgId, encryptedCredentials } + connection: { orgId, encryptedCredentials, projectId: appConnectionProjectId } } = pkiSync; const credentials = await decryptAppConnectionCredentials({ orgId, encryptedCredentials, kmsService, - projectId: pkiSync.projectId + projectId: appConnectionProjectId }); const certificateMap = await $getInfisicalCertificates(pkiSync); @@ -797,7 +790,6 @@ export const pkiSyncQueueFactory = ({ if (err instanceof PkiSyncError && !err.shouldRetry) { isFinalAttempt = true; } else { - // re-throw so job fails throw err; } } finally { @@ -880,7 +872,6 @@ export const pkiSyncQueueFactory = ({ errorMessage = lastRemoveMessage || null; } - // Log notification for now - actual email sending would require SMTP configuration if (projectAdmins.length > 0) { logger.info( `PKI Sync ${action} failure notification would be sent to ${projectAdmins.length} admin(s) for sync "${name}" in project "${project.name}". Error: ${errorMessage}` diff --git a/backend/src/services/pki-sync/pki-sync-schemas.ts b/backend/src/services/pki-sync/pki-sync-schemas.ts index b831d1f0c..4dbcbbc74 100644 --- a/backend/src/services/pki-sync/pki-sync-schemas.ts +++ b/backend/src/services/pki-sync/pki-sync-schemas.ts @@ -40,7 +40,6 @@ export const PkiSyncListItemSchema = PkiSyncSchema.extend({ appConnectionApp: z.string().max(255) }); -// Schema for PKI sync details (includes app connection info) export const PkiSyncDetailsSchema = PkiSyncSchema.extend({ appConnectionName: z.string().max(255), appConnectionApp: z.string().max(255) diff --git a/backend/src/services/pki-sync/pki-sync-service.ts b/backend/src/services/pki-sync/pki-sync-service.ts index 27af42c9f..142eadd93 100644 --- a/backend/src/services/pki-sync/pki-sync-service.ts +++ b/backend/src/services/pki-sync/pki-sync-service.ts @@ -103,6 +103,12 @@ export const pkiSyncServiceFactory = ({ // Validates permission to connect and app is valid for sync destination await appConnectionService.connectAppConnectionById(destinationApp, connectionId, actor); + const defaultSyncOptions = { + canImportCertificates: false, + canRemoveCertificates: true, + ...syncOptions + }; + try { const pkiSync = await pkiSyncDAL.create({ name, @@ -110,7 +116,7 @@ export const pkiSyncServiceFactory = ({ destination, isAutoSyncEnabled, destinationConfig, - syncOptions, + syncOptions: defaultSyncOptions, subscriberId, connectionId, projectId, diff --git a/docs/api-reference/endpoints/certificate-syncs/azure-key-vault/create.mdx b/docs/api-reference/endpoints/certificate-syncs/azure-key-vault/create.mdx new file mode 100644 index 000000000..497874510 --- /dev/null +++ b/docs/api-reference/endpoints/certificate-syncs/azure-key-vault/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/pki-syncs" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/certificate-syncs/azure-key-vault/delete.mdx b/docs/api-reference/endpoints/certificate-syncs/azure-key-vault/delete.mdx new file mode 100644 index 000000000..db1f254c3 --- /dev/null +++ b/docs/api-reference/endpoints/certificate-syncs/azure-key-vault/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/pki-syncs/{pkiSyncId}" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/certificate-syncs/azure-key-vault/get-by-id.mdx b/docs/api-reference/endpoints/certificate-syncs/azure-key-vault/get-by-id.mdx new file mode 100644 index 000000000..38860a416 --- /dev/null +++ b/docs/api-reference/endpoints/certificate-syncs/azure-key-vault/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/pki-syncs/{pkiSyncId}" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/certificate-syncs/azure-key-vault/import-certificates.mdx b/docs/api-reference/endpoints/certificate-syncs/azure-key-vault/import-certificates.mdx new file mode 100644 index 000000000..fbcad174d --- /dev/null +++ b/docs/api-reference/endpoints/certificate-syncs/azure-key-vault/import-certificates.mdx @@ -0,0 +1,4 @@ +--- +title: "Import Certificates" +openapi: "POST /api/v1/pki-syncs/{pkiSyncId}/import" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/certificate-syncs/azure-key-vault/list.mdx b/docs/api-reference/endpoints/certificate-syncs/azure-key-vault/list.mdx new file mode 100644 index 000000000..baedf1688 --- /dev/null +++ b/docs/api-reference/endpoints/certificate-syncs/azure-key-vault/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/pki-syncs" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/certificate-syncs/azure-key-vault/remove-certificates.mdx b/docs/api-reference/endpoints/certificate-syncs/azure-key-vault/remove-certificates.mdx new file mode 100644 index 000000000..94969c823 --- /dev/null +++ b/docs/api-reference/endpoints/certificate-syncs/azure-key-vault/remove-certificates.mdx @@ -0,0 +1,4 @@ +--- +title: "Remove Certificates" +openapi: "POST /api/v1/pki-syncs/{pkiSyncId}/remove" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/certificate-syncs/azure-key-vault/sync-certificates.mdx b/docs/api-reference/endpoints/certificate-syncs/azure-key-vault/sync-certificates.mdx new file mode 100644 index 000000000..65ff9e22d --- /dev/null +++ b/docs/api-reference/endpoints/certificate-syncs/azure-key-vault/sync-certificates.mdx @@ -0,0 +1,4 @@ +--- +title: "Sync Certificates" +openapi: "POST /api/v1/pki-syncs/{pkiSyncId}/sync" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/certificate-syncs/azure-key-vault/update.mdx b/docs/api-reference/endpoints/certificate-syncs/azure-key-vault/update.mdx new file mode 100644 index 000000000..7067acea3 --- /dev/null +++ b/docs/api-reference/endpoints/certificate-syncs/azure-key-vault/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/pki-syncs/{pkiSyncId}" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/certificate-syncs/list.mdx b/docs/api-reference/endpoints/certificate-syncs/list.mdx new file mode 100644 index 000000000..3bfd8e0c5 --- /dev/null +++ b/docs/api-reference/endpoints/certificate-syncs/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List PKI Syncs" +openapi: "GET /api/v1/pki-syncs" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/certificate-syncs/options.mdx b/docs/api-reference/endpoints/certificate-syncs/options.mdx new file mode 100644 index 000000000..132cb26b9 --- /dev/null +++ b/docs/api-reference/endpoints/certificate-syncs/options.mdx @@ -0,0 +1,4 @@ +--- +title: "Options" +openapi: "GET /api/v1/pki-syncs/options" +--- \ No newline at end of file diff --git a/docs/docs.json b/docs/docs.json index 8b64210a0..c264c2ef1 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -711,6 +711,19 @@ "documentation/platform/pki/pki-issuer", "documentation/platform/pki/integration-guides/gloo-mesh" ] + }, + { + "group": "Certificate Syncs", + "pages": [ + "documentation/platform/pki/certificate-syncs", + "documentation/platform/pki/certificate-syncs/overview", + { + "group": "Syncs", + "pages": [ + "documentation/platform/pki/certificate-syncs/azure-key-vault" + ] + } + ] } ] } @@ -2475,6 +2488,26 @@ "api-reference/endpoints/pki-alerts/update", "api-reference/endpoints/pki-alerts/delete" ] + }, + { + "group": "Certificate Syncs", + "pages": [ + "api-reference/endpoints/certificate-syncs/list", + "api-reference/endpoints/certificate-syncs/options", + { + "group": "Azure Key Vault", + "pages": [ + "api-reference/endpoints/certificate-syncs/azure-key-vault/list", + "api-reference/endpoints/certificate-syncs/azure-key-vault/get-by-id", + "api-reference/endpoints/certificate-syncs/azure-key-vault/create", + "api-reference/endpoints/certificate-syncs/azure-key-vault/update", + "api-reference/endpoints/certificate-syncs/azure-key-vault/delete", + "api-reference/endpoints/certificate-syncs/azure-key-vault/sync-certificates", + "api-reference/endpoints/certificate-syncs/azure-key-vault/import-certificates", + "api-reference/endpoints/certificate-syncs/azure-key-vault/remove-certificates" + ] + } + ] } ] }, @@ -2614,9 +2647,9 @@ "href": "https://infisical.com" }, "api": { - "openapi": "https://app.infisical.com/api/docs/json", + "openapi": "https://5e8f77f30103.ngrok-free.app/api/docs/json", "mdx": { - "server": ["https://app.infisical.com"] + "server": ["https://5e8f77f30103.ngrok-free.app"] } }, "appearance": { diff --git a/docs/documentation/platform/pki/certificate-syncs.mdx b/docs/documentation/platform/pki/certificate-syncs.mdx new file mode 100644 index 000000000..51191035e --- /dev/null +++ b/docs/documentation/platform/pki/certificate-syncs.mdx @@ -0,0 +1,8 @@ +--- +sidebarTitle: "Explore Options" +description: "Browse and search through all available certificate syncs for Infisical PKI." +--- + +import { CertificateSyncsBrowser } from "/snippets/CertificateSyncsBrowser.jsx"; + + \ No newline at end of file diff --git a/docs/documentation/platform/pki/certificate-syncs/azure-key-vault.mdx b/docs/documentation/platform/pki/certificate-syncs/azure-key-vault.mdx new file mode 100644 index 000000000..f6f3c62de --- /dev/null +++ b/docs/documentation/platform/pki/certificate-syncs/azure-key-vault.mdx @@ -0,0 +1,136 @@ +--- +title: "Azure Key Vault" +description: "Learn how to configure an Azure Key Vault Certificate Sync for Infisical PKI." +--- + +**Prerequisites:** + + - Set up and configure a [Certificate Authority](/documentation/platform/pki/overview) + - Create an [Azure Key Vault Connection](/integrations/app-connections/azure-key-vault) + - Ensure your network security policies allow incoming requests from Infisical to this certificate sync provider, if network restrictions apply. + + + The Azure Key Vault Certificate Sync requires the following certificate permissions to be set on the user / service principal + for Infisical to sync certificates to Azure Key Vault: `certificates/list`, `certificates/get`, `certificates/import`, `certificates/delete`. + + Any role with these permissions would work such as the **Key Vault Certificates Officer** role. + + + + Certificates synced to Azure Key Vault will be stored as certificate objects, preserving both the certificate and private key components. + + + + + 1. Navigate to **Project** > **Integrations** and select the **Certificate Syncs** tab. Click on the **Add Sync** button. + ![Certificate Syncs Tab](/images/certificate-syncs/general/certificate-sync-tab.png) + + 2. Select the **Azure Key Vault** option. + ![Select Key Vault](/images/certificate-syncs/azure-key-vault/select-key-vault-option.png) + + 3. Configure the **Source** from where certificates should be retrieved, then click **Next**. + ![Configure Source](/images/certificate-syncs/azure-key-vault/vault-source.png) + + - **PKI Subscriber**: The PKI subscriber to retrieve certificates from. + + 4. Configure the **Destination** to where certificates should be deployed, then click **Next**. + ![Configure Destination](/images/certificate-syncs/azure-key-vault/vault-destination.png) + + - **Azure Connection**: The Azure Connection to authenticate with. + - **Vault Base URL**: The URL of your Azure Key Vault. +

+ + 5. Configure the **Sync Options** to specify how certificates should be synced, then click **Next**. + ![Configure Options](/images/certificate-syncs/azure-key-vault/vault-options.png) + + - **Auto-Sync Enabled**: If enabled, certificates will automatically be synced from the source PKI subscriber when changes occur. Disable to enforce manual syncing only. + - **Enable Certificate Removal**: If enabled, Infisical will remove expired certificates from the destination during sync operations. Disable this option if you intend to manage certificate cleanup manually. + + 6. Configure the **Details** of your Azure Key Vault Certificate Sync, then click **Next**. + ![Configure Details](/images/certificate-syncs/azure-key-vault/vault-details.png) + + - **Name**: The name of your sync. Must be slug-friendly. + - **Description**: An optional description for your sync. + + 7. Review your Azure Key Vault Certificate Sync configuration, then click **Create Sync**. + ![Confirm Configuration](/images/certificate-syncs/azure-key-vault/vault-review.png) + + 8. If enabled, your Azure Key Vault Certificate Sync will begin syncing your certificates to the destination endpoint. + ![Sync Certificates](/images/certificate-syncs/azure-key-vault/vault-synced.png) + + + + To create an **Azure Key Vault Certificate Sync**, make an API request to the [Create Azure Key Vault Certificate Sync](/api-reference/endpoints/certificate-syncs/azure-key-vault/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/pki-syncs \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-key-vault-cert-sync", + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "description": "an example certificate sync", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "subscriberId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "destination": "azure-key-vault", + "isAutoSyncEnabled": true, + "syncOptions": { + "canRemoveCertificates": true + }, + "destinationConfig": { + "vaultBaseUrl": "https://my-key-vault.vault.azure.net" + } + }' + ``` + + ### Sample response + + ```json Response + { + "pkiSync": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-key-vault-cert-sync", + "description": "an example certificate sync", + "destination": "azure-key-vault", + "isAutoSyncEnabled": true, + "destinationConfig": { + "vaultBaseUrl": "https://my-key-vault.vault.azure.net" + }, + "syncOptions": { + "canRemoveCertificates": true + }, + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "subscriberId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "createdAt": "2023-01-01T00:00:00.000Z", + "updatedAt": "2023-01-01T00:00:00.000Z" + } + } + ``` + + + +## Certificate Management + +Your Azure Key Vault Certificate Sync will: + +- **Automatic Deployment**: Deploy new certificates issued by your PKI subscriber to Azure Key Vault +- **Certificate Updates**: Update certificates in Azure Key Vault when renewals occur +- **Expiration Handling**: Optionally remove expired certificates from Azure Key Vault (if enabled) +- **Format Preservation**: Maintain certificate format and metadata during sync operations + + + Azure Key Vault Certificate Syncs support both automatic and manual synchronization modes. When auto-sync is enabled, certificates are automatically deployed as they are issued or renewed. + + +## Manual Certificate Import + +You can manually import existing certificates from your PKI subscriber to Azure Key Vault using the import certificates functionality. This is useful for: + +- Initial setup when you have existing certificates to migrate +- One-time imports of specific certificates +- Testing certificate sync configurations + +To manually import certificates, use the [Import Certificates](/api-reference/endpoints/certificate-syncs/azure-key-vault/import) API endpoint or the manual import option in the Infisical UI. \ No newline at end of file diff --git a/docs/documentation/platform/pki/certificate-syncs/overview.mdx b/docs/documentation/platform/pki/certificate-syncs/overview.mdx new file mode 100644 index 000000000..b20c7ff8e --- /dev/null +++ b/docs/documentation/platform/pki/certificate-syncs/overview.mdx @@ -0,0 +1,118 @@ +--- +sidebarTitle: "Overview" +description: "Learn how to sync certificates from Infisical PKI to third-party services." +--- + +Certificate Syncs enable you to sync certificates from Infisical PKI to third-party services using [App Connections](/integrations/app-connections/overview). + + + Certificate Syncs are designed to automatically deploy certificates issued by your Certificate Authority to external services, ensuring your certificates are always up-to-date across your infrastructure. + + +## Concept + +Certificate Syncs are a project-level resource used to sync certificates, via an [App Connection](/integrations/app-connections/overview), from a particular PKI subscriber (source) +to a third-party service (destination). When new certificates are issued or existing certificates are renewed, changes will automatically be propagated to the destination, ensuring +your certificates are always current. + +
+ +

+ + ```mermaid + %%{init: {'flowchart': {'curve': 'linear'} } }%% + graph LR + A[App Connection] + B[Certificate Sync] + C[Certificate 1] + D[Certificate 2] + E[Certificate 3] + F[Third-Party Service] + G[Certificate 1] + H[Certificate 2] + I[Certificate 3] + J[PKI Subscriber] + + B --> A + C --> J + D --> J + E --> J + A --> F + F --> G + F --> H + F --> I + J --> B + + classDef default fill:#ffffff,stroke:#666,stroke-width:2px,rx:10px,color:black + classDef connection fill:#FFF2B2,stroke:#E6C34A,stroke-width:2px,color:black,rx:15px + classDef certificate fill:#E6F4FF,stroke:#0096D6,stroke-width:2px,color:black,rx:15px + classDef sync fill:#F4FFE6,stroke:#96D600,stroke-width:2px,color:black,rx:15px + classDef service fill:#E6E6FF,stroke:#6B4E96,stroke-width:2px,color:black,rx:15px + classDef subscriber fill:#FFE6E6,stroke:#D63F3F,stroke-width:2px,color:black,rx:15px + + class A connection + class B sync + class C,D,E,G,H,I certificate + class F service + class J subscriber + ``` + +
+ +## Workflow + +Configuring a Certificate Sync requires three components: a source PKI subscriber to retrieve certificates from, +a destination endpoint to deploy certificates to, and configuration options to determine how your certificates +should be synced. Follow these steps to start syncing: + + + For step-by-step guides on syncing to a particular third-party service, refer to the Certificate Syncs section in the Navigation Bar. + + +1. Create App Connection: If you have not already done so, create an [App Connection](/integrations/app-connections/overview) +via the UI or API for the third-party service you intend to sync certificates to. + +2. Create Certificate Sync: Configure a Certificate Sync in the desired project by specifying the following parameters via the UI or API: + - Source: The PKI subscriber you wish to retrieve certificates from. + - Destination: The App Connection to utilize and the destination endpoint to deploy certificates to. These can vary between services. + - Options: Customize how certificates should be synced, such as whether or not certificates should be removed from the destination when they expire. + + + Certificate Syncs are the source of truth for connected third-party services. Any certificate, + including associated data, not present or managed by Infisical before syncing will be + overwritten, and changes made directly in the connected service outside of Infisical may also + be overwritten by future syncs. + + + + Some third-party services do not support removing expired certificates automatically. + + +3. Utilize Sync: Any new certificates issued or renewals from the source PKI subscriber will now automatically be propagated to the destination endpoint. + + + Infisical is continuously expanding its Certificate Sync third-party service support. If the service you need isn't available, + contact us at team@infisical.com to make a request. + + +## Certificate Management + +Certificate Syncs handle the full lifecycle of certificate management: + +- **Automatic Deployment**: New certificates are automatically deployed to configured destinations +- **Renewal Propagation**: Certificate renewals are seamlessly pushed to all connected services +- **Expiration Handling**: Expired certificates can be automatically removed from destinations (service-dependent) +- **Certificate Validation**: Certificates are validated before deployment to ensure integrity + +
+ ```mermaid + graph LR + A[Certificate Issued] -->|Deploy| B[Destination Service] + C[Certificate Renewed] -->|Update| B + D[Certificate Expired] -->|Remove| B + style B fill:#F4FFE6,stroke:#96D600,stroke-width:2px,color:black,rx:15px + style A fill:#E6F4FF,stroke:#0096D6,stroke-width:2px,color:black,rx:15px + style C fill:#E6F4FF,stroke:#0096D6,stroke-width:2px,color:black,rx:15px + style D fill:#FFE6E6,stroke:#D63F3F,stroke-width:2px,color:black,rx:15px + ``` +
\ No newline at end of file diff --git a/docs/images/certificate-syncs/azure-key-vault/select-key-vault-option.png b/docs/images/certificate-syncs/azure-key-vault/select-key-vault-option.png new file mode 100644 index 000000000..0a9093ee8 Binary files /dev/null and b/docs/images/certificate-syncs/azure-key-vault/select-key-vault-option.png differ diff --git a/docs/images/certificate-syncs/azure-key-vault/upgrade-path-tool.png b/docs/images/certificate-syncs/azure-key-vault/upgrade-path-tool.png new file mode 100644 index 000000000..a8a538aaf Binary files /dev/null and b/docs/images/certificate-syncs/azure-key-vault/upgrade-path-tool.png differ diff --git a/docs/images/certificate-syncs/azure-key-vault/vault-destination.png b/docs/images/certificate-syncs/azure-key-vault/vault-destination.png new file mode 100644 index 000000000..96295362b Binary files /dev/null and b/docs/images/certificate-syncs/azure-key-vault/vault-destination.png differ diff --git a/docs/images/certificate-syncs/azure-key-vault/vault-details.png b/docs/images/certificate-syncs/azure-key-vault/vault-details.png new file mode 100644 index 000000000..cc2ebc691 Binary files /dev/null and b/docs/images/certificate-syncs/azure-key-vault/vault-details.png differ diff --git a/docs/images/certificate-syncs/azure-key-vault/vault-options.png b/docs/images/certificate-syncs/azure-key-vault/vault-options.png new file mode 100644 index 000000000..980dad0ef Binary files /dev/null and b/docs/images/certificate-syncs/azure-key-vault/vault-options.png differ diff --git a/docs/images/certificate-syncs/azure-key-vault/vault-review.png b/docs/images/certificate-syncs/azure-key-vault/vault-review.png new file mode 100644 index 000000000..4853be086 Binary files /dev/null and b/docs/images/certificate-syncs/azure-key-vault/vault-review.png differ diff --git a/docs/images/certificate-syncs/azure-key-vault/vault-source.png b/docs/images/certificate-syncs/azure-key-vault/vault-source.png new file mode 100644 index 000000000..5ab8c5dc6 Binary files /dev/null and b/docs/images/certificate-syncs/azure-key-vault/vault-source.png differ diff --git a/docs/images/certificate-syncs/azure-key-vault/vault-synced.png b/docs/images/certificate-syncs/azure-key-vault/vault-synced.png new file mode 100644 index 000000000..2bacd1a7b Binary files /dev/null and b/docs/images/certificate-syncs/azure-key-vault/vault-synced.png differ diff --git a/docs/images/certificate-syncs/general/certificate-sync-tab.png b/docs/images/certificate-syncs/general/certificate-sync-tab.png new file mode 100644 index 000000000..f97a10aa3 Binary files /dev/null and b/docs/images/certificate-syncs/general/certificate-sync-tab.png differ diff --git a/frontend/src/components/pki-syncs/DeletePkiSyncModal.tsx b/frontend/src/components/pki-syncs/DeletePkiSyncModal.tsx index 4a71b4dee..c01b70e90 100644 --- a/frontend/src/components/pki-syncs/DeletePkiSyncModal.tsx +++ b/frontend/src/components/pki-syncs/DeletePkiSyncModal.tsx @@ -23,7 +23,8 @@ export const DeletePkiSyncModal = ({ isOpen, onOpenChange, pkiSync, onComplete } try { await deleteSync.mutateAsync({ syncId, - projectId + projectId, + destination }); createNotification({ diff --git a/frontend/src/components/pki-syncs/PkiSyncImportCertificatesModal.tsx b/frontend/src/components/pki-syncs/PkiSyncImportCertificatesModal.tsx index 39895c6ca..d2ed96ae0 100644 --- a/frontend/src/components/pki-syncs/PkiSyncImportCertificatesModal.tsx +++ b/frontend/src/components/pki-syncs/PkiSyncImportCertificatesModal.tsx @@ -24,7 +24,8 @@ const Content = ({ pkiSync, onComplete }: ContentProps) => { try { await triggerImportCertificates.mutateAsync({ syncId, - projectId + projectId, + destination }); createNotification({ diff --git a/frontend/src/components/pki-syncs/PkiSyncRemoveCertificatesModal.tsx b/frontend/src/components/pki-syncs/PkiSyncRemoveCertificatesModal.tsx index 2a96bd902..deb22cf68 100644 --- a/frontend/src/components/pki-syncs/PkiSyncRemoveCertificatesModal.tsx +++ b/frontend/src/components/pki-syncs/PkiSyncRemoveCertificatesModal.tsx @@ -24,7 +24,8 @@ const Content = ({ pkiSync, onComplete }: ContentProps) => { try { await triggerRemoveCertificates.mutateAsync({ syncId, - projectId + projectId, + destination }); createNotification({ diff --git a/frontend/src/components/pki-syncs/forms/CreatePkiSyncForm.tsx b/frontend/src/components/pki-syncs/forms/CreatePkiSyncForm.tsx index b7cfe5018..f9def71c4 100644 --- a/frontend/src/components/pki-syncs/forms/CreatePkiSyncForm.tsx +++ b/frontend/src/components/pki-syncs/forms/CreatePkiSyncForm.tsx @@ -10,13 +10,7 @@ import { createNotification } from "@app/components/notifications"; import { Button, FormControl, Switch } from "@app/components/v2"; import { useProject } from "@app/context"; import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs"; -import { - PkiSync, - TCreatePkiSyncDTO, - TPkiSync, - useCreatePkiSync, - usePkiSyncOption -} from "@app/hooks/api/pkiSyncs"; +import { PkiSync, TPkiSync, useCreatePkiSync, usePkiSyncOption } from "@app/hooks/api/pkiSyncs"; import { PkiSyncDestinationFields } from "./PkiSyncDestinationFields"; import { PkiSyncDetailsFields } from "./PkiSyncDetailsFields"; @@ -69,10 +63,7 @@ export const CreatePkiSyncForm = ({ destination, onComplete, onCancel }: Props) ...formData, connectionId: connection.id, projectId: currentProject.id, - destinationConfig: { - destination, - config: destinationConfig - } as unknown as TCreatePkiSyncDTO["destinationConfig"] + destinationConfig }); createNotification({ diff --git a/frontend/src/components/pki-syncs/forms/EditPkiSyncForm.tsx b/frontend/src/components/pki-syncs/forms/EditPkiSyncForm.tsx index 58fcc512c..b01afa96c 100644 --- a/frontend/src/components/pki-syncs/forms/EditPkiSyncForm.tsx +++ b/frontend/src/components/pki-syncs/forms/EditPkiSyncForm.tsx @@ -43,7 +43,8 @@ export const EditPkiSyncForm = ({ pkiSync, fields, onComplete }: Props) => { syncId: pkiSync.id, ...formData, connectionId: connection.id, - projectId: pkiSync.projectId + projectId: pkiSync.projectId, + destination: pkiSync.destination }); createNotification({ diff --git a/frontend/src/components/pki-syncs/forms/PkiSyncOptionsFields/PkiSyncOptionsFields.tsx b/frontend/src/components/pki-syncs/forms/PkiSyncOptionsFields/PkiSyncOptionsFields.tsx index f98010316..332306978 100644 --- a/frontend/src/components/pki-syncs/forms/PkiSyncOptionsFields/PkiSyncOptionsFields.tsx +++ b/frontend/src/components/pki-syncs/forms/PkiSyncOptionsFields/PkiSyncOptionsFields.tsx @@ -12,6 +12,8 @@ export const PkiSyncOptionsFields = () => { return ( <>

Configure how certificates should be synced.

+ {/* + TODO: Re-enable this when we have a way to import certificates { )} /> + */} { Always Enabled + {/* Hidden for now - Import certificates functionality disabled {syncOptions?.canImportCertificates !== undefined && ( @@ -77,6 +78,7 @@ export const PkiSyncReviewFields = () => { )} + */} {syncOptions?.canRemoveCertificates !== undefined && ( diff --git a/frontend/src/components/pki-syncs/forms/schemas.ts b/frontend/src/components/pki-syncs/forms/schemas.ts index d31af7d00..da5586cbe 100644 --- a/frontend/src/components/pki-syncs/forms/schemas.ts +++ b/frontend/src/components/pki-syncs/forms/schemas.ts @@ -20,7 +20,7 @@ export const PkiSyncFormSchema = z.object({ vaultBaseUrl: z.string().url("Valid URL is required") }), syncOptions: z.object({ - canImportCertificates: z.boolean().default(true), + canImportCertificates: z.boolean().default(false), canRemoveCertificates: z.boolean().default(true) }) }); diff --git a/frontend/src/hooks/api/pkiSyncs/mutations.tsx b/frontend/src/hooks/api/pkiSyncs/mutations.tsx index cb1950f39..a6f77e065 100644 --- a/frontend/src/hooks/api/pkiSyncs/mutations.tsx +++ b/frontend/src/hooks/api/pkiSyncs/mutations.tsx @@ -15,8 +15,11 @@ import { export const useCreatePkiSync = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async (params: TCreatePkiSyncDTO) => { - const { data } = await apiRequest.post("/api/v1/pki-syncs", params); + mutationFn: async ({ destination, ...params }: TCreatePkiSyncDTO) => { + const { data } = await apiRequest.post( + `/api/v1/pki-syncs/${destination}`, + params + ); return data.pkiSync; }, @@ -28,9 +31,9 @@ export const useCreatePkiSync = () => { export const useUpdatePkiSync = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ syncId, projectId, ...params }: TUpdatePkiSyncDTO) => { + mutationFn: async ({ syncId, projectId, destination, ...params }: TUpdatePkiSyncDTO) => { const { data } = await apiRequest.patch( - `/api/v1/pki-syncs/${syncId}`, + `/api/v1/pki-syncs/${destination}/${syncId}`, params, { params: { projectId } } ); @@ -47,8 +50,8 @@ export const useUpdatePkiSync = () => { export const useDeletePkiSync = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ syncId, projectId }: TDeletePkiSyncDTO) => { - const { data } = await apiRequest.delete(`/api/v1/pki-syncs/${syncId}`, { + mutationFn: async ({ syncId, projectId, destination }: TDeletePkiSyncDTO) => { + const { data } = await apiRequest.delete(`/api/v1/pki-syncs/${destination}/${syncId}`, { params: { projectId } }); @@ -64,9 +67,9 @@ export const useDeletePkiSync = () => { export const useTriggerPkiSyncSyncCertificates = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ syncId, projectId }: TTriggerPkiSyncSyncCertificatesDTO) => { + mutationFn: async ({ syncId, projectId, destination }: TTriggerPkiSyncSyncCertificatesDTO) => { const { data } = await apiRequest.post( - `/api/v1/pki-syncs/${syncId}/sync`, + `/api/v1/pki-syncs/${destination}/${syncId}/sync`, {}, { params: { projectId } @@ -85,9 +88,13 @@ export const useTriggerPkiSyncSyncCertificates = () => { export const useTriggerPkiSyncImportCertificates = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ syncId, projectId }: TTriggerPkiSyncImportCertificatesDTO) => { + mutationFn: async ({ + syncId, + projectId, + destination + }: TTriggerPkiSyncImportCertificatesDTO) => { const { data } = await apiRequest.post( - `/api/v1/pki-syncs/${syncId}/import`, + `/api/v1/pki-syncs/${destination}/${syncId}/import`, {}, { params: { projectId } @@ -106,9 +113,13 @@ export const useTriggerPkiSyncImportCertificates = () => { export const useTriggerPkiSyncRemoveCertificates = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ syncId, projectId }: TTriggerPkiSyncRemoveCertificatesDTO) => { + mutationFn: async ({ + syncId, + projectId, + destination + }: TTriggerPkiSyncRemoveCertificatesDTO) => { const { data } = await apiRequest.post( - `/api/v1/pki-syncs/${syncId}/remove`, + `/api/v1/pki-syncs/${destination}/${syncId}/remove`, {}, { params: { projectId } diff --git a/frontend/src/hooks/api/pkiSyncs/types/index.ts b/frontend/src/hooks/api/pkiSyncs/types/index.ts index 2ab2ada34..4702b97f9 100644 --- a/frontend/src/hooks/api/pkiSyncs/types/index.ts +++ b/frontend/src/hooks/api/pkiSyncs/types/index.ts @@ -29,29 +29,34 @@ export type TCreatePkiSyncDTO = DiscriminativePick< | "isAutoSyncEnabled" > & { subscriberId?: string; projectId: string }; -export type TUpdatePkiSyncDTO = Partial> & { +export type TUpdatePkiSyncDTO = Partial> & { syncId: string; projectId: string; + destination: PkiSync; }; export type TDeletePkiSyncDTO = { syncId: string; projectId: string; + destination: PkiSync; }; export type TTriggerPkiSyncSyncCertificatesDTO = { syncId: string; projectId: string; + destination: PkiSync; }; export type TTriggerPkiSyncImportCertificatesDTO = { syncId: string; projectId: string; + destination: PkiSync; }; export type TTriggerPkiSyncRemoveCertificatesDTO = { syncId: string; projectId: string; + destination: PkiSync; }; export * from "./common"; diff --git a/frontend/src/pages/cert-manager/IntegrationsListPage/components/PkiSyncsTab/PkiSyncTable/PkiSyncRow.tsx b/frontend/src/pages/cert-manager/IntegrationsListPage/components/PkiSyncsTab/PkiSyncTable/PkiSyncRow.tsx index 750934cc5..af9b4af8a 100644 --- a/frontend/src/pages/cert-manager/IntegrationsListPage/components/PkiSyncsTab/PkiSyncTable/PkiSyncRow.tsx +++ b/frontend/src/pages/cert-manager/IntegrationsListPage/components/PkiSyncsTab/PkiSyncTable/PkiSyncRow.tsx @@ -44,7 +44,7 @@ import { ProjectPermissionSub } from "@app/context"; import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionContext/types"; import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs"; import { useToggle } from "@app/hooks"; -import { PkiSyncData, PkiSyncStatus, usePkiSyncOption } from "@app/hooks/api/pkiSyncs"; +import { PkiSyncData, PkiSyncStatus } from "@app/hooks/api/pkiSyncs"; import { PkiSyncDestinationCol } from "./PkiSyncDestinationCol"; import { PkiSyncTableCell } from "./PkiSyncTableCell"; @@ -80,8 +80,6 @@ export const PkiSyncRow = ({ projectId } = pkiSync; - const { syncOption } = usePkiSyncOption(destination); - const destinationName = PKI_SYNC_MAP[destination].name; const [isIdCopied, setIsIdCopied] = useToggle(false); @@ -296,38 +294,36 @@ export const PkiSyncRow = ({ )} - {syncOption?.canImportCertificates && ( - - {(isAllowed: boolean) => ( - } - onClick={(e) => { - e.stopPropagation(); - onTriggerImportCertificates(pkiSync); - }} - isDisabled={!isAllowed} + + {(isAllowed: boolean) => ( + } + onClick={(e) => { + e.stopPropagation(); + onTriggerImportCertificates(pkiSync); + }} + isDisabled={!isAllowed} + > + - -
- Import Certificates - -
-
-
- )} -
- )} +
+ Import Certificates + +
+ +
+ )} +
{ await updateSync.mutateAsync({ syncId: pkiSync.id, projectId: pkiSync.projectId, + destination: pkiSync.destination, isAutoSyncEnabled }); @@ -251,7 +252,8 @@ export const PkiSyncsTable = ({ pkiSyncs }: Props) => { try { await triggerSync.mutateAsync({ syncId: pkiSync.id, - projectId: pkiSync.projectId + projectId: pkiSync.projectId, + destination: pkiSync.destination }); createNotification({ diff --git a/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncActionTriggers.tsx b/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncActionTriggers.tsx index 56109bc49..8c8605ef9 100644 --- a/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncActionTriggers.tsx +++ b/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncActionTriggers.tsx @@ -42,7 +42,6 @@ import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs"; import { usePopUp, useToggle } from "@app/hooks"; import { TPkiSync, - usePkiSyncOption, useTriggerPkiSyncSyncCertificates, useUpdatePkiSync } from "@app/hooks/api/pkiSyncs"; @@ -69,7 +68,6 @@ export const PkiSyncActionTriggers = ({ pkiSync }: Props) => { const updatePkiSyncMutation = useUpdatePkiSync(); const destinationName = PKI_SYNC_MAP[destination].name; - const { syncOption } = usePkiSyncOption(destination); const handleCopyId = useCallback(() => { setIsIdCopied.on(); @@ -88,7 +86,8 @@ export const PkiSyncActionTriggers = ({ pkiSync }: Props) => { try { await triggerSyncMutation.mutateAsync({ syncId: id, - projectId + projectId, + destination }); createNotification({ text: "PKI sync job queued successfully", @@ -108,6 +107,7 @@ export const PkiSyncActionTriggers = ({ pkiSync }: Props) => { await updatePkiSyncMutation.mutateAsync({ syncId: id, projectId, + destination, isAutoSyncEnabled: !pkiSync.isAutoSyncEnabled }); createNotification({ @@ -192,65 +192,61 @@ export const PkiSyncActionTriggers = ({ pkiSync }: Props) => { Copy Sync ID - {syncOption?.canImportCertificates && ( - - {(isAllowed: boolean) => ( - } - onClick={() => handlePopUpOpen("importCertificates")} - isDisabled={!isAllowed} + + {(isAllowed: boolean) => ( + } + onClick={() => handlePopUpOpen("importCertificates")} + isDisabled={!isAllowed} + > + - -
- Import Certificates - -
-
-
- )} -
- )} +
+ Import Certificates + +
+ +
+ )} +
- {syncOption?.canRemoveCertificates && ( - - {(isAllowed: boolean) => ( - } - onClick={() => handlePopUpOpen("removeCertificates")} - isDisabled={!isAllowed} + + {(isAllowed: boolean) => ( + } + onClick={() => handlePopUpOpen("removeCertificates")} + isDisabled={!isAllowed} + > + - -
- Remove Certificates - -
-
-
- )} -
- )} +
+ Remove Certificates + +
+ +
+ )} +
{(isAllowed: boolean) => ( diff --git a/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncOptionsSection/PkiSyncOptionsSection.tsx b/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncOptionsSection/PkiSyncOptionsSection.tsx index 53b7edb2d..020e803cd 100644 --- a/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncOptionsSection/PkiSyncOptionsSection.tsx +++ b/frontend/src/pages/cert-manager/PkiSyncDetailsByIDPage/components/PkiSyncOptionsSection/PkiSyncOptionsSection.tsx @@ -16,7 +16,7 @@ type Props = { export const PkiSyncOptionsSection = ({ pkiSync, onEditOptions }: Props) => { const { - syncOptions: { canImportCertificates, canRemoveCertificates } + syncOptions: { canRemoveCertificates } } = pkiSync; const permissionSubject = subject(ProjectPermissionSub.PkiSyncs, { @@ -44,11 +44,13 @@ export const PkiSyncOptionsSection = ({ pkiSync, onEditOptions }: Props) => {
+ {/* Hidden for now - Import certificates functionality disabled {canImportCertificates ? "Enabled" : "Disabled"} + */} {canRemoveCertificates ? "Enabled" : "Disabled"}