mirror of
https://github.com/Infisical/infisical.git
synced 2026-01-09 15:38:03 -05:00
feature: project role templates
This commit is contained in:
8
frontend/package-lock.json
generated
8
frontend/package-lock.json
generated
@@ -78,7 +78,7 @@
|
||||
"react-day-picker": "^9.4.3",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-hook-form": "^7.54.0",
|
||||
"react-hook-form": "^7.56.3",
|
||||
"react-i18next": "^15.2.0",
|
||||
"react-icons": "^5.4.0",
|
||||
"react-markdown": "^10.0.1",
|
||||
@@ -11484,9 +11484,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-hook-form": {
|
||||
"version": "7.54.0",
|
||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.54.0.tgz",
|
||||
"integrity": "sha512-PS05+UQy/IdSbJNojBypxAo9wllhHgGmyr8/dyGQcPoiMf3e7Dfb9PWYVRco55bLbxH9S+1yDDJeTdlYCSxO3A==",
|
||||
"version": "7.56.3",
|
||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.56.3.tgz",
|
||||
"integrity": "sha512-IK18V6GVbab4TAo1/cz3kqajxbDPGofdF0w7VHdCo0Nt8PrPlOZcuuDq9YYIV1BtjcX78x0XsldbQRQnQXWXmw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
"react-day-picker": "^9.4.3",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-hook-form": "^7.54.0",
|
||||
"react-hook-form": "^7.56.3",
|
||||
"react-i18next": "^15.2.0",
|
||||
"react-icons": "^5.4.0",
|
||||
"react-markdown": "^10.0.1",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||
import { faChevronLeft, faPlus, faSave } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faChevronLeft, faSave } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
@@ -9,12 +9,11 @@ import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, Input } from "@app/components/v2";
|
||||
import { ProjectPermissionSub } from "@app/context";
|
||||
import { isCustomProjectRole } from "@app/helpers/roles";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { TProjectTemplate, useUpdateProjectTemplate } from "@app/hooks/api/projectTemplates";
|
||||
import { slugSchema } from "@app/lib/schemas";
|
||||
import { AddPoliciesButton } from "@app/pages/project/RoleDetailsBySlugPage/components/AddPoliciesButton";
|
||||
import { GeneralPermissionPolicies } from "@app/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies";
|
||||
import { PermissionEmptyState } from "@app/pages/project/RoleDetailsBySlugPage/components/PermissionEmptyState";
|
||||
import { PolicySelectionModal } from "@app/pages/project/RoleDetailsBySlugPage/components/PolicySelectionModal";
|
||||
import {
|
||||
formRolePermission2API,
|
||||
PROJECT_PERMISSION_OBJECT,
|
||||
@@ -44,8 +43,6 @@ export const ProjectTemplateEditRoleForm = ({
|
||||
role,
|
||||
isDisabled
|
||||
}: Props) => {
|
||||
const { popUp, handlePopUpToggle } = usePopUp(["addPolicy"] as const);
|
||||
|
||||
const formMethods = useForm<TFormSchema>({
|
||||
values: role ? { ...role, permissions: rolePermission2Form(role.permissions) } : undefined,
|
||||
resolver: zodResolver(formSchema)
|
||||
@@ -120,7 +117,7 @@ export const ProjectTemplateEditRoleForm = ({
|
||||
variant="outline_bg"
|
||||
type="submit"
|
||||
className={twMerge(
|
||||
"h-10 rounded-r-none border border-primary",
|
||||
"mr-4 h-10 border border-primary",
|
||||
isDirty && "bg-primary text-black"
|
||||
)}
|
||||
isDisabled={isSubmitting || !isDirty || isDisabled}
|
||||
@@ -129,19 +126,7 @@ export const ProjectTemplateEditRoleForm = ({
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<Button
|
||||
className="h-10 rounded-l-none"
|
||||
variant="outline_bg"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
isDisabled={isDisabled}
|
||||
onClick={() => handlePopUpToggle("addPolicy")}
|
||||
>
|
||||
Add Policies
|
||||
</Button>
|
||||
<PolicySelectionModal
|
||||
isOpen={popUp.addPolicy.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("addPolicy", isOpen)}
|
||||
/>
|
||||
<AddPoliciesButton isDisabled={isDisabled} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||
import { subject } from "@casl/ability";
|
||||
import {
|
||||
faCaretDown,
|
||||
faChevronLeft,
|
||||
faClock,
|
||||
faPlus,
|
||||
faSave
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { faCaretDown, faChevronLeft, faClock, faSave } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { format, formatDistance } from "date-fns";
|
||||
@@ -33,16 +27,15 @@ import {
|
||||
useProjectPermission,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import {
|
||||
useCreateIdentityProjectAdditionalPrivilege,
|
||||
useGetIdentityProjectPrivilegeDetails,
|
||||
useUpdateIdentityProjectAdditionalPrivilege
|
||||
} from "@app/hooks/api";
|
||||
import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/hooks/api/identityProjectAdditionalPrivilege/types";
|
||||
import { AddPoliciesButton } from "@app/pages/project/RoleDetailsBySlugPage/components/AddPoliciesButton";
|
||||
import { GeneralPermissionPolicies } from "@app/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies";
|
||||
import { PermissionEmptyState } from "@app/pages/project/RoleDetailsBySlugPage/components/PermissionEmptyState";
|
||||
import { PolicySelectionModal } from "@app/pages/project/RoleDetailsBySlugPage/components/PolicySelectionModal";
|
||||
import {
|
||||
formRolePermission2API,
|
||||
PROJECT_PERMISSION_OBJECT,
|
||||
@@ -97,7 +90,6 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({
|
||||
ProjectPermissionIdentityActions.Edit,
|
||||
subject(ProjectPermissionSub.Identity, { identityId })
|
||||
);
|
||||
const { popUp, handlePopUpToggle } = usePopUp(["addPolicy"] as const);
|
||||
|
||||
const form = useForm<TFormSchema>({
|
||||
values: privilegeDetails
|
||||
@@ -224,7 +216,7 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({
|
||||
variant="outline_bg"
|
||||
type="submit"
|
||||
className={twMerge(
|
||||
"h-10 rounded-r-none border border-primary",
|
||||
"mr-4 h-10 border border-primary",
|
||||
isDirty && "bg-primary text-black"
|
||||
)}
|
||||
isDisabled={isSubmitting || !isDirty || isDisabled}
|
||||
@@ -233,15 +225,7 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<Button
|
||||
isDisabled={isDisabled}
|
||||
className="h-10 rounded-l-none"
|
||||
variant="outline_bg"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handlePopUpToggle("addPolicy")}
|
||||
>
|
||||
Add Policies
|
||||
</Button>
|
||||
<AddPoliciesButton isDisabled={isDisabled} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -382,10 +366,6 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
<PolicySelectionModal
|
||||
isOpen={popUp.addPolicy.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("addPolicy", isOpen)}
|
||||
/>
|
||||
</FormProvider>
|
||||
</form>
|
||||
);
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||
import {
|
||||
faCaretDown,
|
||||
faChevronLeft,
|
||||
faClock,
|
||||
faPlus,
|
||||
faSave
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { faCaretDown, faChevronLeft, faClock, faSave } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { format, formatDistance } from "date-fns";
|
||||
@@ -32,16 +26,15 @@ import {
|
||||
useProjectPermission,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import {
|
||||
useCreateProjectUserAdditionalPrivilege,
|
||||
useGetProjectUserPrivilegeDetails,
|
||||
useUpdateProjectUserAdditionalPrivilege
|
||||
} from "@app/hooks/api";
|
||||
import { ProjectUserAdditionalPrivilegeTemporaryMode } from "@app/hooks/api/projectUserAdditionalPrivilege/types";
|
||||
import { AddPoliciesButton } from "@app/pages/project/RoleDetailsBySlugPage/components/AddPoliciesButton";
|
||||
import { GeneralPermissionPolicies } from "@app/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies";
|
||||
import { PermissionEmptyState } from "@app/pages/project/RoleDetailsBySlugPage/components/PermissionEmptyState";
|
||||
import { PolicySelectionModal } from "@app/pages/project/RoleDetailsBySlugPage/components/PolicySelectionModal";
|
||||
import {
|
||||
formRolePermission2API,
|
||||
PROJECT_PERMISSION_OBJECT,
|
||||
@@ -83,8 +76,6 @@ export const MembershipProjectAdditionalPrivilegeModifySection = ({
|
||||
projectMembershipId,
|
||||
isDisabled
|
||||
}: Props) => {
|
||||
const { popUp, handlePopUpToggle } = usePopUp(["addPolicy"] as const);
|
||||
|
||||
const isCreate = !privilegeId;
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const projectId = currentWorkspace?.id || "";
|
||||
@@ -221,7 +212,7 @@ export const MembershipProjectAdditionalPrivilegeModifySection = ({
|
||||
variant="outline_bg"
|
||||
type="submit"
|
||||
className={twMerge(
|
||||
"h-10 rounded-r-none border border-primary",
|
||||
"mr-4 h-10 border border-primary",
|
||||
isDirty && "bg-primary text-black"
|
||||
)}
|
||||
isDisabled={isSubmitting || !isDirty || isDisabled}
|
||||
@@ -230,15 +221,7 @@ export const MembershipProjectAdditionalPrivilegeModifySection = ({
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<Button
|
||||
isDisabled={isDisabled}
|
||||
className="h-10 rounded-l-none"
|
||||
variant="outline_bg"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handlePopUpToggle("addPolicy")}
|
||||
>
|
||||
Add Policies
|
||||
</Button>
|
||||
<AddPoliciesButton isDisabled={isDisabled} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -377,10 +360,6 @@ export const MembershipProjectAdditionalPrivilegeModifySection = ({
|
||||
</GeneralPermissionPolicies>
|
||||
))}
|
||||
</div>
|
||||
<PolicySelectionModal
|
||||
isOpen={popUp.addPolicy.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("addPolicy", isOpen)}
|
||||
/>
|
||||
</FormProvider>
|
||||
</form>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { faAngleDown, faLayerGroup, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import {
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
IconButton
|
||||
} from "@app/components/v2";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { PolicySelectionModal } from "@app/pages/project/RoleDetailsBySlugPage/components/PolicySelectionModal";
|
||||
import { PolicyTemplateModal } from "@app/pages/project/RoleDetailsBySlugPage/components/PolicyTemplateModal";
|
||||
|
||||
type Props = {
|
||||
isDisabled?: boolean;
|
||||
};
|
||||
|
||||
export const AddPoliciesButton = ({ isDisabled }: Props) => {
|
||||
const { popUp, handlePopUpToggle, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||
"addPolicy",
|
||||
"addPolicyOptions",
|
||||
"applyTemplate"
|
||||
] as const);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
className="h-10 rounded-r-none"
|
||||
variant="outline_bg"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
isDisabled={isDisabled}
|
||||
onClick={() => handlePopUpToggle("addPolicy")}
|
||||
>
|
||||
Add Policies
|
||||
</Button>
|
||||
<DropdownMenu
|
||||
open={popUp.addPolicyOptions.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("addPolicyOptions", isOpen)}
|
||||
>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
ariaLabel="add-policies-options"
|
||||
variant="outline_bg"
|
||||
className="rounded-l-none bg-mineshaft-600 p-3"
|
||||
>
|
||||
<FontAwesomeIcon icon={faAngleDown} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent sideOffset={2} align="end">
|
||||
<div className="flex flex-col space-y-1 p-1.5">
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faLayerGroup} className="pr-2" />}
|
||||
onClick={() => {
|
||||
handlePopUpOpen("applyTemplate");
|
||||
handlePopUpClose("addPolicyOptions");
|
||||
}}
|
||||
isDisabled={isDisabled}
|
||||
variant="outline_bg"
|
||||
className="h-10 text-left"
|
||||
isFullWidth
|
||||
>
|
||||
Add From Template
|
||||
</Button>
|
||||
</div>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<PolicySelectionModal
|
||||
isOpen={popUp.addPolicy.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("addPolicy", isOpen)}
|
||||
/>
|
||||
<PolicyTemplateModal
|
||||
isOpen={popUp.applyTemplate.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("applyTemplate", isOpen)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import { cloneElement, useState } from "react";
|
||||
import { Controller, useFieldArray, useFormContext } from "react-hook-form";
|
||||
import { cloneElement, ReactNode, useState } from "react";
|
||||
import { Controller, useFieldArray, useFormContext, useWatch } from "react-hook-form";
|
||||
import {
|
||||
faChevronDown,
|
||||
faChevronRight,
|
||||
@@ -29,6 +29,39 @@ type Props<T extends ProjectPermissionSub> = {
|
||||
isDisabled?: boolean;
|
||||
};
|
||||
|
||||
type ActionProps = {
|
||||
value: string;
|
||||
subject: ProjectPermissionSub;
|
||||
rootIndex: number;
|
||||
label: ReactNode;
|
||||
isDisabled?: boolean;
|
||||
control: any;
|
||||
};
|
||||
|
||||
const ActionCheckbox = ({ value, subject, isDisabled, rootIndex, label, control }: ActionProps) => {
|
||||
// scott: using Controller caused discrepency between field value and actual value, this is a hacky fix
|
||||
const fieldValue = useWatch({ control, name: `permissions.${subject}.${rootIndex}.${value}` });
|
||||
const { setValue } = useFormContext();
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center">
|
||||
<Checkbox
|
||||
isDisabled={isDisabled}
|
||||
isChecked={Boolean(fieldValue)}
|
||||
onCheckedChange={(isChecked) =>
|
||||
setValue(`permissions.${subject}.${rootIndex}.${value}`, isChecked, {
|
||||
shouldDirty: true,
|
||||
shouldTouch: true
|
||||
})
|
||||
}
|
||||
id={`permissions.${subject}.${rootIndex}.${String(value)}`}
|
||||
>
|
||||
{label}
|
||||
</Checkbox>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const GeneralPermissionPolicies = <T extends keyof NonNullable<TFormSchema["permissions"]>>({
|
||||
subject,
|
||||
actions,
|
||||
@@ -41,11 +74,18 @@ export const GeneralPermissionPolicies = <T extends keyof NonNullable<TFormSchem
|
||||
control,
|
||||
name: `permissions.${subject}`
|
||||
});
|
||||
|
||||
// scott: this is a hacky work-around to resolve bug of fields not updating UI when removed
|
||||
const watchFields: any[] = useWatch({
|
||||
control,
|
||||
name: `permissions.${subject}`
|
||||
});
|
||||
|
||||
const [isOpen, setIsOpen] = useToggle();
|
||||
const [draggedItem, setDraggedItem] = useState<number | null>(null);
|
||||
const [dragOverItem, setDragOverItem] = useState<number | null>(null);
|
||||
|
||||
if (!fields.length) return <div />;
|
||||
if (!watchFields?.length) return <div />;
|
||||
|
||||
const handleDragStart = (_: React.DragEvent, index: number) => {
|
||||
setDraggedItem(index);
|
||||
@@ -194,25 +234,14 @@ export const GeneralPermissionPolicies = <T extends keyof NonNullable<TFormSchem
|
||||
}
|
||||
|
||||
return (
|
||||
<Controller
|
||||
<ActionCheckbox
|
||||
key={`${el.id}-${index + 1}`}
|
||||
name={`permissions.${subject}.${rootIndex}.${value}` as any}
|
||||
value={value}
|
||||
label={label}
|
||||
rootIndex={rootIndex}
|
||||
control={control}
|
||||
defaultValue={false}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<div className="flex items-center justify-center">
|
||||
<Checkbox
|
||||
isDisabled={isDisabled}
|
||||
isChecked={Boolean(field.value)}
|
||||
onCheckedChange={field.onChange}
|
||||
id={`permissions.${subject}.${rootIndex}.${String(value)}`}
|
||||
>
|
||||
{label}
|
||||
</Checkbox>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
subject={subject}
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
import { useState } from "react";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
Button,
|
||||
Modal,
|
||||
ModalClose,
|
||||
ModalContent
|
||||
} from "@app/components/v2";
|
||||
import { ProjectPermissionSub } from "@app/context";
|
||||
import { useGetProjectTypeFromRoute } from "@app/hooks";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
import {
|
||||
PROJECT_PERMISSION_OBJECT,
|
||||
RoleTemplate,
|
||||
RoleTemplates,
|
||||
TFormSchema
|
||||
} from "./ProjectRoleModifySection.utils";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (isOpen: boolean) => void;
|
||||
};
|
||||
|
||||
type ContentProps = {
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const Content = ({ onClose }: ContentProps) => {
|
||||
const rootForm = useFormContext<TFormSchema>();
|
||||
const projectType = useGetProjectTypeFromRoute();
|
||||
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<RoleTemplate>();
|
||||
const [conflictingSubjects, setConflictingSubjects] = useState<ProjectPermissionSub[]>([]);
|
||||
const [showConflictingSubjects, setShowConflictingSubjects] = useState(false);
|
||||
|
||||
const templates = RoleTemplates[projectType ?? ProjectType.SecretManager];
|
||||
|
||||
const onSubmit = (skipConflicting = false) => {
|
||||
if (!selectedTemplate) {
|
||||
createNotification({ type: "error", text: "Please select a template" });
|
||||
return;
|
||||
}
|
||||
|
||||
selectedTemplate.permissions.forEach(({ subject, actions }) => {
|
||||
if (skipConflicting && conflictingSubjects.includes(subject)) return;
|
||||
|
||||
rootForm.setValue(
|
||||
`permissions.${subject}`,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore-error akhilmhdh: this is because of ts collision with both
|
||||
[Object.fromEntries(actions.map((action) => [action, true]))],
|
||||
{
|
||||
shouldDirty: true,
|
||||
shouldTouch: true,
|
||||
shouldValidate: true
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
onClose();
|
||||
};
|
||||
|
||||
const onApply = () => {
|
||||
if (!selectedTemplate) {
|
||||
createNotification({ type: "error", text: "Please select a template" });
|
||||
return;
|
||||
}
|
||||
|
||||
const conflictingPolicies: ProjectPermissionSub[] = [];
|
||||
|
||||
selectedTemplate.permissions.forEach(({ subject }) => {
|
||||
const rootPolicyValue = rootForm.getValues("permissions")?.[subject];
|
||||
|
||||
if (rootPolicyValue?.length) {
|
||||
conflictingPolicies.push(subject);
|
||||
}
|
||||
});
|
||||
|
||||
if (conflictingPolicies.length) {
|
||||
setConflictingSubjects(conflictingPolicies);
|
||||
setShowConflictingSubjects(true);
|
||||
return;
|
||||
}
|
||||
|
||||
onSubmit();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal isOpen={showConflictingSubjects} onOpenChange={setShowConflictingSubjects}>
|
||||
<ModalContent
|
||||
title="Conflicting Policies"
|
||||
subTitle="The following resources already have policies assigned to them."
|
||||
>
|
||||
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||
{conflictingSubjects.map((subject) => (
|
||||
<div key={subject}>
|
||||
<span className="text-mineshaft-200">
|
||||
{PROJECT_PERMISSION_OBJECT[subject].title}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-8 flex space-x-4">
|
||||
<ModalClose asChild>
|
||||
<Button colorSchema="danger" onClick={() => onSubmit()}>
|
||||
Overwrite Existing
|
||||
</Button>
|
||||
</ModalClose>
|
||||
<ModalClose asChild>
|
||||
<Button colorSchema="secondary" onClick={() => onSubmit(true)}>
|
||||
Skip Conflicting
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<Accordion
|
||||
type="single"
|
||||
value={selectedTemplate?.id}
|
||||
onValueChange={(value) =>
|
||||
setSelectedTemplate(templates.find((template) => template.id === value))
|
||||
}
|
||||
collapsible
|
||||
className="w-full border-collapse"
|
||||
>
|
||||
{templates.map(({ name, description, permissions, id }) => (
|
||||
<AccordionItem
|
||||
key={id}
|
||||
value={id}
|
||||
className="m-0 border border-mineshaft-600 first:rounded-t last:rounded-b data-[state=open]:border-primary/40 data-[state=open]:bg-mineshaft-600/30"
|
||||
>
|
||||
<AccordionTrigger className="w-full justify-start p-4 py-8 text-mineshaft-100 hover:bg-mineshaft-700 hover:text-mineshaft-100 data-[state=open]:bg-primary/[3%] data-[state=open]:text-mineshaft-100">
|
||||
<div className="mr-auto flex flex-col py-2 text-left">
|
||||
<span>{name}</span>
|
||||
<span className="text-sm leading-3 text-mineshaft-400">{description}</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="border-t border-mineshaft-600">
|
||||
<div className="thin-scrollbar max-h-[20rem] overflow-y-auto">
|
||||
<span className="text-mineshaft-300">Grants the following permissions:</span>
|
||||
<div className="grid grid-cols-2 gap-4 py-2">
|
||||
{permissions
|
||||
.map((permission) => ({
|
||||
...permission,
|
||||
object: PROJECT_PERMISSION_OBJECT[permission.subject]
|
||||
}))
|
||||
.sort((a, b) => a.object.title.localeCompare(b.object.title))
|
||||
.map(({ subject, actions, object }) => {
|
||||
return (
|
||||
<div key={subject}>
|
||||
<span className="text-mineshaft-200">{object.title}</span>
|
||||
<ul className="mt-1 flex list-disc flex-col gap-1 pl-4">
|
||||
{actions.map((action) => (
|
||||
<li key={action}>
|
||||
{object.actions.find((a) => a.value === action)?.label}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
<div className="mt-8 flex space-x-4">
|
||||
<Button isDisabled={!selectedTemplate} onClick={onApply}>
|
||||
Apply Template
|
||||
</Button>
|
||||
<ModalClose asChild>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const PolicyTemplateModal = ({ isOpen, onOpenChange }: Props) => {
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||
<ModalContent
|
||||
title="Policy Templates"
|
||||
subTitle="Select a template with prepopulated policies to get started. You can always add more policies later."
|
||||
className="max-w-3xl"
|
||||
>
|
||||
<Content onClose={() => onOpenChange(false)} />
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -877,19 +877,19 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
|
||||
title: "Dynamic Secrets",
|
||||
actions: [
|
||||
{
|
||||
label: "Read root credentials",
|
||||
label: "Read Root Credentials",
|
||||
value: ProjectPermissionDynamicSecretActions.ReadRootCredential
|
||||
},
|
||||
{
|
||||
label: "Create root credentials",
|
||||
label: "Create Root Credentials",
|
||||
value: ProjectPermissionDynamicSecretActions.CreateRootCredential
|
||||
},
|
||||
{
|
||||
label: "Modify root credentials",
|
||||
label: "Modify Root Credentials",
|
||||
value: ProjectPermissionDynamicSecretActions.EditRootCredential
|
||||
},
|
||||
{
|
||||
label: "Remove root credentials",
|
||||
label: "Remove Root Credentials",
|
||||
value: ProjectPermissionDynamicSecretActions.DeleteRootCredential
|
||||
},
|
||||
{ label: "Manage Leases", value: ProjectPermissionDynamicSecretActions.Lease }
|
||||
@@ -1174,23 +1174,23 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
|
||||
title: "KMIP",
|
||||
actions: [
|
||||
{
|
||||
label: "Read clients",
|
||||
label: "Read Clients",
|
||||
value: ProjectPermissionKmipActions.ReadClients
|
||||
},
|
||||
{
|
||||
label: "Create clients",
|
||||
label: "Create Clients",
|
||||
value: ProjectPermissionKmipActions.CreateClients
|
||||
},
|
||||
{
|
||||
label: "Modify clients",
|
||||
label: "Modify Clients",
|
||||
value: ProjectPermissionKmipActions.UpdateClients
|
||||
},
|
||||
{
|
||||
label: "Delete clients",
|
||||
label: "Delete Clients",
|
||||
value: ProjectPermissionKmipActions.DeleteClients
|
||||
},
|
||||
{
|
||||
label: "Generate client certificates",
|
||||
label: "Generate Client Certificates",
|
||||
value: ProjectPermissionKmipActions.GenerateClientCertificates
|
||||
}
|
||||
]
|
||||
@@ -1280,3 +1280,340 @@ export const ProjectTypePermissionSubjects: Record<
|
||||
...SecretsManagerPermissionSubjects()
|
||||
}
|
||||
};
|
||||
|
||||
export type RoleTemplate = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
permissions: { subject: ProjectPermissionSub; actions: string[] }[];
|
||||
};
|
||||
|
||||
const projectManagerTemplate = (
|
||||
additionalPermissions: RoleTemplate["permissions"] = []
|
||||
): RoleTemplate => ({
|
||||
id: "project-manager",
|
||||
name: "Project Management Policies",
|
||||
description: "Grants access to manage project members and settings",
|
||||
permissions: [
|
||||
{
|
||||
subject: ProjectPermissionSub.AuditLogs,
|
||||
actions: Object.values(ProjectPermissionActions)
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.Groups,
|
||||
actions: Object.values(ProjectPermissionGroupActions)
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.Member,
|
||||
actions: Object.values(ProjectPermissionMemberActions)
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.Identity,
|
||||
actions: Object.values(ProjectPermissionIdentityActions)
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.Project,
|
||||
actions: [ProjectPermissionActions.Edit, ProjectPermissionActions.Delete]
|
||||
},
|
||||
{ subject: ProjectPermissionSub.Role, actions: Object.values(ProjectPermissionActions) },
|
||||
{
|
||||
subject: ProjectPermissionSub.Settings,
|
||||
actions: [ProjectPermissionActions.Read, ProjectPermissionActions.Edit]
|
||||
},
|
||||
...additionalPermissions
|
||||
]
|
||||
});
|
||||
|
||||
export const RoleTemplates: Record<ProjectType, RoleTemplate[]> = {
|
||||
[ProjectType.SSH]: [
|
||||
{
|
||||
id: "ssh-viewer",
|
||||
name: "SSH Viewing Policies",
|
||||
description: "Grants read access to SSH certificates and hosts",
|
||||
permissions: [
|
||||
{
|
||||
subject: ProjectPermissionSub.SshCertificateAuthorities,
|
||||
actions: [ProjectPermissionActions.Read]
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.SshCertificates,
|
||||
actions: [ProjectPermissionActions.Read]
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.SshCertificateTemplates,
|
||||
actions: [ProjectPermissionActions.Read]
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.SshHosts,
|
||||
actions: [ProjectPermissionSshHostActions.Read]
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.SshHostGroups,
|
||||
actions: [ProjectPermissionActions.Read]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "ssh-cert-editor",
|
||||
name: "SSH Certificate Editing Policies",
|
||||
description: "Grants read and edit access to SSH certificates",
|
||||
permissions: [
|
||||
{
|
||||
subject: ProjectPermissionSub.SshCertificateAuthorities,
|
||||
actions: Object.values(ProjectPermissionActions)
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.SshCertificates,
|
||||
actions: Object.values(ProjectPermissionActions)
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.SshCertificateTemplates,
|
||||
actions: Object.values(ProjectPermissionActions)
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "ssh-host-editor",
|
||||
name: "SSH Host Editing Policies",
|
||||
description: "Grants read and edit access to SSH hosts",
|
||||
permissions: [
|
||||
{
|
||||
subject: ProjectPermissionSub.SshHosts,
|
||||
actions: Object.values(ProjectPermissionSshHostActions)
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.SshHostGroups,
|
||||
actions: Object.values(ProjectPermissionActions)
|
||||
}
|
||||
]
|
||||
},
|
||||
projectManagerTemplate()
|
||||
],
|
||||
[ProjectType.KMS]: [
|
||||
{
|
||||
id: "kms-viewer",
|
||||
name: "KMS Viewing Policies",
|
||||
description: "Grants read access to KMS keys and KMIP clients",
|
||||
permissions: [
|
||||
{
|
||||
subject: ProjectPermissionSub.Cmek,
|
||||
actions: [ProjectPermissionCmekActions.Read]
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.Kmip,
|
||||
actions: [ProjectPermissionKmipActions.ReadClients]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "key-editor",
|
||||
name: "KMS Key Editing Policies",
|
||||
description: "Grants read and edit access to KMS keys",
|
||||
permissions: [
|
||||
{
|
||||
subject: ProjectPermissionSub.Cmek,
|
||||
actions: Object.values(ProjectPermissionCmekActions)
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "kmip-editor",
|
||||
name: "KMIP Client Editing Policies",
|
||||
description: "Grants read and edit access to KMIP clients",
|
||||
permissions: [
|
||||
{
|
||||
subject: ProjectPermissionSub.Kmip,
|
||||
actions: Object.values(ProjectPermissionKmipActions)
|
||||
}
|
||||
]
|
||||
},
|
||||
projectManagerTemplate()
|
||||
],
|
||||
[ProjectType.CertificateManager]: [
|
||||
{
|
||||
id: "cert-viewer",
|
||||
name: "Certificate Viewing Policies",
|
||||
description: "Grants read access to certificates and related resources",
|
||||
permissions: [
|
||||
{
|
||||
subject: ProjectPermissionSub.PkiCollections,
|
||||
actions: [ProjectPermissionActions.Read]
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.PkiAlerts,
|
||||
actions: [ProjectPermissionActions.Read]
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.CertificateAuthorities,
|
||||
actions: [ProjectPermissionActions.Read]
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.CertificateTemplates,
|
||||
actions: [ProjectPermissionActions.Read]
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.Certificates,
|
||||
actions: [
|
||||
ProjectPermissionCertificateActions.Read,
|
||||
ProjectPermissionCertificateActions.ReadPrivateKey
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "cert-editor",
|
||||
name: "Certificate Editing Policies",
|
||||
description: "Grants read and edit access to certificates and related resources",
|
||||
permissions: [
|
||||
{
|
||||
subject: ProjectPermissionSub.PkiCollections,
|
||||
actions: Object.values(ProjectPermissionActions)
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.PkiAlerts,
|
||||
actions: Object.values(ProjectPermissionActions)
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.CertificateAuthorities,
|
||||
actions: Object.values(ProjectPermissionActions)
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.CertificateTemplates,
|
||||
actions: Object.values(ProjectPermissionActions)
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.Certificates,
|
||||
actions: Object.values(ProjectPermissionCertificateActions)
|
||||
}
|
||||
]
|
||||
},
|
||||
projectManagerTemplate()
|
||||
],
|
||||
[ProjectType.SecretManager]: [
|
||||
{
|
||||
id: "secret-viewer",
|
||||
name: "Secret Viewing Policies",
|
||||
description: "Grants read access to secrets and related resources",
|
||||
permissions: [
|
||||
{
|
||||
subject: ProjectPermissionSub.SecretRollback,
|
||||
actions: [ProjectPermissionActions.Read]
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.SecretImports,
|
||||
actions: [ProjectPermissionActions.Read]
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.Secrets,
|
||||
actions: [
|
||||
ProjectPermissionSecretActions.DescribeSecret,
|
||||
ProjectPermissionSecretActions.ReadValue
|
||||
]
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.DynamicSecrets,
|
||||
actions: [ProjectPermissionDynamicSecretActions.ReadRootCredential]
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.Environments,
|
||||
actions: [ProjectPermissionActions.Read]
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.Tags,
|
||||
actions: [ProjectPermissionActions.Read]
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.SecretRotation,
|
||||
actions: [ProjectPermissionSecretRotationActions.Read]
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.Integrations,
|
||||
actions: [ProjectPermissionActions.Read]
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.SecretSyncs,
|
||||
actions: [ProjectPermissionSecretSyncActions.Read]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "secret-editor",
|
||||
name: "Secret Editing Policies",
|
||||
description: "Grants read and edit access to secrets and related resources",
|
||||
permissions: [
|
||||
{
|
||||
subject: ProjectPermissionSub.Environments,
|
||||
actions: Object.values(ProjectPermissionActions)
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.DynamicSecrets,
|
||||
actions: Object.values(ProjectPermissionDynamicSecretActions)
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.Secrets,
|
||||
actions: [
|
||||
ProjectPermissionSecretActions.DescribeSecret,
|
||||
ProjectPermissionSecretActions.ReadValue,
|
||||
ProjectPermissionSecretActions.Edit,
|
||||
ProjectPermissionSecretActions.Create,
|
||||
ProjectPermissionSecretActions.Delete
|
||||
]
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.SecretRollback,
|
||||
actions: [ProjectPermissionActions.Read, ProjectPermissionActions.Create]
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.Tags,
|
||||
actions: Object.values(ProjectPermissionActions)
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.SecretImports,
|
||||
actions: Object.values(ProjectPermissionActions)
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.SecretRotation,
|
||||
actions: Object.values(ProjectPermissionSecretRotationActions)
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.SecretFolders,
|
||||
actions: [
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionActions.Delete
|
||||
]
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.Integrations,
|
||||
actions: Object.values(ProjectPermissionActions)
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.SecretSyncs,
|
||||
actions: Object.values(ProjectPermissionSecretSyncActions)
|
||||
}
|
||||
]
|
||||
},
|
||||
projectManagerTemplate([
|
||||
{
|
||||
subject: ProjectPermissionSub.IpAllowList,
|
||||
actions: Object.values(ProjectPermissionActions)
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.Kms,
|
||||
actions: [ProjectPermissionActions.Edit]
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.SecretApproval,
|
||||
actions: Object.values(ProjectPermissionActions)
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.ServiceTokens,
|
||||
actions: Object.values(ProjectPermissionActions)
|
||||
},
|
||||
{
|
||||
subject: ProjectPermissionSub.Webhooks,
|
||||
actions: Object.values(ProjectPermissionActions)
|
||||
}
|
||||
])
|
||||
]
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useMemo } from "react";
|
||||
import { FormProvider, useForm } from "react-hook-form";
|
||||
import { MongoAbility, MongoQuery, RawRuleOf } from "@casl/ability";
|
||||
import { faPlus, faSave } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faSave } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
@@ -12,17 +12,16 @@ import { Button } from "@app/components/v2";
|
||||
import { ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext";
|
||||
import { evaluatePermissionsAbility } from "@app/helpers/permissions";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useGetProjectRoleBySlug, useUpdateProjectRole } from "@app/hooks/api";
|
||||
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
import { PolicySelectionModal } from "@app/pages/project/RoleDetailsBySlugPage/components/PolicySelectionModal";
|
||||
import { AddPoliciesButton } from "@app/pages/project/RoleDetailsBySlugPage/components/AddPoliciesButton";
|
||||
import { GeneralPermissionPolicies } from "@app/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies";
|
||||
import { PermissionEmptyState } from "@app/pages/project/RoleDetailsBySlugPage/components/PermissionEmptyState";
|
||||
|
||||
import { DynamicSecretPermissionConditions } from "./DynamicSecretPermissionConditions";
|
||||
import { GeneralPermissionConditions } from "./GeneralPermissionConditions";
|
||||
import { GeneralPermissionPolicies } from "./GeneralPermissionPolicies";
|
||||
import { IdentityManagementPermissionConditions } from "./IdentityManagementPermissionConditions";
|
||||
import { PermissionEmptyState } from "./PermissionEmptyState";
|
||||
import {
|
||||
formRolePermission2API,
|
||||
isConditionalSubjects,
|
||||
@@ -86,8 +85,6 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
|
||||
|
||||
const { mutateAsync: updateRole } = useUpdateProjectRole();
|
||||
|
||||
const { popUp, handlePopUpToggle } = usePopUp(["addPolicy"] as const);
|
||||
|
||||
const onSubmit = async (el: TFormSchema) => {
|
||||
try {
|
||||
if (!projectId || !role?.id) return;
|
||||
@@ -151,7 +148,7 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
|
||||
variant="outline_bg"
|
||||
type="submit"
|
||||
className={twMerge(
|
||||
"h-10 rounded-r-none border border-primary",
|
||||
"mr-4 h-10 border border-primary",
|
||||
isDirty && "bg-primary text-black"
|
||||
)}
|
||||
isDisabled={isSubmitting || !isDirty}
|
||||
@@ -160,15 +157,7 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<Button
|
||||
isDisabled={isDisabled}
|
||||
className="h-10 rounded-l-none"
|
||||
variant="outline_bg"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handlePopUpToggle("addPolicy")}
|
||||
>
|
||||
Add Policy
|
||||
</Button>
|
||||
<AddPoliciesButton isDisabled={isDisabled} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
@@ -190,10 +179,6 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
|
||||
</GeneralPermissionPolicies>
|
||||
))}
|
||||
</div>
|
||||
<PolicySelectionModal
|
||||
isOpen={popUp.addPolicy.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("addPolicy", isOpen)}
|
||||
/>
|
||||
</FormProvider>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user