Merge pull request #4819 from Infisical/fix/pki-old-templates-issuance

Add new option on legacy templates to issue new certificates
This commit is contained in:
BlackMagiq
2025-11-06 16:45:21 -08:00
committed by GitHub
9 changed files with 102 additions and 43 deletions
+2 -2
View File
@@ -159,8 +159,8 @@ export type TCreateCertificateDTO = {
ttl: string; // string compatible with ms ttl: string; // string compatible with ms
notBefore?: string; notBefore?: string;
notAfter?: string; notAfter?: string;
keyUsages: CertKeyUsage[]; keyUsages: string[];
extendedKeyUsages: CertExtendedKeyUsage[]; extendedKeyUsages: string[];
}; };
export type TCreateCertificateResponse = { export type TCreateCertificateResponse = {
@@ -90,7 +90,12 @@ export const useCreateCertTemplateV2 = () => {
return data.certificateTemplate; return data.certificateTemplate;
}, },
onSuccess: (_, { projectId }) => { onSuccess: (_, { projectId }) => {
queryClient.invalidateQueries({ queryKey: certTemplateKeys.listTemplates({ projectId }) }); queryClient.invalidateQueries({
predicate: (query) => {
const [firstKey, queryProjectId] = query.queryKey;
return firstKey === "list-template" && queryProjectId === projectId;
}
});
} }
}); });
}; };
@@ -107,7 +112,12 @@ export const useUpdateCertTemplateV2 = () => {
return data.certificateTemplate; return data.certificateTemplate;
}, },
onSuccess: (_, { projectId }) => { onSuccess: (_, { projectId }) => {
queryClient.invalidateQueries({ queryKey: certTemplateKeys.listTemplates({ projectId }) }); queryClient.invalidateQueries({
predicate: (query) => {
const [firstKey, queryProjectId] = query.queryKey;
return firstKey === "list-template" && queryProjectId === projectId;
}
});
} }
}); });
}; };
@@ -127,7 +137,12 @@ export const useDeleteCertTemplateV2 = () => {
return data.certificateTemplate; return data.certificateTemplate;
}, },
onSuccess: (_, { projectId }) => { onSuccess: (_, { projectId }) => {
queryClient.invalidateQueries({ queryKey: certTemplateKeys.listTemplates({ projectId }) }); queryClient.invalidateQueries({
predicate: (query) => {
const [firstKey, queryProjectId] = query.queryKey;
return firstKey === "list-template" && queryProjectId === projectId;
}
});
} }
}); });
}; };
@@ -94,9 +94,9 @@ const createSchema = (shouldShowSubjectSection: boolean) => {
export type FormData = z.infer<ReturnType<typeof createSchema>>; export type FormData = z.infer<ReturnType<typeof createSchema>>;
type Props = { type Props = {
popUp: UsePopUpState<["certificateIssuance"]>; popUp: UsePopUpState<["issueCertificate"]>;
handlePopUpToggle: ( handlePopUpToggle: (
popUpName: keyof UsePopUpState<["certificateIssuance"]>, popUpName: keyof UsePopUpState<["issueCertificate"]>,
state?: boolean state?: boolean
) => void; ) => void;
profileId?: string; profileId?: string;
@@ -115,7 +115,7 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
const { currentProject } = useProject(); const { currentProject } = useProject();
const inputSerialNumber = const inputSerialNumber =
(popUp?.certificateIssuance?.data as { serialNumber: string })?.serialNumber || ""; (popUp?.issueCertificate?.data as { serialNumber: string })?.serialNumber || "";
const sanitizedSerialNumber = inputSerialNumber.replace(/[^a-fA-F0-9:]/g, ""); const sanitizedSerialNumber = inputSerialNumber.replace(/[^a-fA-F0-9:]/g, "");
const { data: cert } = useGetCert(sanitizedSerialNumber); const { data: cert } = useGetCert(sanitizedSerialNumber);
@@ -181,7 +181,7 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
} = useCertificateTemplate( } = useCertificateTemplate(
templateData, templateData,
actualSelectedProfile, actualSelectedProfile,
popUp?.certificateIssuance?.isOpen || false, popUp?.issueCertificate?.isOpen || false,
setValue, setValue,
watch watch
); );
@@ -227,10 +227,10 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
}, [cert, reset]); }, [cert, reset]);
useEffect(() => { useEffect(() => {
if (popUp?.certificateIssuance?.isOpen && profileId && !cert) { if (popUp?.issueCertificate?.isOpen && profileId && !cert) {
setValue("profileId", profileId); setValue("profileId", profileId);
} }
}, [popUp?.certificateIssuance?.isOpen, profileId, cert, setValue]); }, [popUp?.issueCertificate?.isOpen, profileId, cert, setValue]);
const onFormSubmit = useCallback( const onFormSubmit = useCallback(
async ({ async ({
@@ -332,9 +332,9 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
return ( return (
<Modal <Modal
isOpen={popUp?.certificateIssuance?.isOpen} isOpen={popUp?.issueCertificate?.isOpen}
onOpenChange={(isOpen) => { onOpenChange={(isOpen) => {
handlePopUpToggle("certificateIssuance", isOpen); handlePopUpToggle("issueCertificate", isOpen);
if (!isOpen) { if (!isOpen) {
resetAllState(); resetAllState();
} }
@@ -503,7 +503,7 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
colorSchema="secondary" colorSchema="secondary"
variant="plain" variant="plain"
onClick={() => { onClick={() => {
handlePopUpToggle("certificateIssuance", false); handlePopUpToggle("issueCertificate", false);
}} }}
> >
Cancel Cancel
@@ -75,6 +75,7 @@ export type FormData = z.infer<typeof schema>;
type Props = { type Props = {
popUp: UsePopUpState<["certificate"]>; popUp: UsePopUpState<["certificate"]>;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["certificate"]>, state?: boolean) => void; handlePopUpToggle: (popUpName: keyof UsePopUpState<["certificate"]>, state?: boolean) => void;
preselectedTemplate?: { id: string; name: string };
}; };
type TCertificateDetails = { type TCertificateDetails = {
@@ -86,7 +87,7 @@ type TCertificateDetails = {
const CERT_TEMPLATE_NONE_VALUE = "none"; const CERT_TEMPLATE_NONE_VALUE = "none";
export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => { export const CertificateModal = ({ popUp, handlePopUpToggle, preselectedTemplate }: Props) => {
const [certificateDetails, setCertificateDetails] = useState<TCertificateDetails | null>(null); const [certificateDetails, setCertificateDetails] = useState<TCertificateDetails | null>(null);
const { currentProject } = useProject(); const { currentProject } = useProject();
const { data: cert } = useGetCert( const { data: cert } = useGetCert(
@@ -147,13 +148,15 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
(cert.extendedKeyUsages || []).map((name) => [name, true]) (cert.extendedKeyUsages || []).map((name) => [name, true])
) )
}); });
} else { } else if (popUp?.certificate?.isOpen) {
const templateId = preselectedTemplate?.id || CERT_TEMPLATE_NONE_VALUE;
reset({ reset({
caId: "", caId: "",
commonName: "", commonName: "",
subjectAltNames: "", subjectAltNames: "",
ttl: "", ttl: "",
certificateTemplateId: CERT_TEMPLATE_NONE_VALUE, certificateTemplateId: templateId,
keyUsages: { keyUsages: {
[CertKeyUsage.DIGITAL_SIGNATURE]: true, [CertKeyUsage.DIGITAL_SIGNATURE]: true,
[CertKeyUsage.KEY_ENCIPHERMENT]: true [CertKeyUsage.KEY_ENCIPHERMENT]: true
@@ -161,7 +164,7 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
extendedKeyUsages: {} extendedKeyUsages: {}
}); });
} }
}, [cert]); }, [cert, preselectedTemplate, popUp?.certificate?.isOpen]);
useEffect(() => { useEffect(() => {
if (!cert && selectedCertTemplate) { if (!cert && selectedCertTemplate) {
@@ -198,10 +201,14 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
ttl, ttl,
keyUsages: Object.entries(keyUsages) keyUsages: Object.entries(keyUsages)
.filter(([, value]) => value) .filter(([, value]) => value)
.map(([key]) => key as CertKeyUsage), .map(([key]) =>
key === CertKeyUsage.CRL_SIGN
? "cRLSign"
: key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase())
),
extendedKeyUsages: Object.entries(extendedKeyUsages) extendedKeyUsages: Object.entries(extendedKeyUsages)
.filter(([, value]) => value) .filter(([, value]) => value)
.map(([key]) => key as CertExtendedKeyUsage) .map(([key]) => key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()))
}); });
reset(); reset();
@@ -269,15 +276,25 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
isRequired isRequired
> >
<Select <Select
defaultValue={field.value} value={field.value}
{...field}
onValueChange={(e) => onChange(e)} onValueChange={(e) => onChange(e)}
className="w-full" className="w-full"
isDisabled={Boolean(cert)} isDisabled={Boolean(cert) || Boolean(preselectedTemplate)}
> >
<SelectItem value={CERT_TEMPLATE_NONE_VALUE} key="cert-template-none"> <SelectItem value={CERT_TEMPLATE_NONE_VALUE} key="cert-template-none">
None None
</SelectItem> </SelectItem>
{preselectedTemplate &&
!templatesData?.certificateTemplates?.find(
(t) => t.id === preselectedTemplate.id
) && (
<SelectItem
value={preselectedTemplate.id}
key={`cert-template-preselected-${preselectedTemplate.id}`}
>
{preselectedTemplate.name}
</SelectItem>
)}
{(templatesData?.certificateTemplates || []).map(({ id, name }) => ( {(templatesData?.certificateTemplates || []).map(({ id, name }) => (
<SelectItem value={id} key={`cert-template-${id}`}> <SelectItem value={id} key={`cert-template-${id}`}>
{name} {name}
@@ -17,7 +17,6 @@ import { CertificateImportModal } from "./CertificateImportModal";
import { CertificateIssuanceModal } from "./CertificateIssuanceModal"; import { CertificateIssuanceModal } from "./CertificateIssuanceModal";
import { CertificateManagePkiSyncsModal } from "./CertificateManagePkiSyncsModal"; import { CertificateManagePkiSyncsModal } from "./CertificateManagePkiSyncsModal";
import { CertificateManageRenewalModal } from "./CertificateManageRenewalModal"; import { CertificateManageRenewalModal } from "./CertificateManageRenewalModal";
import { CertificateModal } from "./CertificateModal";
import { CertificateRenewalModal } from "./CertificateRenewalModal"; import { CertificateRenewalModal } from "./CertificateRenewalModal";
import { CertificateRevocationModal } from "./CertificateRevocationModal"; import { CertificateRevocationModal } from "./CertificateRevocationModal";
import { CertificatesTable } from "./CertificatesTable"; import { CertificatesTable } from "./CertificatesTable";
@@ -26,12 +25,8 @@ export const CertificatesSection = () => {
const { currentProject } = useProject(); const { currentProject } = useProject();
const { mutateAsync: deleteCert } = useDeleteCert(); const { mutateAsync: deleteCert } = useDeleteCert();
// TODO: Use subscription.pkiLegacyTemplates to block legacy templates creation
const isLegacyTemplatesEnabled = true;
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"certificateIssuance", "issueCertificate",
"certificate",
"certificateImport", "certificateImport",
"certificateCert", "certificateCert",
"deleteCertificate", "deleteCertificate",
@@ -76,9 +71,7 @@ export const CertificatesSection = () => {
colorSchema="primary" colorSchema="primary"
type="submit" type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />} leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => onClick={() => handlePopUpOpen("issueCertificate")}
handlePopUpOpen(isLegacyTemplatesEnabled ? "certificate" : "certificateIssuance")
}
isDisabled={!isAllowed} isDisabled={!isAllowed}
> >
Issue Issue
@@ -88,11 +81,7 @@ export const CertificatesSection = () => {
</ProjectPermissionCan> </ProjectPermissionCan>
</div> </div>
<CertificatesTable handlePopUpOpen={handlePopUpOpen} /> <CertificatesTable handlePopUpOpen={handlePopUpOpen} />
{isLegacyTemplatesEnabled ? ( <CertificateIssuanceModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<CertificateModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
) : (
<CertificateIssuanceModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
)}
<CertificateImportModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} /> <CertificateImportModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<CertificateCertModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} /> <CertificateCertModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<CertificateManageRenewalModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} /> <CertificateManageRenewalModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
@@ -64,7 +64,7 @@ type Props = {
handlePopUpOpen: ( handlePopUpOpen: (
popUpName: keyof UsePopUpState< popUpName: keyof UsePopUpState<
[ [
"certificate", "issueCertificate",
"deleteCertificate", "deleteCertificate",
"revokeCertificate", "revokeCertificate",
"certificateCert", "certificateCert",
@@ -297,7 +297,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50" !isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)} )}
onClick={async () => onClick={async () =>
handlePopUpOpen("certificate", { handlePopUpOpen("issueCertificate", {
serialNumber: certificate.serialNumber serialNumber: certificate.serialNumber
}) })
} }
@@ -5,6 +5,7 @@ import {
faCertificate, faCertificate,
faCog, faCog,
faEllipsis, faEllipsis,
faFileContract,
faPencil, faPencil,
faPlus, faPlus,
faTrash faTrash
@@ -40,6 +41,7 @@ import {
Tr Tr
} from "@app/components/v2"; } from "@app/components/v2";
import { import {
ProjectPermissionCertificateActions,
ProjectPermissionPkiTemplateActions, ProjectPermissionPkiTemplateActions,
ProjectPermissionSub, ProjectPermissionSub,
useProject, useProject,
@@ -50,6 +52,7 @@ import { useDeleteCertTemplateV2 } from "@app/hooks/api";
import { useListCertificateTemplates } from "@app/hooks/api/certificateTemplates/queries"; import { useListCertificateTemplates } from "@app/hooks/api/certificateTemplates/queries";
import { ProjectType } from "@app/hooks/api/projects/types"; import { ProjectType } from "@app/hooks/api/projects/types";
import { CertificateModal } from "../CertificatesPage/components/CertificateModal";
import { CertificateTemplateEnrollmentModal } from "../CertificatesPage/components/CertificateTemplateEnrollmentModal"; import { CertificateTemplateEnrollmentModal } from "../CertificatesPage/components/CertificateTemplateEnrollmentModal";
import { PkiTemplateForm } from "./components/PkiTemplateForm"; import { PkiTemplateForm } from "./components/PkiTemplateForm";
@@ -64,7 +67,8 @@ export const PkiTemplateListPage = () => {
"certificateTemplate", "certificateTemplate",
"deleteTemplate", "deleteTemplate",
"enrollmentOptions", "enrollmentOptions",
"estUpgradePlan" "estUpgradePlan",
"certificateFromTemplate"
] as const); ] as const);
const { subscription } = useSubscription(); const { subscription } = useSubscription();
@@ -160,6 +164,27 @@ export const PkiTemplateListPage = () => {
</div> </div>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1"> <DropdownMenuContent align="start" className="p-1">
<ProjectPermissionCan
I={ProjectPermissionCertificateActions.Create}
a={ProjectPermissionSub.Certificates}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed &&
"pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("certificateFromTemplate", template);
}}
disabled={!isAllowed}
icon={<FontAwesomeIcon icon={faFileContract} />}
>
Issue Certificate
</DropdownMenuItem>
)}
</ProjectPermissionCan>
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionPkiTemplateActions.Edit} I={ProjectPermissionPkiTemplateActions.Edit}
a={ProjectPermissionSub.CertificateTemplates} a={ProjectPermissionSub.CertificateTemplates}
@@ -284,6 +309,16 @@ export const PkiTemplateListPage = () => {
</ModalContent> </ModalContent>
</Modal> </Modal>
<CertificateTemplateEnrollmentModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} /> <CertificateTemplateEnrollmentModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<CertificateModal
popUp={{
certificate: {
isOpen: popUp.certificateFromTemplate.isOpen,
data: popUp.certificateFromTemplate.data
}
}}
handlePopUpToggle={(_, state) => handlePopUpToggle("certificateFromTemplate", state)}
preselectedTemplate={popUp.certificateFromTemplate.data}
/>
</div> </div>
<UpgradePlanModal <UpgradePlanModal
isOpen={popUp.estUpgradePlan.isOpen} isOpen={popUp.estUpgradePlan.isOpen}
@@ -44,7 +44,7 @@ export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) =
const { data: caData } = useGetCaById(profile.caId); const { data: caData } = useGetCaById(profile.caId);
const { popUp, handlePopUpToggle } = usePopUp(["certificateIssuance"] as const); const { popUp, handlePopUpToggle } = usePopUp(["issueCertificate"] as const);
const [isIdCopied, setIsIdCopied] = useToggle(false); const [isIdCopied, setIsIdCopied] = useToggle(false);
@@ -147,7 +147,7 @@ export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) =
<DropdownMenuItem <DropdownMenuItem
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
handlePopUpToggle("certificateIssuance"); handlePopUpToggle("issueCertificate");
}} }}
icon={<FontAwesomeIcon icon={faPlus} />} icon={<FontAwesomeIcon icon={faPlus} />}
> >
@@ -251,7 +251,10 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
const { control, handleSubmit, reset, watch, setValue, formState } = useForm<FormData>({ const { control, handleSubmit, reset, watch, setValue, formState } = useForm<FormData>({
resolver: zodResolver(templateSchema), resolver: zodResolver(templateSchema),
defaultValues: getDefaultValues() defaultValues: getDefaultValues(),
mode: "onChange",
reValidateMode: "onChange",
criteriaMode: "all"
}); });
useEffect(() => { useEffect(() => {