diff --git a/backend/src/ee/services/dynamic-secret/providers/azure-entra-id.ts b/backend/src/ee/services/dynamic-secret/providers/azure-entra-id.ts index a8a2b4e8f..e12cbbfa8 100644 --- a/backend/src/ee/services/dynamic-secret/providers/azure-entra-id.ts +++ b/backend/src/ee/services/dynamic-secret/providers/azure-entra-id.ts @@ -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" }); } diff --git a/backend/src/ee/services/dynamic-secret/providers/models.ts b/backend/src/ee/services/dynamic-secret/providers/models.ts index bf336282e..04bf858fe 100644 --- a/backend/src/ee/services/dynamic-secret/providers/models.ts +++ b/backend/src/ee/services/dynamic-secret/providers/models.ts @@ -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 { diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index f5b8f547e..73a246433 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -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 diff --git a/frontend/src/components/basic/ListboxMultiple.tsx b/frontend/src/components/basic/ListboxMultiple.tsx deleted file mode 100644 index f817316c9..000000000 --- a/frontend/src/components/basic/ListboxMultiple.tsx +++ /dev/null @@ -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 { - 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 = ({ - isSelected, - onChange, - data, - text, - buttonAction, - isFull -}: ListBoxProps): JSX.Element => { - return ( - -
- -
- {text} - - {!isSelected || isSelected.length === 0 && "Select"} - {isSelected && isSelected.length > 0 && isSelected[0].primaryText} {isSelected.length > 1 && `(+${isSelected.length - 1})`} - -
- {data && ( -
- -
- )} -
- {data && ( - - - {data.map((user, personIdx) => ( - - `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 }) => ( - <> - - {user.primaryText} {user.secondaryText && ` (${user.secondaryText})`} - - {selected ? ( - - - - ) : null} - - )} - - ))} - {buttonAction && ( - - )} - - - )} -
-
- ); -}; - -export default ListBoxMultiple; diff --git a/frontend/src/hooks/api/dynamicSecret/types.ts b/frontend/src/hooks/api/dynamicSecret/types.ts index 3161b17f0..e97234bc4 100644 --- a/frontend/src/hooks/api/dynamicSecret/types.ts +++ b/frontend/src/hooks/api/dynamicSecret/types.ts @@ -185,6 +185,8 @@ export type TDynamicSecretProvider = tenantId: string; userId: string; email: string; + applicationId: string; + clientSecret: string; }; }; diff --git a/frontend/src/pages/callback/entraid.tsx b/frontend/src/pages/callback/entraid.tsx deleted file mode 100644 index 25e230a8d..000000000 --- a/frontend/src/pages/callback/entraid.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import Head from "next/head"; - -import { AzureEntraIdCallbackPage } from "@app/views/callback/AzureEntraIdCallbackPage"; - -const AzureEntraId = () => { - return ( - <> - - Infisical - - - - - - - - ); -}; - -export default AzureEntraId; - -AzureEntraId.requireAuth = true; diff --git a/frontend/src/pages/integrations/azure-entra-id/callback.tsx b/frontend/src/pages/integrations/azure-entra-id/callback.tsx deleted file mode 100644 index 25e230a8d..000000000 --- a/frontend/src/pages/integrations/azure-entra-id/callback.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import Head from "next/head"; - -import { AzureEntraIdCallbackPage } from "@app/views/callback/AzureEntraIdCallbackPage"; - -const AzureEntraId = () => { - return ( - <> - - Infisical - - - - - - - - ); -}; - -export default AzureEntraId; - -AzureEntraId.requireAuth = true; diff --git a/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/AzureEntraIdInputForm.tsx b/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/AzureEntraIdInputForm.tsx index 49d86d235..25302db20 100644 --- a/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/AzureEntraIdInputForm.tsx +++ b/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/AzureEntraIdInputForm.tsx @@ -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 (
- {onSetup && { setOnSetup(false); }} - onCancel={onCancel} - /> - } - {!onSetup &&
+
@@ -168,7 +165,19 @@ export const AzureEntraIdInputForm = ({
- Configuration + Configuration + + +
+ + Docs + +
+
+
@@ -189,21 +198,62 @@ export const AzureEntraIdInputForm = ({ />
+
+
+ ( + + + + )} + + /> +
+
+
+
+ ( + + + + )} + + /> +
+
+
Select Users
+
+   We create a unique dynamic secret for each user in Entra Id. +
{ - tenantId && !isError && !isFetching && isFetched && data && + configurationComplete && !isError && !isFetching && isFetched && data && ( } isRequired isError={Boolean(error)} errorText={error?.message} @@ -247,13 +297,13 @@ export const AzureEntraIdInputForm = ({ /> } { - tenantId && isFetching && (<>

Loading

) + configurationComplete && isFetching && (

  Loading

) } { - tenantId && !isFetching && isError && (<>

Error loading users please ensure Entra Id app is installed and tenant ID is correct

) + configurationComplete && !isFetching && isError && (

  Error loading users please ensure Entra Id app is installed and configuration is correct

) } { - !tenantId && (<>

Enter tenant ID to fetch users

) + !configurationComplete && (

  Complete configuration to fetch users

) }
@@ -263,15 +313,11 @@ export const AzureEntraIdInputForm = ({ -
- }
); }; diff --git a/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/AzureEntraIdSetup.tsx b/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/AzureEntraIdSetup.tsx deleted file mode 100644 index eb2521017..000000000 --- a/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/AzureEntraIdSetup.tsx +++ /dev/null @@ -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 ( -
-
-
-
- Azure Entra ID Integration Guide -
-
- App Installation -
-
-
- Step 1: Click install app to install the Infisical Azure Entra ID App. -
- Step 2: Choose an account with admin access to Entra Id. -
- Step 3: Allow Infisical persmissions to read and write all users full profiles. -
- Step 4: Copy Tenant ID after installation and paste it in the next step. -
-
-
- -
-
- Role Configuration -
-
-
- Step 1: Open the Azure Entra Id dashboard. -
- Step 2: Go to Roles and admins {">"} User Administrator Role {">"} + Add Assignments. -
- Step 3: Search Infisical {">"} Click on Infisical Enterprise App {">"} Click Add. -
-
-
-
-
-
- - - - - -
-
- ); -}; diff --git a/frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretAzureEntraIdForm.tsx b/frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretAzureEntraIdForm.tsx index 2fd93d616..31ac7b5c2 100644 --- a/frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretAzureEntraIdForm.tsx +++ b/frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretAzureEntraIdForm.tsx @@ -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 = ({
+
+
+ ( + + + + )} + /> +
+
+ ( + + + + )} + /> +
+
+
+
+ ( + + + + )} + /> +
+
+ ( + + + + )} + /> +
+
+
+
+ ( + + + + )} + /> +
+