diff --git a/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts b/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts index 4811eb52a..127de1383 100644 --- a/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts +++ b/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts @@ -1,4 +1,4 @@ -import { ForbiddenError, MongoAbility, RawRuleOf } from "@casl/ability"; +import { ForbiddenError, MongoAbility, RawRuleOf, subject } from "@casl/ability"; import { PackRule, packRules, unpackRules } from "@casl/ability/extra"; import ms from "ms"; @@ -69,7 +69,11 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + subject(ProjectPermissionSub.Identity, { identityId }) + ); + const { permission: targetIdentityPermission } = await permissionService.getProjectPermission( ActorType.IDENTITY, identityId, @@ -146,7 +150,11 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + subject(ProjectPermissionSub.Identity, { identityId }) + ); const { permission: targetIdentityPermission } = await permissionService.getProjectPermission( ActorType.IDENTITY, @@ -241,7 +249,11 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + subject(ProjectPermissionSub.Identity, { identityId }) + ); + const { permission: identityRolePermission } = await permissionService.getProjectPermission( ActorType.IDENTITY, identityProjectMembership.identityId, @@ -294,7 +306,10 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Identity); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Identity, { identityId }) + ); const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({ slug, @@ -333,7 +348,11 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Identity, { identityId }) + ); const identityPrivileges = await identityProjectAdditionalPrivilegeDAL.find({ projectMembershipId: identityProjectMembership.id diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index c6e574fb1..f6d7f715f 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -82,6 +82,10 @@ export type SecretImportSubjectFields = { secretPath: string; }; +export type IdentityManagementSubjectFields = { + identityId: string; +}; + export type ProjectPermissionSet = | [ ProjectPermissionActions, @@ -121,7 +125,10 @@ export type ProjectPermissionSet = | [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens] | [ProjectPermissionActions, ProjectPermissionSub.SecretApproval] | [ProjectPermissionActions, ProjectPermissionSub.SecretRotation] - | [ProjectPermissionActions, ProjectPermissionSub.Identity] + | [ + ProjectPermissionActions, + ProjectPermissionSub.Identity | (ForcedSubject & IdentityManagementSubjectFields) + ] | [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities] | [ProjectPermissionActions, ProjectPermissionSub.Certificates] | [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates] @@ -213,6 +220,21 @@ const SecretConditionV2Schema = z }) .partial(); +const IdentityManagementConditionSchema = z + .object({ + identityId: z.union([ + z.string(), + z + .object({ + [PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ], + [PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ], + [PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN] + }) + .partial() + ]) + }) + .partial(); + const GeneralPermissionSchema = [ z.object({ subject: z.literal(ProjectPermissionSub.SecretApproval).describe("The entity this permission pertains to."), @@ -262,12 +284,6 @@ const GeneralPermissionSchema = [ "Describe what action an entity can take." ) }), - z.object({ - subject: z.literal(ProjectPermissionSub.Identity).describe("The entity this permission pertains to."), - action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe( - "Describe what action an entity can take." - ) - }), z.object({ subject: z.literal(ProjectPermissionSub.ServiceTokens).describe("The entity this permission pertains to."), action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe( @@ -373,6 +389,12 @@ export const ProjectPermissionV1Schema = z.discriminatedUnion("subject", [ "Describe what action an entity can take." ) }), + z.object({ + subject: z.literal(ProjectPermissionSub.Identity).describe("The entity this permission pertains to."), + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe( + "Describe what action an entity can take." + ) + }), ...GeneralPermissionSchema ]); @@ -417,6 +439,16 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [ "When specified, only matching conditions will be allowed to access given resource." ).optional() }), + z.object({ + subject: z.literal(ProjectPermissionSub.Identity).describe("The entity this permission pertains to."), + inverted: z.boolean().optional().describe("Whether rule allows or forbids."), + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe( + "Describe what action an entity can take." + ), + conditions: IdentityManagementConditionSchema.describe( + "When specified, only matching conditions will be allowed to access given resource." + ).optional() + }), ...GeneralPermissionSchema ]); @@ -697,26 +729,26 @@ export const buildServiceTokenProjectPermission = ( [ProjectPermissionSub.Secrets, ProjectPermissionSub.SecretImports, ProjectPermissionSub.SecretFolders].forEach( (subject) => { if (canWrite) { - // TODO: @Akhi - // @ts-expect-error type can(ProjectPermissionActions.Edit, subject, { + // TODO: @Akhi + // @ts-expect-error type secretPath: { $glob: secretPath }, environment }); - // @ts-expect-error type can(ProjectPermissionActions.Create, subject, { + // @ts-expect-error type secretPath: { $glob: secretPath }, environment }); - // @ts-expect-error type can(ProjectPermissionActions.Delete, subject, { + // @ts-expect-error type secretPath: { $glob: secretPath }, environment }); } if (canRead) { - // @ts-expect-error type can(ProjectPermissionActions.Read, subject, { + // @ts-expect-error type secretPath: { $glob: secretPath }, environment }); diff --git a/backend/src/services/identity-project/identity-project-service.ts b/backend/src/services/identity-project/identity-project-service.ts index 7f9cf920e..69364a3cf 100644 --- a/backend/src/services/identity-project/identity-project-service.ts +++ b/backend/src/services/identity-project/identity-project-service.ts @@ -1,4 +1,4 @@ -import { ForbiddenError } from "@casl/ability"; +import { ForbiddenError, subject } from "@casl/ability"; import ms from "ms"; import { ProjectMembershipRole } from "@app/db/schemas"; @@ -161,7 +161,10 @@ export const identityProjectServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + subject(ProjectPermissionSub.Identity, { identityId }) + ); const projectIdentity = await identityProjectDAL.findOne({ identityId, projectId }); if (!projectIdentity) @@ -253,7 +256,11 @@ export const identityProjectServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Identity); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Delete, + subject(ProjectPermissionSub.Identity, { identityId }) + ); + const { permission: identityRolePermission } = await permissionService.getProjectPermission( ActorType.IDENTITY, identityId, @@ -317,7 +324,11 @@ export const identityProjectServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Identity); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Identity, { identityId }) + ); const [identityMembership] = await identityProjectDAL.findByProjectId(projectId, { identityId }); if (!identityMembership) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 91b8d4bc3..64380ae9f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -89,7 +89,7 @@ "react-mailchimp-subscribe": "^2.1.3", "react-markdown": "^8.0.3", "react-redux": "^8.0.2", - "react-select": "^5.8.3", + "react-select": "^5.8.1", "react-table": "^7.8.0", "react-toastify": "^9.1.3", "sanitize-html": "^2.12.1", diff --git a/frontend/src/context/ProjectPermissionContext/types.ts b/frontend/src/context/ProjectPermissionContext/types.ts index 8f10d5f21..673b2b41a 100644 --- a/frontend/src/context/ProjectPermissionContext/types.ts +++ b/frontend/src/context/ProjectPermissionContext/types.ts @@ -33,6 +33,10 @@ export enum PermissionConditionOperators { $GLOB = "$glob" } +export type IdentityManagementSubjectFields = { + identityId: string; +}; + export const formatedConditionsOperatorNames: { [K in PermissionConditionOperators]: string } = { [PermissionConditionOperators.$EQ]: "equal to", [PermissionConditionOperators.$IN]: "contains", @@ -151,7 +155,13 @@ export type ProjectPermissionSet = | [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens] | [ProjectPermissionActions, ProjectPermissionSub.SecretApproval] | [ProjectPermissionActions, ProjectPermissionSub.SecretRotation] - | [ProjectPermissionActions, ProjectPermissionSub.Identity] + | [ + ProjectPermissionActions, + ( + | ProjectPermissionSub.Identity + | (ForcedSubject & IdentityManagementSubjectFields) + ) + ] | [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities] | [ProjectPermissionActions, ProjectPermissionSub.Certificates] | [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates] diff --git a/frontend/src/reactQuery.tsx b/frontend/src/reactQuery.tsx index c897c6e02..1b6e69b36 100644 --- a/frontend/src/reactQuery.tsx +++ b/frontend/src/reactQuery.tsx @@ -92,7 +92,7 @@ export const queryClient = new QueryClient({ >
{serverResponse.details?.map((el, index) => { - const hasConditions = Object.keys(el.conditions || {}).length; + const hasConditions = Boolean(Object.keys(el.conditions || {}).length); return (
diff --git a/frontend/src/views/Project/IdentityDetailsPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeModifySection.tsx b/frontend/src/views/Project/IdentityDetailsPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeModifySection.tsx index f69cd1478..586a1db7a 100644 --- a/frontend/src/views/Project/IdentityDetailsPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeModifySection.tsx +++ b/frontend/src/views/Project/IdentityDetailsPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeModifySection.tsx @@ -17,25 +17,24 @@ import { TtlFormLabel } from "@app/components/features"; import { createNotification } from "@app/components/notifications"; import { Button, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, FormControl, FormLabel, Input, - Modal, - ModalContent, - ModalTrigger, Popover, PopoverContent, PopoverTrigger, Tag, - Tooltip -} from "@app/components/v2"; + Tooltip} from "@app/components/v2"; import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission, useWorkspace } from "@app/context"; -import { usePopUp } from "@app/hooks"; import { useCreateIdentityProjectAdditionalPrivilege, useGetIdentityProjectPrivilegeDetails, @@ -43,14 +42,13 @@ import { } from "@app/hooks/api"; import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/hooks/api/identityProjectAdditionalPrivilege/types"; import { GeneralPermissionPolicies } from "@app/views/Project/RolePage/components/RolePermissionsSection/components/GeneralPermissionPolicies"; -import { NewPermissionRule } from "@app/views/Project/RolePage/components/RolePermissionsSection/components/NewPermissionRule"; import { PermissionEmptyState } from "@app/views/Project/RolePage/components/RolePermissionsSection/PermissionEmptyState"; import { formRolePermission2API, + isConditionalSubjects, PROJECT_PERMISSION_OBJECT, projectRoleFormSchema, - rolePermission2Form -} from "@app/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils"; + rolePermission2Form} from "@app/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils"; import { renderConditionalComponents } from "@app/views/Project/RolePage/components/RolePermissionsSection/RolePermissionsSection"; type Props = { @@ -88,7 +86,6 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({ }: Props) => { const isCreate = !privilegeId; const { currentWorkspace } = useWorkspace(); - const { popUp, handlePopUpToggle } = usePopUp(["createPolicy"] as const); const projectId = currentWorkspace?.id || ""; const { data: privilegeDetails, isLoading } = useGetIdentityProjectPrivilegeDetails({ identityId, @@ -194,6 +191,30 @@ 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 (
Save - handlePopUpToggle("createPolicy", isOpen)} - > - + + - - - handlePopUpToggle("createPolicy")} /> - - + + + {Object.keys(PROJECT_PERMISSION_OBJECT) + .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) => ( + onNewPolicy(subject as ProjectPermissionSub)} + > + {PROJECT_PERMISSION_OBJECT[subject as ProjectPermissionSub].title} + + ))} + +
diff --git a/frontend/src/views/Project/IdentityDetailsPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeSection.tsx b/frontend/src/views/Project/IdentityDetailsPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeSection.tsx index 975ddb5c7..db900b1b2 100644 --- a/frontend/src/views/Project/IdentityDetailsPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeSection.tsx +++ b/frontend/src/views/Project/IdentityDetailsPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeSection.tsx @@ -1,3 +1,4 @@ +import { subject } from "@casl/ability"; import { faEllipsisV, faFolder, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { format, formatDistance } from "date-fns"; @@ -83,7 +84,9 @@ export const IdentityProjectAdditionalPrivilegeSection = ({ identityMembershipDe privilegeId={(popUp?.modifyPrivilege?.data as { id: string })?.id} isDisabled={permission.cannot( ProjectPermissionActions.Edit, - ProjectPermissionSub.Identity + subject(ProjectPermissionSub.Identity, { + identityId + }) )} /> @@ -103,7 +106,9 @@ export const IdentityProjectAdditionalPrivilegeSection = ({ identityMembershipDe @@ -192,7 +197,9 @@ export const IdentityProjectAdditionalPrivilegeSection = ({ identityMembershipDe
diff --git a/frontend/src/views/Project/MemberDetailsPage/components/MemberProjectAdditionalPrivilegeSection/MembershipProjectAdditionalPrivilegeModifySection.tsx b/frontend/src/views/Project/MemberDetailsPage/components/MemberProjectAdditionalPrivilegeSection/MembershipProjectAdditionalPrivilegeModifySection.tsx index e5a6adf4d..4118429b2 100644 --- a/frontend/src/views/Project/MemberDetailsPage/components/MemberProjectAdditionalPrivilegeSection/MembershipProjectAdditionalPrivilegeModifySection.tsx +++ b/frontend/src/views/Project/MemberDetailsPage/components/MemberProjectAdditionalPrivilegeSection/MembershipProjectAdditionalPrivilegeModifySection.tsx @@ -17,12 +17,13 @@ import { TtlFormLabel } from "@app/components/features"; import { createNotification } from "@app/components/notifications"; import { Button, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, FormControl, FormLabel, Input, - Modal, - ModalContent, - ModalTrigger, Popover, PopoverContent, PopoverTrigger, @@ -35,7 +36,6 @@ import { useProjectPermission, useWorkspace } from "@app/context"; -import { usePopUp } from "@app/hooks"; import { useCreateProjectUserAdditionalPrivilege, useGetProjectUserPrivilegeDetails, @@ -43,14 +43,13 @@ import { } from "@app/hooks/api"; import { ProjectUserAdditionalPrivilegeTemporaryMode } from "@app/hooks/api/projectUserAdditionalPrivilege/types"; import { GeneralPermissionPolicies } from "@app/views/Project/RolePage/components/RolePermissionsSection/components/GeneralPermissionPolicies"; -import { NewPermissionRule } from "@app/views/Project/RolePage/components/RolePermissionsSection/components/NewPermissionRule"; import { PermissionEmptyState } from "@app/views/Project/RolePage/components/RolePermissionsSection/PermissionEmptyState"; import { formRolePermission2API, + isConditionalSubjects, PROJECT_PERMISSION_OBJECT, projectRoleFormSchema, - rolePermission2Form -} from "@app/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils"; + rolePermission2Form} from "@app/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils"; import { renderConditionalComponents } from "@app/views/Project/RolePage/components/RolePermissionsSection/RolePermissionsSection"; type Props = { @@ -88,7 +87,6 @@ export const MembershipProjectAdditionalPrivilegeModifySection = ({ }: Props) => { const isCreate = !privilegeId; const { currentWorkspace } = useWorkspace(); - const { popUp, handlePopUpToggle } = usePopUp(["createPolicy"] as const); const projectId = currentWorkspace?.id || ""; const { data: privilegeDetails, isLoading } = useGetProjectUserPrivilegeDetails( privilegeId || "" @@ -167,6 +165,30 @@ 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 isTemporary = privilegeTemporaryAccess?.isTemporary; const isExpired = @@ -229,24 +251,39 @@ export const MembershipProjectAdditionalPrivilegeModifySection = ({ > Save - handlePopUpToggle("createPolicy", isOpen)} - > - + + - - - handlePopUpToggle("createPolicy")} /> - - + + + {Object.keys(PROJECT_PERMISSION_OBJECT) + .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) => ( + onNewPolicy(subject as ProjectPermissionSub)} + > + {PROJECT_PERMISSION_OBJECT[subject as ProjectPermissionSub].title} + + ))} + +
diff --git a/frontend/src/views/Project/MembersPage/components/IdentityTab/IdentityTab.tsx b/frontend/src/views/Project/MembersPage/components/IdentityTab/IdentityTab.tsx index defd32863..362c15656 100644 --- a/frontend/src/views/Project/MembersPage/components/IdentityTab/IdentityTab.tsx +++ b/frontend/src/views/Project/MembersPage/components/IdentityTab/IdentityTab.tsx @@ -1,5 +1,6 @@ import Link from "next/link"; import { useRouter } from "next/router"; +import { subject } from "@casl/ability"; import { faArrowDown, faArrowUp, @@ -349,7 +350,9 @@ export const IdentityTab = withProjectPermission( {(isAllowed) => ( { const formConditions: z.infer = []; @@ -483,8 +490,8 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = { { label: "Remove members", value: "delete" } ] }, - [ProjectPermissionSub.Groups]: { - title: "Group Management", + [ProjectPermissionSub.Identity]: { + title: "Machine Identity Management", actions: [ { label: "Read", value: "read" }, { label: "Create", value: "create" }, @@ -492,8 +499,8 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = { { label: "Remove", value: "delete" } ] }, - [ProjectPermissionSub.Identity]: { - title: "Machine Identity Management", + [ProjectPermissionSub.Groups]: { + title: "Group Management", actions: [ { label: "Read", value: "read" }, { label: "Create", value: "create" }, @@ -527,7 +534,7 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = { ] }, [ProjectPermissionSub.Environments]: { - title: "Environments", + title: "Environment Management", actions: [ { label: "Read", value: "read" }, { label: "Create", value: "create" }, diff --git a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionsSection.tsx b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionsSection.tsx index 1e00306be..16397ac24 100644 --- a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionsSection.tsx +++ b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionsSection.tsx @@ -5,14 +5,19 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { twMerge } from "tailwind-merge"; import { createNotification } from "@app/components/notifications"; -import { Alert, Button, Modal, ModalContent, ModalTrigger } from "@app/components/v2"; +import { + Button, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger +} from "@app/components/v2"; import { ProjectPermissionSub, useWorkspace } from "@app/context"; -import { usePopUp } from "@app/hooks"; import { useGetProjectRoleBySlug, useUpdateProjectRole } from "@app/hooks/api"; import { GeneralPermissionConditions } from "./components/GeneralPermissionConditions"; import { GeneralPermissionPolicies } from "./components/GeneralPermissionPolicies"; -import { NewPermissionRule } from "./components/NewPermissionRule"; +import { IdentityManagementPermissionConditions } from "./components/IdentityManagementPermissionConditions"; import { SecretPermissionConditions } from "./components/SecretPermissionConditions"; import { PermissionEmptyState } from "./PermissionEmptyState"; import { @@ -37,6 +42,10 @@ export const renderConditionalComponents = ( return ; if (isConditionalSubjects(subject)) { + if (subject === ProjectPermissionSub.Identity) { + return ; + } + return ; } @@ -45,7 +54,6 @@ export const renderConditionalComponents = ( export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => { const { currentWorkspace } = useWorkspace(); - const { popUp, handlePopUpToggle } = usePopUp(["createPolicy"] as const); const projectId = currentWorkspace?.id || ""; const { data: role, isLoading } = useGetProjectRoleBySlug( currentWorkspace?.id ?? "", @@ -83,6 +91,30 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => { const isCustomRole = !["admin", "member", "viewer", "no-access"].includes(role?.slug ?? ""); + 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 ( { > Save - handlePopUpToggle("createPolicy", isOpen)} - > - + + - - - handlePopUpToggle("createPolicy")} /> - - + + + {Object.keys(PROJECT_PERMISSION_OBJECT) + .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) => ( + onNewPolicy(subject as ProjectPermissionSub)} + > + {PROJECT_PERMISSION_OBJECT[subject as ProjectPermissionSub].title} + + ))} + + )} -
{!isLoading && } {(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map((subject) => ( diff --git a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/components/GeneralPermissionPolicies.tsx b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/components/GeneralPermissionPolicies.tsx index 9d6e699cb..15d819fc8 100644 --- a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/components/GeneralPermissionPolicies.tsx +++ b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/components/GeneralPermissionPolicies.tsx @@ -157,7 +157,7 @@ export const GeneralPermissionPolicies = { - items.insert(rootIndex, [ + items.insert(rootIndex + 1, [ { read: false, edit: false, create: false, delete: false } as any ]); }} diff --git a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/components/IdentityManagementPermissionConditions.tsx b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/components/IdentityManagementPermissionConditions.tsx new file mode 100644 index 000000000..9409ce46d --- /dev/null +++ b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/components/IdentityManagementPermissionConditions.tsx @@ -0,0 +1,171 @@ +import { Controller, useFieldArray, useFormContext } from "react-hook-form"; +import { faInfoCircle, faPlus, faTrash, faWarning } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { + Button, + FormControl, + IconButton, + Input, + Select, + SelectItem, + Tooltip +} from "@app/components/v2"; +import { + PermissionConditionOperators, + ProjectPermissionSub +} from "@app/context/ProjectPermissionContext/types"; + +import { TFormSchema } from "../ProjectRoleModifySection.utils"; +import { getConditionOperatorHelperInfo } from "./PermissionConditionHelpers"; + +type Props = { + position?: number; + isDisabled?: boolean; +}; + +export const IdentityManagementPermissionConditions = ({ position = 0, isDisabled }: Props) => { + const { + control, + watch, + formState: { errors } + } = useFormContext(); + const permissionSubject = ProjectPermissionSub.Identity; + const items = useFieldArray({ + control, + name: `permissions.${permissionSubject}.${position}.conditions` + }); + + return ( +
+

Conditions

+

+ When this policy should apply (always if no conditions are added). +

+
+ {items.fields.map((el, index) => { + const condition = + (watch(`permissions.${permissionSubject}.${position}.conditions.${index}`) as { + lhs: string; + rhs: string; + operator: string; + }) || {}; + return ( +
+
+ ( + + + + )} + /> +
+
+ ( + + + + )} + /> +
+ + + +
+
+
+ ( + + + + )} + /> +
+
+ items.remove(index)} + > + + +
+
+ ); + })} +
+ {errors?.permissions?.[permissionSubject]?.[position]?.conditions?.message && ( +
+ + {errors?.permissions?.[permissionSubject]?.[position]?.conditions?.message} +
+ )} +
{}
+
+ +
+
+ ); +}; diff --git a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/components/PermissionConditionHelpers.tsx b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/components/PermissionConditionHelpers.tsx index 21fad117a..212f38e92 100644 --- a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/components/PermissionConditionHelpers.tsx +++ b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/components/PermissionConditionHelpers.tsx @@ -21,6 +21,7 @@ export const renderOperatorSelectItems = (type: string) => { if (type === "secretTags") { return Contains; } + return ( <> Equal