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,51 +97,42 @@ 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: { `${MSFT_GRAPH_API_URL}/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",
headers: { Authorization: `Bearer ${data.token}`
"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
}
};
} }
);
default: if (response.status !== 200) {
throw new BadRequestError({ message: "Unknown data to fetch" }); 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 { 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) => {
@@ -165,7 +167,7 @@ export const AzureEntraIdInputForm = ({
</div> </div>
<div> <div>
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200"> <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> <Link href="https://infisical.com/docs/documentation/platform/dynamic-secrets/azure-entra-id" passHref>
<a target="_blank" rel="noopener noreferrer"> <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"> <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>
<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">
{ <Controller
configurationComplete && !isError && !isFetching && isFetched && data && control={control}
<Controller name="selectedUsers"
control={control} render={({ field: { value, onChange }, fieldState: { error } }) => (
name="selectedUsers" <FormControl
render={({ field: { value, onChange }, fieldState: { error } }) => ( isRequired
<FormControl isError={Boolean(error)}
isRequired errorText={error?.message}
isError={Boolean(error)} >
errorText={error?.message} <DropdownMenu >
> <DropdownMenuTrigger
<DropdownMenu> className="w-72"
<DropdownMenuTrigger asChild className="w-72"> disabled={loading || errored || !configurationComplete}
<Input >
isReadOnly <Tooltip
value={value?.length ? `${value.length} selected` : "None"} hidden={!loading && !errored && configurationComplete}
className="text-left" content=
/> {
</DropdownMenuTrigger> <div>
<DropdownMenuContent align="start" {(() => {
style={{ width: "var(--radix-dropdown-menu-trigger-width)" }} 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) => { <div>
const ids = value?.map((selectedUser) => selectedUser.id)
const isChecked = ids?.includes(user.id); <Input
return ( isReadOnly
<DropdownMenuItem value={value?.length ? `${value.length} selected` : ""}
onClick={(evt) => { className={`text-left ${loading || errored || !configurationComplete ? "cursor-not-allowed" : ""}`}
evt.preventDefault(); placeholder="Select users"
onChange( />
isChecked </div>
? value?.filter((el) => el.id !== user.id) </Tooltip>
: [...(value || []), user] </DropdownMenuTrigger>
); <DropdownMenuContent align="start"
}} style={{ width: "var(--radix-dropdown-menu-trigger-width)" }}
key={`create-policy-members-${user.id}`} >
iconPos="right" {data && data.map((user) => {
icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />} const ids = value?.map((selectedUser) => selectedUser.id)
> const isChecked = ids?.includes(user.id);
{user.name} <br /> {`(${user.email})`} return (
</DropdownMenuItem> <DropdownMenuItem
); onClick={(evt) => {
})} evt.preventDefault();
</DropdownMenuContent> onChange(
</DropdownMenu> isChecked
</FormControl> ? value?.filter((el) => el.id !== user.id)
)} : [...(value || []), user]
/> );
} }}
{ key={`create-policy-members-${user.id}`}
configurationComplete && isFetching && (<div className="pl-3 pb-2 w-full flex items-center" ><Spinner size="xs" /><p> &nbsp; Loading </p></div>) iconPos="right"
} icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />}
{ >
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>) {user.name} <br /> {`(${user.email})`}
} </DropdownMenuItem>
{ );
!configurationComplete && (<div className="pl-3 pb-2 w-full flex items-center" ><FontAwesomeIcon icon={faWarning} /><p> &nbsp; Complete configuration to fetch users</p></div>) })}
} </DropdownMenuContent>
</DropdownMenu>
</FormControl>
)}
/>
</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>
)
}