mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-12 07:29:35 +00:00
Addressed PR comments
This commit is contained in:
@@ -4,6 +4,7 @@ export {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionAuditLogsActions,
|
||||
ProjectPermissionCertificateActions,
|
||||
ProjectPermissionCertificateProfileActions,
|
||||
ProjectPermissionCmekActions,
|
||||
ProjectPermissionDynamicSecretActions,
|
||||
ProjectPermissionGroupActions,
|
||||
|
||||
@@ -124,6 +124,14 @@ export enum ProjectPermissionPkiTemplateActions {
|
||||
ListCerts = "list-certs"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionCertificateProfileActions {
|
||||
Read = "read",
|
||||
Create = "create",
|
||||
Edit = "edit",
|
||||
Delete = "delete",
|
||||
IssueCert = "issue-cert"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionSecretRotationActions {
|
||||
Read = "read",
|
||||
ReadGeneratedCredentials = "read-generated-credentials",
|
||||
@@ -217,6 +225,7 @@ export type ConditionalProjectPermissionSubject =
|
||||
| ProjectPermissionSub.SshHosts
|
||||
| ProjectPermissionSub.PkiSubscribers
|
||||
| ProjectPermissionSub.CertificateTemplates
|
||||
| ProjectPermissionSub.CertificateProfiles
|
||||
| ProjectPermissionSub.SecretFolders
|
||||
| ProjectPermissionSub.SecretImports
|
||||
| ProjectPermissionSub.SecretRotation
|
||||
@@ -293,6 +302,7 @@ export enum ProjectPermissionSub {
|
||||
PkiAlerts = "pki-alerts",
|
||||
PkiCollections = "pki-collections",
|
||||
PkiSubscribers = "pki-subscribers",
|
||||
CertificateProfiles = "certificate-profiles",
|
||||
Kms = "kms",
|
||||
Cmek = "cmek",
|
||||
SecretSyncs = "secret-syncs",
|
||||
@@ -470,6 +480,7 @@ export type ProjectPermissionSet =
|
||||
| (ForcedSubject<ProjectPermissionSub.PkiSubscribers> & PkiSubscriberSubjectFields)
|
||||
)
|
||||
]
|
||||
| [ProjectPermissionCertificateProfileActions, ProjectPermissionSub.CertificateProfiles]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
|
||||
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Project]
|
||||
|
||||
@@ -15,6 +15,7 @@ export {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionAuditLogsActions,
|
||||
ProjectPermissionCertificateActions,
|
||||
ProjectPermissionCertificateProfileActions,
|
||||
ProjectPermissionCmekActions,
|
||||
ProjectPermissionDynamicSecretActions,
|
||||
ProjectPermissionGroupActions,
|
||||
|
||||
@@ -79,7 +79,7 @@ export const getProjectHomePage = (type: ProjectType, environments: ProjectEnv[]
|
||||
case ProjectType.SecretManager:
|
||||
return "/projects/secret-management/$projectId/overview" as const;
|
||||
case ProjectType.CertificateManager:
|
||||
return "/projects/cert-management/$projectId/subscribers" as const;
|
||||
return "/projects/cert-management/$projectId/policies" as const;
|
||||
case ProjectType.SecretScanning:
|
||||
return `/projects/${type}/$projectId/data-sources` as const;
|
||||
case ProjectType.PAM:
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
export { AcmeDnsProvider, CaRenewalType, CaStatus, CaType, InternalCaType } from "./enums";
|
||||
export type { TOrderCertificateDTO, TOrderCertificateResponse } from "./types";
|
||||
export {
|
||||
useCreateCa,
|
||||
useCreateCertificate,
|
||||
useCreateCertificateV3,
|
||||
useDeleteCa,
|
||||
useImportCaCertificate,
|
||||
useOrderCertificateWithProfile,
|
||||
useRenewCa,
|
||||
useSignIntermediate,
|
||||
useUpdateCa
|
||||
|
||||
@@ -14,6 +14,8 @@ import {
|
||||
TDeleteCertificateAuthorityDTO,
|
||||
TImportCaCertificateDTO,
|
||||
TImportCaCertificateResponse,
|
||||
TOrderCertificateDTO,
|
||||
TOrderCertificateResponse,
|
||||
TRenewCaDTO,
|
||||
TRenewCaResponse,
|
||||
TSignIntermediateDTO,
|
||||
@@ -168,6 +170,24 @@ export const useCreateCertificateV3 = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useOrderCertificateWithProfile = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<TOrderCertificateResponse, object, TOrderCertificateDTO>({
|
||||
mutationFn: async (body) => {
|
||||
const { data } = await apiRequest.post<TOrderCertificateResponse>(
|
||||
"/api/v3/certificates/order-certificate",
|
||||
body
|
||||
);
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { projectSlug }) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: projectKeys.forProjectCertificates(projectSlug)
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useRenewCa = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<TRenewCaResponse, object, TRenewCaDTO>({
|
||||
|
||||
@@ -155,7 +155,7 @@ export type TCreateCertificateDTO = {
|
||||
pkiCollectionId?: string;
|
||||
friendlyName?: string;
|
||||
commonName: string;
|
||||
altNames: string; // sans
|
||||
subjectAltNames: string; // sans
|
||||
ttl: string; // string compatible with ms
|
||||
notBefore?: string;
|
||||
notAfter?: string;
|
||||
@@ -185,7 +185,7 @@ export type TCreateCertificateV3DTO = {
|
||||
email?: string;
|
||||
streetAddress?: string;
|
||||
postalCode?: string;
|
||||
altNames: string;
|
||||
subjectAltNames: string;
|
||||
ttl: string;
|
||||
notBefore?: string;
|
||||
notAfter?: string;
|
||||
@@ -197,6 +197,54 @@ export type TCreateCertificateV3DTO = {
|
||||
|
||||
export type TCreateCertificateV3Response = TCreateCertificateResponse;
|
||||
|
||||
export type TOrderCertificateDTO = {
|
||||
projectSlug: string;
|
||||
profileId: string;
|
||||
subjectAlternativeNames: Array<{
|
||||
type: "dns" | "ip";
|
||||
value: string;
|
||||
}>;
|
||||
ttl: string;
|
||||
keyUsages?: CertKeyUsage[];
|
||||
extendedKeyUsages?: CertExtendedKeyUsage[];
|
||||
notBefore?: string;
|
||||
notAfter?: string;
|
||||
commonName?: string;
|
||||
signatureAlgorithm?: string;
|
||||
keyAlgorithm?: string;
|
||||
};
|
||||
|
||||
export type TOrderCertificateResponse = {
|
||||
orderId: string;
|
||||
status: "pending" | "processing" | "valid" | "invalid";
|
||||
subjectAlternativeNames: Array<{
|
||||
type: "dns" | "ip";
|
||||
value: string;
|
||||
status: "pending" | "processing" | "valid" | "invalid";
|
||||
}>;
|
||||
authorizations: Array<{
|
||||
identifier: {
|
||||
type: "dns" | "ip";
|
||||
value: string;
|
||||
};
|
||||
status: "pending" | "processing" | "valid" | "invalid";
|
||||
expires?: string;
|
||||
challenges: Array<{
|
||||
type: string;
|
||||
status: "pending" | "processing" | "valid" | "invalid";
|
||||
url: string;
|
||||
token: string;
|
||||
validated?: string;
|
||||
error?: any;
|
||||
}>;
|
||||
}>;
|
||||
certificate?: string;
|
||||
privateKey?: string;
|
||||
expires: string;
|
||||
notBefore: string;
|
||||
notAfter: string;
|
||||
};
|
||||
|
||||
export type TRenewCaDTO = {
|
||||
projectSlug: string;
|
||||
caId: string;
|
||||
|
||||
@@ -124,16 +124,7 @@ export type TListCertificateTemplatesDTO = {
|
||||
|
||||
export type TCertificateTemplateV2Policy = {
|
||||
attributes: Array<{
|
||||
type:
|
||||
| "common_name"
|
||||
| "organization_name"
|
||||
| "organization_unit"
|
||||
| "locality"
|
||||
| "state"
|
||||
| "country"
|
||||
| "email"
|
||||
| "street_address"
|
||||
| "postal_code";
|
||||
type: "common_name";
|
||||
include: "mandatory" | "optional" | "prohibit";
|
||||
value?: string[];
|
||||
}>;
|
||||
|
||||
@@ -25,22 +25,22 @@ export enum CrlReason {
|
||||
}
|
||||
|
||||
export enum CertKeyUsage {
|
||||
DIGITAL_SIGNATURE = "digitalSignature",
|
||||
KEY_ENCIPHERMENT = "keyEncipherment",
|
||||
NON_REPUDIATION = "nonRepudiation",
|
||||
DATA_ENCIPHERMENT = "dataEncipherment",
|
||||
KEY_AGREEMENT = "keyAgreement",
|
||||
KEY_CERT_SIGN = "keyCertSign",
|
||||
CRL_SIGN = "cRLSign",
|
||||
ENCIPHER_ONLY = "encipherOnly",
|
||||
DECIPHER_ONLY = "decipherOnly"
|
||||
DIGITAL_SIGNATURE = "digital_signature",
|
||||
KEY_ENCIPHERMENT = "key_encipherment",
|
||||
NON_REPUDIATION = "non_repudiation",
|
||||
DATA_ENCIPHERMENT = "data_encipherment",
|
||||
KEY_AGREEMENT = "key_agreement",
|
||||
KEY_CERT_SIGN = "key_cert_sign",
|
||||
CRL_SIGN = "crl_sign",
|
||||
ENCIPHER_ONLY = "encipher_only",
|
||||
DECIPHER_ONLY = "decipher_only"
|
||||
}
|
||||
|
||||
export enum CertExtendedKeyUsage {
|
||||
CLIENT_AUTH = "clientAuth",
|
||||
SERVER_AUTH = "serverAuth",
|
||||
CODE_SIGNING = "codeSigning",
|
||||
EMAIL_PROTECTION = "emailProtection",
|
||||
TIMESTAMPING = "timeStamping",
|
||||
OCSP_SIGNING = "ocspSigning"
|
||||
CLIENT_AUTH = "client_auth",
|
||||
SERVER_AUTH = "server_auth",
|
||||
CODE_SIGNING = "code_signing",
|
||||
EMAIL_PROTECTION = "email_protection",
|
||||
TIMESTAMPING = "time_stamping",
|
||||
OCSP_SIGNING = "ocsp_signing"
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ export type TCertificate = {
|
||||
status: CertStatus;
|
||||
friendlyName: string;
|
||||
commonName: string;
|
||||
altNames: string;
|
||||
subjectAltNames: string;
|
||||
serialNumber: string;
|
||||
notBefore: string;
|
||||
notAfter: string;
|
||||
|
||||
@@ -18,7 +18,7 @@ import { Link, Outlet } from "@tanstack/react-router";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import { Lottie, Menu, MenuGroup, MenuItem } from "@app/components/v2";
|
||||
import { useProject, useProjectPermission } from "@app/context";
|
||||
import { useProject, useProjectPermission, useSubscription } from "@app/context";
|
||||
import {
|
||||
useListWorkspaceCertificateTemplates,
|
||||
useListWorkspacePkiSubscribers
|
||||
@@ -29,6 +29,7 @@ import { AssumePrivilegeModeBanner } from "../ProjectLayout/components/AssumePri
|
||||
export const PkiManagerLayout = () => {
|
||||
const { currentProject } = useProject();
|
||||
const { assumedPrivilegeDetails } = useProjectPermission();
|
||||
const { subscription } = useSubscription();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { data: subscribers = [] } = useListWorkspacePkiSubscribers(currentProject?.id || "");
|
||||
@@ -39,7 +40,8 @@ export const PkiManagerLayout = () => {
|
||||
|
||||
const hasExistingSubscribers = subscribers.length > 0;
|
||||
const hasExistingTemplates = templates.length > 0;
|
||||
const showLegacySection = hasExistingSubscribers || hasExistingTemplates;
|
||||
const showLegacySection =
|
||||
subscription.pkiLegacyTemplates || hasExistingSubscribers || hasExistingTemplates;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -166,7 +168,7 @@ export const PkiManagerLayout = () => {
|
||||
</MenuGroup>
|
||||
{showLegacySection && (
|
||||
<MenuGroup title="Legacy">
|
||||
{hasExistingSubscribers && (
|
||||
{(subscription.pkiLegacyTemplates || hasExistingSubscribers) && (
|
||||
<Link
|
||||
to="/projects/cert-management/$projectId/subscribers"
|
||||
params={{
|
||||
@@ -185,7 +187,7 @@ export const PkiManagerLayout = () => {
|
||||
)}
|
||||
</Link>
|
||||
)}
|
||||
{hasExistingTemplates && (
|
||||
{(subscription.pkiLegacyTemplates || hasExistingTemplates) && (
|
||||
<Link
|
||||
to="/projects/cert-management/$projectId/certificate-templates"
|
||||
params={{
|
||||
|
||||
-20
@@ -7,7 +7,6 @@ import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Select,
|
||||
@@ -24,8 +23,6 @@ const schema = z.object({
|
||||
certificatePem: z.string().trim().min(1, "Certificate PEM is required"),
|
||||
privateKeyPem: z.string().trim().min(1, "Private Key PEM is required"),
|
||||
chainPem: z.string().trim().min(1, "Certificate Chain PEM is required"),
|
||||
|
||||
friendlyName: z.string(),
|
||||
collectionId: z.string().optional()
|
||||
});
|
||||
|
||||
@@ -72,7 +69,6 @@ export const CertificateImportModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
certificatePem,
|
||||
privateKeyPem,
|
||||
chainPem,
|
||||
friendlyName,
|
||||
collectionId
|
||||
}: FormData) => {
|
||||
try {
|
||||
@@ -84,8 +80,6 @@ export const CertificateImportModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
certificatePem,
|
||||
privateKeyPem,
|
||||
chainPem,
|
||||
|
||||
friendlyName,
|
||||
pkiCollectionId: collectionId
|
||||
});
|
||||
|
||||
@@ -150,20 +144,6 @@ export const CertificateImportModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="friendlyName"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Friendly Name"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="My Certificate" isDisabled={Boolean(cert)} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
|
||||
+423
-431
File diff suppressed because it is too large
Load Diff
@@ -46,9 +46,8 @@ const schema = z.object({
|
||||
certificateTemplateId: z.string().optional(),
|
||||
caId: z.string(),
|
||||
collectionId: z.string().optional(),
|
||||
friendlyName: z.string(),
|
||||
commonName: z.string().trim().min(1),
|
||||
altNames: z.string(),
|
||||
subjectAltNames: z.string(),
|
||||
ttl: z.string().trim(),
|
||||
keyUsages: z.object({
|
||||
[CertKeyUsage.DIGITAL_SIGNATURE]: z.boolean().optional(),
|
||||
@@ -139,9 +138,8 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
if (cert) {
|
||||
reset({
|
||||
caId: cert.caId,
|
||||
friendlyName: cert.friendlyName,
|
||||
commonName: cert.commonName,
|
||||
altNames: cert.altNames,
|
||||
subjectAltNames: cert.subjectAltNames,
|
||||
certificateTemplateId: cert.certificateTemplateId ?? CERT_TEMPLATE_NONE_VALUE,
|
||||
ttl: "",
|
||||
keyUsages: Object.fromEntries((cert.keyUsages || []).map((name) => [name, true])),
|
||||
@@ -152,9 +150,8 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
} else {
|
||||
reset({
|
||||
caId: "",
|
||||
friendlyName: "",
|
||||
commonName: "",
|
||||
altNames: "",
|
||||
subjectAltNames: "",
|
||||
ttl: "",
|
||||
certificateTemplateId: CERT_TEMPLATE_NONE_VALUE,
|
||||
keyUsages: {
|
||||
@@ -182,10 +179,9 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
|
||||
const onFormSubmit = async ({
|
||||
caId,
|
||||
friendlyName,
|
||||
collectionId,
|
||||
commonName,
|
||||
altNames,
|
||||
subjectAltNames,
|
||||
ttl,
|
||||
keyUsages,
|
||||
extendedKeyUsages
|
||||
@@ -198,9 +194,8 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
certificateTemplateId: selectedCertTemplate ? selectedCertTemplateId : undefined,
|
||||
projectSlug: currentProject.slug,
|
||||
pkiCollectionId: collectionId,
|
||||
friendlyName,
|
||||
commonName,
|
||||
altNames,
|
||||
subjectAltNames,
|
||||
ttl,
|
||||
keyUsages: Object.entries(keyUsages)
|
||||
.filter(([, value]) => value)
|
||||
@@ -359,20 +354,6 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="friendlyName"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Friendly Name"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="My Certificate" isDisabled={Boolean(cert)} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
@@ -391,7 +372,7 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="altNames"
|
||||
name="subjectAltNames"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Alternative Names (SANs)"
|
||||
|
||||
+7
-5
@@ -7,7 +7,8 @@ import { Button, DeleteActionModal } from "@app/components/v2";
|
||||
import {
|
||||
ProjectPermissionPkiSubscriberActions,
|
||||
ProjectPermissionSub,
|
||||
useProject
|
||||
useProject,
|
||||
useSubscription
|
||||
} from "@app/context";
|
||||
import { useDeletePkiSubscriber, useUpdatePkiSubscriber } from "@app/hooks/api";
|
||||
import { PkiSubscriberStatus } from "@app/hooks/api/pkiSubscriber/types";
|
||||
@@ -18,9 +19,10 @@ import { PkiSubscribersTable } from "./PkiSubscribersTable";
|
||||
|
||||
export const PkiSubscriberSection = () => {
|
||||
const { currentProject } = useProject();
|
||||
const { subscription } = useSubscription();
|
||||
const projectId = currentProject.id;
|
||||
|
||||
const allowNewSubscriberCreation = false;
|
||||
const allowNewSubscriberCreation = subscription.pkiLegacyTemplates;
|
||||
const { mutateAsync: deletePkiSubscriber } = useDeletePkiSubscriber();
|
||||
const { mutateAsync: updatePkiSubscriber } = useUpdatePkiSubscriber();
|
||||
|
||||
@@ -85,16 +87,16 @@ export const PkiSubscriberSection = () => {
|
||||
const subscriberName = subscriberStatusData?.subscriberName || "";
|
||||
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="border-mineshaft-600 bg-mineshaft-900 mb-6 rounded-lg border p-4">
|
||||
<div className="mb-4 flex justify-between">
|
||||
<p className="text-xl font-medium text-mineshaft-100">Subscribers</p>
|
||||
<p className="text-mineshaft-100 text-xl font-medium">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="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">
|
||||
<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">
|
||||
Documentation{" "}
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
|
||||
@@ -112,7 +112,9 @@ 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">
|
||||
<div className="mb-4 flex justify-between">
|
||||
{
|
||||
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
|
||||
@@ -134,6 +136,8 @@ export const PkiTemplateListPage = () => {
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
|
||||
+5
@@ -2,6 +2,7 @@ import { useState } from "react";
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, DeleteActionModal } from "@app/components/v2";
|
||||
import { useProjectPermission } from "@app/context";
|
||||
import {
|
||||
@@ -54,6 +55,10 @@ export const CertificateProfilesTab = () => {
|
||||
});
|
||||
setIsDeleteModalOpen(false);
|
||||
setSelectedProfile(null);
|
||||
createNotification({
|
||||
text: `Certificate profile "${selectedProfile.slug}" deleted successfully`,
|
||||
type: "success"
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to delete profile:", error);
|
||||
}
|
||||
|
||||
+27
-8
@@ -122,7 +122,7 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
|
||||
const certificateAuthorities = caData || [];
|
||||
const certificateTemplates = templateData?.certificateTemplates || [];
|
||||
|
||||
const { control, handleSubmit, reset, watch, setValue } = useForm<FormData>({
|
||||
const { control, handleSubmit, reset, watch, setValue, formState } = useForm<FormData>({
|
||||
resolver: zodResolver(isEdit ? editSchema : createSchema),
|
||||
defaultValues: isEdit
|
||||
? {
|
||||
@@ -260,7 +260,7 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
|
||||
name="certificateAuthorityId"
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Certificate Authority"
|
||||
label="Issuing CA"
|
||||
isRequired
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
@@ -296,7 +296,6 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
|
||||
<Select
|
||||
{...field}
|
||||
onValueChange={(value) => {
|
||||
onChange(value);
|
||||
if (watchedEnrollmentType === "est") {
|
||||
setValue("estConfig", {
|
||||
disableBootstrapCaValidation: false,
|
||||
@@ -311,6 +310,7 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
|
||||
});
|
||||
setValue("estConfig", undefined);
|
||||
}
|
||||
onChange(value);
|
||||
}}
|
||||
placeholder="Select a certificate template"
|
||||
className="w-full"
|
||||
@@ -339,7 +339,23 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
|
||||
>
|
||||
<Select
|
||||
{...field}
|
||||
onValueChange={onChange}
|
||||
onValueChange={(value) => {
|
||||
if (value === "est") {
|
||||
setValue("apiConfig", undefined);
|
||||
setValue("estConfig", {
|
||||
disableBootstrapCaValidation: false,
|
||||
passphrase: "",
|
||||
caChain: ""
|
||||
});
|
||||
} else {
|
||||
setValue("estConfig", undefined);
|
||||
setValue("apiConfig", {
|
||||
autoRenew: false,
|
||||
autoRenewDays: 30
|
||||
});
|
||||
}
|
||||
onChange(value);
|
||||
}}
|
||||
className="w-full"
|
||||
position="popper"
|
||||
isDisabled={Boolean(isEdit)}
|
||||
@@ -360,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="flex items-center gap-3 rounded-md border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="border-mineshaft-600 bg-mineshaft-900 flex items-center gap-3 rounded-md border p-4">
|
||||
<Checkbox
|
||||
id="disableBootstrapCaValidation"
|
||||
isChecked={value}
|
||||
onCheckedChange={onChange}
|
||||
/>
|
||||
<div className="space-y-1">
|
||||
<span className="text-sm font-medium text-mineshaft-100">
|
||||
<span className="text-mineshaft-100 text-sm font-medium">
|
||||
Disable Bootstrap CA Validation
|
||||
</span>
|
||||
<p className="text-xs text-bunker-300">
|
||||
<p className="text-bunker-300 text-xs">
|
||||
Skip CA certificate validation during EST bootstrap phase
|
||||
</p>
|
||||
</div>
|
||||
@@ -417,7 +433,7 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
|
||||
rows={6}
|
||||
className="w-full font-mono text-xs"
|
||||
/>
|
||||
<p className="text-xs text-bunker-400">
|
||||
<p className="text-bunker-400 text-xs">
|
||||
Paste the complete CA certificate chain in PEM format
|
||||
</p>
|
||||
</div>
|
||||
@@ -478,6 +494,9 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
|
||||
type="submit"
|
||||
colorSchema="primary"
|
||||
isLoading={isEdit ? updateProfile.isPending : createProfile.isPending}
|
||||
isDisabled={
|
||||
!formState.isValid || (isEdit ? updateProfile.isPending : createProfile.isPending)
|
||||
}
|
||||
>
|
||||
{isEdit ? "Save Changes" : "Create"}
|
||||
</Button>
|
||||
|
||||
+22
-18
@@ -4,6 +4,7 @@ import {
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tr
|
||||
@@ -33,14 +34,6 @@ export const ProfileList = ({ onEditProfile, onDeleteProfile }: Props) => {
|
||||
|
||||
const profiles = data?.certificateProfiles || [];
|
||||
|
||||
if (isLoading) {
|
||||
return <TableSkeleton columns={6} innerKey="certificate-profiles" />;
|
||||
}
|
||||
|
||||
if (!profiles || profiles.length === 0) {
|
||||
return <EmptyState title="No Certificate Profiles" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<TableContainer>
|
||||
<Table>
|
||||
@@ -48,21 +41,32 @@ export const ProfileList = ({ onEditProfile, onDeleteProfile }: Props) => {
|
||||
<Tr>
|
||||
<Th>Name</Th>
|
||||
<Th>Enrollment Type</Th>
|
||||
<Th>Certificate Authority</Th>
|
||||
<Th>Template</Th>
|
||||
<Th>Issuing CA</Th>
|
||||
<Th>Certificate Template</Th>
|
||||
<Th>Certificates</Th>
|
||||
<Th className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{profiles.map((profile) => (
|
||||
<ProfileRow
|
||||
key={profile.id}
|
||||
profile={profile}
|
||||
onEditProfile={onEditProfile}
|
||||
onDeleteProfile={onDeleteProfile}
|
||||
/>
|
||||
))}
|
||||
{isLoading && <TableSkeleton columns={6} innerKey="certificate-profiles" />}
|
||||
{!isLoading && (!profiles || profiles.length === 0) && (
|
||||
<Tr>
|
||||
<Td colSpan={6}>
|
||||
<EmptyState title="No Certificate Profiles" />
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
{!isLoading &&
|
||||
profiles &&
|
||||
profiles.length > 0 &&
|
||||
profiles.map((profile) => (
|
||||
<ProfileRow
|
||||
key={profile.id}
|
||||
profile={profile}
|
||||
onEditProfile={onEditProfile}
|
||||
onDeleteProfile={onDeleteProfile}
|
||||
/>
|
||||
))}
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
+52
-1
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import { faCircleInfo, faEdit, faEllipsis, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faCheck, faCircleInfo, faCopy, faEdit, faEllipsis, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import {
|
||||
@@ -15,11 +15,16 @@ import {
|
||||
import { useProjectPermission } from "@app/context";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionCertificateProfileActions,
|
||||
ProjectPermissionSub
|
||||
} from "@app/context/ProjectPermissionContext/types";
|
||||
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 {
|
||||
profile: TCertificateProfile;
|
||||
@@ -32,6 +37,27 @@ export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) =
|
||||
|
||||
const { data: caData } = useGetCaById(profile.caId);
|
||||
|
||||
const { popUp, handlePopUpToggle } = usePopUp([
|
||||
"certificateIssuance"
|
||||
] as const);
|
||||
|
||||
const [isIdCopied, setIsIdCopied] = useToggle(false);
|
||||
|
||||
const handleCopyId = useCallback(() => {
|
||||
setIsIdCopied.on();
|
||||
navigator.clipboard.writeText(profile.id);
|
||||
|
||||
createNotification({
|
||||
text: "Profile ID copied to clipboard",
|
||||
type: "info"
|
||||
});
|
||||
|
||||
const timer = setTimeout(() => setIsIdCopied.off(), 2000);
|
||||
|
||||
// eslint-disable-next-line consistent-return
|
||||
return () => clearTimeout(timer);
|
||||
}, [isIdCopied]);
|
||||
|
||||
const { data: templateData } = useGetCertificateTemplateV2ById({
|
||||
templateId: profile.certificateTemplateId
|
||||
});
|
||||
@@ -41,6 +67,11 @@ export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) =
|
||||
ProjectPermissionSub.CertificateAuthorities
|
||||
);
|
||||
|
||||
const canIssueCertificate = permission.can(
|
||||
ProjectPermissionCertificateProfileActions.IssueCert,
|
||||
ProjectPermissionSub.CertificateProfiles
|
||||
);
|
||||
|
||||
const canDeleteProfile = permission.can(
|
||||
ProjectPermissionActions.Delete,
|
||||
ProjectPermissionSub.CertificateAuthorities
|
||||
@@ -128,6 +159,12 @@ export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) =
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="p-1">
|
||||
<DropdownMenuItem
|
||||
icon={<FontAwesomeIcon icon={isIdCopied ? faCheck : faCopy} />}
|
||||
onClick={() => handleCopyId()}
|
||||
>
|
||||
Copy Profile ID
|
||||
</DropdownMenuItem>
|
||||
{canEditProfile && (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
@@ -139,6 +176,19 @@ 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>
|
||||
)
|
||||
}
|
||||
{canDeleteProfile && (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
@@ -152,6 +202,7 @@ export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) =
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<CertificateIssuanceModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} profileId={profile.id}/>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
|
||||
+6
-1
@@ -2,6 +2,7 @@ import { useState } from "react";
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, DeleteActionModal } from "@app/components/v2";
|
||||
import { useProjectPermission } from "@app/context";
|
||||
import {
|
||||
@@ -52,7 +53,11 @@ export const CertificateTemplatesV2Tab = () => {
|
||||
});
|
||||
setIsDeleteModalOpen(false);
|
||||
setSelectedTemplate(null);
|
||||
} catch (error) {
|
||||
createNotification({
|
||||
text: `Certificate template "${selectedTemplate.slug}" deleted successfully`,
|
||||
type: "success"
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("Failed to delete template:", error);
|
||||
}
|
||||
};
|
||||
|
||||
+325
-216
@@ -1,5 +1,6 @@
|
||||
import { useMemo } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faExclamationTriangle, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
|
||||
@@ -18,7 +19,8 @@ import {
|
||||
ModalContent,
|
||||
Select,
|
||||
SelectItem,
|
||||
TextArea
|
||||
TextArea,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { useProject } from "@app/context";
|
||||
import {
|
||||
@@ -27,7 +29,7 @@ import {
|
||||
} from "@app/hooks/api/certificateTemplates/mutations";
|
||||
import { TCertificateTemplateV2New } from "@app/hooks/api/certificateTemplates/types";
|
||||
|
||||
import { INCLUDE_OPTIONS, SAN_TYPES, SUBJECT_ATTRIBUTE_TYPES } from "./shared/utils";
|
||||
import { INCLUDE_TYPE_OPTIONS, SAN_TYPE_OPTIONS, SUBJECT_ATTRIBUTE_TYPE_OPTIONS } from "./shared/certificate-constants";
|
||||
import { KeyUsagesSection, TemplateFormData, templateSchema } from "./shared";
|
||||
|
||||
export type FormData = TemplateFormData;
|
||||
@@ -39,26 +41,18 @@ interface Props {
|
||||
mode?: "create" | "edit";
|
||||
}
|
||||
|
||||
const ATTRIBUTE_TYPE_LABELS: Record<(typeof SUBJECT_ATTRIBUTE_TYPES)[number], string> = {
|
||||
common_name: "Common Name (CN)",
|
||||
organization_name: "Organization (O)",
|
||||
organization_unit: "Organizational Unit (OU)",
|
||||
locality: "Locality (L)",
|
||||
state: "State/Province (ST)",
|
||||
country: "Country (C)",
|
||||
email: "Email Address",
|
||||
street_address: "Street Address",
|
||||
postal_code: "Postal Code"
|
||||
const ATTRIBUTE_TYPE_LABELS: Record<(typeof SUBJECT_ATTRIBUTE_TYPE_OPTIONS)[number], string> = {
|
||||
common_name: "Common Name (CN)"
|
||||
};
|
||||
|
||||
const SAN_TYPE_LABELS: Record<(typeof SAN_TYPES)[number], string> = {
|
||||
const SAN_TYPE_LABELS: Record<(typeof SAN_TYPE_OPTIONS)[number], string> = {
|
||||
dns_name: "DNS Name",
|
||||
ip_address: "IP Address",
|
||||
email: "Email",
|
||||
uri: "URI"
|
||||
};
|
||||
|
||||
const INCLUDE_TYPE_LABELS: Record<(typeof INCLUDE_OPTIONS)[number], string> = {
|
||||
const INCLUDE_TYPE_LABELS: Record<(typeof INCLUDE_TYPE_OPTIONS)[number], string> = {
|
||||
mandatory: "Mandatory",
|
||||
optional: "Optional",
|
||||
prohibit: "Prohibited"
|
||||
@@ -89,7 +83,36 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
||||
|
||||
const isEdit = mode === "edit" && template;
|
||||
|
||||
const { control, handleSubmit, reset, watch, setValue } = useForm<FormData>({
|
||||
const validateAttributeRules = (attributes: FormData["attributes"]) => {
|
||||
if (!attributes) return { isValid: true, warnings: [], invalidIndices: [] };
|
||||
|
||||
const warnings: string[] = [];
|
||||
const invalidIndices: number[] = [];
|
||||
|
||||
const attributesByType = attributes.reduce((acc, attr, index) => {
|
||||
if (!acc[attr.type]) acc[attr.type] = [];
|
||||
acc[attr.type].push({ ...attr, index });
|
||||
return acc;
|
||||
}, {} as Record<string, Array<(typeof attributes[0] & { index: number })>>);
|
||||
|
||||
Object.entries(attributesByType).forEach(([type, attrs]) => {
|
||||
const mandatoryAttrs = attrs.filter(attr => attr.include === 'mandatory');
|
||||
|
||||
if (mandatoryAttrs.length > 1) {
|
||||
mandatoryAttrs.forEach(attr => invalidIndices.push(attr.index));
|
||||
warnings.push(`Multiple mandatory values found for ${ATTRIBUTE_TYPE_LABELS[type as keyof typeof ATTRIBUTE_TYPE_LABELS]}. Only one mandatory value is allowed per attribute type.`);
|
||||
}
|
||||
|
||||
if (mandatoryAttrs.length === 1 && attrs.length > 1) {
|
||||
attrs.forEach(attr => invalidIndices.push(attr.index));
|
||||
warnings.push(`When a mandatory value exists for ${ATTRIBUTE_TYPE_LABELS[type as keyof typeof ATTRIBUTE_TYPE_LABELS]}, no other values (optional or forbidden) are allowed for that attribute type.`);
|
||||
}
|
||||
});
|
||||
|
||||
return { isValid: warnings.length === 0, warnings, invalidIndices };
|
||||
};
|
||||
|
||||
const { control, handleSubmit, reset, watch, setValue, formState } = useForm<FormData>({
|
||||
resolver: zodResolver(templateSchema),
|
||||
defaultValues: isEdit
|
||||
? {
|
||||
@@ -138,13 +161,42 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
||||
|
||||
const watchedAttributes = watch("attributes") || [];
|
||||
const watchedSans = watch("subjectAlternativeNames") || [];
|
||||
const watchedKeyUsages = watch("keyUsages");
|
||||
const watchedExtendedKeyUsages = watch("extendedKeyUsages");
|
||||
const watchedKeyUsages = watch("keyUsages") || { requiredUsages: [], optionalUsages: [] };
|
||||
const watchedExtendedKeyUsages = watch("extendedKeyUsages") || { requiredUsages: [], optionalUsages: [] };
|
||||
|
||||
const attributeValidation = useMemo(() =>
|
||||
validateAttributeRules(watchedAttributes),
|
||||
[watchedAttributes]
|
||||
);
|
||||
|
||||
const onFormSubmit = async (data: FormData) => {
|
||||
try {
|
||||
if (!currentProject?.id && !isEdit) return;
|
||||
|
||||
if (!attributeValidation.isValid) {
|
||||
createNotification({
|
||||
text: "Please fix validation errors before submitting",
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const hasEmptyAttributeValues = data.attributes?.some(attr =>
|
||||
!attr.value || attr.value.length === 0 || attr.value.some(v => !v.trim())
|
||||
);
|
||||
|
||||
const hasEmptySanValues = data.subjectAlternativeNames?.some(san =>
|
||||
!san.value || san.value.length === 0 || san.value.some(v => !v.trim())
|
||||
);
|
||||
|
||||
if (hasEmptyAttributeValues || hasEmptySanValues) {
|
||||
createNotification({
|
||||
text: "All values must be non-empty. Use wildcards (*) if needed.",
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (isEdit) {
|
||||
const updateData = {
|
||||
templateId: template.id,
|
||||
@@ -221,9 +273,9 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
||||
|
||||
const addAttribute = () => {
|
||||
const newAttribute = {
|
||||
type: SUBJECT_ATTRIBUTE_TYPES[0],
|
||||
include: INCLUDE_OPTIONS[1],
|
||||
value: []
|
||||
type: SUBJECT_ATTRIBUTE_TYPE_OPTIONS[0],
|
||||
include: INCLUDE_TYPE_OPTIONS[1],
|
||||
value: ["*"]
|
||||
};
|
||||
setValue("attributes", [...watchedAttributes, newAttribute]);
|
||||
};
|
||||
@@ -235,9 +287,9 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
||||
|
||||
const addSan = () => {
|
||||
const newSan = {
|
||||
type: SAN_TYPES[0],
|
||||
include: INCLUDE_OPTIONS[1],
|
||||
value: []
|
||||
type: SAN_TYPE_OPTIONS[0],
|
||||
include: INCLUDE_TYPE_OPTIONS[1],
|
||||
value: ["*"]
|
||||
};
|
||||
setValue("subjectAlternativeNames", [...watchedSans, newSan]);
|
||||
};
|
||||
@@ -247,42 +299,18 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
||||
setValue("subjectAlternativeNames", newSans);
|
||||
};
|
||||
|
||||
const toggleKeyUsage = (usage: string, type: "required" | "optional") => {
|
||||
const current = watchedKeyUsages || { requiredUsages: [], optionalUsages: [] };
|
||||
const otherType = type === "required" ? "optional" : "required";
|
||||
const currentList = Array.isArray(current[`${type}Usages`]) ? current[`${type}Usages`] : [];
|
||||
const otherList = Array.isArray(current[`${otherType}Usages`])
|
||||
? current[`${otherType}Usages`]
|
||||
: [];
|
||||
|
||||
const newOtherList = (otherList || []).filter((u) => u !== usage);
|
||||
const newCurrentList = currentList?.includes(usage)
|
||||
? currentList.filter((u) => u !== usage)
|
||||
: [...(currentList || []), usage];
|
||||
|
||||
const handleKeyUsagesChange = (usages: { requiredUsages: string[]; optionalUsages: string[] }) => {
|
||||
setValue("keyUsages", {
|
||||
[`${type}Usages`]: newCurrentList,
|
||||
[`${otherType}Usages`]: newOtherList
|
||||
} as any);
|
||||
requiredUsages: usages.requiredUsages as any,
|
||||
optionalUsages: usages.optionalUsages as any
|
||||
});
|
||||
};
|
||||
|
||||
const toggleExtendedKeyUsage = (usage: string, type: "required" | "optional") => {
|
||||
const current = watchedExtendedKeyUsages || { requiredUsages: [], optionalUsages: [] };
|
||||
const otherType = type === "required" ? "optional" : "required";
|
||||
const currentList = Array.isArray(current[`${type}Usages`]) ? current[`${type}Usages`] : [];
|
||||
const otherList = Array.isArray(current[`${otherType}Usages`])
|
||||
? current[`${otherType}Usages`]
|
||||
: [];
|
||||
|
||||
const newOtherList = (otherList || []).filter((u) => u !== usage);
|
||||
const newCurrentList = currentList?.includes(usage)
|
||||
? currentList.filter((u) => u !== usage)
|
||||
: [...(currentList || []), usage];
|
||||
|
||||
const handleExtendedKeyUsagesChange = (usages: { requiredUsages: string[]; optionalUsages: string[] }) => {
|
||||
setValue("extendedKeyUsages", {
|
||||
[`${type}Usages`]: newCurrentList,
|
||||
[`${otherType}Usages`]: newOtherList
|
||||
} as any);
|
||||
requiredUsages: usages.requiredUsages as any,
|
||||
optionalUsages: usages.optionalUsages as any
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -297,7 +325,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
||||
>
|
||||
<ModalContent
|
||||
className="max-w-4xl"
|
||||
title={isEdit ? "Edit Certificate Template V2" : "Create Certificate Template V2"}
|
||||
title={isEdit ? "Edit Certificate Template" : "Create Certificate Template"}
|
||||
subTitle={
|
||||
isEdit
|
||||
? `Update configuration for ${template?.slug}`
|
||||
@@ -306,42 +334,36 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
||||
>
|
||||
<form onSubmit={handleSubmit(onFormSubmit)} className="space-y-6">
|
||||
<Accordion type="multiple" defaultValue={["basic"]} className="w-full">
|
||||
<AccordionItem value="basic">
|
||||
<AccordionTrigger>Basic Information</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="space-y-4">
|
||||
<Controller
|
||||
control={control}
|
||||
name="slug"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Template Name"
|
||||
isRequired
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="Enter template name" className="w-full" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="description"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Description"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<TextArea {...field} placeholder="Enter template description" rows={3} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<div className="space-y-4">
|
||||
<Controller
|
||||
control={control}
|
||||
name="slug"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Template Name"
|
||||
isRequired
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="Enter template name" className="w-full" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="description"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Description"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<TextArea {...field} placeholder="Enter template description" rows={3} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<AccordionItem value="attributes">
|
||||
<AccordionTrigger>Subject Attributes</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
@@ -351,92 +373,173 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
||||
type="button"
|
||||
onClick={addAttribute}
|
||||
size="sm"
|
||||
variant="outline_bg"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
>
|
||||
Add Attribute
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Validation warnings */}
|
||||
{!attributeValidation.isValid && attributeValidation.warnings.length > 0 && (
|
||||
<div className="bg-yellow-500/10 border border-yellow-500/20 rounded-md p-3">
|
||||
<div className="flex items-start gap-2">
|
||||
<FontAwesomeIcon icon={faExclamationTriangle} className="text-yellow-500 mt-0.5" />
|
||||
<div className="flex-1">
|
||||
<h4 className="text-yellow-500 font-medium text-sm">Validation Warnings</h4>
|
||||
<ul className="text-yellow-400 text-sm mt-1 space-y-1">
|
||||
{attributeValidation.warnings.map((warning, index) => (
|
||||
<li key={index}>• {warning}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
{watchedAttributes.length === 0 ? (
|
||||
<div className="py-8 text-center text-bunker-300">
|
||||
<div className="text-bunker-300 py-8 text-center">
|
||||
No subject attributes configured yet. Click "Add Attribute" to get
|
||||
started.
|
||||
</div>
|
||||
) : (
|
||||
watchedAttributes.map((attr, index) => (
|
||||
<div
|
||||
key={`attr-${attr.type}`}
|
||||
className="flex flex-col space-y-2 rounded-md border border-mineshaft-600 p-4"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium text-mineshaft-200">
|
||||
{ATTRIBUTE_TYPE_LABELS[attr.type] || attr.type}
|
||||
</span>
|
||||
</div>
|
||||
watchedAttributes.map((attr, index) => {
|
||||
const isInvalid = attributeValidation.invalidIndices.includes(index);
|
||||
const errorClass = isInvalid ? "border-red-500 focus:border-red-500" : "";
|
||||
|
||||
<div className="flex gap-3">
|
||||
<Select
|
||||
value={attr.type}
|
||||
onValueChange={(value) => {
|
||||
const newAttributes = [...watchedAttributes];
|
||||
newAttributes[index] = { ...attr, type: value as any };
|
||||
setValue("attributes", newAttributes);
|
||||
}}
|
||||
position="popper"
|
||||
>
|
||||
{SUBJECT_ATTRIBUTE_TYPES.map((type) => (
|
||||
<SelectItem key={type} value={type}>
|
||||
{ATTRIBUTE_TYPE_LABELS[type]}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
return (
|
||||
<div key={`attr-${attr.type}-${attr.include}-${index}`} className="flex items-start gap-2">
|
||||
{isInvalid ? (
|
||||
<Tooltip content="This attribute has validation errors. Check the warnings above for details.">
|
||||
<div className="flex items-start gap-2 flex-1">
|
||||
<Select
|
||||
value={attr.type}
|
||||
onValueChange={(value) => {
|
||||
const newAttributes = [...watchedAttributes];
|
||||
newAttributes[index] = { ...attr, type: value as any };
|
||||
setValue("attributes", newAttributes);
|
||||
}}
|
||||
className={`w-48 ${errorClass}`}
|
||||
>
|
||||
{SUBJECT_ATTRIBUTE_TYPE_OPTIONS.map((type) => (
|
||||
<SelectItem key={type} value={type}>
|
||||
{ATTRIBUTE_TYPE_LABELS[type]}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
<Select
|
||||
value={attr.include}
|
||||
onValueChange={(value) => {
|
||||
const newAttributes = [...watchedAttributes];
|
||||
newAttributes[index] = { ...attr, include: value as any };
|
||||
setValue("attributes", newAttributes);
|
||||
}}
|
||||
position="popper"
|
||||
>
|
||||
{INCLUDE_OPTIONS.map((type) => (
|
||||
<SelectItem key={type} value={type}>
|
||||
{INCLUDE_TYPE_LABELS[type]}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
<Select
|
||||
value={attr.include}
|
||||
onValueChange={(value) => {
|
||||
const newAttributes = [...watchedAttributes];
|
||||
newAttributes[index] = { ...attr, include: value as any };
|
||||
setValue("attributes", newAttributes);
|
||||
}}
|
||||
className={`w-32 ${errorClass}`}
|
||||
>
|
||||
{INCLUDE_TYPE_OPTIONS.map((type) => (
|
||||
<SelectItem key={type} value={type}>
|
||||
{INCLUDE_TYPE_LABELS[type]}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
<Input
|
||||
placeholder="Pattern/Value (optional)"
|
||||
value={attr.value?.[0] || ""}
|
||||
onChange={(e) => {
|
||||
const newAttributes = [...watchedAttributes];
|
||||
newAttributes[index] = {
|
||||
...attr,
|
||||
value: e.target.value ? [e.target.value] : []
|
||||
};
|
||||
setValue("attributes", newAttributes);
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
placeholder="Pattern/Value (required - use * for wildcards)"
|
||||
value={attr.value?.[0] || ""}
|
||||
onChange={(e) => {
|
||||
const newAttributes = [...watchedAttributes];
|
||||
newAttributes[index] = {
|
||||
...attr,
|
||||
value: e.target.value.trim() ? [e.target.value.trim()] : []
|
||||
};
|
||||
setValue("attributes", newAttributes);
|
||||
}}
|
||||
className={`flex-1 ${errorClass} ${
|
||||
attr.value && attr.value.length > 0 && attr.value[0] === ""
|
||||
? "border-red-500 focus:border-red-500"
|
||||
: ""
|
||||
}`}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<>
|
||||
<Select
|
||||
value={attr.type}
|
||||
onValueChange={(value) => {
|
||||
const newAttributes = [...watchedAttributes];
|
||||
newAttributes[index] = { ...attr, type: value as any };
|
||||
setValue("attributes", newAttributes);
|
||||
}}
|
||||
className="w-48"
|
||||
>
|
||||
{SUBJECT_ATTRIBUTE_TYPE_OPTIONS.map((type) => (
|
||||
<SelectItem key={type} value={type}>
|
||||
{ATTRIBUTE_TYPE_LABELS[type]}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
<Select
|
||||
value={attr.include}
|
||||
onValueChange={(value) => {
|
||||
const newAttributes = [...watchedAttributes];
|
||||
newAttributes[index] = { ...attr, include: value as any };
|
||||
setValue("attributes", newAttributes);
|
||||
}}
|
||||
className="w-32"
|
||||
>
|
||||
{INCLUDE_TYPE_OPTIONS.map((type) => (
|
||||
<SelectItem key={type} value={type}>
|
||||
{INCLUDE_TYPE_LABELS[type]}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
<Input
|
||||
placeholder="Pattern/Value (required - use * for wildcards)"
|
||||
value={attr.value?.[0] || ""}
|
||||
onChange={(e) => {
|
||||
const newAttributes = [...watchedAttributes];
|
||||
newAttributes[index] = {
|
||||
...attr,
|
||||
value: e.target.value.trim() ? [e.target.value.trim()] : []
|
||||
};
|
||||
setValue("attributes", newAttributes);
|
||||
}}
|
||||
className={`flex-1 ${
|
||||
attr.value && attr.value.length > 0 && attr.value[0] === ""
|
||||
? "border-red-500 focus:border-red-500"
|
||||
: ""
|
||||
}`}
|
||||
required
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{watchedAttributes.length > 1 && (
|
||||
<IconButton
|
||||
ariaLabel="delete attribute"
|
||||
ariaLabel="Remove Attribute"
|
||||
variant="plain"
|
||||
size="sm"
|
||||
onClick={() => removeAttribute(index)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} className="text-red-500" />
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="san">
|
||||
<AccordionItem value="san" className="mt-4">
|
||||
<AccordionTrigger>Subject Alternative Names</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="space-y-4">
|
||||
@@ -445,6 +548,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
||||
type="button"
|
||||
onClick={addSan}
|
||||
size="sm"
|
||||
variant="outline_bg"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
>
|
||||
Add SAN
|
||||
@@ -453,72 +557,72 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
||||
|
||||
<div className="space-y-2">
|
||||
{watchedSans.length === 0 ? (
|
||||
<div className="py-8 text-center text-bunker-300">
|
||||
<div className="text-bunker-300 py-8 text-center">
|
||||
No subject alternative names configured yet. Click "Add SAN" to
|
||||
get started.
|
||||
</div>
|
||||
) : (
|
||||
watchedSans.map((san, index) => (
|
||||
<div
|
||||
key={`san-${san.type}`}
|
||||
className="flex flex-col space-y-4 rounded-md border border-mineshaft-600 p-4"
|
||||
>
|
||||
<div className="flex gap-3">
|
||||
<Select
|
||||
value={san.type}
|
||||
onValueChange={(value) => {
|
||||
const newSans = [...watchedSans];
|
||||
newSans[index] = { ...san, type: value as any };
|
||||
setValue("subjectAlternativeNames", newSans);
|
||||
}}
|
||||
position="popper"
|
||||
>
|
||||
{SAN_TYPES.map((type) => (
|
||||
<SelectItem key={type} value={type}>
|
||||
{SAN_TYPE_LABELS[type]}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
<div key={`san-${san.type}-${san.include}-${index}`} className="flex items-start gap-2">
|
||||
<Select
|
||||
value={san.type}
|
||||
onValueChange={(value) => {
|
||||
const newSans = [...watchedSans];
|
||||
newSans[index] = { ...san, type: value as any };
|
||||
setValue("subjectAlternativeNames", newSans);
|
||||
}}
|
||||
className="w-24"
|
||||
>
|
||||
{SAN_TYPE_OPTIONS.map((type) => (
|
||||
<SelectItem key={type} value={type}>
|
||||
{SAN_TYPE_LABELS[type]}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
<Select
|
||||
value={san.include}
|
||||
onValueChange={(value) => {
|
||||
const newSans = [...watchedSans];
|
||||
newSans[index] = { ...san, include: value as any };
|
||||
setValue("subjectAlternativeNames", newSans);
|
||||
}}
|
||||
position="popper"
|
||||
>
|
||||
{INCLUDE_OPTIONS.map((type) => (
|
||||
<SelectItem key={type} value={type}>
|
||||
{INCLUDE_TYPE_LABELS[type]}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
<Select
|
||||
value={san.include}
|
||||
onValueChange={(value) => {
|
||||
const newSans = [...watchedSans];
|
||||
newSans[index] = { ...san, include: value as any };
|
||||
setValue("subjectAlternativeNames", newSans);
|
||||
}}
|
||||
className="w-32"
|
||||
>
|
||||
{INCLUDE_TYPE_OPTIONS.map((type) => (
|
||||
<SelectItem key={type} value={type}>
|
||||
{INCLUDE_TYPE_LABELS[type]}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
<Input
|
||||
placeholder="Pattern/Value (optional)"
|
||||
value={san.value?.[0] || ""}
|
||||
onChange={(e) => {
|
||||
const newSans = [...watchedSans];
|
||||
newSans[index] = {
|
||||
...san,
|
||||
value: e.target.value ? [e.target.value] : []
|
||||
};
|
||||
setValue("subjectAlternativeNames", newSans);
|
||||
}}
|
||||
/>
|
||||
<div className="flex items-center justify-between">
|
||||
<IconButton
|
||||
onClick={() => removeSan(index)}
|
||||
size="sm"
|
||||
variant="plain"
|
||||
ariaLabel="Remove SAN"
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} className="text-red-500" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
<Input
|
||||
placeholder="Pattern/Value (required - use * for wildcards)"
|
||||
value={san.value?.[0] || ""}
|
||||
onChange={(e) => {
|
||||
const newSans = [...watchedSans];
|
||||
newSans[index] = {
|
||||
...san,
|
||||
value: e.target.value.trim() ? [e.target.value.trim()] : []
|
||||
};
|
||||
setValue("subjectAlternativeNames", newSans);
|
||||
}}
|
||||
className={`flex-1 ${
|
||||
san.value && san.value.length > 0 && san.value[0] === ""
|
||||
? "border-red-500 focus:border-red-500"
|
||||
: ""
|
||||
}`}
|
||||
required
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
ariaLabel="Remove SAN"
|
||||
variant="plain"
|
||||
size="sm"
|
||||
onClick={() => removeSan(index)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
@@ -527,19 +631,19 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="usages">
|
||||
<AccordionItem value="usages" className="mt-4">
|
||||
<AccordionTrigger>Key Usages</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<KeyUsagesSection
|
||||
watchedKeyUsages={watchedKeyUsages}
|
||||
watchedExtendedKeyUsages={watchedExtendedKeyUsages}
|
||||
toggleKeyUsage={toggleKeyUsage}
|
||||
toggleExtendedKeyUsage={toggleExtendedKeyUsage}
|
||||
onKeyUsagesChange={handleKeyUsagesChange}
|
||||
onExtendedKeyUsagesChange={handleExtendedKeyUsagesChange}
|
||||
/>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="constraints">
|
||||
<AccordionItem value="constraints" className="mt-4">
|
||||
<AccordionTrigger>Constraints</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="space-y-4">
|
||||
@@ -637,7 +741,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
||||
/>
|
||||
<label
|
||||
htmlFor={`sig-alg-${alg}`}
|
||||
className="cursor-pointer text-sm font-medium text-mineshaft-200"
|
||||
className="text-mineshaft-200 cursor-pointer text-sm font-medium"
|
||||
>
|
||||
{alg}
|
||||
</label>
|
||||
@@ -717,7 +821,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
||||
/>
|
||||
<label
|
||||
htmlFor={`key-alg-${alg}`}
|
||||
className="cursor-pointer text-sm font-medium text-mineshaft-200"
|
||||
className="text-mineshaft-200 cursor-pointer text-sm font-medium"
|
||||
>
|
||||
{alg}
|
||||
</label>
|
||||
@@ -765,6 +869,11 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
||||
type="submit"
|
||||
colorSchema="primary"
|
||||
isLoading={isEdit ? updateTemplate.isPending : createTemplate.isPending}
|
||||
isDisabled={
|
||||
!formState.isValid ||
|
||||
!attributeValidation.isValid ||
|
||||
(isEdit ? updateTemplate.isPending : createTemplate.isPending)
|
||||
}
|
||||
>
|
||||
{isEdit ? "Save Changes" : "Create"}
|
||||
</Button>
|
||||
|
||||
+9
-9
@@ -56,14 +56,6 @@ export const TemplateList = ({ onEditTemplate, onDeleteTemplate }: Props) => {
|
||||
return new Date(dateString).toLocaleDateString();
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return <TableSkeleton columns={4} innerKey="certificate-templates" />;
|
||||
}
|
||||
|
||||
if (!templates || templates.length === 0) {
|
||||
return <EmptyState title="No Certificate Templates" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<TableContainer>
|
||||
<Table>
|
||||
@@ -75,7 +67,15 @@ export const TemplateList = ({ onEditTemplate, onDeleteTemplate }: Props) => {
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{templates.map((template) => (
|
||||
{isLoading && <TableSkeleton columns={3} innerKey="certificate-templates" />}
|
||||
{!isLoading && (!templates || templates.length === 0) && (
|
||||
<Tr>
|
||||
<Td colSpan={3}>
|
||||
<EmptyState title="No Certificate Templates" />
|
||||
</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"
|
||||
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
export enum CertSubjectAlternativeNameType {
|
||||
DNS_NAME = "dns_name",
|
||||
IP_ADDRESS = "ip_address",
|
||||
EMAIL = "email",
|
||||
URI = "uri"
|
||||
}
|
||||
|
||||
export enum CertKeyUsageType {
|
||||
DIGITAL_SIGNATURE = "digital_signature",
|
||||
KEY_ENCIPHERMENT = "key_encipherment",
|
||||
NON_REPUDIATION = "non_repudiation",
|
||||
DATA_ENCIPHERMENT = "data_encipherment",
|
||||
KEY_AGREEMENT = "key_agreement",
|
||||
KEY_CERT_SIGN = "key_cert_sign",
|
||||
CRL_SIGN = "crl_sign",
|
||||
ENCIPHER_ONLY = "encipher_only",
|
||||
DECIPHER_ONLY = "decipher_only"
|
||||
}
|
||||
|
||||
export enum CertExtendedKeyUsageType {
|
||||
CLIENT_AUTH = "client_auth",
|
||||
SERVER_AUTH = "server_auth",
|
||||
CODE_SIGNING = "code_signing",
|
||||
EMAIL_PROTECTION = "email_protection",
|
||||
OCSP_SIGNING = "ocsp_signing",
|
||||
TIME_STAMPING = "time_stamping"
|
||||
}
|
||||
|
||||
export enum CertIncludeType {
|
||||
MANDATORY = "mandatory",
|
||||
OPTIONAL = "optional",
|
||||
PROHIBIT = "prohibit"
|
||||
}
|
||||
|
||||
export enum CertDurationUnit {
|
||||
DAYS = "days",
|
||||
MONTHS = "months",
|
||||
YEARS = "years"
|
||||
}
|
||||
|
||||
export enum CertSubjectAttributeType {
|
||||
COMMON_NAME = "common_name"
|
||||
}
|
||||
|
||||
export const formatSANType = (type: CertSubjectAlternativeNameType): string => {
|
||||
switch (type) {
|
||||
case CertSubjectAlternativeNameType.DNS_NAME:
|
||||
return "DNS Name";
|
||||
case CertSubjectAlternativeNameType.IP_ADDRESS:
|
||||
return "IP Address";
|
||||
case CertSubjectAlternativeNameType.EMAIL:
|
||||
return "Email";
|
||||
case CertSubjectAlternativeNameType.URI:
|
||||
return "URI";
|
||||
default:
|
||||
return type;
|
||||
}
|
||||
};
|
||||
|
||||
export const formatKeyUsage = (usage: CertKeyUsageType): string => {
|
||||
switch (usage) {
|
||||
case CertKeyUsageType.DIGITAL_SIGNATURE:
|
||||
return "Digital Signature";
|
||||
case CertKeyUsageType.KEY_ENCIPHERMENT:
|
||||
return "Key Encipherment";
|
||||
case CertKeyUsageType.NON_REPUDIATION:
|
||||
return "Non Repudiation";
|
||||
case CertKeyUsageType.DATA_ENCIPHERMENT:
|
||||
return "Data Encipherment";
|
||||
case CertKeyUsageType.KEY_AGREEMENT:
|
||||
return "Key Agreement";
|
||||
case CertKeyUsageType.KEY_CERT_SIGN:
|
||||
return "Key Cert Sign";
|
||||
case CertKeyUsageType.CRL_SIGN:
|
||||
return "CRL Sign";
|
||||
case CertKeyUsageType.ENCIPHER_ONLY:
|
||||
return "Encipher Only";
|
||||
case CertKeyUsageType.DECIPHER_ONLY:
|
||||
return "Decipher Only";
|
||||
default:
|
||||
return usage;
|
||||
}
|
||||
};
|
||||
|
||||
export const formatExtendedKeyUsage = (usage: CertExtendedKeyUsageType): string => {
|
||||
switch (usage) {
|
||||
case CertExtendedKeyUsageType.CLIENT_AUTH:
|
||||
return "Client Auth";
|
||||
case CertExtendedKeyUsageType.SERVER_AUTH:
|
||||
return "Server Auth";
|
||||
case CertExtendedKeyUsageType.CODE_SIGNING:
|
||||
return "Code Signing";
|
||||
case CertExtendedKeyUsageType.EMAIL_PROTECTION:
|
||||
return "Email Protection";
|
||||
case CertExtendedKeyUsageType.OCSP_SIGNING:
|
||||
return "OCSP Signing";
|
||||
case CertExtendedKeyUsageType.TIME_STAMPING:
|
||||
return "Time Stamping";
|
||||
default:
|
||||
return usage;
|
||||
}
|
||||
};
|
||||
|
||||
export const formatSubjectAttributeType = (type: CertSubjectAttributeType): string => {
|
||||
switch (type) {
|
||||
case CertSubjectAttributeType.COMMON_NAME:
|
||||
return "Common Name";
|
||||
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";
|
||||
default:
|
||||
return include;
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
default:
|
||||
throw new Error(`Unknown SAN type: ${type}`);
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
+124
-113
@@ -1,141 +1,152 @@
|
||||
import { Button } from "@app/components/v2";
|
||||
import { Checkbox } from "@app/components/v2";
|
||||
|
||||
import {
|
||||
EXTENDED_KEY_USAGES,
|
||||
formatUsageName,
|
||||
getUsageState,
|
||||
KEY_USAGES,
|
||||
toggleUsageState
|
||||
} from "./utils";
|
||||
CertExtendedKeyUsageType,
|
||||
CertKeyUsageType,
|
||||
formatExtendedKeyUsage,
|
||||
formatKeyUsage,
|
||||
EXTENDED_KEY_USAGE_OPTIONS,
|
||||
KEY_USAGE_OPTIONS
|
||||
} from "./certificate-constants";
|
||||
|
||||
type UsageToggleProps = {
|
||||
value: "required" | "optional" | undefined;
|
||||
onChange: (value: "required" | "optional" | undefined) => void;
|
||||
type UsageState = "mandatory" | "optional" | undefined;
|
||||
|
||||
type ThreeStateCheckboxProps = {
|
||||
value: UsageState;
|
||||
onChange: (newValue: UsageState) => void;
|
||||
label: string;
|
||||
id: string;
|
||||
};
|
||||
|
||||
export const UsageToggle = ({ value, onChange }: UsageToggleProps) => {
|
||||
const ThreeStateCheckbox = ({ value, onChange, label, id }: ThreeStateCheckboxProps) => {
|
||||
const handleClick = () => {
|
||||
if (value === undefined) {
|
||||
onChange("optional");
|
||||
} else if (value === "optional") {
|
||||
onChange("mandatory");
|
||||
} else {
|
||||
onChange(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
const getCheckboxState = () => {
|
||||
if (value) return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
const getIndeterminateState = () => {
|
||||
return value === "optional";
|
||||
};
|
||||
|
||||
const getStateLabel = () => {
|
||||
if (value === "mandatory") return " (Mandatory)";
|
||||
if (value === "optional") return " (Optional)";
|
||||
return "";
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="border-mineshaft-600 bg-mineshaft-800 flex gap-x-0.5 rounded-md border p-1">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
onChange(value === "required" ? undefined : "required");
|
||||
}}
|
||||
size="xs"
|
||||
className={`${
|
||||
value === "required" ? "bg-mineshaft-500" : "bg-transparent"
|
||||
} hover:bg-mineshaft-600 min-w-[2.4rem] rounded border-none`}
|
||||
<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"
|
||||
>
|
||||
Required
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
onChange(value === "optional" ? undefined : "optional");
|
||||
}}
|
||||
size="xs"
|
||||
className={`${
|
||||
value === "optional" ? "bg-mineshaft-500" : "bg-transparent"
|
||||
} hover:bg-mineshaft-600 min-w-[2.4rem] rounded border-none`}
|
||||
>
|
||||
Optional
|
||||
</Button>
|
||||
{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[];
|
||||
};
|
||||
toggleKeyUsage: (usage: string, type: "required" | "optional") => void;
|
||||
toggleExtendedKeyUsage: (usage: string, type: "required" | "optional") => void;
|
||||
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,
|
||||
watchedExtendedKeyUsages,
|
||||
toggleKeyUsage,
|
||||
toggleExtendedKeyUsage
|
||||
watchedKeyUsages = { requiredUsages: [], optionalUsages: [] },
|
||||
watchedExtendedKeyUsages = { requiredUsages: [], optionalUsages: [] },
|
||||
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 handleKeyUsageChange = (usage: CertKeyUsageType, newState: UsageState) => {
|
||||
const currentRequired = watchedKeyUsages.requiredUsages || [];
|
||||
const currentOptional = watchedKeyUsages.optionalUsages || [];
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
onKeyUsagesChange({ requiredUsages: newRequired, optionalUsages: newOptional });
|
||||
};
|
||||
|
||||
const handleExtendedKeyUsageChange = (usage: CertExtendedKeyUsageType, newState: UsageState) => {
|
||||
const currentRequired = watchedExtendedKeyUsages.requiredUsages || [];
|
||||
const currentOptional = watchedExtendedKeyUsages.optionalUsages || [];
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
onExtendedKeyUsagesChange({ requiredUsages: newRequired, optionalUsages: newOptional });
|
||||
};
|
||||
|
||||
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-3">
|
||||
{KEY_USAGES.map((usage) => {
|
||||
const requiredUsages = Array.isArray(watchedKeyUsages?.requiredUsages)
|
||||
? watchedKeyUsages.requiredUsages
|
||||
: [];
|
||||
const optionalUsages = Array.isArray(watchedKeyUsages?.optionalUsages)
|
||||
? watchedKeyUsages.optionalUsages
|
||||
: [];
|
||||
|
||||
const currentState = getUsageState(usage, requiredUsages, optionalUsages);
|
||||
|
||||
return (
|
||||
<div key={usage} className="flex items-center justify-between p-2">
|
||||
<span className="text-mineshaft-300 text-sm capitalize">
|
||||
{formatUsageName(usage)}
|
||||
</span>
|
||||
<UsageToggle
|
||||
value={currentState}
|
||||
onChange={(newValue) => {
|
||||
toggleUsageState(
|
||||
usage,
|
||||
newValue,
|
||||
requiredUsages,
|
||||
optionalUsages,
|
||||
(u) => toggleKeyUsage(u, "required"),
|
||||
(u) => toggleKeyUsage(u, "optional")
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<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>
|
||||
</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-3">
|
||||
{EXTENDED_KEY_USAGES.map((usage) => {
|
||||
const requiredUsages = Array.isArray(watchedExtendedKeyUsages?.requiredUsages)
|
||||
? watchedExtendedKeyUsages.requiredUsages
|
||||
: [];
|
||||
const optionalUsages = Array.isArray(watchedExtendedKeyUsages?.optionalUsages)
|
||||
? watchedExtendedKeyUsages.optionalUsages
|
||||
: [];
|
||||
|
||||
const currentState = getUsageState(usage, requiredUsages, optionalUsages);
|
||||
|
||||
return (
|
||||
<div key={usage} className="flex items-center justify-between p-2">
|
||||
<span className="text-mineshaft-300 text-sm capitalize">
|
||||
{formatUsageName(usage)}
|
||||
</span>
|
||||
<UsageToggle
|
||||
value={currentState}
|
||||
onChange={(newValue) => {
|
||||
toggleUsageState(
|
||||
usage,
|
||||
newValue,
|
||||
requiredUsages,
|
||||
optionalUsages,
|
||||
(u) => toggleExtendedKeyUsage(u, "required"),
|
||||
(u) => toggleExtendedKeyUsage(u, "optional")
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<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)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+45
-14
@@ -1,33 +1,64 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { INCLUDE_OPTIONS, SAN_TYPES, SUBJECT_ATTRIBUTE_TYPES } from "./utils";
|
||||
import {
|
||||
CertDurationUnit,
|
||||
CertExtendedKeyUsageType,
|
||||
CertIncludeType,
|
||||
CertKeyUsageType,
|
||||
CertSubjectAlternativeNameType,
|
||||
CertSubjectAttributeType
|
||||
} from "./certificate-constants";
|
||||
|
||||
export const attributeSchema = z.object({
|
||||
type: z.enum(SUBJECT_ATTRIBUTE_TYPES),
|
||||
include: z.enum(INCLUDE_OPTIONS),
|
||||
value: z.array(z.string()).optional()
|
||||
type: z.nativeEnum(CertSubjectAttributeType),
|
||||
include: z.nativeEnum(CertIncludeType),
|
||||
value: z.array(z.string().min(1, "Value cannot be empty")).optional()
|
||||
});
|
||||
|
||||
export const sanSchema = z.object({
|
||||
type: z.enum(SAN_TYPES),
|
||||
include: z.enum(INCLUDE_OPTIONS),
|
||||
value: z.array(z.string()).optional()
|
||||
type: z.nativeEnum(CertSubjectAlternativeNameType),
|
||||
include: z.nativeEnum(CertIncludeType),
|
||||
value: z.array(z.string().min(1, "Value cannot be empty")).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(),
|
||||
attributes: z.array(attributeSchema).optional().refine((attributes) => {
|
||||
if (!attributes) return true;
|
||||
|
||||
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 [, attrs] of Object.entries(attributesByType)) {
|
||||
const mandatoryAttrs = attrs.filter(attr => attr.include === 'mandatory');
|
||||
|
||||
if (mandatoryAttrs.length > 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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"
|
||||
}),
|
||||
keyUsages: z
|
||||
.object({
|
||||
requiredUsages: z.array(z.string()).optional(),
|
||||
optionalUsages: z.array(z.string()).optional()
|
||||
requiredUsages: z.array(z.nativeEnum(CertKeyUsageType)).optional(),
|
||||
optionalUsages: z.array(z.nativeEnum(CertKeyUsageType)).optional()
|
||||
})
|
||||
.optional(),
|
||||
extendedKeyUsages: z
|
||||
.object({
|
||||
requiredUsages: z.array(z.string()).optional(),
|
||||
optionalUsages: z.array(z.string()).optional()
|
||||
requiredUsages: z.array(z.nativeEnum(CertExtendedKeyUsageType)).optional(),
|
||||
optionalUsages: z.array(z.nativeEnum(CertExtendedKeyUsageType)).optional()
|
||||
})
|
||||
.optional(),
|
||||
subjectAlternativeNames: z.array(sanSchema).optional(),
|
||||
@@ -36,13 +67,13 @@ export const templateSchema = z.object({
|
||||
maxDuration: z
|
||||
.object({
|
||||
value: z.number().min(1, "Duration must be at least 1"),
|
||||
unit: z.enum(["days", "months", "years"])
|
||||
unit: z.nativeEnum(CertDurationUnit)
|
||||
})
|
||||
.optional(),
|
||||
minDuration: z
|
||||
.object({
|
||||
value: z.number().min(1, "Duration must be at least 1"),
|
||||
unit: z.enum(["days", "months", "years"])
|
||||
unit: z.nativeEnum(CertDurationUnit)
|
||||
})
|
||||
.optional()
|
||||
})
|
||||
|
||||
+24
-44
@@ -1,47 +1,27 @@
|
||||
export const KEY_USAGES = [
|
||||
"digital_signature",
|
||||
"key_encipherment",
|
||||
"non_repudiation",
|
||||
"data_encipherment",
|
||||
"key_agreement",
|
||||
"key_cert_sign",
|
||||
"crl_sign",
|
||||
"encipher_only",
|
||||
"decipher_only"
|
||||
] as const;
|
||||
|
||||
export const EXTENDED_KEY_USAGES = [
|
||||
"client_auth",
|
||||
"server_auth",
|
||||
"code_signing",
|
||||
"email_protection",
|
||||
"ocsp_signing",
|
||||
"time_stamping"
|
||||
] as const;
|
||||
|
||||
export const SUBJECT_ATTRIBUTE_TYPES = [
|
||||
"common_name",
|
||||
"organization_name",
|
||||
"organization_unit",
|
||||
"locality",
|
||||
"state",
|
||||
"country",
|
||||
"email",
|
||||
"street_address",
|
||||
"postal_code"
|
||||
] as const;
|
||||
|
||||
export const SAN_TYPES = ["dns_name", "ip_address", "email", "uri"] as const;
|
||||
|
||||
export const INCLUDE_OPTIONS = ["mandatory", "optional", "prohibit"] as const;
|
||||
import {
|
||||
CertExtendedKeyUsageType,
|
||||
CertKeyUsageType,
|
||||
formatExtendedKeyUsage,
|
||||
formatKeyUsage
|
||||
} from "./certificate-constants";
|
||||
|
||||
export const formatUsageName = (usage: string): string => {
|
||||
try {
|
||||
if (Object.values(CertKeyUsageType).includes(usage as CertKeyUsageType)) {
|
||||
return formatKeyUsage(usage as CertKeyUsageType);
|
||||
}
|
||||
if (Object.values(CertExtendedKeyUsageType).includes(usage as CertExtendedKeyUsageType)) {
|
||||
return formatExtendedKeyUsage(usage as CertExtendedKeyUsageType);
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
return usage.replace(/_/g, " ");
|
||||
};
|
||||
|
||||
export const getUsageState = (
|
||||
usage: string,
|
||||
requiredUsages: string[],
|
||||
optionalUsages: string[]
|
||||
usage: CertKeyUsageType | CertExtendedKeyUsageType,
|
||||
requiredUsages: (CertKeyUsageType | CertExtendedKeyUsageType)[],
|
||||
optionalUsages: (CertKeyUsageType | CertExtendedKeyUsageType)[]
|
||||
): "required" | "optional" | undefined => {
|
||||
if (requiredUsages.includes(usage)) return "required";
|
||||
if (optionalUsages.includes(usage)) return "optional";
|
||||
@@ -49,12 +29,12 @@ export const getUsageState = (
|
||||
};
|
||||
|
||||
export const toggleUsageState = (
|
||||
usage: string,
|
||||
usage: CertKeyUsageType | CertExtendedKeyUsageType,
|
||||
newState: "required" | "optional" | undefined,
|
||||
currentRequiredUsages: string[],
|
||||
currentOptionalUsages: string[],
|
||||
toggleRequired: (usage: string) => void,
|
||||
toggleOptional: (usage: string) => void
|
||||
currentRequiredUsages: (CertKeyUsageType | CertExtendedKeyUsageType)[],
|
||||
currentOptionalUsages: (CertKeyUsageType | CertExtendedKeyUsageType)[],
|
||||
toggleRequired: (usage: CertKeyUsageType | CertExtendedKeyUsageType) => void,
|
||||
toggleOptional: (usage: CertKeyUsageType | CertExtendedKeyUsageType) => void
|
||||
) => {
|
||||
const isRequired = currentRequiredUsages.includes(usage);
|
||||
const isOptional = currentOptionalUsages.includes(usage);
|
||||
|
||||
+56
-1
@@ -7,6 +7,7 @@ import { Tooltip } from "@app/components/v2";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionCertificateActions,
|
||||
ProjectPermissionCertificateProfileActions,
|
||||
ProjectPermissionCmekActions,
|
||||
ProjectPermissionSub
|
||||
} from "@app/context";
|
||||
@@ -215,6 +216,13 @@ const PkiTemplatePolicyActionSchema = z.object({
|
||||
[ProjectPermissionPkiTemplateActions.IssueCert]: z.boolean().optional(),
|
||||
[ProjectPermissionPkiTemplateActions.ListCerts]: z.boolean().optional()
|
||||
});
|
||||
const CertificateProfilePolicyActionSchema = z.object({
|
||||
[ProjectPermissionCertificateProfileActions.Read]: z.boolean().optional(),
|
||||
[ProjectPermissionCertificateProfileActions.Create]: z.boolean().optional(),
|
||||
[ProjectPermissionCertificateProfileActions.Edit]: z.boolean().optional(),
|
||||
[ProjectPermissionCertificateProfileActions.Delete]: z.boolean().optional(),
|
||||
[ProjectPermissionCertificateProfileActions.IssueCert]: z.boolean().optional()
|
||||
});
|
||||
|
||||
const SecretEventsPolicyActionSchema = z.object({
|
||||
[ProjectPermissionSecretEventActions.SubscribeCreated]: z.boolean().optional(),
|
||||
@@ -385,6 +393,12 @@ export const projectRoleFormSchema = z.object({
|
||||
})
|
||||
.array()
|
||||
.default([]),
|
||||
[ProjectPermissionSub.CertificateProfiles]: CertificateProfilePolicyActionSchema.extend({
|
||||
inverted: z.boolean().optional(),
|
||||
conditions: ConditionSchema
|
||||
})
|
||||
.array()
|
||||
.default([]),
|
||||
[ProjectPermissionSub.SshCertificateAuthorities]: GeneralPolicyActionSchema.array().default(
|
||||
[]
|
||||
),
|
||||
@@ -465,6 +479,7 @@ export const isConditionalSubjects = (
|
||||
subject === ProjectPermissionSub.SecretRotation ||
|
||||
subject === ProjectPermissionSub.PkiSubscribers ||
|
||||
subject === ProjectPermissionSub.CertificateTemplates ||
|
||||
subject === ProjectPermissionSub.CertificateProfiles ||
|
||||
subject === ProjectPermissionSub.SecretSyncs ||
|
||||
subject === ProjectPermissionSub.PkiSyncs ||
|
||||
subject === ProjectPermissionSub.SecretEvents ||
|
||||
@@ -808,7 +823,9 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
||||
const canDelete = action.includes(ProjectPermissionActions.Delete);
|
||||
const canCreate = action.includes(ProjectPermissionActions.Create);
|
||||
|
||||
if (!formVal[subject]) formVal[subject] = [{}];
|
||||
if (!formVal[subject]) {
|
||||
formVal[subject] = [{ conditions: [] }];
|
||||
}
|
||||
if (canRead) formVal[subject as ProjectPermissionSub.Member]![0].read = true;
|
||||
if (canEdit) formVal[subject as ProjectPermissionSub.Member]![0].edit = true;
|
||||
if (canCreate) formVal[subject as ProjectPermissionSub.Member]![0].create = true;
|
||||
@@ -1138,6 +1155,33 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
||||
conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [],
|
||||
inverted
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
if (subject === ProjectPermissionSub.CertificateProfiles) {
|
||||
if (!formVal[subject]) formVal[subject] = [];
|
||||
|
||||
formVal[subject]!.push({
|
||||
[ProjectPermissionCertificateProfileActions.Edit]: action.includes(
|
||||
ProjectPermissionCertificateProfileActions.Edit
|
||||
),
|
||||
[ProjectPermissionCertificateProfileActions.Delete]: action.includes(
|
||||
ProjectPermissionCertificateProfileActions.Delete
|
||||
),
|
||||
[ProjectPermissionCertificateProfileActions.Create]: action.includes(
|
||||
ProjectPermissionCertificateProfileActions.Create
|
||||
),
|
||||
[ProjectPermissionCertificateProfileActions.Read]: action.includes(
|
||||
ProjectPermissionCertificateProfileActions.Read
|
||||
),
|
||||
[ProjectPermissionCertificateProfileActions.IssueCert]: action.includes(
|
||||
ProjectPermissionCertificateProfileActions.IssueCert
|
||||
),
|
||||
conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [],
|
||||
inverted
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (subject === ProjectPermissionSub.PamAccounts) {
|
||||
@@ -1522,6 +1566,16 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
|
||||
{ label: "List Certificates", value: ProjectPermissionPkiTemplateActions.ListCerts }
|
||||
]
|
||||
},
|
||||
[ProjectPermissionSub.CertificateProfiles]: {
|
||||
title: "Certificate Profiles",
|
||||
actions: [
|
||||
{ label: "Read", value: ProjectPermissionCertificateProfileActions.Read },
|
||||
{ label: "Create", value: ProjectPermissionCertificateProfileActions.Create },
|
||||
{ label: "Modify", value: ProjectPermissionCertificateProfileActions.Edit },
|
||||
{ label: "Remove", value: ProjectPermissionCertificateProfileActions.Delete },
|
||||
{ label: "Issue Certificates", value: ProjectPermissionCertificateProfileActions.IssueCert }
|
||||
]
|
||||
},
|
||||
[ProjectPermissionSub.SshCertificateAuthorities]: {
|
||||
title: "SSH Certificate Authorities",
|
||||
actions: [
|
||||
@@ -1869,6 +1923,7 @@ const CertificateManagerPermissionSubjects = (enabled = false) => ({
|
||||
[ProjectPermissionSub.PkiSyncs]: enabled,
|
||||
[ProjectPermissionSub.CertificateAuthorities]: enabled,
|
||||
[ProjectPermissionSub.CertificateTemplates]: enabled,
|
||||
[ProjectPermissionSub.CertificateProfiles]: enabled,
|
||||
[ProjectPermissionSub.Certificates]: enabled
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user