chore: Refactor and improve UI

This commit is contained in:
Meet
2024-09-20 12:29:26 +05:30
parent 040fa511f6
commit 10574bfe26
7 changed files with 169 additions and 215 deletions
@@ -3,10 +3,7 @@ import ms from "ms";
import { z } from "zod"; import { z } from "zod";
import { DynamicSecretLeasesSchema } from "@app/db/schemas"; import { DynamicSecretLeasesSchema } from "@app/db/schemas";
import { import { DynamicSecretProviderSchema } from "@app/ee/services/dynamic-secret/providers/models";
DynamicSecretDataFetchTypes,
DynamicSecretProviderSchema
} from "@app/ee/services/dynamic-secret/providers/models";
import { DYNAMIC_SECRETS } from "@app/lib/api-docs"; import { DYNAMIC_SECRETS } from "@app/lib/api-docs";
import { daysToMillisecond } from "@app/lib/dates"; import { daysToMillisecond } from "@app/lib/dates";
import { removeTrailingSlash } from "@app/lib/fn"; import { removeTrailingSlash } from "@app/lib/fn";
@@ -82,26 +79,33 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
server.route({ server.route({
method: "POST", method: "POST",
url: "/fetch-provider-data", url: "/entra-id/users",
config: { config: {
rateLimit: readLimit rateLimit: readLimit
}, },
schema: { schema: {
body: z.object({ body: z.object({
dataFetchType: z.string().min(1).describe("Type of data to fetch"), tenantId: z.string().min(1).describe("The tenant ID of the Azure Entra ID"),
provider: DynamicSecretProviderSchema.describe(DYNAMIC_SECRETS.CREATE.provider) 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: { response: {
200: z.object({ 200: z
data: z.unknown() .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]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const dataFetchType = const data = await server.services.dynamicSecret.fetchAzureEntraIdUsers({
DynamicSecretDataFetchTypes[req.body.dataFetchType as keyof typeof DynamicSecretDataFetchTypes]; tenantId: req.body.tenantId,
const data = await server.services.dynamicSecret.fetchData({ provider: req.body.provider, dataFetchType }); applicationId: req.body.applicationId,
clientSecret: req.body.clientSecret
});
return data; return data;
} }
}); });
@@ -17,10 +17,10 @@ import {
TCreateDynamicSecretDTO, TCreateDynamicSecretDTO,
TDeleteDynamicSecretDTO, TDeleteDynamicSecretDTO,
TDetailsDynamicSecretDTO, TDetailsDynamicSecretDTO,
TDynamicSecretsFetchDataDTO,
TListDynamicSecretsDTO, TListDynamicSecretsDTO,
TUpdateDynamicSecretDTO TUpdateDynamicSecretDTO
} from "./dynamic-secret-types"; } from "./dynamic-secret-types";
import { AzureEntraIDProvider } from "./providers/azure-entra-id";
import { DynamicSecretProviders, TDynamicProviderFns } from "./providers/models"; import { DynamicSecretProviders, TDynamicProviderFns } from "./providers/models";
type TDynamicSecretServiceFactoryDep = { type TDynamicSecretServiceFactoryDep = {
@@ -333,12 +333,21 @@ export const dynamicSecretServiceFactory = ({
return dynamicSecretCfg; return dynamicSecretCfg;
}; };
const fetchData = async ({ provider, dataFetchType }: TDynamicSecretsFetchDataDTO) => { const fetchAzureEntraIdUsers = async ({
const selectedProvider = dynamicSecretProviders[provider.type]; tenantId,
if (selectedProvider.fetchData) { applicationId,
const data = selectedProvider.fetchData(provider.inputs, dataFetchType); clientSecret
return data; }: {
} tenantId: string;
applicationId: string;
clientSecret: string;
}) => {
const azureEntraIdUsers = await AzureEntraIDProvider().fetchAzureEntraIdUsers(
tenantId,
applicationId,
clientSecret
);
return azureEntraIdUsers;
}; };
return { return {
@@ -347,6 +356,6 @@ export const dynamicSecretServiceFactory = ({
deleteByName, deleteByName,
getDetails, getDetails,
list, list,
fetchData fetchAzureEntraIdUsers
}; };
}; };
@@ -3,7 +3,7 @@ import { customAlphabet } from "nanoid";
import { BadRequestError } from "@app/lib/errors"; 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_GRAPH_API_URL = "https://graph.microsoft.com/v1.0/";
const MSFT_LOGIN_URL = "https://login.microsoftonline.com"; const MSFT_LOGIN_URL = "https://login.microsoftonline.com";
@@ -13,7 +13,11 @@ const generatePassword = () => {
return customAlphabet(charset, 64)(); 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<User[]>;
} => {
const validateProviderInputs = async (inputs: unknown) => { const validateProviderInputs = async (inputs: unknown) => {
const providerInputs = await AzureEntraIDSchema.parseAsync(inputs); const providerInputs = await AzureEntraIDSchema.parseAsync(inputs);
return providerInputs; return providerInputs;
@@ -93,17 +97,13 @@ export const AzureEntraIDProvider = (): TDynamicProviderFns => {
return { entityId }; return { entityId };
}; };
const fetchData = async (inputs: unknown, toFetch: DynamicSecretDataFetchTypes) => { const fetchAzureEntraIdUsers = async (tenantId: string, applicationId: string, clientSecret: string) => {
const providerInputs = await validateProviderInputs(inputs); const data = await getToken(tenantId, applicationId, clientSecret);
const data = await getToken(providerInputs.tenantId, providerInputs.applicationId, providerInputs.clientSecret);
if (!data.success) { if (!data.success) {
throw new BadRequestError({ message: "Failed to authorize to Microsoft Entra ID" }); throw new BadRequestError({ message: "Failed to authorize to Microsoft Entra ID" });
} }
switch (toFetch) { const response = await axios.get<{ value: [{ id: string; displayName: string; userPrincipalName: string }] }>(
case DynamicSecretDataFetchTypes.Users: {
const response = await axios.get<{ value: [{ displayName: string; id: string; userPrincipalName: string }] }>(
`${MSFT_GRAPH_API_URL}/users`, `${MSFT_GRAPH_API_URL}/users`,
{ {
headers: { headers: {
@@ -112,32 +112,27 @@ export const AzureEntraIDProvider = (): TDynamicProviderFns => {
} }
} }
); );
const users = response.data.value.map(
(user: { displayName: string; id: string; userPrincipalName: string }) => { if (response.status !== 200) {
throw new BadRequestError({ message: "Failed to fetch users" });
}
const users = response.data.value.map((user) => {
return { return {
name: user.displayName, name: user.displayName,
id: user.id, id: user.id,
email: user.userPrincipalName email: user.userPrincipalName
}; };
} });
); return users;
return {
data: {
users
}
}; };
}
default:
throw new BadRequestError({ message: "Unknown data to fetch" });
}
};
return { return {
validateProviderInputs, validateProviderInputs,
validateConnection, validateConnection,
create, create,
revoke, revoke,
renew, renew,
fetchData fetchAzureEntraIdUsers
}; };
}; };
@@ -187,10 +187,6 @@ export enum DynamicSecretProviders {
AzureEntraID = "azure-entra-id" AzureEntraID = "azure-entra-id"
} }
export enum DynamicSecretDataFetchTypes {
Users = "users"
}
export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [ export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
z.object({ type: z.literal(DynamicSecretProviders.SqlDatabase), inputs: DynamicSecretSqlDBSchema }), z.object({ type: z.literal(DynamicSecretProviders.SqlDatabase), inputs: DynamicSecretSqlDBSchema }),
z.object({ type: z.literal(DynamicSecretProviders.Cassandra), inputs: DynamicSecretCassandraSchema }), z.object({ type: z.literal(DynamicSecretProviders.Cassandra), inputs: DynamicSecretCassandraSchema }),
@@ -210,5 +206,4 @@ export type TDynamicProviderFns = {
validateProviderInputs: (inputs: object) => Promise<unknown>; validateProviderInputs: (inputs: object) => Promise<unknown>;
revoke: (inputs: unknown, entityId: string) => Promise<{ entityId: string }>; revoke: (inputs: unknown, entityId: string) => Promise<{ entityId: string }>;
renew: (inputs: unknown, entityId: string, expireAt: number) => Promise<{ entityId: string }>; renew: (inputs: unknown, entityId: string, expireAt: number) => Promise<{ entityId: string }>;
fetchData?: (inputs: unknown, toFetch: DynamicSecretDataFetchTypes) => Promise<{ data: unknown }>;
}; };
+12 -16
View File
@@ -6,7 +6,6 @@ import { apiRequest } from "@app/config/request";
import { import {
TDetailsDynamicSecretDTO, TDetailsDynamicSecretDTO,
TDynamicSecret, TDynamicSecret,
TDynamicSecretProvider,
TGetDynamicSecretsByEnvsDTO, TGetDynamicSecretsByEnvsDTO,
TListDynamicSecretDTO TListDynamicSecretDTO
} from "./types"; } from "./types";
@@ -22,12 +21,6 @@ export const dynamicSecretKeys = {
[{ projectSlug, path, environmentSlug, name }, "dynamic-secret-details"] as const [{ projectSlug, path, environmentSlug, name }, "dynamic-secret-details"] as const
}; };
type EntraIDUser = {
name: string;
id: string;
email: string;
};
export const useGetDynamicSecrets = ({ export const useGetDynamicSecrets = ({
projectSlug, projectSlug,
environmentSlug, environmentSlug,
@@ -79,25 +72,28 @@ export const useGetDynamicSecretDetails = ({
}; };
export const useGetDynamicSecretProviderData = ({ export const useGetDynamicSecretProviderData = ({
provider, tenantId,
dataFetchType, applicationId,
clientSecret,
enabled enabled
}: { }: {
provider: TDynamicSecretProvider, tenantId: string;
dataFetchType: "Users", applicationId: string;
clientSecret: string;
enabled: boolean enabled: boolean
}) => { }) => {
return useQuery({ return useQuery({
queryKey: ["users"], queryKey: ["users"],
queryFn: async () => { queryFn: async () => {
const { data } = await apiRequest.post<{ data: { users: [EntraIDUser] } }>( const { data } = await apiRequest.post<{id:string, email: string, name:string}[]>(
"/api/v1/dynamic-secrets/fetch-provider-data", "/api/v1/dynamic-secrets/entra-id/users",
{ {
provider, tenantId,
dataFetchType applicationId,
clientSecret
} }
); );
return data.data.users; return data;
}, },
enabled enabled
}); });
@@ -11,10 +11,10 @@ import { createNotification } from "@app/components/notifications";
import { import {
Button, Button,
FormControl, FormControl,
Input, Input
Spinner,
} from "@app/components/v2"; } from "@app/components/v2";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@app/components/v2/Dropdown/Dropdown"; 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 { useCreateDynamicSecret } from "@app/hooks/api";
import { useGetDynamicSecretProviderData } from "@app/hooks/api/dynamicSecret/queries"; import { useGetDynamicSecretProviderData } from "@app/hooks/api/dynamicSecret/queries";
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
@@ -81,8 +81,10 @@ export const AzureEntraIdInputForm = ({
const applicationId = watch("provider.applicationId"); const applicationId = watch("provider.applicationId");
const clientSecret = watch("provider.clientSecret"); const clientSecret = watch("provider.clientSecret");
const configurationComplete = tenantId && applicationId && 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 { data, isLoading, isError, isFetching } = useGetDynamicSecretProviderData({ tenantId, applicationId, clientSecret, enabled: !!configurationComplete });
const loading = configurationComplete && isFetching;
const errored = configurationComplete && !isFetching && isError;
const createDynamicSecret = useCreateDynamicSecret(); const createDynamicSecret = useCreateDynamicSecret();
const handleCreateDynamicSecret = async ({ name, selectedUsers, provider, maxTTL, defaultTTL }: TForm) => { const handleCreateDynamicSecret = async ({ name, selectedUsers, provider, maxTTL, defaultTTL }: TForm) => {
@@ -247,8 +249,6 @@ export const AzureEntraIdInputForm = ({
</div> </div>
<div className="flex flex-col"> <div className="flex flex-col">
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
{
configurationComplete && !isError && !isFetching && isFetched && data &&
<Controller <Controller
control={control} control={control}
name="selectedUsers" name="selectedUsers"
@@ -259,17 +259,59 @@ export const AzureEntraIdInputForm = ({
errorText={error?.message} errorText={error?.message}
> >
<DropdownMenu > <DropdownMenu >
<DropdownMenuTrigger asChild className="w-72"> <DropdownMenuTrigger
className="w-72"
disabled={loading || errored || !configurationComplete}
>
<Tooltip
hidden={!loading && !errored && configurationComplete}
content=
{
<div>
{(() => {
let icon;
if (errored) {
icon = <FontAwesomeIcon icon={faWarning} color="red" />;
} else if (loading || !configurationComplete) {
icon = <FontAwesomeIcon icon={faWarning} color="yellow" />;
} else {
icon = null;
}
return icon;
})()}
<span className="ml-4 cursor-default text-mineshaft-300 hover:text-mineshaft-200">
{(() => {
let message;
if (loading) {
message = "Loading, please wait...";
} else if (errored) {
message = "Check the configuration";
} else if (!configurationComplete) {
message = "Configuration incomplete";
} else {
message = ""; // or you can leave it undefined
}
return message;
})()}
</span>
</div>
}
>
<div>
<Input <Input
isReadOnly isReadOnly
value={value?.length ? `${value.length} selected` : "None"} value={value?.length ? `${value.length} selected` : ""}
className="text-left" className={`text-left ${loading || errored || !configurationComplete ? "cursor-not-allowed" : ""}`}
placeholder="Select users"
/> />
</div>
</Tooltip>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" <DropdownMenuContent align="start"
style={{ width: "var(--radix-dropdown-menu-trigger-width)" }} style={{ width: "var(--radix-dropdown-menu-trigger-width)" }}
> >
{data.map((user) => { {data && data.map((user) => {
const ids = value?.map((selectedUser) => selectedUser.id) const ids = value?.map((selectedUser) => selectedUser.id)
const isChecked = ids?.includes(user.id); const isChecked = ids?.includes(user.id);
return ( return (
@@ -295,16 +337,6 @@ export const AzureEntraIdInputForm = ({
</FormControl> </FormControl>
)} )}
/> />
}
{
configurationComplete && isFetching && (<div className="pl-3 pb-2 w-full flex items-center" ><Spinner size="xs" /><p> &nbsp; Loading </p></div>)
}
{
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>)
}
{
!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>
</div> </div>
</div> </div>
@@ -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 (
<div className="flex h-screen flex-col justify-between overflow-auto bg-gradient-to-tr from-mineshaft-700 to-bunker-800 text-gray-200 dark:[color-scheme:dark]">
<div />
<div className="mx-auto w-full max-w-xl px-4 py-4 md:px-0">
<div className="mb-8 text-center">
<div className="mb-4 flex justify-center pt-8">
<Link href="https://infisical.com">
<Image
src="/images/gradientLogo.svg"
height={90}
width={120}
alt="Infisical logo"
className="cursor-pointer"
/>
</Link>
</div>
</div>
<div className="m-auto my-8 flex w-full" />
<div className="m-auto mb-2 flex w-full flex-col rounded-md border bg-white/[0.05] border-white-500/30 p-6 pt-5">
<div className="flex flex-col items-start sm:flex-row sm:items-center">
<p className="md:text-md text-md mr-4 w-full">
<p
className="text-bold text-white bg-clip-text text-transparent"
>
Copy Tenant ID add and paste it in the dynamic secret configuration.
</p>{" "}
</p>
</div>
</div>
<div className="m-auto flex w-full flex-col rounded-md border border-primary-500/30 bg-primary/5 p-6 pt-5">
<div className="flex flex-col items-start sm:flex-row sm:items-center">
<p className="md:text-md text-md mr-4 w-full">
<p
className="text-bold bg-gradient-to-tr from-yellow-500 to-primary-500 bg-clip-text text-transparent"
>
Tenant ID
</p>{" "}
<br />
<div className="w-full mr-2 flex items-center rounded-md bg-white/[0.05] p-2 text-base text-gray-400">
<p className="mr-4 break-all text-left">{router.query.tenant}</p>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative ml-auto"
onClick={() => {
if (typeof router.query.tenant === "string") {
navigator.clipboard.writeText(router.query.tenant);
createNotification({
title: "Copied Tenant ID to clipboard succesfully",
type: "success",
text: ""
});
}
}}
>
<FontAwesomeIcon icon={faCopy} />
</IconButton>
</div>
</p>
</div>
</div>
</div>
<div className="w-full bg-mineshaft-600 p-2" />
</div>
)
}