PKI revamp: frontend implementation

This commit is contained in:
Carlos Monastyrski
2025-10-10 01:50:10 -03:00
parent 4ddce06887
commit 15a6b8da04
57 changed files with 5271 additions and 298 deletions
@@ -33,7 +33,8 @@ export const getDefaultOnPremFeatures = () => {
enterpriseSecretSyncs: false,
enterpriseCertificateSyncs: false,
enterpriseAppConnections: true,
machineIdentityAuthTemplates: false
machineIdentityAuthTemplates: false,
pkiLegacyTemplates: false
};
};
@@ -67,6 +67,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
fips: false,
eventSubscriptions: false,
machineIdentityAuthTemplates: false,
pkiLegacyTemplates: false,
pam: false
});
@@ -78,6 +78,7 @@ export type TFeatureSet = {
enterpriseCertificateSyncs: false;
enterpriseAppConnections: false;
machineIdentityAuthTemplates: false;
pkiLegacyTemplates: false;
fips: false;
eventSubscriptions: false;
pam: false;
@@ -69,10 +69,24 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
schema: {
hide: false,
tags: [ApiDocsTags.PkiCertificateProfiles],
querystring: listCertificateProfilesSchema,
querystring: listCertificateProfilesSchema.extend({
includeMetrics: z.coerce.boolean().optional().default(false),
expiringDays: z.coerce.number().min(1).max(365).optional().default(7)
}),
response: {
200: z.object({
certificateProfiles: CertificateProfilesSchema.array(),
certificateProfiles: CertificateProfilesSchema.extend({
metrics: z
.object({
profileId: z.string(),
totalCertificates: z.number(),
activeCertificates: z.number(),
expiredCertificates: z.number(),
expiringCertificates: z.number(),
revokedCertificates: z.number()
})
.optional()
}).array(),
totalCount: z.number()
})
}
@@ -112,6 +126,10 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
hide: false,
tags: [ApiDocsTags.PkiCertificateProfiles],
params: getCertificateProfileByIdSchema,
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: CertificateProfilesSchema.extend({
@@ -145,6 +163,16 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
autoRenew: z.boolean(),
autoRenewDays: 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()
})
})
@@ -160,6 +188,20 @@ 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,
@@ -171,7 +213,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
}
});
return { certificateProfile };
return { certificateProfile: result };
}
});
@@ -322,7 +364,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
status: z.string(),
notBefore: z.date(),
notAfter: z.date(),
isRevoked: z.boolean(),
revokedAt: z.date().nullable().optional(),
createdAt: z.date()
})
)
@@ -343,45 +385,4 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
return { certificates };
}
});
server.route({
method: "GET",
url: "/:id/metrics",
config: {
rateLimit: readLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiCertificateProfiles],
params: getCertificateProfileByIdSchema,
querystring: z.object({
expiringDays: z.number().min(1).max(365).default(30)
}),
response: {
200: z.object({
metrics: z.object({
profileId: z.string(),
totalCertificates: z.number(),
activeCertificates: z.number(),
expiredCertificates: z.number(),
expiringCertificates: z.number(),
revokedCertificates: z.number()
})
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
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
});
return { metrics };
}
});
};
@@ -33,14 +33,6 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
notBefore: validateCaDateField.optional(),
notAfter: validateCaDateField.optional(),
altNames: validateAltNamesField.optional(),
organization: z.string().optional(),
organizationUnit: z.string().optional(),
locality: z.string().optional(),
state: z.string().optional(),
country: z.string().length(2).optional(),
email: z.string().email().optional(),
streetAddress: z.string().optional(),
postalCode: z.string().optional(),
signatureAlgorithm: z.string().optional(),
keyAlgorithm: z.string().optional()
}),
@@ -65,14 +57,6 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
profileId: req.body.profileId,
certificateRequest: {
commonName: req.body.commonName,
organization: req.body.organization,
organizationUnit: req.body.organizationUnit,
locality: req.body.locality,
state: req.body.state,
country: req.body.country,
email: req.body.email,
streetAddress: req.body.streetAddress,
postalCode: req.body.postalCode,
keyUsages: req.body.keyUsages,
extendedKeyUsages: req.body.extendedKeyUsages,
subjectAlternativeNames: req.body.altNames
@@ -221,14 +205,6 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
notBefore: validateCaDateField.optional(),
notAfter: validateCaDateField.optional(),
commonName: validateTemplateRegexField.optional(),
organization: z.string().optional(),
organizationUnit: z.string().optional(),
locality: z.string().optional(),
state: z.string().optional(),
country: z.string().length(2).optional(),
email: z.string().email().optional(),
streetAddress: z.string().optional(),
postalCode: z.string().optional(),
signatureAlgorithm: z.string().optional(),
keyAlgorithm: z.string().optional()
}),
@@ -280,14 +256,6 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
ttl: req.body.ttl
},
commonName: req.body.commonName,
organization: req.body.organization,
organizationUnit: req.body.organizationUnit,
locality: req.body.locality,
state: req.body.state,
country: req.body.country,
email: req.body.email,
streetAddress: req.body.streetAddress,
postalCode: req.body.postalCode,
keyUsages: req.body.keyUsages,
extendedKeyUsages: req.body.extendedKeyUsages,
notBefore: req.body.notBefore ? new Date(req.body.notBefore) : undefined,
@@ -127,11 +127,21 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
search?: string;
enrollmentType?: EnrollmentType;
caId?: string;
includeMetrics?: boolean;
expiringDays?: number;
} = {},
tx?: Knex
) => {
try {
const { offset = 0, limit = 20, search, enrollmentType, caId } = options;
const {
offset = 0,
limit = 20,
search,
enrollmentType,
caId,
includeMetrics = false,
expiringDays = 7
} = options;
let query = (tx || db)(TableName.CertificateProfile).where(
`${TableName.CertificateProfile}.projectId`,
@@ -155,6 +165,38 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
query = query.where(`${TableName.CertificateProfile}.caId`, caId);
}
if (includeMetrics) {
const now = new Date();
const expiringDate = new Date();
expiringDate.setDate(now.getDate() + expiringDays);
const certificateProfiles = await query
.leftJoin(TableName.Certificate, `${TableName.CertificateProfile}.id`, `${TableName.Certificate}.profileId`)
.select(
selectAllTableCols(TableName.CertificateProfile),
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.CertificateProfile}.id`)
.orderBy(`${TableName.CertificateProfile}.createdAt`, "desc")
.offset(offset)
.limit(limit);
return certificateProfiles;
}
const certificateProfiles = await query
.select(selectAllTableCols(TableName.CertificateProfile))
.orderBy(`${TableName.CertificateProfile}.createdAt`, "desc")
@@ -239,13 +281,13 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
if (status) {
switch (status) {
case "active":
query = query.where("notAfter", ">", now).where("isRevoked", false);
query = query.where("notAfter", ">", now).whereNull("revokedAt");
break;
case "expired":
query = query.where("notAfter", "<=", now).where("isRevoked", false);
query = query.where("notAfter", "<=", now).whereNull("revokedAt");
break;
case "revoked":
query = query.where("isRevoked", true);
query = query.whereNotNull("revokedAt");
break;
default:
break;
@@ -259,7 +301,7 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
.select((tx || db).ref("status").withSchema(TableName.Certificate))
.select((tx || db).ref("notBefore").withSchema(TableName.Certificate))
.select((tx || db).ref("notAfter").withSchema(TableName.Certificate))
.select((tx || db).ref("isRevoked").withSchema(TableName.Certificate))
.select((tx || db).ref("revokedAt").withSchema(TableName.Certificate))
.select((tx || db).ref("createdAt").withSchema(TableName.Certificate))
.orderBy("createdAt", "desc")
.offset(offset)
@@ -273,7 +315,7 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
const getProfileMetrics = async (
profileId: string,
expiringDays: number = 30,
expiringDays: number = 7,
tx?: Knex
): Promise<TCertificateProfileMetrics> => {
try {
@@ -285,13 +327,15 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
.where("profileId", profileId)
.select(
db.raw("COUNT(*) as total_certificates"),
db.raw("COUNT(CASE WHEN NOT is_revoked AND not_after > ? THEN 1 END) as active_certificates", [now]),
db.raw("COUNT(CASE WHEN NOT is_revoked AND not_after <= ? THEN 1 END) as expired_certificates", [now]),
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 NOT is_revoked AND not_after > ? AND not_after <= ? THEN 1 END) as expiring_certificates",
'COUNT(CASE WHEN "revokedAt" IS NULL AND "notAfter" > ? AND "notAfter" <= ? THEN 1 END) as expiring_certificates',
[now, expiringDate]
),
db.raw("COUNT(CASE WHEN is_revoked THEN 1 END) as revoked_certificates")
db.raw('COUNT(CASE WHEN "revokedAt" IS NOT NULL THEN 1 END) as revoked_certificates')
)
.first();
@@ -466,7 +466,9 @@ describe("CertificateProfileService", () => {
limit: 20,
search: undefined,
enrollmentType: undefined,
caId: undefined
caId: undefined,
includeMetrics: false,
expiringDays: 30
});
});
@@ -486,7 +488,51 @@ describe("CertificateProfileService", () => {
limit: 5,
search: "test",
enrollmentType: EnrollmentType.API,
caId: "ca-123"
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
});
});
});
@@ -23,7 +23,8 @@ import {
TCertificateProfileInsert,
TCertificateProfileMetrics,
TCertificateProfileUpdate,
TCertificateProfileWithConfigs
TCertificateProfileWithConfigs,
TCertificateProfileWithRawMetrics
} from "./certificate-profile-types";
export type TCertificateProfileCreateData = Omit<TCertificateProfileInsert, "estConfigId" | "apiConfigId"> & {
@@ -48,10 +49,6 @@ const convertDalToService = (dalResult: Record<string, unknown>): TCertificatePr
} as TCertificateProfile;
};
const convertDalArrayToService = (dalResults: Record<string, unknown>[]): TCertificateProfile[] => {
return dalResults.map(convertDalToService);
};
const validateEnrollmentConfig = async (data: {
enrollmentType: EnrollmentType;
estConfig?: TEstConfigData | null;
@@ -289,14 +286,18 @@ export const certificateProfileServiceFactory = ({
actorId,
actorAuthMethod,
actorOrgId,
profileId
profileId,
includeMetrics = false,
expiringDays = 30
}: {
actor: ActorType;
actorId: string;
actorAuthMethod: ActorAuthMethod;
actorOrgId: string;
profileId: string;
}): Promise<TCertificateProfile> => {
includeMetrics?: boolean;
expiringDays?: number;
}): Promise<TCertificateProfile & { metrics?: TCertificateProfileMetrics }> => {
const profile = await certificateProfileDAL.findById(profileId);
if (!profile) {
throw new NotFoundError({ message: "Certificate profile not found" });
@@ -315,7 +316,17 @@ export const certificateProfileServiceFactory = ({
ProjectPermissionSub.CertificateProfiles
);
return convertDalToService(profile);
const converted = convertDalToService(profile);
if (includeMetrics) {
const metrics = await certificateProfileDAL.getProfileMetrics(profileId, expiringDays);
return {
...converted,
metrics
};
}
return converted;
};
const getProfileByIdWithConfigs = async ({
@@ -401,7 +412,9 @@ export const certificateProfileServiceFactory = ({
limit = 20,
search,
enrollmentType,
caId
caId,
includeMetrics = false,
expiringDays = 30
}: {
actor: ActorType;
actorId: string;
@@ -413,8 +426,10 @@ export const certificateProfileServiceFactory = ({
search?: string;
enrollmentType?: EnrollmentType;
caId?: string;
includeMetrics?: boolean;
expiringDays?: number;
}): Promise<{
profiles: TCertificateProfile[];
profiles: (TCertificateProfile & { metrics?: TCertificateProfileMetrics })[];
totalCount: number;
}> => {
const { permission } = await permissionService.getProjectPermission({
@@ -435,7 +450,9 @@ export const certificateProfileServiceFactory = ({
limit,
search,
enrollmentType,
caId
caId,
includeMetrics,
expiringDays
});
const totalCount = await certificateProfileDAL.countByProjectId(projectId, {
@@ -444,8 +461,27 @@ export const certificateProfileServiceFactory = ({
caId
});
const convertedProfiles = profiles.map((profile) => {
const converted = convertDalToService(profile);
if (includeMetrics) {
const profileWithMetrics = profile as TCertificateProfileWithRawMetrics;
return {
...converted,
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 converted;
});
return {
profiles: convertDalArrayToService(profiles),
profiles: convertedProfiles,
totalCount
};
};
@@ -45,6 +45,7 @@ export type TCertificateProfileWithConfigs = TCertificateProfile & {
autoRenew: boolean;
autoRenewDays?: number;
};
metrics?: TCertificateProfileMetrics;
};
export interface TCertificateProfileMetrics {
@@ -63,6 +64,14 @@ export interface TCertificateProfileCertificate {
status: string;
notBefore: Date;
notAfter: Date;
isRevoked: boolean;
revokedAt: Date | null | undefined;
createdAt: Date;
}
export type TCertificateProfileWithRawMetrics = TCertificateProfile & {
total_certificates?: string;
active_certificates?: string;
expired_certificates?: string;
expiring_certificates?: string;
revoked_certificates?: string;
};
@@ -1,16 +1,6 @@
import { z } from "zod";
const attributeTypeSchema = z.enum([
"common_name",
"organization_name",
"organization_unit",
"locality",
"state",
"country",
"email",
"street_address",
"postal_code"
]);
const attributeTypeSchema = z.enum(["common_name"]);
const includeTypeSchema = z.enum(["mandatory", "optional", "prohibit"]);
@@ -137,14 +127,6 @@ export const deleteCertificateTemplateV2Schema = z.object({
export const certificateRequestSchema = z.object({
commonName: z.string().optional(),
organization: z.string().optional(),
organizationUnit: z.string().optional(),
locality: z.string().optional(),
state: z.string().optional(),
country: z.string().length(2).optional(),
email: z.string().email().optional(),
streetAddress: z.string().optional(),
postalCode: z.string().optional(),
keyUsages: z.array(z.string()).optional(),
extendedKeyUsages: z.array(z.string()).optional(),
subjectAlternativeNames: z
@@ -60,14 +60,6 @@ describe("CertificateTemplateV2Service", () => {
type: "common_name",
include: "mandatory",
value: ["example.com"]
},
{
type: "organization_name",
include: "optional"
},
{
type: "country",
include: "prohibit"
}
],
keyUsages: {
@@ -416,7 +408,6 @@ describe("CertificateTemplateV2Service", () => {
describe("validateCertificateRequest", () => {
const validRequest: TCertificateRequest = {
commonName: "example.com",
organization: "Example Corp",
keyUsages: ["digital_signature", "key_encipherment"],
extendedKeyUsages: ["server_auth"],
subjectAlternativeNames: [
@@ -458,15 +449,6 @@ describe("CertificateTemplateV2Service", () => {
expect(result.errors).toContain("common_name is mandatory but not provided in request");
});
it("should detect prohibited attributes", async () => {
const invalidRequest = { ...validRequest, country: "US" };
const result = await service.validateCertificateRequest("template-123", invalidRequest);
expect(result.isValid).toBe(false);
expect(result.errors).toContain("country is prohibited by template policy");
});
it("should validate attribute values against allowed list", async () => {
const invalidRequest = { ...validRequest, commonName: "forbidden.com" };
@@ -614,14 +596,6 @@ describe("CertificateTemplateV2Service", () => {
}
});
it("should allow optional attributes when not provided", async () => {
const requestWithoutOrg = { ...validRequest, organization: undefined };
const result = await service.validateCertificateRequest("template-123", requestWithoutOrg);
expect(result.isValid).toBe(true);
});
it("should allow optional key usages and extended key usages", async () => {
const requestWithOptionalUsages = {
...validRequest,
@@ -805,21 +779,14 @@ describe("CertificateTemplateV2Service", () => {
expect(result.isValid).toBe(true);
});
it("should handle template with all fields prohibited", async () => {
it("should handle template with SAN fields prohibited", async () => {
const prohibitTemplate = {
...sampleTemplate,
attributes: [
{
type: "organization_name",
include: "prohibit" as const
},
{
type: "locality",
include: "prohibit" as const
},
{
type: "country",
include: "prohibit" as const
type: "common_name",
include: "mandatory" as const,
value: ["example.com"]
}
],
keyUsages: {
@@ -845,9 +812,6 @@ describe("CertificateTemplateV2Service", () => {
const requestWithProhibited = {
commonName: "example.com",
organization: "Test Org",
locality: "Test City",
country: "US",
keyUsages: ["digital_signature"],
extendedKeyUsages: ["server_auth"],
subjectAlternativeNames: [
@@ -859,9 +823,6 @@ describe("CertificateTemplateV2Service", () => {
const result = await service.validateCertificateRequest("template-123", requestWithProhibited);
expect(result.isValid).toBe(false);
expect(result.errors).toContain("organization_name is prohibited by template policy");
expect(result.errors).toContain("locality is prohibited by template policy");
expect(result.errors).toContain("country is prohibited by template policy");
expect(result.errors).toContain("email SAN is prohibited by template policy");
expect(result.errors).toContain("uri SAN is prohibited by template policy");
});
@@ -874,16 +835,6 @@ describe("CertificateTemplateV2Service", () => {
type: "common_name",
include: "mandatory" as const,
value: ["example.com", "test.com"]
},
{
type: "organization_name",
include: "optional" as const,
value: ["Example Corp", "Test Corp"]
},
{
type: "country",
include: "mandatory" as const,
value: ["US", "CA"]
}
],
subjectAlternativeNames: [
@@ -898,8 +849,6 @@ describe("CertificateTemplateV2Service", () => {
const validConstrainedRequest = {
commonName: "example.com",
organization: "Example Corp",
country: "US",
keyUsages: ["digital_signature", "key_encipherment"],
extendedKeyUsages: ["server_auth"],
validity: { ttl: "30d" }
@@ -910,8 +859,6 @@ describe("CertificateTemplateV2Service", () => {
const invalidConstrainedRequest = {
commonName: "forbidden.com",
organization: "Forbidden Corp",
country: "FR",
keyUsages: ["digital_signature", "key_encipherment"],
extendedKeyUsages: ["server_auth"],
validity: { ttl: "30d" }
@@ -920,10 +867,6 @@ describe("CertificateTemplateV2Service", () => {
const invalidResult = await service.validateCertificateRequest("template-123", invalidConstrainedRequest);
expect(invalidResult.isValid).toBe(false);
expect(invalidResult.errors).toContain("common_name value 'forbidden.com' is not in allowed values list");
expect(invalidResult.errors).toContain(
"organization_name value 'Forbidden Corp' does not match allowed patterns: Example Corp, Test Corp"
);
expect(invalidResult.errors).toContain("country value 'FR' is not in allowed values list");
});
it("should validate SAN value constraints with multiple types", async () => {
@@ -72,22 +72,6 @@ export const certificateTemplateV2ServiceFactory = ({
switch (attrType) {
case "common_name":
return request.commonName;
case "organization_name":
return request.organization;
case "organization_unit":
return request.organizationUnit;
case "locality":
return request.locality;
case "state":
return request.state;
case "country":
return request.country;
case "email":
return request.email;
case "street_address":
return request.streetAddress;
case "postal_code":
return request.postalCode;
default:
return undefined;
}
@@ -145,8 +129,25 @@ export const certificateTemplateV2ServiceFactory = ({
if (!requestValue) {
errors.push(`${attrPolicy.type} is mandatory but not provided in request`);
} else if (attrPolicy.value && attrPolicy.value.length > 0) {
if (!attrPolicy.value.includes(requestValue)) {
errors.push(`${attrPolicy.type} value '${requestValue}' is not in allowed values list`);
// Check if the request value matches any allowed pattern
const hasWildcards = attrPolicy.value.some((val) => val.includes("*"));
const isValidValue = attrPolicy.value.some((allowedValue) => {
if (allowedValue.includes("*")) {
// Handle wildcard patterns
const pattern = allowedValue.replace(/\./g, "\\.").replace(/\*/g, ".*");
const regex = new RE2(`^${pattern}$`);
return regex.test(requestValue);
}
return allowedValue === requestValue;
});
if (!isValidValue) {
if (hasWildcards) {
errors.push(
`${attrPolicy.type} value '${requestValue}' does not match allowed patterns: ${attrPolicy.value.join(", ")}`
);
} else {
errors.push(`${attrPolicy.type} value '${requestValue}' is not in allowed values list`);
}
}
}
}
@@ -156,18 +157,24 @@ export const certificateTemplateV2ServiceFactory = ({
}
if (attrPolicy.include === "optional" && requestValue && attrPolicy.value && attrPolicy.value.length > 0) {
const hasWildcards = attrPolicy.value.some((val) => val.includes("*"));
const isValidValue = attrPolicy.value.some((allowedValue) => {
if (allowedValue.includes("*")) {
const pattern = allowedValue.replace(/\*/g, "[^.]*");
// Handle wildcard patterns - escape dots and replace * with .*
const pattern = allowedValue.replace(/\./g, "\\.").replace(/\*/g, ".*");
const regex = new RE2(`^${pattern}$`);
return regex.test(requestValue);
}
return allowedValue === requestValue;
});
if (!isValidValue) {
errors.push(
`${attrPolicy.type} value '${requestValue}' does not match allowed patterns: ${attrPolicy.value.join(", ")}`
);
if (hasWildcards) {
errors.push(
`${attrPolicy.type} value '${requestValue}' does not match allowed patterns: ${attrPolicy.value.join(", ")}`
);
} else {
errors.push(`${attrPolicy.type} value '${requestValue}' is not in allowed values list`);
}
}
}
});
@@ -216,9 +223,25 @@ export const certificateTemplateV2ServiceFactory = ({
if (requestSans.length === 0) {
errors.push(`${sanPolicy.type} SAN is mandatory but not provided in request`);
} else if (sanPolicy.value && sanPolicy.value.length > 0) {
const hasWildcards = sanPolicy.value.some((val) => val.includes("*"));
requestSans.forEach((san) => {
if (!sanPolicy.value!.includes(san.value)) {
errors.push(`${sanPolicy.type} SAN value '${san.value}' is not in allowed values list`);
const isValidValue = sanPolicy.value!.some((allowedValue) => {
if (allowedValue.includes("*")) {
// Handle wildcard patterns - escape dots and replace * with .*
const pattern = allowedValue.replace(/\./g, "\\.").replace(/\*/g, ".*");
const regex = new RE2(`^${pattern}$`);
return regex.test(san.value);
}
return allowedValue === san.value;
});
if (!isValidValue) {
if (hasWildcards) {
errors.push(
`${sanPolicy.type} SAN value '${san.value}' does not match allowed patterns: ${sanPolicy.value!.join(", ")}`
);
} else {
errors.push(`${sanPolicy.type} SAN value '${san.value}' is not in allowed values list`);
}
}
});
}
@@ -229,19 +252,25 @@ export const certificateTemplateV2ServiceFactory = ({
}
if (sanPolicy.include === "optional" && sanPolicy.value && sanPolicy.value.length > 0) {
const hasWildcards = sanPolicy.value.some((val) => val.includes("*"));
requestSans.forEach((san) => {
const isValidValue = sanPolicy.value!.some((allowedValue) => {
if (allowedValue.includes("*")) {
const pattern = allowedValue.replace(/\*/g, "[^.]*");
// Handle wildcard patterns - escape dots and replace * with .*
const pattern = allowedValue.replace(/\./g, "\\.").replace(/\*/g, ".*");
const regex = new RE2(`^${pattern}$`);
return regex.test(san.value);
}
return allowedValue === san.value;
});
if (!isValidValue) {
errors.push(
`${sanPolicy.type} SAN value '${san.value}' does not match allowed patterns: ${sanPolicy.value!.join(", ")}`
);
if (hasWildcards) {
errors.push(
`${sanPolicy.type} SAN value '${san.value}' does not match allowed patterns: ${sanPolicy.value!.join(", ")}`
);
} else {
errors.push(`${sanPolicy.type} SAN value '${san.value}' is not in allowed values list`);
}
}
});
}
@@ -2,16 +2,7 @@ import { TCertificateTemplatesV2, TCertificateTemplatesV2Insert } from "@app/db/
export interface TTemplateV2Policy {
attributes: Array<{
type:
| "common_name"
| "organization_name"
| "organization_unit"
| "locality"
| "state"
| "country"
| "email"
| "street_address"
| "postal_code";
type: "common_name";
include: "mandatory" | "optional" | "prohibit";
value?: string[];
}>;
@@ -97,14 +88,6 @@ export type TCertificateTemplateV2Update = Partial<
export interface TCertificateRequest {
commonName?: string;
organization?: string;
organizationUnit?: string;
locality?: string;
state?: string;
country?: string;
email?: string;
streetAddress?: string;
postalCode?: string;
keyUsages?: string[];
extendedKeyUsages?: string[];
subjectAlternativeNames?: Array<{
@@ -76,7 +76,6 @@ describe("CertificateV3Service", () => {
describe("issueCertificateFromProfile", () => {
const mockCertificateRequest = {
commonName: "test.example.com",
organization: "Test Org",
keyUsages: [CertKeyUsage.DIGITAL_SIGNATURE],
extendedKeyUsages: [CertExtendedKeyUsage.SERVER_AUTH],
validity: { ttl: "30d" },
@@ -291,14 +291,6 @@ export const certificateV3ServiceFactory = ({
const certificateRequest = {
commonName: certificateOrder.commonName,
organization: certificateOrder.organization,
organizationUnit: certificateOrder.organizationUnit,
locality: certificateOrder.locality,
state: certificateOrder.state,
country: certificateOrder.country,
email: certificateOrder.email,
streetAddress: certificateOrder.streetAddress,
postalCode: certificateOrder.postalCode,
keyUsages: certificateOrder.keyUsages,
extendedKeyUsages: certificateOrder.extendedKeyUsages,
subjectAlternativeNames: certificateOrder.identifiers.map((id) => ({
@@ -6,14 +6,6 @@ export type TIssueCertificateFromProfileDTO = {
profileId: string;
certificateRequest: {
commonName?: string;
organization?: string;
organizationUnit?: string;
locality?: string;
state?: string;
country?: string;
email?: string;
streetAddress?: string;
postalCode?: string;
keyUsages?: CertKeyUsage[];
extendedKeyUsages?: CertExtendedKeyUsage[];
subjectAlternativeNames?: Array<{
@@ -51,14 +43,6 @@ export type TOrderCertificateFromProfileDTO = {
ttl: string;
};
commonName?: string;
organization?: string;
organizationUnit?: string;
locality?: string;
state?: string;
country?: string;
email?: string;
streetAddress?: string;
postalCode?: string;
keyUsages?: CertKeyUsage[];
extendedKeyUsages?: CertExtendedKeyUsage[];
notBefore?: Date;