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

View File

@@ -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;
}
});

View File

@@ -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
};
};

View File

@@ -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<User[]>;
} => {
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
};
};

View File

@@ -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<unknown>;
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 }>;
};

View File

@@ -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
});

View File

@@ -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 = ({
</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">
@@ -247,64 +249,94 @@ export const AzureEntraIdInputForm = ({
</div>
<div className="flex flex-col">
<div className="flex items-center space-x-4">
{
configurationComplete && !isError && !isFetching && isFetched && data &&
<Controller
control={control}
name="selectedUsers"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isRequired
isError={Boolean(error)}
errorText={error?.message}
>
<DropdownMenu>
<DropdownMenuTrigger asChild className="w-72">
<Input
isReadOnly
value={value?.length ? `${value.length} selected` : "None"}
className="text-left"
/>
</DropdownMenuTrigger>
<DropdownMenuContent align="start"
style={{ width: "var(--radix-dropdown-menu-trigger-width)" }}
<Controller
control={control}
name="selectedUsers"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isRequired
isError={Boolean(error)}
errorText={error?.message}
>
<DropdownMenu >
<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>
}
>
{data.map((user) => {
const ids = value?.map((selectedUser) => selectedUser.id)
const isChecked = ids?.includes(user.id);
return (
<DropdownMenuItem
onClick={(evt) => {
evt.preventDefault();
onChange(
isChecked
? value?.filter((el) => el.id !== user.id)
: [...(value || []), user]
);
}}
key={`create-policy-members-${user.id}`}
iconPos="right"
icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />}
>
{user.name} <br /> {`(${user.email})`}
</DropdownMenuItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
</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>
<Input
isReadOnly
value={value?.length ? `${value.length} selected` : ""}
className={`text-left ${loading || errored || !configurationComplete ? "cursor-not-allowed" : ""}`}
placeholder="Select users"
/>
</div>
</Tooltip>
</DropdownMenuTrigger>
<DropdownMenuContent align="start"
style={{ width: "var(--radix-dropdown-menu-trigger-width)" }}
>
{data && data.map((user) => {
const ids = value?.map((selectedUser) => selectedUser.id)
const isChecked = ids?.includes(user.id);
return (
<DropdownMenuItem
onClick={(evt) => {
evt.preventDefault();
onChange(
isChecked
? value?.filter((el) => el.id !== user.id)
: [...(value || []), user]
);
}}
key={`create-policy-members-${user.id}`}
iconPos="right"
icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />}
>
{user.name} <br /> {`(${user.email})`}
</DropdownMenuItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
</FormControl>
)}
/>
</div>
</div>
</div>

View File

@@ -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>
)
}