PKI UI improvements

This commit is contained in:
Carlos Monastyrski
2025-10-30 00:46:48 -03:00
parent c0654872b9
commit 2eb56d8770
37 changed files with 759 additions and 967 deletions
@@ -121,9 +121,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
limit: z.coerce.number().min(1).max(100).default(20),
search: z.string().optional(),
enrollmentType: z.nativeEnum(EnrollmentType).optional(),
caId: z.string().uuid().optional(),
includeMetrics: z.coerce.boolean().optional().default(false),
expiringDays: z.coerce.number().min(1).max(365).optional().default(7)
caId: z.string().uuid().optional()
}),
response: {
200: z.object({
@@ -195,10 +193,6 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
params: z.object({
id: z.string().uuid()
}),
querystring: z.object({
includeMetrics: z.coerce.boolean().optional().default(false),
expiringDays: z.coerce.number().min(1).max(365).optional().default(7)
}),
response: {
200: z.object({
certificateProfile: PkiCertificateProfilesSchema.extend({
@@ -232,16 +226,6 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
autoRenew: z.boolean(),
renewBeforeDays: z.number().optional()
})
.optional(),
metrics: z
.object({
profileId: z.string(),
totalCertificates: z.number(),
activeCertificates: z.number(),
expiredCertificates: z.number(),
expiringCertificates: z.number(),
revokedCertificates: z.number()
})
.optional()
})
})
@@ -257,20 +241,6 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
profileId: req.params.id
});
let result = certificateProfile;
if (req.query.includeMetrics) {
const metrics = await server.services.certificateProfile.getProfileMetrics({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
profileId: req.params.id,
expiringDays: req.query.expiringDays
});
result = { ...certificateProfile, metrics };
}
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: certificateProfile.projectId,
@@ -283,7 +253,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
}
});
return { certificateProfile: result };
return { certificateProfile };
}
});
@@ -89,6 +89,7 @@ const PkiSyncCertificateSchema = z.object({
updatedAt: z.date(),
certificateSerialNumber: z.string().optional(),
certificateCommonName: z.string().optional(),
certificateAltNames: z.string().optional(),
certificateStatus: z.string().optional(),
certificateNotBefore: z.date().optional(),
certificateNotAfter: z.date().optional(),
@@ -10,10 +10,8 @@ import {
TCertificateProfile,
TCertificateProfileCertificate,
TCertificateProfileInsert,
TCertificateProfileMetrics,
TCertificateProfileUpdate,
TCertificateProfileWithConfigs,
TCertificateProfileWithRawMetrics
TCertificateProfileWithConfigs
} from "./certificate-profile-types";
export type TCertificateProfileDALFactory = ReturnType<typeof certificateProfileDALFactory>;
@@ -203,21 +201,11 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
search?: string;
enrollmentType?: EnrollmentType;
caId?: string;
includeMetrics?: boolean;
expiringDays?: number;
} = {},
tx?: Knex
): Promise<TCertificateProfile[] | TCertificateProfileWithRawMetrics[] | TCertificateProfileWithConfigs[]> => {
): Promise<TCertificateProfile[] | TCertificateProfileWithConfigs[]> => {
try {
const {
offset = 0,
limit = 20,
search,
enrollmentType,
caId,
includeMetrics = false,
expiringDays = 7
} = options;
const { offset = 0, limit = 20, search, enrollmentType, caId } = options;
let baseQuery = (tx || db)(TableName.PkiCertificateProfile).where(
`${TableName.PkiCertificateProfile}.projectId`,
@@ -242,7 +230,7 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
baseQuery = baseQuery.where(`${TableName.PkiCertificateProfile}.caId`, caId);
}
let query = baseQuery
const query = baseQuery
.leftJoin(
TableName.PkiEstEnrollmentConfig,
`${TableName.PkiCertificateProfile}.estConfigId`,
@@ -267,52 +255,6 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
db.ref("renewBeforeDays").withSchema(TableName.PkiApiEnrollmentConfig).as("apiRenewBeforeDays")
);
if (includeMetrics) {
query = query.leftJoin(
TableName.Certificate,
`${TableName.PkiCertificateProfile}.id`,
`${TableName.Certificate}.profileId`
);
const now = new Date();
const expiringDate = new Date();
expiringDate.setDate(now.getDate() + expiringDays);
query = query
.select(
selectAllTableCols(TableName.PkiCertificateProfile),
db.ref("id").withSchema(TableName.PkiEstEnrollmentConfig).as("estId"),
db
.ref("disableBootstrapCaValidation")
.withSchema(TableName.PkiEstEnrollmentConfig)
.as("estDisableBootstrapCaValidation"),
db.ref("hashedPassphrase").withSchema(TableName.PkiEstEnrollmentConfig).as("estHashedPassphrase"),
db.ref("encryptedCaChain").withSchema(TableName.PkiEstEnrollmentConfig).as("estEncryptedCaChain"),
db.ref("id").withSchema(TableName.PkiApiEnrollmentConfig).as("apiId"),
db.ref("autoRenew").withSchema(TableName.PkiApiEnrollmentConfig).as("apiAutoRenew"),
db.ref("renewBeforeDays").withSchema(TableName.PkiApiEnrollmentConfig).as("apiRenewBeforeDays"),
db.raw("COUNT(certificates.id) as total_certificates"),
db.raw(
'COUNT(CASE WHEN certificates."revokedAt" IS NULL AND certificates."notAfter" > ? THEN 1 END) as active_certificates',
[expiringDate]
),
db.raw(
'COUNT(CASE WHEN certificates."revokedAt" IS NULL AND certificates."notAfter" <= ? THEN 1 END) as expired_certificates',
[now]
),
db.raw(
'COUNT(CASE WHEN certificates."revokedAt" IS NULL AND certificates."notAfter" > ? AND certificates."notAfter" <= ? THEN 1 END) as expiring_certificates',
[now, expiringDate]
),
db.raw('COUNT(CASE WHEN certificates."revokedAt" IS NOT NULL THEN 1 END) as revoked_certificates')
)
.groupBy(
`${TableName.PkiCertificateProfile}.id`,
`${TableName.PkiEstEnrollmentConfig}.id`,
`${TableName.PkiApiEnrollmentConfig}.id`
);
}
const results = (await query
.orderBy(`${TableName.PkiCertificateProfile}.createdAt`, "desc")
.offset(offset)
@@ -353,17 +295,6 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
apiConfig
};
if (includeMetrics) {
return {
...baseProfile,
total_certificates: result.total_certificates,
active_certificates: result.active_certificates,
expired_certificates: result.expired_certificates,
expiring_certificates: result.expiring_certificates,
revoked_certificates: result.revoked_certificates
} as TCertificateProfileWithRawMetrics & TCertificateProfileWithConfigs;
}
return baseProfile as TCertificateProfileWithConfigs;
});
} catch (error) {
@@ -485,45 +416,6 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
}
};
const getProfileMetrics = async (
profileId: string,
expiringDays: number = 7,
tx?: Knex
): Promise<TCertificateProfileMetrics> => {
try {
const now = new Date();
const expiringDate = new Date();
expiringDate.setDate(now.getDate() + expiringDays);
const metrics = await (tx || db)(TableName.Certificate)
.where("profileId", profileId)
.select(
db.raw("COUNT(*) as total_certificates"),
db.raw('COUNT(CASE WHEN "revokedAt" IS NULL AND "notAfter" > ? THEN 1 END) as active_certificates', [
expiringDate
]),
db.raw('COUNT(CASE WHEN "revokedAt" IS NULL AND "notAfter" <= ? THEN 1 END) as expired_certificates', [now]),
db.raw(
'COUNT(CASE WHEN "revokedAt" IS NULL AND "notAfter" > ? AND "notAfter" <= ? THEN 1 END) as expiring_certificates',
[now, expiringDate]
),
db.raw('COUNT(CASE WHEN "revokedAt" IS NOT NULL THEN 1 END) as revoked_certificates')
)
.first();
return {
profileId,
totalCertificates: parseInt(String((metrics as Record<string, unknown>)?.total_certificates || 0), 10),
activeCertificates: parseInt(String((metrics as Record<string, unknown>)?.active_certificates || 0), 10),
expiredCertificates: parseInt(String((metrics as Record<string, unknown>)?.expired_certificates || 0), 10),
expiringCertificates: parseInt(String((metrics as Record<string, unknown>)?.expiring_certificates || 0), 10),
revokedCertificates: parseInt(String((metrics as Record<string, unknown>)?.revoked_certificates || 0), 10)
};
} catch (error) {
throw new DatabaseError({ error, name: "Get certificate profile metrics" });
}
};
const isProfileInUse = async (profileId: string, tx?: Knex) => {
try {
const doc = await (tx || db)(TableName.Certificate).where("profileId", profileId).count("*").first();
@@ -546,7 +438,6 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
countByProjectId,
findByNameAndProjectId,
getCertificatesByProfile,
getProfileMetrics,
isProfileInUse
};
};
@@ -127,8 +127,3 @@ export const listCertificatesByProfileSchema = z.object({
status: z.enum(["active", "expired", "revoked"]).optional(),
search: z.string().optional()
});
export const getCertificateProfileMetricsSchema = z.object({
profileId: z.string().uuid(),
expiringDays: z.coerce.number().min(1).max(365).default(30)
});
@@ -47,7 +47,6 @@ describe("CertificateProfileService", () => {
findByNameAndProjectId: vi.fn(),
findByIdWithConfigs: vi.fn(),
getCertificatesByProfile: vi.fn(),
getProfileMetrics: vi.fn(),
isProfileInUse: vi.fn(),
transaction: vi.fn(),
find: vi.fn(),
@@ -493,9 +492,7 @@ describe("CertificateProfileService", () => {
limit: 20,
search: undefined,
enrollmentType: undefined,
caId: undefined,
includeMetrics: false,
expiringDays: 30
caId: undefined
});
});
@@ -515,51 +512,7 @@ describe("CertificateProfileService", () => {
limit: 5,
search: "test",
enrollmentType: EnrollmentType.API,
caId: "ca-123",
includeMetrics: false,
expiringDays: 30
});
});
it("should list profiles with metrics when includeMetrics is true", async () => {
const mockProfilesWithMetrics = [
{
...sampleProfile,
total_certificates: 10,
active_certificates: 8,
expired_certificates: 1,
expiring_certificates: 1,
revoked_certificates: 0
}
];
(mockCertificateProfileDAL.findByProjectId as any).mockResolvedValue(mockProfilesWithMetrics);
const result = await service.listProfiles({
...mockActor,
projectId: "project-123",
includeMetrics: true,
expiringDays: 15
});
expect(result.profiles).toHaveLength(1);
expect(result.profiles[0]).toHaveProperty("metrics");
expect(result.profiles[0].metrics).toEqual({
profileId: sampleProfile.id,
totalCertificates: 10,
activeCertificates: 8,
expiredCertificates: 1,
expiringCertificates: 1,
revokedCertificates: 0
});
expect(mockCertificateProfileDAL.findByProjectId).toHaveBeenCalledWith("project-123", {
offset: 0,
limit: 20,
search: undefined,
enrollmentType: undefined,
caId: undefined,
includeMetrics: true,
expiringDays: 15
caId: "ca-123"
});
});
});
@@ -659,54 +612,6 @@ describe("CertificateProfileService", () => {
});
});
describe("getProfileMetrics", () => {
const mockMetrics = {
profileId: "profile-123",
totalCertificates: 10,
activeCertificates: 8,
expiredCertificates: 1,
expiringCertificates: 2,
revokedCertificates: 1
};
beforeEach(() => {
(mockCertificateProfileDAL.findById as any).mockResolvedValue(sampleProfile);
(mockCertificateProfileDAL.getProfileMetrics as any).mockResolvedValue(mockMetrics);
});
it("should get profile metrics successfully", async () => {
const result = await service.getProfileMetrics({
...mockActor,
profileId: "profile-123"
});
expect(result).toEqual(mockMetrics);
expect(mockCertificateProfileDAL.findById).toHaveBeenCalledWith("profile-123");
expect(mockCertificateProfileDAL.getProfileMetrics).toHaveBeenCalledWith("profile-123", 30);
});
it("should get profile metrics with custom expiring days", async () => {
await service.getProfileMetrics({
...mockActor,
profileId: "profile-123",
expiringDays: 60
});
expect(mockCertificateProfileDAL.getProfileMetrics).toHaveBeenCalledWith("profile-123", 60);
});
it("should throw NotFoundError when profile not found", async () => {
(mockCertificateProfileDAL.findById as any).mockResolvedValue(null);
await expect(
service.getProfileMetrics({
...mockActor,
profileId: "profile-123"
})
).rejects.toThrow(NotFoundError);
});
});
describe("comprehensive certificate profile scenarios", () => {
describe("profile configuration validation", () => {
it("should validate EST enrollment configuration", async () => {
@@ -929,53 +834,6 @@ describe("CertificateProfileService", () => {
});
});
describe("metrics and monitoring", () => {
it("should calculate profile metrics correctly", async () => {
const detailedMetrics = {
profileId: "profile-123",
totalCertificates: 50,
activeCertificates: 40,
expiredCertificates: 5,
expiringCertificates: 3,
revokedCertificates: 2
};
(mockCertificateProfileDAL.findById as any).mockResolvedValue(sampleProfile);
(mockCertificateProfileDAL.getProfileMetrics as any).mockResolvedValue(detailedMetrics);
const result = await service.getProfileMetrics({
...mockActor,
profileId: "profile-123",
expiringDays: 14
});
expect(result).toEqual(detailedMetrics);
expect(mockCertificateProfileDAL.getProfileMetrics).toHaveBeenCalledWith("profile-123", 14);
});
it("should handle zero certificate metrics", async () => {
const emptyMetrics = {
profileId: "profile-123",
totalCertificates: 0,
activeCertificates: 0,
expiredCertificates: 0,
expiringCertificates: 0,
revokedCertificates: 0
};
(mockCertificateProfileDAL.findById as any).mockResolvedValue(sampleProfile);
(mockCertificateProfileDAL.getProfileMetrics as any).mockResolvedValue(emptyMetrics);
const result = await service.getProfileMetrics({
...mockActor,
profileId: "profile-123"
});
expect(result.totalCertificates).toBe(0);
expect(result.activeCertificates).toBe(0);
});
});
describe("error scenarios", () => {
it("should handle database connection errors gracefully", async () => {
(mockCertificateProfileDAL.findById as any).mockRejectedValue(new Error("Database connection failed"));
@@ -27,10 +27,8 @@ import {
TCertificateProfile,
TCertificateProfileCertificate,
TCertificateProfileInsert,
TCertificateProfileMetrics,
TCertificateProfileUpdate,
TCertificateProfileWithConfigs,
TCertificateProfileWithRawMetrics
TCertificateProfileWithConfigs
} from "./certificate-profile-types";
const validateAndEncryptPemCaChain = async (
@@ -361,18 +359,14 @@ export const certificateProfileServiceFactory = ({
actorId,
actorAuthMethod,
actorOrgId,
profileId,
includeMetrics = false,
expiringDays = 30
profileId
}: {
actor: ActorType;
actorId: string;
actorAuthMethod: ActorAuthMethod;
actorOrgId: string;
profileId: string;
includeMetrics?: boolean;
expiringDays?: number;
}): Promise<TCertificateProfile & { metrics?: TCertificateProfileMetrics }> => {
}): Promise<TCertificateProfile> => {
const profile = await certificateProfileDAL.findById(profileId);
if (!profile) {
throw new NotFoundError({ message: "Certificate profile not found" });
@@ -393,14 +387,6 @@ export const certificateProfileServiceFactory = ({
const converted = convertDalToService(profile);
if (includeMetrics) {
const metrics = await certificateProfileDAL.getProfileMetrics(profileId, expiringDays);
return {
...converted,
metrics
};
}
return converted;
};
@@ -506,9 +492,7 @@ export const certificateProfileServiceFactory = ({
limit = 20,
search,
enrollmentType,
caId,
includeMetrics = false,
expiringDays = 30
caId
}: {
actor: ActorType;
actorId: string;
@@ -520,10 +504,8 @@ export const certificateProfileServiceFactory = ({
search?: string;
enrollmentType?: EnrollmentType;
caId?: string;
includeMetrics?: boolean;
expiringDays?: number;
}): Promise<{
profiles: (TCertificateProfileWithConfigs & { metrics?: TCertificateProfileMetrics })[];
profiles: TCertificateProfileWithConfigs[];
totalCount: number;
}> => {
const { permission } = await permissionService.getProjectPermission({
@@ -544,9 +526,7 @@ export const certificateProfileServiceFactory = ({
limit,
search,
enrollmentType,
caId,
includeMetrics,
expiringDays
caId
});
const totalCount = await certificateProfileDAL.countByProjectId(projectId, {
@@ -591,27 +571,12 @@ export const certificateProfileServiceFactory = ({
}
const converted = convertDalToService(profileWithConfigs);
let result: TCertificateProfileWithConfigs & { metrics?: TCertificateProfileMetrics } = {
const result: TCertificateProfileWithConfigs = {
...converted,
estConfig: decryptedEstConfig,
apiConfig: profileWithConfigs.apiConfig
};
if (includeMetrics) {
const profileWithMetrics = profile as TCertificateProfileWithRawMetrics;
result = {
...result,
metrics: {
profileId: converted.id,
totalCertificates: parseInt(String(profileWithMetrics.total_certificates || 0), 10),
activeCertificates: parseInt(String(profileWithMetrics.active_certificates || 0), 10),
expiredCertificates: parseInt(String(profileWithMetrics.expired_certificates || 0), 10),
expiringCertificates: parseInt(String(profileWithMetrics.expiring_certificates || 0), 10),
revokedCertificates: parseInt(String(profileWithMetrics.revoked_certificates || 0), 10)
}
};
}
return result;
})
);
@@ -709,43 +674,6 @@ export const certificateProfileServiceFactory = ({
return certificates;
};
const getProfileMetrics = async ({
actor,
actorId,
actorAuthMethod,
actorOrgId,
profileId,
expiringDays = 30
}: {
actor: ActorType;
actorId: string;
actorAuthMethod: ActorAuthMethod;
actorOrgId: string;
profileId: string;
expiringDays?: number;
}): Promise<TCertificateProfileMetrics> => {
const profile = await certificateProfileDAL.findById(profileId);
if (!profile) {
throw new NotFoundError({ message: "Certificate profile not found" });
}
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: profile.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateProfileActions.Read,
ProjectPermissionSub.CertificateProfiles
);
const metrics = await certificateProfileDAL.getProfileMetrics(profileId, expiringDays);
return metrics;
};
const getEstConfigurationByProfile = async (
params:
| {
@@ -818,7 +746,6 @@ export const certificateProfileServiceFactory = ({
listProfiles,
deleteProfile,
getProfileCertificates,
getProfileMetrics,
getEstConfigurationByProfile
};
};
@@ -54,18 +54,8 @@ export type TCertificateProfileWithConfigs = TCertificateProfile & {
autoRenew: boolean;
renewBeforeDays?: number;
};
metrics?: TCertificateProfileMetrics;
};
export interface TCertificateProfileMetrics {
profileId: string;
totalCertificates: number;
activeCertificates: number;
expiredCertificates: number;
expiringCertificates: number;
revokedCertificates: number;
}
export interface TCertificateProfileCertificate {
id: string;
serialNumber: string;
@@ -76,11 +66,3 @@ export interface TCertificateProfileCertificate {
revokedAt: Date | null;
createdAt: Date;
}
export type TCertificateProfileWithRawMetrics = TCertificateProfile & {
total_certificates?: string;
active_certificates?: string;
expired_certificates?: string;
expiring_certificates?: string;
revoked_certificates?: string;
};
@@ -180,6 +180,7 @@ export const certificateSyncDALFactory = (db: TDbClient) => {
certificateDetails: (TCertificateSyncs & {
certificateSerialNumber?: string;
certificateCommonName?: string;
certificateAltNames?: string;
certificateStatus?: string;
certificateNotBefore?: Date;
certificateNotAfter?: Date;
@@ -211,6 +212,7 @@ export const certificateSyncDALFactory = (db: TDbClient) => {
.select(
db.ref("serialNumber").withSchema(TableName.Certificate).as("certificateSerialNumber"),
db.ref("commonName").withSchema(TableName.Certificate).as("certificateCommonName"),
db.ref("altNames").withSchema(TableName.Certificate).as("certificateAltNames"),
db.ref("status").withSchema(TableName.Certificate).as("certificateStatus"),
db.ref("notBefore").withSchema(TableName.Certificate).as("certificateNotBefore"),
db.ref("notAfter").withSchema(TableName.Certificate).as("certificateNotAfter"),
@@ -229,6 +231,7 @@ export const certificateSyncDALFactory = (db: TDbClient) => {
const certificateDetails = (await query) as (TCertificateSyncs & {
certificateSerialNumber?: string;
certificateCommonName?: string;
certificateAltNames?: string;
certificateStatus?: string;
certificateNotBefore?: Date;
certificateNotAfter?: Date;
@@ -520,7 +520,7 @@ export const awsCertificateManagerPkiSyncFactory = ({
try {
// Small delay to ensure AWS ACM has processed the certificate import
await new Promise<void>((resolve) => {
setTimeout(() => resolve(), 100);
setTimeout(() => resolve(), 500);
});
await withRateLimitRetry(
@@ -606,6 +606,7 @@ export const pkiSyncServiceFactory = ({
updatedAt: detail.updatedAt,
certificateSerialNumber: detail.certificateSerialNumber || undefined,
certificateCommonName: detail.certificateCommonName || undefined,
certificateAltNames: detail.certificateAltNames || undefined,
certificateStatus: detail.certificateStatus || undefined,
certificateNotBefore: detail.certificateNotBefore || undefined,
certificateNotAfter: detail.certificateNotAfter || undefined,
@@ -170,13 +170,14 @@ export type TPkiSyncCertificate = {
lastSyncedAt?: Date;
createdAt: Date;
updatedAt: Date;
certificate?: {
serialNumber: string;
commonName: string;
status: string;
notBefore: Date;
notAfter: Date;
};
certificateSerialNumber?: string;
certificateCommonName?: string;
certificateAltNames?: string;
certificateStatus?: string;
certificateNotBefore?: Date;
certificateNotAfter?: Date;
pkiSyncName?: string;
pkiSyncDestination?: string;
};
export type TPkiSyncRaw = NonNullable<Awaited<ReturnType<TPkiSyncDALFactory["findById"]>>>;