From b4a2a477d34190cba3b3c9eae0a5eb6e1ceda67f Mon Sep 17 00:00:00 2001 From: = Date: Sat, 3 Aug 2024 14:55:30 +0530 Subject: [PATCH] feat: brought back workspace permission and made requested changes --- .../ee/services/permission/org-permission.ts | 12 +- backend/src/server/routes/v1/index.ts | 2 +- .../src/server/routes/v1/org-admin-router.ts | 4 +- .../services/org-admin/org-admin-service.ts | 33 +- .../src/context/OrgPermissionContext/types.ts | 10 +- frontend/src/hooks/api/orgAdmin/mutation.tsx | 4 +- frontend/src/hooks/api/orgAdmin/queries.tsx | 2 +- frontend/src/layouts/AppLayout/AppLayout.tsx | 20 +- .../src/pages/org/[id]/overview/index.tsx | 1215 ++++++++--------- .../WorkspacePermission.tsx | 133 -- .../components/OrgRoleModifySection.utils.ts | 10 +- .../OrgPermissionAdminConsoleRow.tsx | 135 ++ .../OrgRoleWorkspaceRow.tsx | 129 ++ .../RolePermissionRow.tsx | 5 +- .../RolePermissionsSection.tsx | 12 + .../OrgAdminProjects/OrgAdminProjects.tsx | 254 ++-- 16 files changed, 1074 insertions(+), 906 deletions(-) delete mode 100644 frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/WorkspacePermission.tsx create mode 100644 frontend/src/views/Org/RolePage/components/RolePermissionsSection/OrgPermissionAdminConsoleRow.tsx create mode 100644 frontend/src/views/Org/RolePage/components/RolePermissionsSection/OrgRoleWorkspaceRow.tsx diff --git a/backend/src/ee/services/permission/org-permission.ts b/backend/src/ee/services/permission/org-permission.ts index 77eaacd3b..c07107912 100644 --- a/backend/src/ee/services/permission/org-permission.ts +++ b/backend/src/ee/services/permission/org-permission.ts @@ -9,6 +9,10 @@ export enum OrgPermissionActions { Delete = "delete" } +export enum OrgPermissionAdminConsoleAction { + GrantAccessProjects = "grant-access-projects" +} + export enum OrgPermissionSubjects { Workspace = "workspace", Role = "role", @@ -22,7 +26,8 @@ export enum OrgPermissionSubjects { Billing = "billing", SecretScanning = "secret-scanning", Identity = "identity", - Kms = "kms" + Kms = "kms", + AdminConsole = "admin-console" } export type OrgPermissionSet = @@ -39,7 +44,8 @@ export type OrgPermissionSet = | [OrgPermissionActions, OrgPermissionSubjects.SecretScanning] | [OrgPermissionActions, OrgPermissionSubjects.Billing] | [OrgPermissionActions, OrgPermissionSubjects.Identity] - | [OrgPermissionActions, OrgPermissionSubjects.Kms]; + | [OrgPermissionActions, OrgPermissionSubjects.Kms] + | [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole]; const buildAdminPermission = () => { const { can, build } = new AbilityBuilder>(createMongoAbility); @@ -107,6 +113,8 @@ const buildAdminPermission = () => { can(OrgPermissionActions.Edit, OrgPermissionSubjects.Kms); can(OrgPermissionActions.Delete, OrgPermissionSubjects.Kms); + can(OrgPermissionAdminConsoleAction.GrantAccessProjects, OrgPermissionSubjects.AdminConsole); + return build({ conditionsMatcher }); }; diff --git a/backend/src/server/routes/v1/index.ts b/backend/src/server/routes/v1/index.ts index bb6b9b57b..6c988d995 100644 --- a/backend/src/server/routes/v1/index.ts +++ b/backend/src/server/routes/v1/index.ts @@ -51,7 +51,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => { await server.register(registerPasswordRouter, { prefix: "/password" }); await server.register(registerOrgRouter, { prefix: "/organization" }); await server.register(registerAdminRouter, { prefix: "/admin" }); - await server.register(registerOrgAdminRouter, { prefix: "/org-admin" }); + await server.register(registerOrgAdminRouter, { prefix: "/organization-admin" }); await server.register(registerUserRouter, { prefix: "/user" }); await server.register(registerInviteOrgRouter, { prefix: "/invite-org" }); await server.register(registerUserActionRouter, { prefix: "/user-action" }); diff --git a/backend/src/server/routes/v1/org-admin-router.ts b/backend/src/server/routes/v1/org-admin-router.ts index 5339d91b5..2d28b09bd 100644 --- a/backend/src/server/routes/v1/org-admin-router.ts +++ b/backend/src/server/routes/v1/org-admin-router.ts @@ -45,7 +45,7 @@ export const registerOrgAdminRouter = async (server: FastifyZodProvider) => { server.route({ method: "POST", - url: "/projects/:projectId/access", + url: "/projects/:projectId/grant-admin-access", config: { rateLimit: readLimit }, @@ -61,7 +61,7 @@ export const registerOrgAdminRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const { membership } = await server.services.orgAdmin.accessProject({ + const { membership } = await server.services.orgAdmin.grantProjectAdminAccess({ actorOrgId: req.permission.orgId, actorAuthMethod: req.permission.authMethod, actorId: req.permission.id, diff --git a/backend/src/services/org-admin/org-admin-service.ts b/backend/src/services/org-admin/org-admin-service.ts index aef1342b4..cfdda663d 100644 --- a/backend/src/services/org-admin/org-admin-service.ts +++ b/backend/src/services/org-admin/org-admin-service.ts @@ -1,7 +1,10 @@ -import { OrgMembershipRole, ProjectMembershipRole, ProjectVersion, SecretKeyEncoding } from "@app/db/schemas"; +import { ForbiddenError } from "@casl/ability"; + +import { ProjectMembershipRole, ProjectVersion, SecretKeyEncoding } from "@app/db/schemas"; +import { OrgPermissionAdminConsoleAction, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; -import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; +import { BadRequestError } from "@app/lib/errors"; import { TProjectDALFactory } from "../project/project-dal"; import { assignWorkspaceKeysToMembers } from "../project/project-fns"; @@ -42,15 +45,17 @@ export const orgAdminServiceFactory = ({ actorOrgId, actorAuthMethod }: TListOrgProjectsDTO) => { - const { membership } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission( actor, actorId, actorOrgId, actorAuthMethod, actorOrgId ); - const isAdmin = membership.role === OrgMembershipRole.Admin; - if (!isAdmin) throw new UnauthorizedError({ message: "Admin only operation" }); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionAdminConsoleAction.GrantAccessProjects, + OrgPermissionSubjects.AdminConsole + ); const projects = await projectDAL.find( { orgId: actorOrgId, @@ -65,16 +70,24 @@ export const orgAdminServiceFactory = ({ return { projects, count }; }; - const accessProject = async ({ actor, actorId, actorOrgId, actorAuthMethod, projectId }: TAccessProjectDTO) => { - const { membership } = await permissionService.getOrgPermission( + const grantProjectAdminAccess = async ({ + actor, + actorId, + actorOrgId, + actorAuthMethod, + projectId + }: TAccessProjectDTO) => { + const { permission, membership } = await permissionService.getOrgPermission( actor, actorId, actorOrgId, actorAuthMethod, actorOrgId ); - const isAdmin = membership.role === OrgMembershipRole.Admin; - if (!isAdmin) throw new UnauthorizedError({ message: "Admin only operation" }); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionAdminConsoleAction.GrantAccessProjects, + OrgPermissionSubjects.AdminConsole + ); const project = await projectDAL.findById(projectId); if (!project) throw new BadRequestError({ message: "Project not found" }); @@ -174,5 +187,5 @@ export const orgAdminServiceFactory = ({ return { isExistingMember: false, membership: updatedMembership }; }; - return { listOrgProjects, accessProject }; + return { listOrgProjects, grantProjectAdminAccess }; }; diff --git a/frontend/src/context/OrgPermissionContext/types.ts b/frontend/src/context/OrgPermissionContext/types.ts index 36206873d..a36aaac69 100644 --- a/frontend/src/context/OrgPermissionContext/types.ts +++ b/frontend/src/context/OrgPermissionContext/types.ts @@ -20,7 +20,12 @@ export enum OrgPermissionSubjects { Billing = "billing", SecretScanning = "secret-scanning", Identity = "identity", - Kms = "kms" + Kms = "kms", + AdminConsole = "admin-console" +} + +export enum OrgPermissionAdminConsoleAction { + GrantAccessProjects = "grant-access-projects" } export type OrgPermissionSet = @@ -37,6 +42,7 @@ export type OrgPermissionSet = | [OrgPermissionActions, OrgPermissionSubjects.SecretScanning] | [OrgPermissionActions, OrgPermissionSubjects.Billing] | [OrgPermissionActions, OrgPermissionSubjects.Identity] - | [OrgPermissionActions, OrgPermissionSubjects.Kms]; + | [OrgPermissionActions, OrgPermissionSubjects.Kms] + | [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole]; export type TOrgPermission = MongoAbility; diff --git a/frontend/src/hooks/api/orgAdmin/mutation.tsx b/frontend/src/hooks/api/orgAdmin/mutation.tsx index 3b2c69efa..9fa93722e 100644 --- a/frontend/src/hooks/api/orgAdmin/mutation.tsx +++ b/frontend/src/hooks/api/orgAdmin/mutation.tsx @@ -7,7 +7,9 @@ import { TOrgAdminAccessProjectDTO } from "./types"; export const useOrgAdminAccessProject = () => useMutation({ mutationFn: async ({ projectId }: TOrgAdminAccessProjectDTO) => { - const { data } = await apiRequest.post(`/api/v1/org-admin/projects/${projectId}/access`); + const { data } = await apiRequest.post( + `/api/v1/organization-admin/projects/${projectId}/grant-admin-access` + ); return data; } }); diff --git a/frontend/src/hooks/api/orgAdmin/queries.tsx b/frontend/src/hooks/api/orgAdmin/queries.tsx index 23826a9a4..2856de0a2 100644 --- a/frontend/src/hooks/api/orgAdmin/queries.tsx +++ b/frontend/src/hooks/api/orgAdmin/queries.tsx @@ -14,7 +14,7 @@ export const useOrgAdminGetProjects = ({ search, offset, limit = 50 }: TOrgAdmin queryKey: orgAdminQueryKeys.getProjects({ search, offset, limit }), queryFn: async () => { const { data } = await apiRequest.get<{ projects: Workspace[]; count: number }>( - "/api/v1/org-admin/projects", + "/api/v1/organization-admin/projects", { params: { limit, diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index 8a794f901..0568a5821 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -59,7 +59,6 @@ import { OrgPermissionActions, OrgPermissionSubjects, useOrganization, - useOrgPermission, useSubscription, useUser, useWorkspace @@ -132,8 +131,6 @@ export const AppLayout = ({ children }: LayoutProps) => { const { workspaces, currentWorkspace } = useWorkspace(); const { orgs, currentOrg } = useOrganization(); - const { membership } = useOrgPermission(); - const isOrgAdmin = membership?.role === "admin"; const { data: projectFavorites } = useGetUserProjectFavorites(currentOrg?.id!); const { mutateAsync: updateUserProjectFavorites } = useUpdateUserProjectFavorites(); @@ -483,6 +480,11 @@ export const AppLayout = ({ children }: LayoutProps) => { )} + + + Admin Panel + +
+
+ ); + + const renderProjectListItem = (workspace: Workspace, isFavorite: boolean, index: number) => ( + // eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events +
{ + router.push(`/project/${workspace.id}/secrets/overview`); + localStorage.setItem("projectData.id", workspace.id); + }} + key={workspace.id} + className={`min-w-72 group grid h-14 cursor-pointer grid-cols-6 border-t border-l border-r border-mineshaft-600 bg-mineshaft-800 px-6 hover:bg-mineshaft-700 ${ + index === 0 && "rounded-t-md" + } ${index === filteredWorkspaces.length - 1 && "rounded-b-md border-b"}`} + > +
+ +
{workspace.name}
+
+
+
{workspace.environments?.length || 0} environments
- -
- ); - - const renderProjectListItem = (workspace: Workspace, isFavorite: boolean, index: number) => ( - // eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events -
{ - router.push(`/project/${workspace.id}/secrets/overview`); - localStorage.setItem("projectData.id", workspace.id); - }} - key={workspace.id} - className={`min-w-72 group grid h-14 cursor-pointer grid-cols-6 border-t border-l border-r border-mineshaft-600 bg-mineshaft-800 px-6 hover:bg-mineshaft-700 ${ - index === 0 && "rounded-t-md" - } ${index === filteredWorkspaces.length - 1 && "rounded-b-md border-b"}`} - > -
- -
{workspace.name}
-
-
-
- {workspace.environments?.length || 0} environments -
- {isFavorite ? ( - { - e.stopPropagation(); - removeProjectFromFavorites(workspace.id); - }} - /> - ) : ( - { - e.stopPropagation(); - addProjectToFavorites(workspace.id); - }} - /> - )} -
-
- ); - - const projectsGridView = ( - <> - {favoriteWorkspaces.length > 0 && ( - <> -

Favorites

-
0 && "border-b border-mineshaft-600" - } py-4 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4`} - > - {favoriteWorkspaces.map((workspace) => renderProjectGridItem(workspace, true))} -
- + {isFavorite ? ( + { + e.stopPropagation(); + removeProjectFromFavorites(workspace.id); + }} + /> + ) : ( + { + e.stopPropagation(); + addProjectToFavorites(workspace.id); + }} + /> )} -
- {isProjectViewLoading && - Array.apply(0, Array(3)).map((_x, i) => ( -
-
- -
-
- -
-
- -
-
- ))} - {!isProjectViewLoading && - nonFavoriteWorkspaces.map((workspace) => renderProjectGridItem(workspace, false))} -
- - ); +
+ + ); - const projectsListView = ( -
+ const projectsGridView = ( + <> + {favoriteWorkspaces.length > 0 && ( + <> +

Favorites

+
0 && "border-b border-mineshaft-600" + } py-4 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4`} + > + {favoriteWorkspaces.map((workspace) => renderProjectGridItem(workspace, true))} +
+ + )} +
{isProjectViewLoading && Array.apply(0, Array(3)).map((_x, i) => (
- +
+ +
+
+ +
+
+ +
))} {!isProjectViewLoading && - workspacesWithFaveProp.map((workspace, ind) => - renderProjectListItem(workspace, workspace.isFavorite, ind) - )} + nonFavoriteWorkspaces.map((workspace) => renderProjectGridItem(workspace, false))}
- ); + + ); - return ( -
- - {t("common.head-title", { title: t("settings.members.title") })} - - - {!serverDetails?.redisConfigured && ( -
-

Announcements

-
- - Attention: Updated versions of Infisical now require Redis for full functionality. - Learn how to configure it - + {isProjectViewLoading && + Array.apply(0, Array(3)).map((_x, i) => ( +
+ +
+ ))} + {!isProjectViewLoading && + workspacesWithFaveProp.map((workspace, ind) => + renderProjectListItem(workspace, workspace.isFavorite, ind) + )} +
+ ); + + return ( +
+ + {t("common.head-title", { title: t("settings.members.title") })} + + + {!serverDetails?.redisConfigured && ( +
+

Announcements

+
+ + Attention: Updated versions of Infisical now require Redis for full functionality. Learn + how to configure it + + + here + + + . +
+
+ )} +
+
+

Projects

+
+
+ setSearchFilter(e.target.value)} + leftIcon={} + /> +
+ { + localStorage.setItem("projectsViewMode", ProjectsViewMode.GRID); + setProjectsViewMode(ProjectsViewMode.GRID); + }} + ariaLabel="grid" + size="xs" + className={`${ + projectsViewMode === ProjectsViewMode.GRID ? "bg-mineshaft-500" : "bg-transparent" + } min-w-[2.4rem] border-none hover:bg-mineshaft-600`} + > + + + { + localStorage.setItem("projectsViewMode", ProjectsViewMode.LIST); + setProjectsViewMode(ProjectsViewMode.LIST); + }} + ariaLabel="list" + size="xs" + className={`${ + projectsViewMode === ProjectsViewMode.LIST ? "bg-mineshaft-500" : "bg-transparent" + } min-w-[2.4rem] border-none hover:bg-mineshaft-600`} + > + + +
+ + {(isAllowed) => ( + + )} + +
+ {projectsViewMode === ProjectsViewMode.LIST ? projectsListView : projectsGridView} + {isWorkspaceEmpty && ( +
+ +
+ You are not part of any projects in this organization yet. When you are, they will + appear here. +
+
+ Create a new project, or ask other organization members to give you necessary + permissions.
)} -
-
-

Projects

-
-
- setSearchFilter(e.target.value)} - leftIcon={} - /> -
- { - localStorage.setItem("projectsViewMode", ProjectsViewMode.GRID); - setProjectsViewMode(ProjectsViewMode.GRID); - }} - ariaLabel="grid" - size="xs" - className={`${ - projectsViewMode === ProjectsViewMode.GRID ? "bg-mineshaft-500" : "bg-transparent" - } min-w-[2.4rem] border-none hover:bg-mineshaft-600`} - > - - - { - localStorage.setItem("projectsViewMode", ProjectsViewMode.LIST); - setProjectsViewMode(ProjectsViewMode.LIST); - }} - ariaLabel="list" - size="xs" - className={`${ - projectsViewMode === ProjectsViewMode.LIST ? "bg-mineshaft-500" : "bg-transparent" - } min-w-[2.4rem] border-none hover:bg-mineshaft-600`} - > - - -
- - {(isAllowed) => ( -
+
+

Explore Infisical

+
+ {features.map((feature) => ( + - - {!( - new Date().getTime() - new Date(user?.createdAt).getTime() < - 30 * 24 * 60 * 60 * 1000 - ) && ( -
-

Onboarding Guide

-
- - {orgWorkspaces.length !== 0 && ( - <> - - - - )} -
- -
-
+
+ {!(new Date().getTime() - new Date(user?.createdAt).getTime() < 30 * 24 * 60 * 60 * 1000) && ( +
+

Onboarding Guide

+
+ {orgWorkspaces.length !== 0 && ( -
-
-
- - {false && ( -
- -
- )} -
-
Inject secrets locally
-
- Replace .env files with a more secure and efficient alternative. -
+ <> + + + + )} +
+ +
+
+ {orgWorkspaces.length !== 0 && ( +
+
+
+ + {false && ( +
+ +
+ )} +
+
Inject secrets locally
+
+ Replace .env files with a more secure and efficient alternative.
-
- About 2 min -
- - {false &&
} +
+ About 2 min +
- )} - {orgWorkspaces.length !== 0 && ( - - )} -
- )} - { - handlePopUpToggle("addNewWs", isModalOpen); - reset(); - }} + + {false &&
} +
+ )} + {orgWorkspaces.length !== 0 && ( + + )} +
+ )} + { + handlePopUpToggle("addNewWs", isModalOpen); + reset(); + }} + > + - -
+ + ( + + + + )} + /> +
( - - - + name="addMembers" + defaultValue={false} + render={({ field: { onBlur, value, onChange } }) => ( + + {(isAllowed) => ( +
+ + Add all members of my organization to this project + +
+ )} +
)} /> -
- ( - - {(isAllowed) => ( -
- +
+ + + +
Advanced Settings
+
+ + ( + + { - onChange(e); - }} - className="mb-12 w-full bg-mineshaft-600" - > - - Default Infisical KMS + + Default Infisical KMS + + {externalKmsList?.map((kms) => ( + + {kms.slug} - {externalKmsList?.map((kms) => ( - - {kms.slug} - - ))} - - - )} - control={control} - name="kmsKeyId" - /> - -
-
-
- - -
+ ))} + + + )} + control={control} + name="kmsKeyId" + /> + + + +
+ +
- - - - handlePopUpToggle("upgradePlan", isOpen)} - text="You have exceeded the number of projects allowed on the free plan." - /> - {/* */} -
- ); - }, - { - action: OrgPermissionActions.Read, - subject: OrgPermissionSubjects.Workspace - } -); +
+ + + + handlePopUpToggle("upgradePlan", isOpen)} + text="You have exceeded the number of projects allowed on the free plan." + /> + {/* */} +
+ ); +}; Object.assign(OrganizationPage, { requireAuth: true }); diff --git a/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/WorkspacePermission.tsx b/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/WorkspacePermission.tsx deleted file mode 100644 index 465029f8b..000000000 --- a/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/WorkspacePermission.tsx +++ /dev/null @@ -1,133 +0,0 @@ -import { useEffect, useMemo } from "react"; -import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form"; -import { faMoneyBill } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { motion } from "framer-motion"; -import { twMerge } from "tailwind-merge"; - -import { Checkbox, Select, SelectItem } from "@app/components/v2"; -import { useToggle } from "@app/hooks"; - -import { TFormSchema } from "../../../../RolePage/components/OrgRoleModifySection.utils"; - -type Props = { - isNonEditable?: boolean; - setValue: UseFormSetValue; - control: Control; -}; - -enum Permission { - NoAccess = "no-access", - ReadOnly = "read-only", - FullAccess = "full-acess", - Custom = "custom" -} - -const PERMISSIONS = [ - { action: "read", label: "View projects" }, - { action: "create", label: "Create new projects" } -] as const; - -export const WorkspacePermission = ({ isNonEditable, setValue, control }: Props) => { - const rule = useWatch({ - control, - name: "permissions.workspace" - }); - const [isCustom, setIsCustom] = useToggle(); - - const selectedPermissionCategory = useMemo(() => { - const actions = Object.keys(rule || {}) as Array; - const totalActions = PERMISSIONS.length; - const score = actions.map((key) => (rule?.[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number); - - if (isCustom) return Permission.Custom; - if (score === 0) return Permission.NoAccess; - if (score === totalActions) return Permission.FullAccess; - if (score === 1 && rule?.read) return Permission.ReadOnly; - - return Permission.Custom; - }, [rule, isCustom]); - - useEffect(() => { - if (selectedPermissionCategory === Permission.Custom) setIsCustom.on(); - else setIsCustom.off(); - }, [selectedPermissionCategory]); - - const handlePermissionChange = (val: Permission) => { - if (val === Permission.Custom) setIsCustom.on(); - else setIsCustom.off(); - - switch (val) { - case Permission.NoAccess: - setValue("permissions.workspace", { read: false, create: false }, { shouldDirty: true }); - break; - case Permission.FullAccess: - setValue("permissions.workspace", { read: true, create: true }, { shouldDirty: true }); - break; - case Permission.ReadOnly: - setValue("permissions.workspace", { read: true, create: false }, { shouldDirty: true }); - break; - default: - setValue("permissions.workspace", { read: false, create: false }, { shouldDirty: true }); - break; - } - }; - - return ( -
-
-
- -
-
-
Project
-
- View and create new projects in this organization -
-
-
- -
-
- - {isCustom && - PERMISSIONS.map(({ action, label }) => ( - ( - - {label} - - )} - /> - ))} - -
- ); -}; diff --git a/frontend/src/views/Org/RolePage/components/OrgRoleModifySection.utils.ts b/frontend/src/views/Org/RolePage/components/OrgRoleModifySection.utils.ts index e85e62d0c..ba5308cd9 100644 --- a/frontend/src/views/Org/RolePage/components/OrgRoleModifySection.utils.ts +++ b/frontend/src/views/Org/RolePage/components/OrgRoleModifySection.utils.ts @@ -12,6 +12,12 @@ const generalPermissionSchema = z }) .optional(); +const adminConsolePermissionSchmea = z + .object({ + "grant-access-projects": z.boolean().optional() + }) + .optional(); + export const formSchema = z.object({ name: z.string().trim(), description: z.string().trim().optional(), @@ -23,7 +29,6 @@ export const formSchema = z.object({ .object({ workspace: z .object({ - read: z.boolean().optional(), create: z.boolean().optional() }) .optional(), @@ -38,7 +43,8 @@ export const formSchema = z.object({ scim: generalPermissionSchema, ldap: generalPermissionSchema, billing: generalPermissionSchema, - identity: generalPermissionSchema + identity: generalPermissionSchema, + "admin-console": adminConsolePermissionSchmea }) .optional() }); diff --git a/frontend/src/views/Org/RolePage/components/RolePermissionsSection/OrgPermissionAdminConsoleRow.tsx b/frontend/src/views/Org/RolePage/components/RolePermissionsSection/OrgPermissionAdminConsoleRow.tsx new file mode 100644 index 000000000..21d58aabb --- /dev/null +++ b/frontend/src/views/Org/RolePage/components/RolePermissionsSection/OrgPermissionAdminConsoleRow.tsx @@ -0,0 +1,135 @@ +import { useEffect, useMemo } from "react"; +import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form"; +import { faChevronDown, faChevronRight } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { createNotification } from "@app/components/notifications"; +import { Checkbox, Select, SelectItem, Td, Tr } from "@app/components/v2"; +import { useToggle } from "@app/hooks"; +import { TFormSchema } from "@app/views/Org/RolePage/components/OrgRoleModifySection.utils"; + +type Props = { + isEditable: boolean; + setValue: UseFormSetValue; + control: Control; +}; + +enum Permission { + NoAccess = "no-access", + Custom = "custom" +} + +const PERMISSION_ACTIONS = [ + { action: "grant-access-projects", label: "Grant access projects" } +] as const; + +export const OrgPermissionAdminConsoleRow = ({ isEditable, control, setValue }: Props) => { + const [isRowExpanded, setIsRowExpanded] = useToggle(); + const [isCustom, setIsCustom] = useToggle(); + + const rule = useWatch({ + control, + name: "permissions.admin-console" + }); + + const selectedPermissionCategory = useMemo(() => { + if (rule?.["grant-access-projects"]) { + return Permission.Custom; + } + return Permission.NoAccess; + }, [rule, isCustom]); + + useEffect(() => { + if (selectedPermissionCategory === Permission.Custom) setIsCustom.on(); + else setIsCustom.off(); + }, [selectedPermissionCategory]); + + useEffect(() => { + const isRowCustom = selectedPermissionCategory === Permission.Custom; + if (isRowCustom) { + setIsRowExpanded.on(); + } + }, []); + + const handlePermissionChange = (val: Permission) => { + if (!val) return; + if (val === Permission.Custom) { + setIsRowExpanded.on(); + setIsCustom.on(); + return; + } + setIsCustom.off(); + + if (val === Permission.NoAccess) { + setValue( + "permissions.admin-console", + { "grant-access-projects": false }, + { shouldDirty: true } + ); + } + }; + + return ( + <> + setIsRowExpanded.toggle()} + > + + + + Admin Console + + + + + {isRowExpanded && ( + + +
+ {PERMISSION_ACTIONS.map(({ action, label }) => { + return ( + ( + { + if (!isEditable) { + createNotification({ + type: "error", + text: "Failed to update default role" + }); + return; + } + field.onChange(e); + }} + id={`permissions.admin-console.${action}`} + > + {label} + + )} + /> + ); + })} +
+ + + )} + + ); +}; diff --git a/frontend/src/views/Org/RolePage/components/RolePermissionsSection/OrgRoleWorkspaceRow.tsx b/frontend/src/views/Org/RolePage/components/RolePermissionsSection/OrgRoleWorkspaceRow.tsx new file mode 100644 index 000000000..ea7f368c5 --- /dev/null +++ b/frontend/src/views/Org/RolePage/components/RolePermissionsSection/OrgRoleWorkspaceRow.tsx @@ -0,0 +1,129 @@ +import { useEffect, useMemo } from "react"; +import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form"; +import { faChevronDown, faChevronRight } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { createNotification } from "@app/components/notifications"; +import { Checkbox, Select, SelectItem, Td, Tr } from "@app/components/v2"; +import { useToggle } from "@app/hooks"; +import { TFormSchema } from "@app/views/Org/RolePage/components/OrgRoleModifySection.utils"; + +type Props = { + isEditable: boolean; + setValue: UseFormSetValue; + control: Control; +}; + +enum Permission { + NoAccess = "no-access", + Custom = "custom" +} + +const PERMISSION_ACTIONS = [{ action: "create", label: "Create projects" }] as const; + +export const OrgRoleWorkspaceRow = ({ isEditable, control, setValue }: Props) => { + const [isRowExpanded, setIsRowExpanded] = useToggle(); + const [isCustom, setIsCustom] = useToggle(); + + const rule = useWatch({ + control, + name: "permissions.workspace" + }); + + const selectedPermissionCategory = useMemo(() => { + if (rule?.create) { + return Permission.Custom; + } + return Permission.NoAccess; + }, [rule, isCustom]); + + useEffect(() => { + if (selectedPermissionCategory === Permission.Custom) setIsCustom.on(); + else setIsCustom.off(); + }, [selectedPermissionCategory]); + + useEffect(() => { + const isRowCustom = selectedPermissionCategory === Permission.Custom; + if (isRowCustom) { + setIsRowExpanded.on(); + } + }, []); + + const handlePermissionChange = (val: Permission) => { + if (!val) return; + if (val === Permission.Custom) { + setIsRowExpanded.on(); + setIsCustom.on(); + return; + } + setIsCustom.off(); + + if (val === Permission.NoAccess) { + setValue("permissions.workspace", { create: false }, { shouldDirty: true }); + } + }; + + return ( + <> + setIsRowExpanded.toggle()} + > + + + + Project + + + + + {isRowExpanded && ( + + +
+ {PERMISSION_ACTIONS.map(({ action, label }) => { + return ( + ( + { + if (!isEditable) { + createNotification({ + type: "error", + text: "Failed to update default role" + }); + return; + } + field.onChange(e); + }} + id={`permissions.admin-console.${action}`} + > + {label} + + )} + /> + ); + })} +
+ + + )} + + ); +}; diff --git a/frontend/src/views/Org/RolePage/components/RolePermissionsSection/RolePermissionRow.tsx b/frontend/src/views/Org/RolePage/components/RolePermissionsSection/RolePermissionRow.tsx index 6f4cc7c88..7f6c3953e 100644 --- a/frontend/src/views/Org/RolePage/components/RolePermissionsSection/RolePermissionRow.tsx +++ b/frontend/src/views/Org/RolePage/components/RolePermissionsSection/RolePermissionRow.tsx @@ -61,7 +61,10 @@ const getPermissionList = (option: string) => { type Props = { isEditable: boolean; title: string; - formName: keyof Omit, "workspace">; + formName: keyof Omit< + Exclude, + "workspace" | "admin-console" + >; setValue: UseFormSetValue; control: Control; }; diff --git a/frontend/src/views/Org/RolePage/components/RolePermissionsSection/RolePermissionsSection.tsx b/frontend/src/views/Org/RolePage/components/RolePermissionsSection/RolePermissionsSection.tsx index fe19620f5..f4b237cfe 100644 --- a/frontend/src/views/Org/RolePage/components/RolePermissionsSection/RolePermissionsSection.tsx +++ b/frontend/src/views/Org/RolePage/components/RolePermissionsSection/RolePermissionsSection.tsx @@ -12,6 +12,8 @@ import { TFormSchema } from "@app/views/Org/RolePage/components/OrgRoleModifySection.utils"; +import { OrgPermissionAdminConsoleRow } from "./OrgPermissionAdminConsoleRow"; +import { OrgRoleWorkspaceRow } from "./OrgRoleWorkspaceRow"; import { RolePermissionRow } from "./RolePermissionRow"; const SIMPLE_PERMISSION_OPTIONS = [ @@ -153,6 +155,16 @@ export const RolePermissionsSection = ({ roleId }: Props) => { /> ); })} + + diff --git a/frontend/src/views/OrgAdminPage/components/OrgAdminProjects/OrgAdminProjects.tsx b/frontend/src/views/OrgAdminPage/components/OrgAdminProjects/OrgAdminProjects.tsx index d2b66ad18..8461cc893 100644 --- a/frontend/src/views/OrgAdminPage/components/OrgAdminProjects/OrgAdminProjects.tsx +++ b/frontend/src/views/OrgAdminPage/components/OrgAdminProjects/OrgAdminProjects.tsx @@ -23,133 +23,145 @@ import { Td, Th, THead, - Tr} from "@app/components/v2"; + Tr +} from "@app/components/v2"; +import { + OrgPermissionAdminConsoleAction, + OrgPermissionSubjects +} from "@app/context/OrgPermissionContext/types"; +import { withPermission } from "@app/hoc"; import { useDebounce } from "@app/hooks"; import { useOrgAdminAccessProject, useOrgAdminGetProjects } from "@app/hooks/api"; -export const OrgAdminProjects = () => { - const [page, setPage] = useState(1); - const [search, setSearch] = useState(""); - const debouncedSearch = useDebounce(search); - const [perPage, setPerPage] = useState(25); - const router = useRouter(); - const orgAdminAccessProject = useOrgAdminAccessProject(); +export const OrgAdminProjects = withPermission( + () => { + const [page, setPage] = useState(1); + const [search, setSearch] = useState(""); + const debouncedSearch = useDebounce(search); + const [perPage, setPerPage] = useState(25); + const router = useRouter(); + const orgAdminAccessProject = useOrgAdminAccessProject(); - const { data, isLoading: isProjectsLoading } = useOrgAdminGetProjects({ - offset: (page - 1) * perPage, - limit: perPage, - search: debouncedSearch || undefined - }); + const { data, isLoading: isProjectsLoading } = useOrgAdminGetProjects({ + offset: (page - 1) * perPage, + limit: perPage, + search: debouncedSearch || undefined + }); - const projects = data?.projects || []; - const projectCount = data?.count || 0; - const isEmpty = !isProjectsLoading && projects.length === 0; + const projects = data?.projects || []; + const projectCount = data?.count || 0; + const isEmpty = !isProjectsLoading && projects.length === 0; - const handleAccessProject = async (projectId: string) => { - try { - await orgAdminAccessProject.mutateAsync({ - projectId - }); - await router.push({ - pathname: "/project/[projectId]/secrets/overview", - query: { + const handleAccessProject = async (projectId: string) => { + try { + await orgAdminAccessProject.mutateAsync({ projectId - } - }); - } catch { - createNotification({ - text: "Failed to access project", - type: "error" - }); - } - }; + }); + await router.push({ + pathname: "/project/[projectId]/secrets/overview", + query: { + projectId + } + }); + } catch { + createNotification({ + text: "Failed to access project", + type: "error" + }); + } + }; - return ( - -
-
-

Projects

+ return ( + +
+
+

Projects

+
+
+ setSearch(e.target.value)} + leftIcon={} + placeholder="Search by project name" + /> + + + + + + + + + + + {isProjectsLoading && } + {!isProjectsLoading && + projects?.map(({ name, slug, createdAt, id }) => ( + + + + + + + ))} + +
NameSlugCreated At +
{name}{slug}{format(new Date(createdAt), "yyyy-MM-dd, hh:mm aaa")} +
+ + + + + + { + e.stopPropagation(); + e.preventDefault(); + handleAccessProject(id); + }} + icon={} + disabled={ + orgAdminAccessProject.variables?.projectId === id && + orgAdminAccessProject.isLoading + } + > + Access{" "} + {orgAdminAccessProject.variables?.projectId === id && + orgAdminAccessProject.isLoading && } + + + +
+
+ {!isProjectsLoading && ( + setPage(newPage)} + onChangePerPage={(newPerPage) => setPerPage(newPerPage)} + /> + )} + {isEmpty && } +
+
-
- setSearch(e.target.value)} - leftIcon={} - placeholder="Search by project name" - /> - - - - - - - - - - - {isProjectsLoading && } - {!isProjectsLoading && - projects?.map(({ name, slug, createdAt, id }) => ( - - - - - - - ))} - -
NameSlugCreated At -
{name}{slug}{format(new Date(createdAt), "yyyy-MM-dd, hh:mm aaa")} -
- - - - - - { - e.stopPropagation(); - e.preventDefault(); - handleAccessProject(id); - }} - icon={} - disabled={ - orgAdminAccessProject.variables?.projectId === id && - orgAdminAccessProject.isLoading - } - > - Access{" "} - {orgAdminAccessProject.variables?.projectId === id && - orgAdminAccessProject.isLoading && } - - - -
-
- {!isProjectsLoading && ( - setPage(newPage)} - onChangePerPage={(newPerPage) => setPerPage(newPerPage)} - /> - )} - {isEmpty && } -
-
-
- - ); -}; + + ); + }, + { + action: OrgPermissionAdminConsoleAction.GrantAccessProjects, + subject: OrgPermissionSubjects.AdminConsole + } +);