Merge branch 'main' of https://github.com/Infisical/infisical into chore/external-kms-api-refactor

This commit is contained in:
Piyush Gupta
2025-12-04 01:27:37 +05:30
65 changed files with 5215 additions and 895 deletions
+4 -2
View File
@@ -189,10 +189,12 @@ export const useGetCaCertTemplates = (caId: string) => {
export const useGetAzureAdcsTemplates = ({
caId,
projectId
projectId,
isAzureAdcsCa
}: {
caId: string;
projectId: string;
isAzureAdcsCa: boolean;
}) => {
return useQuery({
queryKey: caKeys.getAzureAdcsTemplates(caId, projectId),
@@ -202,6 +204,6 @@ export const useGetAzureAdcsTemplates = ({
}>(`/api/v1/cert-manager/ca/azure-ad-cs/${caId}/templates?projectId=${projectId}`);
return data;
},
enabled: Boolean(caId && projectId)
enabled: Boolean(caId && projectId && isAzureAdcsCa)
});
};
@@ -22,14 +22,25 @@ export type TCertificateProfile = {
apiConfigId?: string;
createdAt: string;
updatedAt: string;
externalConfigs?: Record<string, unknown> | null;
certificateAuthority?: {
id: string;
projectId?: string;
status: string;
name: string;
isExternal?: boolean;
externalType?: string | null;
};
};
export type TCertificateProfileWithDetails = TCertificateProfile & {
certificateAuthority?: {
id: string;
projectId: string;
projectId?: string;
status: string;
name: string;
isExternal?: boolean;
externalType?: string | null;
};
certificateTemplate?: {
id: string;
@@ -72,6 +83,7 @@ export type TCreateCertificateProfileDTO = {
renewBeforeDays?: number;
};
acmeConfig?: unknown;
externalConfigs?: Record<string, unknown> | null;
};
export type TUpdateCertificateProfileDTO = {
@@ -90,6 +102,7 @@ export type TUpdateCertificateProfileDTO = {
renewBeforeDays?: number;
};
acmeConfig?: unknown;
externalConfigs?: Record<string, unknown> | null;
};
export type TDeleteCertificateProfileDTO = {
@@ -13,6 +13,8 @@ import {
TRenewCertificateDTO,
TRenewCertificateResponse,
TRevokeCertDTO,
TUnifiedCertificateIssuanceDTO,
TUnifiedCertificateIssuanceResponse,
TUpdateRenewalConfigDTO
} from "./types";
@@ -185,3 +187,31 @@ export const useDownloadCertPkcs12 = () => {
}
});
};
export const useUnifiedCertificateIssuance = () => {
const queryClient = useQueryClient();
return useMutation<TUnifiedCertificateIssuanceResponse, object, TUnifiedCertificateIssuanceDTO>({
mutationFn: async (body) => {
const { projectSlug, ...requestData } = body;
const { data } = await apiRequest.post<TUnifiedCertificateIssuanceResponse>(
"/api/v1/cert-manager/certificates",
requestData
);
return data;
},
onSuccess: (_, { projectSlug }) => {
queryClient.invalidateQueries({
queryKey: ["certificate-profiles", "list"]
});
queryClient.invalidateQueries({
queryKey: pkiSubscriberKeys.allPkiSubscriberCertificates()
});
queryClient.invalidateQueries({
queryKey: projectKeys.allProjectCertificates()
});
queryClient.invalidateQueries({
queryKey: projectKeys.forProjectCertificates(projectSlug)
});
}
});
};
@@ -7,7 +7,11 @@ import { TCertificate } from "./types";
export const certKeys = {
getCertById: (serialNumber: string) => [{ serialNumber }, "cert"],
getCertBody: (serialNumber: string) => [{ serialNumber }, "certBody"],
getCertBundle: (serialNumber: string) => [{ serialNumber }, "certBundle"]
getCertBundle: (serialNumber: string) => [{ serialNumber }, "certBundle"],
getCertificateRequest: (requestId: string, projectSlug: string) => [
{ requestId, projectSlug },
"certificateRequest"
]
};
export const useGetCert = (serialNumber: string) => {
@@ -64,6 +64,7 @@ export type TRenewCertificateResponse = {
serialNumber: string;
certificateId: string;
projectId: string;
certificateRequestId?: string;
};
export type TUpdateRenewalConfigDTO = {
@@ -79,3 +80,59 @@ export type TDownloadPkcs12DTO = {
password: string;
alias: string;
};
export type TUnifiedCertificateIssuanceDTO = {
projectSlug: string;
profileId: string;
projectId: string;
csr?: string;
attributes?: {
commonName?: string;
keyUsages?: string[];
extendedKeyUsages?: string[];
altNames?: Array<{
type: string;
value: string;
}>;
signatureAlgorithm: string;
keyAlgorithm: string;
subjectAlternativeNames?: Array<{
type: string;
value: string;
}>;
ttl: string;
notBefore?: string;
notAfter?: string;
};
removeRootsFromChain?: boolean;
};
export type TUnifiedCertificateResponse = {
certificate: {
certificate: string;
issuingCaCertificate: string;
certificateChain: string;
privateKey?: string;
serialNumber: string;
certificateId: string;
};
certificateRequestId: string;
};
export type TCertificateRequestResponse = {
certificateRequestId: string;
status: "pending" | "issued" | "failed";
projectId: string;
};
export type TUnifiedCertificateIssuanceResponse =
| TUnifiedCertificateResponse
| TCertificateRequestResponse;
export type TCertificateRequestDetails = {
status: "pending" | "issued" | "failed";
certificate: TCertificate | null;
errorMessage: string | null;
createdAt: string;
updatedAt: string;
};
@@ -20,9 +20,9 @@ import {
} from "@app/components/v2";
import { useProject } from "@app/context";
import { useGetCert } from "@app/hooks/api";
import { useCreateCertificateV3 } from "@app/hooks/api/ca";
import { EnrollmentType, useListCertificateProfiles } from "@app/hooks/api/certificateProfiles";
import { CertExtendedKeyUsage, CertKeyUsage } from "@app/hooks/api/certificates/enums";
import { useUnifiedCertificateIssuance } from "@app/hooks/api/certificates/mutations";
import { useGetCertificateTemplateV2ById } from "@app/hooks/api/certificateTemplates/queries";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { CertSubjectAlternativeNameType } from "@app/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/shared/certificate-constants";
@@ -103,10 +103,11 @@ type Props = {
};
type TCertificateDetails = {
serialNumber: string;
certificate: string;
certificateChain: string;
privateKey: string;
serialNumber?: string;
certificate?: string;
certificateChain?: string;
privateKey?: string;
issuingCaCertificate?: string;
};
export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }: Props) => {
@@ -122,12 +123,11 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
const { data: profilesData } = useListCertificateProfiles({
projectId: currentProject?.id || "",
enrollmentType: EnrollmentType.API
enrollmentType: EnrollmentType.API,
includeConfigs: true
});
const { mutateAsync: createCertificate } = useCreateCertificateV3({
projectId: currentProject?.id
});
const { mutateAsync: issueCertificate } = useUnifiedCertificateIssuance();
const formResolver = useMemo(() => {
return zodResolver(createSchema(shouldShowSubjectSection));
@@ -243,7 +243,7 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
keyUsages,
extendedKeyUsages
}: FormData) => {
if (!currentProject?.slug) {
if (!currentProject?.slug || !currentProject?.id) {
createNotification({
text: "Project not found. Please refresh and try again.",
type: "error"
@@ -275,44 +275,70 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
}
}
const certificateRequest: any = {
profileId: formProfileId,
projectSlug: currentProject.slug,
ttl,
signatureAlgorithm,
keyAlgorithm,
keyUsages: filterUsages(keyUsages) as CertKeyUsage[],
extendedKeyUsages: filterUsages(extendedKeyUsages) as CertExtendedKeyUsage[]
};
try {
// Prepare unified request
const request: any = {
profileId: formProfileId,
projectSlug: currentProject.slug,
projectId: currentProject.id,
attributes: {
ttl,
signatureAlgorithm: signatureAlgorithm || "",
keyAlgorithm: keyAlgorithm || "",
keyUsages: filterUsages(keyUsages) as CertKeyUsage[],
extendedKeyUsages: filterUsages(extendedKeyUsages) as CertExtendedKeyUsage[]
}
};
if (constraints.shouldShowSubjectSection && commonName) {
certificateRequest.commonName = commonName;
}
if (constraints.shouldShowSanSection && subjectAltNames && subjectAltNames.length > 0) {
const formattedSans = formatSubjectAltNames(subjectAltNames);
if (formattedSans && formattedSans.length > 0) {
certificateRequest.altNames = formattedSans;
if (constraints.shouldShowSubjectSection && commonName) {
request.attributes.commonName = commonName;
}
if (constraints.shouldShowSanSection && subjectAltNames && subjectAltNames.length > 0) {
const formattedSans = formatSubjectAltNames(subjectAltNames);
if (formattedSans && formattedSans.length > 0) {
request.attributes.altNames = formattedSans;
}
}
const response = await issueCertificate(request);
// Handle certificate issuance response
if ("certificate" in response && response.certificate) {
const certData = response.certificate;
const certificateDetailsToSet = {
serialNumber: certData.serialNumber || "",
certificate: certData.certificate || "",
certificateChain: certData.certificateChain || "",
privateKey: certData.privateKey || "",
issuingCaCertificate: certData.issuingCaCertificate || ""
};
setCertificateDetails(certificateDetailsToSet);
createNotification({
text: "Successfully created certificate",
type: "success"
});
} else {
// Certificate request - async processing
createNotification({
text: `Certificate request submitted successfully. This may take a few minutes to process. Certificate Request ID: ${response.certificateRequestId}`,
type: "success"
});
handlePopUpToggle("issueCertificate", false);
}
} catch (error) {
createNotification({
text: `Failed to request certificate: ${(error as Error)?.message || "Unknown error"}`,
type: "error"
});
}
const { serialNumber, certificate, certificateChain, privateKey } =
await createCertificate(certificateRequest);
setCertificateDetails({
serialNumber,
certificate,
certificateChain,
privateKey
});
createNotification({
text: "Successfully created certificate",
type: "success"
});
},
[
currentProject?.slug,
createCertificate,
issueCertificate,
constraints.shouldShowSubjectSection,
constraints.shouldShowSanSection
]
@@ -321,13 +347,13 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
const getModalTitle = () => {
if (certificateDetails) return "Certificate Created Successfully";
if (cert) return "Certificate Details";
return "Issue New Certificate";
return "Request New Certificate";
};
const getModalSubTitle = () => {
if (certificateDetails) return "Certificate has been successfully created and is ready for use";
if (cert) return "View certificate information";
return "Issue a new certificate using a certificate profile";
return "Request a new certificate using a certificate profile";
};
return (
@@ -343,10 +369,10 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
<ModalContent title={getModalTitle()} subTitle={getModalSubTitle()}>
{certificateDetails && (
<CertificateContent
serialNumber={certificateDetails.serialNumber}
certificate={certificateDetails.certificate}
certificateChain={certificateDetails.certificateChain}
privateKey={certificateDetails.privateKey}
serialNumber={certificateDetails.serialNumber!}
certificate={certificateDetails.certificate!}
certificateChain={certificateDetails.certificateChain!}
privateKey={certificateDetails.privateKey!}
/>
)}
{cert && (
@@ -498,7 +524,7 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
isLoading={isSubmitting}
isDisabled={isSubmitting || (!actualSelectedProfile && !profileId)}
>
{cert ? "Update" : "Issue Certificate"}
{cert ? "Update" : "Request Certificate"}
</Button>
<Button
colorSchema="secondary"
@@ -20,12 +20,16 @@ export const CertificateRenewalModal = ({ popUp, handlePopUpToggle }: Props) =>
const onRenewConfirm = async () => {
const { certificateId } = popUp.renewCertificate.data as { certificateId: string };
await renewCertificate({
const result = await renewCertificate({
certificateId
});
const notificationText = result.certificateRequestId
? `Certificate renewal initiated successfully. Certificate Request ID: ${result.certificateRequestId}`
: "Certificate renewed successfully";
createNotification({
text: "Certificate renewed successfully",
text: notificationText,
type: "success"
});
@@ -125,7 +125,7 @@ export const CertificatesSection = () => {
onClick={() => handlePopUpOpen("issueCertificate")}
isDisabled={!isAllowed}
>
Issue
Request
</Button>
</div>
)}
@@ -213,7 +213,8 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
// Fetch Azure ADCS templates when Azure CA is selected
const { data: azureTemplates } = useGetAzureAdcsTemplates({
caId: selectedCa?.type === CaType.AZURE_AD_CS ? selectedCaId : "",
projectId
projectId,
isAzureAdcsCa: true
});
// Initialize form with ALL subscriber data including template
@@ -9,6 +9,7 @@ import { createNotification } from "@app/components/notifications";
import {
Button,
Checkbox,
FilterableSelect,
FormControl,
Input,
Modal,
@@ -19,7 +20,8 @@ import {
Tooltip
} from "@app/components/v2";
import { useProject, useSubscription } from "@app/context";
import { useListCasByProjectId } from "@app/hooks/api/ca/queries";
import { CaType } from "@app/hooks/api/ca/enums";
import { useGetAzureAdcsTemplates, useListCasByProjectId } from "@app/hooks/api/ca/queries";
import {
EnrollmentType,
IssuerType,
@@ -77,7 +79,12 @@ const createSchema = z
renewBeforeDays: z.number().min(1).max(365).optional()
})
.optional(),
acmeConfig: z.object({}).optional()
acmeConfig: z.object({}).optional(),
externalConfigs: z
.object({
template: z.string().min(1, "Azure ADCS template is required")
})
.optional()
})
.refine(
(data) => {
@@ -212,7 +219,12 @@ const editSchema = z
renewBeforeDays: z.number().min(1).max(365).optional()
})
.optional(),
acmeConfig: z.object({}).optional()
acmeConfig: z.object({}).optional(),
externalConfigs: z
.object({
template: z.string().optional()
})
.optional()
})
.refine(
(data) => {
@@ -339,7 +351,7 @@ export const CreateProfileModal = ({
const { currentProject } = useProject();
const { subscription } = useSubscription();
const { data: caData } = useListCasByProjectId(currentProject?.id || "");
const { data: allCaData } = useListCasByProjectId(currentProject?.id || "");
const { data: templateData } = useListCertificateTemplatesV2({
projectId: currentProject?.id || "",
limit: 100,
@@ -351,9 +363,23 @@ export const CreateProfileModal = ({
const isEdit = mode === "edit" && profile;
const certificateAuthorities = caData || [];
const certificateAuthorities = (allCaData || []).map((ca) => ({
...ca,
groupType: ca.type === "internal" ? "internal" : "external"
}));
const certificateTemplates = templateData?.certificateTemplates || [];
const getGroupHeaderLabel = (groupType: "internal" | "external") => {
switch (groupType) {
case "internal":
return "Internal CAs";
case "external":
return "External CAs";
default:
return "";
}
};
const { control, handleSubmit, reset, watch, setValue, formState } = useForm<FormData>({
resolver: zodResolver(isEdit ? editSchema : createSchema),
defaultValues: isEdit
@@ -380,7 +406,17 @@ export const CreateProfileModal = ({
renewBeforeDays: profile.apiConfig?.renewBeforeDays || 30
}
: undefined,
acmeConfig: profile.enrollmentType === EnrollmentType.ACME ? {} : undefined
acmeConfig: profile.enrollmentType === EnrollmentType.ACME ? {} : undefined,
externalConfigs: profile.externalConfigs
? {
template:
typeof profile.externalConfigs === "object" &&
profile.externalConfigs !== null &&
typeof profile.externalConfigs.template === "string"
? profile.externalConfigs.template
: ""
}
: undefined
}
: {
slug: "",
@@ -393,15 +429,28 @@ export const CreateProfileModal = ({
autoRenew: false,
renewBeforeDays: 30
},
acmeConfig: {}
acmeConfig: {},
externalConfigs: undefined
}
});
const watchedEnrollmentType = watch("enrollmentType");
const watchedIssuerType = watch("issuerType");
const watchedCertificateAuthorityId = watch("certificateAuthorityId");
const watchedDisableBootstrapValidation = watch("estConfig.disableBootstrapCaValidation");
const watchedAutoRenew = watch("apiConfig.autoRenew");
// Get the selected CA to check if it's Azure ADCS
const selectedCa = certificateAuthorities.find((ca) => ca.id === watchedCertificateAuthorityId);
const isAzureAdcsCa = selectedCa?.type === CaType.AZURE_AD_CS;
// Fetch Azure ADCS templates if needed
const { data: azureAdcsTemplatesData } = useGetAzureAdcsTemplates({
caId: watchedCertificateAuthorityId || "",
projectId: currentProject?.id || "",
isAzureAdcsCa
});
useEffect(() => {
if (isEdit && profile) {
reset({
@@ -427,10 +476,38 @@ export const CreateProfileModal = ({
renewBeforeDays: profile.apiConfig?.renewBeforeDays || 30
}
: undefined,
acmeConfig: profile.enrollmentType === EnrollmentType.ACME ? {} : undefined
acmeConfig: profile.enrollmentType === EnrollmentType.ACME ? {} : undefined,
externalConfigs: profile.externalConfigs
? {
template:
typeof profile.externalConfigs === "object" &&
profile.externalConfigs !== null &&
typeof profile.externalConfigs.template === "string"
? profile.externalConfigs.template
: ""
}
: undefined
});
}
}, [isEdit, profile, reset]);
}, [isEdit, profile, reset, allCaData]);
// Additional effect to reset external configs when Azure ADCS templates are loaded
useEffect(() => {
if (
isEdit &&
profile &&
isAzureAdcsCa &&
azureAdcsTemplatesData?.templates &&
profile.externalConfigs &&
typeof profile.externalConfigs === "object" &&
profile.externalConfigs !== null &&
typeof profile.externalConfigs.template === "string"
) {
// Re-set the external configs to ensure the template value is properly set
// after the Azure ADCS templates have been loaded
setValue("externalConfigs.template", profile.externalConfigs.template);
}
}, [isEdit, profile, isAzureAdcsCa, azureAdcsTemplatesData, setValue]);
const onFormSubmit = async (data: FormData) => {
if (!isEdit && !subscription?.pkiAcme && data.enrollmentType === EnrollmentType.ACME) {
@@ -444,6 +521,18 @@ export const CreateProfileModal = ({
if (!currentProject?.id && !isEdit) return;
// Validate Azure ADCS template requirement
if (
isAzureAdcsCa &&
(!data.externalConfigs?.template || data.externalConfigs.template.trim() === "")
) {
createNotification({
text: "Azure ADCS Certificate Authority requires a template to be specified",
type: "error"
});
return;
}
if (isEdit) {
const updateData: TUpdateCertificateProfileDTO = {
profileId: profile.id,
@@ -460,6 +549,11 @@ export const CreateProfileModal = ({
updateData.acmeConfig = data.acmeConfig;
}
// Add external configs if present
if (data.externalConfigs) {
updateData.externalConfigs = data.externalConfigs;
}
await updateProfile.mutateAsync(updateData);
} else {
if (!currentProject?.id) {
@@ -491,6 +585,11 @@ export const CreateProfileModal = ({
createData.acmeConfig = data.acmeConfig;
}
// Add external configs if present
if (data.externalConfigs) {
createData.externalConfigs = data.externalConfigs;
}
await createProfile.mutateAsync(createData);
}
@@ -587,30 +686,82 @@ export const CreateProfileModal = ({
<Controller
control={control}
name="certificateAuthorityId"
render={({ field: { onChange, value, ...field }, fieldState: { error } }) => (
render={({ field: { onChange, value }, fieldState: { error } }) => (
<FormControl
label="Issuing CA"
isRequired
isError={Boolean(error)}
errorText={error?.message}
>
<Select
{...field}
value={value || undefined}
onValueChange={onChange}
<FilterableSelect
value={certificateAuthorities.find((ca) => ca.id === value) || null}
onChange={(selectedCaValue) => {
if (Array.isArray(selectedCaValue)) {
onChange(selectedCaValue[0]?.id || "");
} else if (
selectedCaValue &&
typeof selectedCaValue === "object" &&
"id" in selectedCaValue
) {
onChange(selectedCaValue.id || "");
} else {
onChange("");
}
}}
getOptionLabel={(ca) =>
ca.type === "internal" && ca.configuration.friendlyName
? ca.configuration.friendlyName
: ca.name
}
getOptionValue={(ca) => ca.id}
options={certificateAuthorities}
groupBy="groupType"
getGroupHeaderLabel={getGroupHeaderLabel}
placeholder="Select a certificate authority"
className="w-full"
position="popper"
isDisabled={Boolean(isEdit)}
>
{certificateAuthorities.map((ca) => (
<SelectItem key={ca.id} value={ca.id}>
{ca.type === "internal" && ca.configuration.friendlyName
? ca.configuration.friendlyName
: ca.name}
</SelectItem>
))}
</Select>
className="w-full"
/>
</FormControl>
)}
/>
)}
{/* Azure ADCS Template Selection */}
{isAzureAdcsCa && (
<Controller
control={control}
name="externalConfigs.template"
render={({ field: { onChange, value }, fieldState: { error } }) => (
<FormControl
label="Windows ADCS Template"
isRequired
isError={Boolean(error)}
errorText={error?.message}
>
<FilterableSelect
value={
azureAdcsTemplatesData?.templates.find((template) => template.id === value) ||
null
}
onChange={(selectedTemplate) => {
if (Array.isArray(selectedTemplate)) {
onChange(selectedTemplate[0]?.id || "");
} else if (
selectedTemplate &&
typeof selectedTemplate === "object" &&
"id" in selectedTemplate
) {
onChange(selectedTemplate.id || "");
} else {
onChange("");
}
}}
getOptionLabel={(template) => template.name}
getOptionValue={(template) => template.id}
options={azureAdcsTemplatesData?.templates || []}
placeholder="Select an Azure ADCS certificate template"
className="w-full"
/>
</FormControl>
)}
/>
@@ -1,3 +1,4 @@
/* eslint-disable no-nested-ternary */
import { useCallback } from "react";
import {
faCheck,
@@ -123,9 +124,11 @@ export const ProfileRow = ({
<span className="text-sm text-mineshaft-300">
{profile.issuerType === IssuerType.SELF_SIGNED
? "Self-signed"
: caData?.configuration.friendlyName ||
caData?.configuration.commonName ||
profile.caId}
: profile.certificateAuthority?.isExternal
? profile.certificateAuthority.name
: caData?.configuration.friendlyName ||
caData?.configuration.commonName ||
profile.caId}
</span>
</Td>
<Td>
@@ -179,7 +182,7 @@ export const ProfileRow = ({
}}
icon={<FontAwesomeIcon icon={faPlus} className="w-3" />}
>
Issue Certificate
Request Certificate
</DropdownMenuItem>
)}
{canDeleteProfile && (
@@ -22,6 +22,7 @@ import {
Tooltip,
Tr
} from "@app/components/v2";
import { CopyButton } from "@app/components/v2/CopyButton";
import {
OrgPermissionIdentityActions,
OrgPermissionSubjects,
@@ -153,6 +154,7 @@ export const IdentityTokenAuthTokensTable = ({ tokens, identityId }: Props) => {
</Td>
<Td>
<div className="flex items-center gap-2">
<CopyButton value={id} size="xs" variant="plain" name="Token ID" />
<VariablePermissionCan
type={projectId ? "project" : "org"}
I={