mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 13:22:08 +00:00
Merge pull request #4300 from Infisical/feat/machineAuthTemplates
Add Machine Auth Templates
This commit is contained in:
@@ -21,6 +21,15 @@ export enum OrgGatewayPermissionActions {
|
||||
AttachGateways = "attach-gateways"
|
||||
}
|
||||
|
||||
export enum OrgPermissionMachineIdentityAuthTemplateActions {
|
||||
ListTemplates = "list-templates",
|
||||
CreateTemplates = "create-templates",
|
||||
EditTemplates = "edit-templates",
|
||||
DeleteTemplates = "delete-templates",
|
||||
UnlinkTemplates = "unlink-templates",
|
||||
AttachTemplates = "attach-templates"
|
||||
}
|
||||
|
||||
export enum OrgPermissionSubjects {
|
||||
Workspace = "workspace",
|
||||
Role = "role",
|
||||
@@ -42,7 +51,8 @@ export enum OrgPermissionSubjects {
|
||||
Kmip = "kmip",
|
||||
Gateway = "gateway",
|
||||
SecretShare = "secret-share",
|
||||
GithubOrgSync = "github-org-sync"
|
||||
GithubOrgSync = "github-org-sync",
|
||||
MachineIdentityAuthTemplate = "machine-identity-auth-template"
|
||||
}
|
||||
|
||||
export enum OrgPermissionAdminConsoleAction {
|
||||
@@ -113,6 +123,10 @@ export type OrgPermissionSet =
|
||||
| [OrgPermissionAppConnectionActions, OrgPermissionSubjects.AppConnections]
|
||||
| [OrgPermissionIdentityActions, OrgPermissionSubjects.Identity]
|
||||
| [OrgPermissionKmipActions, OrgPermissionSubjects.Kmip]
|
||||
| [
|
||||
OrgPermissionMachineIdentityAuthTemplateActions,
|
||||
OrgPermissionSubjects.MachineIdentityAuthTemplate
|
||||
]
|
||||
| [OrgGatewayPermissionActions, OrgPermissionSubjects.Gateway]
|
||||
| [OrgPermissionSecretShareAction, OrgPermissionSubjects.SecretShare];
|
||||
// TODO(scott): add back once org UI refactored
|
||||
|
||||
@@ -1385,6 +1385,7 @@ export const useAddIdentityLdapAuth = () => {
|
||||
return useMutation<IdentityLdapAuth, object, AddIdentityLdapAuthDTO>({
|
||||
mutationFn: async ({
|
||||
identityId,
|
||||
templateId,
|
||||
url,
|
||||
bindDN,
|
||||
bindPass,
|
||||
@@ -1400,6 +1401,7 @@ export const useAddIdentityLdapAuth = () => {
|
||||
const { data } = await apiRequest.post<{ identityLdapAuth: IdentityLdapAuth }>(
|
||||
`/api/v1/auth/ldap-auth/identities/${identityId}`,
|
||||
{
|
||||
templateId,
|
||||
url,
|
||||
bindDN,
|
||||
bindPass,
|
||||
@@ -1432,6 +1434,7 @@ export const useUpdateIdentityLdapAuth = () => {
|
||||
return useMutation<IdentityLdapAuth, object, UpdateIdentityLdapAuthDTO>({
|
||||
mutationFn: async ({
|
||||
identityId,
|
||||
templateId,
|
||||
url,
|
||||
bindDN,
|
||||
bindPass,
|
||||
@@ -1447,6 +1450,7 @@ export const useUpdateIdentityLdapAuth = () => {
|
||||
const { data } = await apiRequest.patch<{ identityLdapAuth: IdentityLdapAuth }>(
|
||||
`/api/v1/auth/ldap-auth/identities/${identityId}`,
|
||||
{
|
||||
templateId,
|
||||
url,
|
||||
bindDN,
|
||||
bindPass,
|
||||
|
||||
@@ -567,10 +567,11 @@ export type IdentityTokenAuth = {
|
||||
export type AddIdentityLdapAuthDTO = {
|
||||
organizationId: string;
|
||||
identityId: string;
|
||||
url: string;
|
||||
bindDN: string;
|
||||
bindPass: string;
|
||||
searchBase: string;
|
||||
templateId?: string;
|
||||
url?: string;
|
||||
bindDN?: string;
|
||||
bindPass?: string;
|
||||
searchBase?: string;
|
||||
searchFilter: string;
|
||||
ldapCaCertificate?: string;
|
||||
allowedFields?: {
|
||||
@@ -588,6 +589,7 @@ export type AddIdentityLdapAuthDTO = {
|
||||
export type UpdateIdentityLdapAuthDTO = {
|
||||
identityId: string;
|
||||
organizationId: string;
|
||||
templateId?: string;
|
||||
url?: string;
|
||||
bindDN?: string;
|
||||
bindPass?: string;
|
||||
@@ -612,10 +614,11 @@ export type DeleteIdentityLdapAuthDTO = {
|
||||
};
|
||||
|
||||
export type IdentityLdapAuth = {
|
||||
url: string;
|
||||
bindDN: string;
|
||||
bindPass: string;
|
||||
searchBase: string;
|
||||
url?: string;
|
||||
bindDN?: string;
|
||||
templateId?: string;
|
||||
bindPass?: string;
|
||||
searchBase?: string;
|
||||
searchFilter: string;
|
||||
ldapCaCertificate?: string;
|
||||
allowedFields?: {
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from "./mutations";
|
||||
export * from "./queries";
|
||||
export * from "./types";
|
||||
@@ -0,0 +1,97 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { identityAuthTemplatesKeys } from "./queries";
|
||||
import {
|
||||
CreateIdentityAuthTemplateDTO,
|
||||
DeleteIdentityAuthTemplateDTO,
|
||||
IdentityAuthTemplate,
|
||||
MachineAuthTemplateUsage,
|
||||
UnlinkTemplateUsageDTO,
|
||||
UpdateIdentityAuthTemplateDTO
|
||||
} from "./types";
|
||||
|
||||
export const useCreateIdentityAuthTemplate = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (dto: CreateIdentityAuthTemplateDTO) => {
|
||||
const { data } = await apiRequest.post<{ template: IdentityAuthTemplate }>(
|
||||
"/api/v1/identity-templates",
|
||||
dto
|
||||
);
|
||||
return data.template;
|
||||
},
|
||||
onSuccess: (_, { organizationId }) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: identityAuthTemplatesKeys.getTemplates({ organizationId })
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateIdentityAuthTemplate = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (dto: UpdateIdentityAuthTemplateDTO) => {
|
||||
const { data } = await apiRequest.patch<{ template: IdentityAuthTemplate }>(
|
||||
`/api/v1/identity-templates/${dto.templateId}`,
|
||||
dto
|
||||
);
|
||||
return data.template;
|
||||
},
|
||||
onSuccess: (_, { organizationId, templateId }) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: identityAuthTemplatesKeys.getTemplates({ organizationId })
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: identityAuthTemplatesKeys.getTemplate(templateId)
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteIdentityAuthTemplate = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (dto: DeleteIdentityAuthTemplateDTO) => {
|
||||
await apiRequest.delete(`/api/v1/identity-templates/${dto.templateId}`, {
|
||||
params: { organizationId: dto.organizationId }
|
||||
});
|
||||
},
|
||||
onSuccess: (_, { organizationId, templateId }) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: identityAuthTemplatesKeys.getTemplates({ organizationId })
|
||||
});
|
||||
queryClient.removeQueries({
|
||||
queryKey: identityAuthTemplatesKeys.getTemplate(templateId)
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useUnlinkTemplateUsage = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (dto: UnlinkTemplateUsageDTO) => {
|
||||
const { data } = await apiRequest.post<MachineAuthTemplateUsage[]>(
|
||||
`/api/v1/identity-templates/${dto.templateId}/delete-usage`,
|
||||
{ identityIds: dto.identityIds },
|
||||
{ params: { organizationId: dto.organizationId } }
|
||||
);
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { templateId, organizationId }) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: identityAuthTemplatesKeys.getTemplateUsages(templateId)
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: identityAuthTemplatesKeys.getTemplates({ organizationId })
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,89 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import {
|
||||
GetIdentityAuthTemplatesDTO,
|
||||
GetTemplateUsagesDTO,
|
||||
IdentityAuthTemplate,
|
||||
MachineAuthTemplateUsage,
|
||||
MachineIdentityAuthMethod
|
||||
} from "./types";
|
||||
|
||||
export const identityAuthTemplatesKeys = {
|
||||
all: ["identity-auth-templates"] as const,
|
||||
getTemplates: (dto: GetIdentityAuthTemplatesDTO) =>
|
||||
[...identityAuthTemplatesKeys.all, "list", dto] as const,
|
||||
getTemplate: (templateId: string) =>
|
||||
[...identityAuthTemplatesKeys.all, "single", templateId] as const,
|
||||
getAvailableTemplates: (authMethod: MachineIdentityAuthMethod) =>
|
||||
[...identityAuthTemplatesKeys.all, "available", authMethod] as const,
|
||||
getTemplateUsages: (templateId: string) =>
|
||||
[...identityAuthTemplatesKeys.all, "usages", templateId] as const
|
||||
};
|
||||
|
||||
export const useGetIdentityAuthTemplates = (dto: GetIdentityAuthTemplatesDTO) => {
|
||||
return useQuery({
|
||||
queryKey: identityAuthTemplatesKeys.getTemplates(dto),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{
|
||||
templates: IdentityAuthTemplate[];
|
||||
totalCount: number;
|
||||
}>("/api/v1/identity-templates/search", {
|
||||
params: {
|
||||
organizationId: dto.organizationId,
|
||||
limit: dto.limit || 50,
|
||||
offset: dto.offset || 0,
|
||||
...(dto.search && { search: dto.search })
|
||||
}
|
||||
});
|
||||
return data;
|
||||
},
|
||||
enabled: Boolean(dto.organizationId)
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetIdentityAuthTemplate = (templateId: string, organizationId: string) => {
|
||||
return useQuery({
|
||||
queryKey: identityAuthTemplatesKeys.getTemplate(templateId),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<IdentityAuthTemplate>(
|
||||
`/api/v1/identity-templates/${templateId}`,
|
||||
{
|
||||
params: { organizationId }
|
||||
}
|
||||
);
|
||||
return data;
|
||||
},
|
||||
enabled: Boolean(templateId) && Boolean(organizationId)
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetAvailableTemplates = (authMethod: MachineIdentityAuthMethod) => {
|
||||
return useQuery({
|
||||
queryKey: identityAuthTemplatesKeys.getAvailableTemplates(authMethod),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<IdentityAuthTemplate[]>("/api/v1/identity-templates", {
|
||||
params: { authMethod }
|
||||
});
|
||||
return data;
|
||||
},
|
||||
enabled: Boolean(authMethod)
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetTemplateUsages = (dto: GetTemplateUsagesDTO) => {
|
||||
return useQuery({
|
||||
queryKey: identityAuthTemplatesKeys.getTemplateUsages(dto.templateId),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<MachineAuthTemplateUsage[]>(
|
||||
`/api/v1/identity-templates/${dto.templateId}/usage`,
|
||||
{
|
||||
params: { organizationId: dto.organizationId }
|
||||
}
|
||||
);
|
||||
return data;
|
||||
},
|
||||
enabled: Boolean(dto.templateId) && Boolean(dto.organizationId)
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,78 @@
|
||||
export enum MachineIdentityAuthMethod {
|
||||
LDAP = "ldap"
|
||||
}
|
||||
|
||||
export interface LdapTemplateFields {
|
||||
url: string;
|
||||
bindDN: string;
|
||||
bindPass: string;
|
||||
searchBase: string;
|
||||
ldapCaCertificate?: string;
|
||||
}
|
||||
|
||||
export interface IdentityAuthTemplate {
|
||||
id: string;
|
||||
name: string;
|
||||
authMethod: MachineIdentityAuthMethod;
|
||||
organizationId: string;
|
||||
templateFields: LdapTemplateFields;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface CreateIdentityAuthTemplateDTO {
|
||||
organizationId: string;
|
||||
name: string;
|
||||
authMethod: MachineIdentityAuthMethod;
|
||||
templateFields: LdapTemplateFields;
|
||||
}
|
||||
|
||||
export interface UpdateIdentityAuthTemplateDTO {
|
||||
templateId: string;
|
||||
organizationId: string;
|
||||
name?: string;
|
||||
templateFields?: Partial<LdapTemplateFields>;
|
||||
}
|
||||
|
||||
export interface DeleteIdentityAuthTemplateDTO {
|
||||
templateId: string;
|
||||
organizationId: string;
|
||||
}
|
||||
|
||||
export interface GetIdentityAuthTemplatesDTO {
|
||||
organizationId: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
search?: string;
|
||||
}
|
||||
|
||||
export interface MachineAuthTemplateUsage {
|
||||
identityId: string;
|
||||
identityName: string;
|
||||
}
|
||||
|
||||
export interface GetTemplateUsagesDTO {
|
||||
templateId: string;
|
||||
organizationId: string;
|
||||
}
|
||||
|
||||
export interface UnlinkTemplateUsageDTO {
|
||||
templateId: string;
|
||||
identityIds: string[];
|
||||
organizationId: string;
|
||||
}
|
||||
|
||||
export const TEMPLATE_ERROR_MESSAGES = {
|
||||
UNLINK_SUCCESS: "Successfully unlinked template usages",
|
||||
UNLINK_FAILED: "Failed to unlink template usages",
|
||||
SINGLE_UNLINK_SUCCESS: "Successfully unlinked template usage",
|
||||
SINGLE_UNLINK_FAILED: "Failed to unlink template usage"
|
||||
} as const;
|
||||
|
||||
export const TEMPLATE_UI_LABELS = {
|
||||
VIEW_USAGES: "View Usages",
|
||||
EDIT_TEMPLATE: "Edit Template",
|
||||
DELETE_TEMPLATE: "Delete Template",
|
||||
UNLINK: "Unlink",
|
||||
UNSELECT_ALL: "Unselect All"
|
||||
} as const;
|
||||
@@ -15,6 +15,7 @@ export * from "./gateways";
|
||||
export * from "./githubOrgSyncConfig";
|
||||
export * from "./groups";
|
||||
export * from "./identities";
|
||||
export * from "./identityAuthTemplates";
|
||||
export * from "./identityProjectAdditionalPrivilege";
|
||||
export * from "./incidentContacts";
|
||||
export * from "./integrationAuth";
|
||||
|
||||
@@ -53,4 +53,5 @@ export type SubscriptionPlan = {
|
||||
secretScanning: boolean;
|
||||
enterpriseSecretSyncs: boolean;
|
||||
enterpriseAppConnections: boolean;
|
||||
machineIdentityAuthTemplates: boolean;
|
||||
};
|
||||
|
||||
+317
@@ -0,0 +1,317 @@
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Select,
|
||||
SelectItem,
|
||||
TextArea
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization } from "@app/context";
|
||||
import {
|
||||
MachineIdentityAuthMethod,
|
||||
useCreateIdentityAuthTemplate,
|
||||
useUpdateIdentityAuthTemplate
|
||||
} from "@app/hooks/api/identityAuthTemplates";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const authMethods = [{ label: "LDAP Auth", value: MachineIdentityAuthMethod.LDAP }];
|
||||
|
||||
const schema = z.object({
|
||||
name: z.string().min(1, "Template name is required"),
|
||||
method: z.nativeEnum(MachineIdentityAuthMethod),
|
||||
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"),
|
||||
ldapCaCertificate: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((val) => val || undefined)
|
||||
});
|
||||
|
||||
export type FormData = z.infer<typeof schema>;
|
||||
|
||||
type Props = {
|
||||
popUp: UsePopUpState<["createTemplate", "editTemplate"]>;
|
||||
handlePopUpToggle: (
|
||||
popUpName: keyof UsePopUpState<["createTemplate", "editTemplate"]>,
|
||||
state?: boolean
|
||||
) => void;
|
||||
};
|
||||
|
||||
export const IdentityAuthTemplateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const orgId = currentOrg?.id || "";
|
||||
|
||||
const { mutateAsync: createTemplate } = useCreateIdentityAuthTemplate();
|
||||
const { mutateAsync: updateTemplate } = useUpdateIdentityAuthTemplate();
|
||||
|
||||
const isEdit = popUp.editTemplate.isOpen;
|
||||
const template = popUp.editTemplate?.data?.template;
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
watch,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
name: "",
|
||||
method: MachineIdentityAuthMethod.LDAP,
|
||||
url: "",
|
||||
bindDN: "",
|
||||
bindPass: "",
|
||||
searchBase: "",
|
||||
ldapCaCertificate: ""
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isEdit && template) {
|
||||
reset({
|
||||
name: template.name || "",
|
||||
method: MachineIdentityAuthMethod.LDAP,
|
||||
url: template.templateFields?.url || "",
|
||||
bindDN: template.templateFields?.bindDN || "",
|
||||
bindPass: template.templateFields?.bindPass || "",
|
||||
searchBase: template.templateFields?.searchBase || "",
|
||||
ldapCaCertificate: template.templateFields?.ldapCaCertificate || ""
|
||||
});
|
||||
} else {
|
||||
reset({
|
||||
name: "",
|
||||
method: MachineIdentityAuthMethod.LDAP,
|
||||
url: "",
|
||||
bindDN: "",
|
||||
bindPass: "",
|
||||
searchBase: "",
|
||||
ldapCaCertificate: ""
|
||||
});
|
||||
}
|
||||
}, [isEdit, template, reset]);
|
||||
|
||||
const selectedMethod = watch("method");
|
||||
|
||||
const onFormSubmit = async (data: FormData) => {
|
||||
try {
|
||||
if (isEdit && template) {
|
||||
await updateTemplate({
|
||||
templateId: template.id,
|
||||
organizationId: orgId,
|
||||
name: data.name,
|
||||
templateFields: {
|
||||
url: data.url,
|
||||
bindDN: data.bindDN,
|
||||
bindPass: data.bindPass,
|
||||
searchBase: data.searchBase,
|
||||
ldapCaCertificate: data.ldapCaCertificate
|
||||
}
|
||||
});
|
||||
createNotification({
|
||||
text: "Successfully updated auth template",
|
||||
type: "success"
|
||||
});
|
||||
} else {
|
||||
await createTemplate({
|
||||
organizationId: orgId,
|
||||
name: data.name,
|
||||
authMethod: data.method,
|
||||
templateFields: {
|
||||
url: data.url,
|
||||
bindDN: data.bindDN,
|
||||
bindPass: data.bindPass,
|
||||
searchBase: data.searchBase,
|
||||
ldapCaCertificate: data.ldapCaCertificate
|
||||
}
|
||||
});
|
||||
createNotification({
|
||||
text: "Successfully created auth template",
|
||||
type: "success"
|
||||
});
|
||||
}
|
||||
|
||||
handlePopUpToggle(isEdit ? "editTemplate" : "createTemplate", false);
|
||||
reset();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
const error = err as any;
|
||||
const text =
|
||||
error?.response?.data?.message ?? `Failed to ${isEdit ? "update" : "create"} auth template`;
|
||||
|
||||
createNotification({
|
||||
text,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
handlePopUpToggle(isEdit ? "editTemplate" : "createTemplate", false);
|
||||
reset();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={popUp.createTemplate.isOpen || popUp.editTemplate.isOpen}
|
||||
onOpenChange={handleClose}
|
||||
>
|
||||
<ModalContent
|
||||
title={isEdit ? "Edit Identity Auth Template" : "Create Identity Auth Template"}
|
||||
subTitle={
|
||||
isEdit ? "Update the authentication template" : "Create a new authentication template"
|
||||
}
|
||||
>
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="name"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Template Name"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isRequired
|
||||
>
|
||||
<Input {...field} placeholder="My Template" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="method"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Authentication Method"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isRequired
|
||||
>
|
||||
<Select
|
||||
{...field}
|
||||
className="w-full"
|
||||
position="popper"
|
||||
placeholder="Select auth method..."
|
||||
dropdownContainerClassName="max-w-none"
|
||||
onValueChange={(value) => field.onChange(value)}
|
||||
>
|
||||
{authMethods.map(({ label, value }) => (
|
||||
<SelectItem key={value} value={value}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* LDAP Configuration Fields */}
|
||||
{selectedMethod === "ldap" && (
|
||||
<>
|
||||
<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
|
||||
label="Bind DN"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isRequired
|
||||
>
|
||||
<Input {...field} placeholder="cn=infisical,ou=Users,dc=example,dc=com" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="bindPass"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Bind Pass"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isRequired
|
||||
>
|
||||
<Input {...field} placeholder="********" type="password" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="searchBase"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Search Base / DN"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isRequired
|
||||
>
|
||||
<Input {...field} placeholder="ou=machines,dc=acme,dc=com" />
|
||||
</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>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="mt-8 flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
{isEdit ? "Update Template" : "Create Template"}
|
||||
</Button>
|
||||
<Button colorSchema="secondary" variant="plain" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
+281
@@ -0,0 +1,281 @@
|
||||
import {
|
||||
faArrowDown,
|
||||
faArrowUp,
|
||||
faEdit,
|
||||
faEllipsisV,
|
||||
faEye,
|
||||
faMagnifyingGlass,
|
||||
faServer,
|
||||
faTrash
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
EmptyState,
|
||||
IconButton,
|
||||
Input,
|
||||
Pagination,
|
||||
Spinner,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||
import { OrgPermissionMachineIdentityAuthTemplateActions } from "@app/context/OrgPermissionContext/types";
|
||||
import {
|
||||
getUserTablePreference,
|
||||
PreferenceKey,
|
||||
setUserTablePreference
|
||||
} from "@app/helpers/userTablePreferences";
|
||||
import { usePagination, useResetPageHelper } from "@app/hooks";
|
||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
import {
|
||||
TEMPLATE_UI_LABELS,
|
||||
useGetIdentityAuthTemplates
|
||||
} from "@app/hooks/api/identityAuthTemplates";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
enum TemplatesOrderBy {
|
||||
Name = "name",
|
||||
AuthMethod = "authMethod"
|
||||
}
|
||||
|
||||
type Props = {
|
||||
handlePopUpOpen: (
|
||||
popUpName: keyof UsePopUpState<
|
||||
["deleteTemplate", "createTemplate", "editTemplate", "viewUsages"]
|
||||
>,
|
||||
data?: any
|
||||
) => void;
|
||||
};
|
||||
|
||||
export const IdentityAuthTemplatesTable = ({ handlePopUpOpen }: Props) => {
|
||||
const { currentOrg } = useOrganization();
|
||||
|
||||
const {
|
||||
offset,
|
||||
limit,
|
||||
orderBy,
|
||||
setOrderBy,
|
||||
orderDirection,
|
||||
setOrderDirection,
|
||||
search,
|
||||
debouncedSearch,
|
||||
setPage,
|
||||
setSearch,
|
||||
perPage,
|
||||
page,
|
||||
setPerPage
|
||||
} = usePagination<TemplatesOrderBy>(TemplatesOrderBy.Name, {
|
||||
initPerPage: getUserTablePreference("templatesTable", PreferenceKey.PerPage, 20)
|
||||
});
|
||||
|
||||
const handlePerPageChange = (newPerPage: number) => {
|
||||
setPerPage(newPerPage);
|
||||
setUserTablePreference("templatesTable", PreferenceKey.PerPage, newPerPage);
|
||||
};
|
||||
|
||||
const organizationId = currentOrg?.id || "";
|
||||
|
||||
const { data, isPending, isFetching } = useGetIdentityAuthTemplates({
|
||||
organizationId,
|
||||
limit,
|
||||
offset,
|
||||
search: debouncedSearch
|
||||
});
|
||||
|
||||
const { templates = [], totalCount = 0 } = data ?? {};
|
||||
useResetPageHelper({
|
||||
totalCount,
|
||||
offset,
|
||||
setPage
|
||||
});
|
||||
|
||||
const handleSort = (column: TemplatesOrderBy) => {
|
||||
if (column === orderBy) {
|
||||
setOrderDirection((prev) =>
|
||||
prev === OrderByDirection.ASC ? OrderByDirection.DESC : OrderByDirection.ASC
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setOrderBy(column);
|
||||
setOrderDirection(OrderByDirection.ASC);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-4 flex items-center space-x-2">
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search templates by name..."
|
||||
/>
|
||||
</div>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr className="h-14">
|
||||
<Th className="w-1/6">
|
||||
<div className="flex items-center">
|
||||
Name
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={`ml-2 ${orderBy === TemplatesOrderBy.Name ? "" : "opacity-30"}`}
|
||||
ariaLabel="sort"
|
||||
onClick={() => handleSort(TemplatesOrderBy.Name)}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={
|
||||
orderDirection === OrderByDirection.DESC &&
|
||||
orderBy === TemplatesOrderBy.Name
|
||||
? faArrowUp
|
||||
: faArrowDown
|
||||
}
|
||||
/>
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th className="w-1/6">
|
||||
<div className="flex items-center">
|
||||
Method
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={`ml-2 ${orderBy === TemplatesOrderBy.AuthMethod ? "" : "opacity-30"}`}
|
||||
ariaLabel="sort"
|
||||
onClick={() => handleSort(TemplatesOrderBy.AuthMethod)}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={
|
||||
orderDirection === OrderByDirection.DESC &&
|
||||
orderBy === TemplatesOrderBy.AuthMethod
|
||||
? faArrowUp
|
||||
: faArrowDown
|
||||
}
|
||||
/>
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th className="w-2/3">URL</Th>
|
||||
<Th className="w-16">{isFetching ? <Spinner size="xs" /> : null}</Th>
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isPending && <TableSkeleton columns={4} innerKey="identity-auth-templates" />}
|
||||
{!isPending &&
|
||||
templates?.map((template) => (
|
||||
<Tr
|
||||
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
||||
key={`template-${template.id}`}
|
||||
>
|
||||
<Td>{template.name}</Td>
|
||||
<Td>
|
||||
<div className="flex items-center">
|
||||
<span className="uppercase">{template.authMethod}</span>
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
<span className="text-sm text-mineshaft-400">
|
||||
{template.templateFields.url}
|
||||
</span>
|
||||
</Td>
|
||||
<Td>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
ariaLabel="Options"
|
||||
className="w-6"
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
>
|
||||
<FontAwesomeIcon icon={faEllipsisV} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent sideOffset={2} align="end">
|
||||
<DropdownMenuItem
|
||||
icon={<FontAwesomeIcon icon={faEye} />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("viewUsages", { template });
|
||||
}}
|
||||
>
|
||||
{TEMPLATE_UI_LABELS.VIEW_USAGES}
|
||||
</DropdownMenuItem>
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates}
|
||||
a={OrgPermissionSubjects.MachineIdentityAuthTemplate}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
icon={<FontAwesomeIcon icon={faEdit} />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("editTemplate", { template });
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
{TEMPLATE_UI_LABELS.EDIT_TEMPLATE}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates}
|
||||
a={OrgPermissionSubjects.MachineIdentityAuthTemplate}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("deleteTemplate", {
|
||||
templateId: template.id,
|
||||
name: template.name
|
||||
});
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||
>
|
||||
{TEMPLATE_UI_LABELS.DELETE_TEMPLATE}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</TBody>
|
||||
</Table>
|
||||
{!isPending && data && totalCount > 0 && (
|
||||
<Pagination
|
||||
count={totalCount}
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
onChangePage={(newPage) => setPage(newPage)}
|
||||
onChangePerPage={handlePerPageChange}
|
||||
/>
|
||||
)}
|
||||
{!isPending && data && templates.length === 0 && (
|
||||
<EmptyState
|
||||
title={
|
||||
debouncedSearch.trim().length > 0
|
||||
? "No templates match search filter"
|
||||
: "No identity auth templates have been created"
|
||||
}
|
||||
icon={faServer}
|
||||
/>
|
||||
)}
|
||||
</TableContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+286
-82
@@ -11,6 +11,8 @@ import {
|
||||
FormControl,
|
||||
IconButton,
|
||||
Input,
|
||||
Select,
|
||||
SelectItem,
|
||||
Tab,
|
||||
TabList,
|
||||
TabPanel,
|
||||
@@ -18,23 +20,31 @@ 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,
|
||||
useGetIdentityLdapAuth,
|
||||
useUpdateIdentityLdapAuth
|
||||
} from "@app/hooks/api";
|
||||
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
|
||||
import { useGetAvailableTemplates } from "@app/hooks/api/identityAuthTemplates/queries";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
import { IdentityFormTab } from "./types";
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
url: z.string().min(1),
|
||||
bindDN: z.string(),
|
||||
bindPass: z.string(),
|
||||
searchBase: z.string(),
|
||||
scope: z.enum(["template", "custom"]),
|
||||
templateId: z.string().optional(),
|
||||
url: z.string().optional(),
|
||||
bindDN: z.string().optional(),
|
||||
bindPass: z.string().optional(),
|
||||
searchBase: z.string().optional(),
|
||||
searchFilter: z.string(), // defaults to (uid={{username}})
|
||||
ldapCaCertificate: z
|
||||
.string()
|
||||
@@ -66,7 +76,50 @@ const schema = z
|
||||
)
|
||||
.min(1)
|
||||
})
|
||||
.required();
|
||||
.superRefine((data, ctx) => {
|
||||
// Validation based on scope
|
||||
if (data.scope === "template") {
|
||||
if (!data.templateId) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Template is required when using template scope",
|
||||
path: ["templateId"]
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.scope === "custom") {
|
||||
if (!data.url) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "LDAP URL is required when using custom scope",
|
||||
path: ["url"]
|
||||
});
|
||||
}
|
||||
if (!data.bindDN) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Bind DN is required when using custom scope",
|
||||
path: ["bindDN"]
|
||||
});
|
||||
}
|
||||
if (!data.bindPass) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Bind Pass is required when using custom scope",
|
||||
path: ["bindPass"]
|
||||
});
|
||||
}
|
||||
if (!data.searchBase) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Search Base is required when using custom scope",
|
||||
path: ["searchBase"]
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export type FormData = z.infer<typeof schema>;
|
||||
|
||||
@@ -93,6 +146,13 @@ export const IdentityLdapAuthForm = ({
|
||||
const { mutateAsync: addMutateAsync } = useAddIdentityLdapAuth();
|
||||
const { mutateAsync: updateMutateAsync } = useUpdateIdentityLdapAuth();
|
||||
const [tabValue, setTabValue] = useState<IdentityFormTab>(IdentityFormTab.Configuration);
|
||||
const { data: templates } = useGetAvailableTemplates(MachineIdentityAuthMethod.LDAP);
|
||||
const { permission } = useOrgPermission();
|
||||
|
||||
const canAttachTemplates = permission.can(
|
||||
OrgPermissionMachineIdentityAuthTemplateActions.AttachTemplates,
|
||||
OrgPermissionSubjects.MachineIdentityAuthTemplate
|
||||
);
|
||||
|
||||
const { data } = useGetIdentityLdapAuth(identityId ?? "", {
|
||||
enabled: isUpdate
|
||||
@@ -102,11 +162,14 @@ export const IdentityLdapAuthForm = ({
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
|
||||
watch,
|
||||
setValue,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
scope: "custom",
|
||||
templateId: "",
|
||||
url: "",
|
||||
bindDN: "",
|
||||
bindPass: "",
|
||||
@@ -119,6 +182,8 @@ export const IdentityLdapAuthForm = ({
|
||||
}
|
||||
});
|
||||
|
||||
const scope = watch("scope");
|
||||
|
||||
const {
|
||||
fields: accessTokenTrustedIpsFields,
|
||||
append: appendAccessTokenTrustedIp,
|
||||
@@ -131,16 +196,30 @@ export const IdentityLdapAuthForm = ({
|
||||
remove: removeAllowedField
|
||||
} = useFieldArray({ control, name: "allowedFields" });
|
||||
|
||||
// Helper function to determine scope based on existing data
|
||||
const determineScope = (authData: any) => {
|
||||
// If templateId exists in the data, it's template scope
|
||||
if (authData.templateId) {
|
||||
return "template";
|
||||
}
|
||||
// Default to custom if we can't determine
|
||||
return "custom";
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
const detectedScope = determineScope(data);
|
||||
|
||||
reset({
|
||||
url: data.url,
|
||||
bindDN: data.bindDN,
|
||||
bindPass: data.bindPass,
|
||||
searchBase: data.searchBase,
|
||||
scope: detectedScope,
|
||||
templateId: data.templateId || "",
|
||||
url: data.url || "",
|
||||
bindDN: data.bindDN || "",
|
||||
bindPass: data.bindPass || "",
|
||||
searchBase: data.searchBase || "",
|
||||
searchFilter: data.searchFilter,
|
||||
ldapCaCertificate: data.ldapCaCertificate || undefined,
|
||||
allowedFields: data.allowedFields,
|
||||
allowedFields: data.allowedFields || [],
|
||||
accessTokenTTL: String(data.accessTokenTTL),
|
||||
accessTokenMaxTTL: String(data.accessTokenMaxTTL),
|
||||
accessTokenNumUsesLimit: String(data.accessTokenNumUsesLimit),
|
||||
@@ -152,78 +231,81 @@ export const IdentityLdapAuthForm = ({
|
||||
}
|
||||
)
|
||||
});
|
||||
} else {
|
||||
reset({
|
||||
url: "",
|
||||
bindDN: "",
|
||||
bindPass: "",
|
||||
searchBase: "",
|
||||
searchFilter: "(uid={{username}})",
|
||||
ldapCaCertificate: undefined,
|
||||
allowedFields: [],
|
||||
accessTokenTTL: "2592000",
|
||||
accessTokenMaxTTL: "2592000",
|
||||
accessTokenNumUsesLimit: "0",
|
||||
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
|
||||
});
|
||||
return;
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
reset({
|
||||
scope: "custom",
|
||||
templateId: "",
|
||||
url: "",
|
||||
bindDN: "",
|
||||
bindPass: "",
|
||||
searchBase: "",
|
||||
searchFilter: "(uid={{username}})",
|
||||
ldapCaCertificate: undefined,
|
||||
allowedFields: [],
|
||||
accessTokenTTL: "2592000",
|
||||
accessTokenMaxTTL: "2592000",
|
||||
accessTokenNumUsesLimit: "0",
|
||||
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
|
||||
});
|
||||
}, [data, reset]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!subscription?.ldap) {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
handlePopUpToggle("identityAuthMethod", false);
|
||||
}
|
||||
}, [subscription]);
|
||||
}, [subscription, handlePopUpOpen, handlePopUpToggle]);
|
||||
|
||||
const onFormSubmit = async ({
|
||||
url,
|
||||
bindDN,
|
||||
bindPass,
|
||||
searchBase,
|
||||
searchFilter,
|
||||
ldapCaCertificate,
|
||||
allowedFields,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps
|
||||
}: FormData) => {
|
||||
const onFormSubmit = async (formData: FormData) => {
|
||||
try {
|
||||
if (!identityId) return;
|
||||
|
||||
const {
|
||||
scope: submissionScope,
|
||||
templateId: submissionTemplateId,
|
||||
url: submissionUrl,
|
||||
bindDN: submissionBindDN,
|
||||
bindPass: submissionBindPass,
|
||||
searchBase: submissionSearchBase,
|
||||
searchFilter,
|
||||
ldapCaCertificate,
|
||||
allowedFields,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps
|
||||
} = formData;
|
||||
|
||||
const basePayload = {
|
||||
organizationId: orgId,
|
||||
identityId,
|
||||
searchFilter,
|
||||
ldapCaCertificate,
|
||||
allowedFields,
|
||||
accessTokenTTL: Number(accessTokenTTL),
|
||||
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
||||
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
|
||||
accessTokenTrustedIps
|
||||
};
|
||||
|
||||
// Add scope-specific fields
|
||||
const payload =
|
||||
submissionScope === "template"
|
||||
? { ...basePayload, templateId: submissionTemplateId }
|
||||
: {
|
||||
...basePayload,
|
||||
url: submissionUrl,
|
||||
bindDN: submissionBindDN,
|
||||
bindPass: submissionBindPass,
|
||||
searchBase: submissionSearchBase
|
||||
};
|
||||
|
||||
if (data) {
|
||||
await updateMutateAsync({
|
||||
organizationId: orgId,
|
||||
identityId,
|
||||
url,
|
||||
bindDN,
|
||||
bindPass,
|
||||
searchBase,
|
||||
searchFilter,
|
||||
ldapCaCertificate,
|
||||
allowedFields,
|
||||
accessTokenTTL: Number(accessTokenTTL),
|
||||
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
||||
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
|
||||
accessTokenTrustedIps
|
||||
});
|
||||
await updateMutateAsync(payload);
|
||||
} else {
|
||||
await addMutateAsync({
|
||||
organizationId: orgId,
|
||||
identityId,
|
||||
url,
|
||||
bindDN,
|
||||
bindPass,
|
||||
searchBase,
|
||||
searchFilter,
|
||||
ldapCaCertificate,
|
||||
allowedFields,
|
||||
accessTokenTTL: Number(accessTokenTTL),
|
||||
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
||||
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
|
||||
accessTokenTrustedIps
|
||||
});
|
||||
await addMutateAsync(payload);
|
||||
}
|
||||
|
||||
handlePopUpToggle("identityAuthMethod", false);
|
||||
@@ -247,6 +329,8 @@ export const IdentityLdapAuthForm = ({
|
||||
onSubmit={handleSubmit(onFormSubmit, (fields) => {
|
||||
setTabValue(
|
||||
[
|
||||
"scope",
|
||||
"templateId",
|
||||
"url",
|
||||
"bindDN",
|
||||
"bindPass",
|
||||
@@ -268,18 +352,102 @@ export const IdentityLdapAuthForm = ({
|
||||
<Tab value={IdentityFormTab.Advanced}>Advanced</Tab>
|
||||
</TabList>
|
||||
<TabPanel value={IdentityFormTab.Configuration}>
|
||||
{canAttachTemplates && (
|
||||
<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);
|
||||
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
|
||||
control={control}
|
||||
name="templateId"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Template"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isRequired
|
||||
>
|
||||
<Select
|
||||
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"
|
||||
dropdownContainerClassName="max-w-none"
|
||||
placeholder="Select a template"
|
||||
>
|
||||
{templates?.map((template) => {
|
||||
return (
|
||||
<SelectItem value={template.id} key={template.id}>
|
||||
{template.name}
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
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" />
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="ldaps://domain-or-ip:636"
|
||||
type="text"
|
||||
isDisabled={scope === "template"}
|
||||
containerClassName={scope === "template" ? "opacity-55" : ""}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
@@ -292,14 +460,23 @@ export const IdentityLdapAuthForm = ({
|
||||
label="Bind DN"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText={
|
||||
scope === "template"
|
||||
? "This field cannot be modified when using a template"
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<Input {...field} placeholder="cn=infisical,ou=Users,dc=example,dc=com" />
|
||||
<Input
|
||||
{...field}
|
||||
containerClassName={scope === "template" ? "opacity-55" : ""}
|
||||
placeholder="cn=infisical,ou=Users,dc=example,dc=com"
|
||||
isDisabled={scope === "template"}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="bindPass"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
@@ -307,8 +484,19 @@ export const IdentityLdapAuthForm = ({
|
||||
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" />
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="********"
|
||||
type="password"
|
||||
containerClassName={scope === "template" ? "opacity-55" : ""}
|
||||
isDisabled={scope === "template"}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
@@ -321,8 +509,18 @@ export const IdentityLdapAuthForm = ({
|
||||
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" />
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="ou=machines,dc=acme,dc=com"
|
||||
containerClassName={scope === "template" ? "opacity-55" : ""}
|
||||
isDisabled={scope === "template"}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
@@ -452,7 +650,6 @@ export const IdentityLdapAuthForm = ({
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
@@ -467,7 +664,6 @@ export const IdentityLdapAuthForm = ({
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenMaxTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
@@ -482,7 +678,6 @@ export const IdentityLdapAuthForm = ({
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="0"
|
||||
name="accessTokenNumUsesLimit"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
@@ -506,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>
|
||||
)}
|
||||
/>
|
||||
|
||||
+147
-46
@@ -11,15 +11,18 @@ import {
|
||||
useOrganization,
|
||||
useSubscription
|
||||
} from "@app/context";
|
||||
import { OrgPermissionMachineIdentityAuthTemplateActions } from "@app/context/OrgPermissionContext/types";
|
||||
import { withPermission } from "@app/hoc";
|
||||
import { useDeleteIdentity } from "@app/hooks/api";
|
||||
import { useDeleteIdentityAuthTemplate } from "@app/hooks/api/identityAuthTemplates";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
// import { IdentityAuthMethodModal } from "./IdentityAuthMethodModal";
|
||||
import { IdentityAuthTemplateModal } from "./IdentityAuthTemplateModal";
|
||||
import { IdentityAuthTemplatesTable } from "./IdentityAuthTemplatesTable";
|
||||
import { IdentityModal } from "./IdentityModal";
|
||||
import { IdentityTable } from "./IdentityTable";
|
||||
import { IdentityTokenAuthTokenModal } from "./IdentityTokenAuthTokenModal";
|
||||
// import { IdentityUniversalAuthClientSecretModal } from "./IdentityUniversalAuthClientSecretModal";
|
||||
import { MachineAuthTemplateUsagesModal } from "./MachineAuthTemplateUsagesModal";
|
||||
|
||||
export const IdentitySection = withPermission(
|
||||
() => {
|
||||
@@ -28,6 +31,7 @@ export const IdentitySection = withPermission(
|
||||
const orgId = currentOrg?.id || "";
|
||||
|
||||
const { mutateAsync: deleteMutateAsync } = useDeleteIdentity();
|
||||
const { mutateAsync: deleteTemplateMutateAsync } = useDeleteIdentityAuthTemplate();
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"identity",
|
||||
"identityAuthMethod",
|
||||
@@ -35,7 +39,11 @@ export const IdentitySection = withPermission(
|
||||
"universalAuthClientSecret",
|
||||
"deleteUniversalAuthClientSecret",
|
||||
"upgradePlan",
|
||||
"tokenAuthToken"
|
||||
"tokenAuthToken",
|
||||
"createTemplate",
|
||||
"editTemplate",
|
||||
"deleteTemplate",
|
||||
"viewUsages"
|
||||
] as const);
|
||||
|
||||
const isMoreIdentitiesAllowed = subscription?.identityLimit
|
||||
@@ -69,53 +77,133 @@ export const IdentitySection = withPermission(
|
||||
}
|
||||
};
|
||||
|
||||
const onDeleteTemplateSubmit = async (templateId: string) => {
|
||||
try {
|
||||
await deleteTemplateMutateAsync({
|
||||
templateId,
|
||||
organizationId: orgId
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully deleted template",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
handlePopUpClose("deleteTemplate");
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
const error = err as any;
|
||||
const text = error?.response?.data?.message ?? "Failed to delete template";
|
||||
|
||||
createNotification({
|
||||
text,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
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 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}
|
||||
>
|
||||
Create Identity
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
<IdentityTable handlePopUpOpen={handlePopUpOpen} />
|
||||
</div>
|
||||
{/* Identity Auth Templates Section */}
|
||||
<div className="mt-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>
|
||||
<IdentityTable handlePopUpOpen={handlePopUpOpen} />
|
||||
<IdentityModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<IdentityAuthTemplateModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<MachineAuthTemplateUsagesModal
|
||||
isOpen={popUp.viewUsages.isOpen}
|
||||
onClose={() => handlePopUpClose("viewUsages")}
|
||||
templateId={
|
||||
(popUp?.viewUsages?.data as { template: { id: string; name: string } })?.template?.id ||
|
||||
""
|
||||
}
|
||||
templateName={
|
||||
(popUp?.viewUsages?.data as { template: { id: string; name: string } })?.template
|
||||
?.name || ""
|
||||
}
|
||||
/>
|
||||
{/* <IdentityAuthMethodModal
|
||||
popUp={popUp}
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
@@ -140,6 +228,19 @@ export const IdentitySection = withPermission(
|
||||
)
|
||||
}
|
||||
/>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteTemplate.isOpen}
|
||||
title={`Are you sure you want to delete ${
|
||||
(popUp?.deleteTemplate?.data as { name: string })?.name || ""
|
||||
}?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteTemplate", isOpen)}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={() =>
|
||||
onDeleteTemplateSubmit(
|
||||
(popUp?.deleteTemplate?.data as { templateId: string })?.templateId
|
||||
)
|
||||
}
|
||||
/>
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
import { faCertificate } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
|
||||
import {
|
||||
EmptyState,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization } from "@app/context";
|
||||
import { useGetTemplateUsages } from "@app/hooks/api/identityAuthTemplates";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
templateId: string;
|
||||
templateName: string;
|
||||
};
|
||||
|
||||
export const MachineAuthTemplateUsagesModal = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
templateId,
|
||||
templateName
|
||||
}: Props) => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const organizationId = currentOrg?.id || "";
|
||||
|
||||
const { data: usages = [], isPending } = useGetTemplateUsages({
|
||||
templateId,
|
||||
organizationId
|
||||
});
|
||||
|
||||
return (
|
||||
<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}`}
|
||||
onClick={() =>
|
||||
navigate({
|
||||
to: "/organization/identities/$identityId",
|
||||
params: {
|
||||
identityId: usage.identityId
|
||||
}
|
||||
})
|
||||
}
|
||||
>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
OrgPermissionGroupActions,
|
||||
OrgPermissionIdentityActions,
|
||||
OrgPermissionKmipActions,
|
||||
OrgPermissionMachineIdentityAuthTemplateActions,
|
||||
OrgPermissionSecretShareAction
|
||||
} from "@app/context/OrgPermissionContext/types";
|
||||
import { TPermission } from "@app/hooks/api/roles/types";
|
||||
@@ -82,6 +83,17 @@ const orgGatewayPermissionSchema = z
|
||||
})
|
||||
.optional();
|
||||
|
||||
const machineIdentityAuthTemplatePermissionSchema = z
|
||||
.object({
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates]: z.boolean().optional(),
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates]: z.boolean().optional(),
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates]: z.boolean().optional(),
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates]: z.boolean().optional(),
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates]: z.boolean().optional(),
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.AttachTemplates]: z.boolean().optional()
|
||||
})
|
||||
.optional();
|
||||
|
||||
const adminConsolePermissionSchmea = z
|
||||
.object({
|
||||
"access-all-projects": z.boolean().optional()
|
||||
@@ -129,6 +141,7 @@ export const formSchema = z.object({
|
||||
"app-connections": appConnectionsPermissionSchema,
|
||||
kmip: kmipPermissionSchema,
|
||||
gateway: orgGatewayPermissionSchema,
|
||||
"machine-identity-auth-template": machineIdentityAuthTemplatePermissionSchema,
|
||||
"secret-share": secretSharingPermissionSchema
|
||||
})
|
||||
.optional()
|
||||
|
||||
+211
@@ -0,0 +1,211 @@
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form";
|
||||
import { faChevronDown, faChevronRight } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Checkbox, Select, SelectItem, Td, Tr } from "@app/components/v2";
|
||||
import { OrgPermissionMachineIdentityAuthTemplateActions } from "@app/context/OrgPermissionContext/types";
|
||||
import { useToggle } from "@app/hooks";
|
||||
|
||||
import { TFormSchema } from "../OrgRoleModifySection.utils";
|
||||
|
||||
type Props = {
|
||||
isEditable: boolean;
|
||||
setValue: UseFormSetValue<TFormSchema>;
|
||||
control: Control<TFormSchema>;
|
||||
};
|
||||
|
||||
enum Permission {
|
||||
NoAccess = "no-access",
|
||||
ReadOnly = "read-only",
|
||||
FullAccess = "full-access",
|
||||
Custom = "custom"
|
||||
}
|
||||
|
||||
const PERMISSION_ACTIONS = [
|
||||
{
|
||||
action: OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates,
|
||||
label: "List Templates"
|
||||
},
|
||||
{
|
||||
action: OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates,
|
||||
label: "Create Templates"
|
||||
},
|
||||
{
|
||||
action: OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates,
|
||||
label: "Edit Templates"
|
||||
},
|
||||
{
|
||||
action: OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates,
|
||||
label: "Delete Templates"
|
||||
},
|
||||
{
|
||||
action: OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates,
|
||||
label: "Unlink Templates"
|
||||
},
|
||||
{
|
||||
action: OrgPermissionMachineIdentityAuthTemplateActions.AttachTemplates,
|
||||
label: "Attach Templates"
|
||||
}
|
||||
] as const;
|
||||
|
||||
export const OrgPermissionMachineIdentityAuthTemplateRow = ({
|
||||
isEditable,
|
||||
control,
|
||||
setValue
|
||||
}: Props) => {
|
||||
const [isRowExpanded, setIsRowExpanded] = useToggle();
|
||||
const [isCustom, setIsCustom] = useToggle();
|
||||
|
||||
const rule = useWatch({
|
||||
control,
|
||||
name: "permissions.machine-identity-auth-template"
|
||||
});
|
||||
|
||||
const selectedPermissionCategory = useMemo(() => {
|
||||
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
|
||||
const totalActions = PERMISSION_ACTIONS.length;
|
||||
const score = actions.map((key) => (rule?.[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
|
||||
|
||||
if (isCustom) return Permission.Custom;
|
||||
if (score === 0) return Permission.NoAccess;
|
||||
if (score === totalActions) return Permission.FullAccess;
|
||||
if (score === 1 && rule?.[OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates])
|
||||
return Permission.ReadOnly;
|
||||
|
||||
return Permission.Custom;
|
||||
}, [rule, isCustom]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
|
||||
else setIsCustom.off();
|
||||
}, [selectedPermissionCategory]);
|
||||
|
||||
useEffect(() => {
|
||||
const isRowCustom = selectedPermissionCategory === Permission.Custom;
|
||||
if (isRowCustom) {
|
||||
setIsRowExpanded.on();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handlePermissionChange = (val: Permission) => {
|
||||
if (!val) return;
|
||||
if (val === Permission.Custom) {
|
||||
setIsRowExpanded.on();
|
||||
setIsCustom.on();
|
||||
return;
|
||||
}
|
||||
setIsCustom.off();
|
||||
|
||||
switch (val) {
|
||||
case Permission.FullAccess:
|
||||
setValue(
|
||||
"permissions.machine-identity-auth-template",
|
||||
{
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates]: true,
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates]: true,
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates]: true,
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates]: true,
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates]: true,
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.AttachTemplates]: true
|
||||
},
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
break;
|
||||
case Permission.ReadOnly:
|
||||
setValue(
|
||||
"permissions.machine-identity-auth-template",
|
||||
{
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates]: true,
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates]: false,
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates]: false,
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates]: false,
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates]: false,
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.AttachTemplates]: true
|
||||
},
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
break;
|
||||
|
||||
case Permission.NoAccess:
|
||||
default:
|
||||
setValue(
|
||||
"permissions.machine-identity-auth-template",
|
||||
{
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates]: false,
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates]: false,
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates]: false,
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates]: false,
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates]: false,
|
||||
[OrgPermissionMachineIdentityAuthTemplateActions.AttachTemplates]: false
|
||||
},
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tr
|
||||
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
||||
onClick={() => setIsRowExpanded.toggle()}
|
||||
>
|
||||
<Td className="w-4">
|
||||
<FontAwesomeIcon className="w-4" icon={isRowExpanded ? faChevronDown : faChevronRight} />
|
||||
</Td>
|
||||
<Td className="w-full select-none">Machine Identity Auth Templates</Td>
|
||||
<Td>
|
||||
<Select
|
||||
value={selectedPermissionCategory}
|
||||
className="h-8 w-40 bg-mineshaft-700"
|
||||
dropdownContainerClassName="border text-left border-mineshaft-600 bg-mineshaft-800"
|
||||
onValueChange={handlePermissionChange}
|
||||
isDisabled={!isEditable}
|
||||
position="popper"
|
||||
>
|
||||
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
|
||||
<SelectItem value={Permission.ReadOnly}>Read Only</SelectItem>
|
||||
<SelectItem value={Permission.FullAccess}>Full Access</SelectItem>
|
||||
<SelectItem value={Permission.Custom}>Custom</SelectItem>
|
||||
</Select>
|
||||
</Td>
|
||||
</Tr>
|
||||
{isRowExpanded && (
|
||||
<Tr>
|
||||
<Td colSpan={3} className="border-mineshaft-500 bg-mineshaft-900 p-8">
|
||||
<div className="flex flex-grow flex-wrap justify-start gap-x-8 gap-y-4">
|
||||
{PERMISSION_ACTIONS.map(({ action, label }) => {
|
||||
return (
|
||||
<Controller
|
||||
name={`permissions.machine-identity-auth-template.${action}`}
|
||||
key={`permissions.machine-identity-auth-template.${action}`}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Checkbox
|
||||
isChecked={Boolean(field.value)}
|
||||
onCheckedChange={(e) => {
|
||||
if (!isEditable) {
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to update default role"
|
||||
});
|
||||
return;
|
||||
}
|
||||
field.onChange(e);
|
||||
}}
|
||||
id={`permissions.machine-identity-auth-template.${action}`}
|
||||
>
|
||||
{label}
|
||||
</Checkbox>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
+7
-1
@@ -65,7 +65,13 @@ type Props = {
|
||||
title: string;
|
||||
formName: keyof Omit<
|
||||
Exclude<TFormSchema["permissions"], undefined>,
|
||||
"workspace" | "organization-admin-console" | "kmip" | "gateway" | "secret-share" | "billing"
|
||||
| "workspace"
|
||||
| "organization-admin-console"
|
||||
| "kmip"
|
||||
| "gateway"
|
||||
| "secret-share"
|
||||
| "billing"
|
||||
| "machine-identity-auth-template"
|
||||
>;
|
||||
setValue: UseFormSetValue<TFormSchema>;
|
||||
control: Control<TFormSchema>;
|
||||
|
||||
+6
@@ -21,6 +21,7 @@ import { OrgGatewayPermissionRow } from "./OrgPermissionGatewayRow";
|
||||
import { OrgPermissionGroupRow } from "./OrgPermissionGroupRow";
|
||||
import { OrgPermissionIdentityRow } from "./OrgPermissionIdentityRow";
|
||||
import { OrgPermissionKmipRow } from "./OrgPermissionKmipRow";
|
||||
import { OrgPermissionMachineIdentityAuthTemplateRow } from "./OrgPermissionMachineIdentityAuthTemplateRow";
|
||||
import { OrgPermissionSecretShareRow } from "./OrgPermissionSecretShareRow";
|
||||
import { OrgRoleWorkspaceRow } from "./OrgRoleWorkspaceRow";
|
||||
import { RolePermissionRow } from "./RolePermissionRow";
|
||||
@@ -205,6 +206,11 @@ export const RolePermissionsSection = ({ roleId }: Props) => {
|
||||
setValue={setValue}
|
||||
isEditable={isCustomRole}
|
||||
/>
|
||||
<OrgPermissionMachineIdentityAuthTemplateRow
|
||||
control={control}
|
||||
setValue={setValue}
|
||||
isEditable={isCustomRole}
|
||||
/>
|
||||
<OrgPermissionKmipRow
|
||||
control={control}
|
||||
setValue={setValue}
|
||||
|
||||
Reference in New Issue
Block a user