mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-21 16:28:12 +00:00
PKI revamp, last changes and improvements on API and UI
This commit is contained in:
+7
-7
@@ -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>
|
||||
|
||||
+32
-24
@@ -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>
|
||||
);
|
||||
|
||||
+3
-3
@@ -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}
|
||||
/>
|
||||
</>
|
||||
|
||||
+483
-500
File diff suppressed because it is too large
Load Diff
+56
-53
@@ -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>
|
||||
|
||||
+109
-45
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
+124
-117
@@ -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>
|
||||
|
||||
+86
-58
@@ -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>;
|
||||
|
||||
+10
-7
@@ -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