Merge pull request #4820 from Infisical/daniel/cert-auth

feat(app-connections/azure-client-secrets): certificate auth
This commit is contained in:
Daniel Hougaard
2025-11-08 04:42:54 +04:00
committed by GitHub
12 changed files with 448 additions and 35 deletions
@@ -12,12 +12,14 @@ const syntaxHighlight = (
isVisible?: boolean,
isImport?: boolean,
isLoadingValue?: boolean,
isErrorLoadingValue?: boolean
isErrorLoadingValue?: boolean,
placeholder?: string
) => {
if (isLoadingValue) return HIDDEN_SECRET_VALUE;
if (isErrorLoadingValue)
return <span className="ph-no-capture text-red/75">Error loading secret value.</span>;
if (isImport && !content) return "IMPORTED";
if (placeholder && (content === "" || !content)) return placeholder;
if (content === "") return "EMPTY";
if (!content) return "EMPTY";
if (!isVisible) return HIDDEN_SECRET_VALUE;
@@ -79,6 +81,7 @@ export const SecretInput = forwardRef<HTMLTextAreaElement, Props>(
canEditButNotView,
isLoadingValue,
isErrorLoadingValue,
placeholder,
...props
},
ref
@@ -93,18 +96,25 @@ export const SecretInput = forwardRef<HTMLTextAreaElement, Props>(
<div className="relative overflow-hidden">
<pre aria-hidden className="m-0">
<code className={`inline-block w-full ${commonClassName}`}>
<span style={{ whiteSpace: "break-spaces" }}>
<span
className={twMerge(
"whitespace-break-spaces",
placeholder && !value && "text-gray-500/50"
)}
>
{syntaxHighlight(
value,
isVisible || (isSecretFocused && !valueAlwaysHidden),
isImport,
isLoadingValue,
isErrorLoadingValue
isErrorLoadingValue,
placeholder
)}
</span>
</code>
</pre>
<textarea
placeholder={placeholder}
style={{ whiteSpace: "break-spaces" }}
aria-label="secret value"
ref={ref}
+3
View File
@@ -1,6 +1,7 @@
import { faGithub, IconDefinition } from "@fortawesome/free-brands-svg-icons";
import {
faBullseye,
faCertificate,
faKey,
faLink,
faLock,
@@ -211,6 +212,8 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"])
case AzureKeyVaultConnectionMethod.ClientSecret:
case AzureDevOpsConnectionMethod.ClientSecret:
return { name: "Client Secret", icon: faKey };
case AzureClientSecretsConnectionMethod.Certificate:
return { name: "Certificate", icon: faCertificate };
default:
throw new Error(`Unhandled App Connection Method: ${method}`);
}
@@ -3,7 +3,8 @@ import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-con
export enum AzureClientSecretsConnectionMethod {
OAuth = "oauth",
ClientSecret = "client-secret"
ClientSecret = "client-secret",
Certificate = "certificate"
}
export type TAzureClientSecretsConnection = TRootAppConnection & {
@@ -24,4 +25,13 @@ export type TAzureClientSecretsConnection = TRootAppConnection & {
tenantId: string;
};
}
| {
method: AzureClientSecretsConnectionMethod.Certificate;
credentials: {
clientId: string;
tenantId: string;
certificateBody: string;
privateKey: string;
};
}
);
@@ -6,7 +6,15 @@ import { Controller, FormProvider, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Button, FormControl, Input, ModalClose, Select, SelectItem } from "@app/components/v2";
import {
Button,
FormControl,
Input,
ModalClose,
SecretInput,
Select,
SelectItem
} from "@app/components/v2";
import {
APP_CONNECTION_MAP,
getAppConnectionMethodDetails,
@@ -27,10 +35,13 @@ import {
} from "./GenericAppConnectionFields";
type ClientSecretForm = z.infer<typeof clientSecretSchema>;
type CertificateForm = z.infer<typeof certificateSchema>;
type TInputFormData = ClientSecretForm | CertificateForm;
type Props = {
appConnection?: TAzureClientSecretsConnection;
onSubmit: (formData: ClientSecretForm) => Promise<void>;
onSubmit: (formData: TInputFormData) => Promise<void>;
projectId: string | undefined | null;
};
@@ -53,7 +64,21 @@ const clientSecretSchema = baseSchema.extend({
})
});
const formSchema = z.discriminatedUnion("method", [oauthSchema, clientSecretSchema]);
const certificateSchema = baseSchema.extend({
method: z.literal(AzureClientSecretsConnectionMethod.Certificate),
credentials: z.object({
clientId: z.string().trim().min(1, "Client ID is required"),
certificateBody: z.string().trim().min(1, "Certificate is required"),
privateKey: z.string().trim().min(1, "Private Key is required"),
tenantId: z.string().trim().min(1, "Tenant ID is required")
})
});
const formSchema = z.discriminatedUnion("method", [
oauthSchema,
clientSecretSchema,
certificateSchema
]);
type FormData = z.infer<typeof formSchema>;
@@ -96,6 +121,20 @@ const getDefaultValues = (appConnection?: TAzureClientSecretsConnection): Partia
};
}
break;
case AzureClientSecretsConnectionMethod.Certificate:
if ("clientId" in credentials && "tenantId" in credentials) {
return {
...base,
method: AzureClientSecretsConnectionMethod.Certificate,
credentials: {
clientId: credentials.clientId,
tenantId: credentials.tenantId,
certificateBody: "",
privateKey: ""
}
};
}
break;
default:
return base;
}
@@ -152,6 +191,9 @@ export const AzureClientSecretsConnectionForm = ({ appConnection, onSubmit, proj
case AzureClientSecretsConnectionMethod.ClientSecret:
await onSubmit(formData);
break;
case AzureClientSecretsConnectionMethod.Certificate:
await onSubmit(formData);
break;
default:
throw new Error(`Unhandled Azure Connection method: ${(formData as FormData).method}`);
}
@@ -207,7 +249,11 @@ export const AzureClientSecretsConnectionForm = ({ appConnection, onSubmit, proj
/>
<Controller
name="tenantId"
name={
selectedMethod === AzureClientSecretsConnectionMethod.OAuth
? "tenantId"
: "credentials.tenantId"
}
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
@@ -264,6 +310,59 @@ export const AzureClientSecretsConnectionForm = ({ appConnection, onSubmit, proj
</>
)}
{selectedMethod === AzureClientSecretsConnectionMethod.Certificate && (
<>
<Controller
name="credentials.clientId"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
label="Client ID"
errorText={error?.message}
>
<Input {...field} placeholder="00000000-0000-0000-0000-000000000000" />
</FormControl>
)}
/>
<Controller
name="credentials.certificateBody"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
label="Certificate"
errorText={error?.message}
>
<SecretInput
containerClassName="text-gray-400 group-focus-within:border-primary-400/50! border border-mineshaft-500 bg-mineshaft-900 px-2.5 py-1.5"
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder="-----BEGIN CERTIFICATE-----..."
/>
</FormControl>
)}
/>
<Controller
name="credentials.privateKey"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
label="Private Key"
errorText={error?.message}
>
<SecretInput
placeholder="-----BEGIN PRIVATE KEY-----..."
containerClassName="text-gray-400 group-focus-within:border-primary-400/50! border border-mineshaft-500 bg-mineshaft-900 px-2.5 py-1.5"
value={value}
onChange={(e) => onChange(e.target.value)}
/>
</FormControl>
)}
/>
</>
)}
<div className="mt-8 flex items-center">
<Button
className="mr-4"