mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-15 20:25:43 +00:00
Merge branch 'main' into ENG-2661
This commit is contained in:
+5
@@ -12,6 +12,7 @@ import { AppConnectionHeader } from "../AppConnectionHeader";
|
||||
import { Auth0ConnectionForm } from "./Auth0ConnectionForm";
|
||||
import { AwsConnectionForm } from "./AwsConnectionForm";
|
||||
import { AzureAppConfigurationConnectionForm } from "./AzureAppConfigurationConnectionForm";
|
||||
import { AzureClientSecretsConnectionForm } from "./AzureClientSecretsConnectionForm";
|
||||
import { AzureKeyVaultConnectionForm } from "./AzureKeyVaultConnectionForm";
|
||||
import { CamundaConnectionForm } from "./CamundaConnectionForm";
|
||||
import { DatabricksConnectionForm } from "./DatabricksConnectionForm";
|
||||
@@ -87,6 +88,8 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => {
|
||||
return <MsSqlConnectionForm onSubmit={onSubmit} />;
|
||||
case AppConnection.Camunda:
|
||||
return <CamundaConnectionForm onSubmit={onSubmit} />;
|
||||
case AppConnection.AzureClientSecrets:
|
||||
return <AzureClientSecretsConnectionForm />;
|
||||
case AppConnection.Windmill:
|
||||
return <WindmillConnectionForm onSubmit={onSubmit} />;
|
||||
case AppConnection.Auth0:
|
||||
@@ -155,6 +158,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
|
||||
return <MsSqlConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||
case AppConnection.Camunda:
|
||||
return <CamundaConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||
case AppConnection.AzureClientSecrets:
|
||||
return <AzureClientSecretsConnectionForm appConnection={appConnection} />;
|
||||
case AppConnection.Windmill:
|
||||
return <WindmillConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||
case AppConnection.Auth0:
|
||||
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
import crypto from "crypto";
|
||||
|
||||
import { useState } from "react";
|
||||
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 { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
|
||||
import { isInfisicalCloud } from "@app/helpers/platform";
|
||||
import {
|
||||
AzureClientSecretsConnectionMethod,
|
||||
TAzureClientSecretsConnection,
|
||||
useGetAppConnectionOption
|
||||
} from "@app/hooks/api/appConnections";
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
|
||||
import {
|
||||
genericAppConnectionFieldsSchema,
|
||||
GenericAppConnectionsFields
|
||||
} from "./GenericAppConnectionFields";
|
||||
|
||||
type Props = {
|
||||
appConnection?: TAzureClientSecretsConnection;
|
||||
};
|
||||
|
||||
const formSchema = genericAppConnectionFieldsSchema.extend({
|
||||
app: z.literal(AppConnection.AzureClientSecrets),
|
||||
method: z.nativeEnum(AzureClientSecretsConnectionMethod),
|
||||
tenantId: z.string().trim().min(1, "Tenant ID is required")
|
||||
});
|
||||
|
||||
type FormData = z.infer<typeof formSchema>;
|
||||
|
||||
export const AzureClientSecretsConnectionForm = ({ appConnection }: Props) => {
|
||||
const isUpdate = Boolean(appConnection);
|
||||
const [isRedirecting, setIsRedirecting] = useState(false);
|
||||
|
||||
const {
|
||||
option: { oauthClientId },
|
||||
isLoading
|
||||
} = useGetAppConnectionOption(AppConnection.AzureClientSecrets);
|
||||
|
||||
const form = useForm<FormData>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: appConnection
|
||||
? {
|
||||
...appConnection,
|
||||
tenantId: appConnection.credentials.tenantId
|
||||
}
|
||||
: {
|
||||
app: AppConnection.AzureClientSecrets,
|
||||
method: AzureClientSecretsConnectionMethod.OAuth
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
watch,
|
||||
formState: { isSubmitting, isDirty }
|
||||
} = form;
|
||||
|
||||
const selectedMethod = watch("method");
|
||||
|
||||
const onSubmit = (formData: FormData) => {
|
||||
setIsRedirecting(true);
|
||||
const state = crypto.randomBytes(16).toString("hex");
|
||||
localStorage.setItem("latestCSRFToken", state);
|
||||
localStorage.setItem(
|
||||
"azureClientSecretsConnectionFormData",
|
||||
JSON.stringify({ ...formData, connectionId: appConnection?.id })
|
||||
);
|
||||
|
||||
switch (formData.method) {
|
||||
case AzureClientSecretsConnectionMethod.OAuth:
|
||||
window.location.assign(
|
||||
`https://login.microsoftonline.com/${formData.tenantId || "common"}/oauth2/v2.0/authorize?client_id=${oauthClientId}&response_type=code&redirect_uri=${window.location.origin}/organization/app-connections/azure/oauth/callback&response_mode=query&scope=https://azconfig.io/.default%20openid%20offline_access&state=${state}<:>azure-client-secrets`
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unhandled Azure Connection method: ${(formData as FormData).method}`);
|
||||
}
|
||||
};
|
||||
|
||||
const isMissingConfig = !oauthClientId;
|
||||
|
||||
const methodDetails = getAppConnectionMethodDetails(selectedMethod);
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
{!isUpdate && <GenericAppConnectionsFields />}
|
||||
|
||||
<Controller
|
||||
name="tenantId"
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
tooltipText="The Directory (tenant) ID."
|
||||
isError={Boolean(error?.message)}
|
||||
label="Tenant ID"
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="e4f34ea5-ad23-4291-8585-66d20d603cc8" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="method"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
tooltipText={`The method you would like to use to connect with ${
|
||||
APP_CONNECTION_MAP[AppConnection.AzureClientSecrets].name
|
||||
}. This field cannot be changed after creation.`}
|
||||
errorText={
|
||||
!isLoading && isMissingConfig
|
||||
? `Environment variables have not been configured. ${
|
||||
isInfisicalCloud()
|
||||
? "Please contact Infisical."
|
||||
: `See documentation to configure Azure ${methodDetails.name} Connections.`
|
||||
}`
|
||||
: error?.message
|
||||
}
|
||||
isError={Boolean(error?.message) || isMissingConfig}
|
||||
label="Method"
|
||||
>
|
||||
<Select
|
||||
isDisabled={isUpdate}
|
||||
value={value}
|
||||
onValueChange={(val) => onChange(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
position="popper"
|
||||
dropdownContainerClassName="max-w-none"
|
||||
>
|
||||
{Object.values(AzureClientSecretsConnectionMethod).map((method) => {
|
||||
return (
|
||||
<SelectItem value={method} key={method}>
|
||||
{getAppConnectionMethodDetails(method).name}
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="mt-8 flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
colorSchema="secondary"
|
||||
isLoading={isSubmitting || isRedirecting}
|
||||
isDisabled={isSubmitting || (!isUpdate && !isDirty) || isMissingConfig || isRedirecting}
|
||||
>
|
||||
{isUpdate ? "Reconnect to Azure" : "Connect to Azure"}
|
||||
</Button>
|
||||
<ModalClose asChild>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
+61
-1
@@ -7,9 +7,11 @@ import { ROUTE_PATHS } from "@app/const/routes";
|
||||
import { APP_CONNECTION_MAP } from "@app/helpers/appConnections";
|
||||
import {
|
||||
AzureAppConfigurationConnectionMethod,
|
||||
AzureClientSecretsConnectionMethod,
|
||||
AzureKeyVaultConnectionMethod,
|
||||
GitHubConnectionMethod,
|
||||
TAzureAppConfigurationConnection,
|
||||
TAzureClientSecretsConnection,
|
||||
TAzureKeyVaultConnection,
|
||||
TGitHubConnection,
|
||||
useCreateAppConnection,
|
||||
@@ -32,18 +34,26 @@ type AzureAppConfigurationFormData = BaseFormData &
|
||||
Pick<TAzureAppConfigurationConnection, "name" | "method" | "description"> &
|
||||
Pick<TAzureAppConfigurationConnection["credentials"], "tenantId">;
|
||||
|
||||
type AzureClientSecretsFormData = BaseFormData &
|
||||
Pick<TAzureClientSecretsConnection, "name" | "method" | "description"> &
|
||||
Pick<TAzureClientSecretsConnection["credentials"], "tenantId">;
|
||||
|
||||
type FormDataMap = {
|
||||
[AppConnection.GitHub]: GithubFormData & { app: AppConnection.GitHub };
|
||||
[AppConnection.AzureKeyVault]: AzureKeyVaultFormData & { app: AppConnection.AzureKeyVault };
|
||||
[AppConnection.AzureAppConfiguration]: AzureAppConfigurationFormData & {
|
||||
app: AppConnection.AzureAppConfiguration;
|
||||
};
|
||||
[AppConnection.AzureClientSecrets]: AzureClientSecretsFormData & {
|
||||
app: AppConnection.AzureClientSecrets;
|
||||
};
|
||||
};
|
||||
|
||||
const formDataStorageFieldMap: Partial<Record<AppConnection, string>> = {
|
||||
[AppConnection.GitHub]: "githubConnectionFormData",
|
||||
[AppConnection.AzureKeyVault]: "azureKeyVaultConnectionFormData",
|
||||
[AppConnection.AzureAppConfiguration]: "azureAppConfigurationConnectionFormData"
|
||||
[AppConnection.AzureAppConfiguration]: "azureAppConfigurationConnectionFormData",
|
||||
[AppConnection.AzureClientSecrets]: "azureClientSecretsConnectionFormData"
|
||||
};
|
||||
|
||||
export const OAuthCallbackPage = () => {
|
||||
@@ -194,6 +204,54 @@ export const OAuthCallbackPage = () => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleAzureClientSecrets = useCallback(async () => {
|
||||
const formData = getFormData(AppConnection.AzureClientSecrets);
|
||||
if (formData === null) return null;
|
||||
|
||||
clearState(AppConnection.AzureClientSecrets);
|
||||
|
||||
const { connectionId, name, description, returnUrl } = formData;
|
||||
|
||||
try {
|
||||
if (connectionId) {
|
||||
await updateAppConnection.mutateAsync({
|
||||
app: AppConnection.AzureClientSecrets,
|
||||
connectionId,
|
||||
credentials: {
|
||||
code: code as string,
|
||||
tenantId: formData.tenantId
|
||||
}
|
||||
});
|
||||
} else {
|
||||
await createAppConnection.mutateAsync({
|
||||
app: AppConnection.AzureClientSecrets,
|
||||
name,
|
||||
description,
|
||||
method: AzureClientSecretsConnectionMethod.OAuth,
|
||||
credentials: {
|
||||
code: code as string,
|
||||
tenantId: formData.tenantId
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (err: any) {
|
||||
createNotification({
|
||||
title: `Failed to ${connectionId ? "update" : "add"} Azure Client Secrets Connection`,
|
||||
text: err?.message,
|
||||
type: "error"
|
||||
});
|
||||
navigate({
|
||||
to: returnUrl ?? "/organization/app-connections"
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
connectionId,
|
||||
returnUrl,
|
||||
appConnectionName: formData.app
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleGithub = useCallback(async () => {
|
||||
const formData = getFormData(AppConnection.GitHub);
|
||||
if (formData === null) return null;
|
||||
@@ -280,6 +338,8 @@ export const OAuthCallbackPage = () => {
|
||||
data = await handleAzureKeyVault();
|
||||
} else if (appConnection === AppConnection.AzureAppConfiguration) {
|
||||
data = await handleAzureAppConfiguration();
|
||||
} else if (appConnection === AppConnection.AzureClientSecrets) {
|
||||
data = await handleAzureClientSecrets();
|
||||
}
|
||||
|
||||
if (data) {
|
||||
|
||||
@@ -23,6 +23,7 @@ import { OrgLDAPSection } from "./OrgLDAPSection";
|
||||
import { OrgOIDCSection } from "./OrgOIDCSection";
|
||||
import { OrgScimSection } from "./OrgSCIMSection";
|
||||
import { OrgSSOSection } from "./OrgSSOSection";
|
||||
import { OrgUserAccessTokenLimitSection } from "./OrgUserAccessTokenLimitSection";
|
||||
import { SSOModal } from "./SSOModal";
|
||||
|
||||
export const OrgAuthTab = withPermission(
|
||||
@@ -167,6 +168,7 @@ export const OrgAuthTab = withPermission(
|
||||
return (
|
||||
<>
|
||||
<OrgGenericAuthSection />
|
||||
<OrgUserAccessTokenLimitSection />
|
||||
{shouldShowCreateIdentityProviderView ? (
|
||||
createIdentityProviderView
|
||||
) : (
|
||||
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { Button, FormControl, Input, Select, SelectItem } from "@app/components/v2";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||
import { useUpdateOrg } from "@app/hooks/api";
|
||||
|
||||
const formSchema = z.object({
|
||||
expirationValue: z.number().min(1, "Value must be at least 1"),
|
||||
expirationUnit: z.enum(["m", "h", "d", "w"], {
|
||||
invalid_type_error: "Please select a valid time unit"
|
||||
})
|
||||
});
|
||||
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
// Function to parse duration string like "30d" into value and unit
|
||||
const parseDuration = (duration: string): { value: number; unit: string } => {
|
||||
const match = duration.match(/^(\d+)([mhdw])$/);
|
||||
if (match) {
|
||||
return {
|
||||
value: parseInt(match[1], 10),
|
||||
unit: match[2]
|
||||
};
|
||||
}
|
||||
// Default to 30 days if invalid format
|
||||
return { value: 30, unit: "d" };
|
||||
};
|
||||
|
||||
// Function to format value and unit back to duration string
|
||||
const formatDuration = (value: number, unit: string): string => {
|
||||
return `${value}${unit}`;
|
||||
};
|
||||
|
||||
export const OrgUserAccessTokenLimitSection = () => {
|
||||
const { mutateAsync: updateUserTokenExpiration } = useUpdateOrg();
|
||||
const { currentOrg } = useOrganization();
|
||||
|
||||
// Parse the current duration or use default
|
||||
const currentDuration = parseDuration(currentOrg?.userTokenExpiration || "30d");
|
||||
|
||||
const {
|
||||
control,
|
||||
formState: { isSubmitting, isDirty },
|
||||
handleSubmit
|
||||
} = useForm<TForm>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
expirationValue: currentDuration.value,
|
||||
expirationUnit: currentDuration.unit as "m" | "h" | "d" | "w"
|
||||
}
|
||||
});
|
||||
|
||||
if (!currentOrg) return null;
|
||||
|
||||
const handleUserTokenExpirationSubmit = async (formData: TForm) => {
|
||||
try {
|
||||
const userTokenExpiration = formatDuration(formData.expirationValue, formData.expirationUnit);
|
||||
|
||||
await updateUserTokenExpiration({
|
||||
userTokenExpiration,
|
||||
orgId: currentOrg.id
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully updated user token expiration",
|
||||
type: "success"
|
||||
});
|
||||
} catch {
|
||||
createNotification({
|
||||
text: "Failed updating user token expiration",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Units for the dropdown with readable labels
|
||||
const timeUnits = [
|
||||
{ value: "m", label: "Minutes" },
|
||||
{ value: "h", label: "Hours" },
|
||||
{ value: "d", label: "Days" },
|
||||
{ value: "w", label: "Weeks" }
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<p className="text-xl font-semibold">User Token Expiration</p>
|
||||
</div>
|
||||
<p className="mb-4 mt-2 text-sm text-gray-400">
|
||||
This defines the maximum time a user token will be valid. After this time, the user will
|
||||
need to re-authenticate.
|
||||
</p>
|
||||
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Settings}>
|
||||
{(isAllowed) => (
|
||||
<form onSubmit={handleSubmit(handleUserTokenExpirationSubmit)} autoComplete="off">
|
||||
<div className="flex max-w-md gap-4">
|
||||
<div className="flex-1">
|
||||
<Controller
|
||||
control={control}
|
||||
name="expirationValue"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="Expiration value"
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
min={1}
|
||||
step={1}
|
||||
value={field.value}
|
||||
onChange={(e) => field.onChange(parseInt(e.target.value, 10))}
|
||||
disabled={!isAllowed}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<Controller
|
||||
control={control}
|
||||
name="expirationUnit"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="Time unit"
|
||||
>
|
||||
<Select
|
||||
value={field.value}
|
||||
className="pr-2"
|
||||
onValueChange={field.onChange}
|
||||
placeholder="Select time unit"
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
{timeUnits.map(({ value, label }) => (
|
||||
<SelectItem
|
||||
key={value}
|
||||
value={value}
|
||||
className="relative py-2 pl-6 pr-8 text-sm hover:bg-mineshaft-700"
|
||||
>
|
||||
<div className="ml-3 font-medium">{label}</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
disabled={!isDirty}
|
||||
className="mt-4"
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user