mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat: switch to custom app installation flow
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
import axios from "axios";
|
||||
import { customAlphabet } from "nanoid";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
|
||||
import { AzureEntraIDSchema, DynamicSecretDataFetchTypes, TDynamicProviderFns } from "./models";
|
||||
@@ -20,14 +19,17 @@ export const AzureEntraIDProvider = (): TDynamicProviderFns => {
|
||||
return providerInputs;
|
||||
};
|
||||
|
||||
const getToken = async (tenantId: string): Promise<{ token?: string; success: boolean }> => {
|
||||
const appCfg = getConfig();
|
||||
const getToken = async (
|
||||
tenantId: string,
|
||||
applicationId: string,
|
||||
clientSecret: string
|
||||
): Promise<{ token?: string; success: boolean }> => {
|
||||
const response = await axios.post<{ access_token: string }>(
|
||||
`${MSFT_LOGIN_URL}/${tenantId}/oauth2/v2.0/token`,
|
||||
{
|
||||
grant_type: "client_credentials",
|
||||
client_id: appCfg.MSFT_ENTRA_ID_APPLICATION_ID,
|
||||
client_secret: appCfg.MSFT_ENTRA_ID_CLIENT_SECRET,
|
||||
client_id: applicationId,
|
||||
client_secret: clientSecret,
|
||||
scope: "https://graph.microsoft.com/.default"
|
||||
},
|
||||
{
|
||||
@@ -45,7 +47,7 @@ export const AzureEntraIDProvider = (): TDynamicProviderFns => {
|
||||
|
||||
const validateConnection = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const data = await getToken(providerInputs.tenantId);
|
||||
const data = await getToken(providerInputs.tenantId, providerInputs.applicationId, providerInputs.clientSecret);
|
||||
return data.success;
|
||||
};
|
||||
|
||||
@@ -56,7 +58,7 @@ export const AzureEntraIDProvider = (): TDynamicProviderFns => {
|
||||
|
||||
const create = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const data = await getToken(providerInputs.tenantId);
|
||||
const data = await getToken(providerInputs.tenantId, providerInputs.applicationId, providerInputs.clientSecret);
|
||||
if (!data.success) {
|
||||
throw new BadRequestError({ message: "Failed to authorize to Microsoft Entra ID" });
|
||||
}
|
||||
@@ -94,7 +96,7 @@ export const AzureEntraIDProvider = (): TDynamicProviderFns => {
|
||||
const fetchData = async (inputs: unknown, toFetch: DynamicSecretDataFetchTypes) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
|
||||
const data = await getToken(providerInputs.tenantId);
|
||||
const data = await getToken(providerInputs.tenantId, providerInputs.applicationId, providerInputs.clientSecret);
|
||||
if (!data.success) {
|
||||
throw new BadRequestError({ message: "Failed to authorize to Microsoft Entra ID" });
|
||||
}
|
||||
|
||||
@@ -169,7 +169,9 @@ export const DynamicSecretMongoDBSchema = z.object({
|
||||
export const AzureEntraIDSchema = z.object({
|
||||
tenantId: z.string().trim().min(1),
|
||||
userId: z.string().trim().min(1),
|
||||
email: z.string().trim().min(1)
|
||||
email: z.string().trim().min(1),
|
||||
applicationId: z.string().trim().min(1),
|
||||
clientSecret: z.string().trim().min(1)
|
||||
});
|
||||
|
||||
export enum DynamicSecretProviders {
|
||||
|
||||
@@ -134,9 +134,6 @@ const envSchema = z
|
||||
LICENSE_SERVER_KEY: zpStr(z.string().optional()),
|
||||
LICENSE_KEY: zpStr(z.string().optional()),
|
||||
LICENSE_KEY_OFFLINE: zpStr(z.string().optional()),
|
||||
// MICROSOFT ENTRA ID APP
|
||||
MSFT_ENTRA_ID_APPLICATION_ID: zpStr(z.string().optional()),
|
||||
MSFT_ENTRA_ID_CLIENT_SECRET: zpStr(z.string().optional()),
|
||||
|
||||
// GENERIC
|
||||
STANDALONE_MODE: z
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
import React, { Fragment } from "react";
|
||||
import { faAngleDown, faCheck, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Listbox, Transition } from "@headlessui/react";
|
||||
|
||||
interface TextProps {
|
||||
primaryText: string;
|
||||
secondaryText: string;
|
||||
}
|
||||
|
||||
interface ListBoxProps<T extends TextProps> {
|
||||
isSelected: T[];
|
||||
onChange: (value: T[]) => void;
|
||||
data: T[] | null;
|
||||
text?: string;
|
||||
buttonAction?: () => void;
|
||||
isFull?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the component that we use for drop down lists.
|
||||
* @param {object} obj
|
||||
* @param {object[]} obj.isSelected - the item that is currently selected
|
||||
* @param {function} obj.onChange - what happends if you select the item inside a list
|
||||
* @param {object[]} obj.data - all the options available
|
||||
* @param {string} obj.text - the text that shows us in front of the select option
|
||||
* @param {function} obj.buttonAction - if there is a button at the bottom of the list, this is the action that happens when you click the button
|
||||
* @returns
|
||||
*/
|
||||
const ListBoxMultiple = <T extends TextProps>({
|
||||
isSelected,
|
||||
onChange,
|
||||
data,
|
||||
text,
|
||||
buttonAction,
|
||||
isFull
|
||||
}: ListBoxProps<T>): JSX.Element => {
|
||||
return (
|
||||
<Listbox value={isSelected} onChange={onChange} multiple>
|
||||
<div className="relative w-full">
|
||||
<Listbox.Button
|
||||
className={`relative text-gray-400 ${isFull ? "w-full" : "w-52"
|
||||
} focus-visible:ring-offset-orange-300 cursor-default rounded-md bg-white/[0.07] py-2.5 pl-3 pr-10 text-left shadow-md duration-200 hover:bg-white/[0.11] focus:outline-none focus-visible:border-indigo-500 focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75 focus-visible:ring-offset-2 sm:text-sm`}
|
||||
>
|
||||
<div className="flex flex-row">
|
||||
{text}
|
||||
<span className="ml-1 block cursor-pointer truncate font-semibold text-gray-300">
|
||||
{!isSelected || isSelected.length === 0 && "Select"}
|
||||
{isSelected && isSelected.length > 0 && isSelected[0].primaryText} {isSelected.length > 1 && `(+${isSelected.length - 1})`}
|
||||
</span>
|
||||
</div>
|
||||
{data && (
|
||||
<div className="pointer-events-none absolute inset-y-0 right-0 flex cursor-pointer items-center pr-2">
|
||||
<FontAwesomeIcon icon={faAngleDown} className="text-md mr-1.5" />
|
||||
</div>
|
||||
)}
|
||||
</Listbox.Button>
|
||||
{data && (
|
||||
<Transition
|
||||
as={Fragment}
|
||||
leave="transition ease-in duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Listbox.Options className="no-scrollbar::-webkit-scrollbar absolute z-[70] mt-1 max-h-60 w-full overflow-auto rounded-md border border-mineshaft-700 bg-bunker p-2 text-base shadow-lg ring-1 ring-black ring-opacity-5 no-scrollbar focus:outline-none sm:text-sm">
|
||||
{data.map((user, personIdx) => (
|
||||
<Listbox.Option
|
||||
key={`${user}.${personIdx + 1}`}
|
||||
className={({ active, selected }) =>
|
||||
`relative my-0.5 cursor-default select-none rounded-md py-2 pl-10 pr-4 ${selected ? "bg-white/10 font-bold text-gray-400" : ""
|
||||
} ${active && !selected
|
||||
? "cursor-pointer bg-white/5 text-mineshaft-200"
|
||||
: "text-gray-400"
|
||||
} `
|
||||
}
|
||||
value={user}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
<span
|
||||
className={`block truncate text-primary${selected ? "font-medium" : "font-normal"
|
||||
}`}
|
||||
>
|
||||
{user.primaryText} {user.secondaryText && ` (${user.secondaryText})`}
|
||||
</span>
|
||||
{selected ? (
|
||||
<span className="absolute inset-y-0 left-0 flex items-center rounded-lg pl-3 text-primary">
|
||||
<FontAwesomeIcon icon={faCheck} className="text-md ml-1" />
|
||||
</span>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
{buttonAction && (
|
||||
<button type="button" onClick={buttonAction} className="w-full cursor-pointer">
|
||||
<div className="relative my-0.5 mt-2 flex cursor-pointer select-none justify-start rounded-md py-2 pl-10 pr-4 text-gray-400 duration-200 hover:bg-lime-300 hover:font-semibold hover:text-black">
|
||||
<span className="absolute inset-y-0 left-0 flex items-center rounded-lg pl-3 pr-4">
|
||||
<FontAwesomeIcon icon={faPlus} className="text-lg" />
|
||||
</span>
|
||||
Add Project
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
)}
|
||||
</div>
|
||||
</Listbox>
|
||||
);
|
||||
};
|
||||
|
||||
export default ListBoxMultiple;
|
||||
@@ -185,6 +185,8 @@ export type TDynamicSecretProvider =
|
||||
tenantId: string;
|
||||
userId: string;
|
||||
email: string;
|
||||
applicationId: string;
|
||||
clientSecret: string;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import Head from "next/head";
|
||||
|
||||
import { AzureEntraIdCallbackPage } from "@app/views/callback/AzureEntraIdCallbackPage";
|
||||
|
||||
const AzureEntraId = () => {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Infisical</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
<meta property="og:image" content="/images/message.png" />
|
||||
<meta property="og:title" content="" />
|
||||
<meta name="og:description" content="" />
|
||||
</Head>
|
||||
<AzureEntraIdCallbackPage />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AzureEntraId;
|
||||
|
||||
AzureEntraId.requireAuth = true;
|
||||
@@ -1,22 +0,0 @@
|
||||
import Head from "next/head";
|
||||
|
||||
import { AzureEntraIdCallbackPage } from "@app/views/callback/AzureEntraIdCallbackPage";
|
||||
|
||||
const AzureEntraId = () => {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Infisical</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
<meta property="og:image" content="/images/message.png" />
|
||||
<meta property="og:title" content="" />
|
||||
<meta name="og:description" content="" />
|
||||
</Head>
|
||||
<AzureEntraIdCallbackPage />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AzureEntraId;
|
||||
|
||||
AzureEntraId.requireAuth = true;
|
||||
@@ -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">
|
||||
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> 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> 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> 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
|
||||
|
||||
Reference in New Issue
Block a user