feat: Add callback and edit dynamic secret for Azure Entra ID

This commit is contained in:
Meet
2024-09-18 01:33:04 +05:30
parent b3f0d36ddc
commit 342c713805
13 changed files with 555 additions and 167 deletions
@@ -1,5 +1,7 @@
import { useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { faWarning } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import ms from "ms";
import { z } from "zod";
@@ -11,12 +13,16 @@ import {
Button,
FormControl,
Input,
Spinner,
} 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({
provider: z.object({
tenantId: z.string().min(1),
}),
defaultTTL: z.string().superRefine((val, ctx) => {
const valMs = ms(val);
if (valMs < 60 * 1000)
@@ -49,6 +55,12 @@ type Props = {
environment: string;
};
type EntraIDUser = {
name: string;
id: string;
email: string;
};
export const AzureEntraIdInputForm = ({
onCompleted,
onCancel,
@@ -59,24 +71,39 @@ export const AzureEntraIdInputForm = ({
const {
control,
formState: { isSubmitting },
watch,
handleSubmit
} = useForm<TForm>({
resolver: zodResolver(formSchema)
});
const [selectedUsers, setSelectedUsers] = useState([]);
const users = useGetDynamicSecretProviderData({ provider: { type: DynamicSecretProviders.AzureEntraId, inputs: { tenantId: "<tenant id from callback>", userId: "test" } }, dataFetchType: "Users" }).data
const tenantId = watch("provider.tenantId");
const [selectedUsers, setSelectedUsers] = useState<EntraIDUser[]>([]);
const { data, isLoading, isFetched, isError, isFetching } = useGetDynamicSecretProviderData({ dataFetchType: "Users", provider: {type: DynamicSecretProviders.AzureEntraId, inputs: { userId : "unused", email: "unused", tenantId }}, enabled: !!tenantId });
const mappedUserList = data?.map((user) => ({
primaryText: user.name,
secondaryText: user.email,
id: user.id,
}));
const handleChange = (selected: { primaryText: string; secondaryText: string; id: string }[]) => {
// Map back to EntraIDUser based on the id or other attributes
const selectedMappedUsers = data?.filter(user =>
selected.some(selectedItem => selectedItem.id === user.id)
);
if(selectedMappedUsers){
setSelectedUsers(selectedMappedUsers);
}
};
const createDynamicSecret = useCreateDynamicSecret();
const handleCreateDynamicSecret = async ({ name, maxTTL, defaultTTL }: TForm) => {
const handleCreateDynamicSecret = async ({ name, provider, maxTTL, defaultTTL }: TForm) => {
// wait till previous request is finished
if (createDynamicSecret.isLoading) return;
try {
selectedUsers.map(async (user: {id: string, name: string})=>{
selectedUsers.map(async (user: { id: string, name: string, email: string }) => {
await createDynamicSecret.mutateAsync({
provider: { type: DynamicSecretProviders.AzureEntraId, inputs: { userId: user.id, tenantId: "<tenant id from callback>" } },
provider: { type: DynamicSecretProviders.AzureEntraId, inputs: { userId: user.id, tenantId: provider.tenantId, email: user.email } },
maxTTL,
name: `${name }-${ user.name}`,
name: `${name}-${user.name}`,
path: secretPath,
defaultTTL,
projectSlug,
@@ -146,25 +173,61 @@ export const AzureEntraIdInputForm = ({
/>
</div>
</div>
<div>
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
Configuration
</div>
<div className="flex flex-col">
<div className="flex-grow">
<Controller
control={control}
defaultValue=""
name="provider.tenantId"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Tenant Id"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="Tenant Id from Azure Entra ID App installation" />
</FormControl>
)}
/>
</div>
</div>
</div>
<div>
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
Select Users
</div>
<div className="flex flex-col">
<div className="flex items-center space-x-2">
{users &&
{
tenantId && !isError && !isFetching && isFetched && data && mappedUserList &&
<ListBoxMultiple
isSelected={selectedUsers}
data={users}
onChange={setSelectedUsers}
isSelected={mappedUserList.filter(user =>
selectedUsers.some(selectedUser => selectedUser.id === user.id)
)}
onChange={handleChange}
data={mappedUserList}
/>
}
{
tenantId && isFetching && (<><Spinner size="xs" /><p> Loading</p></>)
}
{
tenantId && !isFetching && isError && (<><FontAwesomeIcon icon={faWarning} /> <p> Error loading users please ensure Entra Id app is installed and tenant ID is correct</p></>)
}
{
!tenantId && (<><FontAwesomeIcon icon={faWarning} /><p> Enter tenant ID to fetch users</p></>)
}
</div>
</div>
</div>
</div>
<div className="mt-4 flex items-center space-x-4">
<Button type="submit" isLoading={isSubmitting}>
<Button type="submit" isLoading={isSubmitting} isDisabled={isLoading || isError}>
Submit
</Button>
<Button variant="outline_bg" onClick={onCancel}>
@@ -16,6 +16,9 @@ export const AzureEntraIdSetup = ({
<div>
<div>
<div>
<div className="mb-4 mt-4 pb-2 pl-1 font-medium text-mineshaft-200">
Azure Entra ID Integration Guide
</div>
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
App Installation
</div>
@@ -26,6 +29,8 @@ export const AzureEntraIdSetup = ({
Step 2: Choose an account with admin access to Entra Id.
<br />
Step 3: Allow Infisical persmissions to read and write all users full profiles.
<br />
Step 4: Copy Tenant ID after installation and paste it in the next step.
</div>
</div>
</div>
@@ -47,10 +52,13 @@ export const AzureEntraIdSetup = ({
</div>
</div>
<div className="mt-4 flex items-center space-x-4">
<a href={`https://login.microsoftonline.com/common/adminconsent?client_id=9805c35f-88d4-4625-9daf-66f741e4129c&redirect_uri=${window.location.origin}/integrations/azure-entra-id/callback`} target="_blank" rel="noreferrer">
<Button type="submit">
Install App
</Button>
</a>
<Button type="submit" onClick={onCompleted}>
{/* <Link target="_blank" href="https://login.microsoftonline.com/common/adminconsent?client_id=9805c35f-88d4-4625-9daf-66f741e4129c&redirect_uri=https://meet-dev.in.ngrok.io" content="Link" > */}
Install App
{/* </Link> */}
Next
</Button>
<Button variant="outline_bg" onClick={onCancel}>
Cancel
@@ -177,14 +177,14 @@ const renderOutputForm = (provider: DynamicSecretProviders, data: unknown) => {
}
if (provider === DynamicSecretProviders.AzureEntraId) {
const { userId, password } = data as {
userId: string;
const { email, password } = data as {
email: string;
password: string;
};
return (
<div>
<OutputDisplay label="UserId" value={userId} />
<OutputDisplay label="Email" value={email} />
<OutputDisplay
label="Password"
value={password}
@@ -0,0 +1,168 @@
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import ms from "ms";
import { z } from "zod";
import { TtlFormLabel } from "@app/components/features";
import { createNotification } from "@app/components/notifications";
import {
Button,
FormControl,
Input,
} from "@app/components/v2";
import { useUpdateDynamicSecret } from "@app/hooks/api";
import { TDynamicSecret } 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" });
}),
newName: z
.string()
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
.optional()
});
type TForm = z.infer<typeof formSchema>;
type Props = {
onClose: () => void;
dynamicSecret: TDynamicSecret & { inputs: unknown };
secretPath: string;
environment: string;
projectSlug: string;
};
export const EditDynamicSecretAzureEntraIdForm = ({
onClose,
dynamicSecret,
secretPath,
environment,
projectSlug
}: Props) => {
const {
control,
formState: { isSubmitting },
handleSubmit
} = useForm<TForm>({
resolver: zodResolver(formSchema),
values: {
defaultTTL: dynamicSecret.defaultTTL,
maxTTL: dynamicSecret.maxTTL,
newName: dynamicSecret.name,
}
});
const updateDynamicSecret = useUpdateDynamicSecret();
const handleUpdateDynamicSecret = async ({ maxTTL, defaultTTL, newName }: TForm) => {
// wait till previous request is finished
if (updateDynamicSecret.isLoading) return;
try {
await updateDynamicSecret.mutateAsync({
name: dynamicSecret.name,
path: secretPath,
projectSlug,
environmentSlug: environment,
data: {
maxTTL: maxTTL || undefined,
defaultTTL,
newName: newName === dynamicSecret.name ? undefined : newName
}
});
onClose();
createNotification({
type: "success",
text: "Successfully updated dynamic secret"
});
} catch (err) {
createNotification({
type: "error",
text: "Failed to update dynamic secret"
});
}
};
return (
<div>
<form onSubmit={handleSubmit(handleUpdateDynamicSecret)} autoComplete="off">
<div>
<div className="flex items-center space-x-2">
<div className="flex-grow">
<Controller
control={control}
defaultValue=""
name="newName"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Secret Name"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="dynamic-secret" />
</FormControl>
)}
/>
</div>
<div className="w-32">
<Controller
control={control}
name="defaultTTL"
defaultValue="1h"
render={({ field, fieldState: { error } }) => (
<FormControl
label={<TtlFormLabel label="Default TTL" />}
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input {...field} />
</FormControl>
)}
/>
</div>
<div className="w-32">
<Controller
control={control}
name="maxTTL"
defaultValue="24h"
render={({ field, fieldState: { error } }) => (
<FormControl
label={<TtlFormLabel label="Max TTL" />}
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input {...field} />
</FormControl>
)}
/>
</div>
</div>
</div>
<div className="mt-4 flex items-center space-x-4">
<Button type="submit" isLoading={isSubmitting}>
Submit
</Button>
<Button variant="outline_bg" onClick={onClose}>
Cancel
</Button>
</div>
</form>
</div>
);
};
@@ -6,6 +6,7 @@ import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
import { EditDynamicSecretAwsElastiCacheProviderForm } from "./EditDynamicSecretAwsElastiCacheProviderForm";
import { EditDynamicSecretAwsIamForm } from "./EditDynamicSecretAwsIamForm";
import { EditDynamicSecretAzureEntraIdForm } from "./EditDynamicSecretAzureEntraIdForm";
import { EditDynamicSecretCassandraForm } from "./EditDynamicSecretCassandraForm";
import { EditDynamicSecretElasticSearchForm } from "./EditDynamicSecretElasticSearchForm";
import { EditDynamicSecretMongoAtlasForm } from "./EditDynamicSecretMongoAtlasForm";
@@ -202,6 +203,24 @@ export const EditDynamicSecretForm = ({
/>
</motion.div>
)}
{dynamicSecretDetails?.type === DynamicSecretProviders.AzureEntraId && (
<motion.div
key="azure-entra-id-edit"
transition={{ duration: 0.1 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: -30 }}
>
<EditDynamicSecretAzureEntraIdForm
onClose={onClose}
projectSlug={projectSlug}
secretPath={secretPath}
dynamicSecret={dynamicSecretDetails}
environment={environment}
/>
</motion.div>
)}
</AnimatePresence>
);
};
@@ -0,0 +1,72 @@
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>
<h1 className="bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-4xl font-medium text-transparent">
Installed Entra Id App successfully
</h1>
</div>
<div className="m-auto my-8 flex w-full">
<div className="w-full border-t border-mineshaft-600" />
</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">
<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="mr-2 flex items-center justify-end rounded-md bg-white/[0.05] p-2 text-base text-gray-400">
<p className="mr-4 break-all">{router.query.tenant}</p>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative ml-2"
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>
)
}