PKI revamp EST improvements and fixes

This commit is contained in:
Carlos Monastyrski
2025-10-18 03:16:36 -03:00
parent ebac93e500
commit 0df3181308
29 changed files with 1994 additions and 839 deletions
@@ -47,7 +47,7 @@ const createSchema = z
estConfig: z
.object({
disableBootstrapCaValidation: z.boolean().optional(),
passphrase: z.string().min(1, "EST passphrase is required"),
passphraseInput: z.string().min(1, "EST passphrase is required"),
caChain: z.string().min(1, "EST CA chain is required").optional()
})
.refine(
@@ -58,7 +58,8 @@ const createSchema = z
return true;
},
{
message: "EST CA chain is required"
message: "EST CA chain is required when bootstrap CA validation is enabled",
path: ["caChain"]
}
)
.optional(),
@@ -106,7 +107,7 @@ const editSchema = z
estConfig: z
.object({
disableBootstrapCaValidation: z.boolean().optional(),
passphrase: z.string().optional(),
passphraseInput: z.string().optional(),
caChain: z.string().optional()
})
.optional(),
@@ -168,15 +169,22 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
enrollmentType: profile.enrollmentType,
certificateAuthorityId: profile.caId,
certificateTemplateId: profile.certificateTemplateId,
estConfig: {
disableBootstrapCaValidation: profile.estConfig?.disableBootstrapCaValidation || false,
passphrase: "",
caChain: undefined
},
apiConfig: {
autoRenew: profile.apiConfig?.autoRenew || false,
autoRenewDays: profile.apiConfig?.autoRenewDays || 30
}
estConfig:
profile.enrollmentType === "est"
? {
disableBootstrapCaValidation:
profile.estConfig?.disableBootstrapCaValidation || false,
passphraseInput: "",
caChain: profile.estConfig?.encryptedCaChain || ""
}
: undefined,
apiConfig:
profile.enrollmentType === "api"
? {
autoRenew: profile.apiConfig?.autoRenew || false,
autoRenewDays: profile.apiConfig?.autoRenewDays || 30
}
: undefined
}
: {
slug: "",
@@ -229,8 +237,8 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
if (data.enrollmentType === "est" && data.estConfig) {
createData.estConfig = {
passphrase: data.estConfig.passphrase,
caChain: data.estConfig.caChain || "",
passphraseInput: data.estConfig.passphraseInput,
caChain: data.estConfig.caChain || undefined,
disableBootstrapCaValidation: data.estConfig.disableBootstrapCaValidation
};
} else if (data.enrollmentType === "api" && data.apiConfig) {
@@ -346,8 +354,7 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
if (watchedEnrollmentType === "est") {
setValue("estConfig", {
disableBootstrapCaValidation: false,
passphrase: "",
caChain: ""
passphraseInput: ""
});
setValue("apiConfig", undefined);
} else {
@@ -391,8 +398,7 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
setValue("apiConfig", undefined);
setValue("estConfig", {
disableBootstrapCaValidation: false,
passphrase: "",
caChain: ""
passphraseInput: ""
});
} else {
setValue("estConfig", undefined);
@@ -444,7 +450,7 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
<Controller
control={control}
name="estConfig.passphrase"
name="estConfig.passphraseInput"
render={({ field, fieldState: { error } }) => (
<FormControl
label="EST Passphrase"
@@ -201,7 +201,7 @@ export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) =
Edit Profile
</DropdownMenuItem>
)}
{canIssueCertificate && (
{canIssueCertificate && profile.enrollmentType === "api" && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
@@ -9,8 +9,8 @@ import {
ProjectPermissionPkiTemplateActions,
ProjectPermissionSub
} from "@app/context/ProjectPermissionContext/types";
import { useDeleteCertificateTemplateV2New } from "@app/hooks/api/certificateTemplates/mutations";
import { TCertificateTemplateV2New } from "@app/hooks/api/certificateTemplates/types";
import { useDeleteCertificateTemplateV2WithPolicies } from "@app/hooks/api/certificateTemplates/mutations";
import { TCertificateTemplateV2WithPolicies } from "@app/hooks/api/certificateTemplates/types";
import { CreateTemplateModal } from "./CreateTemplateModal";
import { TemplateList } from "./TemplateList";
@@ -21,9 +21,10 @@ export const CertificateTemplatesV2Tab = () => {
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [selectedTemplate, setSelectedTemplate] = useState<TCertificateTemplateV2New | null>(null);
const [selectedTemplate, setSelectedTemplate] =
useState<TCertificateTemplateV2WithPolicies | null>(null);
const deleteTemplateV2 = useDeleteCertificateTemplateV2New();
const deleteTemplateV2 = useDeleteCertificateTemplateV2WithPolicies();
const canCreateTemplate = permission.can(
ProjectPermissionPkiTemplateActions.Create,
@@ -34,12 +35,12 @@ export const CertificateTemplatesV2Tab = () => {
setIsCreateModalOpen(true);
};
const handleEditTemplate = (template: TCertificateTemplateV2New) => {
const handleEditTemplate = (template: TCertificateTemplateV2WithPolicies) => {
setSelectedTemplate(template);
setIsEditModalOpen(true);
};
const handleDeleteTemplate = (template: TCertificateTemplateV2New) => {
const handleDeleteTemplate = (template: TCertificateTemplateV2WithPolicies) => {
setSelectedTemplate(template);
setIsDeleteModalOpen(true);
};
@@ -24,12 +24,12 @@ import {
} from "@app/components/v2";
import { useProject } from "@app/context";
import {
useCreateCertificateTemplateV2New,
useUpdateCertificateTemplateV2New
useCreateCertificateTemplateV2WithPolicies,
useUpdateCertificateTemplateV2WithPolicies
} from "@app/hooks/api/certificateTemplates/mutations";
import {
TCertificateTemplateV2New,
TCertificateTemplateV2Policy
TCertificateTemplateV2Policy,
TCertificateTemplateV2WithPolicies
} from "@app/hooks/api/certificateTemplates/types";
import {
@@ -57,7 +57,7 @@ type ValidityTransform = TCertificateTemplateV2Policy["validity"];
interface Props {
isOpen: boolean;
onClose: () => void;
template?: TCertificateTemplateV2New;
template?: TCertificateTemplateV2WithPolicies;
mode?: "create" | "edit";
}
@@ -106,12 +106,12 @@ const KEY_ALGORITHMS = [
export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create" }: Props) => {
const { currentProject } = useProject();
const createTemplate = useCreateCertificateTemplateV2New();
const updateTemplate = useUpdateCertificateTemplateV2New();
const createTemplate = useCreateCertificateTemplateV2WithPolicies();
const updateTemplate = useUpdateCertificateTemplateV2WithPolicies();
const isEdit = mode === "edit" && template;
const convertApiToUiFormat = (templateData: TCertificateTemplateV2New): FormData => {
const convertApiToUiFormat = (templateData: TCertificateTemplateV2WithPolicies): FormData => {
const attributes: FormData["attributes"] = [];
if (templateData.subject && Array.isArray(templateData.subject)) {
templateData.subject.forEach((subj) => {
@@ -244,12 +244,10 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
maxDuration: { value: 365, unit: CertDurationUnit.DAYS }
},
signatureAlgorithm: {
allowedAlgorithms: [],
defaultAlgorithm: ""
allowedAlgorithms: []
},
keyAlgorithm: {
allowedKeyTypes: [],
defaultKeyType: ""
allowedKeyTypes: []
}
});
@@ -275,8 +273,38 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
optionalUsages: []
};
const transformToNewApiFormat = (data: FormData) => {
const subject =
const consolidateByType = <
T extends { type: string; allowed?: string[]; required?: string[]; denied?: string[] }
>(
items: T[]
): T[] => {
const consolidated = new Map<string, T>();
items.forEach((item) => {
const existing = consolidated.get(item.type);
if (existing) {
const mergedItem = {
...item,
allowed: [...new Set([...(existing.allowed || []), ...(item.allowed || [])])],
required: [...new Set([...(existing.required || []), ...(item.required || [])])],
denied: [...new Set([...(existing.denied || []), ...(item.denied || [])])]
} as T;
if (mergedItem.allowed?.length === 0) delete mergedItem.allowed;
if (mergedItem.required?.length === 0) delete mergedItem.required;
if (mergedItem.denied?.length === 0) delete mergedItem.denied;
consolidated.set(item.type, mergedItem);
} else {
consolidated.set(item.type, item);
}
});
return Array.from(consolidated.values());
};
const transformToApiFormat = (data: FormData) => {
const subjectRaw =
data.attributes?.map((attr) => {
const result: AttributeTransform = { type: attr.type };
@@ -289,7 +317,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
return result;
}) || [];
const sans =
const sansRaw =
data.subjectAlternativeNames?.map((san) => {
const result: SanTransform = { type: san.type };
@@ -304,7 +332,13 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
return result;
}) || [];
const keyUsages: KeyUsagesTransform = {};
const subject = consolidateByType(subjectRaw);
const sans = consolidateByType(sansRaw);
const keyUsages: KeyUsagesTransform = {
required: [],
allowed: []
};
if (data.keyUsages?.requiredUsages && data.keyUsages.requiredUsages.length > 0) {
keyUsages.required = data.keyUsages.requiredUsages;
}
@@ -312,7 +346,10 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
keyUsages.allowed = data.keyUsages.optionalUsages;
}
const extendedKeyUsages: ExtendedKeyUsagesTransform = {};
const extendedKeyUsages: ExtendedKeyUsagesTransform = {
required: [],
allowed: []
};
if (
data.extendedKeyUsages?.requiredUsages &&
data.extendedKeyUsages.requiredUsages.length > 0
@@ -364,10 +401,10 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
description: data.description,
subject,
sans,
keyUsages: Object.keys(keyUsages).length > 0 ? keyUsages : undefined,
extendedKeyUsages: Object.keys(extendedKeyUsages).length > 0 ? extendedKeyUsages : undefined,
algorithms: Object.keys(algorithms).length > 0 ? algorithms : undefined,
validity: Object.keys(validity).length > 0 ? validity : undefined
keyUsages,
extendedKeyUsages,
algorithms,
validity
};
};
@@ -391,7 +428,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
return;
}
const transformedData = transformToNewApiFormat(data);
const transformedData = transformToApiFormat(data);
if (isEdit) {
const updateData = {
@@ -23,11 +23,11 @@ import {
ProjectPermissionSub
} from "@app/context/ProjectPermissionContext/types";
import { useListCertificateTemplatesV2 } from "@app/hooks/api/certificateTemplates/queries";
import { TCertificateTemplateV2New } from "@app/hooks/api/certificateTemplates/types";
import { TCertificateTemplateV2WithPolicies } from "@app/hooks/api/certificateTemplates/types";
interface Props {
onEditTemplate: (template: TCertificateTemplateV2New) => void;
onDeleteTemplate: (template: TCertificateTemplateV2New) => void;
onEditTemplate: (template: TCertificateTemplateV2WithPolicies) => void;
onDeleteTemplate: (template: TCertificateTemplateV2WithPolicies) => void;
}
export const TemplateList = ({ onEditTemplate, onDeleteTemplate }: Props) => {
@@ -40,13 +40,15 @@ export const uiValiditySchema = z.object({
});
export const uiSignatureAlgorithmSchema = z.object({
allowedAlgorithms: z.array(z.string()).optional(),
defaultAlgorithm: z.string().optional()
allowedAlgorithms: z
.array(z.string().min(1, "Algorithm cannot be empty"))
.min(1, "At least one algorithm must be selected")
});
export const uiKeyAlgorithmSchema = z.object({
allowedKeyTypes: z.array(z.string()).optional(),
defaultKeyType: z.string().optional()
allowedKeyTypes: z
.array(z.string().min(1, "Key type cannot be empty"))
.min(1, "At least one key type must be selected")
});
export const templateSchema = z.object({
@@ -69,8 +71,8 @@ export const templateSchema = z.object({
keyUsages: uiKeyUsagesSchema.optional(),
extendedKeyUsages: uiExtendedKeyUsagesSchema.optional(),
validity: uiValiditySchema.optional(),
signatureAlgorithm: uiSignatureAlgorithmSchema.optional(),
keyAlgorithm: uiKeyAlgorithmSchema.optional()
signatureAlgorithm: uiSignatureAlgorithmSchema,
keyAlgorithm: uiKeyAlgorithmSchema
});
export type TemplateFormData = z.infer<typeof templateSchema>;