mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 11:29:42 +00:00
General improvements on auth templates
This commit is contained in:
@@ -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 {
|
||||
|
||||
+33
-7
@@ -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>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
+1
-1
@@ -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>
|
||||
|
||||
+163
-94
@@ -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>
|
||||
)}
|
||||
/>
|
||||
|
||||
+84
-84
@@ -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
|
||||
|
||||
+37
-202
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+2
-1
@@ -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();
|
||||
|
||||
|
||||
+10
-3
@@ -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 }
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user