diff --git a/backend/src/ee/routes/v1/dynamic-secret-router.ts b/backend/src/ee/routes/v1/dynamic-secret-router.ts index 049370743..952386a31 100644 --- a/backend/src/ee/routes/v1/dynamic-secret-router.ts +++ b/backend/src/ee/routes/v1/dynamic-secret-router.ts @@ -3,7 +3,10 @@ import ms from "ms"; import { z } from "zod"; import { DynamicSecretLeasesSchema } from "@app/db/schemas"; -import { DynamicSecretProviderSchema } from "@app/ee/services/dynamic-secret/providers/models"; +import { + DynamicSecretDataFetchTypes, + DynamicSecretProviderSchema +} from "@app/ee/services/dynamic-secret/providers/models"; import { DYNAMIC_SECRETS } from "@app/lib/api-docs"; import { daysToMillisecond } from "@app/lib/dates"; import { removeTrailingSlash } from "@app/lib/fn"; @@ -77,6 +80,32 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) => } }); + server.route({ + method: "POST", + url: "/fetch-provider-data", + config: { + rateLimit: readLimit + }, + schema: { + body: z.object({ + dataFetchType: z.string().min(1).describe("Type of data to fetch"), + provider: DynamicSecretProviderSchema.describe(DYNAMIC_SECRETS.CREATE.provider) + }), + response: { + 200: z.object({ + data: z.unknown() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const dataFetchType = + DynamicSecretDataFetchTypes[req.body.dataFetchType as keyof typeof DynamicSecretDataFetchTypes]; + const data = await server.services.dynamicSecret.fetchData({ provider: req.body.provider, dataFetchType }); + return data; + } + }); + server.route({ method: "PATCH", url: "/:name", diff --git a/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts b/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts index ea08b212a..9127a45d2 100644 --- a/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts +++ b/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts @@ -17,6 +17,7 @@ import { TCreateDynamicSecretDTO, TDeleteDynamicSecretDTO, TDetailsDynamicSecretDTO, + TDynamicSecretsFetchDataDTO, TListDynamicSecretsDTO, TUpdateDynamicSecretDTO } from "./dynamic-secret-types"; @@ -332,11 +333,20 @@ export const dynamicSecretServiceFactory = ({ return dynamicSecretCfg; }; + const fetchData = async ({ provider, dataFetchType }: TDynamicSecretsFetchDataDTO) => { + const selectedProvider = dynamicSecretProviders[provider.type]; + if (selectedProvider.fetchData) { + const data = selectedProvider.fetchData(provider.inputs, dataFetchType); + return data; + } + }; + return { create, updateByName, deleteByName, getDetails, - list + list, + fetchData }; }; diff --git a/backend/src/ee/services/dynamic-secret/dynamic-secret-types.ts b/backend/src/ee/services/dynamic-secret/dynamic-secret-types.ts index 02f2cbb86..9bb013a5d 100644 --- a/backend/src/ee/services/dynamic-secret/dynamic-secret-types.ts +++ b/backend/src/ee/services/dynamic-secret/dynamic-secret-types.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { TProjectPermission } from "@app/lib/types"; -import { DynamicSecretProviderSchema } from "./providers/models"; +import { DynamicSecretDataFetchTypes, DynamicSecretProviderSchema } from "./providers/models"; // various status for dynamic secret that happens in background export enum DynamicSecretStatus { @@ -52,3 +52,8 @@ export type TListDynamicSecretsDTO = { environmentSlug: string; projectSlug: string; } & Omit; + +export type TDynamicSecretsFetchDataDTO = { + provider: TProvider; + dataFetchType: DynamicSecretDataFetchTypes; +}; 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 new file mode 100644 index 000000000..c04541c3b --- /dev/null +++ b/backend/src/ee/services/dynamic-secret/providers/azure-entra-id.ts @@ -0,0 +1,140 @@ +import axios from "axios"; +import { customAlphabet } from "nanoid"; + +import { getConfig } from "@app/lib/config/env"; +import { BadRequestError } from "@app/lib/errors"; +import { logger } from "@app/lib/logger"; + +import { AzureEntraIDSchema, DynamicSecretDataFetchTypes, TDynamicProviderFns } from "./models"; + +const MSFT_GRAPH_API_URL = "https://graph.microsoft.com/v1.0/"; +const MSFT_LOGIN_URL = "https://login.microsoftonline.com"; + +const generatePassword = () => { + const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#"; + return customAlphabet(charset, 64)(); +}; + +export const AzureEntraIDProvider = (): TDynamicProviderFns => { + const validateProviderInputs = async (inputs: unknown) => { + const providerInputs = await AzureEntraIDSchema.parseAsync(inputs); + return providerInputs; + }; + + const getToken = async (tenantId: string): Promise<{ token?: string; success: boolean }> => { + const appCfg = getConfig(); + 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, + scope: "https://graph.microsoft.com/.default" + }, + { + headers: { + "Content-Type": "application/x-www-form-urlencoded" + } + } + ); + + if (response.status === 200) { + return { token: response.data.access_token, success: true }; + } + return { success: false }; + }; + + const validateConnection = async (inputs: unknown) => { + const providerInputs = await validateProviderInputs(inputs); + const data = await getToken(providerInputs.tenantId); + return data.success; + }; + + const renew = async (inputs: unknown, entityId: string) => { + // Do nothing + return { entityId }; + }; + + const create = async (inputs: unknown) => { + const providerInputs = await validateProviderInputs(inputs); + const data = await getToken(providerInputs.tenantId); + if (!data.success) { + throw new BadRequestError({ message: "Failed to authorize to Microsoft Entra ID" }); + } + + const password = generatePassword(); + try { + const response = await axios.patch( + `${MSFT_GRAPH_API_URL}/users/${providerInputs.userId}`, + { + passwordProfile: { + forceChangePasswordNextSignIn: false, + password + } + }, + { + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${data.token}` + } + } + ); + if (response.status !== 204) { + throw new BadRequestError({ message: "Failed to update password" }); + } + } catch (error) { + logger.info(JSON.stringify(error)); + } + + return { entityId: providerInputs.userId, data: { userId: providerInputs.userId, password } }; + }; + + const revoke = async (inputs: unknown, entityId: string) => { + await create(inputs); + return { entityId }; + }; + + const fetchData = async (inputs: unknown, toFetch: DynamicSecretDataFetchTypes) => { + const providerInputs = await validateProviderInputs(inputs); + + const data = await getToken(providerInputs.tenantId); + if (!data.success) { + throw new BadRequestError({ message: "Failed to authorize to Microsoft Entra ID" }); + } + + switch (toFetch) { + case DynamicSecretDataFetchTypes.Users: { + const response = await axios.get<{ value: [{ displayName: string; id: string }] }>( + `${MSFT_GRAPH_API_URL}/users`, + { + headers: { + "Content-Type": "application/x-www-form-urlencoded", + Authorization: `Bearer ${data.token}` + } + } + ); + const users = response.data.value.map((user: { displayName: string; id: string }) => { + return { + name: user.displayName, + id: user.id + }; + }); + return { + data: { + users + } + }; + } + default: + throw new BadRequestError({ message: "Unknown data to fetch" }); + } + }; + return { + validateProviderInputs, + validateConnection, + create, + revoke, + renew, + fetchData + }; +}; diff --git a/backend/src/ee/services/dynamic-secret/providers/index.ts b/backend/src/ee/services/dynamic-secret/providers/index.ts index 6ae22c869..8f2dbdc13 100644 --- a/backend/src/ee/services/dynamic-secret/providers/index.ts +++ b/backend/src/ee/services/dynamic-secret/providers/index.ts @@ -1,5 +1,6 @@ import { AwsElastiCacheDatabaseProvider } from "./aws-elasticache"; import { AwsIamProvider } from "./aws-iam"; +import { AzureEntraIDProvider } from "./azure-entra-id"; import { CassandraProvider } from "./cassandra"; import { ElasticSearchProvider } from "./elastic-search"; import { DynamicSecretProviders } from "./models"; @@ -18,5 +19,6 @@ export const buildDynamicSecretProviders = () => ({ [DynamicSecretProviders.MongoAtlas]: MongoAtlasProvider(), [DynamicSecretProviders.MongoDB]: MongoDBProvider(), [DynamicSecretProviders.ElasticSearch]: ElasticSearchProvider(), - [DynamicSecretProviders.RabbitMq]: RabbitMqProvider() + [DynamicSecretProviders.RabbitMq]: RabbitMqProvider(), + [DynamicSecretProviders.AzureEntraID]: AzureEntraIDProvider() }); diff --git a/backend/src/ee/services/dynamic-secret/providers/models.ts b/backend/src/ee/services/dynamic-secret/providers/models.ts index f23a60df7..7fb45e84a 100644 --- a/backend/src/ee/services/dynamic-secret/providers/models.ts +++ b/backend/src/ee/services/dynamic-secret/providers/models.ts @@ -166,6 +166,11 @@ export const DynamicSecretMongoDBSchema = z.object({ ) }); +export const AzureEntraIDSchema = z.object({ + tenantId: z.string().trim().min(1), + userId: z.string().trim().min(1) +}); + export enum DynamicSecretProviders { SqlDatabase = "sql-database", Cassandra = "cassandra", @@ -175,7 +180,12 @@ export enum DynamicSecretProviders { MongoAtlas = "mongo-db-atlas", ElasticSearch = "elastic-search", MongoDB = "mongo-db", - RabbitMq = "rabbit-mq" + RabbitMq = "rabbit-mq", + AzureEntraID = "azure-entra-id" +} + +export enum DynamicSecretDataFetchTypes { + Users = "users" } export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [ @@ -187,7 +197,8 @@ export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [ z.object({ type: z.literal(DynamicSecretProviders.MongoAtlas), inputs: DynamicSecretMongoAtlasSchema }), z.object({ type: z.literal(DynamicSecretProviders.ElasticSearch), inputs: DynamicSecretElasticSearchSchema }), z.object({ type: z.literal(DynamicSecretProviders.MongoDB), inputs: DynamicSecretMongoDBSchema }), - z.object({ type: z.literal(DynamicSecretProviders.RabbitMq), inputs: DynamicSecretRabbitMqSchema }) + z.object({ type: z.literal(DynamicSecretProviders.RabbitMq), inputs: DynamicSecretRabbitMqSchema }), + z.object({ type: z.literal(DynamicSecretProviders.AzureEntraID), inputs: AzureEntraIDSchema }) ]); export type TDynamicProviderFns = { @@ -196,4 +207,5 @@ export type TDynamicProviderFns = { validateProviderInputs: (inputs: object) => Promise; revoke: (inputs: unknown, entityId: string) => Promise<{ entityId: string }>; renew: (inputs: unknown, entityId: string, expireAt: number) => Promise<{ entityId: string }>; + fetchData?: (inputs: unknown, toFetch: DynamicSecretDataFetchTypes) => Promise<{ data: unknown }>; }; diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index 73a246433..f5b8f547e 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -134,6 +134,9 @@ 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 new file mode 100644 index 000000000..106f7d000 --- /dev/null +++ b/frontend/src/components/basic/ListboxMultiple.tsx @@ -0,0 +1,112 @@ +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 ListBoxProps { + isSelected: {name:string}[]; + onChange: (arg: unknown) => void; + data: {name:string}[] | 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 && "None"} + {isSelected && isSelected.length > 0 && isSelected[0].name} {isSelected.length>1 && `(+${isSelected.length-1})`} + +
+ {data && ( +
+ +
+ )} +
+ {data && ( + + + {data.map((person, 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={person} + > + {({ selected }) => ( + <> + + {person.name} + + {selected ? ( + + + + ) : null} + + )} + + ))} + {buttonAction && ( + + )} + + + )} +
+
+ ); +}; + +export default ListBoxMultiple; diff --git a/frontend/src/hooks/api/dynamicSecret/queries.ts b/frontend/src/hooks/api/dynamicSecret/queries.ts index 481b431cc..a4c230be2 100644 --- a/frontend/src/hooks/api/dynamicSecret/queries.ts +++ b/frontend/src/hooks/api/dynamicSecret/queries.ts @@ -6,6 +6,7 @@ import { apiRequest } from "@app/config/request"; import { TDetailsDynamicSecretDTO, TDynamicSecret, + TDynamicSecretProvider, TGetDynamicSecretsByEnvsDTO, TListDynamicSecretDTO } from "./types"; @@ -71,6 +72,28 @@ export const useGetDynamicSecretDetails = ({ }); }; +export const useGetDynamicSecretProviderData = ({ + provider, + dataFetchType +}: { + provider: TDynamicSecretProvider, + dataFetchType: "Users" +}) => { + return useQuery({ + queryKey: ["users"], + queryFn: async () => { + const { data } = await apiRequest.post<{ data: { users: [{ name: string, id: string }] } }>( + "/api/v1/dynamic-secrets/fetch-provider-data", + { + provider, + dataFetchType + } + ); + return data.data.users; + } + }); +}; + export const useGetDynamicSecretsOfAllEnv = ({ path, projectSlug, diff --git a/frontend/src/hooks/api/dynamicSecret/types.ts b/frontend/src/hooks/api/dynamicSecret/types.ts index 32c9b15d0..d01fa899c 100644 --- a/frontend/src/hooks/api/dynamicSecret/types.ts +++ b/frontend/src/hooks/api/dynamicSecret/types.ts @@ -24,7 +24,8 @@ export enum DynamicSecretProviders { MongoAtlas = "mongo-db-atlas", ElasticSearch = "elastic-search", MongoDB = "mongo-db", - RabbitMq = "rabbit-mq" + RabbitMq = "rabbit-mq", + AzureEntraId = "azure-entra-id" } export enum SqlProviders { @@ -177,7 +178,14 @@ export type TDynamicSecretProvider = }; ca?: string; }; - }; + } + | { + type: DynamicSecretProviders.AzureEntraId; + inputs: { + tenantId: string; + userId: string; + }; + }; export type TCreateDynamicSecretDTO = { projectSlug: string; diff --git a/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/AzureEntraIdInputForm.tsx b/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/AzureEntraIdInputForm.tsx new file mode 100644 index 000000000..a296ca907 --- /dev/null +++ b/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/AzureEntraIdInputForm.tsx @@ -0,0 +1,177 @@ +import { useState } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import ms from "ms"; +import { z } from "zod"; + +import ListBoxMultiple from "@app/components/basic/ListboxMultiple"; +import { TtlFormLabel } from "@app/components/features"; +import { createNotification } from "@app/components/notifications"; +import { + Button, + FormControl, + Input, +} from "@app/components/v2"; +import { useCreateDynamicSecret } from "@app/hooks/api"; +import { useGetDynamicSecretProviderData } from "@app/hooks/api/dynamicSecret/queries"; +import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; + +const formSchema = z.object({ + defaultTTL: z.string().superRefine((val, ctx) => { + const valMs = ms(val); + if (valMs < 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" }); + // a day + if (valMs > 24 * 60 * 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); + }), + maxTTL: z + .string() + .optional() + .superRefine((val, ctx) => { + if (!val) return; + const valMs = ms(val); + if (valMs < 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" }); + // a day + if (valMs > 24 * 60 * 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); + }), + name: z.string().min(1).refine((val) => val.toLowerCase() === val, "Must be lowercase") +}); +type TForm = z.infer; + +type Props = { + onCompleted: () => void; + onCancel: () => void; + secretPath: string; + projectSlug: string; + environment: string; +}; + +export const AzureEntraIdInputForm = ({ + onCompleted, + onCancel, + environment, + secretPath, + projectSlug +}: Props) => { + const { + control, + formState: { isSubmitting }, + handleSubmit + } = useForm({ + resolver: zodResolver(formSchema) + }); + + const [selectedUsers, setSelectedUsers] = useState([]); + const users = useGetDynamicSecretProviderData({ provider: { type: DynamicSecretProviders.AzureEntraId, inputs: { tenantId: "", userId: "test" } }, dataFetchType: "Users" }).data + const createDynamicSecret = useCreateDynamicSecret(); + + const handleCreateDynamicSecret = async ({ name, maxTTL, defaultTTL }: TForm) => { + // wait till previous request is finished + if (createDynamicSecret.isLoading) return; + try { + selectedUsers.map(async (user: {id: string, name: string})=>{ + await createDynamicSecret.mutateAsync({ + provider: { type: DynamicSecretProviders.AzureEntraId, inputs: { userId: user.id, tenantId: "" } }, + maxTTL, + name: `${name }-${ user.name}`, + path: secretPath, + defaultTTL, + projectSlug, + environmentSlug: environment + }); + }); + onCompleted(); + } catch (err) { + createNotification({ + type: "error", + text: "Failed to create dynamic secret" + }); + } + }; + + return ( +
+
+
+
+
+ ( + + + + )} + /> +
+
+ ( + } + isError={Boolean(error?.message)} + errorText={error?.message} + > + + + )} + /> +
+
+ ( + } + isError={Boolean(error?.message)} + errorText={error?.message} + > + + + )} + /> +
+
+
+
+ Select Users +
+
+
+ {users && + + } +
+
+
+
+
+ + +
+
+
+ ); +}; diff --git a/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/AzureEntraIdSetup.tsx b/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/AzureEntraIdSetup.tsx new file mode 100644 index 000000000..0465be184 --- /dev/null +++ b/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/AzureEntraIdSetup.tsx @@ -0,0 +1,61 @@ +import { + Button, +} from "@app/components/v2"; + +type Props = { + onCompleted: () => void; + onCancel: () => void; +}; + +export const AzureEntraIdSetup = ({ + onCompleted, + onCancel, +}: Props) => { + + return ( +
+
+
+
+ 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. +
+
+
+ +
+
+ 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/ActionBar/CreateDynamicSecretForm/CreateDynamicSecretForm.tsx b/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/CreateDynamicSecretForm.tsx index 90eb74012..6cbbbee60 100644 --- a/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/CreateDynamicSecretForm.tsx +++ b/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/CreateDynamicSecretForm.tsx @@ -11,6 +11,8 @@ import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; import { AwsElastiCacheInputForm } from "./AwsElastiCacheInputForm"; import { AwsIamInputForm } from "./AwsIamInputForm"; +import { AzureEntraIdInputForm } from "./AzureEntraIdInputForm"; +import { AzureEntraIdSetup } from "./AzureEntraIdSetup"; import { CassandraInputForm } from "./CassandraInputForm"; import { ElasticSearchInputForm } from "./ElasticSearchInputForm"; import { MongoAtlasInputForm } from "./MongoAtlasInputForm"; @@ -29,6 +31,7 @@ type Props = { enum WizardSteps { SelectProvider = "select-provider", + ProviderSetup = "provider-setup", ProviderInputs = "provider-inputs" } @@ -77,6 +80,12 @@ const DYNAMIC_SECRET_LIST = [ icon: , provider: DynamicSecretProviders.RabbitMq, title: "RabbitMQ" + }, + { + icon: , + provider: DynamicSecretProviders.AzureEntraId, + title: "Azure Entra ID", + hasSetupStep: true } ]; @@ -114,7 +123,7 @@ export const CreateDynamicSecretForm = ({ >
Select a service to connect to:
- {DYNAMIC_SECRET_LIST.map(({ icon, provider, title }) => ( + {DYNAMIC_SECRET_LIST.map(({ icon, provider, title, hasSetupStep }) => (
{ setSelectedProvider(provider); - setWizardStep(WizardSteps.ProviderInputs); + if(hasSetupStep){ + setWizardStep(WizardSteps.ProviderSetup); + } else { + setWizardStep(WizardSteps.ProviderInputs); + } }} onKeyDown={(evt) => { if (evt.key === "Enter") { setSelectedProvider(provider); setWizardStep(WizardSteps.ProviderInputs); } + if(hasSetupStep){ + setWizardStep(WizardSteps.ProviderSetup); + } else { + setWizardStep(WizardSteps.ProviderInputs); + } }} > {icon} @@ -138,6 +156,23 @@ export const CreateDynamicSecretForm = ({
)} + { + wizardStep === WizardSteps.ProviderSetup && selectedProvider === DynamicSecretProviders.AzureEntraId + && ( + + { setWizardStep(WizardSteps.ProviderInputs) }} + onCancel={handleFormReset} + /> + + ) + } {wizardStep === WizardSteps.ProviderInputs && selectedProvider === DynamicSecretProviders.SqlDatabase && ( )} + {wizardStep === WizardSteps.ProviderInputs && + selectedProvider === DynamicSecretProviders.AzureEntraId && ( + + + + ) + } diff --git a/frontend/src/views/SecretMainPage/components/DynamicSecretListView/CreateDynamicSecretLease.tsx b/frontend/src/views/SecretMainPage/components/DynamicSecretListView/CreateDynamicSecretLease.tsx index 85be20fc1..4c60c744b 100644 --- a/frontend/src/views/SecretMainPage/components/DynamicSecretListView/CreateDynamicSecretLease.tsx +++ b/frontend/src/views/SecretMainPage/components/DynamicSecretListView/CreateDynamicSecretLease.tsx @@ -176,6 +176,24 @@ const renderOutputForm = (provider: DynamicSecretProviders, data: unknown) => { ); } + if (provider === DynamicSecretProviders.AzureEntraId) { + const { userId, password } = data as { + userId: string; + password: string; + }; + + return ( +
+ + +
+ ); + } + return null; };