mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-15 04:14:38 +00:00
PKI revamp - backend initial changes
This commit is contained in:
@@ -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
|
||||
});
|
||||
+1243
File diff suppressed because it is too large
Load Diff
@@ -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[];
|
||||
}
|
||||
Reference in New Issue
Block a user