General improvements on auth templates

This commit is contained in:
Carlos Monastyrski
2025-08-04 15:29:07 -03:00
parent ebe05661d3
commit 3dde786621
29 changed files with 598 additions and 557 deletions
@@ -26,7 +26,8 @@ export enum OrgPermissionMachineIdentityAuthTemplateActions {
CreateTemplates = "create-templates",
EditTemplates = "edit-templates",
DeleteTemplates = "delete-templates",
UnlinkTemplates = "unlink-templates"
UnlinkTemplates = "unlink-templates",
UseTemplates = "use-templates"
}
export enum OrgPermissionSubjects {
@@ -18,7 +18,7 @@ export const useCreateIdentityAuthTemplate = () => {
return useMutation({
mutationFn: async (dto: CreateIdentityAuthTemplateDTO) => {
const { data } = await apiRequest.post<{ template: IdentityAuthTemplate }>(
"/api/v1/identities/templates",
"/api/v1/identity-templates",
dto
);
return data.template;
@@ -37,7 +37,7 @@ export const useUpdateIdentityAuthTemplate = () => {
return useMutation({
mutationFn: async (dto: UpdateIdentityAuthTemplateDTO) => {
const { data } = await apiRequest.patch<{ template: IdentityAuthTemplate }>(
`/api/v1/identities/templates/${dto.templateId}`,
`/api/v1/identity-templates/${dto.templateId}`,
dto
);
return data.template;
@@ -58,7 +58,7 @@ export const useDeleteIdentityAuthTemplate = () => {
return useMutation({
mutationFn: async (dto: DeleteIdentityAuthTemplateDTO) => {
await apiRequest.delete(`/api/v1/identities/templates/${dto.templateId}`, {
await apiRequest.delete(`/api/v1/identity-templates/${dto.templateId}`, {
params: { organizationId: dto.organizationId }
});
},
@@ -79,7 +79,7 @@ export const useUnlinkTemplateUsage = () => {
return useMutation({
mutationFn: async (dto: UnlinkTemplateUsageDTO) => {
const { data } = await apiRequest.post<MachineAuthTemplateUsage[]>(
`/api/v1/identities/templates/${dto.templateId}/usage`,
`/api/v1/identity-templates/${dto.templateId}/delete-usage`,
{ identityIds: dto.identityIds },
{ params: { organizationId: dto.organizationId } }
);
@@ -29,7 +29,7 @@ export const useGetIdentityAuthTemplates = (dto: GetIdentityAuthTemplatesDTO) =>
const { data } = await apiRequest.get<{
templates: IdentityAuthTemplate[];
totalCount: number;
}>("/api/v1/identities/templates/search", {
}>("/api/v1/identity-templates/search", {
params: {
organizationId: dto.organizationId,
limit: dto.limit || 50,
@@ -47,13 +47,13 @@ export const useGetIdentityAuthTemplate = (templateId: string, organizationId: s
return useQuery({
queryKey: identityAuthTemplatesKeys.getTemplate(templateId),
queryFn: async () => {
const { data } = await apiRequest.get<{ template: IdentityAuthTemplate }>(
`/api/v1/identities/templates/${templateId}`,
const { data } = await apiRequest.get<IdentityAuthTemplate>(
`/api/v1/identity-templates/${templateId}`,
{
params: { organizationId }
}
);
return data.template;
return data;
},
enabled: Boolean(templateId) && Boolean(organizationId)
});
@@ -63,12 +63,9 @@ export const useGetAvailableTemplates = (authMethod: MachineIdentityAuthMethod)
return useQuery({
queryKey: identityAuthTemplatesKeys.getAvailableTemplates(authMethod),
queryFn: async () => {
const { data } = await apiRequest.get<IdentityAuthTemplate[]>(
"/api/v1/identities/templates",
{
params: { authMethod }
}
);
const { data } = await apiRequest.get<IdentityAuthTemplate[]>("/api/v1/identity-templates", {
params: { authMethod }
});
return data;
},
enabled: Boolean(authMethod)
@@ -80,7 +77,7 @@ export const useGetTemplateUsages = (dto: GetTemplateUsagesDTO) => {
queryKey: identityAuthTemplatesKeys.getTemplateUsages(dto.templateId),
queryFn: async () => {
const { data } = await apiRequest.get<MachineAuthTemplateUsage[]>(
`/api/v1/identities/templates/${dto.templateId}/usage`,
`/api/v1/identity-templates/${dto.templateId}/usage`,
{
params: { organizationId: dto.organizationId }
}
@@ -7,6 +7,7 @@ export interface LdapTemplateFields {
bindDN: string;
bindPass: string;
searchBase: string;
ldapCaCertificate?: string;
}
export interface IdentityAuthTemplate {
@@ -11,7 +11,8 @@ import {
Modal,
ModalContent,
Select,
SelectItem
SelectItem,
TextArea
} from "@app/components/v2";
import { useOrganization } from "@app/context";
import {
@@ -29,7 +30,11 @@ const schema = z.object({
url: z.string().min(1, "LDAP URL is required"),
bindDN: z.string().min(1, "Bind DN is required"),
bindPass: z.string().min(1, "Bind Pass is required"),
searchBase: z.string().min(1, "Search Base / DN is required")
searchBase: z.string().min(1, "Search Base / DN is required"),
ldapCaCertificate: z
.string()
.optional()
.transform((val) => val || undefined)
});
export type FormData = z.infer<typeof schema>;
@@ -66,7 +71,8 @@ export const IdentityAuthTemplateModal = ({ popUp, handlePopUpToggle }: Props) =
url: "",
bindDN: "",
bindPass: "",
searchBase: ""
searchBase: "",
ldapCaCertificate: ""
}
});
@@ -78,7 +84,8 @@ export const IdentityAuthTemplateModal = ({ popUp, handlePopUpToggle }: Props) =
url: template.templateFields?.url || "",
bindDN: template.templateFields?.bindDN || "",
bindPass: template.templateFields?.bindPass || "",
searchBase: template.templateFields?.searchBase || ""
searchBase: template.templateFields?.searchBase || "",
ldapCaCertificate: template.templateFields?.ldapCaCertificate || ""
});
} else {
reset({
@@ -87,7 +94,8 @@ export const IdentityAuthTemplateModal = ({ popUp, handlePopUpToggle }: Props) =
url: "",
bindDN: "",
bindPass: "",
searchBase: ""
searchBase: "",
ldapCaCertificate: ""
});
}
}, [isEdit, template, reset]);
@@ -105,7 +113,8 @@ export const IdentityAuthTemplateModal = ({ popUp, handlePopUpToggle }: Props) =
url: data.url,
bindDN: data.bindDN,
bindPass: data.bindPass,
searchBase: data.searchBase
searchBase: data.searchBase,
ldapCaCertificate: data.ldapCaCertificate
}
});
createNotification({
@@ -121,7 +130,8 @@ export const IdentityAuthTemplateModal = ({ popUp, handlePopUpToggle }: Props) =
url: data.url,
bindDN: data.bindDN,
bindPass: data.bindPass,
searchBase: data.searchBase
searchBase: data.searchBase,
ldapCaCertificate: data.ldapCaCertificate
}
});
createNotification({
@@ -267,6 +277,22 @@ export const IdentityAuthTemplateModal = ({ popUp, handlePopUpToggle }: Props) =
</FormControl>
)}
/>
<Controller
control={control}
name="ldapCaCertificate"
render={({ field, fieldState: { error } }) => (
<FormControl
label="CA Certificate"
isOptional
errorText={error?.message}
isError={Boolean(error)}
tooltipText="An optional PEM-encoded CA cert for the LDAP server. This is used by the TLS client for secure communication with the LDAP server."
>
<TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." />
</FormControl>
)}
/>
</>
)}
@@ -182,7 +182,7 @@ export const IdentityAuthTemplatesTable = ({ handlePopUpOpen }: Props) => {
<Td>{template.name}</Td>
<Td>
<div className="flex items-center">
<span className="capitalize">{template.authMethod}</span>
<span className="uppercase">{template.authMethod}</span>
</div>
</Td>
<Td>
@@ -20,7 +20,11 @@ import {
TextArea,
Tooltip
} from "@app/components/v2";
import { useOrganization, useSubscription } from "@app/context";
import { useOrganization, useOrgPermission, useSubscription } from "@app/context";
import {
OrgPermissionMachineIdentityAuthTemplateActions,
OrgPermissionSubjects
} from "@app/context/OrgPermissionContext/types";
import {
MachineIdentityAuthMethod,
useAddIdentityLdapAuth,
@@ -143,6 +147,12 @@ export const IdentityLdapAuthForm = ({
const { mutateAsync: updateMutateAsync } = useUpdateIdentityLdapAuth();
const [tabValue, setTabValue] = useState<IdentityFormTab>(IdentityFormTab.Configuration);
const { data: templates } = useGetAvailableTemplates(MachineIdentityAuthMethod.LDAP);
const { permission } = useOrgPermission();
const canUseTemplates = permission.can(
OrgPermissionMachineIdentityAuthTemplateActions.UseTemplates,
OrgPermissionSubjects.MachineIdentityAuthTemplate
);
const { data } = useGetIdentityLdapAuth(identityId ?? "", {
enabled: isUpdate
@@ -153,11 +163,12 @@ export const IdentityLdapAuthForm = ({
handleSubmit,
reset,
watch,
setValue,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: {
scope: "template",
scope: "custom",
templateId: "",
url: "",
bindDN: "",
@@ -188,15 +199,11 @@ export const IdentityLdapAuthForm = ({
// Helper function to determine scope based on existing data
const determineScope = (authData: any) => {
// If templateId exists in the data, it's template scope
// If url, bindDN, bindPass, searchBase exist, it's custom scope
if (authData.templateId) {
return "template";
}
if (authData.url || authData.bindDN || authData.bindPass || authData.searchBase) {
return "custom";
}
// Default to template if we can't determine
return "template";
// Default to custom if we can't determine
return "custom";
};
useEffect(() => {
@@ -228,7 +235,7 @@ export const IdentityLdapAuthForm = ({
}
reset({
scope: "template",
scope: "custom",
templateId: "",
url: "",
bindDN: "",
@@ -345,30 +352,38 @@ export const IdentityLdapAuthForm = ({
<Tab value={IdentityFormTab.Advanced}>Advanced</Tab>
</TabList>
<TabPanel value={IdentityFormTab.Configuration}>
<Controller
control={control}
name="scope"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
label="Configuration Type"
isError={Boolean(error)}
errorText={error?.message}
>
<Select
value={value}
onValueChange={(val) => {
onChange(val);
}}
className="w-full"
position="popper"
dropdownContainerClassName="max-w-none"
{canUseTemplates && (
<Controller
control={control}
name="scope"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
label="Configuration Type"
isError={Boolean(error)}
errorText={error?.message}
>
<SelectItem value="template">Use Template</SelectItem>
<SelectItem value="custom">Custom Configuration</SelectItem>
</Select>
</FormControl>
)}
/>
<Select
value={value}
onValueChange={(val) => {
onChange(val);
setValue("templateId", data?.templateId || "");
setValue("url", data?.url || "");
setValue("bindDN", data?.bindDN || "");
setValue("bindPass", data?.bindPass || "");
setValue("searchBase", data?.searchBase || "");
setValue("ldapCaCertificate", data?.ldapCaCertificate || "");
}}
className="w-full"
position="popper"
dropdownContainerClassName="max-w-none"
>
<SelectItem value="template">Use Template</SelectItem>
<SelectItem value="custom">Custom Configuration</SelectItem>
</Select>
</FormControl>
)}
/>
)}
{scope === "template" && (
<Controller
@@ -385,6 +400,13 @@ export const IdentityLdapAuthForm = ({
value={value}
onValueChange={(val) => {
onChange(val);
const tmp = templates?.find((t) => t.id === val);
if (!tmp) return;
setValue("url", tmp.templateFields.url);
setValue("bindDN", tmp.templateFields.bindDN);
setValue("bindPass", tmp.templateFields.bindPass);
setValue("searchBase", tmp.templateFields.searchBase);
setValue("ldapCaCertificate", tmp.templateFields.ldapCaCertificate);
}}
className="w-full"
position="popper"
@@ -404,66 +426,104 @@ export const IdentityLdapAuthForm = ({
/>
)}
{scope === "custom" && (
<>
<Controller
control={control}
name="url"
render={({ field, fieldState: { error } }) => (
<FormControl
label="LDAP URL"
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<Input {...field} placeholder="ldaps://domain-or-ip:636" type="text" />
</FormControl>
)}
/>
<Controller
control={control}
name="bindDN"
render={({ field, fieldState: { error } }) => (
<FormControl
isRequired
label="Bind DN"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="cn=infisical,ou=Users,dc=example,dc=com" />
</FormControl>
)}
/>
<Controller
control={control}
name="bindPass"
render={({ field, fieldState: { error } }) => (
<FormControl
isRequired
label="Bind Pass"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="********" type="password" />
</FormControl>
)}
/>
<Controller
control={control}
name="searchBase"
render={({ field, fieldState: { error } }) => (
<FormControl
isRequired
label="Search Base / DN"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="ou=machines,dc=acme,dc=com" />
</FormControl>
)}
/>
</>
)}
<Controller
control={control}
name="url"
render={({ field, fieldState: { error } }) => (
<FormControl
label="LDAP URL"
isError={Boolean(error)}
errorText={error?.message}
tooltipText={
scope === "template"
? "This field cannot be modified when using a template"
: undefined
}
isRequired
>
<Input
{...field}
placeholder="ldaps://domain-or-ip:636"
type="text"
isDisabled={scope === "template"}
containerClassName={scope === "template" ? "opacity-55" : ""}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="bindDN"
render={({ field, fieldState: { error } }) => (
<FormControl
isRequired
label="Bind DN"
isError={Boolean(error)}
errorText={error?.message}
tooltipText={
scope === "template"
? "This field cannot be modified when using a template"
: undefined
}
>
<Input
{...field}
containerClassName={scope === "template" ? "opacity-55" : ""}
placeholder="cn=infisical,ou=Users,dc=example,dc=com"
isDisabled={scope === "template"}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="bindPass"
render={({ field, fieldState: { error } }) => (
<FormControl
isRequired
label="Bind Pass"
isError={Boolean(error)}
errorText={error?.message}
tooltipText={
scope === "template"
? "This field cannot be modified when using a template"
: undefined
}
>
<Input
{...field}
placeholder="********"
type="password"
containerClassName={scope === "template" ? "opacity-55" : ""}
isDisabled={scope === "template"}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="searchBase"
render={({ field, fieldState: { error } }) => (
<FormControl
isRequired
label="Search Base / DN"
isError={Boolean(error)}
errorText={error?.message}
tooltipText={
scope === "template"
? "This field cannot be modified when using a template"
: undefined
}
>
<Input
{...field}
placeholder="ou=machines,dc=acme,dc=com"
containerClassName={scope === "template" ? "opacity-55" : ""}
isDisabled={scope === "template"}
/>
</FormControl>
)}
/>
<Controller
control={control}
@@ -641,9 +701,18 @@ export const IdentityLdapAuthForm = ({
isOptional
errorText={error?.message}
isError={Boolean(error)}
tooltipText="An optional PEM-encoded CA cert for the LDAP server. This is used by the TLS client for secure communication with the LDAP server."
tooltipText={
scope === "template"
? "This field cannot be modified when using a template"
: "An optional PEM-encoded CA cert for the LDAP server. This is used by the TLS client for secure communication with the LDAP server."
}
>
<TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." />
<TextArea
{...field}
placeholder="-----BEGIN CERTIFICATE----- ..."
className={scope === "template" ? "opacity-55" : ""}
isDisabled={scope === "template"}
/>
</FormControl>
)}
/>
@@ -103,93 +103,93 @@ export const IdentitySection = withPermission(
};
return (
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-center justify-between">
<div className="flex items-center gap-1">
<p className="text-xl font-semibold text-mineshaft-100">Identities</p>
<a
href="https://infisical.com/docs/documentation/platform/identities/overview"
target="_blank"
rel="noopener noreferrer"
>
<div className="ml-1 mt-[0.16rem] inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
<span>Docs</span>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1.5 text-[10px]"
/>
</div>
</a>
</div>
<OrgPermissionCan
I={OrgPermissionIdentityActions.Create}
a={OrgPermissionSubjects.Identity}
>
{(isAllowed) => (
<Button
colorSchema="secondary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
if (!isMoreIdentitiesAllowed && !isEnterprise) {
handlePopUpOpen("upgradePlan", {
description: "You can add more identities if you upgrade your Infisical plan."
});
return;
}
handlePopUpOpen("identity");
}}
isDisabled={!isAllowed}
<div>
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-center justify-between">
<div className="flex items-center gap-1">
<p className="text-xl font-semibold text-mineshaft-100">Identities</p>
<a
href="https://infisical.com/docs/documentation/platform/identities/overview"
target="_blank"
rel="noopener noreferrer"
>
Create Identity
</Button>
)}
</OrgPermissionCan>
</div>
<IdentityTable handlePopUpOpen={handlePopUpOpen} />
{/* Identity Auth Templates Section */}
{subscription.machineIdentityAuthTemplates && (
<div className="mt-8">
<div className="mb-4 flex items-center justify-between">
<div className="flex items-center gap-1">
<p className="text-xl font-semibold text-mineshaft-100">Identity Auth Templates</p>
<a
href="https://infisical.com/docs/documentation/platform/identities/auth-templates"
target="_blank"
rel="noopener noreferrer"
>
<div className="ml-1 mt-[0.16rem] inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
<span>Docs</span>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1.5 text-[10px]"
/>
</div>
</a>
</div>
<OrgPermissionCan
I={OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates}
a={OrgPermissionSubjects.MachineIdentityAuthTemplate}
>
{(isAllowed) => (
<Button
colorSchema="secondary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("createTemplate")}
isDisabled={!isAllowed}
>
Create Template
</Button>
)}
</OrgPermissionCan>
<div className="ml-1 mt-[0.16rem] inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
<span>Docs</span>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1.5 text-[10px]"
/>
</div>
</a>
</div>
<IdentityAuthTemplatesTable handlePopUpOpen={handlePopUpOpen} />
<OrgPermissionCan
I={OrgPermissionIdentityActions.Create}
a={OrgPermissionSubjects.Identity}
>
{(isAllowed) => (
<Button
colorSchema="secondary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
if (!isMoreIdentitiesAllowed && !isEnterprise) {
handlePopUpOpen("upgradePlan", {
description:
"You can add more identities if you upgrade your Infisical plan."
});
return;
}
handlePopUpOpen("identity");
}}
isDisabled={!isAllowed}
>
Create Identity
</Button>
)}
</OrgPermissionCan>
</div>
)}
<IdentityTable handlePopUpOpen={handlePopUpOpen} />
</div>
{/* Identity Auth Templates Section */}
<div className="mb-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-center justify-between">
<div className="flex items-center gap-1">
<p className="text-xl font-semibold text-mineshaft-100">Identity Auth Templates</p>
<a
href="https://infisical.com/docs/documentation/platform/identities/auth-templates"
target="_blank"
rel="noopener noreferrer"
>
<div className="ml-1 mt-[0.16rem] inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
<span>Docs</span>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1.5 text-[10px]"
/>
</div>
</a>
</div>
<OrgPermissionCan
I={OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates}
a={OrgPermissionSubjects.MachineIdentityAuthTemplate}
>
{(isAllowed) => (
<Button
colorSchema="secondary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("createTemplate")}
isDisabled={!isAllowed}
>
Create Template
</Button>
)}
</OrgPermissionCan>
</div>
<IdentityAuthTemplatesTable handlePopUpOpen={handlePopUpOpen} />
</div>
<IdentityModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<IdentityAuthTemplateModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<MachineAuthTemplateUsagesModal
@@ -1,14 +1,6 @@
import { useEffect, useState } from "react";
import { faCertificate, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { faCertificate } from "@fortawesome/free-solid-svg-icons";
import { createNotification } from "@app/components/notifications";
import {
Badge,
Button,
Checkbox,
DeleteActionModal,
EmptyState,
Modal,
ModalContent,
@@ -22,14 +14,7 @@ import {
Tr
} from "@app/components/v2";
import { useOrganization } from "@app/context";
import {
MachineAuthTemplateUsage,
TEMPLATE_ERROR_MESSAGES,
TEMPLATE_UI_LABELS,
useGetTemplateUsages,
useUnlinkTemplateUsage
} from "@app/hooks/api/identityAuthTemplates";
import { usePopUp } from "@app/hooks/usePopUp";
import { useGetTemplateUsages } from "@app/hooks/api/identityAuthTemplates";
type Props = {
isOpen: boolean;
@@ -45,201 +30,51 @@ export const MachineAuthTemplateUsagesModal = ({
templateName
}: Props) => {
const { currentOrg } = useOrganization();
const [selectedUsageIds, setSelectedUsageIds] = useState<string[]>([]);
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"unlinkUsages"
] as const);
useEffect(() => {
if (!isOpen) {
setSelectedUsageIds([]);
handlePopUpClose("unlinkUsages");
}
}, [isOpen, handlePopUpClose]);
const organizationId = currentOrg?.id || "";
const {
data: usages = [],
isPending,
refetch
} = useGetTemplateUsages({
const { data: usages = [], isPending } = useGetTemplateUsages({
templateId,
organizationId
});
const { mutateAsync: unlinkUsage } = useUnlinkTemplateUsage();
const handleUnlinkUsages = async (selectedUsages: MachineAuthTemplateUsage[]) => {
try {
await unlinkUsage({
templateId,
identityIds: selectedUsages.map((usage) => usage.identityId),
organizationId
});
createNotification({
text: TEMPLATE_ERROR_MESSAGES.UNLINK_SUCCESS,
type: "success"
});
setSelectedUsageIds([]);
handlePopUpClose("unlinkUsages");
refetch();
} catch {
createNotification({
text: TEMPLATE_ERROR_MESSAGES.UNLINK_FAILED,
type: "error"
});
}
};
const handleUsageToggle = (usageId: string) => {
setSelectedUsageIds((prev) =>
prev.includes(usageId) ? prev.filter((id) => id !== usageId) : [...prev, usageId]
);
};
const handleSelectAll = () => {
if (selectedUsageIds.length === usages.length) {
setSelectedUsageIds([]);
} else {
setSelectedUsageIds(usages.map((usage) => usage.identityId));
}
};
return (
<>
<Modal isOpen={isOpen} onOpenChange={onClose}>
<ModalContent
title={`Auth Template Usages: ${templateName}`}
subTitle="Manage identities using this template"
className="max-w-4xl"
>
<div>
<div
className={twMerge(
"h-0 flex-shrink-0 overflow-hidden transition-all",
selectedUsageIds.length > 0 && "h-16"
)}
>
<div className="flex items-center rounded-md border border-mineshaft-600 bg-mineshaft-800 px-4 py-2 text-bunker-300">
<div className="mr-2 text-sm">{selectedUsageIds.length} Selected</div>
<button
type="button"
className="mr-auto text-xs text-mineshaft-400 underline-offset-2 hover:text-mineshaft-200 hover:underline"
onClick={() => setSelectedUsageIds([])}
>
{TEMPLATE_UI_LABELS.UNSELECT_ALL}
</button>
<Button
variant="outline_bg"
colorSchema="danger"
leftIcon={<FontAwesomeIcon icon={faTrash} />}
className="ml-2"
onClick={() => {
const selectedUsagesList = usages.filter((usage) =>
selectedUsageIds.includes(usage.identityId)
);
if (!selectedUsagesList.length) return;
handlePopUpOpen("unlinkUsages", { selectedUsagesList });
}}
size="xs"
>
{TEMPLATE_UI_LABELS.UNLINK}
</Button>
</div>
</div>
<TableContainer>
<Table>
<THead>
<Tr className="h-14">
<Th className="w-12">
<Checkbox
id="select-all"
className="mr-2"
isChecked={usages.length > 0 && selectedUsageIds.length === usages.length}
onCheckedChange={handleSelectAll}
/>
</Th>
<Th>Identity Name</Th>
<Th>Identity ID</Th>
</Tr>
</THead>
<TBody>
{isPending && <TableSkeleton columns={3} innerKey="template-usages" />}
{!isPending &&
usages.map((usage) => (
<Tr
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
key={`usage-${usage.identityId}`}
>
<Td>
<Checkbox
id={`select-usage-${usage.identityId}`}
isChecked={selectedUsageIds.includes(usage.identityId)}
onCheckedChange={() => handleUsageToggle(usage.identityId)}
/>
</Td>
<Td>{usage.identityName}</Td>
<Td>
<span className="text-sm text-mineshaft-400">{usage.identityId}</span>
</Td>
</Tr>
))}
</TBody>
</Table>
{!isPending && usages.length === 0 && (
<EmptyState
title="This template is not currently being used by any identities"
icon={faCertificate}
/>
)}
</TableContainer>
</div>
</ModalContent>
</Modal>
<DeleteActionModal
isOpen={popUp.unlinkUsages.isOpen}
title="Are you sure you want to unlink the following template usages?"
onChange={(isDeleteOpen) => handlePopUpToggle("unlinkUsages", isDeleteOpen)}
deleteKey="confirm"
onDeleteApproved={() =>
handleUnlinkUsages(
popUp.unlinkUsages.data.selectedUsagesList as MachineAuthTemplateUsage[]
)
}
buttonText={TEMPLATE_UI_LABELS.UNLINK}
>
<div className="mt-4 text-sm text-mineshaft-400">
This template will no longer be used by the following{" "}
{popUp.unlinkUsages.data?.selectedUsagesList?.length > 1 ? "identities" : "identity"}:
</div>
<div className="mt-2 max-h-[20rem] overflow-y-auto rounded border border-mineshaft-600 bg-red/10 p-4 pl-8 text-sm text-red-200">
<ul className="list-disc">
{(popUp.unlinkUsages.data?.selectedUsagesList as MachineAuthTemplateUsage[])?.map(
(usage) => (
<li key={usage.identityId}>
<div className="mb-1 flex items-center">
<span className="break-all">{usage.identityName}</span>
<Badge
variant="danger"
className="ml-2 inline-flex w-min items-center gap-1.5 whitespace-nowrap"
<Modal isOpen={isOpen} onOpenChange={onClose}>
<ModalContent title={`Usages for Identity Auth Template: ${templateName}`}>
<div>
<TableContainer>
<Table>
<THead>
<Tr className="h-14">
<Th>Identity Name</Th>
<Th>Identity ID</Th>
</Tr>
</THead>
<TBody>
{isPending && <TableSkeleton columns={3} innerKey="template-usages" />}
{!isPending &&
usages.map((usage) => (
<Tr
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
key={`usage-${usage.identityId}`}
>
{usage.identityId}
</Badge>
</div>
</li>
)
<Td>{usage.identityName}</Td>
<Td>
<span className="text-sm text-mineshaft-400">{usage.identityId}</span>
</Td>
</Tr>
))}
</TBody>
</Table>
{!isPending && usages.length === 0 && (
<EmptyState
title="This template is not currently being used by any identities"
icon={faCertificate}
/>
)}
</ul>
</TableContainer>
</div>
</DeleteActionModal>
</>
</ModalContent>
</Modal>
);
};
@@ -89,7 +89,8 @@ const machineIdentityAuthTemplatePermissionSchema = z
[OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates]: z.boolean().optional(),
[OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates]: z.boolean().optional(),
[OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates]: z.boolean().optional(),
[OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates]: z.boolean().optional()
[OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates]: z.boolean().optional(),
[OrgPermissionMachineIdentityAuthTemplateActions.UseTemplates]: z.boolean().optional()
})
.optional();
@@ -43,6 +43,10 @@ const PERMISSION_ACTIONS = [
{
action: OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates,
label: "Unlink Templates"
},
{
action: OrgPermissionMachineIdentityAuthTemplateActions.UseTemplates,
label: "Attach Templates"
}
] as const;
@@ -103,7 +107,8 @@ export const OrgPermissionMachineIdentityAuthTemplateRow = ({
[OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates]: true,
[OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates]: true,
[OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates]: true,
[OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates]: true
[OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates]: true,
[OrgPermissionMachineIdentityAuthTemplateActions.UseTemplates]: true
},
{ shouldDirty: true }
);
@@ -116,7 +121,8 @@ export const OrgPermissionMachineIdentityAuthTemplateRow = ({
[OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates]: false,
[OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates]: false,
[OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates]: false,
[OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates]: false
[OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates]: false,
[OrgPermissionMachineIdentityAuthTemplateActions.UseTemplates]: true
},
{ shouldDirty: true }
);
@@ -131,7 +137,8 @@ export const OrgPermissionMachineIdentityAuthTemplateRow = ({
[OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates]: false,
[OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates]: false,
[OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates]: false,
[OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates]: false
[OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates]: false,
[OrgPermissionMachineIdentityAuthTemplateActions.UseTemplates]: false
},
{ shouldDirty: true }
);