From 10574bfe267e8219ee9bef8f90ea1629268ce6d9 Mon Sep 17 00:00:00 2001 From: Meet Date: Fri, 20 Sep 2024 12:29:26 +0530 Subject: [PATCH] chore: Refactor and improve UI --- .../src/ee/routes/v1/dynamic-secret-router.ts | 30 ++-- .../dynamic-secret/dynamic-secret-service.ts | 25 ++- .../providers/azure-entra-id.ts | 63 ++++--- .../dynamic-secret/providers/models.ts | 5 - .../src/hooks/api/dynamicSecret/queries.ts | 28 ++-- .../AzureEntraIdInputForm.tsx | 156 +++++++++++------- .../callback/AzureEntraIdCallbackPage.tsx | 77 --------- 7 files changed, 169 insertions(+), 215 deletions(-) delete mode 100644 frontend/src/views/callback/AzureEntraIdCallbackPage.tsx diff --git a/backend/src/ee/routes/v1/dynamic-secret-router.ts b/backend/src/ee/routes/v1/dynamic-secret-router.ts index 952386a31..d48aa1dfd 100644 --- a/backend/src/ee/routes/v1/dynamic-secret-router.ts +++ b/backend/src/ee/routes/v1/dynamic-secret-router.ts @@ -3,10 +3,7 @@ import ms from "ms"; import { z } from "zod"; import { DynamicSecretLeasesSchema } from "@app/db/schemas"; -import { - DynamicSecretDataFetchTypes, - DynamicSecretProviderSchema -} from "@app/ee/services/dynamic-secret/providers/models"; +import { 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"; @@ -82,26 +79,33 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) => server.route({ method: "POST", - url: "/fetch-provider-data", + url: "/entra-id/users", 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) + tenantId: z.string().min(1).describe("The tenant ID of the Azure Entra ID"), + applicationId: z.string().min(1).describe("The application ID of the Azure Entra ID App Registration"), + clientSecret: z.string().min(1).describe("The client secret of the Azure Entra ID App Registration") }), response: { - 200: z.object({ - data: z.unknown() - }) + 200: z + .object({ + name: z.string().min(1).describe("The name of the user"), + id: z.string().min(1).describe("The ID of the user"), + email: z.string().min(1).describe("The email of the user") + }) + .array() } }, 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 }); + const data = await server.services.dynamicSecret.fetchAzureEntraIdUsers({ + tenantId: req.body.tenantId, + applicationId: req.body.applicationId, + clientSecret: req.body.clientSecret + }); return data; } }); 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 9127a45d2..c44b2fecd 100644 --- a/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts +++ b/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts @@ -17,10 +17,10 @@ import { TCreateDynamicSecretDTO, TDeleteDynamicSecretDTO, TDetailsDynamicSecretDTO, - TDynamicSecretsFetchDataDTO, TListDynamicSecretsDTO, TUpdateDynamicSecretDTO } from "./dynamic-secret-types"; +import { AzureEntraIDProvider } from "./providers/azure-entra-id"; import { DynamicSecretProviders, TDynamicProviderFns } from "./providers/models"; type TDynamicSecretServiceFactoryDep = { @@ -333,12 +333,21 @@ 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; - } + const fetchAzureEntraIdUsers = async ({ + tenantId, + applicationId, + clientSecret + }: { + tenantId: string; + applicationId: string; + clientSecret: string; + }) => { + const azureEntraIdUsers = await AzureEntraIDProvider().fetchAzureEntraIdUsers( + tenantId, + applicationId, + clientSecret + ); + return azureEntraIdUsers; }; return { @@ -347,6 +356,6 @@ export const dynamicSecretServiceFactory = ({ deleteByName, getDetails, list, - fetchData + fetchAzureEntraIdUsers }; }; 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 e12cbbfa8..e2dfe2d4b 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 @@ -3,7 +3,7 @@ import { customAlphabet } from "nanoid"; import { BadRequestError } from "@app/lib/errors"; -import { AzureEntraIDSchema, DynamicSecretDataFetchTypes, TDynamicProviderFns } from "./models"; +import { AzureEntraIDSchema, TDynamicProviderFns } from "./models"; const MSFT_GRAPH_API_URL = "https://graph.microsoft.com/v1.0/"; const MSFT_LOGIN_URL = "https://login.microsoftonline.com"; @@ -13,7 +13,11 @@ const generatePassword = () => { return customAlphabet(charset, 64)(); }; -export const AzureEntraIDProvider = (): TDynamicProviderFns => { +type User = { name: string; id: string; email: string }; + +export const AzureEntraIDProvider = (): TDynamicProviderFns & { + fetchAzureEntraIdUsers: (tenantId: string, applicationId: string, clientSecret: string) => Promise; +} => { const validateProviderInputs = async (inputs: unknown) => { const providerInputs = await AzureEntraIDSchema.parseAsync(inputs); return providerInputs; @@ -93,51 +97,42 @@ export const AzureEntraIDProvider = (): TDynamicProviderFns => { return { entityId }; }; - const fetchData = async (inputs: unknown, toFetch: DynamicSecretDataFetchTypes) => { - const providerInputs = await validateProviderInputs(inputs); - - const data = await getToken(providerInputs.tenantId, providerInputs.applicationId, providerInputs.clientSecret); + const fetchAzureEntraIdUsers = async (tenantId: string, applicationId: string, clientSecret: string) => { + const data = await getToken(tenantId, applicationId, clientSecret); 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; userPrincipalName: 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; userPrincipalName: string }) => { - return { - name: user.displayName, - id: user.id, - email: user.userPrincipalName - }; - } - ); - return { - data: { - users - } - }; + const response = await axios.get<{ value: [{ id: string; displayName: string; userPrincipalName: string }] }>( + `${MSFT_GRAPH_API_URL}/users`, + { + headers: { + "Content-Type": "application/x-www-form-urlencoded", + Authorization: `Bearer ${data.token}` + } } + ); - default: - throw new BadRequestError({ message: "Unknown data to fetch" }); + if (response.status !== 200) { + throw new BadRequestError({ message: "Failed to fetch users" }); } + + const users = response.data.value.map((user) => { + return { + name: user.displayName, + id: user.id, + email: user.userPrincipalName + }; + }); + return users; }; + return { validateProviderInputs, validateConnection, create, revoke, renew, - fetchData + fetchAzureEntraIdUsers }; }; diff --git a/backend/src/ee/services/dynamic-secret/providers/models.ts b/backend/src/ee/services/dynamic-secret/providers/models.ts index 04bf858fe..18a7b3dc9 100644 --- a/backend/src/ee/services/dynamic-secret/providers/models.ts +++ b/backend/src/ee/services/dynamic-secret/providers/models.ts @@ -187,10 +187,6 @@ export enum DynamicSecretProviders { AzureEntraID = "azure-entra-id" } -export enum DynamicSecretDataFetchTypes { - Users = "users" -} - export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [ z.object({ type: z.literal(DynamicSecretProviders.SqlDatabase), inputs: DynamicSecretSqlDBSchema }), z.object({ type: z.literal(DynamicSecretProviders.Cassandra), inputs: DynamicSecretCassandraSchema }), @@ -210,5 +206,4 @@ 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/frontend/src/hooks/api/dynamicSecret/queries.ts b/frontend/src/hooks/api/dynamicSecret/queries.ts index e17faf203..f84fd0712 100644 --- a/frontend/src/hooks/api/dynamicSecret/queries.ts +++ b/frontend/src/hooks/api/dynamicSecret/queries.ts @@ -6,7 +6,6 @@ import { apiRequest } from "@app/config/request"; import { TDetailsDynamicSecretDTO, TDynamicSecret, - TDynamicSecretProvider, TGetDynamicSecretsByEnvsDTO, TListDynamicSecretDTO } from "./types"; @@ -22,12 +21,6 @@ export const dynamicSecretKeys = { [{ projectSlug, path, environmentSlug, name }, "dynamic-secret-details"] as const }; -type EntraIDUser = { - name: string; - id: string; - email: string; -}; - export const useGetDynamicSecrets = ({ projectSlug, environmentSlug, @@ -79,25 +72,28 @@ export const useGetDynamicSecretDetails = ({ }; export const useGetDynamicSecretProviderData = ({ - provider, - dataFetchType, + tenantId, + applicationId, + clientSecret, enabled }: { - provider: TDynamicSecretProvider, - dataFetchType: "Users", + tenantId: string; + applicationId: string; + clientSecret: string; enabled: boolean }) => { return useQuery({ queryKey: ["users"], queryFn: async () => { - const { data } = await apiRequest.post<{ data: { users: [EntraIDUser] } }>( - "/api/v1/dynamic-secrets/fetch-provider-data", + const { data } = await apiRequest.post<{id:string, email: string, name:string}[]>( + "/api/v1/dynamic-secrets/entra-id/users", { - provider, - dataFetchType + tenantId, + applicationId, + clientSecret } ); - return data.data.users; + return data; }, enabled }); diff --git a/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/AzureEntraIdInputForm.tsx b/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/AzureEntraIdInputForm.tsx index 25302db20..2fb2250a0 100644 --- a/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/AzureEntraIdInputForm.tsx +++ b/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/AzureEntraIdInputForm.tsx @@ -11,10 +11,10 @@ import { createNotification } from "@app/components/notifications"; import { Button, FormControl, - Input, - Spinner, + Input } from "@app/components/v2"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@app/components/v2/Dropdown/Dropdown"; +import { Tooltip } from "@app/components/v2/Tooltip"; import { useCreateDynamicSecret } from "@app/hooks/api"; import { useGetDynamicSecretProviderData } from "@app/hooks/api/dynamicSecret/queries"; import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; @@ -81,8 +81,10 @@ export const AzureEntraIdInputForm = ({ 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 configurationComplete = !!(tenantId && applicationId && clientSecret); + const { data, isLoading, isError, isFetching } = useGetDynamicSecretProviderData({ tenantId, applicationId, clientSecret, enabled: !!configurationComplete }); + const loading = configurationComplete && isFetching; + const errored = configurationComplete && !isFetching && isError; const createDynamicSecret = useCreateDynamicSecret(); const handleCreateDynamicSecret = async ({ name, selectedUsers, provider, maxTTL, defaultTTL }: TForm) => { @@ -165,7 +167,7 @@ export const AzureEntraIdInputForm = ({
- Configuration + Configuration
@@ -247,64 +249,94 @@ export const AzureEntraIdInputForm = ({
- { - configurationComplete && !isError && !isFetching && isFetched && data && - ( - - - - - - ( + + + +
+ } > - {data.map((user) => { - const ids = value?.map((selectedUser) => selectedUser.id) - const isChecked = ids?.includes(user.id); - return ( - { - evt.preventDefault(); - onChange( - isChecked - ? value?.filter((el) => el.id !== user.id) - : [...(value || []), user] - ); - }} - key={`create-policy-members-${user.id}`} - iconPos="right" - icon={isChecked && } - > - {user.name}
{`(${user.email})`} -
- ); - })} - - - - )} - /> - } - { - configurationComplete && isFetching && (

  Loading

) - } - { - configurationComplete && !isFetching && isError && (

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

) - } - { - !configurationComplete && (

  Complete configuration to fetch users

) - } +
+ + +
+ + + + {data && data.map((user) => { + const ids = value?.map((selectedUser) => selectedUser.id) + const isChecked = ids?.includes(user.id); + return ( + { + evt.preventDefault(); + onChange( + isChecked + ? value?.filter((el) => el.id !== user.id) + : [...(value || []), user] + ); + }} + key={`create-policy-members-${user.id}`} + iconPos="right" + icon={isChecked && } + > + {user.name}
{`(${user.email})`} +
+ ); + })} +
+ + + )} + />
diff --git a/frontend/src/views/callback/AzureEntraIdCallbackPage.tsx b/frontend/src/views/callback/AzureEntraIdCallbackPage.tsx deleted file mode 100644 index 5950bfa79..000000000 --- a/frontend/src/views/callback/AzureEntraIdCallbackPage.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import Image from "next/image" -import Link from "next/link" -import { useRouter } from "next/router"; -import { faCopy } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; - -import { createNotification } from "@app/components/notifications"; -import { IconButton } from "@app/components/v2"; - -export const AzureEntraIdCallbackPage = () => { - const router = useRouter(); - return ( -
-
-
-
-
- - Infisical logo - -
-
-
-
-
-

-

- Copy Tenant ID add and paste it in the dynamic secret configuration. -

{" "} -

-
-
-
-
-

-

- Tenant ID -

{" "} -
-
-

{router.query.tenant}

- { - if (typeof router.query.tenant === "string") { - navigator.clipboard.writeText(router.query.tenant); - createNotification({ - title: "Copied Tenant ID to clipboard succesfully", - type: "success", - text: "" - }); - } - }} - > - - -
-

-
-
-
-
-
- ) -} \ No newline at end of file