PKI revamp - backend initial changes

This commit is contained in:
Carlos Monastyrski
2025-10-09 00:29:55 -03:00
parent c1833b5cbd
commit c4dcc3dd83
47 changed files with 9624 additions and 26 deletions
@@ -0,0 +1,219 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { TCertificateTemplatesV2Insert } from "@app/db/schemas/certificate-templates-v2";
import { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex";
import {
TCertificateTemplateV2,
TCertificateTemplateV2Insert,
TCertificateTemplateV2Update
} from "./certificate-template-v2-types";
export type TCertificateTemplateV2DALFactory = ReturnType<typeof certificateTemplateV2DALFactory>;
export const certificateTemplateV2DALFactory = (db: TDbClient) => {
const certificateTemplateV2Orm = ormify(db, TableName.CertificateTemplateV2);
const serializeJsonFields = (data: TCertificateTemplateV2Insert | TCertificateTemplateV2Update) => {
const serialized = { ...data } as Record<string, unknown>;
const jsonFields = [
"attributes",
"keyUsages",
"extendedKeyUsages",
"subjectAlternativeNames",
"validity",
"signatureAlgorithm",
"keyAlgorithm"
];
jsonFields.forEach((field) => {
const value = (data as Record<string, unknown>)[field];
if (value !== undefined) {
serialized[field] = JSON.stringify(value);
}
});
return serialized;
};
const parseJsonFields = (raw: Record<string, unknown>): TCertificateTemplateV2 => {
const jsonFields = [
"attributes",
"keyUsages",
"extendedKeyUsages",
"subjectAlternativeNames",
"validity",
"signatureAlgorithm",
"keyAlgorithm"
];
const parsed = { ...raw };
jsonFields.forEach((field) => {
const value = raw[field];
if (value) {
parsed[field] = typeof value === "string" ? JSON.parse(value) : value;
}
});
return parsed as TCertificateTemplateV2;
};
const create = async (data: TCertificateTemplateV2Insert, tx?: Knex) => {
try {
const serializedData = serializeJsonFields(data);
const [certificateTemplateV2] = await (tx || db)(TableName.CertificateTemplateV2)
.insert(serializedData as TCertificateTemplatesV2Insert)
.returning("*");
if (!certificateTemplateV2) {
throw new Error("Failed to create certificate template v2");
}
return parseJsonFields(certificateTemplateV2);
} catch (error) {
throw new DatabaseError({ error, name: "Create certificate template v2" });
}
};
const updateById = async (id: string, data: TCertificateTemplateV2Update, tx?: Knex) => {
try {
const serializedData = serializeJsonFields(data);
const [certificateTemplateV2] = await (tx || db)(TableName.CertificateTemplateV2)
.where({ id })
.update(serializedData)
.returning("*");
if (!certificateTemplateV2) {
return null;
}
return parseJsonFields(certificateTemplateV2);
} catch (error) {
throw new DatabaseError({ error, name: "Update certificate template v2" });
}
};
const deleteById = async (id: string, tx?: Knex) => {
try {
const [certificateTemplateV2] = await (tx || db)(TableName.CertificateTemplateV2)
.where({ id })
.del()
.returning("*");
return certificateTemplateV2;
} catch (error) {
throw new DatabaseError({ error, name: "Delete certificate template v2" });
}
};
const findById = async (id: string, tx?: Knex) => {
try {
const certificateTemplateV2 = await (tx || db)(TableName.CertificateTemplateV2).where({ id }).first();
if (!certificateTemplateV2) {
return null;
}
return parseJsonFields(certificateTemplateV2);
} catch (error) {
throw new DatabaseError({ error, name: "Find certificate template v2 by id" });
}
};
const findByProjectId = async (
projectId: string,
options: {
offset?: number;
limit?: number;
search?: string;
} = {},
tx?: Knex
) => {
try {
const { offset = 0, limit = 20, search } = options;
let query = (tx || db)(TableName.CertificateTemplateV2).where({ projectId });
if (search) {
query = query.where((builder) => {
void builder.whereILike("name", `%${search}%`).orWhereILike("description", `%${search}%`);
});
}
const certificateTemplatesV2 = await query.orderBy("createdAt", "desc").offset(offset).limit(limit);
return certificateTemplatesV2.map((template: Record<string, unknown>) => parseJsonFields(template));
} catch (error) {
throw new DatabaseError({ error, name: "Find certificate templates v2 by project id" });
}
};
const countByProjectId = async (
projectId: string,
options: {
search?: string;
} = {},
tx?: Knex
) => {
try {
const { search } = options;
let query = (tx || db)(TableName.CertificateTemplateV2).where({ projectId });
if (search) {
query = query.where((builder) => {
void builder.whereILike("name", `%${search}%`).orWhereILike("description", `%${search}%`);
});
}
const result = await query.count("*").first();
return parseInt((result as unknown as { count: string }).count || "0", 10);
} catch (error) {
throw new DatabaseError({ error, name: "Count certificate templates v2 by project id" });
}
};
const findByNameAndProjectId = async (name: string, projectId: string, tx?: Knex) => {
try {
const certificateTemplateV2 = await (tx || db)(TableName.CertificateTemplateV2)
.where({ name, projectId })
.first();
if (!certificateTemplateV2) {
return null;
}
return parseJsonFields(certificateTemplateV2);
} catch (error) {
throw new DatabaseError({ error, name: "Find certificate template v2 by name and project id" });
}
};
const isTemplateInUse = async (templateId: string, tx?: Knex) => {
try {
const profileCount = await (tx || db)(TableName.CertificateProfile)
.where({ certificateTemplateId: templateId })
.count("*")
.first();
return parseInt(profileCount || "0", 10) > 0;
} catch (error) {
throw new DatabaseError({ error, name: "Check if certificate template v2 is in use" });
}
};
return {
...certificateTemplateV2Orm,
create,
updateById,
deleteById,
findById,
findByProjectId,
countByProjectId,
findByNameAndProjectId,
isTemplateInUse
};
};
@@ -0,0 +1,170 @@
import { z } from "zod";
const attributeTypeSchema = z.enum([
"common_name",
"organization_name",
"organization_unit",
"locality",
"state",
"country",
"email",
"street_address",
"postal_code"
]);
const includeTypeSchema = z.enum(["mandatory", "optional", "prohibit"]);
const sanTypeSchema = z.enum(["dns_name", "ip_address", "email", "uri"]);
const durationUnitSchema = z.enum(["days", "months", "years"]);
export const templateV2AttributeSchema = z
.object({
type: attributeTypeSchema,
include: includeTypeSchema,
value: z.array(z.string()).optional()
})
.refine(
(data) => {
if (data.include === "mandatory" && (!data.value || data.value.length > 1)) {
return false;
}
return true;
},
{
message: "Mandatory attributes can only have one value or no value (empty)"
}
);
export const templateV2KeyUsagesSchema = z.object({
requiredUsages: z.object({
all: z.array(z.string())
}),
optionalUsages: z.object({
all: z.array(z.string())
})
});
export const templateV2ExtendedKeyUsagesSchema = z.object({
requiredUsages: z.object({
all: z.array(z.string())
}),
optionalUsages: z.object({
all: z.array(z.string())
})
});
export const templateV2SanSchema = z
.object({
type: sanTypeSchema,
include: includeTypeSchema,
value: z.array(z.string()).optional()
})
.refine(
(data) => {
if (data.include === "mandatory" && (!data.value || data.value.length > 1)) {
return false;
}
return true;
},
{
message: "Mandatory SANs can only have one value or no value (empty)"
}
);
export const templateV2ValiditySchema = z.object({
maxDuration: z.object({
value: z.number().positive(),
unit: durationUnitSchema
}),
minDuration: z
.object({
value: z.number().positive(),
unit: durationUnitSchema
})
.optional()
});
export const templateV2SignatureAlgorithmSchema = z.object({
allowedAlgorithms: z.array(z.string()).min(1),
defaultAlgorithm: z.string()
});
export const templateV2KeyAlgorithmSchema = z.object({
allowedKeyTypes: z.array(z.string()).min(1),
defaultKeyType: z.string()
});
export const createCertificateTemplateV2Schema = z.object({
projectId: z.string().min(1),
name: z.string().min(1).max(255),
description: z.string().max(1000).optional(),
attributes: z.array(templateV2AttributeSchema).optional(),
keyUsages: templateV2KeyUsagesSchema.optional(),
extendedKeyUsages: templateV2ExtendedKeyUsagesSchema.optional(),
subjectAlternativeNames: z.array(templateV2SanSchema).optional(),
validity: templateV2ValiditySchema.optional(),
signatureAlgorithm: templateV2SignatureAlgorithmSchema.optional(),
keyAlgorithm: templateV2KeyAlgorithmSchema.optional()
});
export const updateCertificateTemplateV2Schema = z.object({
name: z.string().min(1).max(255).optional(),
description: z.string().max(1000).optional(),
attributes: z.array(templateV2AttributeSchema).optional(),
keyUsages: templateV2KeyUsagesSchema.optional(),
extendedKeyUsages: templateV2ExtendedKeyUsagesSchema.optional(),
subjectAlternativeNames: z.array(templateV2SanSchema).optional(),
validity: templateV2ValiditySchema.optional(),
signatureAlgorithm: templateV2SignatureAlgorithmSchema.optional(),
keyAlgorithm: templateV2KeyAlgorithmSchema.optional()
});
export const getCertificateTemplateV2ByIdSchema = z.object({
id: z.string().uuid()
});
export const listCertificateTemplatesV2Schema = z.object({
projectId: z.string().min(1),
offset: z.coerce.number().min(0).default(0),
limit: z.coerce.number().min(1).max(100).default(20),
search: z.string().optional()
});
export const deleteCertificateTemplateV2Schema = z.object({
id: z.string().uuid()
});
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
.array(
z.object({
type: sanTypeSchema,
value: z.string()
})
)
.optional(),
validity: z
.object({
ttl: z.string()
})
.optional(),
signatureAlgorithm: z.string().optional(),
keyAlgorithm: z.string().optional()
});
export const validateCertificateRequestSchema = z.object({
templateId: z.string().uuid(),
request: certificateRequestSchema
});
@@ -0,0 +1,548 @@
import { ForbiddenError } from "@casl/ability";
import RE2 from "re2";
import { ActionProjectType } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import {
ProjectPermissionPkiTemplateActions,
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
import { TCertificateTemplateV2DALFactory } from "./certificate-template-v2-dal";
import {
TCertificateRequest,
TCertificateTemplateV2,
TCertificateTemplateV2Insert,
TCertificateTemplateV2Update,
TTemplateV2Policy,
TTemplateValidationResult
} from "./certificate-template-v2-types";
type TCertificateTemplateV2ServiceFactoryDep = {
certificateTemplateV2DAL: TCertificateTemplateV2DALFactory;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
};
export type TCertificateTemplateV2ServiceFactory = ReturnType<typeof certificateTemplateV2ServiceFactory>;
export const certificateTemplateV2ServiceFactory = ({
certificateTemplateV2DAL,
permissionService
}: TCertificateTemplateV2ServiceFactoryDep) => {
const parseTTL = (ttl: string): number => {
const regex = new RE2("^(\\d+)([dmyh])$");
const match = regex.exec(ttl);
if (!match) {
throw new Error(`Invalid TTL format: ${ttl}`);
}
const value = parseInt(match[1], 10);
const unit = match[2];
switch (unit) {
case "h":
return value * 60 * 60 * 1000;
case "d":
return value * 24 * 60 * 60 * 1000;
case "m":
return value * 30 * 24 * 60 * 60 * 1000;
case "y":
return value * 365 * 24 * 60 * 60 * 1000;
default:
throw new Error(`Unsupported TTL unit: ${unit}`);
}
};
const convertToMilliseconds = (value: number, unit: "days" | "months" | "years"): number => {
switch (unit) {
case "days":
return value * 24 * 60 * 60 * 1000;
case "months":
return value * 30 * 24 * 60 * 60 * 1000;
case "years":
return value * 365 * 24 * 60 * 60 * 1000;
default:
throw new Error(`Unsupported duration unit: ${unit as string}`);
}
};
const getRequestAttributeValue = (request: TCertificateRequest, attrType: string): string | undefined => {
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;
}
};
const validateTemplatePolicy = (policy: Partial<TTemplateV2Policy>): void => {
if (!policy) {
throw new Error("Template policy is required");
}
if (!policy.attributes || policy.attributes.length === 0) {
throw new Error("Template policy must include attributes array");
}
if (!policy.keyUsages || !policy.keyUsages.requiredUsages || !policy.keyUsages.optionalUsages) {
throw new Error("Template policy must include valid key usages configuration");
}
if (policy.signatureAlgorithm) {
if (!policy.signatureAlgorithm.allowedAlgorithms.includes(policy.signatureAlgorithm.defaultAlgorithm)) {
throw new Error("Default signature algorithm must be in allowed algorithms list");
}
}
if (policy.keyAlgorithm) {
if (!policy.keyAlgorithm.allowedKeyTypes.includes(policy.keyAlgorithm.defaultKeyType)) {
throw new Error("Default key algorithm must be in allowed key types list");
}
}
};
const hasAnyPolicyField = (data: TCertificateTemplateV2Update): boolean => {
return !!(
data.attributes ||
data.keyUsages ||
data.extendedKeyUsages ||
data.subjectAlternativeNames ||
data.validity ||
data.signatureAlgorithm ||
data.keyAlgorithm
);
};
const validateRequestAgainstPolicy = (
template: TCertificateTemplateV2,
request: TCertificateRequest
): TTemplateValidationResult => {
const errors: string[] = [];
const warnings: string[] = [];
template.attributes?.forEach((attrPolicy) => {
const requestValue = getRequestAttributeValue(request, attrPolicy.type);
if (attrPolicy.include === "mandatory") {
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`);
}
}
}
if (attrPolicy.include === "prohibit" && requestValue) {
errors.push(`${attrPolicy.type} is prohibited by template policy`);
}
if (attrPolicy.include === "optional" && requestValue && attrPolicy.value && attrPolicy.value.length > 0) {
const isValidValue = attrPolicy.value.some((allowedValue) => {
if (allowedValue.includes("*")) {
const pattern = allowedValue.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 (template.keyUsages) {
const missingRequired = template.keyUsages.requiredUsages.all.filter(
(usage) => !request.keyUsages?.includes(usage)
);
if (missingRequired.length > 0) {
errors.push(`Missing required key usages: ${missingRequired.join(", ")}`);
}
if (request.keyUsages) {
const allAllowedUsages = [...template.keyUsages.requiredUsages.all, ...template.keyUsages.optionalUsages.all];
const invalidUsages = request.keyUsages.filter((usage) => !allAllowedUsages.includes(usage));
if (invalidUsages.length > 0) {
errors.push(`Invalid key usages: ${invalidUsages.join(", ")}`);
}
}
}
if (template.extendedKeyUsages) {
const missingRequired = template.extendedKeyUsages.requiredUsages.all.filter(
(usage) => !request.extendedKeyUsages?.includes(usage)
);
if (missingRequired.length > 0) {
errors.push(`Missing required extended key usages: ${missingRequired.join(", ")}`);
}
if (request.extendedKeyUsages) {
const allAllowedUsages = [
...template.extendedKeyUsages.requiredUsages.all,
...template.extendedKeyUsages.optionalUsages.all
];
const invalidUsages = request.extendedKeyUsages.filter((usage) => !allAllowedUsages.includes(usage));
if (invalidUsages.length > 0) {
errors.push(`Invalid extended key usages: ${invalidUsages.join(", ")}`);
}
}
}
template.subjectAlternativeNames?.forEach((sanPolicy) => {
const requestSans = request.subjectAlternativeNames?.filter((san) => san.type === sanPolicy.type) || [];
if (sanPolicy.include === "mandatory") {
if (requestSans.length === 0) {
errors.push(`${sanPolicy.type} SAN is mandatory but not provided in request`);
} else if (sanPolicy.value && sanPolicy.value.length > 0) {
requestSans.forEach((san) => {
if (!sanPolicy.value!.includes(san.value)) {
errors.push(`${sanPolicy.type} SAN value '${san.value}' is not in allowed values list`);
}
});
}
}
if (sanPolicy.include === "prohibit" && requestSans.length > 0) {
errors.push(`${sanPolicy.type} SAN is prohibited by template policy`);
}
if (sanPolicy.include === "optional" && sanPolicy.value && sanPolicy.value.length > 0) {
requestSans.forEach((san) => {
const isValidValue = sanPolicy.value!.some((allowedValue) => {
if (allowedValue.includes("*")) {
const pattern = allowedValue.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 (request.signatureAlgorithm && template.signatureAlgorithm) {
if (!template.signatureAlgorithm?.allowedAlgorithms.includes(request.signatureAlgorithm)) {
errors.push(`Signature algorithm '${request.signatureAlgorithm}' is not allowed by template policy`);
}
}
if (request.keyAlgorithm && template.keyAlgorithm) {
if (!template.keyAlgorithm?.allowedKeyTypes.includes(request.keyAlgorithm)) {
errors.push(`Key algorithm '${request.keyAlgorithm}' is not allowed by template policy`);
}
}
if (request.validity?.ttl && template.validity) {
const requestDuration = parseTTL(request.validity.ttl);
const maxDuration = convertToMilliseconds(
template.validity.maxDuration.value,
template.validity.maxDuration.unit
);
if (requestDuration > maxDuration) {
errors.push(`Requested validity period exceeds maximum allowed duration`);
}
if (template.validity.minDuration) {
const minDuration = convertToMilliseconds(
template.validity.minDuration.value,
template.validity.minDuration.unit
);
if (requestDuration < minDuration) {
errors.push(`Requested validity period is below minimum required duration`);
}
}
}
return {
isValid: errors.length === 0,
errors,
warnings
};
};
const createTemplateV2 = async ({
actor,
actorId,
actorAuthMethod,
actorOrgId,
projectId,
data
}: {
actor: ActorType;
actorId: string;
actorAuthMethod: ActorAuthMethod;
actorOrgId: string;
projectId: string;
data: Omit<TCertificateTemplateV2Insert, "projectId">;
}): Promise<TCertificateTemplateV2> => {
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPkiTemplateActions.Create,
ProjectPermissionSub.CertificateTemplates
);
if (!data) {
throw new Error("Template data is required");
}
validateTemplatePolicy({
attributes: data.attributes,
keyUsages: data.keyUsages,
extendedKeyUsages: data.extendedKeyUsages,
subjectAlternativeNames: data.subjectAlternativeNames,
validity: data.validity,
signatureAlgorithm: data.signatureAlgorithm,
keyAlgorithm: data.keyAlgorithm
});
const template = await certificateTemplateV2DAL.create({
...data,
projectId
});
return template;
};
const updateTemplateV2 = async ({
actor,
actorId,
actorAuthMethod,
actorOrgId,
templateId,
data
}: {
actor: ActorType;
actorId: string;
actorAuthMethod: ActorAuthMethod;
actorOrgId: string;
templateId: string;
data: TCertificateTemplateV2Update;
}): Promise<TCertificateTemplateV2> => {
const existingTemplate = await certificateTemplateV2DAL.findById(templateId);
if (!existingTemplate) {
throw new NotFoundError({ message: "Certificate template not found" });
}
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: existingTemplate.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPkiTemplateActions.Edit,
ProjectPermissionSub.CertificateTemplates
);
if (hasAnyPolicyField(data)) {
const mergedPolicy = {
attributes: data.attributes || existingTemplate.attributes,
keyUsages: data.keyUsages || existingTemplate.keyUsages,
extendedKeyUsages: data.extendedKeyUsages || existingTemplate.extendedKeyUsages,
subjectAlternativeNames: data.subjectAlternativeNames || existingTemplate.subjectAlternativeNames,
validity: data.validity || existingTemplate.validity,
signatureAlgorithm: data.signatureAlgorithm || existingTemplate.signatureAlgorithm,
keyAlgorithm: data.keyAlgorithm || existingTemplate.keyAlgorithm
};
validateTemplatePolicy(mergedPolicy);
}
const updatedTemplate = await certificateTemplateV2DAL.updateById(templateId, data);
if (!updatedTemplate) {
throw new NotFoundError({ message: "Failed to update certificate template" });
}
return updatedTemplate;
};
const getTemplateV2ById = async ({
actor,
actorId,
actorAuthMethod,
actorOrgId,
templateId
}: {
actor: ActorType;
actorId: string;
actorAuthMethod: ActorAuthMethod;
actorOrgId: string;
templateId: string;
}): Promise<TCertificateTemplateV2> => {
const template = await certificateTemplateV2DAL.findById(templateId);
if (!template) {
throw new NotFoundError({ message: "Certificate template not found" });
}
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: template.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPkiTemplateActions.Read,
ProjectPermissionSub.CertificateTemplates
);
return template;
};
const listTemplatesV2 = async ({
actor,
actorId,
actorAuthMethod,
actorOrgId,
projectId,
offset = 0,
limit = 20,
search
}: {
actor: ActorType;
actorId: string;
actorAuthMethod: ActorAuthMethod;
actorOrgId: string;
projectId: string;
offset?: number;
limit?: number;
search?: string;
}): Promise<{
templates: TCertificateTemplateV2[];
totalCount: number;
}> => {
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPkiTemplateActions.Read,
ProjectPermissionSub.CertificateTemplates
);
const templates = await certificateTemplateV2DAL.findByProjectId(projectId, {
offset,
limit,
search
});
const totalCount = await certificateTemplateV2DAL.countByProjectId(projectId, { search });
return {
templates,
totalCount
};
};
const deleteTemplateV2 = async ({
actor,
actorId,
actorAuthMethod,
actorOrgId,
templateId
}: {
actor: ActorType;
actorId: string;
actorAuthMethod: ActorAuthMethod;
actorOrgId: string;
templateId: string;
}): Promise<TCertificateTemplateV2> => {
const template = await certificateTemplateV2DAL.findById(templateId);
if (!template) {
throw new NotFoundError({ message: "Certificate template not found" });
}
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: template.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPkiTemplateActions.Delete,
ProjectPermissionSub.CertificateTemplates
);
const isInUse = await certificateTemplateV2DAL.isTemplateInUse(templateId);
if (isInUse) {
throw new ForbiddenRequestError({
message: "Cannot delete template that is in use by certificate profiles"
});
}
const deletedTemplate = await certificateTemplateV2DAL.deleteById(templateId);
if (!deletedTemplate) {
throw new NotFoundError({ message: "Failed to delete certificate template" });
}
return deletedTemplate as TCertificateTemplateV2;
};
const validateCertificateRequest = async (
templateId: string,
request: TCertificateRequest
): Promise<TTemplateValidationResult> => {
const template = await certificateTemplateV2DAL.findById(templateId);
if (!template) {
throw new NotFoundError({ message: "Certificate template not found" });
}
return validateRequestAgainstPolicy(template, request);
};
return {
createTemplateV2,
updateTemplateV2,
getTemplateV2ById,
listTemplatesV2,
deleteTemplateV2,
validateCertificateRequest
};
};
@@ -0,0 +1,125 @@
import { TCertificateTemplatesV2, TCertificateTemplatesV2Insert } from "@app/db/schemas/certificate-templates-v2";
export interface TTemplateV2Policy {
attributes: Array<{
type:
| "common_name"
| "organization_name"
| "organization_unit"
| "locality"
| "state"
| "country"
| "email"
| "street_address"
| "postal_code";
include: "mandatory" | "optional" | "prohibit";
value?: string[];
}>;
keyUsages: {
requiredUsages: { all: string[] };
optionalUsages: { all: string[] };
};
extendedKeyUsages: {
requiredUsages: { all: string[] };
optionalUsages: { all: string[] };
};
subjectAlternativeNames: Array<{
type: "dns_name" | "ip_address" | "email" | "uri";
include: "mandatory" | "optional" | "prohibit";
value?: string[];
}>;
validity: {
maxDuration: { value: number; unit: "days" | "months" | "years" };
minDuration?: { value: number; unit: "days" | "months" | "years" };
};
signatureAlgorithm: {
allowedAlgorithms: string[];
defaultAlgorithm: string;
};
keyAlgorithm: {
allowedKeyTypes: string[];
defaultKeyType: string;
};
}
export type TCertificateTemplateV2 = Omit<
TCertificateTemplatesV2,
| "attributes"
| "keyUsages"
| "extendedKeyUsages"
| "subjectAlternativeNames"
| "validity"
| "signatureAlgorithm"
| "keyAlgorithm"
> & {
attributes: TTemplateV2Policy["attributes"];
keyUsages: TTemplateV2Policy["keyUsages"];
extendedKeyUsages: TTemplateV2Policy["extendedKeyUsages"];
subjectAlternativeNames: TTemplateV2Policy["subjectAlternativeNames"];
validity: TTemplateV2Policy["validity"];
signatureAlgorithm: TTemplateV2Policy["signatureAlgorithm"];
keyAlgorithm: TTemplateV2Policy["keyAlgorithm"];
};
export type TCertificateTemplateV2Insert = Omit<
TCertificateTemplatesV2Insert,
| "attributes"
| "keyUsages"
| "extendedKeyUsages"
| "subjectAlternativeNames"
| "validity"
| "signatureAlgorithm"
| "keyAlgorithm"
> & {
attributes?: TTemplateV2Policy["attributes"];
keyUsages?: TTemplateV2Policy["keyUsages"];
extendedKeyUsages?: TTemplateV2Policy["extendedKeyUsages"];
subjectAlternativeNames?: TTemplateV2Policy["subjectAlternativeNames"];
validity?: TTemplateV2Policy["validity"];
signatureAlgorithm?: TTemplateV2Policy["signatureAlgorithm"];
keyAlgorithm?: TTemplateV2Policy["keyAlgorithm"];
};
export type TCertificateTemplateV2Update = Partial<
Pick<
TCertificateTemplateV2,
| "name"
| "description"
| "attributes"
| "keyUsages"
| "extendedKeyUsages"
| "subjectAlternativeNames"
| "validity"
| "signatureAlgorithm"
| "keyAlgorithm"
>
>;
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<{
type: "dns_name" | "ip_address" | "email" | "uri";
value: string;
}>;
validity?: {
ttl: string;
};
signatureAlgorithm?: string;
keyAlgorithm?: string;
}
export interface TTemplateValidationResult {
isValid: boolean;
errors: string[];
warnings: string[];
}