PKI Syncs revamp

This commit is contained in:
Carlos Monastyrski
2025-10-29 13:01:27 -03:00
parent 3e83ae95d1
commit 39eecb328d
64 changed files with 2799 additions and 350 deletions
+8
View File
@@ -62,6 +62,9 @@ import {
TCertificateSecretsUpdate,
TCertificatesInsert,
TCertificatesUpdate,
TCertificateSyncs,
TCertificateSyncsInsert,
TCertificateSyncsUpdate,
TCertificateTemplateEstConfigs,
TCertificateTemplateEstConfigsInsert,
TCertificateTemplateEstConfigsUpdate,
@@ -738,6 +741,11 @@ declare module "knex/types/tables" {
TPkiSubscribersUpdate
>;
[TableName.PkiSync]: KnexOriginal.CompositeTableType<TPkiSyncs, TPkiSyncsInsert, TPkiSyncsUpdate>;
[TableName.CertificateSync]: KnexOriginal.CompositeTableType<
TCertificateSyncs,
TCertificateSyncsInsert,
TCertificateSyncsUpdate
>;
[TableName.UserGroupMembership]: KnexOriginal.CompositeTableType<
TUserGroupMembership,
TUserGroupMembershipInsert,
@@ -0,0 +1,35 @@
import { Knex } from "knex";
import { TableName } from "@app/db/schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "@app/db/utils";
import { CertificateSyncStatus } from "@app/services/certificate-sync/certificate-sync-enums";
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasTable(TableName.CertificateSync))) {
await knex.schema.createTable(TableName.CertificateSync, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.uuid("pkiSyncId").notNullable();
t.foreign("pkiSyncId").references("id").inTable(TableName.PkiSync).onDelete("CASCADE");
t.uuid("certificateId").notNullable();
t.foreign("certificateId").references("id").inTable(TableName.Certificate).onDelete("CASCADE");
t.string("syncStatus").defaultTo(CertificateSyncStatus.Pending);
t.text("lastSyncMessage");
t.datetime("lastSyncedAt");
t.timestamps(true, true, true);
// Ensure unique combination of pki sync and certificate
t.unique(["pkiSyncId", "certificateId"]);
t.index("pkiSyncId");
t.index("certificateId");
t.index("syncStatus");
});
await createOnUpdateTrigger(knex, TableName.CertificateSync);
}
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.CertificateSync);
await dropOnUpdateTrigger(knex, TableName.CertificateSync);
}
@@ -0,0 +1,23 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const CertificateSyncsSchema = z.object({
id: z.string().uuid(),
pkiSyncId: z.string().uuid(),
certificateId: z.string().uuid(),
syncStatus: z.string().default("pending").nullable().optional(),
lastSyncMessage: z.string().nullable().optional(),
lastSyncedAt: z.date().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TCertificateSyncs = z.infer<typeof CertificateSyncsSchema>;
export type TCertificateSyncsInsert = Omit<z.input<typeof CertificateSyncsSchema>, TImmutableDBKeys>;
export type TCertificateSyncsUpdate = Partial<Omit<z.input<typeof CertificateSyncsSchema>, TImmutableDBKeys>>;
+1
View File
@@ -17,6 +17,7 @@ export * from "./certificate-authority-crl";
export * from "./certificate-authority-secret";
export * from "./certificate-bodies";
export * from "./certificate-secrets";
export * from "./certificate-syncs";
export * from "./certificate-template-est-configs";
export * from "./certificate-templates";
export * from "./certificates";
+1
View File
@@ -161,6 +161,7 @@ export enum TableName {
AppConnection = "app_connections",
SecretSync = "secret_syncs",
PkiSync = "pki_syncs",
CertificateSync = "certificate_syncs",
KmipClient = "kmip_clients",
KmipOrgConfig = "kmip_org_configs",
KmipOrgServerCertificates = "kmip_org_server_certificates",
+11 -3
View File
@@ -172,6 +172,7 @@ import { internalCertificateAuthorityServiceFactory } from "@app/services/certif
import { certificateEstV3ServiceFactory } from "@app/services/certificate-est-v3/certificate-est-v3-service";
import { certificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal";
import { certificateProfileServiceFactory } from "@app/services/certificate-profile/certificate-profile-service";
import { certificateSyncDALFactory } from "@app/services/certificate-sync/certificate-sync-dal";
import { certificateTemplateDALFactory } from "@app/services/certificate-template/certificate-template-dal";
import { certificateTemplateEstConfigDALFactory } from "@app/services/certificate-template/certificate-template-est-config-dal";
import { certificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
@@ -1060,6 +1061,7 @@ export const registerRoutes = async (
const certificateDAL = certificateDALFactory(db);
const certificateBodyDAL = certificateBodyDALFactory(db);
const certificateSecretDAL = certificateSecretDALFactory(db);
const certificateSyncDAL = certificateSyncDALFactory(db);
const pkiAlertDAL = pkiAlertDALFactory(db);
const pkiCollectionDAL = pkiCollectionDALFactory(db);
@@ -2020,7 +2022,8 @@ export const registerRoutes = async (
certificateBodyDAL,
certificateSecretDAL,
certificateAuthorityDAL,
certificateAuthorityCertDAL
certificateAuthorityCertDAL,
certificateSyncDAL
});
const pkiSyncCleanup = pkiSyncCleanupQueueServiceFactory({
@@ -2131,6 +2134,7 @@ export const registerRoutes = async (
permissionService,
pkiCollectionDAL,
pkiCollectionItemDAL,
certificateSyncDAL,
pkiSyncDAL,
pkiSyncQueue
});
@@ -2142,7 +2146,10 @@ export const registerRoutes = async (
certificateProfileDAL,
certificateTemplateV2Service,
internalCaService: internalCertificateAuthorityService,
permissionService
permissionService,
certificateSyncDAL,
pkiSyncDAL,
pkiSyncQueue
});
const certificateV3Queue = certificateV3QueueServiceFactory({
@@ -2188,7 +2195,8 @@ export const registerRoutes = async (
appConnectionService,
permissionService,
licenseService,
pkiSyncQueue
pkiSyncQueue,
certificateSyncDAL
});
const pkiTemplateService = pkiTemplatesServiceFactory({
@@ -26,7 +26,7 @@ export const registerSyncPkiEndpoints = ({
syncOptions?: Record<string, unknown>;
description?: string;
isAutoSyncEnabled?: boolean;
subscriberId?: string;
subscriberId?: string | null;
}>;
updateSchema: z.ZodType<{
connectionId?: string;
@@ -35,7 +35,7 @@ export const registerSyncPkiEndpoints = ({
syncOptions?: Record<string, unknown>;
description?: string;
isAutoSyncEnabled?: boolean;
subscriberId?: string;
subscriberId?: string | null;
}>;
responseSchema: z.ZodTypeAny;
syncOptions: {
@@ -2,10 +2,11 @@ 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 { readLimit, writeLimit } 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 { CertificateSyncStatus } from "@app/services/certificate-sync/certificate-sync-enums";
import { PkiSync } from "@app/services/pki-sync/pki-sync-enums";
const PkiSyncSchema = z.object({
@@ -60,7 +61,8 @@ const PkiSyncSchema = z.object({
name: z.string()
})
.nullable()
.optional()
.optional(),
hasCertificate: z.boolean().optional()
});
const PkiSyncOptionsSchema = z.object({
@@ -76,6 +78,24 @@ const PkiSyncOptionsSchema = z.object({
minCertificateNameLength: z.number().optional()
});
const PkiSyncCertificateSchema = z.object({
id: z.string().uuid(),
pkiSyncId: z.string().uuid(),
certificateId: z.string().uuid(),
syncStatus: z.nativeEnum(CertificateSyncStatus),
lastSyncMessage: z.string().nullable().optional(),
lastSyncedAt: z.date().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date(),
certificateSerialNumber: z.string().optional(),
certificateCommonName: z.string().optional(),
certificateStatus: z.string().optional(),
certificateNotBefore: z.date().optional(),
certificateNotAfter: z.date().optional(),
pkiSyncName: z.string().optional(),
pkiSyncDestination: z.string().optional()
});
export const registerPkiSyncRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
@@ -111,7 +131,8 @@ export const registerPkiSyncRouter = async (server: FastifyZodProvider) => {
tags: [ApiDocsTags.PkiSyncs],
description: "List all the PKI Syncs for the specified project.",
querystring: z.object({
projectId: z.string().trim().min(1)
projectId: z.string().trim().min(1),
certificateId: z.string().uuid().optional()
}),
response: {
200: z.object({ pkiSyncs: PkiSyncSchema.array() })
@@ -120,11 +141,11 @@ export const registerPkiSyncRouter = async (server: FastifyZodProvider) => {
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const {
query: { projectId },
query: { projectId, certificateId },
permission
} = req;
const pkiSyncs = await server.services.pkiSync.listPkiSyncsByProjectId({ projectId }, permission);
const pkiSyncs = await server.services.pkiSync.listPkiSyncsByProjectId({ projectId, certificateId }, permission);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
@@ -179,4 +200,167 @@ export const registerPkiSyncRouter = async (server: FastifyZodProvider) => {
return pkiSync;
}
});
server.route({
method: "GET",
url: "/:pkiSyncId/certificates",
config: {
rateLimit: readLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiSyncs],
description: "List all certificates associated with a PKI Sync.",
params: z.object({
pkiSyncId: z.string().uuid()
}),
querystring: z.object({
offset: z.coerce.number().min(0).default(0),
limit: z.coerce.number().min(1).max(100).default(20)
}),
response: {
200: z.object({
certificates: PkiSyncCertificateSchema.array(),
totalCount: z.number()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { pkiSyncId } = req.params;
const { offset, limit } = req.query;
const result = await server.services.pkiSync.listPkiSyncCertificates(
{ pkiSyncId, offset, limit },
req.permission
);
const pkiSync = await server.services.pkiSync.findPkiSyncById({ id: pkiSyncId }, req.permission);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: pkiSync.projectId,
event: {
type: EventType.GET_PKI_SYNC,
metadata: {
syncId: pkiSyncId,
destination: pkiSync.destination
}
}
});
return result;
}
});
server.route({
method: "POST",
url: "/:pkiSyncId/certificates",
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiSyncs],
description: "Add certificates to a PKI Sync.",
params: z.object({
pkiSyncId: z.string().uuid()
}),
body: z.object({
certificateIds: z.array(z.string().uuid()).min(1, "At least one certificate ID is required")
}),
response: {
200: z.object({
addedCertificates: z.array(
z.object({
id: z.string().uuid(),
pkiSyncId: z.string().uuid(),
certificateId: z.string().uuid(),
syncStatus: z.string().default("pending").optional().nullable(),
lastSyncMessage: z.string().optional().nullable(),
lastSyncedAt: z.date().optional().nullable(),
createdAt: z.date(),
updatedAt: z.date()
})
)
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { pkiSyncId } = req.params;
const { certificateIds } = req.body;
const addedCertificates = await server.services.pkiSync.addCertificatesToPkiSync(
{ pkiSyncId, certificateIds },
req.permission
);
const pkiSync = await server.services.pkiSync.findPkiSyncById({ id: pkiSyncId }, req.permission);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: pkiSync.projectId,
event: {
type: EventType.UPDATE_PKI_SYNC,
metadata: {
pkiSyncId,
name: pkiSync.name
}
}
});
return { addedCertificates };
}
});
server.route({
method: "DELETE",
url: "/:pkiSyncId/certificates",
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiSyncs],
description: "Remove certificates from a PKI Sync.",
params: z.object({
pkiSyncId: z.string().uuid()
}),
body: z.object({
certificateIds: z.array(z.string().uuid()).min(1, "At least one certificate ID is required")
}),
response: {
200: z.object({
removedCount: z.number()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { pkiSyncId } = req.params;
const { certificateIds } = req.body;
const result = await server.services.pkiSync.removeCertificatesFromPkiSync(
{ pkiSyncId, certificateIds },
req.permission
);
const pkiSync = await server.services.pkiSync.findPkiSyncById({ id: pkiSyncId }, req.permission);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: pkiSync.projectId,
event: {
type: EventType.UPDATE_PKI_SYNC,
metadata: {
pkiSyncId,
name: pkiSync.name
}
}
});
return result;
}
});
};
@@ -1195,8 +1195,13 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
querystring: z.object({
friendlyName: z.string().optional().describe(PROJECTS.LIST_CERTIFICATES.friendlyName),
commonName: z.string().optional().describe(PROJECTS.LIST_CERTIFICATES.commonName),
offset: z.coerce.number().min(0).max(100).default(0).describe(PROJECTS.LIST_CERTIFICATES.offset),
limit: z.coerce.number().min(1).max(100).default(25).describe(PROJECTS.LIST_CERTIFICATES.limit)
offset: z.coerce.number().min(0).default(0).describe(PROJECTS.LIST_CERTIFICATES.offset),
limit: z.coerce.number().min(1).max(100).default(25).describe(PROJECTS.LIST_CERTIFICATES.limit),
forPkiSync: z.coerce
.boolean()
.default(false)
.optional()
.describe("Retrieve only certificates available for PKI sync")
}),
response: {
200: z.object({
@@ -0,0 +1,259 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName, TCertificateSyncs } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { buildFindFilter, ormify, selectAllTableCols } from "@app/lib/knex";
import { CertificateSyncStatus } from "./certificate-sync-enums";
export type TCertificateSyncDALFactory = ReturnType<typeof certificateSyncDALFactory>;
type CertificateSyncFindFilter = Parameters<typeof buildFindFilter<TCertificateSyncs>>[0];
export const certificateSyncDALFactory = (db: TDbClient) => {
const certificateSyncOrm = ormify(db, TableName.CertificateSync);
const findByPkiSyncId = async (pkiSyncId: string, tx?: Knex) => {
try {
const docs = await (tx || db.replicaNode())(TableName.CertificateSync)
.where({ pkiSyncId })
.select(selectAllTableCols(TableName.CertificateSync));
return docs;
} catch (error) {
throw new DatabaseError({ error, name: "FindByPkiSyncId" });
}
};
const findByCertificateId = async (certificateId: string, tx?: Knex) => {
try {
const docs = await (tx || db.replicaNode())(TableName.CertificateSync)
.where({ certificateId })
.select(selectAllTableCols(TableName.CertificateSync));
return docs;
} catch (error) {
throw new DatabaseError({ error, name: "FindByCertificateId" });
}
};
const findByPkiSyncAndCertificate = async (pkiSyncId: string, certificateId: string, tx?: Knex) => {
try {
const doc = await (tx || db.replicaNode())(TableName.CertificateSync)
.where({ pkiSyncId, certificateId })
.select(selectAllTableCols(TableName.CertificateSync))
.first();
return doc;
} catch (error) {
throw new DatabaseError({ error, name: "FindByPkiSyncAndCertificate" });
}
};
const findCertificateIdsByPkiSyncId = async (pkiSyncId: string, tx?: Knex): Promise<string[]> => {
try {
const docs = (await (tx || db.replicaNode())(TableName.CertificateSync)
.where({ pkiSyncId })
.select("certificateId")) as Array<{ certificateId: string }>;
return docs.map((doc) => doc.certificateId);
} catch (error) {
throw new DatabaseError({ error, name: "FindCertificateIdsByPkiSyncId" });
}
};
const findPkiSyncIdsByCertificateId = async (certificateId: string, tx?: Knex): Promise<string[]> => {
try {
const docs = (await (tx || db.replicaNode())(TableName.CertificateSync)
.where({ certificateId })
.select("pkiSyncId")) as Array<{ pkiSyncId: string }>;
return docs.map((doc) => doc.pkiSyncId);
} catch (error) {
throw new DatabaseError({ error, name: "FindPkiSyncIdsByCertificateId" });
}
};
const addCertificates = async (
pkiSyncId: string,
certificateIds: string[],
tx?: Knex
): Promise<TCertificateSyncs[]> => {
try {
const insertData = certificateIds.map((certificateId) => ({
pkiSyncId,
certificateId,
syncStatus: CertificateSyncStatus.Pending
}));
const docs = await (tx || db)(TableName.CertificateSync).insert(insertData).returning("*");
return docs;
} catch (error) {
throw new DatabaseError({ error, name: "AddCertificates" });
}
};
const removeCertificates = async (pkiSyncId: string, certificateIds: string[], tx?: Knex): Promise<number> => {
try {
const deletedCount = await (tx || db)(TableName.CertificateSync)
.where({ pkiSyncId })
.whereIn("certificateId", certificateIds)
.del();
return deletedCount;
} catch (error) {
throw new DatabaseError({ error, name: "RemoveCertificates" });
}
};
const removeAllCertificatesFromSync = async (pkiSyncId: string, tx?: Knex): Promise<number> => {
try {
const deletedCount = await (tx || db)(TableName.CertificateSync).where({ pkiSyncId }).del();
return deletedCount;
} catch (error) {
throw new DatabaseError({ error, name: "RemoveAllCertificatesFromSync" });
}
};
const updateSyncStatus = async (
pkiSyncId: string,
certificateId: string,
status: string,
message?: string,
tx?: Knex
): Promise<TCertificateSyncs | undefined> => {
try {
const updateData: Partial<TCertificateSyncs> = {
syncStatus: status,
lastSyncedAt: new Date()
};
if (message !== undefined) {
updateData.lastSyncMessage = message;
}
const docs = await (tx || db)(TableName.CertificateSync)
.where({ pkiSyncId, certificateId })
.update(updateData)
.returning("*");
return docs[0];
} catch (error) {
throw new DatabaseError({ error, name: "UpdateSyncStatus" });
}
};
const bulkUpdateSyncStatus = async (
updates: Array<{
pkiSyncId: string;
certificateId: string;
status: string;
message?: string;
}>,
tx?: Knex
): Promise<void> => {
try {
if (tx) {
for (const update of updates) {
// eslint-disable-next-line no-await-in-loop
await updateSyncStatus(update.pkiSyncId, update.certificateId, update.status, update.message, tx);
}
} else {
await certificateSyncOrm.transaction(async (trx) => {
for (const update of updates) {
// eslint-disable-next-line no-await-in-loop
await updateSyncStatus(update.pkiSyncId, update.certificateId, update.status, update.message, trx);
}
});
}
} catch (error) {
throw new DatabaseError({ error, name: "BulkUpdateSyncStatus" });
}
};
const findWithDetails = async (
options: {
filter?: CertificateSyncFindFilter;
pkiSyncId?: string;
offset?: number;
limit?: number;
},
tx?: Knex
): Promise<{
certificateDetails: (TCertificateSyncs & {
certificateSerialNumber?: string;
certificateCommonName?: string;
certificateStatus?: string;
certificateNotBefore?: Date;
certificateNotAfter?: Date;
pkiSyncName?: string;
pkiSyncDestination?: string;
})[];
totalCount: number;
}> => {
try {
const { filter, pkiSyncId, offset, limit } = options;
const baseQuery = (tx || db.replicaNode())(TableName.CertificateSync)
.leftJoin(TableName.Certificate, `${TableName.CertificateSync}.certificateId`, `${TableName.Certificate}.id`)
.leftJoin(TableName.PkiSync, `${TableName.CertificateSync}.pkiSyncId`, `${TableName.PkiSync}.id`);
if (filter) {
// eslint-disable-next-line @typescript-eslint/no-misused-promises
void baseQuery.where(buildFindFilter(filter));
}
if (pkiSyncId) {
void baseQuery.where(`${TableName.CertificateSync}.pkiSyncId`, pkiSyncId);
}
const countResult = await baseQuery.clone().count("* as count");
const totalCount = Number((countResult[0] as unknown as { count: string | number }).count);
const query = baseQuery
.select(selectAllTableCols(TableName.CertificateSync))
.select(
db.ref("serialNumber").withSchema(TableName.Certificate).as("certificateSerialNumber"),
db.ref("commonName").withSchema(TableName.Certificate).as("certificateCommonName"),
db.ref("status").withSchema(TableName.Certificate).as("certificateStatus"),
db.ref("notBefore").withSchema(TableName.Certificate).as("certificateNotBefore"),
db.ref("notAfter").withSchema(TableName.Certificate).as("certificateNotAfter"),
db.ref("name").withSchema(TableName.PkiSync).as("pkiSyncName"),
db.ref("destination").withSchema(TableName.PkiSync).as("pkiSyncDestination")
)
.orderBy(`${TableName.CertificateSync}.createdAt`, "desc");
if (offset !== undefined) {
void query.offset(offset);
}
if (limit !== undefined) {
void query.limit(limit);
}
const certificateDetails = (await query) as (TCertificateSyncs & {
certificateSerialNumber?: string;
certificateCommonName?: string;
certificateStatus?: string;
certificateNotBefore?: Date;
certificateNotAfter?: Date;
pkiSyncName?: string;
pkiSyncDestination?: string;
})[];
return { certificateDetails, totalCount };
} catch (error) {
throw new DatabaseError({ error, name: "FindWithDetails" });
}
};
return {
...certificateSyncOrm,
findByPkiSyncId,
findByCertificateId,
findByPkiSyncAndCertificate,
findCertificateIdsByPkiSyncId,
findPkiSyncIdsByCertificateId,
addCertificates,
removeCertificates,
removeAllCertificatesFromSync,
updateSyncStatus,
bulkUpdateSyncStatus,
findWithDetails
};
};
@@ -0,0 +1,6 @@
export enum CertificateSyncStatus {
Pending = "pending",
Syncing = "syncing",
Succeeded = "succeeded",
Failed = "failed"
}
@@ -133,7 +133,18 @@ describe("CertificateV3Service", () => {
certificateProfileDAL: mockCertificateProfileDAL,
certificateTemplateV2Service: mockCertificateTemplateV2Service,
internalCaService: mockInternalCaService,
permissionService: mockPermissionService
permissionService: mockPermissionService,
certificateSyncDAL: {
findPkiSyncIdsByCertificateId: vi.fn().mockResolvedValue([]),
addCertificates: vi.fn().mockResolvedValue([]),
removeCertificates: vi.fn().mockResolvedValue(0)
},
pkiSyncDAL: {
find: vi.fn().mockResolvedValue([])
},
pkiSyncQueue: {
queuePkiSyncSyncCertificatesById: vi.fn().mockResolvedValue(undefined)
}
});
});
@@ -48,6 +48,10 @@ import {
mapEnumsForValidation,
normalizeDateForApi
} from "../certificate-common/certificate-utils";
import { TCertificateSyncDALFactory } from "../certificate-sync/certificate-sync-dal";
import { TPkiSyncDALFactory } from "../pki-sync/pki-sync-dal";
import { TPkiSyncQueueFactory } from "../pki-sync/pki-sync-queue";
import { replaceCertificateInSyncs, triggerAutoSyncForCertificate } from "../pki-sync/pki-sync-utils";
import {
TCertificateFromProfileResponse,
TCertificateOrderResponse,
@@ -72,6 +76,12 @@ type TCertificateV3ServiceFactoryDep = {
>;
internalCaService: Pick<TInternalCertificateAuthorityServiceFactory, "signCertFromCa" | "issueCertFromCa">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
certificateSyncDAL: Pick<
TCertificateSyncDALFactory,
"findPkiSyncIdsByCertificateId" | "removeCertificates" | "addCertificates"
>;
pkiSyncDAL: Pick<TPkiSyncDALFactory, "find">;
pkiSyncQueue: Pick<TPkiSyncQueueFactory, "queuePkiSyncSyncCertificatesById">;
};
export type TCertificateV3ServiceFactory = ReturnType<typeof certificateV3ServiceFactory>;
@@ -328,7 +338,10 @@ export const certificateV3ServiceFactory = ({
certificateProfileDAL,
certificateTemplateV2Service,
internalCaService,
permissionService
permissionService,
certificateSyncDAL,
pkiSyncDAL,
pkiSyncQueue
}: TCertificateV3ServiceFactoryDep) => {
const issueCertificateFromProfile = async ({
profileId,
@@ -872,6 +885,8 @@ export const certificateV3ServiceFactory = ({
tx
);
await replaceCertificateInSyncs(originalCert.id, newCert.id, { certificateSyncDAL }, tx);
return {
certificate,
certificateChain,
@@ -883,6 +898,12 @@ export const certificateV3ServiceFactory = ({
};
});
await triggerAutoSyncForCertificate(renewalResult.newCert.id, {
certificateSyncDAL,
pkiSyncDAL,
pkiSyncQueue
});
return {
certificate: renewalResult.certificate,
issuingCaCertificate: renewalResult.issuingCaCertificate,
@@ -1,3 +1,5 @@
import RE2 from "re2";
import { TDbClient } from "@app/db";
import { TableName, TCertificates } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
@@ -60,11 +62,13 @@ export const certificateDALFactory = (db: TDbClient) => {
.where(`${TableName.Project}.id`, projectId);
if (friendlyName) {
query = query.andWhere(`${TableName.Certificate}.friendlyName`, friendlyName);
const sanitizedValue = String(friendlyName).replace(new RE2("[%_\\\\]", "g"), "\\$&");
query = query.andWhere(`${TableName.Certificate}.friendlyName`, "like", `%${sanitizedValue}%`);
}
if (commonName) {
query = query.andWhere(`${TableName.Certificate}.commonName`, commonName);
const sanitizedValue = String(commonName).replace(new RE2("[%_\\\\]", "g"), "\\$&");
query = query.andWhere(`${TableName.Certificate}.commonName`, "like", `%${sanitizedValue}%`);
}
const count = await query.count("*").first();
@@ -114,6 +118,109 @@ export const certificateDALFactory = (db: TDbClient) => {
}
};
const findActiveCertificatesByIds = async (certificateIds: string[]): Promise<TCertificates[]> => {
try {
if (certificateIds.length === 0) {
return [];
}
const certs = await db
.replicaNode()(TableName.Certificate)
.whereIn("id", certificateIds)
.where({ status: CertStatus.ACTIVE })
.where("notAfter", ">", new Date())
.orderBy("notBefore", "desc")
.select("*");
return certs;
} catch (error) {
throw new DatabaseError({ error, name: "Find active certificates by IDs" });
}
};
const findActiveCertificatesForSync = async (
filter: Partial<TCertificates & { friendlyName?: string; commonName?: string }>,
options?: { limit?: number; offset?: number }
): Promise<(TCertificates & { hasPrivateKey: boolean })[]> => {
try {
let query = db
.replicaNode()(TableName.Certificate)
.leftJoin(TableName.CertificateSecret, `${TableName.Certificate}.id`, `${TableName.CertificateSecret}.certId`)
.select(selectAllTableCols(TableName.Certificate))
.select(db.ref(`${TableName.CertificateSecret}.certId`).as("privateKeyRef"))
.where({ status: CertStatus.ACTIVE })
.where("notAfter", ">", new Date())
.whereNull("renewedByCertificateId");
Object.entries(filter).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
if (key === "friendlyName" || key === "commonName") {
const sanitizedValue = String(value).replace(new RE2("[%_\\\\]", "g"), "\\$&");
query = query.andWhere(`${TableName.Certificate}.${key}`, "like", `%${sanitizedValue}%`);
} else {
query = query.andWhere(`${TableName.Certificate}.${key}`, value);
}
}
});
if (options?.offset) {
query = query.offset(options.offset);
}
if (options?.limit) {
query = query.limit(options.limit);
}
query = query.orderBy("createdAt", "desc");
const certs = await query;
return certs.map((cert) => ({ ...cert, hasPrivateKey: Boolean(cert.privateKeyRef) }));
} catch (error) {
throw new DatabaseError({ error, name: "Find active certificates for sync" });
}
};
const countActiveCertificatesForSync = async ({
projectId,
friendlyName,
commonName
}: {
projectId: string;
friendlyName?: string;
commonName?: string;
}) => {
try {
interface CountResult {
count: string;
}
let query = db
.replicaNode()(TableName.Certificate)
.join(TableName.CertificateAuthority, `${TableName.Certificate}.caId`, `${TableName.CertificateAuthority}.id`)
.join(TableName.Project, `${TableName.CertificateAuthority}.projectId`, `${TableName.Project}.id`)
.where(`${TableName.Project}.id`, projectId)
.where(`${TableName.Certificate}.status`, CertStatus.ACTIVE)
.where(`${TableName.Certificate}.notAfter`, ">", new Date())
.whereNull(`${TableName.Certificate}.renewedByCertificateId`);
if (friendlyName) {
const sanitizedValue = String(friendlyName).replace(new RE2("[%_\\\\]", "g"), "\\$&");
query = query.andWhere(`${TableName.Certificate}.friendlyName`, "like", `%${sanitizedValue}%`);
}
if (commonName) {
const sanitizedValue = String(commonName).replace(new RE2("[%_\\\\]", "g"), "\\$&");
query = query.andWhere(`${TableName.Certificate}.commonName`, "like", `%${sanitizedValue}%`);
}
const count = await query.count("*").first();
return parseInt((count as unknown as CountResult).count || "0", 10);
} catch (error) {
throw new DatabaseError({ error, name: "Count active certificates for sync" });
}
};
const findCertificatesEligibleForRenewal = async ({
limit,
offset
@@ -159,7 +266,7 @@ export const certificateDALFactory = (db: TDbClient) => {
};
const findWithPrivateKeyInfo = async (
filter: Partial<TCertificates>,
filter: Partial<TCertificates & { friendlyName?: string; commonName?: string }>,
options?: { offset?: number; limit?: number; sort?: [string, "asc" | "desc"][] }
): Promise<(TCertificates & { hasPrivateKey: boolean })[]> => {
try {
@@ -167,8 +274,18 @@ export const certificateDALFactory = (db: TDbClient) => {
.replicaNode()(TableName.Certificate)
.leftJoin(TableName.CertificateSecret, `${TableName.Certificate}.id`, `${TableName.CertificateSecret}.certId`)
.select(selectAllTableCols(TableName.Certificate))
.select(db.ref(`${TableName.CertificateSecret}.certId`).as("privateKeyRef"))
.where(filter);
.select(db.ref(`${TableName.CertificateSecret}.certId`).as("privateKeyRef"));
Object.entries(filter).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
if (key === "friendlyName" || key === "commonName") {
const sanitizedValue = String(value).replace(new RE2("[%_\\\\]", "g"), "\\$&");
query = query.andWhere(`${TableName.Certificate}.${key}`, "like", `%${sanitizedValue}%`);
} else {
query = query.andWhere(`${TableName.Certificate}.${key}`, value);
}
}
});
if (options?.offset) {
query = query.offset(options.offset);
@@ -197,10 +314,13 @@ export const certificateDALFactory = (db: TDbClient) => {
return {
...certificateOrm,
countCertificatesInProject,
countActiveCertificatesForSync,
countCertificatesForPkiSubscriber,
findLatestActiveCertForSubscriber,
findAllActiveCertsForSubscriber,
findExpiredSyncedCertificates,
findActiveCertificatesByIds,
findActiveCertificatesForSync,
findCertificatesEligibleForRenewal,
findWithPrivateKeyInfo
};
@@ -18,12 +18,13 @@ import { TCertificateAuthorityDALFactory } from "@app/services/certificate-autho
import { CaCapability, CaType } from "@app/services/certificate-authority/certificate-authority-enums";
import { caSupportsCapability } from "@app/services/certificate-authority/certificate-authority-maps";
import { TCertificateAuthoritySecretDALFactory } from "@app/services/certificate-authority/certificate-authority-secret-dal";
import { TCertificateSyncDALFactory } from "@app/services/certificate-sync/certificate-sync-dal";
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 { TPkiSyncQueueFactory } from "@app/services/pki-sync/pki-sync-queue";
import { triggerAutoSyncForSubscriber } from "@app/services/pki-sync/pki-sync-utils";
import { triggerAutoSyncForCertificate } from "@app/services/pki-sync/pki-sync-utils";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
@@ -57,6 +58,7 @@ type TCertificateServiceFactoryDep = {
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction">;
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
certificateSyncDAL: Pick<TCertificateSyncDALFactory, "findPkiSyncIdsByCertificateId">;
pkiSyncDAL: Pick<TPkiSyncDALFactory, "find">;
pkiSyncQueue: Pick<TPkiSyncQueueFactory, "queuePkiSyncSyncCertificatesById">;
};
@@ -76,6 +78,7 @@ export const certificateServiceFactory = ({
projectDAL,
kmsService,
permissionService,
certificateSyncDAL,
pkiSyncDAL,
pkiSyncQueue
}: TCertificateServiceFactoryDep) => {
@@ -166,10 +169,12 @@ export const certificateServiceFactory = ({
const deletedCert = await certificateDAL.deleteById(cert.id);
// Trigger auto sync for PKI syncs connected to this certificate's subscriber
if (cert.pkiSubscriberId) {
await triggerAutoSyncForSubscriber(cert.pkiSubscriberId, { pkiSyncDAL, pkiSyncQueue });
}
// Trigger auto sync for PKI syncs connected to this certificate
await triggerAutoSyncForCertificate(cert.id, {
certificateSyncDAL,
pkiSyncDAL,
pkiSyncQueue
});
return {
deletedCert
@@ -235,10 +240,12 @@ export const certificateServiceFactory = ({
}
);
// Trigger auto sync for PKI syncs connected to this certificate's subscriber
if (cert.pkiSubscriberId) {
await triggerAutoSyncForSubscriber(cert.pkiSubscriberId, { pkiSyncDAL, pkiSyncQueue });
}
// Trigger auto sync for PKI syncs connected to this certificate
await triggerAutoSyncForCertificate(cert.id, {
certificateSyncDAL,
pkiSyncDAL,
pkiSyncQueue
});
// Note: External CA revocation handling would go here for supported CA types
// Currently, only internal CAs and ACME CAs support revocation
@@ -4,6 +4,7 @@ import RE2 from "re2";
import { z } from "zod";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
import { AppConnection, AWSRegion } from "@app/services/app-connection/app-connection-enums";
import { decryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns";
@@ -101,7 +102,8 @@ const findInfisicalCertificateTag = (tags: AWS.ACM.TagList | undefined): AWS.ACM
const validateCertificateIdentification = (
certName: string,
existingCert: { arn?: string; Tags?: AWS.ACM.TagList; cert?: string; privateKey?: string; certificateChain?: string }
existingCert: { arn?: string; Tags?: AWS.ACM.TagList; cert?: string; privateKey?: string; certificateChain?: string },
alternativeCertNames?: string[]
): boolean => {
if (!existingCert?.arn || !existingCert?.Tags) {
return false;
@@ -113,12 +115,15 @@ const validateCertificateIdentification = (
return false;
}
return certNameTag.Value === certName;
};
if (certNameTag.Value === certName) {
return true;
}
type TAwsCertificateManagerPkiSyncFactoryDeps = {
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "updateById">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
if (alternativeCertNames && alternativeCertNames.includes(certNameTag.Value)) {
return true;
}
return false;
};
const validateCertificateNameSchema = (schema: string): void => {
@@ -174,6 +179,11 @@ const generateCertificateName = (certificateName: string, pkiSync: TPkiSyncWithC
return sanitizedCertificateName;
};
type TAwsCertificateManagerPkiSyncFactoryDeps = {
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "updateById">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
};
const getAwsAcmClient = async (
connectionId: string,
region: AWSRegion,
@@ -392,48 +402,59 @@ export const awsCertificateManagerPkiSyncFactory = ({
kmsService
);
const { acmCertificates } = await $getAwsAcmCertificates(acm, pkiSync.id);
const {
acmCertificates
}: {
acmCertificates: Record<
string,
{ cert: string; privateKey: string; certificateChain?: string; arn?: string; Tags?: AWS.ACM.TagList }
>;
} = await $getAwsAcmCertificates(acm, pkiSync.id);
const setCertificates: CertificateImportRequest[] = [];
const validationErrors: Array<{ name: string; error: string }> = [];
const activeCertificateNames = Object.keys(certificateMap);
const syncOptions = pkiSync.syncOptions as { preserveArn?: boolean } | undefined;
const preserveArn = syncOptions?.preserveArn ?? true;
Object.entries(certificateMap).forEach(([certName, certData]) => {
const { cert, privateKey, certificateChain } = certData;
const certificateName = generateCertificateName(certName, pkiSync);
const existingCert = Object.values(acmCertificates).find((acmCert) =>
validateCertificateIdentification(certName, acmCert)
);
const shouldUpdateCert = !existingCert || existingCert.cert !== cert;
const { cert, privateKey, certificateChain, alternativeNames } = certData;
try {
validateCertificateContent(cert, privateKey);
} catch (validationError) {
throw new PkiSyncError({
message: `Certificate validation failed for ${certName}: ${validationError instanceof Error ? validationError.message : String(validationError)}`,
shouldRetry: false,
context: {
certificateName,
certName
}
const errorMessage = validationError instanceof Error ? validationError.message : String(validationError);
validationErrors.push({
name: certName,
error: `Certificate validation failed: ${errorMessage}`
});
return;
}
if (shouldUpdateCert) {
setCertificates.push({
key: certName,
name: certificateName,
cert,
privateKey,
certificateChain,
existingArn: existingCert?.arn
});
const certificateName = generateCertificateName(certName, pkiSync);
let existingArn: string | undefined;
const existingCert = Object.values(acmCertificates).find((acmCert) => {
return validateCertificateIdentification(certName, acmCert, alternativeNames);
});
if (existingCert?.arn && preserveArn) {
// When preserveArn is true, reuse the existing ARN
existingArn = existingCert.arn;
}
setCertificates.push({
key: certName,
name: certificateName,
cert,
privateKey,
certificateChain,
existingArn
});
});
// Identify expired/removed certificates that need to be cleaned up from ACM
const certificatesToRemove = Object.values(acmCertificates)
.filter((acmCert) => {
if (!acmCert.arn || !acmCert.Tags) {
@@ -445,8 +466,22 @@ export const awsCertificateManagerPkiSyncFactory = ({
return false;
}
const isActive = activeCertificateNames.includes(certNameTag.Value);
return !isActive;
const isActive = activeCertificateNames.some((activeCertName) => {
const certData = certificateMap[activeCertName];
if (!certData) return false;
return validateCertificateIdentification(activeCertName, acmCert, certData.alternativeNames);
});
if (!isActive) {
return true;
}
if (!preserveArn && isActive) {
return true;
}
return false;
})
.map((acmCert) => acmCert.arn!)
.filter((arn) => arn);
@@ -457,14 +492,17 @@ export const awsCertificateManagerPkiSyncFactory = ({
try {
const importParams: AWS.ACM.ImportCertificateRequest = {
Certificate: cert,
PrivateKey: privateKey,
Tags: [
PrivateKey: privateKey
};
if (!existingArn) {
importParams.Tags = [
{
Key: INFISICAL_CERTIFICATE_TAG,
Value: key
}
]
};
];
}
if (certificateChain && certificateChain.trim().length > 0) {
importParams.CertificateChain = certificateChain;
@@ -478,6 +516,39 @@ export const awsCertificateManagerPkiSyncFactory = ({
syncId: pkiSync.id
});
if (existingArn && response.CertificateArn) {
try {
// Small delay to ensure AWS ACM has processed the certificate import
await new Promise<void>((resolve) => {
setTimeout(() => resolve(), 100);
});
await withRateLimitRetry(
() =>
acm
.addTagsToCertificate({
CertificateArn: response.CertificateArn!,
Tags: [
{
Key: INFISICAL_CERTIFICATE_TAG,
Value: key
}
]
})
.promise(),
{
operation: "add-tags-to-certificate",
syncId: pkiSync.id
}
);
} catch (tagError) {
const errorMessage = tagError instanceof Error ? tagError.message : "Unknown tagging error";
logger.warn(
`Failed to add tags to certificate ${key} (ARN: ${response.CertificateArn}): ${errorMessage}`
);
}
}
return { key, name, success: true, response };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error";
@@ -520,15 +591,21 @@ export const awsCertificateManagerPkiSyncFactory = ({
const details: {
failedUploads?: Array<{ name: string; error: string }>;
failedRemovals?: Array<{ name: string; error: string }>;
validationErrors?: Array<{ name: string; error: string }>;
} = {};
if (validationErrors.length > 0) {
details.validationErrors = validationErrors;
}
if (failedUploads.length > 0) {
details.failedUploads = failedUploads.map((failure, index) => {
const certificateName = setCertificates[index]?.name || "unknown";
const certificateRequest = setCertificates[index];
const certificateName = certificateRequest?.name || certificateRequest?.key || "unknown";
let errorMessage = "Unknown error";
if (failure.status === "rejected") {
errorMessage = failure.reason instanceof Error ? failure.reason.message : "Unknown error";
errorMessage = failure.reason instanceof Error ? failure.reason.message : String(failure.reason);
}
return {
@@ -577,7 +654,14 @@ export const awsCertificateManagerPkiSyncFactory = ({
kmsService
);
const { acmCertificates } = await $getAwsAcmCertificates(acm, pkiSync.id);
const {
acmCertificates
}: {
acmCertificates: Record<
string,
{ cert: string; privateKey: string; certificateChain?: string; arn?: string; Tags?: AWS.ACM.TagList }
>;
} = await $getAwsAcmCertificates(acm, pkiSync.id);
const certificateArnsToRemove: string[] = [];
@@ -14,6 +14,7 @@ export const AwsCertificateManagerPkiSyncConfigSchema = z.object({
const AwsCertificateManagerPkiSyncOptionsSchema = z.object({
canImportCertificates: z.boolean().default(false),
canRemoveCertificates: z.boolean().default(true),
preserveArn: z.boolean().default(true),
certificateNameSchema: z
.string()
.optional()
@@ -28,6 +29,9 @@ const AwsCertificateManagerPkiSyncOptionsSchema = z.object({
const testName = schema
.replace(new RE2("\\{\\{certificateId\\}\\}", "g"), "test-cert-id")
.replace(new RE2("\\{\\{profileId\\}\\}", "g"), "test-profile-id")
.replace(new RE2("\\{\\{commonName\\}\\}", "g"), "test-common-name")
.replace(new RE2("\\{\\{friendlyName\\}\\}", "g"), "test-friendly-name")
.replace(new RE2("\\{\\{environment\\}\\}", "g"), "test-env");
const hasForbiddenChars = AWS_CERTIFICATE_MANAGER_CERTIFICATE_NAMING.FORBIDDEN_CHARACTERS.split("").some(
@@ -43,7 +47,7 @@ const AwsCertificateManagerPkiSyncOptionsSchema = z.object({
},
{
message:
"Certificate name schema must include {{certificateId}} placeholder and result in names that contain only alphanumeric characters, spaces, hyphens, and underscores and be 1-256 characters long when compiled for AWS Certificate Manager"
"Certificate name schema must include {{certificateId}} placeholder and result in names that contain only alphanumeric characters, spaces, hyphens, and underscores and be 1-256 characters long when compiled for AWS Certificate Manager. Available placeholders: {{certificateId}}, {{profileId}}, {{commonName}}, {{friendlyName}}, {{environment}}"
}
)
});
@@ -60,9 +64,10 @@ export const CreateAwsCertificateManagerPkiSyncSchema = z.object({
isAutoSyncEnabled: z.boolean().default(true),
destinationConfig: AwsCertificateManagerPkiSyncConfigSchema,
syncOptions: AwsCertificateManagerPkiSyncOptionsSchema.optional().default({}),
subscriberId: z.string().optional(),
subscriberId: z.string().nullish(),
connectionId: z.string(),
projectId: z.string().trim().min(1)
projectId: z.string().trim().min(1),
certificateIds: z.array(z.string().uuid()).optional()
});
export const UpdateAwsCertificateManagerPkiSyncSchema = z.object({
@@ -71,7 +76,7 @@ export const UpdateAwsCertificateManagerPkiSyncSchema = z.object({
isAutoSyncEnabled: z.boolean().optional(),
destinationConfig: AwsCertificateManagerPkiSyncConfigSchema.optional(),
syncOptions: AwsCertificateManagerPkiSyncOptionsSchema.optional(),
subscriberId: z.string().optional(),
subscriberId: z.string().nullish(),
connectionId: z.string().optional()
});
@@ -39,6 +39,7 @@ export interface SyncCertificatesResult {
details?: {
failedUploads?: Array<{ name: string; error: string }>;
failedRemovals?: Array<{ name: string; error: string }>;
validationErrors?: Array<{ name: string; error: string }>;
};
}
@@ -52,7 +52,8 @@ export const CreateAzureKeyVaultPkiSyncSchema = z.object({
syncOptions: AzureKeyVaultPkiSyncOptionsSchema.optional().default({}),
subscriberId: z.string().optional(),
connectionId: z.string(),
projectId: z.string().trim().min(1)
projectId: z.string().trim().min(1),
certificateIds: z.array(z.string().uuid()).optional()
});
export const UpdateAzureKeyVaultPkiSyncSchema = z.object({
@@ -194,6 +194,7 @@ export const PkiSyncFns = {
failedUploads?: Array<{ name: string; error: string }>;
failedRemovals?: Array<{ name: string; error: string }>;
skippedCertificates?: Array<{ name: string; reason: string }>;
validationErrors?: Array<{ name: string; error: string }>;
};
}> => {
switch (pkiSync.destination) {
+152 -26
View File
@@ -5,6 +5,7 @@ import { AxiosError } from "axios";
import { Job } from "bullmq";
import handlebars from "handlebars";
import { TCertificates } from "@app/db/schemas";
import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore";
@@ -25,6 +26,7 @@ import { TCertificateSecretDALFactory } from "../certificate/certificate-secret-
import { TCertificateAuthorityCertDALFactory } from "../certificate-authority/certificate-authority-cert-dal";
import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal";
import { getCaCertChain } from "../certificate-authority/certificate-authority-fns";
import { TCertificateSyncDALFactory } from "../certificate-sync/certificate-sync-dal";
import { TPkiSyncDALFactory } from "./pki-sync-dal";
import { PkiSyncStatus } from "./pki-sync-enums";
import { PkiSyncError } from "./pki-sync-errors";
@@ -57,12 +59,21 @@ type TPkiSyncQueueFactoryDep = {
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
certificateDAL: Pick<
TCertificateDALFactory,
"findLatestActiveCertForSubscriber" | "findAllActiveCertsForSubscriber" | "create"
| "findLatestActiveCertForSubscriber"
| "findAllActiveCertsForSubscriber"
| "findActiveCertificatesByIds"
| "create"
| "findById"
| "find"
>;
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "findOne" | "create">;
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne" | "create">;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findById">;
certificateSyncDAL: Pick<
TCertificateSyncDALFactory,
"findCertificateIdsByPkiSyncId" | "updateSyncStatus" | "bulkUpdateSyncStatus"
>;
};
type PkiSyncActionJob = Job<
@@ -93,7 +104,8 @@ export const pkiSyncQueueFactory = ({
certificateBodyDAL,
certificateSecretDAL,
certificateAuthorityDAL,
certificateAuthorityCertDAL
certificateAuthorityCertDAL,
certificateSyncDAL
}: TPkiSyncQueueFactoryDep) => {
const appCfg = getConfig();
@@ -153,25 +165,39 @@ export const pkiSyncQueueFactory = ({
const $getInfisicalCertificates = async (
pkiSync: TPkiSyncRaw | TPkiSyncWithCredentials
): Promise<TCertificateMap> => {
const { projectId, subscriberId } = pkiSync;
if (!subscriberId) {
throw new PkiSyncError({
message: "Invalid PKI Sync source configuration: subscriber no longer exists. Please update source subscriber.",
shouldRetry: false
});
}
): Promise<{ certificateMap: TCertificateMap; certificateMetadata: Map<string, { id: string; name: string }> }> => {
const { projectId, subscriberId, id: pkiSyncId } = pkiSync;
const certificateMap: TCertificateMap = {};
const certificateMetadata = new Map<string, { id: string; name: string }>();
let certificates: Array<{ id: string; projectId: string; caCertId?: string | null }> = [];
try {
// Get all active certificates for the subscriber (not just the latest)
const certificates = await certificateDAL.findAllActiveCertsForSubscriber({
subscriberId
});
if (subscriberId) {
const subscriberCertificates = await certificateDAL.findAllActiveCertsForSubscriber({
subscriberId
});
certificates.push(...subscriberCertificates);
}
const certificateIds = await certificateSyncDAL.findCertificateIdsByPkiSyncId(pkiSyncId);
if (certificateIds.length > 0) {
const directCertificates = await certificateDAL.findActiveCertificatesByIds(certificateIds);
certificates.push(...directCertificates);
}
const uniqueCertificates = certificates.filter(
(cert, index, self) => self.findIndex((c) => c.id === cert.id) === index
);
if (uniqueCertificates.length === 0) {
return { certificateMap, certificateMetadata };
}
certificates = uniqueCertificates;
for (const certificate of certificates) {
const cert = certificate as TCertificates;
try {
// Get the certificate body and decrypt the certificate data
const certBody = await certificateBodyDAL.findOne({ certId: certificate.id });
@@ -246,19 +272,44 @@ export const pkiSyncQueueFactory = ({
if (certificateNameSchema) {
const environment = "global";
certificateName = handlebars.compile(certificateNameSchema)({
const templateData = {
certificateId: certificate.id.replace(/-/g, ""),
profileId: cert.profileId?.replace(/-/g, "") || certificate.id.replace(/-/g, ""),
commonName: cert.commonName || "",
friendlyName: cert.friendlyName || "",
environment
});
};
certificateName = handlebars.compile(certificateNameSchema)(templateData);
} else {
certificateName = `Infisical-${certificate.id.replace(/-/g, "")}`;
const stableId = cert.profileId
? `${cert.profileId.replace(/-/g, "")}-${(cert.commonName || "").replace(/[^a-zA-Z0-9]/g, "")}`
: certificate.id.replace(/-/g, "");
certificateName = `Infisical-${stableId}`;
}
const alternativeNames: string[] = [];
const legacyName = `Infisical-${certificate.id.replace(/-/g, "")}`;
if (legacyName !== certificateName) {
alternativeNames.push(legacyName);
}
if (cert.renewedFromCertificateId) {
const originalLegacyName = `Infisical-${cert.renewedFromCertificateId.replace(/-/g, "")}`;
alternativeNames.push(originalLegacyName);
}
certificateMap[certificateName] = {
cert: certificatePem,
privateKey: certPrivateKey || "",
certificateChain
certificateChain,
alternativeNames
};
certificateMetadata.set(certificateName, {
id: certificate.id,
name: certificateName
});
} else {
logger.warn({ certificateId: certificate.id, subscriberId }, "Certificate body not found for certificate");
}
@@ -281,7 +332,7 @@ export const pkiSyncQueueFactory = ({
});
}
return certificateMap;
return { certificateMap, certificateMetadata };
};
const queuePkiSyncSyncCertificatesById = async (payload: TQueuePkiSyncSyncCertificatesByIdDTO) =>
@@ -348,12 +399,17 @@ export const pkiSyncQueueFactory = ({
try {
const {
connection: { orgId, encryptedCredentials, projectId: appConnectionProjectId }
connection: { id: connectionId, orgId, projectId: appConnectionProjectId }
} = pkiSync;
const appConnection = await appConnectionDAL.findById(connectionId);
if (!appConnection) {
throw new Error(`App connection not found: ${connectionId}`);
}
const credentials = await decryptAppConnectionCredentials({
orgId,
encryptedCredentials,
encryptedCredentials: appConnection.encryptedCredentials,
kmsService,
projectId: appConnectionProjectId
});
@@ -366,7 +422,18 @@ export const pkiSyncQueueFactory = ({
}
} as TPkiSyncWithCredentials;
const certificateMap = await $getInfisicalCertificates(pkiSync);
const { certificateMap, certificateMetadata } = await $getInfisicalCertificates(pkiSync);
const statusUpdates = Array.from(certificateMetadata.entries()).map(([, metadata]) => ({
pkiSyncId: pkiSync.id,
certificateId: metadata.id,
status: "running",
message: "Syncing certificate to destination"
}));
if (statusUpdates.length > 0) {
await certificateSyncDAL.bulkUpdateSyncStatus(statusUpdates);
}
const syncResult = await PkiSyncFns.syncCertificates(pkiSyncWithCredentials, certificateMap, {
appConnectionDAL,
@@ -384,6 +451,60 @@ export const pkiSyncQueueFactory = ({
"PKI sync operation completed with certificate cleanup"
);
const postSyncUpdates: Array<{
pkiSyncId: string;
certificateId: string;
status: string;
message?: string;
}> = [];
for (const [, metadata] of certificateMetadata.entries()) {
postSyncUpdates.push({
pkiSyncId: pkiSync.id,
certificateId: metadata.id,
status: "succeeded",
message: "Certificate successfully synced to destination"
});
}
if (syncResult.details?.validationErrors) {
for (const validationError of syncResult.details.validationErrors) {
const metadata = certificateMetadata.get(validationError.name);
if (metadata) {
const updateIndex = postSyncUpdates.findIndex((u) => u.certificateId === metadata.id);
if (updateIndex >= 0) {
postSyncUpdates[updateIndex] = {
pkiSyncId: pkiSync.id,
certificateId: metadata.id,
status: "failed",
message: `${validationError.error}`
};
}
}
}
}
if (syncResult.details?.failedUploads) {
for (const failure of syncResult.details.failedUploads) {
const metadata = certificateMetadata.get(failure.name);
if (metadata) {
const updateIndex = postSyncUpdates.findIndex((u) => u.certificateId === metadata.id);
if (updateIndex >= 0) {
postSyncUpdates[updateIndex] = {
pkiSyncId: pkiSync.id,
certificateId: metadata.id,
status: "failed",
message: `Failed to sync certificate: ${failure.error}`
};
}
}
}
}
if (postSyncUpdates.length > 0) {
await certificateSyncDAL.bulkUpdateSyncStatus(postSyncUpdates);
}
isSynced = true;
} catch (err) {
logger.error(
@@ -550,17 +671,22 @@ export const pkiSyncQueueFactory = ({
try {
const {
connection: { orgId, encryptedCredentials, projectId: appConnectionProjectId }
connection: { id: connectionId, orgId, projectId: appConnectionProjectId }
} = pkiSync;
const appConnection = await appConnectionDAL.findById(connectionId);
if (!appConnection) {
throw new Error(`App connection not found: ${connectionId}`);
}
const credentials = await decryptAppConnectionCredentials({
orgId,
encryptedCredentials,
encryptedCredentials: appConnection.encryptedCredentials,
kmsService,
projectId: appConnectionProjectId
});
const certificateMap = await $getInfisicalCertificates(pkiSync);
const { certificateMap } = await $getInfisicalCertificates(pkiSync);
await PkiSyncFns.removeCertificates(
{
@@ -10,17 +10,23 @@ import { AppConnection } from "@app/services/app-connection/app-connection-enums
import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
import { TPkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal";
import { TCertificateSyncDALFactory } from "../certificate-sync/certificate-sync-dal";
import { CertificateSyncStatus } from "../certificate-sync/certificate-sync-enums";
import { TPkiSyncDALFactory } from "./pki-sync-dal";
import { PkiSync, PkiSyncStatus } from "./pki-sync-enums";
import { enterprisePkiSyncCheck, getPkiSyncProviderCapabilities, listPkiSyncOptions } from "./pki-sync-fns";
import { PKI_SYNC_CONNECTION_MAP, PKI_SYNC_NAME_MAP } from "./pki-sync-maps";
import { TPkiSyncQueueFactory } from "./pki-sync-queue";
import {
TAddCertificatesToPkiSyncDTO,
TCreatePkiSyncDTO,
TDeletePkiSyncDTO,
TFindPkiSyncByIdDTO,
TListPkiSyncCertificatesDTO,
TListPkiSyncsByProjectId,
TPkiSync,
TPkiSyncCertificate,
TRemoveCertificatesFromPkiSyncDTO,
TTriggerPkiSyncImportCertificatesByIdDTO,
TTriggerPkiSyncRemoveCertificatesByIdDTO,
TTriggerPkiSyncSyncCertificatesByIdDTO,
@@ -42,6 +48,16 @@ type TPkiSyncServiceFactoryDep = {
TPkiSyncDALFactory,
"findById" | "findByProjectIdWithSubscribers" | "findByNameAndProjectId" | "create" | "updateById" | "deleteById"
>;
certificateSyncDAL: Pick<
TCertificateSyncDALFactory,
| "findByPkiSyncId"
| "findByCertificateId"
| "findCertificateIdsByPkiSyncId"
| "addCertificates"
| "removeCertificates"
| "removeAllCertificatesFromSync"
| "findWithDetails"
>;
pkiSubscriberDAL: Pick<TPkiSubscriberDALFactory, "findById">;
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
@@ -56,6 +72,7 @@ export type TPkiSyncServiceFactory = ReturnType<typeof pkiSyncServiceFactory>;
export const pkiSyncServiceFactory = ({
pkiSyncDAL,
certificateSyncDAL,
pkiSubscriberDAL,
appConnectionService,
permissionService,
@@ -72,7 +89,8 @@ export const pkiSyncServiceFactory = ({
syncOptions = {},
subscriberId,
connectionId,
projectId
projectId,
certificateIds = []
}: Omit<TCreatePkiSyncDTO, "auditLogInfo">,
actor: OrgServiceActor
): Promise<TPkiSync> => {
@@ -128,6 +146,10 @@ export const pkiSyncServiceFactory = ({
...(isAutoSyncEnabled && { syncStatus: PkiSyncStatus.Pending })
});
if (certificateIds.length > 0) {
await certificateSyncDAL.addCertificates(pkiSync.id, certificateIds);
}
if (pkiSync.isAutoSyncEnabled) {
await pkiSyncQueue.queuePkiSyncSyncCertificatesById({ syncId: pkiSync.id });
}
@@ -152,7 +174,8 @@ export const pkiSyncServiceFactory = ({
destinationConfig,
syncOptions,
subscriberId,
connectionId
connectionId,
certificateIds
}: Omit<TUpdatePkiSyncDTO, "auditLogInfo" | "projectId">,
actor: OrgServiceActor
): Promise<TPkiSync> => {
@@ -221,6 +244,13 @@ export const pkiSyncServiceFactory = ({
};
}
if (certificateIds !== undefined) {
await certificateSyncDAL.removeAllCertificatesFromSync(id);
if (certificateIds.length > 0) {
await certificateSyncDAL.addCertificates(id, certificateIds);
}
}
const updatedPkiSync = await pkiSyncDAL.updateById(id, {
name,
description,
@@ -266,7 +296,7 @@ export const pkiSyncServiceFactory = ({
};
const listPkiSyncsByProjectId = async (
{ projectId }: TListPkiSyncsByProjectId,
{ projectId, certificateId }: TListPkiSyncsByProjectId,
actor: OrgServiceActor
): Promise<TPkiSync[]> => {
const { permission } = await permissionService.getProjectPermission({
@@ -282,6 +312,29 @@ export const pkiSyncServiceFactory = ({
const pkiSyncsWithSubscribers = await pkiSyncDAL.findByProjectIdWithSubscribers(projectId);
if (certificateId) {
const syncsWithCertificateInfo = await Promise.all(
pkiSyncsWithSubscribers.map(async (sync) => {
try {
const certificateSyncs = await certificateSyncDAL.findByPkiSyncId(sync.id);
const hasCertificate = certificateSyncs.some((certSync) => certSync.certificateId === certificateId);
return {
...sync,
hasCertificate
};
} catch (error) {
return {
...sync,
hasCertificate: false
};
}
})
);
return syncsWithCertificateInfo as TPkiSync[];
}
return pkiSyncsWithSubscribers as TPkiSync[];
};
@@ -433,6 +486,136 @@ export const pkiSyncServiceFactory = ({
return listPkiSyncOptions();
};
const addCertificatesToPkiSync = async (
{ pkiSyncId, certificateIds }: Omit<TAddCertificatesToPkiSyncDTO, "auditLogInfo" | "projectId">,
actor: OrgServiceActor
) => {
const pkiSync = await pkiSyncDAL.findById(pkiSyncId);
if (!pkiSync) throw new NotFoundError({ message: "PKI sync not found" });
const { permission } = await permissionService.getProjectPermission({
actor: actor.type,
actorId: actor.id,
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId,
actionProjectType: ActionProjectType.CertificateManager,
projectId: pkiSync.projectId
});
let subscriber;
if (pkiSync.subscriberId) {
subscriber = await pkiSubscriberDAL.findById(pkiSync.subscriberId);
}
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPkiSyncActions.Edit,
subscriber
? subject(ProjectPermissionSub.PkiSyncs, { subscriberName: subscriber.name })
: ProjectPermissionSub.PkiSyncs
);
const addedCertificates = await certificateSyncDAL.addCertificates(pkiSyncId, certificateIds);
if (pkiSync.isAutoSyncEnabled) {
await pkiSyncQueue.queuePkiSyncSyncCertificatesById({ syncId: pkiSyncId });
}
return addedCertificates;
};
const removeCertificatesFromPkiSync = async (
{ pkiSyncId, certificateIds }: Omit<TRemoveCertificatesFromPkiSyncDTO, "auditLogInfo" | "projectId">,
actor: OrgServiceActor
) => {
const pkiSync = await pkiSyncDAL.findById(pkiSyncId);
if (!pkiSync) throw new NotFoundError({ message: "PKI sync not found" });
const { permission } = await permissionService.getProjectPermission({
actor: actor.type,
actorId: actor.id,
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId,
actionProjectType: ActionProjectType.CertificateManager,
projectId: pkiSync.projectId
});
let subscriber;
if (pkiSync.subscriberId) {
subscriber = await pkiSubscriberDAL.findById(pkiSync.subscriberId);
}
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPkiSyncActions.Edit,
subscriber
? subject(ProjectPermissionSub.PkiSyncs, { subscriberName: subscriber.name })
: ProjectPermissionSub.PkiSyncs
);
const removedCount = await certificateSyncDAL.removeCertificates(pkiSyncId, certificateIds);
if (pkiSync.isAutoSyncEnabled) {
await pkiSyncQueue.queuePkiSyncSyncCertificatesById({ syncId: pkiSyncId });
}
return { removedCount };
};
const listPkiSyncCertificates = async (
{ pkiSyncId, offset = 0, limit = 20 }: Omit<TListPkiSyncCertificatesDTO, "projectId">,
actor: OrgServiceActor
): Promise<{ certificates: TPkiSyncCertificate[]; totalCount: number }> => {
const pkiSync = await pkiSyncDAL.findById(pkiSyncId);
if (!pkiSync) throw new NotFoundError({ message: "PKI sync not found" });
const { permission } = await permissionService.getProjectPermission({
actor: actor.type,
actorId: actor.id,
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId,
actionProjectType: ActionProjectType.CertificateManager,
projectId: pkiSync.projectId
});
let subscriber;
if (pkiSync.subscriberId) {
subscriber = await pkiSubscriberDAL.findById(pkiSync.subscriberId);
}
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPkiSyncActions.Read,
subscriber
? subject(ProjectPermissionSub.PkiSyncs, { subscriberName: subscriber.name })
: ProjectPermissionSub.PkiSyncs
);
const result = await certificateSyncDAL.findWithDetails({
pkiSyncId,
offset,
limit
});
const { certificateDetails, totalCount } = result;
const certificates = certificateDetails.map((detail) => ({
id: detail.id,
pkiSyncId: detail.pkiSyncId,
certificateId: detail.certificateId,
syncStatus: (detail.syncStatus as CertificateSyncStatus) || CertificateSyncStatus.Pending,
lastSyncMessage: detail.lastSyncMessage || undefined,
lastSyncedAt: detail.lastSyncedAt || undefined,
createdAt: detail.createdAt,
updatedAt: detail.updatedAt,
certificateSerialNumber: detail.certificateSerialNumber || undefined,
certificateCommonName: detail.certificateCommonName || undefined,
certificateStatus: detail.certificateStatus || undefined,
certificateNotBefore: detail.certificateNotBefore || undefined,
certificateNotAfter: detail.certificateNotAfter || undefined,
pkiSyncName: detail.pkiSyncName || undefined,
pkiSyncDestination: detail.pkiSyncDestination || undefined
}));
return { certificates, totalCount };
};
return {
createPkiSync,
updatePkiSync,
@@ -442,6 +625,9 @@ export const pkiSyncServiceFactory = ({
triggerPkiSyncSyncCertificatesById,
triggerPkiSyncImportCertificatesById,
triggerPkiSyncRemoveCertificatesById,
getPkiSyncOptions
getPkiSyncOptions,
addCertificatesToPkiSync,
removeCertificatesFromPkiSync,
listPkiSyncCertificates
};
};
@@ -2,6 +2,7 @@ import { Job } from "bullmq";
import { AuditLogInfo } from "@app/ee/services/audit-log/audit-log-types";
import { QueueJobs } from "@app/queue";
import { CertificateSyncStatus } from "@app/services/certificate-sync/certificate-sync-enums";
import { ResourceMetadataDTO } from "@app/services/resource-metadata/resource-metadata-schema";
import { TPkiSyncDALFactory } from "./pki-sync-dal";
@@ -70,7 +71,10 @@ export type TPkiSyncListItem = TPkiSync & {
appConnectionApp: string;
};
export type TCertificateMap = Record<string, { cert: string; privateKey: string; certificateChain?: string }>;
export type TCertificateMap = Record<
string,
{ cert: string; privateKey: string; certificateChain?: string; alternativeNames?: string[] }
>;
export type TCreatePkiSyncDTO = {
name: string;
@@ -79,9 +83,10 @@ export type TCreatePkiSyncDTO = {
isAutoSyncEnabled?: boolean;
destinationConfig: Record<string, unknown>;
syncOptions?: Record<string, unknown>;
subscriberId?: string;
subscriberId?: string | null;
connectionId: string;
projectId: string;
certificateIds?: string[];
auditLogInfo: AuditLogInfo;
resourceMetadata?: ResourceMetadataDTO;
};
@@ -94,8 +99,9 @@ export type TUpdatePkiSyncDTO = {
isAutoSyncEnabled?: boolean;
destinationConfig?: Record<string, unknown>;
syncOptions?: Record<string, unknown>;
subscriberId?: string;
subscriberId?: string | null;
connectionId?: string;
certificateIds?: string[];
auditLogInfo: AuditLogInfo;
resourceMetadata?: ResourceMetadataDTO;
};
@@ -108,6 +114,7 @@ export type TDeletePkiSyncDTO = {
export type TListPkiSyncsByProjectId = {
projectId: string;
certificateId?: string;
};
export type TFindPkiSyncByIdDTO = {
@@ -133,6 +140,45 @@ export type TTriggerPkiSyncRemoveCertificatesByIdDTO = {
auditLogInfo: AuditLogInfo;
};
export type TAddCertificatesToPkiSyncDTO = {
pkiSyncId: string;
certificateIds: string[];
projectId?: string;
auditLogInfo: AuditLogInfo;
};
export type TRemoveCertificatesFromPkiSyncDTO = {
pkiSyncId: string;
certificateIds: string[];
projectId?: string;
auditLogInfo: AuditLogInfo;
};
export type TListPkiSyncCertificatesDTO = {
pkiSyncId: string;
projectId?: string;
offset?: number;
limit?: number;
};
export type TPkiSyncCertificate = {
id: string;
pkiSyncId: string;
certificateId: string;
syncStatus: CertificateSyncStatus;
lastSyncMessage?: string;
lastSyncedAt?: Date;
createdAt: Date;
updatedAt: Date;
certificate?: {
serialNumber: string;
commonName: string;
status: string;
notBefore: Date;
notAfter: Date;
};
};
export type TPkiSyncRaw = NonNullable<Awaited<ReturnType<TPkiSyncDALFactory["findById"]>>>;
export type TQueuePkiSyncSyncCertificatesByIdDTO = {
@@ -1,5 +1,8 @@
import { Knex } from "knex";
import { logger } from "@app/lib/logger";
import { TCertificateSyncDALFactory } from "../certificate-sync/certificate-sync-dal";
import { TPkiSyncDALFactory } from "./pki-sync-dal";
import { TPkiSyncQueueFactory } from "./pki-sync-queue";
@@ -25,3 +28,67 @@ export const triggerAutoSyncForSubscriber = async (
logger.error(error, `Failed to trigger auto sync for subscriber ${subscriberId}:`);
}
};
export const triggerAutoSyncForCertificate = async (
certificateId: string,
dependencies: {
certificateSyncDAL: Pick<TCertificateSyncDALFactory, "findPkiSyncIdsByCertificateId">;
pkiSyncDAL: Pick<TPkiSyncDALFactory, "find">;
pkiSyncQueue: Pick<TPkiSyncQueueFactory, "queuePkiSyncSyncCertificatesById">;
}
) => {
try {
const pkiSyncIds = await dependencies.certificateSyncDAL.findPkiSyncIdsByCertificateId(certificateId);
if (pkiSyncIds.length === 0) {
return;
}
const allPkiSyncs = await dependencies.pkiSyncDAL.find({
isAutoSyncEnabled: true
});
const pkiSyncs = allPkiSyncs.filter((sync) => pkiSyncIds.includes(sync.id));
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 certificate ${certificateId}:`);
}
};
export const replaceCertificateInSyncs = async (
oldCertificateId: string,
newCertificateId: string,
dependencies: {
certificateSyncDAL: Pick<
TCertificateSyncDALFactory,
"findPkiSyncIdsByCertificateId" | "removeCertificates" | "addCertificates"
>;
},
tx?: Knex
) => {
try {
const pkiSyncIds = await dependencies.certificateSyncDAL.findPkiSyncIdsByCertificateId(oldCertificateId);
if (pkiSyncIds.length === 0) {
return;
}
const replacementPromises = pkiSyncIds.map(async (pkiSyncId) => {
await dependencies.certificateSyncDAL.removeCertificates(pkiSyncId, [oldCertificateId], tx);
await dependencies.certificateSyncDAL.addCertificates(pkiSyncId, [newCertificateId], tx);
});
await Promise.all(replacementPromises);
logger.info(
`Successfully replaced certificate ${oldCertificateId} with ${newCertificateId} in ${pkiSyncIds.length} PKI sync(s)`
);
} catch (error) {
logger.error(error, `Failed to replace certificate ${oldCertificateId} with ${newCertificateId} in syncs:`);
throw error;
}
};
+37 -14
View File
@@ -154,7 +154,14 @@ type TProjectServiceFactoryDep = {
>;
pkiSubscriberDAL: Pick<TPkiSubscriberDALFactory, "find">;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find" | "findWithAssociatedCa">;
certificateDAL: Pick<TCertificateDALFactory, "find" | "countCertificatesInProject" | "findWithPrivateKeyInfo">;
certificateDAL: Pick<
TCertificateDALFactory,
| "find"
| "countCertificatesInProject"
| "findWithPrivateKeyInfo"
| "findActiveCertificatesForSync"
| "countActiveCertificatesForSync"
>;
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "getCertTemplatesByProjectId">;
pkiAlertDAL: Pick<TPkiAlertDALFactory, "find">;
pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "find">;
@@ -915,6 +922,7 @@ export const projectServiceFactory = ({
offset = 0,
friendlyName,
commonName,
forPkiSync = false,
actorId,
actorOrgId,
actorAuthMethod,
@@ -938,20 +946,35 @@ export const projectServiceFactory = ({
ProjectPermissionSub.Certificates
);
const certificates = await certificateDAL.findWithPrivateKeyInfo(
{
projectId,
...(friendlyName && { friendlyName }),
...(commonName && { commonName })
},
{ offset, limit, sort: [["notAfter", "desc"]] }
);
const certificates = forPkiSync
? await certificateDAL.findActiveCertificatesForSync(
{
projectId,
...(friendlyName && { friendlyName }),
...(commonName && { commonName })
},
{ offset, limit }
)
: await certificateDAL.findWithPrivateKeyInfo(
{
projectId,
...(friendlyName && { friendlyName }),
...(commonName && { commonName })
},
{ offset, limit, sort: [["notAfter", "desc"]] }
);
const count = await certificateDAL.countCertificatesInProject({
projectId,
friendlyName,
commonName
});
const count = forPkiSync
? await certificateDAL.countActiveCertificatesForSync({
projectId,
friendlyName,
commonName
})
: await certificateDAL.countCertificatesInProject({
projectId,
friendlyName,
commonName
});
return {
certificates,
@@ -142,6 +142,7 @@ export type TListProjectCertsDTO = {
limit: number;
friendlyName?: string;
commonName?: string;
forPkiSync?: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TListProjectAlertsDTO = TProjectPermission;