feature: project role templates

This commit is contained in:
Scott Wilson
2025-05-08 16:02:41 -07:00
parent 04a8931cf6
commit 800ea5ce78
10 changed files with 698 additions and 123 deletions

View File

@@ -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"

View File

@@ -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",

View File

@@ -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>
)}

View File

@@ -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>
);

View File

@@ -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>
);

View File

@@ -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)}
/>
</>
);
};

View File

@@ -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}
/>
);
})}

View File

@@ -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>
);
};

View File

@@ -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)
}
])
]
};

View File

@@ -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>