feat: switch to custom app installation flow

This commit is contained in:
Meet
2024-09-19 07:35:23 +05:30
parent e4a83ad2e2
commit 75099f159f
10 changed files with 202 additions and 266 deletions
@@ -1,13 +1,12 @@
import { useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { faCheckCircle, faWarning } from "@fortawesome/free-solid-svg-icons";
import Link from "next/link";
import { faArrowUpRightFromSquare, faBookOpen, faCheckCircle, faWarning } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import ms from "ms";
import { z } from "zod";
import { TtlFormLabel } from "@app/components/features";
import { FormLabelToolTip } from "@app/components/features/FormLabelToolTip";
import { createNotification } from "@app/components/notifications";
import {
Button,
@@ -20,8 +19,6 @@ import { useCreateDynamicSecret } from "@app/hooks/api";
import { useGetDynamicSecretProviderData } from "@app/hooks/api/dynamicSecret/queries";
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
import { AzureEntraIdSetup } from "./AzureEntraIdSetup";
const formSchema = z.object({
selectedUsers: z.array(z.object({
id: z.string().min(1),
@@ -30,6 +27,8 @@ const formSchema = z.object({
})),
provider: z.object({
tenantId: z.string().min(1),
applicationId: z.string().min(1),
clientSecret: z.string().min(1)
}),
defaultTTL: z.string().superRefine((val, ctx) => {
const valMs = ms(val);
@@ -79,8 +78,11 @@ export const AzureEntraIdInputForm = ({
resolver: zodResolver(formSchema)
});
const tenantId = watch("provider.tenantId");
const [onSetup, setOnSetup] = useState(true);
const { data, isLoading, isFetched, isError, isFetching } = useGetDynamicSecretProviderData({ dataFetchType: "Users", provider: { type: DynamicSecretProviders.AzureEntraId, inputs: { userId: "unused", email: "unused", tenantId } }, enabled: !!tenantId });
const applicationId = watch("provider.applicationId");
const clientSecret = watch("provider.clientSecret");
const configurationComplete = tenantId && applicationId && clientSecret;
const { data, isLoading, isFetched, isError, isFetching } = useGetDynamicSecretProviderData({ dataFetchType: "Users", provider: { type: DynamicSecretProviders.AzureEntraId, inputs: { userId: "unused", email: "unused", tenantId, applicationId, clientSecret } }, enabled: !!configurationComplete });
const createDynamicSecret = useCreateDynamicSecret();
const handleCreateDynamicSecret = async ({ name, selectedUsers, provider, maxTTL, defaultTTL }: TForm) => {
@@ -89,7 +91,7 @@ export const AzureEntraIdInputForm = ({
try {
selectedUsers.map(async (user: { id: string, name: string, email: string }) => {
await createDynamicSecret.mutateAsync({
provider: { type: DynamicSecretProviders.AzureEntraId, inputs: { userId: user.id, tenantId: provider.tenantId, email: user.email } },
provider: { type: DynamicSecretProviders.AzureEntraId, inputs: { userId: user.id, tenantId: provider.tenantId, email: user.email, applicationId: provider.applicationId, clientSecret: provider.clientSecret } },
maxTTL,
name: `${name}-${user.name}`,
path: secretPath,
@@ -109,12 +111,7 @@ export const AzureEntraIdInputForm = ({
return (
<div>
{onSetup && <AzureEntraIdSetup
onCompleted={() => { setOnSetup(false); }}
onCancel={onCancel}
/>
}
{!onSetup && <form onSubmit={handleSubmit(handleCreateDynamicSecret)} autoComplete="off">
<form onSubmit={handleSubmit(handleCreateDynamicSecret)} autoComplete="off">
<div>
<div className="flex items-center space-x-2">
<div className="flex-grow">
@@ -168,7 +165,19 @@ export const AzureEntraIdInputForm = ({
</div>
<div>
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
Configuration
Configuration
<Link href="https://infisical.com/docs/documentation/platform/dynamic-secrets/azure-entra-id" passHref>
<a target="_blank" rel="noopener noreferrer">
<div className="ml-2 mb-1 inline-block cursor-default rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
Docs
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="ml-1.5 mb-[0.07rem] text-xxs"
/>
</div>
</a>
</Link>
</div>
<div className="flex flex-col">
<div className="flex-grow">
@@ -189,21 +198,62 @@ export const AzureEntraIdInputForm = ({
/>
</div>
</div>
<div className="flex flex-col">
<div className="flex-grow">
<Controller
control={control}
defaultValue=""
name="provider.applicationId"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Application Id"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="Application ID from Azure Entra ID App installation" />
</FormControl>
)}
/>
</div>
</div>
<div className="flex flex-col">
<div className="flex-grow">
<Controller
control={control}
defaultValue=""
name="provider.clientSecret"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Client Secret"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="Client Secret from Azure Entra ID App installation" />
</FormControl>
)}
/>
</div>
</div>
</div>
<div>
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
Select Users
</div>
<div className="mb-4 flex items-center text-sm font-normal text-mineshaft-400">
&nbsp; We create a unique dynamic secret for each user in Entra Id.
</div>
<div className="flex flex-col">
<div className="flex items-center space-x-4">
{
tenantId && !isError && !isFetching && isFetched && data &&
configurationComplete && !isError && !isFetching && isFetched && data &&
<Controller
control={control}
name="selectedUsers"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
label={<FormLabelToolTip content="We create a secret for each user" label="Select Users" linkToMore=""/>}
isRequired
isError={Boolean(error)}
errorText={error?.message}
@@ -247,13 +297,13 @@ export const AzureEntraIdInputForm = ({
/>
}
{
tenantId && isFetching && (<><Spinner size="xs" /><p> Loading</p></>)
configurationComplete && isFetching && (<div className="pl-3 pb-2 w-full flex items-center" ><Spinner size="xs" /><p> &nbsp; Loading </p></div>)
}
{
tenantId && !isFetching && isError && (<><FontAwesomeIcon icon={faWarning} /> <p> Error loading users please ensure Entra Id app is installed and tenant ID is correct</p></>)
configurationComplete && !isFetching && isError && (<div className="pl-3 pb-2 w-full flex items-center"><FontAwesomeIcon icon={faWarning} /> <p> &nbsp; Error loading users please ensure Entra Id app is installed and configuration is correct</p></div>)
}
{
!tenantId && (<><FontAwesomeIcon icon={faWarning} /><p> Enter tenant ID to fetch users</p></>)
!configurationComplete && (<div className="pl-3 pb-2 w-full flex items-center" ><FontAwesomeIcon icon={faWarning} /><p> &nbsp; Complete configuration to fetch users</p></div>)
}
</div>
</div>
@@ -263,15 +313,11 @@ export const AzureEntraIdInputForm = ({
<Button type="submit" isLoading={isSubmitting} isDisabled={isLoading || isError}>
Submit
</Button>
<Button variant="outline_bg" onClick={() => { setOnSetup(true); }}>
Back
</Button>
<Button variant="outline_bg" onClick={onCancel}>
Cancel
</Button>
</div>
</form>
}
</div>
);
};
@@ -1,71 +0,0 @@
import {
Button,
} from "@app/components/v2";
type Props = {
onCompleted: () => void;
onCancel: () => void;
};
const MSFT_ENTRA_ID_APPLICATION_ID = "9805c35f-88d4-4625-9daf-66f741e4129c"
export const AzureEntraIdSetup = ({
onCompleted,
onCancel,
}: Props) => {
return (
<div>
<div>
<div>
<div className="mb-4 mt-4 pb-2 pl-1 font-medium text-mineshaft-200">
Azure Entra ID Integration Guide
</div>
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
App Installation
</div>
<div className="flex flex-col">
<div className="flex items-center space-x-2">
Step 1: Click install app to install the Infisical Azure Entra ID App.
<br />
Step 2: Choose an account with admin access to Entra Id.
<br />
Step 3: Allow Infisical persmissions to read and write all users full profiles.
<br />
Step 4: Copy Tenant ID after installation and paste it in the next step.
</div>
</div>
</div>
<div>
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
Role Configuration
</div>
<div className="flex flex-col">
<div className="flex items-center space-x-2">
Step 1: Open the Azure Entra Id dashboard.
<br />
Step 2: Go to Roles and admins {">"} User Administrator Role {">"} + Add Assignments.
<br />
Step 3: Search Infisical {">"} Click on Infisical Enterprise App {">"} Click Add.
<br />
</div>
</div>
</div>
</div>
<div className="mt-4 flex items-center space-x-4">
<a href={`https://login.microsoftonline.com/common/adminconsent?client_id=${MSFT_ENTRA_ID_APPLICATION_ID}&redirect_uri=${window.location.origin}/integrations/azure-entra-id/callback`} target="_blank" rel="noreferrer">
<Button type="submit">
Install App
</Button>
</a>
<Button type="submit" onClick={onCompleted}>
Next
</Button>
<Button variant="outline_bg" onClick={onCancel}>
Cancel
</Button>
</div>
</div>
);
};
@@ -9,11 +9,19 @@ import {
Button,
FormControl,
Input,
SecretInput,
} from "@app/components/v2";
import { useUpdateDynamicSecret } from "@app/hooks/api";
import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
const formSchema = z.object({
inputs: z.object({
email: z.string(),
userId: z.string(),
tenantId: z.string(),
applicationId: z.string(),
clientSecret: z.string()
}),
defaultTTL: z.string().superRefine((val, ctx) => {
const valMs = ms(val);
if (valMs < 60 * 1000)
@@ -66,12 +74,15 @@ export const EditDynamicSecretAzureEntraIdForm = ({
defaultTTL: dynamicSecret.defaultTTL,
maxTTL: dynamicSecret.maxTTL,
newName: dynamicSecret.name,
inputs: {
...(dynamicSecret.inputs as TForm["inputs"])
}
}
});
const updateDynamicSecret = useUpdateDynamicSecret();
const handleUpdateDynamicSecret = async ({ maxTTL, defaultTTL, newName }: TForm) => {
const handleUpdateDynamicSecret = async ({ maxTTL, defaultTTL, newName, inputs }: TForm) => {
// wait till previous request is finished
if (updateDynamicSecret.isLoading) return;
try {
@@ -83,7 +94,8 @@ export const EditDynamicSecretAzureEntraIdForm = ({
data: {
maxTTL: maxTTL || undefined,
defaultTTL,
newName: newName === dynamicSecret.name ? undefined : newName
newName: newName === dynamicSecret.name ? undefined : newName,
inputs
}
});
onClose();
@@ -154,6 +166,109 @@ export const EditDynamicSecretAzureEntraIdForm = ({
</div>
</div>
</div>
<div className="flex items-center space-x-2">
<div className="flex-grow">
<Controller
control={control}
defaultValue=""
name="inputs.email"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Email"
isError={Boolean(error)}
errorText={error?.message}
>
<SecretInput
value={field.value}
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 py-1.5"
/>
</FormControl>
)}
/>
</div>
<div className="flex-grow">
<Controller
control={control}
defaultValue=""
name="inputs.userId"
render={({ field, fieldState: { error } }) => (
<FormControl
label="User ID"
isError={Boolean(error)}
errorText={error?.message}
>
<SecretInput
isReadOnly
value={field.value}
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 py-1.5"
/>
</FormControl>
)}
/>
</div>
</div>
<div className="flex items-center space-x-2">
<div className="flex-grow">
<Controller
control={control}
defaultValue=""
name="inputs.tenantId"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Tenant ID"
isError={Boolean(error)}
errorText={error?.message}
>
<SecretInput
isReadOnly
value={field.value}
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 py-1.5"
/>
</FormControl>
)}
/>
</div>
<div className="flex-grow">
<Controller
control={control}
defaultValue=""
name="inputs.applicationId"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Application ID"
isError={Boolean(error)}
errorText={error?.message}
>
<SecretInput
{...field}
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 py-1.5"
/>
</FormControl>
)}
/>
</div>
</div>
<div className="flex items-center space-x-2">
<div className="flex-grow">
<Controller
control={control}
defaultValue=""
name="inputs.clientSecret"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Client Secret"
isError={Boolean(error)}
errorText={error?.message}
>
<SecretInput
{...field}
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 py-1.5"
/>
</FormControl>
)}
/>
</div>
</div>
<div className="mt-4 flex items-center space-x-4">
<Button type="submit" isLoading={isSubmitting}>
Submit