mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
PKI revamp, last changes and improvements on API and UI
This commit is contained in:
@@ -10,20 +10,19 @@ export async function up(knex: Knex): Promise<void> {
|
||||
t.string("projectId").notNullable();
|
||||
t.foreign("projectId").references("id").inTable(TableName.Project);
|
||||
|
||||
t.string("slug").notNullable();
|
||||
t.string("name").notNullable();
|
||||
t.string("description");
|
||||
|
||||
t.jsonb("attributes");
|
||||
t.jsonb("subject");
|
||||
t.jsonb("sans");
|
||||
t.jsonb("keyUsages");
|
||||
t.jsonb("extendedKeyUsages");
|
||||
t.jsonb("subjectAlternativeNames");
|
||||
t.jsonb("algorithms");
|
||||
t.jsonb("validity");
|
||||
t.jsonb("signatureAlgorithm");
|
||||
t.jsonb("keyAlgorithm");
|
||||
|
||||
t.timestamps(true, true, true);
|
||||
|
||||
t.unique(["slug", "projectId"]);
|
||||
t.unique(["name", "projectId"]);
|
||||
});
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.CertificateTemplateV2);
|
||||
|
||||
@@ -10,15 +10,14 @@ import { TImmutableDBKeys } from "./models";
|
||||
export const CertificateTemplatesV2Schema = z.object({
|
||||
id: z.string().uuid(),
|
||||
projectId: z.string(),
|
||||
slug: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string().nullable().optional(),
|
||||
attributes: z.unknown().nullable().optional(),
|
||||
subject: z.unknown().nullable().optional(),
|
||||
sans: z.unknown().nullable().optional(),
|
||||
keyUsages: z.unknown().nullable().optional(),
|
||||
extendedKeyUsages: z.unknown().nullable().optional(),
|
||||
subjectAlternativeNames: z.unknown().nullable().optional(),
|
||||
algorithms: z.unknown().nullable().optional(),
|
||||
validity: z.unknown().nullable().optional(),
|
||||
signatureAlgorithm: z.unknown().nullable().optional(),
|
||||
keyAlgorithm: z.unknown().nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
@@ -11,8 +11,8 @@ import { registerAuthRoutes } from "./auth-router";
|
||||
import { registerProjectBotRouter } from "./bot-router";
|
||||
import { registerCaRouter } from "./certificate-authority-router";
|
||||
import { CERTIFICATE_AUTHORITY_REGISTER_ROUTER_MAP } from "./certificate-authority-routers";
|
||||
import { registerCertRouter } from "./certificate-router";
|
||||
import { registerCertificateProfilesRouter } from "./certificate-profiles-router";
|
||||
import { registerCertRouter } from "./certificate-router";
|
||||
import { registerCertificateTemplateRouter } from "./certificate-template-router";
|
||||
import { registerDeprecatedProjectEnvRouter } from "./deprecated-project-env-router";
|
||||
import { registerDeprecatedProjectMembershipRouter } from "./deprecated-project-membership-router";
|
||||
|
||||
@@ -1,110 +1,133 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { CertificateTemplatesV2Schema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { ApiDocsTags } from "@app/lib/api-docs";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { slugSchema } from "@app/server/lib/schemas";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import {
|
||||
CertDurationUnit,
|
||||
CertExtendedKeyUsageType,
|
||||
CertIncludeType,
|
||||
CertKeyUsageType,
|
||||
CertSubjectAlternativeNameType,
|
||||
CertSubjectAttributeType
|
||||
} from "@app/services/certificate-common/certificate-constants";
|
||||
import { certificateTemplateV2ResponseSchema } from "@app/services/certificate-template-v2/certificate-template-v2-schemas";
|
||||
|
||||
const attributeTypeSchema = z.nativeEnum(CertSubjectAttributeType);
|
||||
const sanTypeSchema = z.nativeEnum(CertSubjectAlternativeNameType);
|
||||
|
||||
const templateV2SubjectSchema = z
|
||||
.object({
|
||||
type: attributeTypeSchema,
|
||||
allowed: z.array(z.string()).optional(),
|
||||
required: z.array(z.string()).optional(),
|
||||
denied: z.array(z.string()).optional()
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
if (!data.allowed && !data.required && !data.denied) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: "Subject attribute must have at least one allowed, required, or denied value"
|
||||
}
|
||||
);
|
||||
|
||||
const templateV2KeyUsagesSchema = z
|
||||
.object({
|
||||
allowed: z.array(z.nativeEnum(CertKeyUsageType)).optional(),
|
||||
required: z.array(z.nativeEnum(CertKeyUsageType)).optional(),
|
||||
denied: z.array(z.nativeEnum(CertKeyUsageType)).optional()
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
if (!data.allowed && !data.required && !data.denied) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: "Key usages must have at least one allowed, required, or denied value"
|
||||
}
|
||||
);
|
||||
|
||||
const templateV2ExtendedKeyUsagesSchema = z
|
||||
.object({
|
||||
allowed: z.array(z.nativeEnum(CertExtendedKeyUsageType)).optional(),
|
||||
required: z.array(z.nativeEnum(CertExtendedKeyUsageType)).optional(),
|
||||
denied: z.array(z.nativeEnum(CertExtendedKeyUsageType)).optional()
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
if (!data.allowed && !data.required && !data.denied) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: "Extended key usages must have at least one allowed, required, or denied value"
|
||||
}
|
||||
);
|
||||
|
||||
const templateV2SanSchema = z
|
||||
.object({
|
||||
type: sanTypeSchema,
|
||||
allowed: z.array(z.string()).optional(),
|
||||
required: z.array(z.string()).optional(),
|
||||
denied: z.array(z.string()).optional()
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
if (!data.allowed && !data.required && !data.denied) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: "SAN must have at least one allowed, required, or denied value"
|
||||
}
|
||||
);
|
||||
|
||||
const templateV2ValiditySchema = z.object({
|
||||
max: z
|
||||
.string()
|
||||
.regex(/^\d+[dhmy]$/, {
|
||||
message: "Max validity must be in format like '365d', '12m', '1y', or '24h'"
|
||||
})
|
||||
.optional()
|
||||
});
|
||||
|
||||
const templateV2AlgorithmsSchema = z.object({
|
||||
signature: z.array(z.string()).min(1, "At least one signature algorithm must be provided").optional(),
|
||||
keyAlgorithm: z.array(z.string()).min(1, "At least one key algorithm must be provided").optional()
|
||||
});
|
||||
|
||||
const createCertificateTemplateV2Schema = z.object({
|
||||
projectId: z.string().min(1),
|
||||
name: z.string().min(1).max(255, "Name must be between 1 and 255 characters"),
|
||||
description: z.string().max(1000).optional(),
|
||||
subject: z.array(templateV2SubjectSchema).optional(),
|
||||
sans: z.array(templateV2SanSchema).optional(),
|
||||
keyUsages: templateV2KeyUsagesSchema.optional(),
|
||||
extendedKeyUsages: templateV2ExtendedKeyUsagesSchema.optional(),
|
||||
algorithms: templateV2AlgorithmsSchema.optional(),
|
||||
validity: templateV2ValiditySchema.optional()
|
||||
});
|
||||
|
||||
const updateCertificateTemplateV2Schema = z.object({
|
||||
name: z.string().min(1).max(255, "Name must be between 1 and 255 characters").optional(),
|
||||
description: z.string().max(1000).optional(),
|
||||
subject: z.array(templateV2SubjectSchema).optional(),
|
||||
sans: z.array(templateV2SanSchema).optional(),
|
||||
keyUsages: templateV2KeyUsagesSchema.optional(),
|
||||
extendedKeyUsages: templateV2ExtendedKeyUsagesSchema.optional(),
|
||||
algorithms: templateV2AlgorithmsSchema.optional(),
|
||||
validity: templateV2ValiditySchema.optional()
|
||||
});
|
||||
|
||||
export const registerCertificateTemplatesV2Router = async (server: FastifyZodProvider) => {
|
||||
const templateV2AttributeSchema = z
|
||||
.object({
|
||||
type: z.nativeEnum(CertSubjectAttributeType),
|
||||
include: z.nativeEnum(CertIncludeType),
|
||||
value: z.array(z.string()).optional()
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
if (data.type === CertSubjectAttributeType.COMMON_NAME && data.value && data.value.length > 1) {
|
||||
return false;
|
||||
}
|
||||
if (data.include === CertIncludeType.MANDATORY && (!data.value || data.value.length > 1)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: "Common name can only have one value. Mandatory attributes can only have one value or no value (empty)"
|
||||
}
|
||||
);
|
||||
|
||||
const templateV2KeyUsagesSchema = z.object({
|
||||
requiredUsages: z
|
||||
.object({
|
||||
all: z.array(z.nativeEnum(CertKeyUsageType))
|
||||
})
|
||||
.optional(),
|
||||
optionalUsages: z
|
||||
.object({
|
||||
all: z.array(z.nativeEnum(CertKeyUsageType))
|
||||
})
|
||||
.optional()
|
||||
});
|
||||
|
||||
const templateV2ExtendedKeyUsagesSchema = z.object({
|
||||
requiredUsages: z
|
||||
.object({
|
||||
all: z.array(z.nativeEnum(CertExtendedKeyUsageType))
|
||||
})
|
||||
.optional(),
|
||||
optionalUsages: z
|
||||
.object({
|
||||
all: z.array(z.nativeEnum(CertExtendedKeyUsageType))
|
||||
})
|
||||
.optional()
|
||||
});
|
||||
|
||||
const templateV2SanSchema = z
|
||||
.object({
|
||||
type: z.nativeEnum(CertSubjectAlternativeNameType),
|
||||
include: z.nativeEnum(CertIncludeType),
|
||||
value: z.array(z.string()).optional()
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
if (data.include === CertIncludeType.MANDATORY && (!data.value || data.value.length > 1)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: "Mandatory SANs can only have one value or no value (empty)"
|
||||
}
|
||||
);
|
||||
|
||||
const templateV2ValiditySchema = z.object({
|
||||
maxDuration: z.object({
|
||||
value: z.number().positive(),
|
||||
unit: z.nativeEnum(CertDurationUnit)
|
||||
}),
|
||||
minDuration: z
|
||||
.object({
|
||||
value: z.number().positive(),
|
||||
unit: z.nativeEnum(CertDurationUnit)
|
||||
})
|
||||
.optional()
|
||||
});
|
||||
|
||||
const templateV2SignatureAlgorithmSchema = z.object({
|
||||
allowedAlgorithms: z.array(z.string()).min(1),
|
||||
defaultAlgorithm: z.string()
|
||||
});
|
||||
|
||||
const templateV2KeyAlgorithmSchema = z.object({
|
||||
allowedKeyTypes: z.array(z.string()).min(1),
|
||||
defaultKeyType: z.string()
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/",
|
||||
@@ -114,39 +137,10 @@ export const registerCertificateTemplatesV2Router = async (server: FastifyZodPro
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiCertificateTemplates],
|
||||
body: z
|
||||
.object({
|
||||
projectId: z.string().min(1),
|
||||
slug: slugSchema({ 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()
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
const hasConstraints =
|
||||
(data.attributes && data.attributes.length > 0) ||
|
||||
(data.subjectAlternativeNames && data.subjectAlternativeNames.length > 0) ||
|
||||
data.keyUsages ||
|
||||
data.extendedKeyUsages ||
|
||||
data.validity ||
|
||||
data.signatureAlgorithm ||
|
||||
data.keyAlgorithm;
|
||||
return hasConstraints;
|
||||
},
|
||||
{
|
||||
message:
|
||||
"Certificate template must define at least one constraint (attributes, SANs, key usages, validity, or algorithms)"
|
||||
}
|
||||
),
|
||||
body: createCertificateTemplateV2Schema,
|
||||
response: {
|
||||
200: z.object({
|
||||
certificateTemplate: CertificateTemplatesV2Schema
|
||||
certificateTemplate: certificateTemplateV2ResponseSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
@@ -169,7 +163,7 @@ export const registerCertificateTemplatesV2Router = async (server: FastifyZodPro
|
||||
type: EventType.CREATE_CERTIFICATE_TEMPLATE,
|
||||
metadata: {
|
||||
certificateTemplateId: certificateTemplate.id,
|
||||
name: certificateTemplate.slug,
|
||||
name: certificateTemplate.name,
|
||||
projectId: certificateTemplate.projectId
|
||||
}
|
||||
}
|
||||
@@ -196,7 +190,7 @@ export const registerCertificateTemplatesV2Router = async (server: FastifyZodPro
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
certificateTemplates: CertificateTemplatesV2Schema.array(),
|
||||
certificateTemplates: certificateTemplateV2ResponseSchema.array(),
|
||||
totalCount: z.number()
|
||||
})
|
||||
}
|
||||
@@ -240,7 +234,7 @@ export const registerCertificateTemplatesV2Router = async (server: FastifyZodPro
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
certificateTemplate: CertificateTemplatesV2Schema
|
||||
certificateTemplate: certificateTemplateV2ResponseSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
@@ -261,7 +255,7 @@ export const registerCertificateTemplatesV2Router = async (server: FastifyZodPro
|
||||
type: EventType.GET_CERTIFICATE_TEMPLATE,
|
||||
metadata: {
|
||||
certificateTemplateId: certificateTemplate.id,
|
||||
name: certificateTemplate.slug
|
||||
name: certificateTemplate.name
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -282,20 +276,10 @@ export const registerCertificateTemplatesV2Router = async (server: FastifyZodPro
|
||||
params: z.object({
|
||||
id: z.string().uuid()
|
||||
}),
|
||||
body: z.object({
|
||||
slug: slugSchema({ 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()
|
||||
}),
|
||||
body: updateCertificateTemplateV2Schema,
|
||||
response: {
|
||||
200: z.object({
|
||||
certificateTemplate: CertificateTemplatesV2Schema
|
||||
certificateTemplate: certificateTemplateV2ResponseSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
@@ -317,7 +301,7 @@ export const registerCertificateTemplatesV2Router = async (server: FastifyZodPro
|
||||
type: EventType.UPDATE_CERTIFICATE_TEMPLATE,
|
||||
metadata: {
|
||||
certificateTemplateId: certificateTemplate.id,
|
||||
name: certificateTemplate.slug
|
||||
name: certificateTemplate.name
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -340,7 +324,7 @@ export const registerCertificateTemplatesV2Router = async (server: FastifyZodPro
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
certificateTemplate: CertificateTemplatesV2Schema
|
||||
certificateTemplate: certificateTemplateV2ResponseSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
@@ -361,7 +345,7 @@ export const registerCertificateTemplatesV2Router = async (server: FastifyZodPro
|
||||
type: EventType.DELETE_CERTIFICATE_TEMPLATE,
|
||||
metadata: {
|
||||
certificateTemplateId: certificateTemplate.id,
|
||||
name: certificateTemplate.slug
|
||||
name: certificateTemplate.name
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -32,6 +32,17 @@ export enum CertIncludeType {
|
||||
PROHIBIT = "prohibit"
|
||||
}
|
||||
|
||||
export enum CertAttributeRule {
|
||||
ALLOW = "allow",
|
||||
DENY = "deny"
|
||||
}
|
||||
|
||||
export enum CertSanEffect {
|
||||
ALLOW = "allow",
|
||||
DENY = "deny",
|
||||
REQUIRE = "require"
|
||||
}
|
||||
|
||||
export enum CertDurationUnit {
|
||||
DAYS = "days",
|
||||
MONTHS = "months",
|
||||
@@ -39,7 +50,9 @@ export enum CertDurationUnit {
|
||||
}
|
||||
|
||||
export enum CertSubjectAttributeType {
|
||||
COMMON_NAME = "common_name"
|
||||
COMMON_NAME = "common_name",
|
||||
ORGANIZATION = "organization",
|
||||
COUNTRY = "country"
|
||||
}
|
||||
|
||||
export const mapSANTypeToLegacy = (type: CertSubjectAlternativeNameType): string => {
|
||||
@@ -184,3 +197,5 @@ export const EXTENDED_KEY_USAGE_OPTIONS = Object.values(CertExtendedKeyUsageType
|
||||
export const INCLUDE_TYPE_OPTIONS = Object.values(CertIncludeType);
|
||||
export const DURATION_UNIT_OPTIONS = Object.values(CertDurationUnit);
|
||||
export const SUBJECT_ATTRIBUTE_TYPE_OPTIONS = Object.values(CertSubjectAttributeType);
|
||||
export const ATTRIBUTE_RULE_OPTIONS = Object.values(CertAttributeRule);
|
||||
export const SAN_EFFECT_OPTIONS = Object.values(CertSanEffect);
|
||||
|
||||
@@ -50,43 +50,27 @@ export const buildCertificateSubjectFromTemplate = (
|
||||
request: Record<string, unknown>,
|
||||
templateAttributes?: Array<{
|
||||
type: string;
|
||||
include: "mandatory" | "optional" | "prohibit";
|
||||
value?: string[];
|
||||
allowed?: string[];
|
||||
required?: string[];
|
||||
denied?: string[];
|
||||
}>
|
||||
): Record<string, string | undefined> => {
|
||||
const subject: Record<string, string> = {};
|
||||
const attributeMap: Record<string, string> = {
|
||||
common_name: "commonName"
|
||||
common_name: "commonName",
|
||||
organization: "organization",
|
||||
country: "country"
|
||||
};
|
||||
|
||||
if (!templateAttributes || templateAttributes.length === 0) {
|
||||
throw new Error(
|
||||
"Template must define allowed certificate attributes. Cannot issue certificate without template attribute constraints."
|
||||
);
|
||||
return subject;
|
||||
}
|
||||
|
||||
const allowedAttributes = new Set(templateAttributes.map((attr) => attributeMap[attr.type]));
|
||||
|
||||
Object.keys(attributeMap).forEach((templateType) => {
|
||||
const requestKey = attributeMap[templateType];
|
||||
const value = request[requestKey];
|
||||
|
||||
if (value && !allowedAttributes.has(requestKey)) {
|
||||
throw new Error(
|
||||
`Certificate attribute '${requestKey}' is not allowed by the template. Template must define constraints for all requested attributes.`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
templateAttributes.forEach((attr) => {
|
||||
if (attr.include === "prohibit") {
|
||||
return;
|
||||
}
|
||||
|
||||
const requestKey = attributeMap[attr.type];
|
||||
const value = request[requestKey];
|
||||
|
||||
if (value && typeof value === "string") {
|
||||
if (value && typeof value === "string" && (attr.allowed || attr.required)) {
|
||||
subject[attr.type] = value;
|
||||
}
|
||||
});
|
||||
@@ -98,8 +82,9 @@ export const buildSubjectAlternativeNamesFromTemplate = (
|
||||
request: { subjectAlternativeNames?: Array<{ type: string; value: string }> },
|
||||
templateSans?: Array<{
|
||||
type: string;
|
||||
include: "mandatory" | "optional" | "prohibit";
|
||||
value?: string[];
|
||||
allowed?: string[];
|
||||
required?: string[];
|
||||
denied?: string[];
|
||||
}>
|
||||
): string => {
|
||||
if (!request.subjectAlternativeNames || request.subjectAlternativeNames.length === 0) {
|
||||
@@ -107,33 +92,13 @@ export const buildSubjectAlternativeNamesFromTemplate = (
|
||||
}
|
||||
|
||||
if (!templateSans || templateSans.length === 0) {
|
||||
if (request.subjectAlternativeNames.length > 0) {
|
||||
throw new Error(
|
||||
"Template must define allowed subject alternative names. Cannot issue certificate with SANs when template has no SAN constraints."
|
||||
);
|
||||
}
|
||||
return "";
|
||||
return request.subjectAlternativeNames.map((san) => san.value).join(",");
|
||||
}
|
||||
|
||||
const templateSanTypes = new Set(templateSans.map((san) => san.type));
|
||||
const prohibitedTypes = new Set(templateSans.filter((san) => san.include === "prohibit").map((san) => san.type));
|
||||
|
||||
request.subjectAlternativeNames.forEach((san) => {
|
||||
const sanType = san.type === "dns_name" ? "dns_name" : san.type;
|
||||
if (!templateSanTypes.has(sanType)) {
|
||||
throw new Error(
|
||||
`Subject Alternative Name type '${sanType}' is not allowed by the template. Template must define constraints for all requested SAN types.`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const allowedSans: string[] = [];
|
||||
|
||||
request.subjectAlternativeNames.forEach((san) => {
|
||||
const sanType = san.type === "dns_name" ? "dns_name" : san.type;
|
||||
if (!prohibitedTypes.has(sanType)) {
|
||||
allowedSans.push(san.value);
|
||||
}
|
||||
allowedSans.push(san.value);
|
||||
});
|
||||
|
||||
return allowedSans.join(",");
|
||||
|
||||
@@ -68,7 +68,7 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
||||
(tx || db).ref("name").withSchema(TableName.CertificateAuthority).as("caName"),
|
||||
(tx || db).ref("id").withSchema(TableName.CertificateTemplateV2).as("templateId"),
|
||||
(tx || db).ref("projectId").withSchema(TableName.CertificateTemplateV2).as("templateProjectId"),
|
||||
(tx || db).ref("slug").withSchema(TableName.CertificateTemplateV2).as("templateName"),
|
||||
(tx || db).ref("name").withSchema(TableName.CertificateTemplateV2).as("templateName"),
|
||||
(tx || db).ref("description").withSchema(TableName.CertificateTemplateV2).as("templateDescription"),
|
||||
(tx || db).ref("id").withSchema(TableName.PkiEstEnrollmentConfig).as("estConfigId"),
|
||||
(tx || db)
|
||||
|
||||
@@ -23,19 +23,12 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => {
|
||||
|
||||
const serializeJsonFields = (data: TCertificateTemplateV2Insert | TCertificateTemplateV2Update) => {
|
||||
const serialized = { ...data } as Record<string, unknown>;
|
||||
const jsonFields = [
|
||||
"attributes",
|
||||
"keyUsages",
|
||||
"extendedKeyUsages",
|
||||
"subjectAlternativeNames",
|
||||
"validity",
|
||||
"signatureAlgorithm",
|
||||
"keyAlgorithm"
|
||||
];
|
||||
|
||||
const jsonFields = ["subject", "sans", "keyUsages", "extendedKeyUsages", "algorithms", "validity"];
|
||||
|
||||
jsonFields.forEach((field) => {
|
||||
const value = (data as Record<string, unknown>)[field];
|
||||
if (value !== undefined) {
|
||||
const value = serialized[field];
|
||||
if (value !== undefined && typeof value !== "string") {
|
||||
serialized[field] = JSON.stringify(value);
|
||||
}
|
||||
});
|
||||
@@ -44,21 +37,15 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => {
|
||||
};
|
||||
|
||||
const parseJsonFields = (raw: Record<string, unknown>): TCertificateTemplateV2 => {
|
||||
const jsonFields = [
|
||||
"attributes",
|
||||
"keyUsages",
|
||||
"extendedKeyUsages",
|
||||
"subjectAlternativeNames",
|
||||
"validity",
|
||||
"signatureAlgorithm",
|
||||
"keyAlgorithm"
|
||||
];
|
||||
const parsed = { ...raw };
|
||||
const jsonFields = ["subject", "sans", "keyUsages", "extendedKeyUsages", "algorithms", "validity"];
|
||||
const parsed = { ...raw } as Record<string, unknown>;
|
||||
|
||||
jsonFields.forEach((field) => {
|
||||
const value = raw[field];
|
||||
if (value) {
|
||||
if (value !== null && value !== undefined) {
|
||||
parsed[field] = typeof value === "string" ? JSON.parse(value) : value;
|
||||
} else {
|
||||
parsed[field] = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -143,7 +130,7 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => {
|
||||
|
||||
if (search) {
|
||||
query = query.where((builder) => {
|
||||
void builder.whereILike("slug", `%${search}%`).orWhereILike("description", `%${search}%`);
|
||||
void builder.whereILike("name", `%${search}%`).orWhereILike("description", `%${search}%`);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -169,7 +156,7 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => {
|
||||
|
||||
if (search) {
|
||||
query = query.where((builder) => {
|
||||
void builder.whereILike("slug", `%${search}%`).orWhereILike("description", `%${search}%`);
|
||||
void builder.whereILike("name", `%${search}%`).orWhereILike("description", `%${search}%`);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -180,10 +167,10 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const findBySlugAndProjectId = async (slug: string, projectId: string, tx?: Knex) => {
|
||||
const findByNameAndProjectId = async (name: string, projectId: string, tx?: Knex) => {
|
||||
try {
|
||||
const certificateTemplateV2 = await (tx || db)(TableName.CertificateTemplateV2)
|
||||
.where({ slug, projectId })
|
||||
.where({ name, projectId })
|
||||
.first();
|
||||
|
||||
if (!certificateTemplateV2) {
|
||||
@@ -192,7 +179,7 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => {
|
||||
|
||||
return parseJsonFields(certificateTemplateV2);
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find certificate template v2 by slug and project id" });
|
||||
throw new DatabaseError({ error, name: "Find certificate template v2 by name and project id" });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -238,7 +225,7 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => {
|
||||
findById,
|
||||
findByProjectId,
|
||||
countByProjectId,
|
||||
findBySlugAndProjectId,
|
||||
findByNameAndProjectId,
|
||||
isTemplateInUse,
|
||||
getProfilesUsingTemplate
|
||||
};
|
||||
|
||||
@@ -1,166 +1,123 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { slugSchema } from "@app/server/lib/schemas";
|
||||
import {
|
||||
CertDurationUnit,
|
||||
CertExtendedKeyUsageType,
|
||||
CertIncludeType,
|
||||
CertKeyUsageType,
|
||||
CertSubjectAlternativeNameType,
|
||||
CertSubjectAttributeType
|
||||
} from "@app/services/certificate-common/certificate-constants";
|
||||
|
||||
const attributeTypeSchema = z.nativeEnum(CertSubjectAttributeType);
|
||||
|
||||
const includeTypeSchema = z.nativeEnum(CertIncludeType);
|
||||
|
||||
const sanTypeSchema = z.nativeEnum(CertSubjectAlternativeNameType);
|
||||
|
||||
const durationUnitSchema = z.nativeEnum(CertDurationUnit);
|
||||
|
||||
export const templateV2AttributeSchema = z
|
||||
const templateV2SubjectSchema = z
|
||||
.object({
|
||||
type: attributeTypeSchema,
|
||||
include: includeTypeSchema,
|
||||
value: z.array(z.string()).optional()
|
||||
allowed: z.array(z.string()).optional(),
|
||||
required: z.array(z.string()).optional(),
|
||||
denied: z.array(z.string()).optional()
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
if (data.type === "common_name" && data.value && data.value.length > 1) {
|
||||
return false;
|
||||
}
|
||||
if (data.include === "mandatory" && (!data.value || data.value.length > 1)) {
|
||||
if (!data.allowed && !data.required && !data.denied) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: "Common name can only have one value. Mandatory attributes can only have one value or no value (empty)"
|
||||
message: "Subject attribute must have at least one allowed, required, or denied value"
|
||||
}
|
||||
);
|
||||
|
||||
export const templateV2KeyUsagesSchema = z.object({
|
||||
requiredUsages: z
|
||||
.object({
|
||||
all: z.array(z.nativeEnum(CertKeyUsageType))
|
||||
})
|
||||
.optional(),
|
||||
optionalUsages: z
|
||||
.object({
|
||||
all: z.array(z.nativeEnum(CertKeyUsageType))
|
||||
})
|
||||
.optional()
|
||||
});
|
||||
const templateV2KeyUsagesSchema = z
|
||||
.object({
|
||||
allowed: z.array(z.nativeEnum(CertKeyUsageType)).optional(),
|
||||
required: z.array(z.nativeEnum(CertKeyUsageType)).optional(),
|
||||
denied: z.array(z.nativeEnum(CertKeyUsageType)).optional()
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
if (!data.allowed && !data.required && !data.denied) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: "Key usages must have at least one allowed, required, or denied value"
|
||||
}
|
||||
);
|
||||
|
||||
export const templateV2ExtendedKeyUsagesSchema = z.object({
|
||||
requiredUsages: z
|
||||
.object({
|
||||
all: z.array(z.nativeEnum(CertExtendedKeyUsageType))
|
||||
})
|
||||
.optional(),
|
||||
optionalUsages: z
|
||||
.object({
|
||||
all: z.array(z.nativeEnum(CertExtendedKeyUsageType))
|
||||
})
|
||||
.optional()
|
||||
});
|
||||
const templateV2ExtendedKeyUsagesSchema = z
|
||||
.object({
|
||||
allowed: z.array(z.nativeEnum(CertExtendedKeyUsageType)).optional(),
|
||||
required: z.array(z.nativeEnum(CertExtendedKeyUsageType)).optional(),
|
||||
denied: z.array(z.nativeEnum(CertExtendedKeyUsageType)).optional()
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
if (!data.allowed && !data.required && !data.denied) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: "Extended key usages must have at least one allowed, required, or denied value"
|
||||
}
|
||||
);
|
||||
|
||||
export const templateV2SanSchema = z
|
||||
const templateV2SanSchema = z
|
||||
.object({
|
||||
type: sanTypeSchema,
|
||||
include: includeTypeSchema,
|
||||
value: z.array(z.string()).optional()
|
||||
allowed: z.array(z.string()).optional(),
|
||||
required: z.array(z.string()).optional(),
|
||||
denied: z.array(z.string()).optional()
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
if (data.include === "mandatory" && (!data.value || data.value.length > 1)) {
|
||||
if (!data.allowed && !data.required && !data.denied) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: "Mandatory SANs can only have one value or no value (empty)"
|
||||
message: "SAN must have at least one allowed, required, or denied value"
|
||||
}
|
||||
);
|
||||
|
||||
export const templateV2ValiditySchema = z.object({
|
||||
maxDuration: z.object({
|
||||
value: z.number().positive(),
|
||||
unit: durationUnitSchema
|
||||
}),
|
||||
minDuration: z
|
||||
.object({
|
||||
value: z.number().positive(),
|
||||
unit: durationUnitSchema
|
||||
const templateV2ValiditySchema = z.object({
|
||||
max: z
|
||||
.string()
|
||||
.regex(/^\d+[dhmy]$/, {
|
||||
message: "Max validity must be in format like '365d', '12m', '1y', or '24h'"
|
||||
})
|
||||
.optional()
|
||||
});
|
||||
|
||||
export const templateV2SignatureAlgorithmSchema = z
|
||||
.object({
|
||||
allowedAlgorithms: z.array(z.string()).min(1),
|
||||
defaultAlgorithm: z.string()
|
||||
})
|
||||
.refine((data) => data.allowedAlgorithms.includes(data.defaultAlgorithm), {
|
||||
message: "Default signature algorithm must be included in the allowed algorithms list"
|
||||
});
|
||||
|
||||
export const templateV2KeyAlgorithmSchema = z
|
||||
.object({
|
||||
allowedKeyTypes: z.array(z.string()).min(1),
|
||||
defaultKeyType: z.string()
|
||||
})
|
||||
.refine((data) => data.allowedKeyTypes.includes(data.defaultKeyType), {
|
||||
message: "Default key algorithm must be included in the allowed key types list"
|
||||
});
|
||||
|
||||
export const createCertificateTemplateV2Schema = z.object({
|
||||
projectId: z.string().min(1),
|
||||
slug: slugSchema({ min: 1, max: 255 }),
|
||||
description: z.string().max(1000).optional(),
|
||||
attributes: z.array(templateV2AttributeSchema).min(1),
|
||||
keyUsages: templateV2KeyUsagesSchema,
|
||||
extendedKeyUsages: templateV2ExtendedKeyUsagesSchema.optional(),
|
||||
subjectAlternativeNames: z.array(templateV2SanSchema).optional(),
|
||||
validity: templateV2ValiditySchema.optional(),
|
||||
signatureAlgorithm: templateV2SignatureAlgorithmSchema.optional(),
|
||||
keyAlgorithm: templateV2KeyAlgorithmSchema.optional()
|
||||
const templateV2AlgorithmsSchema = z.object({
|
||||
signature: z.array(z.string()).min(1, "At least one signature algorithm must be provided").optional(),
|
||||
keyAlgorithm: z.array(z.string()).min(1, "At least one key algorithm must be provided").optional()
|
||||
});
|
||||
|
||||
export const updateCertificateTemplateV2Schema = z.object({
|
||||
slug: slugSchema({ min: 1, max: 255 }).optional(),
|
||||
description: z.string().max(1000).optional(),
|
||||
attributes: z.array(templateV2AttributeSchema).optional(),
|
||||
export const certificateTemplateV2ResponseSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
projectId: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string().nullable().optional(),
|
||||
subject: z.array(templateV2SubjectSchema).optional(),
|
||||
sans: z.array(templateV2SanSchema).optional(),
|
||||
keyUsages: templateV2KeyUsagesSchema.optional(),
|
||||
extendedKeyUsages: templateV2ExtendedKeyUsagesSchema.optional(),
|
||||
subjectAlternativeNames: z.array(templateV2SanSchema).optional(),
|
||||
algorithms: templateV2AlgorithmsSchema.optional(),
|
||||
validity: templateV2ValiditySchema.optional(),
|
||||
signatureAlgorithm: templateV2SignatureAlgorithmSchema.optional(),
|
||||
keyAlgorithm: templateV2KeyAlgorithmSchema.optional()
|
||||
});
|
||||
|
||||
export const getCertificateTemplateV2ByIdSchema = z.object({
|
||||
id: z.string().uuid()
|
||||
});
|
||||
|
||||
export const getCertificateTemplateV2BySlugSchema = z.object({
|
||||
projectId: z.string().min(1),
|
||||
slug: slugSchema()
|
||||
});
|
||||
|
||||
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()
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export const certificateRequestSchema = z.object({
|
||||
commonName: z.string().optional(),
|
||||
organization: z.string().optional(),
|
||||
organizationName: z.string().optional(),
|
||||
country: z.string().optional(),
|
||||
keyUsages: z.array(z.nativeEnum(CertKeyUsageType)).optional(),
|
||||
extendedKeyUsages: z.array(z.nativeEnum(CertExtendedKeyUsageType)).optional(),
|
||||
subjectAlternativeNames: z
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -12,7 +12,7 @@ import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
||||
import { CertIncludeType, CertSubjectAttributeType } from "../certificate-common/certificate-constants";
|
||||
import { CertSubjectAttributeType } from "../certificate-common/certificate-constants";
|
||||
import { TCertificateTemplateV2DALFactory } from "./certificate-template-v2-dal";
|
||||
import {
|
||||
TCertificateRequest,
|
||||
@@ -57,75 +57,86 @@ export const certificateTemplateV2ServiceFactory = ({
|
||||
}
|
||||
};
|
||||
|
||||
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 validateSubjectAttributePolicy = (
|
||||
subject: Array<{ type: string; allowed?: string[]; required?: string[]; denied?: string[] }>
|
||||
) => {
|
||||
if (!subject || subject.length === 0) return;
|
||||
|
||||
const validateSubjectAttributePolicy = (attributes: Array<{ type: string; include: string; value?: string[] }>) => {
|
||||
if (!attributes || attributes.length === 0) return;
|
||||
|
||||
const attributesByType = attributes.reduce(
|
||||
(acc, attr) => {
|
||||
if (!acc[attr.type]) acc[attr.type] = [];
|
||||
acc[attr.type].push(attr);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, typeof attributes>
|
||||
);
|
||||
|
||||
for (const [type, attrs] of Object.entries(attributesByType)) {
|
||||
const mandatoryAttrs = attrs.filter((attr) => attr.include === CertIncludeType.MANDATORY);
|
||||
|
||||
if (mandatoryAttrs.length > 1) {
|
||||
// Validate each subject attribute policy
|
||||
for (const attr of subject) {
|
||||
// Ensure at least one field is provided
|
||||
if (!attr.allowed && !attr.required && !attr.denied) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: `Multiple mandatory values found for subject attribute type '${type}'. Only one mandatory value is allowed per attribute type.`
|
||||
message: `Subject attribute type '${attr.type}' must have at least one allowed, required, or denied value`
|
||||
});
|
||||
}
|
||||
|
||||
if (mandatoryAttrs.length === 1 && attrs.length > 1) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: `When a mandatory value exists for subject attribute type '${type}', no other values (optional or forbidden) are allowed for that attribute type.`
|
||||
});
|
||||
// Check for duplicate values within arrays
|
||||
const arrays = [
|
||||
{ name: "allowed", values: attr.allowed },
|
||||
{ name: "required", values: attr.required },
|
||||
{ name: "denied", values: attr.denied }
|
||||
];
|
||||
|
||||
for (const { name, values } of arrays) {
|
||||
if (values && values.length > 0) {
|
||||
const uniqueValues = new Set(values);
|
||||
if (uniqueValues.size !== values.length) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: `Duplicate values found in ${name} list for subject attribute type '${attr.type}'`
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getRequestAttributeValue = (
|
||||
request: TCertificateRequest,
|
||||
attrType: CertSubjectAttributeType | string
|
||||
): string | undefined => {
|
||||
switch (attrType) {
|
||||
case CertSubjectAttributeType.COMMON_NAME:
|
||||
case "common_name":
|
||||
return request.commonName;
|
||||
default:
|
||||
return undefined;
|
||||
const validateSanPolicy = (
|
||||
sans: Array<{ type: string; allowed?: string[]; required?: string[]; denied?: string[] }>
|
||||
) => {
|
||||
if (!sans || sans.length === 0) return;
|
||||
|
||||
// Validate each SAN policy
|
||||
for (const san of sans) {
|
||||
if (!san.allowed && !san.required && !san.denied) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: `SAN type '${san.type}' must have at least one allowed, required, or denied value`
|
||||
});
|
||||
}
|
||||
|
||||
const arrays = [
|
||||
{ name: "allowed", values: san.allowed },
|
||||
{ name: "required", values: san.required },
|
||||
{ name: "denied", values: san.denied }
|
||||
];
|
||||
|
||||
for (const { name, values } of arrays) {
|
||||
if (values && values.length > 0) {
|
||||
const uniqueValues = new Set(values);
|
||||
if (uniqueValues.size !== values.length) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: `Duplicate values found in ${name} list for SAN type '${san.type}'`
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const generateTemplateSlug = (baseSlug?: string): string => {
|
||||
if (baseSlug) {
|
||||
return slugify(baseSlug);
|
||||
const generateTemplateSlug = (baseName?: string): string => {
|
||||
if (baseName) {
|
||||
return slugify(baseName);
|
||||
}
|
||||
return slugify(alphaNumericNanoId(12));
|
||||
};
|
||||
|
||||
const ensureUniqueSlug = async (projectId: string, desiredSlug: string, templateId?: string): Promise<string> => {
|
||||
const existingTemplate = await certificateTemplateV2DAL.findBySlugAndProjectId(desiredSlug, projectId);
|
||||
const existingTemplate = await certificateTemplateV2DAL.findByNameAndProjectId(desiredSlug, projectId);
|
||||
if (!existingTemplate || (templateId && existingTemplate.id === templateId)) {
|
||||
return desiredSlug;
|
||||
}
|
||||
const alternativeSlug = `${desiredSlug}-${alphaNumericNanoId(8)}`;
|
||||
const existingAlternative = await certificateTemplateV2DAL.findBySlugAndProjectId(alternativeSlug, projectId);
|
||||
const existingAlternative = await certificateTemplateV2DAL.findByNameAndProjectId(alternativeSlug, projectId);
|
||||
if (!existingAlternative) {
|
||||
return alternativeSlug;
|
||||
}
|
||||
@@ -139,8 +150,12 @@ export const certificateTemplateV2ServiceFactory = ({
|
||||
};
|
||||
|
||||
const createWildcardRegex = (pattern: string): RegExp => {
|
||||
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
||||
const regexPattern = escaped.replace(/\*/g, ".*");
|
||||
const wildcardRegex = new RE2(/\*/g);
|
||||
const withPlaceholder = pattern.replace(wildcardRegex, "__WILDCARD__");
|
||||
const escapeRegex = new RE2(/[.+?^${}()|[\]\\]/g);
|
||||
const escaped = withPlaceholder.replace(escapeRegex, "\\$&");
|
||||
const placeholderRegex = new RE2(/__WILDCARD__/g);
|
||||
const regexPattern = escaped.replace(placeholderRegex, ".*");
|
||||
return new RE2(`^${regexPattern}$`);
|
||||
};
|
||||
|
||||
@@ -166,6 +181,64 @@ export const certificateTemplateV2ServiceFactory = ({
|
||||
return mapping[templateFormat] || templateFormat;
|
||||
};
|
||||
|
||||
const validateKeyUsagePolicy = (keyUsages: { allowed?: string[]; required?: string[]; denied?: string[] }) => {
|
||||
if (!keyUsages) return;
|
||||
|
||||
if (!keyUsages.allowed && !keyUsages.required && !keyUsages.denied) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Key usages must have at least one allowed, required, or denied value"
|
||||
});
|
||||
}
|
||||
|
||||
const arrays = [
|
||||
{ name: "allowed", values: keyUsages.allowed },
|
||||
{ name: "required", values: keyUsages.required },
|
||||
{ name: "denied", values: keyUsages.denied }
|
||||
];
|
||||
|
||||
for (const { name, values } of arrays) {
|
||||
if (values && values.length > 0) {
|
||||
const uniqueValues = new Set(values);
|
||||
if (uniqueValues.size !== values.length) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: `Duplicate values found in ${name} key usages list`
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const validateExtendedKeyUsagePolicy = (extendedKeyUsages: {
|
||||
allowed?: string[];
|
||||
required?: string[];
|
||||
denied?: string[];
|
||||
}) => {
|
||||
if (!extendedKeyUsages) return;
|
||||
|
||||
if (!extendedKeyUsages.allowed && !extendedKeyUsages.required && !extendedKeyUsages.denied) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Extended key usages must have at least one allowed, required, or denied value"
|
||||
});
|
||||
}
|
||||
|
||||
const arrays = [
|
||||
{ name: "allowed", values: extendedKeyUsages.allowed },
|
||||
{ name: "required", values: extendedKeyUsages.required },
|
||||
{ name: "denied", values: extendedKeyUsages.denied }
|
||||
];
|
||||
|
||||
for (const { name, values } of arrays) {
|
||||
if (values && values.length > 0) {
|
||||
const uniqueValues = new Set(values);
|
||||
if (uniqueValues.size !== values.length) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: `Duplicate values found in ${name} extended key usages list`
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const validateValueAgainstConstraints = (
|
||||
value: string,
|
||||
allowedValues: string[],
|
||||
@@ -184,7 +257,7 @@ export const certificateTemplateV2ServiceFactory = ({
|
||||
if (regex.test(value)) {
|
||||
return { isValid: true };
|
||||
}
|
||||
} catch {
|
||||
} catch (error) {
|
||||
if (allowedValue === value) {
|
||||
return { isValid: true };
|
||||
}
|
||||
@@ -213,89 +286,186 @@ export const certificateTemplateV2ServiceFactory = ({
|
||||
const errors: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
|
||||
const templateAttributeTypes = new Set(template.attributes?.map((attr) => attr.type) || []);
|
||||
// Validate subject attributes
|
||||
const subjectPolicies = template.subject;
|
||||
const requestAttributes = new Map<string, string>();
|
||||
if (request.commonName) requestAttributes.set(CertSubjectAttributeType.COMMON_NAME, request.commonName);
|
||||
if (request.organization || request.organizationName) {
|
||||
requestAttributes.set(CertSubjectAttributeType.ORGANIZATION, request.organization || request.organizationName!);
|
||||
}
|
||||
if (request.country) requestAttributes.set(CertSubjectAttributeType.COUNTRY, request.country);
|
||||
|
||||
const attributePoliciesByType = new Map<string, typeof template.attributes>();
|
||||
template.attributes?.forEach((attrPolicy) => {
|
||||
const existing = attributePoliciesByType.get(attrPolicy.type) || [];
|
||||
attributePoliciesByType.set(attrPolicy.type, [...existing, attrPolicy]);
|
||||
});
|
||||
if (subjectPolicies && subjectPolicies.length > 0) {
|
||||
// Validate each template subject attribute policy
|
||||
for (const attrPolicy of subjectPolicies) {
|
||||
const requestValue = requestAttributes.get(attrPolicy.type);
|
||||
|
||||
for (const [attrType, policies] of attributePoliciesByType) {
|
||||
const requestValue = getRequestAttributeValue(request, attrType);
|
||||
|
||||
const hasMandatory = policies.some((p) => p.include === CertIncludeType.MANDATORY);
|
||||
const hasProhibit = policies.some((p) => p.include === CertIncludeType.PROHIBIT);
|
||||
|
||||
if (hasProhibit && requestValue) {
|
||||
errors.push(`${attrType} is prohibited by template policy`);
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
}
|
||||
|
||||
if (hasMandatory && !requestValue) {
|
||||
errors.push(`${attrType} is mandatory but not provided in request`);
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
}
|
||||
|
||||
if (requestValue) {
|
||||
const policiesWithValues = policies.filter(
|
||||
(p) =>
|
||||
p.value &&
|
||||
p.value.length > 0 &&
|
||||
(p.include === CertIncludeType.MANDATORY || p.include === CertIncludeType.OPTIONAL)
|
||||
);
|
||||
|
||||
if (policiesWithValues.length > 0) {
|
||||
const allAllowedValues = policiesWithValues.flatMap((p) => p.value || []);
|
||||
|
||||
const validation = validateValueAgainstConstraints(requestValue, allAllowedValues, attrType);
|
||||
if (!validation.isValid && validation.error) {
|
||||
errors.push(validation.error);
|
||||
// Check denied values first
|
||||
if (requestValue && attrPolicy.denied && attrPolicy.denied.length > 0) {
|
||||
const validation = validateValueAgainstConstraints(requestValue, attrPolicy.denied, attrPolicy.type);
|
||||
if (validation.isValid) {
|
||||
errors.push(`${attrPolicy.type} value '${requestValue}' is denied by template policy`);
|
||||
// Skip further validation for this attribute if it's denied
|
||||
} else if (requestValue && attrPolicy.allowed && attrPolicy.allowed.length > 0) {
|
||||
// Check allowed values if present and not denied
|
||||
const allowedValidation = validateValueAgainstConstraints(
|
||||
requestValue,
|
||||
attrPolicy.allowed,
|
||||
attrPolicy.type
|
||||
);
|
||||
if (!allowedValidation.isValid && allowedValidation.error) {
|
||||
errors.push(allowedValidation.error);
|
||||
}
|
||||
}
|
||||
} else if (requestValue && attrPolicy.allowed && attrPolicy.allowed.length > 0) {
|
||||
// Check allowed values if present and not denied
|
||||
const allowedValidation = validateValueAgainstConstraints(requestValue, attrPolicy.allowed, attrPolicy.type);
|
||||
if (!allowedValidation.isValid && allowedValidation.error) {
|
||||
errors.push(allowedValidation.error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const requestAttributeTypes: CertSubjectAttributeType[] = [];
|
||||
if (request.commonName) requestAttributeTypes.push(CertSubjectAttributeType.COMMON_NAME);
|
||||
// Check for required subject attributes
|
||||
for (const attrPolicy of subjectPolicies) {
|
||||
if (attrPolicy.required && attrPolicy.required.length > 0) {
|
||||
const requestValue = requestAttributes.get(attrPolicy.type);
|
||||
if (!requestValue) {
|
||||
errors.push(`Missing required ${attrPolicy.type} attribute`);
|
||||
} else {
|
||||
// Validate that the request value matches at least one required pattern
|
||||
const hasMatchingRequired = attrPolicy.required.some((requiredValue) => {
|
||||
const validation = validateValueAgainstConstraints(requestValue, [requiredValue], attrPolicy.type);
|
||||
return validation.isValid;
|
||||
});
|
||||
if (!hasMatchingRequired) {
|
||||
errors.push(
|
||||
`${attrPolicy.type} value '${requestValue}' does not match any required patterns: ${attrPolicy.required.join(", ")}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const requestAttrType of requestAttributeTypes) {
|
||||
if (!templateAttributeTypes.has(requestAttrType)) {
|
||||
errors.push(`${requestAttrType} is not allowed by template policy (not defined in template)`);
|
||||
// Check if any request attributes are not covered by template policies
|
||||
for (const [attrType] of requestAttributes) {
|
||||
const hasPolicy = subjectPolicies.some((policy) => policy.type === attrType);
|
||||
if (!hasPolicy) {
|
||||
errors.push(`${attrType} is not allowed by template policy (not defined in template)`);
|
||||
}
|
||||
}
|
||||
} else if (requestAttributes.size > 0) {
|
||||
// No subject policies defined but request has subject attributes - deny all
|
||||
for (const [attrType] of requestAttributes) {
|
||||
errors.push(`${attrType} is not allowed by template policy (no subject policies defined)`);
|
||||
}
|
||||
}
|
||||
if (template.keyUsages) {
|
||||
if (template.keyUsages.requiredUsages && template.keyUsages.requiredUsages.all.length > 0) {
|
||||
const missingRequired = template.keyUsages.requiredUsages.all.filter(
|
||||
(usage) => !request.keyUsages?.includes(usage)
|
||||
);
|
||||
|
||||
// Validate Subject Alternative Names
|
||||
const sansPolicies = template.sans;
|
||||
if (sansPolicies && sansPolicies.length > 0) {
|
||||
const requestSansByType = new Map<string, string[]>();
|
||||
|
||||
// Group request SANs by type
|
||||
if (request.subjectAlternativeNames) {
|
||||
for (const san of request.subjectAlternativeNames) {
|
||||
if (!requestSansByType.has(san.type)) {
|
||||
requestSansByType.set(san.type, []);
|
||||
}
|
||||
requestSansByType.get(san.type)!.push(san.value);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate each SAN policy
|
||||
for (const sanPolicy of sansPolicies) {
|
||||
const requestSans = requestSansByType.get(sanPolicy.type) || [];
|
||||
|
||||
// Check REQUIRED values - at least one SAN must match each required pattern
|
||||
if (sanPolicy.required && sanPolicy.required.length > 0) {
|
||||
for (const requiredValue of sanPolicy.required) {
|
||||
const hasMatchingRequiredSan = requestSans.some((sanValue) => {
|
||||
const validation = validateValueAgainstConstraints(sanValue, [requiredValue], `${sanPolicy.type} SAN`);
|
||||
return validation.isValid;
|
||||
});
|
||||
|
||||
if (!hasMatchingRequiredSan) {
|
||||
errors.push(`Required ${sanPolicy.type} SAN matching pattern '${requiredValue}' not found in request`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check DENIED values - no SAN should match denied patterns
|
||||
if (sanPolicy.denied && sanPolicy.denied.length > 0) {
|
||||
for (const sanValue of requestSans) {
|
||||
const validation = validateValueAgainstConstraints(sanValue, sanPolicy.denied, `${sanPolicy.type} SAN`);
|
||||
if (validation.isValid) {
|
||||
errors.push(`${sanPolicy.type} SAN matching denied pattern '${sanValue}' found in request`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check ALLOWED values - if present, all SANs must match at least one allowed pattern
|
||||
if (sanPolicy.allowed && sanPolicy.allowed.length > 0 && requestSans.length > 0) {
|
||||
for (const sanValue of requestSans) {
|
||||
const validation = validateValueAgainstConstraints(sanValue, sanPolicy.allowed, `${sanPolicy.type} SAN`);
|
||||
if (!validation.isValid && validation.error) {
|
||||
errors.push(validation.error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if any request SANs are for types not covered by template policies
|
||||
for (const [requestSanType] of requestSansByType) {
|
||||
const hasPolicy = sansPolicies.some((policy) => policy.type === requestSanType);
|
||||
if (!hasPolicy) {
|
||||
errors.push(`${requestSanType} SAN is not allowed by template policy (not defined in template)`);
|
||||
}
|
||||
}
|
||||
} else if (request.subjectAlternativeNames && request.subjectAlternativeNames.length > 0) {
|
||||
// No SAN policies defined but request has SANs - deny all
|
||||
for (const san of request.subjectAlternativeNames) {
|
||||
errors.push(`${san.type} SAN is not allowed by template policy (no SAN policies defined)`);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate key usages
|
||||
const keyUsagePolicy = template.keyUsages;
|
||||
if (keyUsagePolicy) {
|
||||
// Check REQUIRED key usages - must have all required usages
|
||||
if (keyUsagePolicy.required && keyUsagePolicy.required.length > 0) {
|
||||
const missingRequired = keyUsagePolicy.required.filter((usage) => !request.keyUsages?.includes(usage));
|
||||
if (missingRequired.length > 0) {
|
||||
errors.push(`Missing required key usages: ${missingRequired.join(", ")}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (request.keyUsages && (template.keyUsages.requiredUsages || template.keyUsages.optionalUsages)) {
|
||||
const allAllowedUsages = [
|
||||
...(template.keyUsages.requiredUsages?.all || []),
|
||||
...(template.keyUsages.optionalUsages?.all || [])
|
||||
];
|
||||
// Check DENIED key usages - must not have any denied usages
|
||||
if (request.keyUsages && keyUsagePolicy.denied && keyUsagePolicy.denied.length > 0) {
|
||||
const deniedUsages = request.keyUsages.filter((usage) => keyUsagePolicy?.denied?.includes(usage));
|
||||
if (deniedUsages.length > 0) {
|
||||
errors.push(`Denied key usages found in request: ${deniedUsages.join(", ")}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (allAllowedUsages.length > 0) {
|
||||
const invalidUsages = request.keyUsages.filter((usage) => !allAllowedUsages.includes(usage));
|
||||
if (invalidUsages.length > 0) {
|
||||
errors.push(`Invalid key usages: ${invalidUsages.join(", ")}`);
|
||||
}
|
||||
// Check ALLOWED key usages - if present, all usages must be in allowed list
|
||||
if (request.keyUsages && keyUsagePolicy && keyUsagePolicy.allowed && keyUsagePolicy.allowed.length > 0) {
|
||||
const allAllowedUsages = [...(keyUsagePolicy.required || []), ...(keyUsagePolicy.allowed || [])];
|
||||
const invalidUsages = request.keyUsages.filter((usage) => !allAllowedUsages.includes(usage));
|
||||
if (invalidUsages.length > 0) {
|
||||
errors.push(`Invalid key usages: ${invalidUsages.join(", ")}`);
|
||||
}
|
||||
}
|
||||
} else if (request.keyUsages && request.keyUsages.length > 0) {
|
||||
errors.push(`Key usages are not allowed by template policy (not defined in template)`);
|
||||
}
|
||||
|
||||
if (template.extendedKeyUsages) {
|
||||
if (template.extendedKeyUsages.requiredUsages && template.extendedKeyUsages.requiredUsages.all.length > 0) {
|
||||
const missingRequired = template.extendedKeyUsages.requiredUsages.all.filter(
|
||||
// Validate extended key usages
|
||||
const extendedKeyUsagePolicy = template.extendedKeyUsages;
|
||||
if (extendedKeyUsagePolicy) {
|
||||
// Check REQUIRED extended key usages - must have all required usages
|
||||
if (extendedKeyUsagePolicy.required && extendedKeyUsagePolicy.required.length > 0) {
|
||||
const missingRequired = extendedKeyUsagePolicy.required.filter(
|
||||
(usage) => !request.extendedKeyUsages?.includes(usage)
|
||||
);
|
||||
if (missingRequired.length > 0) {
|
||||
@@ -303,89 +473,46 @@ export const certificateTemplateV2ServiceFactory = ({
|
||||
}
|
||||
}
|
||||
|
||||
// Check DENIED extended key usages - must not have any denied usages
|
||||
if (request.extendedKeyUsages && extendedKeyUsagePolicy.denied && extendedKeyUsagePolicy.denied.length > 0) {
|
||||
const deniedUsages = request.extendedKeyUsages.filter((usage) =>
|
||||
extendedKeyUsagePolicy?.denied?.includes(usage)
|
||||
);
|
||||
if (deniedUsages.length > 0) {
|
||||
errors.push(`Denied extended key usages found in request: ${deniedUsages.join(", ")}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Check ALLOWED extended key usages - if present, all usages must be in allowed list
|
||||
if (
|
||||
request.extendedKeyUsages &&
|
||||
(template.extendedKeyUsages.requiredUsages || template.extendedKeyUsages.optionalUsages)
|
||||
extendedKeyUsagePolicy &&
|
||||
extendedKeyUsagePolicy.allowed &&
|
||||
extendedKeyUsagePolicy.allowed.length > 0
|
||||
) {
|
||||
const allAllowedUsages = [
|
||||
...(template.extendedKeyUsages.requiredUsages?.all || []),
|
||||
...(template.extendedKeyUsages.optionalUsages?.all || [])
|
||||
const allAllowedExtendedUsages = [
|
||||
...(extendedKeyUsagePolicy.required || []),
|
||||
...(extendedKeyUsagePolicy.allowed || [])
|
||||
];
|
||||
|
||||
if (allAllowedUsages.length > 0) {
|
||||
const invalidUsages = request.extendedKeyUsages.filter((usage) => !allAllowedUsages.includes(usage));
|
||||
if (invalidUsages.length > 0) {
|
||||
errors.push(`Invalid extended key usages: ${invalidUsages.join(", ")}`);
|
||||
}
|
||||
const invalidExtendedUsages = request.extendedKeyUsages.filter(
|
||||
(usage) => !allAllowedExtendedUsages.includes(usage)
|
||||
);
|
||||
if (invalidExtendedUsages.length > 0) {
|
||||
errors.push(`Invalid extended key usages: ${invalidExtendedUsages.join(", ")}`);
|
||||
}
|
||||
}
|
||||
} else if (request.extendedKeyUsages && request.extendedKeyUsages.length > 0) {
|
||||
errors.push(`Extended key usages are not allowed by template policy (not defined in template)`);
|
||||
}
|
||||
|
||||
const templateSanTypes = new Set(template.subjectAlternativeNames?.map((san) => san.type) || []);
|
||||
|
||||
const sanPoliciesByType = new Map<string, typeof template.subjectAlternativeNames>();
|
||||
template.subjectAlternativeNames?.forEach((sanPolicy) => {
|
||||
const existing = sanPoliciesByType.get(sanPolicy.type) || [];
|
||||
sanPoliciesByType.set(sanPolicy.type, [...existing, sanPolicy]);
|
||||
});
|
||||
|
||||
for (const [sanType, policies] of sanPoliciesByType) {
|
||||
const requestSans = request.subjectAlternativeNames?.filter((san) => san.type === sanType) || [];
|
||||
|
||||
const hasMandatory = policies.some((p) => p.include === CertIncludeType.MANDATORY);
|
||||
const hasProhibit = policies.some((p) => p.include === CertIncludeType.PROHIBIT);
|
||||
|
||||
if (hasProhibit && requestSans.length > 0) {
|
||||
errors.push(`${sanType} SAN is prohibited by template policy`);
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
}
|
||||
|
||||
if (hasMandatory && requestSans.length === 0) {
|
||||
errors.push(`${sanType} SAN is mandatory but not provided in request`);
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
}
|
||||
|
||||
if (requestSans.length > 0) {
|
||||
const policiesWithValues = policies.filter(
|
||||
(p) =>
|
||||
p.value &&
|
||||
p.value.length > 0 &&
|
||||
(p.include === CertIncludeType.MANDATORY || p.include === CertIncludeType.OPTIONAL)
|
||||
);
|
||||
|
||||
if (policiesWithValues.length > 0) {
|
||||
const allAllowedValues = policiesWithValues.flatMap((p) => p.value || []);
|
||||
|
||||
requestSans.forEach((san) => {
|
||||
const validation = validateValueAgainstConstraints(san.value, allAllowedValues, `${sanType} SAN`);
|
||||
if (!validation.isValid && validation.error) {
|
||||
errors.push(validation.error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const requestSanTypes = new Set(request.subjectAlternativeNames?.map((san) => san.type) || []);
|
||||
for (const requestSanType of requestSanTypes) {
|
||||
if (!templateSanTypes.has(requestSanType)) {
|
||||
errors.push(`${requestSanType} SAN is not allowed by template policy (not defined in template)`);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate algorithms with new structure
|
||||
if (request.signatureAlgorithm) {
|
||||
if (template.signatureAlgorithm && template.signatureAlgorithm.allowedAlgorithms) {
|
||||
const mappedTemplateAlgorithms = template.signatureAlgorithm.allowedAlgorithms.map(
|
||||
mapTemplateSignatureAlgorithmToApi
|
||||
);
|
||||
if (template.algorithms?.signature && template.algorithms.signature.length > 0) {
|
||||
const mappedTemplateAlgorithms = template.algorithms.signature.map(mapTemplateSignatureAlgorithmToApi);
|
||||
if (!mappedTemplateAlgorithms.includes(request.signatureAlgorithm)) {
|
||||
errors.push(`Signature algorithm '${request.signatureAlgorithm}' is not allowed by template policy`);
|
||||
}
|
||||
} else if (!template.signatureAlgorithm) {
|
||||
} else if (!template.algorithms?.signature) {
|
||||
errors.push(
|
||||
`Signature algorithm '${request.signatureAlgorithm}' is not allowed by template policy (not defined in template)`
|
||||
);
|
||||
@@ -393,40 +520,19 @@ export const certificateTemplateV2ServiceFactory = ({
|
||||
}
|
||||
|
||||
if (request.keyAlgorithm) {
|
||||
if (template.keyAlgorithm && template.keyAlgorithm.allowedKeyTypes) {
|
||||
const mappedTemplateKeyTypes = template.keyAlgorithm.allowedKeyTypes.map(mapTemplateKeyAlgorithmToApi);
|
||||
if (template.algorithms?.keyAlgorithm && template.algorithms.keyAlgorithm.length > 0) {
|
||||
const mappedTemplateKeyTypes = template.algorithms.keyAlgorithm.map(mapTemplateKeyAlgorithmToApi);
|
||||
if (!mappedTemplateKeyTypes.includes(request.keyAlgorithm)) {
|
||||
errors.push(`Key algorithm '${request.keyAlgorithm}' is not allowed by template policy`);
|
||||
}
|
||||
} else if (!template.keyAlgorithm) {
|
||||
} else if (!template.algorithms?.keyAlgorithm) {
|
||||
errors.push(
|
||||
`Key algorithm '${request.keyAlgorithm}' is not allowed by template policy (not defined in template)`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate validity with new structure
|
||||
if (request.validity?.ttl && (request.notBefore || request.notAfter)) {
|
||||
errors.push(
|
||||
"Cannot specify both TTL and notBefore/notAfter. Use either TTL for duration-based validity or notBefore/notAfter for explicit date range."
|
||||
@@ -437,30 +543,32 @@ export const certificateTemplateV2ServiceFactory = ({
|
||||
errors.push("notBefore must be earlier than notAfter");
|
||||
}
|
||||
|
||||
if ((request.notBefore || request.notAfter) && template.validity) {
|
||||
// Validate TTL against template validity constraints
|
||||
if (request.validity?.ttl && template.validity) {
|
||||
const requestDurationMs = parseTTL(request.validity.ttl);
|
||||
|
||||
// Check maximum duration using max field
|
||||
if (template.validity.max) {
|
||||
const maxDurationMs = parseTTL(template.validity.max);
|
||||
|
||||
if (requestDurationMs > maxDurationMs) {
|
||||
errors.push("Requested validity period exceeds maximum allowed duration");
|
||||
}
|
||||
}
|
||||
}
|
||||
// Validate explicit date range against max duration
|
||||
if ((request.notBefore || request.notAfter) && template.validity?.max) {
|
||||
const notBefore = request.notBefore || new Date();
|
||||
const { notAfter } = request;
|
||||
|
||||
if (notAfter && notBefore && notAfter instanceof Date && notBefore instanceof Date) {
|
||||
const requestDuration = notAfter.getTime() - notBefore.getTime();
|
||||
const maxDurationMs = parseTTL(template.validity.max);
|
||||
|
||||
const maxDuration = convertToMilliseconds(
|
||||
template.validity.maxDuration.value,
|
||||
template.validity.maxDuration.unit
|
||||
);
|
||||
|
||||
if (requestDuration > maxDuration) {
|
||||
errors.push(`Requested validity period (notBefore to notAfter) exceeds maximum allowed duration`);
|
||||
}
|
||||
|
||||
if (template.validity.minDuration) {
|
||||
const minDuration = convertToMilliseconds(
|
||||
template.validity.minDuration.value,
|
||||
template.validity.minDuration.unit
|
||||
if (requestDuration > maxDurationMs) {
|
||||
errors.push(
|
||||
`Requested validity period (notBefore to notAfter) exceeds maximum allowed duration of ${template.validity.max}`
|
||||
);
|
||||
if (requestDuration < minDuration) {
|
||||
errors.push(`Requested validity period (notBefore to notAfter) is below minimum required duration`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -505,16 +613,33 @@ export const certificateTemplateV2ServiceFactory = ({
|
||||
throw new Error("Template data is required");
|
||||
}
|
||||
|
||||
if (data.attributes) {
|
||||
validateSubjectAttributePolicy(data.attributes);
|
||||
if (data.subject) {
|
||||
validateSubjectAttributePolicy(data.subject);
|
||||
}
|
||||
|
||||
const slug = data.slug || generateTemplateSlug();
|
||||
if (data.sans) {
|
||||
validateSanPolicy(data.sans);
|
||||
}
|
||||
|
||||
if (data.keyUsages) {
|
||||
validateKeyUsagePolicy(data.keyUsages);
|
||||
}
|
||||
|
||||
if (data.extendedKeyUsages) {
|
||||
validateExtendedKeyUsagePolicy(data.extendedKeyUsages);
|
||||
}
|
||||
|
||||
// Generate slug from name and ensure it's unique within project
|
||||
if (!data.name) {
|
||||
throw new ForbiddenRequestError({ message: "Template name is required" });
|
||||
}
|
||||
|
||||
const slug = generateTemplateSlug(data.name);
|
||||
const uniqueSlug = await ensureUniqueSlug(projectId, slug);
|
||||
|
||||
const template = await certificateTemplateV2DAL.create({
|
||||
...data,
|
||||
slug: uniqueSlug,
|
||||
name: uniqueSlug,
|
||||
projectId
|
||||
});
|
||||
|
||||
@@ -555,14 +680,29 @@ export const certificateTemplateV2ServiceFactory = ({
|
||||
ProjectPermissionSub.CertificateTemplates
|
||||
);
|
||||
|
||||
if (data.attributes) {
|
||||
validateSubjectAttributePolicy(data.attributes);
|
||||
if (data.subject) {
|
||||
validateSubjectAttributePolicy(data.subject);
|
||||
}
|
||||
|
||||
if (data.sans) {
|
||||
validateSanPolicy(data.sans);
|
||||
}
|
||||
|
||||
if (data.keyUsages) {
|
||||
validateKeyUsagePolicy(data.keyUsages);
|
||||
}
|
||||
|
||||
if (data.extendedKeyUsages) {
|
||||
validateExtendedKeyUsagePolicy(data.extendedKeyUsages);
|
||||
}
|
||||
|
||||
const updateData = { ...data };
|
||||
if (data.slug && typeof data.slug === "string" && data.slug !== existingTemplate.slug) {
|
||||
const uniqueSlug = await ensureUniqueSlug(existingTemplate.projectId, data.slug, templateId);
|
||||
updateData.slug = uniqueSlug;
|
||||
if (data.name && typeof data.name === "string") {
|
||||
const newSlug = generateTemplateSlug(data.name);
|
||||
if (newSlug !== existingTemplate.name) {
|
||||
const uniqueSlug = await ensureUniqueSlug(existingTemplate.projectId, newSlug, templateId);
|
||||
updateData.name = uniqueSlug;
|
||||
}
|
||||
}
|
||||
|
||||
const updatedTemplate = await certificateTemplateV2DAL.updateById(templateId, updateData);
|
||||
@@ -636,7 +776,7 @@ export const certificateTemplateV2ServiceFactory = ({
|
||||
ProjectPermissionSub.CertificateTemplates
|
||||
);
|
||||
|
||||
const template = await certificateTemplateV2DAL.findBySlugAndProjectId(slug, projectId);
|
||||
const template = await certificateTemplateV2DAL.findByNameAndProjectId(slug, projectId);
|
||||
if (!template) {
|
||||
throw new NotFoundError({ message: "Certificate template not found" });
|
||||
}
|
||||
@@ -734,8 +874,8 @@ export const certificateTemplateV2ServiceFactory = ({
|
||||
throw new ForbiddenRequestError({
|
||||
message:
|
||||
profilesUsingTemplate.length > 0
|
||||
? `Cannot delete template '${template.slug}' as it is currently in use by the following certificate profiles: ${profileNames}. Please remove this template from these profiles before deleting it.`
|
||||
: `Cannot delete template '${template.slug}' as it is currently in use by one or more certificates. Please ensure no certificates are using this template before deleting it.`
|
||||
? `Cannot delete template '${template.name}' as it is currently in use by the following certificate profiles: ${profileNames}. Please remove this template from these profiles before deleting it.`
|
||||
: `Cannot delete template '${template.name}' as it is currently in use by one or more certificates. Please ensure no certificates are using this template before deleting it.`
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,101 +1,71 @@
|
||||
import { TCertificateTemplatesV2, TCertificateTemplatesV2Insert } from "@app/db/schemas/certificate-templates-v2";
|
||||
import {
|
||||
CertDurationUnit,
|
||||
CertExtendedKeyUsageType,
|
||||
CertIncludeType,
|
||||
CertKeyUsageType,
|
||||
CertSubjectAlternativeNameType,
|
||||
CertSubjectAttributeType
|
||||
} from "@app/services/certificate-common/certificate-constants";
|
||||
|
||||
export interface TTemplateV2Policy {
|
||||
attributes: Array<{
|
||||
subject?: Array<{
|
||||
type: CertSubjectAttributeType;
|
||||
include: CertIncludeType;
|
||||
value?: string[];
|
||||
allowed?: string[];
|
||||
required?: string[];
|
||||
denied?: string[];
|
||||
}>;
|
||||
keyUsages: {
|
||||
requiredUsages?: { all: CertKeyUsageType[] };
|
||||
optionalUsages?: { all: CertKeyUsageType[] };
|
||||
};
|
||||
extendedKeyUsages: {
|
||||
requiredUsages?: { all: CertExtendedKeyUsageType[] };
|
||||
optionalUsages?: { all: CertExtendedKeyUsageType[] };
|
||||
};
|
||||
subjectAlternativeNames: Array<{
|
||||
sans?: Array<{
|
||||
type: CertSubjectAlternativeNameType;
|
||||
include: CertIncludeType;
|
||||
value?: string[];
|
||||
allowed?: string[];
|
||||
required?: string[];
|
||||
denied?: string[];
|
||||
}>;
|
||||
validity: {
|
||||
maxDuration: { value: number; unit: CertDurationUnit };
|
||||
minDuration?: { value: number; unit: CertDurationUnit };
|
||||
keyUsages?: {
|
||||
allowed?: CertKeyUsageType[];
|
||||
required?: CertKeyUsageType[];
|
||||
denied?: CertKeyUsageType[];
|
||||
};
|
||||
signatureAlgorithm: {
|
||||
allowedAlgorithms: string[];
|
||||
defaultAlgorithm: string;
|
||||
extendedKeyUsages?: {
|
||||
allowed?: CertExtendedKeyUsageType[];
|
||||
required?: CertExtendedKeyUsageType[];
|
||||
denied?: CertExtendedKeyUsageType[];
|
||||
};
|
||||
keyAlgorithm: {
|
||||
allowedKeyTypes: string[];
|
||||
defaultKeyType: string;
|
||||
algorithms?: {
|
||||
signature?: string[];
|
||||
keyAlgorithm?: string[];
|
||||
};
|
||||
validity?: {
|
||||
max?: 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"];
|
||||
export type TCertificateTemplateV2 = TCertificateTemplatesV2 & {
|
||||
subject?: TTemplateV2Policy["subject"];
|
||||
sans?: TTemplateV2Policy["sans"];
|
||||
keyUsages?: TTemplateV2Policy["keyUsages"];
|
||||
extendedKeyUsages?: TTemplateV2Policy["extendedKeyUsages"];
|
||||
subjectAlternativeNames?: TTemplateV2Policy["subjectAlternativeNames"];
|
||||
algorithms?: TTemplateV2Policy["algorithms"];
|
||||
validity?: TTemplateV2Policy["validity"];
|
||||
};
|
||||
|
||||
export type TCertificateTemplateV2Insert = TCertificateTemplatesV2Insert & {
|
||||
subject?: TTemplateV2Policy["subject"];
|
||||
sans?: TTemplateV2Policy["sans"];
|
||||
keyUsages?: TTemplateV2Policy["keyUsages"];
|
||||
extendedKeyUsages?: TTemplateV2Policy["extendedKeyUsages"];
|
||||
algorithms?: TTemplateV2Policy["algorithms"];
|
||||
validity?: TTemplateV2Policy["validity"];
|
||||
signatureAlgorithm?: TTemplateV2Policy["signatureAlgorithm"];
|
||||
keyAlgorithm?: TTemplateV2Policy["keyAlgorithm"];
|
||||
};
|
||||
|
||||
export type TCertificateTemplateV2Update = Partial<
|
||||
Pick<
|
||||
TCertificateTemplateV2,
|
||||
| "slug"
|
||||
| "description"
|
||||
| "attributes"
|
||||
| "keyUsages"
|
||||
| "extendedKeyUsages"
|
||||
| "subjectAlternativeNames"
|
||||
| "validity"
|
||||
| "signatureAlgorithm"
|
||||
| "keyAlgorithm"
|
||||
"name" | "description" | "subject" | "sans" | "keyUsages" | "extendedKeyUsages" | "algorithms" | "validity"
|
||||
>
|
||||
>;
|
||||
|
||||
export interface TCertificateRequest {
|
||||
commonName?: string;
|
||||
organization?: string;
|
||||
organizationName?: string;
|
||||
organizationUnit?: string;
|
||||
locality?: string;
|
||||
|
||||
@@ -102,12 +102,12 @@ const validateCaSupport = (ca: TCertificateAuthorityWithAssociatedCa, operation:
|
||||
const validateAlgorithmCompatibility = (
|
||||
ca: TCertificateAuthorityWithAssociatedCa,
|
||||
template: {
|
||||
signatureAlgorithm?: {
|
||||
allowedAlgorithms?: string[];
|
||||
algorithms?: {
|
||||
signature?: string[];
|
||||
};
|
||||
}
|
||||
) => {
|
||||
if (!template.signatureAlgorithm || !template.signatureAlgorithm.allowedAlgorithms) {
|
||||
if (!template.algorithms?.signature || template.algorithms.signature.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ const validateAlgorithmCompatibility = (
|
||||
throw new BadRequestError({ message: "CA key algorithm not found" });
|
||||
}
|
||||
|
||||
const compatibleAlgorithms = template.signatureAlgorithm.allowedAlgorithms.filter((sigAlg: string) => {
|
||||
const compatibleAlgorithms = template.algorithms.signature.filter((sigAlg: string) => {
|
||||
const parts = sigAlg.split("-");
|
||||
const keyType = parts[parts.length - 1];
|
||||
|
||||
@@ -133,7 +133,7 @@ const validateAlgorithmCompatibility = (
|
||||
|
||||
if (compatibleAlgorithms.length === 0) {
|
||||
throw new BadRequestError({
|
||||
message: `Template signature algorithms (${template.signatureAlgorithm.allowedAlgorithms.join(", ")}) are not compatible with CA key algorithm (${caKeyAlgorithm})`
|
||||
message: `Template signature algorithms (${template.algorithms.signature.join(", ")}) are not compatible with CA key algorithm (${caKeyAlgorithm})`
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -180,7 +180,10 @@ export const certificateV3ServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
const mappedCertificateRequest = mapEnumsForValidation(certificateRequest);
|
||||
const mappedCertificateRequest = mapEnumsForValidation({
|
||||
...certificateRequest,
|
||||
subjectAlternativeNames: certificateRequest.altNames
|
||||
});
|
||||
const validationResult = await certificateTemplateV2Service.validateCertificateRequest(
|
||||
profile.certificateTemplateId,
|
||||
mappedCertificateRequest
|
||||
@@ -217,26 +220,25 @@ export const certificateV3ServiceFactory = ({
|
||||
|
||||
validateAlgorithmCompatibility(ca, template);
|
||||
|
||||
const effectiveSignatureAlgorithm =
|
||||
certificateRequest.signatureAlgorithm || template.signatureAlgorithm?.defaultAlgorithm;
|
||||
const effectiveKeyAlgorithm = certificateRequest.keyAlgorithm || template.keyAlgorithm?.defaultKeyType;
|
||||
const effectiveSignatureAlgorithm = certificateRequest.signatureAlgorithm;
|
||||
const effectiveKeyAlgorithm = certificateRequest.keyAlgorithm;
|
||||
|
||||
if (template.keyAlgorithm?.allowedKeyTypes && !effectiveKeyAlgorithm) {
|
||||
if (template.algorithms?.keyAlgorithm && !effectiveKeyAlgorithm) {
|
||||
throw new BadRequestError({
|
||||
message: "Key algorithm is required by template policy but not provided in request or template default"
|
||||
message: "Key algorithm is required by template policy but not provided in request"
|
||||
});
|
||||
}
|
||||
|
||||
if (template.signatureAlgorithm?.allowedAlgorithms && !effectiveSignatureAlgorithm) {
|
||||
if (template.algorithms?.signature && !effectiveSignatureAlgorithm) {
|
||||
throw new BadRequestError({
|
||||
message: "Signature algorithm is required by template policy but not provided in request or template default"
|
||||
message: "Signature algorithm is required by template policy but not provided in request"
|
||||
});
|
||||
}
|
||||
|
||||
const certificateSubject = buildCertificateSubjectFromTemplate(certificateRequest, template.attributes);
|
||||
const certificateSubject = buildCertificateSubjectFromTemplate(certificateRequest, template.subject);
|
||||
const subjectAlternativeNames = buildSubjectAlternativeNamesFromTemplate(
|
||||
{ subjectAlternativeNames: certificateRequest.altNames },
|
||||
template.subjectAlternativeNames
|
||||
template.sans
|
||||
);
|
||||
|
||||
const { certificate, certificateChain, issuingCaCertificate, privateKey, serialNumber } =
|
||||
@@ -326,18 +328,18 @@ export const certificateV3ServiceFactory = ({
|
||||
|
||||
validateAlgorithmCompatibility(ca, template);
|
||||
|
||||
const effectiveSignatureAlgorithm = signatureAlgorithm || template.signatureAlgorithm?.defaultAlgorithm;
|
||||
const effectiveKeyAlgorithm = keyAlgorithm || template.keyAlgorithm?.defaultKeyType;
|
||||
const effectiveSignatureAlgorithm = signatureAlgorithm;
|
||||
const effectiveKeyAlgorithm = keyAlgorithm;
|
||||
|
||||
if (template.keyAlgorithm?.allowedKeyTypes && !effectiveKeyAlgorithm) {
|
||||
if (template.algorithms?.keyAlgorithm && !effectiveKeyAlgorithm) {
|
||||
throw new BadRequestError({
|
||||
message: "Key algorithm is required by template policy but not provided in request or template default"
|
||||
message: "Key algorithm is required by template policy but not provided in request"
|
||||
});
|
||||
}
|
||||
|
||||
if (template.signatureAlgorithm?.allowedAlgorithms && !effectiveSignatureAlgorithm) {
|
||||
if (template.algorithms?.signature && !effectiveSignatureAlgorithm) {
|
||||
throw new BadRequestError({
|
||||
message: "Signature algorithm is required by template policy but not provided in request or template default"
|
||||
message: "Signature algorithm is required by template policy but not provided in request"
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
export { AcmeDnsProvider, CaRenewalType, CaStatus, CaType, InternalCaType } from "./enums";
|
||||
export type { TOrderCertificateDTO, TOrderCertificateResponse } from "./types";
|
||||
export {
|
||||
useCreateCa,
|
||||
useCreateCertificate,
|
||||
@@ -24,3 +23,4 @@ export {
|
||||
useListCasByTypeAndProjectId,
|
||||
useListExternalCasByProjectId
|
||||
} from "./queries";
|
||||
export type { TOrderCertificateDTO, TOrderCertificateResponse } from "./types";
|
||||
|
||||
@@ -123,78 +123,74 @@ export type TListCertificateTemplatesDTO = {
|
||||
};
|
||||
|
||||
export type TCertificateTemplateV2Policy = {
|
||||
attributes: Array<{
|
||||
type: "common_name";
|
||||
include: "mandatory" | "optional" | "prohibit";
|
||||
value?: string[];
|
||||
subject?: Array<{
|
||||
type: "common_name" | "organization" | "country";
|
||||
allowed?: string[];
|
||||
required?: string[];
|
||||
denied?: string[];
|
||||
}>;
|
||||
keyUsages: {
|
||||
requiredUsages: { all: string[] };
|
||||
optionalUsages: { all: string[] };
|
||||
};
|
||||
extendedKeyUsages: {
|
||||
requiredUsages: { all: string[] };
|
||||
optionalUsages: { all: string[] };
|
||||
};
|
||||
subjectAlternativeNames: Array<{
|
||||
sans?: Array<{
|
||||
type: "dns_name" | "ip_address" | "email" | "uri";
|
||||
include: "mandatory" | "optional" | "prohibit";
|
||||
value?: string[];
|
||||
allowed?: string[];
|
||||
required?: string[];
|
||||
denied?: string[];
|
||||
}>;
|
||||
validity: {
|
||||
maxDuration: { value: number; unit: "days" | "months" | "years" };
|
||||
minDuration?: { value: number; unit: "days" | "months" | "years" };
|
||||
keyUsages?: {
|
||||
allowed?: string[];
|
||||
required?: string[];
|
||||
denied?: string[];
|
||||
};
|
||||
signatureAlgorithm: {
|
||||
allowedAlgorithms: string[];
|
||||
defaultAlgorithm: string;
|
||||
extendedKeyUsages?: {
|
||||
allowed?: string[];
|
||||
required?: string[];
|
||||
denied?: string[];
|
||||
};
|
||||
keyAlgorithm: {
|
||||
allowedKeyTypes: string[];
|
||||
defaultKeyType: string;
|
||||
algorithms?: {
|
||||
signature?: string[];
|
||||
keyAlgorithm?: string[];
|
||||
};
|
||||
validity?: {
|
||||
max?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type TCertificateTemplateV2New = {
|
||||
id: string;
|
||||
projectId: string;
|
||||
slug: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
attributes: any;
|
||||
keyUsages: any;
|
||||
extendedKeyUsages: any;
|
||||
subjectAlternativeNames: any;
|
||||
validity: any;
|
||||
signatureAlgorithm: any;
|
||||
keyAlgorithm: any;
|
||||
subject?: TCertificateTemplateV2Policy["subject"];
|
||||
sans?: TCertificateTemplateV2Policy["sans"];
|
||||
keyUsages?: TCertificateTemplateV2Policy["keyUsages"];
|
||||
extendedKeyUsages?: TCertificateTemplateV2Policy["extendedKeyUsages"];
|
||||
algorithms?: TCertificateTemplateV2Policy["algorithms"];
|
||||
validity?: TCertificateTemplateV2Policy["validity"];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type TCreateCertificateTemplateV2NewDTO = {
|
||||
projectId: string;
|
||||
slug: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
attributes: TCertificateTemplateV2Policy["attributes"];
|
||||
keyUsages: TCertificateTemplateV2Policy["keyUsages"];
|
||||
extendedKeyUsages: TCertificateTemplateV2Policy["extendedKeyUsages"];
|
||||
subjectAlternativeNames: TCertificateTemplateV2Policy["subjectAlternativeNames"];
|
||||
validity: TCertificateTemplateV2Policy["validity"];
|
||||
signatureAlgorithm: TCertificateTemplateV2Policy["signatureAlgorithm"];
|
||||
keyAlgorithm: TCertificateTemplateV2Policy["keyAlgorithm"];
|
||||
subject?: TCertificateTemplateV2Policy["subject"];
|
||||
sans?: TCertificateTemplateV2Policy["sans"];
|
||||
keyUsages?: TCertificateTemplateV2Policy["keyUsages"];
|
||||
extendedKeyUsages?: TCertificateTemplateV2Policy["extendedKeyUsages"];
|
||||
algorithms?: TCertificateTemplateV2Policy["algorithms"];
|
||||
validity?: TCertificateTemplateV2Policy["validity"];
|
||||
};
|
||||
|
||||
export type TUpdateCertificateTemplateV2NewDTO = {
|
||||
templateId: string;
|
||||
slug?: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
attributes?: TCertificateTemplateV2Policy["attributes"];
|
||||
subject?: TCertificateTemplateV2Policy["subject"];
|
||||
sans?: TCertificateTemplateV2Policy["sans"];
|
||||
keyUsages?: TCertificateTemplateV2Policy["keyUsages"];
|
||||
extendedKeyUsages?: TCertificateTemplateV2Policy["extendedKeyUsages"];
|
||||
subjectAlternativeNames?: TCertificateTemplateV2Policy["subjectAlternativeNames"];
|
||||
algorithms?: TCertificateTemplateV2Policy["algorithms"];
|
||||
validity?: TCertificateTemplateV2Policy["validity"];
|
||||
signatureAlgorithm?: TCertificateTemplateV2Policy["signatureAlgorithm"];
|
||||
keyAlgorithm?: TCertificateTemplateV2Policy["keyAlgorithm"];
|
||||
};
|
||||
|
||||
export type TDeleteCertificateTemplateV2NewDTO = {
|
||||
|
||||
@@ -53,10 +53,10 @@ export const PkiManagerLayout = () => {
|
||||
animate={{ x: 0 }}
|
||||
exit={{ x: -150 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="border-mineshaft-600 bg-linear-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 dark w-full border-r md:w-60"
|
||||
className="dark w-full border-r border-mineshaft-600 bg-linear-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60"
|
||||
>
|
||||
<nav className="items-between dark:scheme-dark flex h-full flex-col overflow-y-auto">
|
||||
<div className="border-mineshaft-600 flex items-center gap-3 border-b px-4 py-3.5 text-lg text-white">
|
||||
<nav className="items-between flex h-full flex-col overflow-y-auto dark:scheme-dark">
|
||||
<div className="flex items-center gap-3 border-b border-mineshaft-600 px-4 py-3.5 text-lg text-white">
|
||||
<Lottie className="inline-block h-5 w-5 shrink-0" icon="note" />
|
||||
PKI Manager
|
||||
</div>
|
||||
@@ -267,7 +267,7 @@ export const PkiManagerLayout = () => {
|
||||
<Menu>
|
||||
<Link to="/organization/projects">
|
||||
<MenuItem
|
||||
className="text-mineshaft-400 hover:text-mineshaft-300 relative flex items-center gap-2 overflow-hidden text-sm"
|
||||
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300"
|
||||
leftIcon={
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon className="mx-1 inline-block shrink-0" icon={faHome} />
|
||||
@@ -281,13 +281,13 @@ export const PkiManagerLayout = () => {
|
||||
</div>
|
||||
</nav>
|
||||
</motion.div>
|
||||
<div className="bg-bunker-800 flex-1 overflow-y-auto overflow-x-hidden p-4 pt-8">
|
||||
<div className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 p-4 pt-8">
|
||||
{assumedPrivilegeDetails && <AssumePrivilegeModeBanner />}
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="z-200 bg-bunker-800 flex h-screen w-screen flex-col items-center justify-center md:hidden">
|
||||
<div className="z-200 flex h-screen w-screen flex-col items-center justify-center bg-bunker-800 md:hidden">
|
||||
<FontAwesomeIcon icon={faMobile} className="mb-8 text-7xl text-gray-300" />
|
||||
<p className="max-w-sm px-6 text-center text-lg text-gray-200">
|
||||
{` ${t("common.no-mobile")} `}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
/* eslint-disable react/no-array-index-key */
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import { useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { faQuestionCircle } from "@fortawesome/free-regular-svg-icons";
|
||||
import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faPlus, faQuestionCircle, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
@@ -35,13 +32,13 @@ import {
|
||||
KEY_USAGES_OPTIONS,
|
||||
SIGNATURE_ALGORITHMS_OPTIONS
|
||||
} from "@app/hooks/api/certificates/constants";
|
||||
import {
|
||||
CertExtendedKeyUsage,
|
||||
CertKeyAlgorithm,
|
||||
CertKeyUsage
|
||||
} from "@app/hooks/api/certificates/enums";
|
||||
import { CertExtendedKeyUsage, CertKeyUsage } from "@app/hooks/api/certificates/enums";
|
||||
import { useGetCertificateTemplateV2ById } from "@app/hooks/api/certificateTemplates/queries";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
import {
|
||||
mapTemplateKeyAlgorithmToApi,
|
||||
mapTemplateSignatureAlgorithmToApi
|
||||
} from "@app/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/shared/certificate-constants";
|
||||
|
||||
import { CertificateContent } from "./CertificateContent";
|
||||
|
||||
@@ -109,9 +106,12 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
||||
const [certificateDetails, setCertificateDetails] = useState<TCertificateDetails | null>(null);
|
||||
const [allowedKeyUsages, setAllowedKeyUsages] = useState<string[]>([]);
|
||||
const [allowedExtendedKeyUsages, setAllowedExtendedKeyUsages] = useState<string[]>([]);
|
||||
const [requiredKeyUsages, setRequiredKeyUsages] = useState<string[]>([]);
|
||||
const [requiredExtendedKeyUsages, setRequiredExtendedKeyUsages] = useState<string[]>([]);
|
||||
const [allowedSignatureAlgorithms, setAllowedSignatureAlgorithms] = useState<string[]>([]);
|
||||
const [allowedKeyAlgorithms, setAllowedKeyAlgorithms] = useState<string[]>([]);
|
||||
const { currentProject } = useProject();
|
||||
|
||||
const { data: cert } = useGetCert(
|
||||
(popUp?.certificateIssuance?.data as { serialNumber: string })?.serialNumber || ""
|
||||
);
|
||||
@@ -133,7 +133,7 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
profileId: profileId ? profileId : "",
|
||||
profileId: profileId || "",
|
||||
subjectAttributes: [{ type: "common_name", value: "" }],
|
||||
subjectAltNames: [],
|
||||
ttl: "30d",
|
||||
@@ -144,276 +144,122 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
||||
}
|
||||
});
|
||||
|
||||
const resetAllState = useCallback(() => {
|
||||
setCertificateDetails(null);
|
||||
setAllowedKeyUsages([]);
|
||||
setAllowedExtendedKeyUsages([]);
|
||||
setRequiredKeyUsages([]);
|
||||
setRequiredExtendedKeyUsages([]);
|
||||
setAllowedSignatureAlgorithms([]);
|
||||
setAllowedKeyAlgorithms([]);
|
||||
reset();
|
||||
}, [reset]);
|
||||
|
||||
const selectedProfileId = watch("profileId");
|
||||
const selectedProfile = profilesData?.certificateProfiles?.find(
|
||||
(p) => p.id === selectedProfileId
|
||||
const selectedProfile = useMemo(
|
||||
() => profilesData?.certificateProfiles?.find((p) => p.id === selectedProfileId),
|
||||
[profilesData?.certificateProfiles, selectedProfileId]
|
||||
);
|
||||
|
||||
const { data: templateData } = useGetCertificateTemplateV2ById({
|
||||
templateId: selectedProfile?.certificateTemplateId || ""
|
||||
});
|
||||
|
||||
const filteredKeyUsages = useMemo(() => {
|
||||
if (allowedKeyUsages.length === 0) return KEY_USAGES_OPTIONS;
|
||||
return KEY_USAGES_OPTIONS.filter(({ value }) => allowedKeyUsages.includes(value));
|
||||
}, [allowedKeyUsages]);
|
||||
|
||||
const filteredExtendedKeyUsages = useMemo(() => {
|
||||
if (allowedExtendedKeyUsages.length === 0) return EXTENDED_KEY_USAGES_OPTIONS;
|
||||
return EXTENDED_KEY_USAGES_OPTIONS.filter(({ value }) =>
|
||||
allowedExtendedKeyUsages.includes(value)
|
||||
);
|
||||
}, [allowedExtendedKeyUsages]);
|
||||
|
||||
const availableSignatureAlgorithms = useMemo(() => {
|
||||
if (allowedSignatureAlgorithms.length === 0) {
|
||||
return SIGNATURE_ALGORITHMS_OPTIONS;
|
||||
}
|
||||
return allowedSignatureAlgorithms.map((templateAlgorithm) => {
|
||||
const apiAlgorithm = mapTemplateSignatureAlgorithmToApi(templateAlgorithm);
|
||||
return {
|
||||
value: apiAlgorithm,
|
||||
label: apiAlgorithm
|
||||
};
|
||||
});
|
||||
}, [allowedSignatureAlgorithms]);
|
||||
|
||||
const availableKeyAlgorithms = useMemo(() => {
|
||||
if (allowedKeyAlgorithms.length === 0) {
|
||||
return certKeyAlgorithms;
|
||||
}
|
||||
return allowedKeyAlgorithms.map((templateAlgorithm) => {
|
||||
const apiAlgorithm = mapTemplateKeyAlgorithmToApi(templateAlgorithm);
|
||||
return {
|
||||
value: apiAlgorithm,
|
||||
label: apiAlgorithm
|
||||
};
|
||||
});
|
||||
}, [allowedKeyAlgorithms]);
|
||||
|
||||
useEffect(() => {
|
||||
if (templateData && selectedProfile) {
|
||||
if (templateData.signatureAlgorithm?.allowedAlgorithms && templateData.signatureAlgorithm.allowedAlgorithms.length > 0) {
|
||||
const sigAlgMap: Record<string, string> = {
|
||||
"SHA256-RSA": "RSA-SHA256",
|
||||
"SHA384-RSA": "RSA-SHA384",
|
||||
"SHA512-RSA": "RSA-SHA512",
|
||||
"SHA256-ECDSA": "ECDSA-SHA256",
|
||||
"SHA384-ECDSA": "ECDSA-SHA384",
|
||||
"SHA512-ECDSA": "ECDSA-SHA512"
|
||||
};
|
||||
|
||||
let defaultValue = templateData.signatureAlgorithm.defaultAlgorithm;
|
||||
if (defaultValue && sigAlgMap[defaultValue]) {
|
||||
defaultValue = sigAlgMap[defaultValue];
|
||||
}
|
||||
|
||||
const allowedValues = templateData.signatureAlgorithm.allowedAlgorithms.map((alg: string) => sigAlgMap[alg] || alg);
|
||||
|
||||
if (defaultValue && allowedValues.includes(defaultValue)) {
|
||||
setValue("signatureAlgorithm", defaultValue);
|
||||
} else if (allowedValues.length > 0) {
|
||||
setValue("signatureAlgorithm", allowedValues[0]);
|
||||
}
|
||||
}
|
||||
if (templateData.keyAlgorithm?.allowedKeyTypes && templateData.keyAlgorithm.allowedKeyTypes.length > 0) {
|
||||
const keyAlgMap: Record<string, string> = {
|
||||
"RSA-2048": CertKeyAlgorithm.RSA_2048,
|
||||
"RSA-3072": CertKeyAlgorithm.RSA_3072,
|
||||
"RSA-4096": CertKeyAlgorithm.RSA_4096,
|
||||
"ECDSA-P256": CertKeyAlgorithm.ECDSA_P256,
|
||||
"ECDSA-P384": CertKeyAlgorithm.ECDSA_P384,
|
||||
[CertKeyAlgorithm.ECDSA_P256]: CertKeyAlgorithm.ECDSA_P256,
|
||||
[CertKeyAlgorithm.ECDSA_P384]: CertKeyAlgorithm.ECDSA_P384
|
||||
};
|
||||
|
||||
let defaultValue = templateData.keyAlgorithm.defaultKeyType;
|
||||
if (defaultValue && keyAlgMap[defaultValue]) {
|
||||
defaultValue = keyAlgMap[defaultValue];
|
||||
}
|
||||
|
||||
const allowedValues = templateData.keyAlgorithm.allowedKeyTypes.map((alg: string) => keyAlgMap[alg] || alg);
|
||||
|
||||
if (defaultValue && allowedValues.includes(defaultValue)) {
|
||||
setValue("keyAlgorithm", defaultValue);
|
||||
} else if (allowedValues.length > 0) {
|
||||
setValue("keyAlgorithm", allowedValues[0]);
|
||||
}
|
||||
}
|
||||
|
||||
if (templateData.validity?.maxDuration) {
|
||||
const { value, unit } = templateData.validity.maxDuration;
|
||||
let ttlValue = "";
|
||||
|
||||
switch (unit) {
|
||||
case "days":
|
||||
ttlValue = `${value}d`;
|
||||
break;
|
||||
case "months":
|
||||
ttlValue = `${value}m`;
|
||||
break;
|
||||
case "years":
|
||||
ttlValue = `${value}y`;
|
||||
break;
|
||||
default:
|
||||
ttlValue = `${value}d`;
|
||||
}
|
||||
|
||||
setValue("ttl", ttlValue);
|
||||
}
|
||||
|
||||
if (templateData.signatureAlgorithm?.allowedAlgorithms) {
|
||||
const mappedSigAlgs = templateData.signatureAlgorithm.allowedAlgorithms.map(
|
||||
(alg: string) => {
|
||||
const sigAlgMap: Record<string, string> = {
|
||||
"SHA256-RSA": "RSA-SHA256",
|
||||
"SHA384-RSA": "RSA-SHA384",
|
||||
"SHA512-RSA": "RSA-SHA512",
|
||||
"SHA256-ECDSA": "ECDSA-SHA256",
|
||||
"SHA384-ECDSA": "ECDSA-SHA384",
|
||||
"SHA512-ECDSA": "ECDSA-SHA512"
|
||||
};
|
||||
return sigAlgMap[alg] || alg;
|
||||
}
|
||||
);
|
||||
setAllowedSignatureAlgorithms(mappedSigAlgs);
|
||||
}
|
||||
|
||||
if (templateData.keyAlgorithm?.allowedKeyTypes) {
|
||||
const mappedKeyAlgs = templateData.keyAlgorithm.allowedKeyTypes.map((alg: string) => {
|
||||
const keyAlgMap: Record<string, string> = {
|
||||
"RSA-2048": CertKeyAlgorithm.RSA_2048,
|
||||
"RSA-3072": CertKeyAlgorithm.RSA_3072,
|
||||
"RSA-4096": CertKeyAlgorithm.RSA_4096,
|
||||
"ECDSA-P256": CertKeyAlgorithm.ECDSA_P256,
|
||||
"ECDSA-P384": CertKeyAlgorithm.ECDSA_P384
|
||||
};
|
||||
return keyAlgMap[alg] || alg;
|
||||
});
|
||||
setAllowedKeyAlgorithms(mappedKeyAlgs);
|
||||
}
|
||||
|
||||
const allAllowedKeyUsages: string[] = [];
|
||||
if (templateData.keyUsages?.requiredUsages?.all) {
|
||||
allAllowedKeyUsages.push(...templateData.keyUsages.requiredUsages.all);
|
||||
}
|
||||
if (templateData.keyUsages?.optionalUsages?.all) {
|
||||
allAllowedKeyUsages.push(...templateData.keyUsages.optionalUsages.all);
|
||||
}
|
||||
setAllowedKeyUsages([...new Set(allAllowedKeyUsages)]);
|
||||
|
||||
const allAllowedExtendedKeyUsages: string[] = [];
|
||||
if (templateData.extendedKeyUsages?.requiredUsages?.all) {
|
||||
allAllowedExtendedKeyUsages.push(...templateData.extendedKeyUsages.requiredUsages.all);
|
||||
}
|
||||
if (templateData.extendedKeyUsages?.optionalUsages?.all) {
|
||||
allAllowedExtendedKeyUsages.push(...templateData.extendedKeyUsages.optionalUsages.all);
|
||||
}
|
||||
setAllowedExtendedKeyUsages([...new Set(allAllowedExtendedKeyUsages)]);
|
||||
|
||||
if (templateData.attributes && Array.isArray(templateData.attributes)) {
|
||||
const subjectAttrs: Array<{
|
||||
type: "common_name";
|
||||
value: string;
|
||||
}> = [];
|
||||
|
||||
templateData.attributes.forEach((attr) => {
|
||||
if (
|
||||
(attr.include === "mandatory" ||
|
||||
attr.include === "optional" ||
|
||||
attr.include === "prohibit") &&
|
||||
attr.value &&
|
||||
attr.value.length > 0
|
||||
) {
|
||||
attr.value.forEach((val: string) => {
|
||||
subjectAttrs.push({ type: attr.type as any, value: val });
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (subjectAttrs.length > 0) {
|
||||
setValue("subjectAttributes", subjectAttrs);
|
||||
} else {
|
||||
setValue("subjectAttributes", [{ type: "common_name", value: "" }]);
|
||||
}
|
||||
if (templateData && selectedProfile && popUp?.certificateIssuance?.isOpen) {
|
||||
if (templateData.algorithms?.signature && templateData.algorithms.signature.length > 0) {
|
||||
setAllowedSignatureAlgorithms(templateData.algorithms.signature);
|
||||
} else {
|
||||
setAllowedSignatureAlgorithms([]);
|
||||
}
|
||||
|
||||
if (
|
||||
templateData.subjectAlternativeNames &&
|
||||
Array.isArray(templateData.subjectAlternativeNames)
|
||||
templateData.algorithms?.keyAlgorithm &&
|
||||
templateData.algorithms.keyAlgorithm.length > 0
|
||||
) {
|
||||
const templateSans: Array<{ type: "dns" | "ip" | "email" | "uri"; value: string }> = [];
|
||||
|
||||
templateData.subjectAlternativeNames.forEach((sanPolicy) => {
|
||||
if (
|
||||
(sanPolicy.include === "mandatory" ||
|
||||
sanPolicy.include === "optional" ||
|
||||
sanPolicy.include === "prohibit") &&
|
||||
sanPolicy.value &&
|
||||
sanPolicy.value.length > 0
|
||||
) {
|
||||
const typeMapping: Record<string, "dns" | "ip" | "email" | "uri"> = {
|
||||
dns_name: "dns",
|
||||
ip_address: "ip",
|
||||
email: "email",
|
||||
uri: "uri"
|
||||
};
|
||||
|
||||
const mappedType = typeMapping[sanPolicy.type];
|
||||
if (mappedType) {
|
||||
sanPolicy.value.forEach((val: string) => {
|
||||
templateSans.push({ type: mappedType, value: val });
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (templateSans.length > 0) {
|
||||
setValue("subjectAltNames", templateSans);
|
||||
}
|
||||
setAllowedKeyAlgorithms(templateData.algorithms.keyAlgorithm);
|
||||
} else {
|
||||
setAllowedKeyAlgorithms([]);
|
||||
}
|
||||
|
||||
const resetKeyUsages = {
|
||||
[CertKeyUsage.DIGITAL_SIGNATURE]: false,
|
||||
[CertKeyUsage.KEY_ENCIPHERMENT]: false,
|
||||
[CertKeyUsage.NON_REPUDIATION]: false,
|
||||
[CertKeyUsage.DATA_ENCIPHERMENT]: false,
|
||||
[CertKeyUsage.KEY_AGREEMENT]: false,
|
||||
[CertKeyUsage.KEY_CERT_SIGN]: false,
|
||||
[CertKeyUsage.CRL_SIGN]: false,
|
||||
[CertKeyUsage.ENCIPHER_ONLY]: false,
|
||||
[CertKeyUsage.DECIPHER_ONLY]: false
|
||||
};
|
||||
|
||||
const resetExtendedKeyUsages = {
|
||||
[CertExtendedKeyUsage.CLIENT_AUTH]: false,
|
||||
[CertExtendedKeyUsage.CODE_SIGNING]: false,
|
||||
[CertExtendedKeyUsage.EMAIL_PROTECTION]: false,
|
||||
[CertExtendedKeyUsage.OCSP_SIGNING]: false,
|
||||
[CertExtendedKeyUsage.SERVER_AUTH]: false,
|
||||
[CertExtendedKeyUsage.TIMESTAMPING]: false
|
||||
};
|
||||
|
||||
const templateToEnumMap = {
|
||||
digital_signature: CertKeyUsage.DIGITAL_SIGNATURE,
|
||||
digitalSignature: CertKeyUsage.DIGITAL_SIGNATURE,
|
||||
key_encipherment: CertKeyUsage.KEY_ENCIPHERMENT,
|
||||
keyEncipherment: CertKeyUsage.KEY_ENCIPHERMENT,
|
||||
non_repudiation: CertKeyUsage.NON_REPUDIATION,
|
||||
nonRepudiation: CertKeyUsage.NON_REPUDIATION,
|
||||
data_encipherment: CertKeyUsage.DATA_ENCIPHERMENT,
|
||||
dataEncipherment: CertKeyUsage.DATA_ENCIPHERMENT,
|
||||
key_agreement: CertKeyUsage.KEY_AGREEMENT,
|
||||
keyAgreement: CertKeyUsage.KEY_AGREEMENT,
|
||||
key_cert_sign: CertKeyUsage.KEY_CERT_SIGN,
|
||||
keyCertSign: CertKeyUsage.KEY_CERT_SIGN,
|
||||
crl_sign: CertKeyUsage.CRL_SIGN,
|
||||
cRLSign: CertKeyUsage.CRL_SIGN,
|
||||
encipher_only: CertKeyUsage.ENCIPHER_ONLY,
|
||||
encipherOnly: CertKeyUsage.ENCIPHER_ONLY,
|
||||
decipher_only: CertKeyUsage.DECIPHER_ONLY,
|
||||
decipherOnly: CertKeyUsage.DECIPHER_ONLY,
|
||||
client_auth: CertExtendedKeyUsage.CLIENT_AUTH,
|
||||
clientAuth: CertExtendedKeyUsage.CLIENT_AUTH,
|
||||
server_auth: CertExtendedKeyUsage.SERVER_AUTH,
|
||||
serverAuth: CertExtendedKeyUsage.SERVER_AUTH,
|
||||
code_signing: CertExtendedKeyUsage.CODE_SIGNING,
|
||||
codeSigning: CertExtendedKeyUsage.CODE_SIGNING,
|
||||
email_protection: CertExtendedKeyUsage.EMAIL_PROTECTION,
|
||||
emailProtection: CertExtendedKeyUsage.EMAIL_PROTECTION,
|
||||
ocsp_signing: CertExtendedKeyUsage.OCSP_SIGNING,
|
||||
ocspSigning: CertExtendedKeyUsage.OCSP_SIGNING,
|
||||
time_stamping: CertExtendedKeyUsage.TIMESTAMPING,
|
||||
timestamping: CertExtendedKeyUsage.TIMESTAMPING,
|
||||
timeStamping: CertExtendedKeyUsage.TIMESTAMPING
|
||||
};
|
||||
|
||||
const currentKeyUsages = { ...resetKeyUsages };
|
||||
if (templateData.keyUsages?.requiredUsages?.all) {
|
||||
templateData.keyUsages.requiredUsages.all.forEach((usage: string) => {
|
||||
const enumValue = (templateToEnumMap as any)[usage];
|
||||
if (enumValue && enumValue in currentKeyUsages) {
|
||||
(currentKeyUsages as any)[enumValue] = true;
|
||||
}
|
||||
});
|
||||
if (templateData.validity?.max) {
|
||||
setValue("ttl", templateData.validity.max);
|
||||
}
|
||||
|
||||
const currentExtendedKeyUsages = { ...resetExtendedKeyUsages };
|
||||
if (templateData.extendedKeyUsages?.requiredUsages?.all) {
|
||||
templateData.extendedKeyUsages.requiredUsages.all.forEach((usage: string) => {
|
||||
const enumValue = (templateToEnumMap as any)[usage];
|
||||
if (enumValue && enumValue in currentExtendedKeyUsages) {
|
||||
(currentExtendedKeyUsages as any)[enumValue] = true;
|
||||
}
|
||||
});
|
||||
const keyUsages: string[] = [];
|
||||
if (templateData.keyUsages?.required) {
|
||||
keyUsages.push(...templateData.keyUsages.required);
|
||||
}
|
||||
if (templateData.keyUsages?.allowed) {
|
||||
keyUsages.push(...templateData.keyUsages.allowed);
|
||||
}
|
||||
setAllowedKeyUsages(keyUsages);
|
||||
|
||||
setValue("keyUsages", currentKeyUsages);
|
||||
setValue("extendedKeyUsages", currentExtendedKeyUsages);
|
||||
const extendedKeyUsages: string[] = [];
|
||||
if (templateData.extendedKeyUsages?.required) {
|
||||
extendedKeyUsages.push(...templateData.extendedKeyUsages.required);
|
||||
}
|
||||
if (templateData.extendedKeyUsages?.allowed) {
|
||||
extendedKeyUsages.push(...templateData.extendedKeyUsages.allowed);
|
||||
}
|
||||
setAllowedExtendedKeyUsages(extendedKeyUsages);
|
||||
|
||||
setRequiredKeyUsages(templateData.keyUsages?.required || []);
|
||||
setRequiredExtendedKeyUsages(templateData.extendedKeyUsages?.required || []);
|
||||
|
||||
const initialKeyUsages: Record<string, boolean> = {};
|
||||
const initialExtendedKeyUsages: Record<string, boolean> = {};
|
||||
|
||||
(templateData.keyUsages?.required || []).forEach((usage: string) => {
|
||||
initialKeyUsages[usage] = true;
|
||||
});
|
||||
|
||||
(templateData.extendedKeyUsages?.required || []).forEach((usage: string) => {
|
||||
initialExtendedKeyUsages[usage] = true;
|
||||
});
|
||||
|
||||
setValue("keyUsages", initialKeyUsages);
|
||||
setValue("extendedKeyUsages", initialExtendedKeyUsages);
|
||||
}
|
||||
}, [templateData, selectedProfile, setValue]);
|
||||
}, [templateData, selectedProfile, setValue, popUp?.certificateIssuance?.isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (cert) {
|
||||
@@ -451,83 +297,138 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
||||
}
|
||||
}, [popUp?.certificateIssuance?.isOpen, profileId, cert, setValue]);
|
||||
|
||||
const onFormSubmit = async ({
|
||||
profileId,
|
||||
subjectAttributes,
|
||||
subjectAltNames,
|
||||
ttl,
|
||||
signatureAlgorithm,
|
||||
keyAlgorithm,
|
||||
keyUsages,
|
||||
extendedKeyUsages
|
||||
}: FormData) => {
|
||||
try {
|
||||
if (!currentProject?.slug) return;
|
||||
const getAttributeValue = useCallback(
|
||||
(subjectAttributes: typeof schema._type.subjectAttributes, type: string) => {
|
||||
const foundAttr = subjectAttributes.find((attr) => attr.type === type);
|
||||
return foundAttr?.value || "";
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const getAttributeValue = (type: string) => {
|
||||
const foundAttr = subjectAttributes.find((attr) => attr.type === type);
|
||||
return foundAttr?.value || "";
|
||||
};
|
||||
const formatSubjectAltNames = useCallback(
|
||||
(subjectAltNames: typeof schema._type.subjectAltNames) => {
|
||||
return subjectAltNames
|
||||
.filter((san) => san.value.trim())
|
||||
.map((san) => san.value.trim())
|
||||
.join(", ");
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const { serialNumber, certificate, certificateChain, privateKey } = await createCertificate({
|
||||
profileId,
|
||||
projectSlug: currentProject.slug,
|
||||
commonName: getAttributeValue("common_name"),
|
||||
subjectAltNames: subjectAltNames
|
||||
.filter((san) => san.value.trim())
|
||||
.map((san) => san.value.trim())
|
||||
.join(", "),
|
||||
ttl,
|
||||
signatureAlgorithm: (() => {
|
||||
const frontendToBackendSigAlg: Record<string, string> = {
|
||||
"RSA-SHA256": "RSA-SHA256",
|
||||
"RSA-SHA384": "RSA-SHA384",
|
||||
"RSA-SHA512": "RSA-SHA512",
|
||||
"ECDSA-SHA256": "ECDSA-SHA256",
|
||||
"ECDSA-SHA384": "ECDSA-SHA384",
|
||||
"ECDSA-SHA512": "ECDSA-SHA512"
|
||||
};
|
||||
return signatureAlgorithm
|
||||
? frontendToBackendSigAlg[signatureAlgorithm] || signatureAlgorithm
|
||||
: undefined;
|
||||
})(),
|
||||
keyAlgorithm: (() => {
|
||||
const frontendToBackendKeyAlg: Record<string, string> = {
|
||||
RSA_2048: "RSA_2048",
|
||||
RSA_3072: "RSA_3072",
|
||||
RSA_4096: "RSA_4096",
|
||||
EC_prime256v1: "EC_prime256v1",
|
||||
EC_secp384r1: "EC_secp384r1"
|
||||
};
|
||||
return keyAlgorithm ? frontendToBackendKeyAlg[keyAlgorithm] || keyAlgorithm : undefined;
|
||||
})(),
|
||||
keyUsages: Object.entries(keyUsages)
|
||||
.filter(([, value]) => value)
|
||||
.map(([key]) => key as CertKeyUsage),
|
||||
extendedKeyUsages: Object.entries(extendedKeyUsages)
|
||||
.filter(([, value]) => value)
|
||||
.map(([key]) => key as CertExtendedKeyUsage)
|
||||
});
|
||||
const filterUsages = useCallback(<T extends Record<string, boolean>>(usages: T) => {
|
||||
return Object.entries(usages)
|
||||
.filter(([, value]) => value)
|
||||
.map(([key]) => key);
|
||||
}, []);
|
||||
|
||||
reset();
|
||||
const onFormSubmit = useCallback(
|
||||
async ({
|
||||
profileId: formProfileId,
|
||||
subjectAttributes,
|
||||
subjectAltNames,
|
||||
ttl,
|
||||
signatureAlgorithm,
|
||||
keyAlgorithm,
|
||||
keyUsages,
|
||||
extendedKeyUsages
|
||||
}: FormData) => {
|
||||
try {
|
||||
if (!currentProject?.slug) {
|
||||
createNotification({
|
||||
text: "Project not found. Please refresh and try again.",
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setCertificateDetails({
|
||||
serialNumber,
|
||||
certificate,
|
||||
certificateChain,
|
||||
privateKey
|
||||
});
|
||||
if (!formProfileId) {
|
||||
createNotification({
|
||||
text: "Please select a certificate profile.",
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
createNotification({
|
||||
text: "Successfully created certificate",
|
||||
type: "success"
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to create certificate",
|
||||
type: "error"
|
||||
});
|
||||
const commonName = getAttributeValue(subjectAttributes, "common_name");
|
||||
if (!commonName.trim()) {
|
||||
createNotification({
|
||||
text: "Common name is required.",
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const { serialNumber, certificate, certificateChain, privateKey } = await createCertificate(
|
||||
{
|
||||
profileId: formProfileId,
|
||||
projectSlug: currentProject.slug,
|
||||
commonName,
|
||||
subjectAltNames: formatSubjectAltNames(subjectAltNames),
|
||||
ttl,
|
||||
signatureAlgorithm,
|
||||
keyAlgorithm,
|
||||
keyUsages: filterUsages(keyUsages) as CertKeyUsage[],
|
||||
extendedKeyUsages: filterUsages(extendedKeyUsages) as CertExtendedKeyUsage[]
|
||||
}
|
||||
);
|
||||
|
||||
setCertificateDetails({
|
||||
serialNumber,
|
||||
certificate,
|
||||
certificateChain,
|
||||
privateKey
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully created certificate",
|
||||
type: "success"
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Certificate creation failed:", err);
|
||||
const errorMessage =
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: "An unexpected error occurred while creating the certificate";
|
||||
createNotification({
|
||||
text: `Failed to create certificate: ${errorMessage}`,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
},
|
||||
[
|
||||
currentProject?.slug,
|
||||
createCertificate,
|
||||
reset,
|
||||
getAttributeValue,
|
||||
formatSubjectAltNames,
|
||||
filterUsages
|
||||
]
|
||||
);
|
||||
|
||||
const getModalTitle = () => {
|
||||
if (certificateDetails) return "Certificate Created Successfully";
|
||||
if (cert) return "Certificate Details";
|
||||
return "Issue New Certificate";
|
||||
};
|
||||
|
||||
const getModalSubTitle = () => {
|
||||
if (certificateDetails) return "Certificate has been successfully created and is ready for use";
|
||||
if (cert) return "View certificate information";
|
||||
return "Issue a new certificate using a certificate profile";
|
||||
};
|
||||
|
||||
const getSanPlaceholder = (sanType: string) => {
|
||||
switch (sanType) {
|
||||
case "dns":
|
||||
return "example.com or *.example.com";
|
||||
case "ip":
|
||||
return "192.168.1.1";
|
||||
case "email":
|
||||
return "admin@example.com";
|
||||
case "uri":
|
||||
return "https://example.com";
|
||||
default:
|
||||
return "Enter value";
|
||||
}
|
||||
};
|
||||
|
||||
@@ -536,26 +437,12 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
||||
isOpen={popUp?.certificateIssuance?.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle("certificateIssuance", isOpen);
|
||||
setCertificateDetails(null);
|
||||
reset();
|
||||
if (!isOpen) {
|
||||
resetAllState();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ModalContent
|
||||
title={
|
||||
certificateDetails
|
||||
? "Certificate Created Successfully"
|
||||
: cert
|
||||
? "Certificate Details"
|
||||
: "Issue New Certificate"
|
||||
}
|
||||
subTitle={
|
||||
certificateDetails
|
||||
? "Certificate has been successfully created and is ready for use"
|
||||
: cert
|
||||
? "View certificate information"
|
||||
: "Issue a new certificate using a certificate profile"
|
||||
}
|
||||
>
|
||||
<ModalContent title={getModalTitle()} subTitle={getModalSubTitle()}>
|
||||
{certificateDetails && (
|
||||
<CertificateContent
|
||||
serialNumber={certificateDetails.serialNumber}
|
||||
@@ -567,10 +454,10 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
||||
{cert && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="text-mineshaft-300 text-sm font-medium">Certificate Details</h4>
|
||||
<p className="text-mineshaft-400 text-sm">Serial Number: {cert.serialNumber}</p>
|
||||
<p className="text-mineshaft-400 text-sm">Common Name: {cert.commonName}</p>
|
||||
<p className="text-mineshaft-400 text-sm">Status: {cert.status}</p>
|
||||
<h4 className="text-sm font-medium text-mineshaft-300">Certificate Details</h4>
|
||||
<p className="text-sm text-mineshaft-400">Serial Number: {cert.serialNumber}</p>
|
||||
<p className="text-sm text-mineshaft-400">Common Name: {cert.commonName}</p>
|
||||
<p className="text-sm text-mineshaft-400">Status: {cert.status}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -592,9 +479,9 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
||||
className="text-center"
|
||||
content={
|
||||
<span>
|
||||
Certificate profiles define the policies and enrollment methods for
|
||||
certificate issuance. The selected profile will enforce validation
|
||||
rules and determine the CA used for signing.
|
||||
Certificate profiles define the policies and enrollment methods
|
||||
for certificate issuance. The selected profile will enforce
|
||||
validation rules and determine the CA used for signing.
|
||||
</span>
|
||||
}
|
||||
>
|
||||
@@ -629,7 +516,6 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
||||
|
||||
{(selectedProfile || profileId) && (
|
||||
<>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="subjectAttributes"
|
||||
@@ -642,7 +528,8 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
||||
>
|
||||
<div className="space-y-2">
|
||||
{value.map((attr, index) => (
|
||||
<div key={`attr-${index}`} className="flex items-start gap-2">
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<div key={`subject-attr-${index}`} className="flex items-center gap-2">
|
||||
<Select
|
||||
value={attr.type}
|
||||
onValueChange={(newType) => {
|
||||
@@ -710,7 +597,11 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
||||
>
|
||||
<div className="space-y-2">
|
||||
{value.map((san, index) => (
|
||||
<div key={`san-${index}`} className="flex items-start gap-2">
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<div
|
||||
key={`subject-alt-name-${index}`}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Select
|
||||
value={san.type}
|
||||
onValueChange={(newType) => {
|
||||
@@ -735,15 +626,7 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
||||
newValue[index] = { ...san, value: e.target.value };
|
||||
onChange(newValue);
|
||||
}}
|
||||
placeholder={
|
||||
san.type === "dns"
|
||||
? "example.com or *.example.com"
|
||||
: san.type === "ip"
|
||||
? "192.168.1.1"
|
||||
: san.type === "email"
|
||||
? "admin@example.com"
|
||||
: "https://example.com"
|
||||
}
|
||||
placeholder={getSanPlaceholder(san.type)}
|
||||
className="flex-1"
|
||||
/>
|
||||
<IconButton
|
||||
@@ -807,13 +690,14 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
placeholder="Use template default"
|
||||
placeholder={
|
||||
availableSignatureAlgorithms.length > 0
|
||||
? "Select signature algorithm"
|
||||
: "No algorithms available"
|
||||
}
|
||||
position="popper"
|
||||
>
|
||||
{SIGNATURE_ALGORITHMS_OPTIONS.filter((algorithm) => {
|
||||
if (allowedSignatureAlgorithms.length === 0) return true;
|
||||
return allowedSignatureAlgorithms.includes(algorithm.value);
|
||||
}).map((algorithm) => (
|
||||
{availableSignatureAlgorithms.map((algorithm) => (
|
||||
<SelectItem key={algorithm.value} value={algorithm.value}>
|
||||
{algorithm.label}
|
||||
</SelectItem>
|
||||
@@ -839,19 +723,18 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
placeholder="Use template default"
|
||||
placeholder={
|
||||
availableKeyAlgorithms.length > 0
|
||||
? "Select key algorithm"
|
||||
: "No algorithms available"
|
||||
}
|
||||
position="popper"
|
||||
>
|
||||
{certKeyAlgorithms
|
||||
.filter((algorithm) => {
|
||||
if (allowedKeyAlgorithms.length === 0) return true;
|
||||
return allowedKeyAlgorithms.includes(algorithm.value);
|
||||
})
|
||||
.map((algorithm) => (
|
||||
<SelectItem key={algorithm.value} value={algorithm.value}>
|
||||
{algorithm.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
{availableKeyAlgorithms.map((algorithm) => (
|
||||
<SelectItem key={algorithm.value} value={algorithm.value}>
|
||||
{algorithm.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
@@ -863,88 +746,80 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
||||
<AccordionItem value="key-usages">
|
||||
<AccordionTrigger>Key Usages</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="grid grid-cols-2 gap-2 pl-2">
|
||||
{KEY_USAGES_OPTIONS.filter(({ value }) => {
|
||||
if (allowedKeyUsages.length === 0) return true;
|
||||
const templateToEnumMap = {
|
||||
digital_signature: CertKeyUsage.DIGITAL_SIGNATURE,
|
||||
key_encipherment: CertKeyUsage.KEY_ENCIPHERMENT,
|
||||
non_repudiation: CertKeyUsage.NON_REPUDIATION,
|
||||
data_encipherment: CertKeyUsage.DATA_ENCIPHERMENT,
|
||||
key_agreement: CertKeyUsage.KEY_AGREEMENT,
|
||||
key_cert_sign: CertKeyUsage.KEY_CERT_SIGN,
|
||||
crl_sign: CertKeyUsage.CRL_SIGN,
|
||||
encipher_only: CertKeyUsage.ENCIPHER_ONLY,
|
||||
decipher_only: CertKeyUsage.DECIPHER_ONLY
|
||||
};
|
||||
return allowedKeyUsages.some(
|
||||
(allowedUsage) => (templateToEnumMap as any)[allowedUsage] === value
|
||||
);
|
||||
}).map(({ label, value }) => (
|
||||
<Controller
|
||||
key={label}
|
||||
control={control}
|
||||
name={`keyUsages.${value}` as any}
|
||||
render={({ field }) => (
|
||||
<div className="flex items-center space-x-3">
|
||||
<Checkbox
|
||||
id={`key-usage-${value}`}
|
||||
isChecked={field.value || false}
|
||||
onCheckedChange={(checked) => field.onChange(checked)}
|
||||
/>
|
||||
<FormLabel
|
||||
id={`key-usage-${value}`}
|
||||
className="text-mineshaft-300 cursor-pointer text-sm"
|
||||
label={label}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2 pl-2">
|
||||
{filteredKeyUsages.map(({ label, value }) => {
|
||||
const isRequired = requiredKeyUsages.includes(value);
|
||||
return (
|
||||
<Controller
|
||||
key={label}
|
||||
control={control}
|
||||
name={`keyUsages.${value}` as any}
|
||||
render={({ field }) => (
|
||||
<div className="flex items-center space-x-3">
|
||||
<Checkbox
|
||||
id={`key-usage-${value}`}
|
||||
isChecked={field.value || false}
|
||||
onCheckedChange={(checked) => {
|
||||
if (!isRequired) {
|
||||
field.onChange(checked);
|
||||
}
|
||||
}}
|
||||
isDisabled={isRequired}
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<FormLabel
|
||||
id={`key-usage-${value}`}
|
||||
className={`text-sm ${isRequired ? "text-mineshaft-200" : "cursor-pointer text-mineshaft-300"}`}
|
||||
label={label}
|
||||
/>
|
||||
{isRequired && <span className="text-xs">(Required)</span>}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="extended-key-usages">
|
||||
<AccordionTrigger>Extended Key Usages</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="grid grid-cols-2 gap-2 pl-2">
|
||||
{EXTENDED_KEY_USAGES_OPTIONS.filter(({ value }) => {
|
||||
if (allowedExtendedKeyUsages.length === 0) return true;
|
||||
const templateToEnumMap = {
|
||||
client_auth: CertExtendedKeyUsage.CLIENT_AUTH,
|
||||
server_auth: CertExtendedKeyUsage.SERVER_AUTH,
|
||||
code_signing: CertExtendedKeyUsage.CODE_SIGNING,
|
||||
email_protection: CertExtendedKeyUsage.EMAIL_PROTECTION,
|
||||
ocsp_signing: CertExtendedKeyUsage.OCSP_SIGNING,
|
||||
time_stamping: CertExtendedKeyUsage.TIMESTAMPING,
|
||||
timestamping: CertExtendedKeyUsage.TIMESTAMPING
|
||||
};
|
||||
return allowedExtendedKeyUsages.some(
|
||||
(allowedUsage) => (templateToEnumMap as any)[allowedUsage] === value
|
||||
);
|
||||
}).map(({ label, value }) => (
|
||||
<Controller
|
||||
key={label}
|
||||
control={control}
|
||||
name={`extendedKeyUsages.${value}` as any}
|
||||
render={({ field }) => (
|
||||
<div className="flex items-center space-x-3">
|
||||
<Checkbox
|
||||
id={`ext-key-usage-${value}`}
|
||||
isChecked={field.value || false}
|
||||
onCheckedChange={(checked) => field.onChange(checked)}
|
||||
/>
|
||||
<FormLabel
|
||||
id={`ext-key-usage-${value}`}
|
||||
className="text-mineshaft-300 cursor-pointer text-sm"
|
||||
label={label}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2 pl-2">
|
||||
{filteredExtendedKeyUsages.map(({ label, value }) => {
|
||||
const isRequired = requiredExtendedKeyUsages.includes(value);
|
||||
return (
|
||||
<Controller
|
||||
key={label}
|
||||
control={control}
|
||||
name={`extendedKeyUsages.${value}` as any}
|
||||
render={({ field }) => (
|
||||
<div className="flex items-center space-x-3">
|
||||
<Checkbox
|
||||
id={`ext-key-usage-${value}`}
|
||||
isChecked={field.value || false}
|
||||
onCheckedChange={(checked) => {
|
||||
if (!isRequired) {
|
||||
field.onChange(checked);
|
||||
}
|
||||
}}
|
||||
isDisabled={isRequired}
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<FormLabel
|
||||
id={`ext-key-usage-${value}`}
|
||||
className={`text-sm ${isRequired ? "text-mineshaft-200" : "cursor-pointer text-mineshaft-300"}`}
|
||||
label={label}
|
||||
/>
|
||||
{isRequired && <span className="text-xs">(Required)</span>}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
@@ -964,7 +839,9 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => handlePopUpToggle("certificateIssuance", false)}
|
||||
onClick={() => {
|
||||
handlePopUpToggle("certificateIssuance", false);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
@@ -87,16 +87,16 @@ export const PkiSubscriberSection = () => {
|
||||
const subscriberName = subscriberStatusData?.subscriberName || "";
|
||||
|
||||
return (
|
||||
<div className="border-mineshaft-600 bg-mineshaft-900 mb-6 rounded-lg border p-4">
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex justify-between">
|
||||
<p className="text-mineshaft-100 text-xl font-medium">Subscribers</p>
|
||||
<p className="text-xl font-medium text-mineshaft-100">Subscribers</p>
|
||||
<div className="flex w-full justify-end">
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://infisical.com/docs/documentation/platform/pki/subscribers"
|
||||
>
|
||||
<span className="border-mineshaft-500 bg-mineshaft-600 text-mineshaft-200 hover:border-primary/40 hover:bg-primary/10 flex w-max cursor-pointer items-center rounded-md border px-4 py-2 duration-200 hover:text-white">
|
||||
<span className="flex w-max cursor-pointer items-center rounded-md border border-mineshaft-500 bg-mineshaft-600 px-4 py-2 text-mineshaft-200 duration-200 hover:border-primary/40 hover:bg-primary/10 hover:text-white">
|
||||
Documentation{" "}
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
|
||||
@@ -112,32 +112,30 @@ export const PkiTemplateListPage = () => {
|
||||
/>
|
||||
</div>
|
||||
<div className="container mx-auto mb-6 max-w-7xl rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
{
|
||||
subscription?.pkiLegacyTemplates && (
|
||||
<div className="mb-4 flex justify-between">
|
||||
<p className="text-xl font-medium text-mineshaft-100">Templates</p>
|
||||
<div className="flex w-full justify-end">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionPkiTemplateActions.Create}
|
||||
a={ProjectPermissionSub.CertificateTemplates}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handlePopUpOpen("certificateTemplate")}
|
||||
isDisabled={!isAllowed}
|
||||
className="ml-4"
|
||||
>
|
||||
Add Template
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
{subscription?.pkiLegacyTemplates && (
|
||||
<div className="mb-4 flex justify-between">
|
||||
<p className="text-xl font-medium text-mineshaft-100">Templates</p>
|
||||
<div className="flex w-full justify-end">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionPkiTemplateActions.Create}
|
||||
a={ProjectPermissionSub.CertificateTemplates}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handlePopUpOpen("certificateTemplate")}
|
||||
isDisabled={!isAllowed}
|
||||
className="ml-4"
|
||||
>
|
||||
Add Template
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)}
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
|
||||
@@ -165,7 +165,7 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
|
||||
if (isEdit) {
|
||||
const updateData: any = {
|
||||
profileId: profile.id,
|
||||
name: data.slug,
|
||||
slug: data.slug,
|
||||
description: data.description
|
||||
};
|
||||
|
||||
@@ -240,7 +240,7 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="your-profile-name" isDisabled={Boolean(isEdit)} />
|
||||
<Input {...field} placeholder="your-profile-name" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
@@ -319,7 +319,7 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
|
||||
>
|
||||
{certificateTemplates.map((template) => (
|
||||
<SelectItem key={template.id} value={template.id}>
|
||||
{template.slug}
|
||||
{template.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
@@ -376,17 +376,17 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
|
||||
name="estConfig.disableBootstrapCaValidation"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||
<div className="border-mineshaft-600 bg-mineshaft-900 flex items-center gap-3 rounded-md border p-4">
|
||||
<div className="flex items-center gap-3 rounded-md border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<Checkbox
|
||||
id="disableBootstrapCaValidation"
|
||||
isChecked={value}
|
||||
onCheckedChange={onChange}
|
||||
/>
|
||||
<div className="space-y-1">
|
||||
<span className="text-mineshaft-100 text-sm font-medium">
|
||||
<span className="text-sm font-medium text-mineshaft-100">
|
||||
Disable Bootstrap CA Validation
|
||||
</span>
|
||||
<p className="text-bunker-300 text-xs">
|
||||
<p className="text-xs text-bunker-300">
|
||||
Skip CA certificate validation during EST bootstrap phase
|
||||
</p>
|
||||
</div>
|
||||
@@ -433,7 +433,7 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
|
||||
rows={6}
|
||||
className="w-full font-mono text-xs"
|
||||
/>
|
||||
<p className="text-bunker-400 text-xs">
|
||||
<p className="text-xs text-bunker-400">
|
||||
Paste the complete CA certificate chain in PEM format
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import { faCheck, faCircleInfo, faCopy, faEdit, faEllipsis, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useCallback } from "react";
|
||||
import {
|
||||
faCheck,
|
||||
faCircleInfo,
|
||||
faCopy,
|
||||
faEdit,
|
||||
faEllipsis,
|
||||
faPlus,
|
||||
faTrash
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Badge,
|
||||
DropdownMenu,
|
||||
@@ -18,12 +28,10 @@ import {
|
||||
ProjectPermissionCertificateProfileActions,
|
||||
ProjectPermissionSub
|
||||
} from "@app/context/ProjectPermissionContext/types";
|
||||
import { usePopUp, useToggle } from "@app/hooks";
|
||||
import { useGetCaById } from "@app/hooks/api/ca/queries";
|
||||
import { TCertificateProfile } from "@app/hooks/api/certificateProfiles";
|
||||
import { useGetCertificateTemplateV2ById } from "@app/hooks/api/certificateTemplates/queries";
|
||||
import { usePopUp, useToggle } from "@app/hooks";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { useCallback } from "react";
|
||||
import { CertificateIssuanceModal } from "@app/pages/cert-manager/CertificatesPage/components/CertificateIssuanceModal";
|
||||
|
||||
interface Props {
|
||||
@@ -37,9 +45,7 @@ export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) =
|
||||
|
||||
const { data: caData } = useGetCaById(profile.caId);
|
||||
|
||||
const { popUp, handlePopUpToggle } = usePopUp([
|
||||
"certificateIssuance"
|
||||
] as const);
|
||||
const { popUp, handlePopUpToggle } = usePopUp(["certificateIssuance"] as const);
|
||||
|
||||
const [isIdCopied, setIsIdCopied] = useToggle(false);
|
||||
|
||||
@@ -100,15 +106,15 @@ export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) =
|
||||
)}
|
||||
</div>
|
||||
</Td>
|
||||
<Td className="text-center">{getEnrollmentTypeBadge(profile.enrollmentType)}</Td>
|
||||
<Td className="text-center">
|
||||
<Td className="text-start">{getEnrollmentTypeBadge(profile.enrollmentType)}</Td>
|
||||
<Td className="text-start">
|
||||
<span className="text-sm text-mineshaft-300">
|
||||
{caData?.friendlyName || caData?.commonName || profile.caId}
|
||||
</span>
|
||||
</Td>
|
||||
<Td>
|
||||
<span className="text-sm text-mineshaft-300">
|
||||
{templateData?.slug || profile.certificateTemplateId}
|
||||
{templateData?.name || profile.certificateTemplateId}
|
||||
</span>
|
||||
</Td>
|
||||
<Td>
|
||||
@@ -176,19 +182,17 @@ export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) =
|
||||
Edit Profile
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{
|
||||
canIssueCertificate && (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpToggle("certificateIssuance");
|
||||
}}
|
||||
icon={<FontAwesomeIcon icon={faPlus} />}
|
||||
>
|
||||
Issue Certificate
|
||||
</DropdownMenuItem>
|
||||
)
|
||||
}
|
||||
{canIssueCertificate && (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpToggle("certificateIssuance");
|
||||
}}
|
||||
icon={<FontAwesomeIcon icon={faPlus} />}
|
||||
>
|
||||
Issue Certificate
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{canDeleteProfile && (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
@@ -202,7 +206,11 @@ export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) =
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<CertificateIssuanceModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} profileId={profile.id}/>
|
||||
<CertificateIssuanceModal
|
||||
popUp={popUp}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
profileId={profile.id}
|
||||
/>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
|
||||
@@ -54,7 +54,7 @@ export const CertificateTemplatesV2Tab = () => {
|
||||
setIsDeleteModalOpen(false);
|
||||
setSelectedTemplate(null);
|
||||
createNotification({
|
||||
text: `Certificate template "${selectedTemplate.slug}" deleted successfully`,
|
||||
text: `Certificate template "${selectedTemplate.name}" deleted successfully`,
|
||||
type: "success"
|
||||
});
|
||||
} catch (error: any) {
|
||||
@@ -103,12 +103,12 @@ export const CertificateTemplatesV2Tab = () => {
|
||||
|
||||
<DeleteActionModal
|
||||
isOpen={isDeleteModalOpen}
|
||||
title={`Delete Certificate Template ${selectedTemplate.slug}?`}
|
||||
title={`Delete Certificate Template ${selectedTemplate.name}?`}
|
||||
onChange={(isOpen) => {
|
||||
setIsDeleteModalOpen(isOpen);
|
||||
if (!isOpen) setSelectedTemplate(null);
|
||||
}}
|
||||
deleteKey={selectedTemplate.slug}
|
||||
deleteKey={selectedTemplate.name}
|
||||
onDeleteApproved={handleDeleteConfirm}
|
||||
/>
|
||||
</>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -75,61 +75,64 @@ export const TemplateList = ({ onEditTemplate, onDeleteTemplate }: Props) => {
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
{!isLoading && templates && templates.length > 0 && templates.map((template) => (
|
||||
<Tr
|
||||
key={template.id}
|
||||
className="h-10 transition-colors duration-100 hover:bg-mineshaft-700"
|
||||
>
|
||||
<Td>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="font-medium">{template.slug}</div>
|
||||
{template.description && (
|
||||
<Tooltip content={template.description}>
|
||||
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
<span className="text-sm text-bunker-300">{formatDate(template.createdAt)}</span>
|
||||
</Td>
|
||||
<Td className="text-right">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||
<Tooltip content="More options">
|
||||
<FontAwesomeIcon size="lg" icon={faEllipsis} />
|
||||
{!isLoading &&
|
||||
templates &&
|
||||
templates.length > 0 &&
|
||||
templates.map((template) => (
|
||||
<Tr
|
||||
key={template.id}
|
||||
className="h-10 transition-colors duration-100 hover:bg-mineshaft-700"
|
||||
>
|
||||
<Td>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="font-medium">{template.name}</div>
|
||||
{template.description && (
|
||||
<Tooltip content={template.description}>
|
||||
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="p-1">
|
||||
{canEditTemplate && (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onEditTemplate(template);
|
||||
}}
|
||||
icon={<FontAwesomeIcon icon={faEdit} />}
|
||||
>
|
||||
Edit Template
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{canDeleteTemplate && (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDeleteTemplate(template);
|
||||
}}
|
||||
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||
>
|
||||
Delete Template
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
<span className="text-sm text-bunker-300">{formatDate(template.createdAt)}</span>
|
||||
</Td>
|
||||
<Td className="text-right">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||
<Tooltip content="More options">
|
||||
<FontAwesomeIcon size="lg" icon={faEllipsis} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="p-1">
|
||||
{canEditTemplate && (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onEditTemplate(template);
|
||||
}}
|
||||
icon={<FontAwesomeIcon icon={faEdit} />}
|
||||
>
|
||||
Edit Template
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{canDeleteTemplate && (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDeleteTemplate(template);
|
||||
}}
|
||||
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||
>
|
||||
Delete Template
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
@@ -26,10 +26,16 @@ export enum CertExtendedKeyUsageType {
|
||||
TIME_STAMPING = "time_stamping"
|
||||
}
|
||||
|
||||
export enum CertIncludeType {
|
||||
MANDATORY = "mandatory",
|
||||
OPTIONAL = "optional",
|
||||
PROHIBIT = "prohibit"
|
||||
export enum CertAttributeRule {
|
||||
ALLOW = "allow",
|
||||
DENY = "deny",
|
||||
REQUIRE = "require"
|
||||
}
|
||||
|
||||
export enum CertSanEffect {
|
||||
ALLOW = "allow",
|
||||
DENY = "deny",
|
||||
REQUIRE = "require"
|
||||
}
|
||||
|
||||
export enum CertDurationUnit {
|
||||
@@ -39,7 +45,9 @@ export enum CertDurationUnit {
|
||||
}
|
||||
|
||||
export enum CertSubjectAttributeType {
|
||||
COMMON_NAME = "common_name"
|
||||
COMMON_NAME = "common_name",
|
||||
ORGANIZATION = "organization",
|
||||
COUNTRY = "country"
|
||||
}
|
||||
|
||||
export const formatSANType = (type: CertSubjectAlternativeNameType): string => {
|
||||
@@ -104,61 +112,117 @@ export const formatExtendedKeyUsage = (usage: CertExtendedKeyUsageType): string
|
||||
export const formatSubjectAttributeType = (type: CertSubjectAttributeType): string => {
|
||||
switch (type) {
|
||||
case CertSubjectAttributeType.COMMON_NAME:
|
||||
return "Common Name";
|
||||
return "Common Name (CN)";
|
||||
case CertSubjectAttributeType.ORGANIZATION:
|
||||
return "Organization";
|
||||
case CertSubjectAttributeType.COUNTRY:
|
||||
return "Country";
|
||||
default:
|
||||
return type;
|
||||
}
|
||||
};
|
||||
|
||||
export const formatIncludeType = (include: CertIncludeType): string => {
|
||||
switch (include) {
|
||||
case CertIncludeType.MANDATORY:
|
||||
return "Mandatory";
|
||||
case CertIncludeType.OPTIONAL:
|
||||
return "Optional";
|
||||
case CertIncludeType.PROHIBIT:
|
||||
return "Prohibit";
|
||||
export const formatAttributeRule = (rule: CertAttributeRule): string => {
|
||||
switch (rule) {
|
||||
case CertAttributeRule.ALLOW:
|
||||
return "Allow";
|
||||
case CertAttributeRule.DENY:
|
||||
return "Deny";
|
||||
case CertAttributeRule.REQUIRE:
|
||||
return "Require";
|
||||
default:
|
||||
return include;
|
||||
return rule;
|
||||
}
|
||||
};
|
||||
|
||||
export const mapLegacySANTypeToStandard = (type: string): CertSubjectAlternativeNameType => {
|
||||
switch (type) {
|
||||
case "dns":
|
||||
case "dns_name":
|
||||
return CertSubjectAlternativeNameType.DNS_NAME;
|
||||
case "ip":
|
||||
case "ip_address":
|
||||
return CertSubjectAlternativeNameType.IP_ADDRESS;
|
||||
case "email":
|
||||
return CertSubjectAlternativeNameType.EMAIL;
|
||||
case "uri":
|
||||
case "url":
|
||||
return CertSubjectAlternativeNameType.URI;
|
||||
export const formatSanEffect = (effect: CertSanEffect): string => {
|
||||
switch (effect) {
|
||||
case CertSanEffect.ALLOW:
|
||||
return "Allow";
|
||||
case CertSanEffect.DENY:
|
||||
return "Deny";
|
||||
case CertSanEffect.REQUIRE:
|
||||
return "Require";
|
||||
default:
|
||||
throw new Error(`Unknown SAN type: ${type}`);
|
||||
return effect;
|
||||
}
|
||||
};
|
||||
|
||||
export const mapSANTypeToLegacy = (type: CertSubjectAlternativeNameType): string => {
|
||||
switch (type) {
|
||||
case CertSubjectAlternativeNameType.DNS_NAME:
|
||||
return "dns";
|
||||
case CertSubjectAlternativeNameType.IP_ADDRESS:
|
||||
return "ip";
|
||||
case CertSubjectAlternativeNameType.EMAIL:
|
||||
return "email";
|
||||
case CertSubjectAlternativeNameType.URI:
|
||||
return "uri";
|
||||
default:
|
||||
return type;
|
||||
}
|
||||
};
|
||||
|
||||
export const SAN_TYPE_OPTIONS = Object.values(CertSubjectAlternativeNameType);
|
||||
export const KEY_USAGE_OPTIONS = Object.values(CertKeyUsageType);
|
||||
export const EXTENDED_KEY_USAGE_OPTIONS = Object.values(CertExtendedKeyUsageType);
|
||||
export const INCLUDE_TYPE_OPTIONS = Object.values(CertIncludeType);
|
||||
export const DURATION_UNIT_OPTIONS = Object.values(CertDurationUnit);
|
||||
export const SUBJECT_ATTRIBUTE_TYPE_OPTIONS = Object.values(CertSubjectAttributeType);
|
||||
export const SUBJECT_ATTRIBUTE_TYPE_OPTIONS = Object.values(CertSubjectAttributeType);
|
||||
export const ATTRIBUTE_RULE_OPTIONS = Object.values(CertAttributeRule);
|
||||
export const SAN_EFFECT_OPTIONS = Object.values(CertSanEffect);
|
||||
|
||||
export const SUBJECT_ATTRIBUTE_INCLUDE_OPTIONS = ["optional", "prohibit"] as const;
|
||||
export const SAN_INCLUDE_OPTIONS = ["mandatory", "optional", "prohibit"] as const;
|
||||
|
||||
export const USAGE_STATES = {
|
||||
REQUIRED: "required",
|
||||
OPTIONAL: "optional"
|
||||
} as const;
|
||||
|
||||
export type UsageState = typeof USAGE_STATES[keyof typeof USAGE_STATES] | undefined;
|
||||
|
||||
export const TEMPLATE_SIGNATURE_ALGORITHMS = [
|
||||
"SHA256-RSA",
|
||||
"SHA384-RSA",
|
||||
"SHA512-RSA",
|
||||
"SHA256-ECDSA",
|
||||
"SHA384-ECDSA",
|
||||
"SHA512-ECDSA"
|
||||
] as const;
|
||||
|
||||
export const TEMPLATE_KEY_ALGORITHMS = [
|
||||
"RSA-2048",
|
||||
"RSA-3072",
|
||||
"RSA-4096",
|
||||
"ECDSA-P256",
|
||||
"ECDSA-P384"
|
||||
] as const;
|
||||
|
||||
// API format algorithm constants
|
||||
export const API_SIGNATURE_ALGORITHMS = [
|
||||
"RSA-SHA256",
|
||||
"RSA-SHA384",
|
||||
"RSA-SHA512",
|
||||
"ECDSA-SHA256",
|
||||
"ECDSA-SHA384",
|
||||
"ECDSA-SHA512"
|
||||
] as const;
|
||||
|
||||
export const API_KEY_ALGORITHMS = [
|
||||
"RSA_2048",
|
||||
"RSA_3072",
|
||||
"RSA_4096",
|
||||
"EC_prime256v1",
|
||||
"EC_secp384r1"
|
||||
] as const;
|
||||
|
||||
// Mapping functions between template and API formats
|
||||
export const mapTemplateSignatureAlgorithmToApi = (templateFormat: string): string => {
|
||||
const mapping: Record<string, string> = {
|
||||
"SHA256-RSA": "RSA-SHA256",
|
||||
"SHA384-RSA": "RSA-SHA384",
|
||||
"SHA512-RSA": "RSA-SHA512",
|
||||
"SHA256-ECDSA": "ECDSA-SHA256",
|
||||
"SHA384-ECDSA": "ECDSA-SHA384",
|
||||
"SHA512-ECDSA": "ECDSA-SHA512"
|
||||
};
|
||||
return mapping[templateFormat] || templateFormat;
|
||||
};
|
||||
|
||||
export const mapTemplateKeyAlgorithmToApi = (templateFormat: string): string => {
|
||||
const mapping: Record<string, string> = {
|
||||
"RSA-2048": "RSA_2048",
|
||||
"RSA-3072": "RSA_3072",
|
||||
"RSA-4096": "RSA_4096",
|
||||
"ECDSA-P256": "EC_prime256v1",
|
||||
"ECDSA-P384": "EC_secp384r1"
|
||||
};
|
||||
return mapping[templateFormat] || templateFormat;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,152 +1,159 @@
|
||||
import { Checkbox } from "@app/components/v2";
|
||||
import React from "react";
|
||||
|
||||
import { Select, SelectItem } from "@app/components/v2";
|
||||
|
||||
import {
|
||||
CertExtendedKeyUsageType,
|
||||
CertKeyUsageType,
|
||||
EXTENDED_KEY_USAGE_OPTIONS,
|
||||
formatExtendedKeyUsage,
|
||||
formatKeyUsage,
|
||||
EXTENDED_KEY_USAGE_OPTIONS,
|
||||
KEY_USAGE_OPTIONS
|
||||
} from "./certificate-constants";
|
||||
|
||||
type UsageState = "mandatory" | "optional" | undefined;
|
||||
type KeyUsagePolicy = "allow" | "require" | "deny" | "none";
|
||||
|
||||
type ThreeStateCheckboxProps = {
|
||||
value: UsageState;
|
||||
onChange: (newValue: UsageState) => void;
|
||||
label: string;
|
||||
id: string;
|
||||
};
|
||||
|
||||
const ThreeStateCheckbox = ({ value, onChange, label, id }: ThreeStateCheckboxProps) => {
|
||||
const handleClick = () => {
|
||||
if (value === undefined) {
|
||||
onChange("optional");
|
||||
} else if (value === "optional") {
|
||||
onChange("mandatory");
|
||||
} else {
|
||||
onChange(undefined);
|
||||
}
|
||||
interface KeyUsagesSectionProps {
|
||||
watchedKeyUsages: {
|
||||
requiredUsages: CertKeyUsageType[];
|
||||
optionalUsages: CertKeyUsageType[];
|
||||
};
|
||||
|
||||
const getCheckboxState = () => {
|
||||
if (value) return true;
|
||||
return false;
|
||||
watchedExtendedKeyUsages: {
|
||||
requiredUsages: CertExtendedKeyUsageType[];
|
||||
optionalUsages: CertExtendedKeyUsageType[];
|
||||
};
|
||||
onKeyUsagesChange: (usages: {
|
||||
requiredUsages: CertKeyUsageType[];
|
||||
optionalUsages: CertKeyUsageType[];
|
||||
}) => void;
|
||||
onExtendedKeyUsagesChange: (usages: {
|
||||
requiredUsages: CertExtendedKeyUsageType[];
|
||||
optionalUsages: CertExtendedKeyUsageType[];
|
||||
}) => void;
|
||||
}
|
||||
|
||||
const getIndeterminateState = () => {
|
||||
return value === "optional";
|
||||
};
|
||||
|
||||
const getStateLabel = () => {
|
||||
if (value === "mandatory") return " (Mandatory)";
|
||||
if (value === "optional") return " (Optional)";
|
||||
return "";
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center space-x-3">
|
||||
<Checkbox
|
||||
id={id}
|
||||
isChecked={getCheckboxState()}
|
||||
isIndeterminate={getIndeterminateState()}
|
||||
onCheckedChange={handleClick}
|
||||
/>
|
||||
<label
|
||||
htmlFor={id}
|
||||
className="text-mineshaft-200 cursor-pointer text-sm font-medium"
|
||||
>
|
||||
{label}
|
||||
{value && (
|
||||
<span className="text-mineshaft-400 text-xs ml-1">
|
||||
{getStateLabel()}
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type KeyUsagesSectionProps = {
|
||||
watchedKeyUsages?: { requiredUsages?: string[]; optionalUsages?: string[] };
|
||||
watchedExtendedKeyUsages?: { requiredUsages?: string[]; optionalUsages?: string[] };
|
||||
onKeyUsagesChange: (usages: { requiredUsages: string[]; optionalUsages: string[] }) => void;
|
||||
onExtendedKeyUsagesChange: (usages: { requiredUsages: string[]; optionalUsages: string[] }) => void;
|
||||
};
|
||||
|
||||
export const KeyUsagesSection = ({
|
||||
watchedKeyUsages = { requiredUsages: [], optionalUsages: [] },
|
||||
watchedExtendedKeyUsages = { requiredUsages: [], optionalUsages: [] },
|
||||
export const KeyUsagesSection: React.FC<KeyUsagesSectionProps> = ({
|
||||
watchedKeyUsages,
|
||||
watchedExtendedKeyUsages,
|
||||
onKeyUsagesChange,
|
||||
onExtendedKeyUsagesChange
|
||||
}: KeyUsagesSectionProps) => {
|
||||
const getUsageState = (usage: string, data: { requiredUsages?: string[]; optionalUsages?: string[] }): UsageState => {
|
||||
if (data.requiredUsages?.includes(usage)) return "mandatory";
|
||||
if (data.optionalUsages?.includes(usage)) return "optional";
|
||||
return undefined;
|
||||
}) => {
|
||||
const getKeyUsagePolicy = (usage: CertKeyUsageType): KeyUsagePolicy => {
|
||||
if (watchedKeyUsages.requiredUsages.includes(usage)) return "require";
|
||||
if (watchedKeyUsages.optionalUsages.includes(usage)) return "allow";
|
||||
return "deny";
|
||||
};
|
||||
|
||||
const handleKeyUsageChange = (usage: CertKeyUsageType, newState: UsageState) => {
|
||||
const currentRequired = watchedKeyUsages.requiredUsages || [];
|
||||
const currentOptional = watchedKeyUsages.optionalUsages || [];
|
||||
const getExtendedKeyUsagePolicy = (usage: CertExtendedKeyUsageType): KeyUsagePolicy => {
|
||||
if (watchedExtendedKeyUsages.requiredUsages.includes(usage)) return "require";
|
||||
if (watchedExtendedKeyUsages.optionalUsages.includes(usage)) return "allow";
|
||||
return "deny";
|
||||
};
|
||||
|
||||
let newRequired = currentRequired.filter(u => u !== usage);
|
||||
let newOptional = currentOptional.filter(u => u !== usage);
|
||||
const handleKeyUsagePolicyChange = (usage: CertKeyUsageType, policy: KeyUsagePolicy) => {
|
||||
const newRequired = watchedKeyUsages.requiredUsages.filter((u) => u !== usage);
|
||||
const newOptional = watchedKeyUsages.optionalUsages.filter((u) => u !== usage);
|
||||
|
||||
if (newState === "mandatory") {
|
||||
newRequired = [...newRequired, usage];
|
||||
} else if (newState === "optional") {
|
||||
newOptional = [...newOptional, usage];
|
||||
if (policy === "require") {
|
||||
newRequired.push(usage);
|
||||
} else if (policy === "allow") {
|
||||
newOptional.push(usage);
|
||||
}
|
||||
|
||||
onKeyUsagesChange({ requiredUsages: newRequired, optionalUsages: newOptional });
|
||||
onKeyUsagesChange({
|
||||
requiredUsages: newRequired,
|
||||
optionalUsages: newOptional
|
||||
});
|
||||
};
|
||||
|
||||
const handleExtendedKeyUsageChange = (usage: CertExtendedKeyUsageType, newState: UsageState) => {
|
||||
const currentRequired = watchedExtendedKeyUsages.requiredUsages || [];
|
||||
const currentOptional = watchedExtendedKeyUsages.optionalUsages || [];
|
||||
const handleExtendedKeyUsagePolicyChange = (
|
||||
usage: CertExtendedKeyUsageType,
|
||||
policy: KeyUsagePolicy
|
||||
) => {
|
||||
const newRequired = watchedExtendedKeyUsages.requiredUsages.filter((u) => u !== usage);
|
||||
const newOptional = watchedExtendedKeyUsages.optionalUsages.filter((u) => u !== usage);
|
||||
|
||||
let newRequired = currentRequired.filter(u => u !== usage);
|
||||
let newOptional = currentOptional.filter(u => u !== usage);
|
||||
|
||||
if (newState === "mandatory") {
|
||||
newRequired = [...newRequired, usage];
|
||||
} else if (newState === "optional") {
|
||||
newOptional = [...newOptional, usage];
|
||||
if (policy === "require") {
|
||||
newRequired.push(usage);
|
||||
} else if (policy === "allow") {
|
||||
newOptional.push(usage);
|
||||
}
|
||||
|
||||
onExtendedKeyUsagesChange({ requiredUsages: newRequired, optionalUsages: newOptional });
|
||||
onExtendedKeyUsagesChange({
|
||||
requiredUsages: newRequired,
|
||||
optionalUsages: newOptional
|
||||
});
|
||||
};
|
||||
|
||||
const keyUsagePolicyOptions = [
|
||||
{ value: "deny", label: "Deny" },
|
||||
{ value: "allow", label: "Allow" },
|
||||
{ value: "require", label: "Require" }
|
||||
];
|
||||
|
||||
const extendedKeyUsagePolicyOptions = [
|
||||
{ value: "deny", label: "Deny" },
|
||||
{ value: "allow", label: "Allow" },
|
||||
{ value: "require", label: "Require" }
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-mineshaft-200 text-sm font-medium">Key Usages</h3>
|
||||
<div className="grid grid-cols-2 gap-2 pl-2">
|
||||
{KEY_USAGE_OPTIONS.map((usage) => (
|
||||
<ThreeStateCheckbox
|
||||
key={usage}
|
||||
id={`key-usage-${usage}`}
|
||||
label={formatKeyUsage(usage)}
|
||||
value={getUsageState(usage, watchedKeyUsages)}
|
||||
onChange={(newState) => handleKeyUsageChange(usage, newState)}
|
||||
/>
|
||||
))}
|
||||
<div className="space-y-8">
|
||||
{/* Key Usages */}
|
||||
<div className="space-y-4">
|
||||
<h4 className="text-sm font-medium text-bunker-200">Key Usages</h4>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{KEY_USAGE_OPTIONS.map((usage) => {
|
||||
const policy = getKeyUsagePolicy(usage);
|
||||
return (
|
||||
<div key={usage} className="flex items-center justify-between">
|
||||
<span className="text-sm text-bunker-200">{formatKeyUsage(usage)}</span>
|
||||
<Select
|
||||
value={policy}
|
||||
onValueChange={(value) =>
|
||||
handleKeyUsagePolicyChange(usage, value as KeyUsagePolicy)
|
||||
}
|
||||
className="w-32"
|
||||
>
|
||||
{keyUsagePolicyOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-mineshaft-200 text-sm font-medium">Extended Key Usages</h3>
|
||||
<div className="grid grid-cols-2 gap-2 pl-2">
|
||||
{EXTENDED_KEY_USAGE_OPTIONS.map((usage) => (
|
||||
<ThreeStateCheckbox
|
||||
key={usage}
|
||||
id={`ext-key-usage-${usage}`}
|
||||
label={formatExtendedKeyUsage(usage)}
|
||||
value={getUsageState(usage, watchedExtendedKeyUsages)}
|
||||
onChange={(newState) => handleExtendedKeyUsageChange(usage, newState)}
|
||||
/>
|
||||
))}
|
||||
{/* Extended Key Usages */}
|
||||
<div className="space-y-4">
|
||||
<h4 className="text-sm font-medium text-bunker-200">Extended Key Usages</h4>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{EXTENDED_KEY_USAGE_OPTIONS.map((usage) => {
|
||||
const policy = getExtendedKeyUsagePolicy(usage);
|
||||
return (
|
||||
<div key={usage} className="flex items-center justify-between">
|
||||
<span className="text-sm text-bunker-200">{formatExtendedKeyUsage(usage)}</span>
|
||||
<Select
|
||||
value={policy}
|
||||
onValueChange={(value) =>
|
||||
handleExtendedKeyUsagePolicyChange(usage, value as KeyUsagePolicy)
|
||||
}
|
||||
className="w-32"
|
||||
>
|
||||
{extendedKeyUsagePolicyOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,93 +3,121 @@ import { z } from "zod";
|
||||
import {
|
||||
CertDurationUnit,
|
||||
CertExtendedKeyUsageType,
|
||||
CertIncludeType,
|
||||
CertKeyUsageType,
|
||||
CertSubjectAlternativeNameType,
|
||||
CertSubjectAttributeType
|
||||
CertSubjectAttributeType,
|
||||
SUBJECT_ATTRIBUTE_INCLUDE_OPTIONS,
|
||||
SAN_INCLUDE_OPTIONS
|
||||
} from "./certificate-constants";
|
||||
|
||||
export const attributeSchema = z.object({
|
||||
export const uiAttributeSchema = z.object({
|
||||
type: z.nativeEnum(CertSubjectAttributeType),
|
||||
include: z.nativeEnum(CertIncludeType),
|
||||
value: z.array(z.string().min(1, "Value cannot be empty")).optional()
|
||||
include: z.enum(SUBJECT_ATTRIBUTE_INCLUDE_OPTIONS),
|
||||
value: z.array(z.string().min(1, "Value cannot be empty"))
|
||||
});
|
||||
|
||||
export const sanSchema = z.object({
|
||||
export const uiSanSchema = z.object({
|
||||
type: z.nativeEnum(CertSubjectAlternativeNameType),
|
||||
include: z.nativeEnum(CertIncludeType),
|
||||
value: z.array(z.string().min(1, "Value cannot be empty")).optional()
|
||||
include: z.enum(SAN_INCLUDE_OPTIONS),
|
||||
value: z.array(z.string().min(1, "Value cannot be empty"))
|
||||
});
|
||||
|
||||
export const uiKeyUsagesSchema = z.object({
|
||||
requiredUsages: z.array(z.nativeEnum(CertKeyUsageType)),
|
||||
optionalUsages: z.array(z.nativeEnum(CertKeyUsageType))
|
||||
});
|
||||
|
||||
export const uiExtendedKeyUsagesSchema = z.object({
|
||||
requiredUsages: z.array(z.nativeEnum(CertExtendedKeyUsageType)),
|
||||
optionalUsages: z.array(z.nativeEnum(CertExtendedKeyUsageType))
|
||||
});
|
||||
|
||||
export const uiValiditySchema = z.object({
|
||||
maxDuration: z.object({
|
||||
value: z.number().min(1, "Duration must be at least 1"),
|
||||
unit: z.nativeEnum(CertDurationUnit)
|
||||
})
|
||||
});
|
||||
|
||||
export const uiSignatureAlgorithmSchema = z.object({
|
||||
allowedAlgorithms: z.array(z.string()).optional(),
|
||||
defaultAlgorithm: z.string().optional()
|
||||
});
|
||||
|
||||
export const uiKeyAlgorithmSchema = z.object({
|
||||
allowedKeyTypes: z.array(z.string()).optional(),
|
||||
defaultKeyType: z.string().optional()
|
||||
});
|
||||
|
||||
export const templateSchema = z.object({
|
||||
slug: z.string().trim().min(1, "Template name is required"),
|
||||
description: z.string().optional(),
|
||||
attributes: z.array(attributeSchema).optional().refine((attributes) => {
|
||||
if (!attributes) return true;
|
||||
attributes: z.array(uiAttributeSchema).optional(),
|
||||
subjectAlternativeNames: z.array(uiSanSchema).optional(),
|
||||
keyUsages: uiKeyUsagesSchema.optional(),
|
||||
extendedKeyUsages: uiExtendedKeyUsagesSchema.optional(),
|
||||
validity: uiValiditySchema.optional(),
|
||||
signatureAlgorithm: uiSignatureAlgorithmSchema.optional(),
|
||||
keyAlgorithm: uiKeyAlgorithmSchema.optional()
|
||||
});
|
||||
|
||||
const attributesByType = attributes.reduce((acc, attr) => {
|
||||
if (!acc[attr.type]) acc[attr.type] = [];
|
||||
acc[attr.type].push(attr);
|
||||
return acc;
|
||||
}, {} as Record<string, typeof attributes>);
|
||||
export type TemplateFormData = z.infer<typeof templateSchema>;
|
||||
|
||||
for (const [, attrs] of Object.entries(attributesByType)) {
|
||||
const mandatoryAttrs = attrs.filter(attr => attr.include === 'mandatory');
|
||||
export const apiSubjectSchema = z
|
||||
.object({
|
||||
type: z.nativeEnum(CertSubjectAttributeType),
|
||||
allowed: z.array(z.string().min(1, "Value cannot be empty")).optional(),
|
||||
required: z.array(z.string().min(1, "Value cannot be empty")).optional(),
|
||||
denied: z.array(z.string().min(1, "Value cannot be empty")).optional()
|
||||
})
|
||||
.refine((data) => data.allowed || data.required || data.denied, {
|
||||
message: "At least one allowed, required, or denied value must be provided"
|
||||
});
|
||||
|
||||
if (mandatoryAttrs.length > 1) {
|
||||
return false;
|
||||
}
|
||||
export const apiSanSchema = z
|
||||
.object({
|
||||
type: z.nativeEnum(CertSubjectAlternativeNameType),
|
||||
allowed: z.array(z.string().min(1, "Value cannot be empty")).optional(),
|
||||
required: z.array(z.string().min(1, "Value cannot be empty")).optional(),
|
||||
denied: z.array(z.string().min(1, "Value cannot be empty")).optional()
|
||||
})
|
||||
.refine((data) => data.allowed || data.required || data.denied, {
|
||||
message: "At least one allowed, required, or denied value must be provided"
|
||||
});
|
||||
|
||||
if (mandatoryAttrs.length === 1 && attrs.length > 1) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}, {
|
||||
message: "Attribute validation failed: when a mandatory value exists, no other values are allowed for that attribute type"
|
||||
}),
|
||||
export const apiTemplateSchema = z.object({
|
||||
name: z.string().trim().min(1, "Template name is required"),
|
||||
description: z.string().optional(),
|
||||
subject: z.array(apiSubjectSchema).optional(),
|
||||
sans: z.array(apiSanSchema).optional(),
|
||||
keyUsages: z
|
||||
.object({
|
||||
requiredUsages: z.array(z.nativeEnum(CertKeyUsageType)).optional(),
|
||||
optionalUsages: z.array(z.nativeEnum(CertKeyUsageType)).optional()
|
||||
allowed: z.array(z.nativeEnum(CertKeyUsageType)).optional(),
|
||||
required: z.array(z.nativeEnum(CertKeyUsageType)).optional(),
|
||||
denied: z.array(z.nativeEnum(CertKeyUsageType)).optional()
|
||||
})
|
||||
.optional(),
|
||||
extendedKeyUsages: z
|
||||
.object({
|
||||
requiredUsages: z.array(z.nativeEnum(CertExtendedKeyUsageType)).optional(),
|
||||
optionalUsages: z.array(z.nativeEnum(CertExtendedKeyUsageType)).optional()
|
||||
allowed: z.array(z.nativeEnum(CertExtendedKeyUsageType)).optional(),
|
||||
required: z.array(z.nativeEnum(CertExtendedKeyUsageType)).optional(),
|
||||
denied: z.array(z.nativeEnum(CertExtendedKeyUsageType)).optional()
|
||||
})
|
||||
.optional(),
|
||||
algorithms: z
|
||||
.object({
|
||||
signature: z.array(z.string()).optional(),
|
||||
keyAlgorithm: z.array(z.string()).optional()
|
||||
})
|
||||
.optional(),
|
||||
subjectAlternativeNames: z.array(sanSchema).optional(),
|
||||
validity: z
|
||||
.object({
|
||||
maxDuration: z
|
||||
.object({
|
||||
value: z.number().min(1, "Duration must be at least 1"),
|
||||
unit: z.nativeEnum(CertDurationUnit)
|
||||
})
|
||||
.optional(),
|
||||
minDuration: z
|
||||
.object({
|
||||
value: z.number().min(1, "Duration must be at least 1"),
|
||||
unit: z.nativeEnum(CertDurationUnit)
|
||||
})
|
||||
max: z
|
||||
.string()
|
||||
.regex(/^\d+[dhmy]$/, "Must be in format like '365d', '12m', '1y', or '24h'")
|
||||
.optional()
|
||||
})
|
||||
.optional(),
|
||||
signatureAlgorithm: z
|
||||
.object({
|
||||
allowedAlgorithms: z.array(z.string()).optional(),
|
||||
defaultAlgorithm: z.string().optional()
|
||||
})
|
||||
.optional(),
|
||||
keyAlgorithm: z
|
||||
.object({
|
||||
allowedKeyTypes: z.array(z.string()).optional(),
|
||||
defaultKeyType: z.string().optional()
|
||||
})
|
||||
.optional()
|
||||
});
|
||||
|
||||
export type TemplateFormData = z.infer<typeof templateSchema>;
|
||||
export type ApiTemplateFormData = z.infer<typeof apiTemplateSchema>;
|
||||
|
||||
@@ -2,7 +2,9 @@ import {
|
||||
CertExtendedKeyUsageType,
|
||||
CertKeyUsageType,
|
||||
formatExtendedKeyUsage,
|
||||
formatKeyUsage
|
||||
formatKeyUsage,
|
||||
USAGE_STATES,
|
||||
UsageState
|
||||
} from "./certificate-constants";
|
||||
|
||||
export const formatUsageName = (usage: string): string => {
|
||||
@@ -14,6 +16,7 @@ export const formatUsageName = (usage: string): string => {
|
||||
return formatExtendedKeyUsage(usage as CertExtendedKeyUsageType);
|
||||
}
|
||||
} catch {
|
||||
// Handle any errors in type checking
|
||||
}
|
||||
return usage.replace(/_/g, " ");
|
||||
};
|
||||
@@ -22,15 +25,15 @@ export const getUsageState = (
|
||||
usage: CertKeyUsageType | CertExtendedKeyUsageType,
|
||||
requiredUsages: (CertKeyUsageType | CertExtendedKeyUsageType)[],
|
||||
optionalUsages: (CertKeyUsageType | CertExtendedKeyUsageType)[]
|
||||
): "required" | "optional" | undefined => {
|
||||
if (requiredUsages.includes(usage)) return "required";
|
||||
if (optionalUsages.includes(usage)) return "optional";
|
||||
): UsageState => {
|
||||
if (requiredUsages.includes(usage)) return USAGE_STATES.REQUIRED;
|
||||
if (optionalUsages.includes(usage)) return USAGE_STATES.OPTIONAL;
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const toggleUsageState = (
|
||||
usage: CertKeyUsageType | CertExtendedKeyUsageType,
|
||||
newState: "required" | "optional" | undefined,
|
||||
newState: UsageState,
|
||||
currentRequiredUsages: (CertKeyUsageType | CertExtendedKeyUsageType)[],
|
||||
currentOptionalUsages: (CertKeyUsageType | CertExtendedKeyUsageType)[],
|
||||
toggleRequired: (usage: CertKeyUsageType | CertExtendedKeyUsageType) => void,
|
||||
@@ -39,10 +42,10 @@ export const toggleUsageState = (
|
||||
const isRequired = currentRequiredUsages.includes(usage);
|
||||
const isOptional = currentOptionalUsages.includes(usage);
|
||||
|
||||
if (newState === "required") {
|
||||
if (newState === USAGE_STATES.REQUIRED) {
|
||||
if (isOptional) toggleOptional(usage);
|
||||
if (!isRequired) toggleRequired(usage);
|
||||
} else if (newState === "optional") {
|
||||
} else if (newState === USAGE_STATES.OPTIONAL) {
|
||||
if (isRequired) toggleRequired(usage);
|
||||
if (!isOptional) toggleOptional(usage);
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user