Merge pull request #3563 from Infisical/policy-selection-modal

improvement(project-roles): Add Policy Selection Modal
This commit is contained in:
Scott Wilson
2025-05-07 16:49:05 -07:00
committed by GitHub
14 changed files with 317 additions and 385 deletions
@@ -169,7 +169,7 @@ const getParameterStoreTagsRecord = async (
throw new SecretSyncError({ throw new SecretSyncError({
message: message:
"IAM role has inadequate permissions to manage resource tags. Ensure the following polices are present: ssm:ListTagsForResource, ssm:AddTagsToResource, and ssm:RemoveTagsFromResource", "IAM role has inadequate permissions to manage resource tags. Ensure the following policies are present: ssm:ListTagsForResource, ssm:AddTagsToResource, and ssm:RemoveTagsFromResource",
shouldRetry: false shouldRetry: false
}); });
} }
@@ -27,7 +27,7 @@ User identities can have metadata attributes assigned directly. These attributes
</Tabs> </Tabs>
#### Applying ABAC Policies with User Metadata #### Applying ABAC Policies with User Metadata
Attribute-based access controls are currently only available for polices defined on Secrets Manager projects. Attribute-based access controls are currently only available for policies defined on Secrets Manager projects.
You can set ABAC permissions to dynamically set access to environments, folders, secrets, and secret tags. You can set ABAC permissions to dynamically set access to environments, folders, secrets, and secret tags.
<img src="/images/platform/access-controls/example-abac-1.png" /> <img src="/images/platform/access-controls/example-abac-1.png" />
@@ -165,7 +165,7 @@ spec:
</Accordion> </Accordion>
<Accordion title="managedSecretReference.creationPolicy"> <Accordion title="managedSecretReference.creationPolicy">
Creation polices allow you to control whether or not owner references should be added to the managed Kubernetes secret that is generated by the Infisical operator. Creation policies allow you to control whether or not owner references should be added to the managed Kubernetes secret that is generated by the Infisical operator.
This is useful for tools such as ArgoCD, where every resource requires an owner reference; otherwise, it will be pruned automatically. This is useful for tools such as ArgoCD, where every resource requires an owner reference; otherwise, it will be pruned automatically.
#### Available options #### Available options
@@ -832,7 +832,7 @@ The namespace of the managed Kubernetes secret to be created.
Override the default Opaque type for managed secrets with this field. Useful for creating kubernetes.io/dockerconfigjson secrets. Override the default Opaque type for managed secrets with this field. Useful for creating kubernetes.io/dockerconfigjson secrets.
</Accordion> </Accordion>
<Accordion title="managedKubeSecretReferences[].creationPolicy"> <Accordion title="managedKubeSecretReferences[].creationPolicy">
Creation polices allow you to control whether or not owner references should be added to the managed Kubernetes secret that is generated by the Infisical operator. Creation policies allow you to control whether or not owner references should be added to the managed Kubernetes secret that is generated by the Infisical operator.
This is useful for tools such as ArgoCD, where every resource requires an owner reference; otherwise, it will be pruned automatically. This is useful for tools such as ArgoCD, where every resource requires an owner reference; otherwise, it will be pruned automatically.
#### Available options #### Available options
@@ -940,7 +940,7 @@ The Infisical operator will automatically create the Kubernetes config map in th
The namespace of the managed Kubernetes config map that your Infisical data will be stored in. The namespace of the managed Kubernetes config map that your Infisical data will be stored in.
</Accordion> </Accordion>
<Accordion title="managedKubeConfigMapReferences[].creationPolicy"> <Accordion title="managedKubeConfigMapReferences[].creationPolicy">
Creation polices allow you to control whether or not owner references should be added to the managed Kubernetes config map that is generated by the Infisical operator. Creation policies allow you to control whether or not owner references should be added to the managed Kubernetes config map that is generated by the Infisical operator.
This is useful for tools such as ArgoCD, where every resource requires an owner reference; otherwise, it will be pruned automatically. This is useful for tools such as ArgoCD, where every resource requires an owner reference; otherwise, it will be pruned automatically.
#### Available options #### Available options
@@ -6,15 +6,15 @@ import { twMerge } from "tailwind-merge";
import { z } from "zod"; import { z } from "zod";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input, Modal, ModalContent, ModalTrigger } from "@app/components/v2"; import { Button, FormControl, Input } from "@app/components/v2";
import { ProjectPermissionSub } from "@app/context"; import { ProjectPermissionSub } from "@app/context";
import { isCustomProjectRole } from "@app/helpers/roles"; import { isCustomProjectRole } from "@app/helpers/roles";
import { usePopUp } from "@app/hooks"; import { usePopUp } from "@app/hooks";
import { TProjectTemplate, useUpdateProjectTemplate } from "@app/hooks/api/projectTemplates"; import { TProjectTemplate, useUpdateProjectTemplate } from "@app/hooks/api/projectTemplates";
import { slugSchema } from "@app/lib/schemas"; import { slugSchema } from "@app/lib/schemas";
import { GeneralPermissionPolicies } from "@app/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies"; import { GeneralPermissionPolicies } from "@app/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies";
import { NewPermissionRule } from "@app/pages/project/RoleDetailsBySlugPage/components/NewPermissionRule";
import { PermissionEmptyState } from "@app/pages/project/RoleDetailsBySlugPage/components/PermissionEmptyState"; import { PermissionEmptyState } from "@app/pages/project/RoleDetailsBySlugPage/components/PermissionEmptyState";
import { PolicySelectionModal } from "@app/pages/project/RoleDetailsBySlugPage/components/PolicySelectionModal";
import { import {
formRolePermission2API, formRolePermission2API,
PROJECT_PERMISSION_OBJECT, PROJECT_PERMISSION_OBJECT,
@@ -44,7 +44,7 @@ export const ProjectTemplateEditRoleForm = ({
role, role,
isDisabled isDisabled
}: Props) => { }: Props) => {
const { popUp, handlePopUpToggle } = usePopUp(["createPolicy"] as const); const { popUp, handlePopUpToggle } = usePopUp(["addPolicy"] as const);
const formMethods = useForm<TFormSchema>({ const formMethods = useForm<TFormSchema>({
values: role ? { ...role, permissions: rolePermission2Form(role.permissions) } : undefined, values: role ? { ...role, permissions: rolePermission2Form(role.permissions) } : undefined,
@@ -119,34 +119,29 @@ export const ProjectTemplateEditRoleForm = ({
<Button <Button
variant="outline_bg" variant="outline_bg"
type="submit" type="submit"
className={twMerge("h-10 rounded-r-none", isDirty && "bg-primary text-black")} className={twMerge(
"h-10 rounded-r-none border border-primary",
isDirty && "bg-primary text-black"
)}
isDisabled={isSubmitting || !isDirty || isDisabled} isDisabled={isSubmitting || !isDirty || isDisabled}
isLoading={isSubmitting} isLoading={isSubmitting}
leftIcon={<FontAwesomeIcon icon={faSave} />} leftIcon={<FontAwesomeIcon icon={faSave} />}
> >
Save Save
</Button> </Button>
<Modal <Button
isOpen={popUp.createPolicy.isOpen} className="h-10 rounded-l-none"
onOpenChange={(isOpen) => handlePopUpToggle("createPolicy", isOpen)} variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
isDisabled={isDisabled}
onClick={() => handlePopUpToggle("addPolicy")}
> >
<ModalTrigger asChild> Add Policies
<Button </Button>
className="h-10 rounded-l-none" <PolicySelectionModal
variant="outline_bg" isOpen={popUp.addPolicy.isOpen}
leftIcon={<FontAwesomeIcon icon={faPlus} />} onOpenChange={(isOpen) => handlePopUpToggle("addPolicy", isOpen)}
isDisabled={isDisabled} />
>
New Policy
</Button>
</ModalTrigger>
<ModalContent
title="New Policy"
subTitle="Policies grant additional permissions."
>
<NewPermissionRule onClose={() => handlePopUpToggle("createPolicy")} />
</ModalContent>
</Modal>
</div> </div>
</div> </div>
)} )}
@@ -1,3 +1,4 @@
import { useMemo } from "react";
import { useRouterState } from "@tanstack/react-router"; import { useRouterState } from "@tanstack/react-router";
import { ProjectType } from "@app/hooks/api/workspace/types"; import { ProjectType } from "@app/hooks/api/workspace/types";
@@ -5,9 +6,17 @@ import { ProjectType } from "@app/hooks/api/workspace/types";
export const useGetProjectTypeFromRoute = () => { export const useGetProjectTypeFromRoute = () => {
const { location } = useRouterState(); const { location } = useRouterState();
const segment = location.pathname.split("/")[2]; return useMemo(() => {
const segments = location.pathname.split("/");
if (!Object.values(ProjectType).includes(segment as ProjectType)) return undefined; let type: ProjectType | undefined;
return segment as ProjectType; // location of project type can vary in router path, so we need to check all possible values
segments.forEach((segment) => {
if (Object.values(ProjectType).includes(segment as ProjectType))
type = segment as ProjectType;
});
return type;
}, [location]);
}; };
@@ -18,10 +18,6 @@ import { TtlFormLabel } from "@app/components/features";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { import {
Button, Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
FormControl, FormControl,
FormLabel, FormLabel,
Input, Input,
@@ -37,6 +33,7 @@ import {
useProjectPermission, useProjectPermission,
useWorkspace useWorkspace
} from "@app/context"; } from "@app/context";
import { usePopUp } from "@app/hooks";
import { import {
useCreateIdentityProjectAdditionalPrivilege, useCreateIdentityProjectAdditionalPrivilege,
useGetIdentityProjectPrivilegeDetails, useGetIdentityProjectPrivilegeDetails,
@@ -45,12 +42,11 @@ import {
import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/hooks/api/identityProjectAdditionalPrivilege/types"; import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/hooks/api/identityProjectAdditionalPrivilege/types";
import { GeneralPermissionPolicies } from "@app/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies"; import { GeneralPermissionPolicies } from "@app/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies";
import { PermissionEmptyState } from "@app/pages/project/RoleDetailsBySlugPage/components/PermissionEmptyState"; import { PermissionEmptyState } from "@app/pages/project/RoleDetailsBySlugPage/components/PermissionEmptyState";
import { PolicySelectionModal } from "@app/pages/project/RoleDetailsBySlugPage/components/PolicySelectionModal";
import { import {
formRolePermission2API, formRolePermission2API,
isConditionalSubjects,
PROJECT_PERMISSION_OBJECT, PROJECT_PERMISSION_OBJECT,
projectRoleFormSchema, projectRoleFormSchema,
ProjectTypePermissionSubjects,
rolePermission2Form rolePermission2Form
} from "@app/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils"; } from "@app/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils";
import { renderConditionalComponents } from "@app/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection"; import { renderConditionalComponents } from "@app/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection";
@@ -101,6 +97,7 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({
ProjectPermissionIdentityActions.Edit, ProjectPermissionIdentityActions.Edit,
subject(ProjectPermissionSub.Identity, { identityId }) subject(ProjectPermissionSub.Identity, { identityId })
); );
const { popUp, handlePopUpToggle } = usePopUp(["addPolicy"] as const);
const form = useForm<TFormSchema>({ const form = useForm<TFormSchema>({
values: privilegeDetails values: privilegeDetails
@@ -195,30 +192,6 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({
} }
} }
const onNewPolicy = (selectedSubject: ProjectPermissionSub) => {
const rootPolicyValue = form.getValues(`permissions.${selectedSubject}`);
if (rootPolicyValue && isConditionalSubjects(selectedSubject)) {
form.setValue(
`permissions.${selectedSubject}`,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error akhilmhdh: this is because of ts collision with both
[...rootPolicyValue, {}],
{ shouldDirty: true, shouldTouch: true }
);
} else {
form.setValue(
`permissions.${selectedSubject}`,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error akhilmhdh: this is because of ts collision with both
[{}],
{
shouldDirty: true,
shouldTouch: true
}
);
}
};
return ( return (
<form <form
onSubmit={handleSubmit(onSubmit)} onSubmit={handleSubmit(onSubmit)}
@@ -250,52 +223,25 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({
<Button <Button
variant="outline_bg" variant="outline_bg"
type="submit" type="submit"
className={twMerge("h-10 rounded-r-none", isDirty && "bg-primary text-black")} className={twMerge(
"h-10 rounded-r-none border border-primary",
isDirty && "bg-primary text-black"
)}
isDisabled={isSubmitting || !isDirty || isDisabled} isDisabled={isSubmitting || !isDirty || isDisabled}
isLoading={isSubmitting} isLoading={isSubmitting}
leftIcon={<FontAwesomeIcon icon={faSave} />} leftIcon={<FontAwesomeIcon icon={faSave} />}
> >
Save Save
</Button> </Button>
<DropdownMenu> <Button
<DropdownMenuTrigger> isDisabled={isDisabled}
<Button className="h-10 rounded-l-none"
isDisabled={isDisabled} variant="outline_bg"
className="h-10 rounded-l-none" leftIcon={<FontAwesomeIcon icon={faPlus} />}
variant="outline_bg" onClick={() => handlePopUpToggle("addPolicy")}
leftIcon={<FontAwesomeIcon icon={faPlus} />} >
> Add Policies
New policy </Button>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="thin-scrollbar max-h-96" align="end">
{Object.keys(PROJECT_PERMISSION_OBJECT)
.filter(
(sub) =>
ProjectTypePermissionSubjects[currentWorkspace.type][
sub as ProjectPermissionSub
]
)
.sort((a, b) =>
PROJECT_PERMISSION_OBJECT[a as keyof typeof PROJECT_PERMISSION_OBJECT].title
.toLowerCase()
.localeCompare(
PROJECT_PERMISSION_OBJECT[
b as keyof typeof PROJECT_PERMISSION_OBJECT
].title.toLowerCase()
)
)
.map((permissionSubject) => (
<DropdownMenuItem
key={`permission-create-${permissionSubject}`}
className="py-3"
onClick={() => onNewPolicy(permissionSubject as ProjectPermissionSub)}
>
{PROJECT_PERMISSION_OBJECT[permissionSubject as ProjectPermissionSub].title}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div> </div>
</div> </div>
</div> </div>
@@ -436,6 +382,10 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({
) )
)} )}
</div> </div>
<PolicySelectionModal
isOpen={popUp.addPolicy.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("addPolicy", isOpen)}
/>
</FormProvider> </FormProvider>
</form> </form>
); );
@@ -17,10 +17,6 @@ import { TtlFormLabel } from "@app/components/features";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { import {
Button, Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
FormControl, FormControl,
FormLabel, FormLabel,
Input, Input,
@@ -36,6 +32,7 @@ import {
useProjectPermission, useProjectPermission,
useWorkspace useWorkspace
} from "@app/context"; } from "@app/context";
import { usePopUp } from "@app/hooks";
import { import {
useCreateProjectUserAdditionalPrivilege, useCreateProjectUserAdditionalPrivilege,
useGetProjectUserPrivilegeDetails, useGetProjectUserPrivilegeDetails,
@@ -44,12 +41,11 @@ import {
import { ProjectUserAdditionalPrivilegeTemporaryMode } from "@app/hooks/api/projectUserAdditionalPrivilege/types"; import { ProjectUserAdditionalPrivilegeTemporaryMode } from "@app/hooks/api/projectUserAdditionalPrivilege/types";
import { GeneralPermissionPolicies } from "@app/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies"; import { GeneralPermissionPolicies } from "@app/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies";
import { PermissionEmptyState } from "@app/pages/project/RoleDetailsBySlugPage/components/PermissionEmptyState"; import { PermissionEmptyState } from "@app/pages/project/RoleDetailsBySlugPage/components/PermissionEmptyState";
import { PolicySelectionModal } from "@app/pages/project/RoleDetailsBySlugPage/components/PolicySelectionModal";
import { import {
formRolePermission2API, formRolePermission2API,
isConditionalSubjects,
PROJECT_PERMISSION_OBJECT, PROJECT_PERMISSION_OBJECT,
projectRoleFormSchema, projectRoleFormSchema,
ProjectTypePermissionSubjects,
rolePermission2Form rolePermission2Form
} from "@app/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils"; } from "@app/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils";
import { renderConditionalComponents } from "@app/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection"; import { renderConditionalComponents } from "@app/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection";
@@ -87,6 +83,8 @@ export const MembershipProjectAdditionalPrivilegeModifySection = ({
projectMembershipId, projectMembershipId,
isDisabled isDisabled
}: Props) => { }: Props) => {
const { popUp, handlePopUpToggle } = usePopUp(["addPolicy"] as const);
const isCreate = !privilegeId; const isCreate = !privilegeId;
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const projectId = currentWorkspace?.id || ""; const projectId = currentWorkspace?.id || "";
@@ -167,30 +165,6 @@ export const MembershipProjectAdditionalPrivilegeModifySection = ({
} }
}; };
const onNewPolicy = (selectedSubject: ProjectPermissionSub) => {
const rootPolicyValue = form.getValues(`permissions.${selectedSubject}`);
if (rootPolicyValue && isConditionalSubjects(selectedSubject)) {
form.setValue(
`permissions.${selectedSubject}`,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error akhilmhdh: this is because of ts collision with both
[...rootPolicyValue, {}],
{ shouldDirty: true, shouldTouch: true }
);
} else {
form.setValue(
`permissions.${selectedSubject}`,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error akhilmhdh: this is because of ts collision with both
[{}],
{
shouldDirty: true,
shouldTouch: true
}
);
}
};
const privilegeTemporaryAccess = form.watch("temporaryAccess"); const privilegeTemporaryAccess = form.watch("temporaryAccess");
const isTemporary = privilegeTemporaryAccess?.isTemporary; const isTemporary = privilegeTemporaryAccess?.isTemporary;
const isExpired = const isExpired =
@@ -246,52 +220,25 @@ export const MembershipProjectAdditionalPrivilegeModifySection = ({
<Button <Button
variant="outline_bg" variant="outline_bg"
type="submit" type="submit"
className={twMerge("h-10 rounded-r-none", isDirty && "bg-primary text-black")} className={twMerge(
"h-10 rounded-r-none border border-primary",
isDirty && "bg-primary text-black"
)}
isDisabled={isSubmitting || !isDirty || isDisabled} isDisabled={isSubmitting || !isDirty || isDisabled}
isLoading={isSubmitting} isLoading={isSubmitting}
leftIcon={<FontAwesomeIcon icon={faSave} />} leftIcon={<FontAwesomeIcon icon={faSave} />}
> >
Save Save
</Button> </Button>
<DropdownMenu> <Button
<DropdownMenuTrigger> isDisabled={isDisabled}
<Button className="h-10 rounded-l-none"
isDisabled={isDisabled} variant="outline_bg"
className="h-10 rounded-l-none" leftIcon={<FontAwesomeIcon icon={faPlus} />}
variant="outline_bg" onClick={() => handlePopUpToggle("addPolicy")}
leftIcon={<FontAwesomeIcon icon={faPlus} />} >
> Add Policies
New policy </Button>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="thin-scrollbar max-h-96" align="end">
{Object.keys(PROJECT_PERMISSION_OBJECT)
.filter(
(subject) =>
ProjectTypePermissionSubjects[currentWorkspace.type][
subject as ProjectPermissionSub
]
)
.sort((a, b) =>
PROJECT_PERMISSION_OBJECT[a as keyof typeof PROJECT_PERMISSION_OBJECT].title
.toLowerCase()
.localeCompare(
PROJECT_PERMISSION_OBJECT[
b as keyof typeof PROJECT_PERMISSION_OBJECT
].title.toLowerCase()
)
)
.map((subject) => (
<DropdownMenuItem
key={`permission-create-${subject}`}
className="py-3"
onClick={() => onNewPolicy(subject as ProjectPermissionSub)}
>
{PROJECT_PERMISSION_OBJECT[subject as ProjectPermissionSub].title}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div> </div>
</div> </div>
</div> </div>
@@ -430,6 +377,10 @@ export const MembershipProjectAdditionalPrivilegeModifySection = ({
</GeneralPermissionPolicies> </GeneralPermissionPolicies>
))} ))}
</div> </div>
<PolicySelectionModal
isOpen={popUp.addPolicy.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("addPolicy", isOpen)}
/>
</FormProvider> </FormProvider>
</form> </form>
); );
@@ -1,136 +0,0 @@
import { Controller, useForm, useFormContext } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import {
Button,
Checkbox,
FormControl,
FormLabel,
ModalClose,
Select,
SelectItem
} from "@app/components/v2";
import { ProjectPermissionSub } from "@app/context";
import { useGetProjectTypeFromRoute } from "@app/hooks";
import { ProjectType } from "@app/hooks/api/workspace/types";
import {
isConditionalSubjects,
PROJECT_PERMISSION_OBJECT,
projectRoleFormSchema,
ProjectTypePermissionSubjects,
TFormSchema
} from "./ProjectRoleModifySection.utils";
type Props = {
onClose: () => void;
};
export const NewPermissionRule = ({ onClose }: Props) => {
const rootForm = useFormContext<TFormSchema>();
const form = useForm<{
type: ProjectPermissionSub;
permissions: NonNullable<TFormSchema["permissions"]>;
}>({
resolver: zodResolver(
projectRoleFormSchema
.pick({ permissions: true })
.extend({ type: z.nativeEnum(ProjectPermissionSub) })
),
defaultValues: {
type: ProjectPermissionSub.Project
}
});
const selectedSubject = form.watch("type");
const projectType = useGetProjectTypeFromRoute();
return (
<div>
<Controller
control={form.control}
name="type"
defaultValue={ProjectPermissionSub.Project}
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl label="Subject" errorText={error?.message} isError={Boolean(error)}>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{Object.keys(PROJECT_PERMISSION_OBJECT)
.filter(
(subject) =>
ProjectTypePermissionSubjects[projectType ?? ProjectType.SecretManager][
subject as ProjectPermissionSub
]
)
.map((subject) => (
<SelectItem value={subject} key={`permission-create-${subject}`}>
{PROJECT_PERMISSION_OBJECT[subject as ProjectPermissionSub].title}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<FormLabel label="Actions" className="my-2" />
<div className="flex flex-grow flex-wrap justify-start gap-8">
{PROJECT_PERMISSION_OBJECT?.[selectedSubject]?.actions?.map(({ label, value }) => (
<Controller
key={`create-permission-${selectedSubject}-${label}`}
name={`permissions.${selectedSubject}.0.${value as any}` as any}
control={form.control}
defaultValue={false}
render={({ field }) => (
<div className="flex items-center justify-center">
<Checkbox
isChecked={field.value}
onCheckedChange={field.onChange}
id={`new-permissions.${selectedSubject}.0.${String(value)}`}
>
{label}
</Checkbox>
</div>
)}
/>
))}
</div>
<div className="mt-8 flex space-x-4">
<Button
onClick={form.handleSubmit((el) => {
const rootPolicyValue = rootForm.getValues("permissions")?.[el.type];
if (rootPolicyValue && isConditionalSubjects(selectedSubject)) {
rootForm.setValue(
`permissions.${el.type}`,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error akhilmhdh: this is because of ts collision with both
[...rootPolicyValue, ...(el?.permissions[el.type] || [])],
{ shouldDirty: true, shouldTouch: true }
);
} else {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error akhilmhdh: this is because of ts collision with both
rootForm.setValue(`permissions.${el.type}`, el?.permissions?.[el.type], {
shouldDirty: true,
shouldTouch: true
});
}
onClose();
})}
>
Create
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</div>
);
};
@@ -0,0 +1,214 @@
import { useState } from "react";
import { Controller, useForm, useFormContext } from "react-hook-form";
import { faCheck, faSearch, faXmark, faXmarkCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
Button,
EmptyState,
IconButton,
Input,
Modal,
ModalClose,
ModalContent,
Table,
TableContainer,
TBody,
Td,
Tooltip,
Tr
} from "@app/components/v2";
import { ProjectPermissionSub } from "@app/context";
import { useGetProjectTypeFromRoute } from "@app/hooks";
import { ProjectType } from "@app/hooks/api/workspace/types";
import {
isConditionalSubjects,
PROJECT_PERMISSION_OBJECT,
ProjectTypePermissionSubjects,
TFormSchema
} from "./ProjectRoleModifySection.utils";
type Props = {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
};
type ContentProps = {
onClose: () => void;
};
type TForm = { permissions: Record<ProjectPermissionSub, boolean> };
const Content = ({ onClose }: ContentProps) => {
const rootForm = useFormContext<TFormSchema>();
const [search, setSearch] = useState("");
const {
control,
handleSubmit,
formState: { isDirty },
setValue,
reset
} = useForm<TForm>({
defaultValues: {
permissions: Object.fromEntries(
Object.values(ProjectPermissionSub).map((subject) => [subject, false])
)
}
});
const projectType = useGetProjectTypeFromRoute();
const filteredPolicies = Object.entries(PROJECT_PERMISSION_OBJECT)
.filter(
([subject, { title }]) =>
ProjectTypePermissionSubjects[projectType ?? ProjectType.SecretManager][
subject as ProjectPermissionSub
] && (search ? title.toLowerCase().includes(search.toLowerCase()) : true)
)
.sort((a, b) => a[1].title.localeCompare(b[1].title))
.map(([subject]) => subject);
const onSubmit = () =>
handleSubmit((form) => {
Object.entries(form.permissions).forEach(([subject, add]) => {
if (!add) return;
const type = subject as ProjectPermissionSub;
const rootPolicyValue = rootForm.getValues("permissions")?.[type];
if (rootPolicyValue && isConditionalSubjects(subject as ProjectPermissionSub)) {
rootForm.setValue(
`permissions.${type}`,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error akhilmhdh: this is because of ts collision with both
[...rootPolicyValue, {}],
{ shouldDirty: true, shouldTouch: true }
);
} else if (!rootPolicyValue?.length) {
rootForm.setValue(
`permissions.${type}`,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error akhilmhdh: this is because of ts collision with both
[{}],
{
shouldDirty: true,
shouldTouch: true
}
);
}
});
onClose();
})();
return (
<>
<Input
placeholder="Search policies..."
value={search}
onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faSearch} />}
rightIcon={
search ? (
<IconButton ariaLabel="Clear search" variant="plain" onClick={() => setSearch("")}>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
) : null
}
/>
<TableContainer className="thin-scrollbar mt-4 max-h-[28rem]">
<div className="sticky top-0 z-30 flex justify-between border-b border-b-mineshaft-600 bg-mineshaft-800 py-3 pl-5 pr-4 font-inter text-sm font-medium text-bunker-300">
<span>Resource</span>
<div className="flex gap-2">
<Button
variant="plain"
className="p-0 text-mineshaft-400"
size="xs"
colorSchema="secondary"
onClick={() => {
setValue(
"permissions",
Object.fromEntries(
filteredPolicies.map((subject) => [subject, true])
) as TForm["permissions"],
{ shouldDirty: true }
);
}}
>
Select All
</Button>
<Tooltip content="Clear selection">
<IconButton
ariaLabel="Clear selection"
onClick={() => reset()}
variant="plain"
size="xs"
className={`text-mineshaft-400 ${!isDirty ? "pointer-events-none opacity-50" : ""} hover:text-red`}
isDisabled={!isDirty}
>
<FontAwesomeIcon icon={faXmarkCircle} />
</IconButton>
</Tooltip>
</div>
</div>
<Table>
<TBody>
{filteredPolicies.map((subject) => (
<Controller
control={control}
key={`permission-add-${subject}`}
render={({ field: { value, onChange } }) => (
<Tr
className={`${value ? "bg-mineshaft-600/30" : ""} cursor-pointer hover:bg-mineshaft-700`}
onClick={() => onChange(!value)}
>
<Td className={`${value ? "text-mineshaft-100" : "text-mineshaft-300"} w-full`}>
{PROJECT_PERMISSION_OBJECT[subject as ProjectPermissionSub].title}
</Td>
<Td>
{value ? <FontAwesomeIcon className="text-green" icon={faCheck} /> : null}
</Td>
</Tr>
)}
name={`permissions.${subject as ProjectPermissionSub}`}
/>
))}
</TBody>
</Table>
{!filteredPolicies.length && (
<EmptyState
iconSize="2x"
icon={faSearch}
className="!pb-4 !pt-8"
title="No policies match search"
/>
)}
</TableContainer>
<div className="mt-8 flex space-x-4">
<Button isDisabled={!isDirty} onClick={onSubmit}>
Add Policies
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</>
);
};
export const PolicySelectionModal = ({ isOpen, onOpenChange }: Props) => {
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent
title="Add Policies"
subTitle="Select one or more policies to add to this role."
className="max-w-3xl"
>
<Content onClose={() => onOpenChange(false)} />
</ModalContent>
</Modal>
);
};
@@ -913,7 +913,7 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
actions: [{ label: "Modify", value: "edit" }] actions: [{ label: "Modify", value: "edit" }]
}, },
[ProjectPermissionSub.Integrations]: { [ProjectPermissionSub.Integrations]: {
title: "Integrations", title: "Native Integrations",
actions: [ actions: [
{ label: "Read", value: "read" }, { label: "Read", value: "read" },
{ label: "Create", value: "create" }, { label: "Create", value: "create" },
@@ -8,19 +8,15 @@ import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { AccessTree } from "@app/components/permissions"; import { AccessTree } from "@app/components/permissions";
import { import { Button } from "@app/components/v2";
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from "@app/components/v2";
import { ProjectPermissionSub, useWorkspace } from "@app/context"; import { ProjectPermissionSub, useWorkspace } from "@app/context";
import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext"; import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext";
import { evaluatePermissionsAbility } from "@app/helpers/permissions"; import { evaluatePermissionsAbility } from "@app/helpers/permissions";
import { usePopUp } from "@app/hooks";
import { useGetProjectRoleBySlug, useUpdateProjectRole } from "@app/hooks/api"; import { useGetProjectRoleBySlug, useUpdateProjectRole } from "@app/hooks/api";
import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
import { ProjectType } from "@app/hooks/api/workspace/types"; import { ProjectType } from "@app/hooks/api/workspace/types";
import { PolicySelectionModal } from "@app/pages/project/RoleDetailsBySlugPage/components/PolicySelectionModal";
import { DynamicSecretPermissionConditions } from "./DynamicSecretPermissionConditions"; import { DynamicSecretPermissionConditions } from "./DynamicSecretPermissionConditions";
import { GeneralPermissionConditions } from "./GeneralPermissionConditions"; import { GeneralPermissionConditions } from "./GeneralPermissionConditions";
@@ -90,6 +86,8 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
const { mutateAsync: updateRole } = useUpdateProjectRole(); const { mutateAsync: updateRole } = useUpdateProjectRole();
const { popUp, handlePopUpToggle } = usePopUp(["addPolicy"] as const);
const onSubmit = async (el: TFormSchema) => { const onSubmit = async (el: TFormSchema) => {
try { try {
if (!projectId || !role?.id) return; if (!projectId || !role?.id) return;
@@ -110,30 +108,6 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
(role?.slug ?? "") as ProjectMembershipRole (role?.slug ?? "") as ProjectMembershipRole
); );
const onNewPolicy = (selectedSubject: ProjectPermissionSub) => {
const rootPolicyValue = form.getValues(`permissions.${selectedSubject}`);
if (rootPolicyValue && isConditionalSubjects(selectedSubject)) {
form.setValue(
`permissions.${selectedSubject}`,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error akhilmhdh: this is because of ts collision with both
[...rootPolicyValue, {}],
{ shouldDirty: true, shouldTouch: true }
);
} else {
form.setValue(
`permissions.${selectedSubject}`,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error akhilmhdh: this is because of ts collision with both
[{}],
{
shouldDirty: true,
shouldTouch: true
}
);
}
};
const permissions = form.watch("permissions"); const permissions = form.watch("permissions");
const formattedPermissions = useMemo( const formattedPermissions = useMemo(
@@ -176,54 +150,25 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
<Button <Button
variant="outline_bg" variant="outline_bg"
type="submit" type="submit"
className={twMerge("h-10 rounded-r-none", isDirty && "bg-primary text-black")} className={twMerge(
"h-10 rounded-r-none border border-primary",
isDirty && "bg-primary text-black"
)}
isDisabled={isSubmitting || !isDirty} isDisabled={isSubmitting || !isDirty}
isLoading={isSubmitting} isLoading={isSubmitting}
leftIcon={<FontAwesomeIcon icon={faSave} />} leftIcon={<FontAwesomeIcon icon={faSave} />}
> >
Save Save
</Button> </Button>
<DropdownMenu> <Button
<DropdownMenuTrigger> isDisabled={isDisabled}
<Button className="h-10 rounded-l-none"
isDisabled={isDisabled} variant="outline_bg"
className="h-10 rounded-l-none" leftIcon={<FontAwesomeIcon icon={faPlus} />}
variant="outline_bg" onClick={() => handlePopUpToggle("addPolicy")}
leftIcon={<FontAwesomeIcon icon={faPlus} />} >
> Add Policy
New policy </Button>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="thin-scrollbar max-h-96" align="end">
{Object.keys(PROJECT_PERMISSION_OBJECT)
.filter(
(subject) =>
ProjectTypePermissionSubjects[currentWorkspace.type][
subject as ProjectPermissionSub
]
)
.sort((a, b) =>
PROJECT_PERMISSION_OBJECT[
a as keyof typeof PROJECT_PERMISSION_OBJECT
].title
.toLowerCase()
.localeCompare(
PROJECT_PERMISSION_OBJECT[
b as keyof typeof PROJECT_PERMISSION_OBJECT
].title.toLowerCase()
)
)
.map((subject) => (
<DropdownMenuItem
key={`permission-create-${subject}`}
className="py-3"
onClick={() => onNewPolicy(subject as ProjectPermissionSub)}
>
{PROJECT_PERMISSION_OBJECT[subject as ProjectPermissionSub].title}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div> </div>
</> </>
)} )}
@@ -245,6 +190,10 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
</GeneralPermissionPolicies> </GeneralPermissionPolicies>
))} ))}
</div> </div>
<PolicySelectionModal
isOpen={popUp.addPolicy.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("addPolicy", isOpen)}
/>
</FormProvider> </FormProvider>
</form> </form>
</div> </div>
@@ -251,7 +251,7 @@ export const AccessPolicyForm = ({
label="Policy Type" label="Policy Type"
isRequired isRequired
isError={Boolean(error)} isError={Boolean(error)}
tooltipText="Change polices govern secret changes within a given environment and secret path. Access polices allow underprivileged user to request access to environment/secret path." tooltipText="Change policies govern secret changes within a given environment and secret path. Access policies allow underprivileged user to request access to environment/secret path."
errorText={error?.message} errorText={error?.message}
> >
<Select <Select
@@ -163,8 +163,8 @@ export const CreateSecretImportForm = ({
onValueChange={(val) => onChange(val === "true")} onValueChange={(val) => onChange(val === "true")}
className="w-full border border-mineshaft-500" className="w-full border border-mineshaft-500"
> >
<SelectItem value="false">Ignore secret approval polices</SelectItem> <SelectItem value="false">Ignore secret approval policies</SelectItem>
<SelectItem value="true">Respect secret approval polices</SelectItem> <SelectItem value="true">Respect secret approval policies</SelectItem>
</Select> </Select>
</FormControl> </FormControl>
)} )}