From 13e0dd8e0feae856260f9a133a5fe9984f33ee93 Mon Sep 17 00:00:00 2001 From: = Date: Fri, 2 Aug 2024 16:22:42 +0530 Subject: [PATCH 1/7] feat: completed org admin based project access feature --- backend/src/@types/fastify.d.ts | 2 + .../ee/services/audit-log/audit-log-types.ts | 16 +- backend/src/lib/knex/index.ts | 36 +++- backend/src/server/routes/index.ts | 14 +- backend/src/server/routes/v1/index.ts | 2 + .../src/server/routes/v1/org-admin-router.ts | 90 +++++++++ .../src/services/org-admin/org-admin-dal.ts | 5 + .../services/org-admin/org-admin-service.ts | 178 ++++++++++++++++++ .../src/services/org-admin/org-admin-types.ts | 11 ++ .../project-membership-service.ts | 1 - .../components/v2/Pagination/Pagination.tsx | 2 +- .../src/hooks/api/auditLogs/constants.tsx | 3 +- frontend/src/hooks/api/auditLogs/enums.tsx | 3 +- frontend/src/hooks/api/auditLogs/types.tsx | 13 +- frontend/src/hooks/api/index.tsx | 1 + frontend/src/hooks/api/orgAdmin/index.tsx | 2 + frontend/src/hooks/api/orgAdmin/mutation.tsx | 13 ++ frontend/src/hooks/api/orgAdmin/queries.tsx | 30 +++ frontend/src/hooks/api/orgAdmin/types.ts | 9 + frontend/src/hooks/api/workspace/queries.tsx | 1 + frontend/src/hooks/api/workspace/types.ts | 1 + frontend/src/hooks/usePopUp.tsx | 2 +- frontend/src/layouts/AppLayout/AppLayout.tsx | 15 ++ frontend/src/pages/org/[id]/admin/index.tsx | 21 +++ .../src/views/OrgAdminPage/OrgAdminPage.tsx | 30 +++ .../OrgAdminProjects/OrgAdminProjects.tsx | 155 +++++++++++++++ .../components/OrgAdminProjects/index.tsx | 1 + frontend/src/views/OrgAdminPage/index.tsx | 1 + .../AuditLogsPage/components/LogsTableRow.tsx | 6 + 29 files changed, 649 insertions(+), 15 deletions(-) create mode 100644 backend/src/server/routes/v1/org-admin-router.ts create mode 100644 backend/src/services/org-admin/org-admin-dal.ts create mode 100644 backend/src/services/org-admin/org-admin-service.ts create mode 100644 backend/src/services/org-admin/org-admin-types.ts create mode 100644 frontend/src/hooks/api/orgAdmin/index.tsx create mode 100644 frontend/src/hooks/api/orgAdmin/mutation.tsx create mode 100644 frontend/src/hooks/api/orgAdmin/queries.tsx create mode 100644 frontend/src/hooks/api/orgAdmin/types.ts create mode 100644 frontend/src/pages/org/[id]/admin/index.tsx create mode 100644 frontend/src/views/OrgAdminPage/OrgAdminPage.tsx create mode 100644 frontend/src/views/OrgAdminPage/components/OrgAdminProjects/OrgAdminProjects.tsx create mode 100644 frontend/src/views/OrgAdminPage/components/OrgAdminProjects/index.tsx create mode 100644 frontend/src/views/OrgAdminPage/index.tsx diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index 8ae892560..cfffdeac8 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -50,6 +50,7 @@ import { TIntegrationServiceFactory } from "@app/services/integration/integratio import { TIntegrationAuthServiceFactory } from "@app/services/integration-auth/integration-auth-service"; import { TOrgRoleServiceFactory } from "@app/services/org/org-role-service"; import { TOrgServiceFactory } from "@app/services/org/org-service"; +import { TOrgAdminServiceFactory } from "@app/services/org-admin/org-admin-service"; import { TProjectServiceFactory } from "@app/services/project/project-service"; import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service"; import { TProjectEnvServiceFactory } from "@app/services/project-env/project-env-service"; @@ -165,6 +166,7 @@ declare module "fastify" { rateLimit: TRateLimitServiceFactory; userEngagement: TUserEngagementServiceFactory; externalKms: TExternalKmsServiceFactory; + orgAdmin: TOrgAdminServiceFactory; }; // this is exclusive use for middlewares in which we need to inject data // everywhere else access using service layer diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index bfc1dbf92..6f78ea0d6 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -147,7 +147,8 @@ export enum EventType { GET_KMS = "get-kms", UPDATE_PROJECT_KMS = "update-project-kms", GET_PROJECT_KMS_BACKUP = "get-project-kms-backup", - LOAD_PROJECT_KMS_BACKUP = "load-project-kms-backup" + LOAD_PROJECT_KMS_BACKUP = "load-project-kms-backup", + ORG_ADMIN_ACCESS_PROJECT = "org-admin-accessed-project" } interface UserActorMetadata { @@ -1245,6 +1246,16 @@ interface LoadProjectKmsBackupEvent { metadata: Record; // no metadata yet } +interface OrgAdminAccessProjectEvent { + type: EventType.ORG_ADMIN_ACCESS_PROJECT; + metadata: { + userId: string; + username: string; + email: string; + projectId: string; + }; // no metadata yet +} + export type Event = | GetSecretsEvent | GetSecretEvent @@ -1354,4 +1365,5 @@ export type Event = | GetKmsEvent | UpdateProjectKmsEvent | GetProjectKmsBackupEvent - | LoadProjectKmsBackupEvent; + | LoadProjectKmsBackupEvent + | OrgAdminAccessProjectEvent; diff --git a/backend/src/lib/knex/index.ts b/backend/src/lib/knex/index.ts index c01d146ec..dcab16218 100644 --- a/backend/src/lib/knex/index.ts +++ b/backend/src/lib/knex/index.ts @@ -19,23 +19,43 @@ export const withTransaction = (db: Knex, dal: K) => ({ export type TFindFilter = Partial & { $in?: Partial<{ [k in keyof R]: R[k][] }>; + $search?: Partial<{ [k in keyof R]: R[k] }>; }; export const buildFindFilter = - ({ $in, ...filter }: TFindFilter) => + ({ $in, $search, ...filter }: TFindFilter) => (bd: Knex.QueryBuilder) => { void bd.where(filter); if ($in) { Object.entries($in).forEach(([key, val]) => { - void bd.whereIn(key as never, val as never); + if (val) { + void bd.whereIn(key as never, val as never); + } + }); + } + if ($search) { + Object.entries($search).forEach(([key, val]) => { + if (val) { + void bd.whereILike(key as never, val as never); + } }); } return bd; }; -export type TFindOpt = { +export type TFindReturn = Array< + Awaited[0] & + (TCount extends true + ? { + count: string; + } + : unknown) +>; + +export type TFindOpt = { limit?: number; offset?: number; sort?: Array<[keyof R, "asc" | "desc"] | [keyof R, "asc" | "desc", "first" | "last"]>; + count?: TCount; tx?: Knex; }; @@ -66,18 +86,22 @@ export const ormify = (db: Kne throw new DatabaseError({ error, name: "Find one" }); } }, - find: async ( + find: async ( filter: TFindFilter, - { offset, limit, sort, tx }: TFindOpt = {} + { offset, limit, sort, count, tx }: TFindOpt = {} ) => { try { const query = (tx || db.replicaNode())(tableName).where(buildFindFilter(filter)); + if (count) { + void query.select(db.raw("COUNT(*) OVER() AS count")); + void query.select("*"); + } if (limit) void query.limit(limit); if (offset) void query.offset(offset); if (sort) { void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls }))); } - const res = await query; + const res = (await query) as TFindReturn; return res; } catch (error) { throw new DatabaseError({ error, name: "Find one" }); diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index e8f80f020..a6ef33a72 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -129,6 +129,7 @@ import { orgDALFactory } from "@app/services/org/org-dal"; import { orgRoleDALFactory } from "@app/services/org/org-role-dal"; import { orgRoleServiceFactory } from "@app/services/org/org-role-service"; import { orgServiceFactory } from "@app/services/org/org-service"; +import { orgAdminServiceFactory } from "@app/services/org-admin/org-admin-service"; import { orgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; import { projectDALFactory } from "@app/services/project/project-dal"; import { projectQueueFactory } from "@app/services/project/project-queue"; @@ -498,6 +499,16 @@ export const registerRoutes = async ( keyStore, licenseService }); + const orgAdminService = orgAdminServiceFactory({ + projectDAL, + permissionService, + projectUserMembershipRoleDAL, + userDAL, + projectBotDAL, + projectKeyDAL, + projectMembershipDAL + }); + const rateLimitService = rateLimitServiceFactory({ rateLimitDAL, licenseService @@ -1113,7 +1124,8 @@ export const registerRoutes = async ( identityProjectAdditionalPrivilege: identityProjectAdditionalPrivilegeService, secretSharing: secretSharingService, userEngagement: userEngagementService, - externalKms: externalKmsService + externalKms: externalKmsService, + orgAdmin: orgAdminService }); const cronJobs: CronJob[] = []; diff --git a/backend/src/server/routes/v1/index.ts b/backend/src/server/routes/v1/index.ts index 43ce44eaa..bb6b9b57b 100644 --- a/backend/src/server/routes/v1/index.ts +++ b/backend/src/server/routes/v1/index.ts @@ -15,6 +15,7 @@ import { registerIdentityUaRouter } from "./identity-universal-auth-router"; import { registerIntegrationAuthRouter } from "./integration-auth-router"; import { registerIntegrationRouter } from "./integration-router"; import { registerInviteOrgRouter } from "./invite-org-router"; +import { registerOrgAdminRouter } from "./org-admin-router"; import { registerOrgRouter } from "./organization-router"; import { registerPasswordRouter } from "./password-router"; import { registerProjectEnvRouter } from "./project-env-router"; @@ -50,6 +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(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 new file mode 100644 index 000000000..71f636452 --- /dev/null +++ b/backend/src/server/routes/v1/org-admin-router.ts @@ -0,0 +1,90 @@ +import { z } from "zod"; + +import { ProjectMembershipsSchema } from "@app/db/schemas"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { readLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; + +import { SanitizedProjectSchema } from "../sanitizedSchemas"; + +export const registerOrgAdminRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "GET", + url: "/projects", + config: { + rateLimit: readLimit + }, + schema: { + querystring: z.object({ + search: z.string().optional(), + offset: z.coerce.number().default(0), + limit: z.coerce.number().max(100).default(50) + }), + response: { + 200: z.object({ + projects: SanitizedProjectSchema.array(), + count: z.coerce.number() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { projects, count } = await server.services.orgAdmin.listOrgProjects({ + limit: req.query.limit, + offset: req.query.offset, + search: req.query.search, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + actorId: req.permission.id, + actor: req.permission.type + }); + return { projects, count }; + } + }); + + server.route({ + method: "POST", + url: "/projects/:projectId/access", + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + projectId: z.string() + }), + response: { + 200: z.object({ + membership: ProjectMembershipsSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { membership, isExistingMember } = await server.services.orgAdmin.accessProject({ + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + actorId: req.permission.id, + actor: req.permission.type, + projectId: req.params.projectId + }); + if (!isExistingMember && req.auth.authMode === AuthMode.JWT) { + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: req.params.projectId, + event: { + type: EventType.ORG_ADMIN_ACCESS_PROJECT, + metadata: { + projectId: req.params.projectId, + username: req.auth.user.username, + email: req.auth.user.email || "", + userId: req.auth.userId + } + } + }); + } + + return { membership }; + } + }); +}; diff --git a/backend/src/services/org-admin/org-admin-dal.ts b/backend/src/services/org-admin/org-admin-dal.ts new file mode 100644 index 000000000..da2ccf2f6 --- /dev/null +++ b/backend/src/services/org-admin/org-admin-dal.ts @@ -0,0 +1,5 @@ +export type TOrgAdminDALFactory = ReturnType; + +export const orgAdminDALFactory = () => { + return {}; +}; diff --git a/backend/src/services/org-admin/org-admin-service.ts b/backend/src/services/org-admin/org-admin-service.ts new file mode 100644 index 000000000..89b516dcd --- /dev/null +++ b/backend/src/services/org-admin/org-admin-service.ts @@ -0,0 +1,178 @@ +import { OrgMembershipRole, ProjectMembershipRole, ProjectVersion, SecretKeyEncoding } from "@app/db/schemas"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; +import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; + +import { TProjectDALFactory } from "../project/project-dal"; +import { assignWorkspaceKeysToMembers } from "../project/project-fns"; +import { TProjectBotDALFactory } from "../project-bot/project-bot-dal"; +import { TProjectKeyDALFactory } from "../project-key/project-key-dal"; +import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; +import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal"; +import { TUserDALFactory } from "../user/user-dal"; +import { TAccessProjectDTO, TListOrgProjectsDTO } from "./org-admin-types"; + +type TOrgAdminServiceFactoryDep = { + permissionService: Pick; + projectDAL: Pick; + projectMembershipDAL: Pick; + projectKeyDAL: Pick; + projectBotDAL: Pick; + userDAL: Pick; + projectUserMembershipRoleDAL: Pick; +}; + +export type TOrgAdminServiceFactory = ReturnType; + +export const orgAdminServiceFactory = ({ + permissionService, + projectDAL, + projectMembershipDAL, + projectKeyDAL, + projectBotDAL, + userDAL, + projectUserMembershipRoleDAL +}: TOrgAdminServiceFactoryDep) => { + const listOrgProjects = async ({ + actor, + limit, + actorId, + offset, + search, + actorOrgId, + actorAuthMethod + }: TListOrgProjectsDTO) => { + const { membership } = await permissionService.getOrgPermission( + actor, + actorId, + actorOrgId, + actorAuthMethod, + actorOrgId + ); + const isAdmin = membership.role === OrgMembershipRole.Admin; + if (!isAdmin) throw new UnauthorizedError({ message: "Admin only operation" }); + const projects = await projectDAL.find( + { + orgId: actorOrgId, + $search: { + name: search ? `%${search}%` : undefined + } + }, + { offset, limit, sort: [["name", "asc"]], count: true } + ); + + const count = projects?.[0]?.count ? parseInt(projects?.[0]?.count, 10) : 0; + return { projects, count }; + }; + + const accessProject = async ({ actor, actorId, actorOrgId, actorAuthMethod, projectId }: TAccessProjectDTO) => { + const { membership } = await permissionService.getOrgPermission( + actor, + actorId, + actorOrgId, + actorAuthMethod, + actorOrgId + ); + const isAdmin = membership.role === OrgMembershipRole.Admin; + if (!isAdmin) throw new UnauthorizedError({ message: "Admin only operation" }); + + const project = await projectDAL.findById(projectId); + if (!project) throw new BadRequestError({ message: "Project not found" }); + + if (project.version === ProjectVersion.V1) { + throw new BadRequestError({ message: "Please upgrade your project on your dashboard" }); + } + + // check already there exist a membership if there return it + const projectMembership = await projectMembershipDAL.findOne({ + projectId, + userId: actorId + }); + if (projectMembership) { + // reset and make the user admin + await projectMembershipDAL.transaction(async (tx) => { + await projectUserMembershipRoleDAL.delete({ projectMembershipId: projectMembership.id }, tx); + await projectUserMembershipRoleDAL.create( + { + projectMembershipId: projectMembership.id, + role: ProjectMembershipRole.Admin + }, + tx + ); + }); + return { isExistingMember: true, membership: projectMembership }; + } + + // missing membership thus add admin back as admin to project + const ghostUser = await projectDAL.findProjectGhostUser(projectId); + if (!ghostUser) { + throw new BadRequestError({ + message: "Failed to find sudo user" + }); + } + + const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, projectId); + if (!ghostUserLatestKey) { + throw new BadRequestError({ + message: "Failed to find sudo user latest key" + }); + } + + const bot = await projectBotDAL.findOne({ projectId }); + if (!bot) { + throw new BadRequestError({ + message: "Failed to find bot" + }); + } + + const botPrivateKey = infisicalSymmetricDecrypt({ + keyEncoding: bot.keyEncoding as SecretKeyEncoding, + iv: bot.iv, + tag: bot.tag, + ciphertext: bot.encryptedPrivateKey + }); + + const userEncryptionKey = await userDAL.findUserEncKeyByUserId(actorId); + if (!userEncryptionKey) throw new BadRequestError({ message: "user encryption key not found" }); + const [newWsMember] = assignWorkspaceKeysToMembers({ + decryptKey: ghostUserLatestKey, + userPrivateKey: botPrivateKey, + members: [ + { + orgMembershipId: membership.id, + projectMembershipRole: ProjectMembershipRole.Admin, + userPublicKey: userEncryptionKey.publicKey + } + ] + }); + + const updatedMembership = await projectMembershipDAL.transaction(async (tx) => { + const newProjectMembership = await projectMembershipDAL.create( + { + projectId, + userId: actorId + }, + tx + ); + await projectUserMembershipRoleDAL.create( + { projectMembershipId: newProjectMembership.id, role: ProjectMembershipRole.Admin }, + tx + ); + + await projectKeyDAL.create( + { + encryptedKey: newWsMember.workspaceEncryptedKey, + nonce: newWsMember.workspaceEncryptedNonce, + senderId: ghostUser.id, + receiverId: actorId, + projectId + }, + tx + ); + return projectMembership; + }); + return { isExistingMember: false, membership: updatedMembership }; + }; + + return { listOrgProjects, accessProject }; +}; diff --git a/backend/src/services/org-admin/org-admin-types.ts b/backend/src/services/org-admin/org-admin-types.ts new file mode 100644 index 000000000..85669fc56 --- /dev/null +++ b/backend/src/services/org-admin/org-admin-types.ts @@ -0,0 +1,11 @@ +import { TOrgPermission } from "@app/lib/types"; + +export type TListOrgProjectsDTO = { + limit?: number; + offset?: number; + search?: string; +} & Omit; + +export type TAccessProjectDTO = { + projectId: string; +} & Omit; diff --git a/backend/src/services/project-membership/project-membership-service.ts b/backend/src/services/project-membership/project-membership-service.ts index a03aec934..8f87e8d55 100644 --- a/backend/src/services/project-membership/project-membership-service.ts +++ b/backend/src/services/project-membership/project-membership-service.ts @@ -256,7 +256,6 @@ export const projectMembershipServiceFactory = ({ } const bot = await projectBotDAL.findOne({ projectId }); - if (!bot) { throw new BadRequestError({ message: "Failed to find bot" diff --git a/frontend/src/components/v2/Pagination/Pagination.tsx b/frontend/src/components/v2/Pagination/Pagination.tsx index c8afb389b..f0ba950c1 100644 --- a/frontend/src/components/v2/Pagination/Pagination.tsx +++ b/frontend/src/components/v2/Pagination/Pagination.tsx @@ -50,7 +50,7 @@ export const Pagination = ({ >
- {(page - 1) * perPage} - {(page - 1) * perPage + perPage} of {count} + {(page - 1) * perPage} - {Math.min((page - 1) * perPage + perPage, count)} of {count}
diff --git a/frontend/src/hooks/api/auditLogs/constants.tsx b/frontend/src/hooks/api/auditLogs/constants.tsx index 082bff02c..819c36c20 100644 --- a/frontend/src/hooks/api/auditLogs/constants.tsx +++ b/frontend/src/hooks/api/auditLogs/constants.tsx @@ -56,7 +56,8 @@ export const eventToNameMap: { [K in EventType]: string } = { [EventType.GET_CERT]: "Get certificate", [EventType.DELETE_CERT]: "Delete certificate", [EventType.REVOKE_CERT]: "Revoke certificate", - [EventType.GET_CERT_BODY]: "Get certificate body" + [EventType.GET_CERT_BODY]: "Get certificate body", + [EventType.ORG_ADMIN_ACCESS_PROJECT]: "Org admin accessed project" }; export const userAgentTTypeoNameMap: { [K in UserAgentType]: string } = { diff --git a/frontend/src/hooks/api/auditLogs/enums.tsx b/frontend/src/hooks/api/auditLogs/enums.tsx index ad49998c5..94963f640 100644 --- a/frontend/src/hooks/api/auditLogs/enums.tsx +++ b/frontend/src/hooks/api/auditLogs/enums.tsx @@ -70,5 +70,6 @@ export enum EventType { GET_CERT = "get-cert", DELETE_CERT = "delete-cert", REVOKE_CERT = "revoke-cert", - GET_CERT_BODY = "get-cert-body" + GET_CERT_BODY = "get-cert-body", + ORG_ADMIN_ACCESS_PROJECT = "org-admin-accessed-project" } diff --git a/frontend/src/hooks/api/auditLogs/types.tsx b/frontend/src/hooks/api/auditLogs/types.tsx index cdc973ed9..1d80d6128 100644 --- a/frontend/src/hooks/api/auditLogs/types.tsx +++ b/frontend/src/hooks/api/auditLogs/types.tsx @@ -579,6 +579,16 @@ interface GetCertBody { }; } +interface OrgAdminAccessProjectEvent { + type: EventType.ORG_ADMIN_ACCESS_PROJECT; + metadata: { + userId: string; + username: string; + email: string; + projectId: string; + }; // no metadata yet +} + export type Event = | GetSecretsEvent | GetSecretEvent @@ -635,7 +645,8 @@ export type Event = | GetCert | DeleteCert | RevokeCert - | GetCertBody; + | GetCertBody + | OrgAdminAccessProjectEvent; export type AuditLog = { id: string; diff --git a/frontend/src/hooks/api/index.tsx b/frontend/src/hooks/api/index.tsx index ea3e7f560..5cbd1fb27 100644 --- a/frontend/src/hooks/api/index.tsx +++ b/frontend/src/hooks/api/index.tsx @@ -19,6 +19,7 @@ export * from "./keys"; export * from "./kms"; export * from "./ldapConfig"; export * from "./oidcConfig"; +export * from "./orgAdmin"; export * from "./organization"; export * from "./projectUserAdditionalPrivilege"; export * from "./rateLimit"; diff --git a/frontend/src/hooks/api/orgAdmin/index.tsx b/frontend/src/hooks/api/orgAdmin/index.tsx new file mode 100644 index 000000000..57eed413a --- /dev/null +++ b/frontend/src/hooks/api/orgAdmin/index.tsx @@ -0,0 +1,2 @@ +export { useOrgAdminAccessProject } from "./mutation"; +export { useOrgAdminGetProjects } from "./queries"; diff --git a/frontend/src/hooks/api/orgAdmin/mutation.tsx b/frontend/src/hooks/api/orgAdmin/mutation.tsx new file mode 100644 index 000000000..3b2c69efa --- /dev/null +++ b/frontend/src/hooks/api/orgAdmin/mutation.tsx @@ -0,0 +1,13 @@ +import { useMutation } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { TOrgAdminAccessProjectDTO } from "./types"; + +export const useOrgAdminAccessProject = () => + useMutation({ + mutationFn: async ({ projectId }: TOrgAdminAccessProjectDTO) => { + const { data } = await apiRequest.post(`/api/v1/org-admin/projects/${projectId}/access`); + return data; + } + }); diff --git a/frontend/src/hooks/api/orgAdmin/queries.tsx b/frontend/src/hooks/api/orgAdmin/queries.tsx new file mode 100644 index 000000000..23826a9a4 --- /dev/null +++ b/frontend/src/hooks/api/orgAdmin/queries.tsx @@ -0,0 +1,30 @@ +import { useQuery } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { Workspace } from "../types"; +import { TOrgAdminGetProjectsDTO } from "./types"; + +export const orgAdminQueryKeys = { + getProjects: (filter: TOrgAdminGetProjectsDTO) => ["org-admin-projects", filter] as const +}; + +export const useOrgAdminGetProjects = ({ search, offset, limit = 50 }: TOrgAdminGetProjectsDTO) => { + return useQuery({ + queryKey: orgAdminQueryKeys.getProjects({ search, offset, limit }), + queryFn: async () => { + const { data } = await apiRequest.get<{ projects: Workspace[]; count: number }>( + "/api/v1/org-admin/projects", + { + params: { + limit, + offset, + search + } + } + ); + + return data; + } + }); +}; diff --git a/frontend/src/hooks/api/orgAdmin/types.ts b/frontend/src/hooks/api/orgAdmin/types.ts new file mode 100644 index 000000000..87626a466 --- /dev/null +++ b/frontend/src/hooks/api/orgAdmin/types.ts @@ -0,0 +1,9 @@ +export type TOrgAdminGetProjectsDTO = { + limit?: number; + offset?: number; + search?: string; +}; + +export type TOrgAdminAccessProjectDTO = { + projectId: string; +}; diff --git a/frontend/src/hooks/api/workspace/queries.tsx b/frontend/src/hooks/api/workspace/queries.tsx index f3ffaf86e..57087feb9 100644 --- a/frontend/src/hooks/api/workspace/queries.tsx +++ b/frontend/src/hooks/api/workspace/queries.tsx @@ -317,6 +317,7 @@ export const useDeleteWorkspace = () => { }, onSuccess: () => { queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace); + queryClient.invalidateQueries(["org-admin-projects"]); } }); }; diff --git a/frontend/src/hooks/api/workspace/types.ts b/frontend/src/hooks/api/workspace/types.ts index 783b15f1e..51bb08e3d 100644 --- a/frontend/src/hooks/api/workspace/types.ts +++ b/frontend/src/hooks/api/workspace/types.ts @@ -21,6 +21,7 @@ export type Workspace = { pitVersionLimit: number; auditLogsRetentionDays: number; slug: string; + createdAt: string; }; export type WorkspaceEnv = { diff --git a/frontend/src/hooks/usePopUp.tsx b/frontend/src/hooks/usePopUp.tsx index e9d8257e3..28780db9e 100644 --- a/frontend/src/hooks/usePopUp.tsx +++ b/frontend/src/hooks/usePopUp.tsx @@ -13,7 +13,7 @@ interface UsePopUpProps { export type UsePopUpState | UsePopUpProps[]> = { [P in T extends UsePopUpProps[] ? T[number]["name"] : T[number]]: { isOpen: boolean; - data?: unknown; + data?: any; }; }; diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index 87d4c8458..0ec57ba0d 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -59,6 +59,7 @@ import { OrgPermissionActions, OrgPermissionSubjects, useOrganization, + useOrgPermission, useSubscription, useUser, useWorkspace @@ -131,6 +132,8 @@ 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(); @@ -748,6 +751,18 @@ export const AppLayout = ({ children }: LayoutProps) => { )} + {isOrgAdmin && ( + + + + Admin + + + + )} + + {t("common.head-title", { title: t("settings.org.title") })} + + + + + ); +} + +SettingsOrg.requireAuth = true; diff --git a/frontend/src/views/OrgAdminPage/OrgAdminPage.tsx b/frontend/src/views/OrgAdminPage/OrgAdminPage.tsx new file mode 100644 index 000000000..eadd732d3 --- /dev/null +++ b/frontend/src/views/OrgAdminPage/OrgAdminPage.tsx @@ -0,0 +1,30 @@ +import { useState } from "react"; + +import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; + +import { OrgAdminProjects } from "./components/OrgAdminProjects"; + +enum TabSections { + Projects = "projects" +} + +export const OrgAdminPage = () => { + const [activeTab, setActiveTab] = useState(TabSections.Projects); + return ( +
+
+
+

Admin Panel

+
+ setActiveTab(el as TabSections)}> + + Projects + + + + + +
+
+ ); +}; diff --git a/frontend/src/views/OrgAdminPage/components/OrgAdminProjects/OrgAdminProjects.tsx b/frontend/src/views/OrgAdminPage/components/OrgAdminProjects/OrgAdminProjects.tsx new file mode 100644 index 000000000..d2b66ad18 --- /dev/null +++ b/frontend/src/views/OrgAdminPage/components/OrgAdminProjects/OrgAdminProjects.tsx @@ -0,0 +1,155 @@ +import { useState } from "react"; +import { useRouter } from "next/router"; +import { faEllipsis, faMagnifyingGlass, faSignIn } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { format } from "date-fns"; +import { motion } from "framer-motion"; + +import { createNotification } from "@app/components/notifications"; +import { + Button, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + EmptyState, + Input, + Pagination, + Spinner, + Table, + TableContainer, + TableSkeleton, + TBody, + Td, + Th, + THead, + Tr} from "@app/components/v2"; +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(); + + 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 handleAccessProject = async (projectId: string) => { + try { + await orgAdminAccessProject.mutateAsync({ + projectId + }); + await router.push({ + pathname: "/project/[projectId]/secrets/overview", + query: { + projectId + } + }); + } catch { + createNotification({ + text: "Failed to access project", + type: "error" + }); + } + }; + + 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 && } +
+
+
+
+ ); +}; diff --git a/frontend/src/views/OrgAdminPage/components/OrgAdminProjects/index.tsx b/frontend/src/views/OrgAdminPage/components/OrgAdminProjects/index.tsx new file mode 100644 index 000000000..b331589a4 --- /dev/null +++ b/frontend/src/views/OrgAdminPage/components/OrgAdminProjects/index.tsx @@ -0,0 +1 @@ +export { OrgAdminProjects } from "./OrgAdminProjects"; diff --git a/frontend/src/views/OrgAdminPage/index.tsx b/frontend/src/views/OrgAdminPage/index.tsx new file mode 100644 index 000000000..1fe7b5541 --- /dev/null +++ b/frontend/src/views/OrgAdminPage/index.tsx @@ -0,0 +1 @@ +export { OrgAdminPage } from "./OrgAdminPage"; diff --git a/frontend/src/views/Project/AuditLogsPage/components/LogsTableRow.tsx b/frontend/src/views/Project/AuditLogsPage/components/LogsTableRow.tsx index 5eada2663..668b5e5c6 100644 --- a/frontend/src/views/Project/AuditLogsPage/components/LogsTableRow.tsx +++ b/frontend/src/views/Project/AuditLogsPage/components/LogsTableRow.tsx @@ -317,6 +317,12 @@ export const LogsTableRow = ({ auditLog }: Props) => { })} ); + case EventType.ORG_ADMIN_ACCESS_PROJECT: + return ( + +

{`Email: ${event.metadata.email}`}

+ + ); case EventType.CREATE_CA: case EventType.GET_CA: case EventType.UPDATE_CA: From cf5260b38352a6ef86a518da0738e4cdfec7449a Mon Sep 17 00:00:00 2001 From: = Date: Fri, 2 Aug 2024 19:28:31 +0530 Subject: [PATCH 2/7] feat: minor bug fix on access operation --- backend/src/server/routes/v1/org-admin-router.ts | 4 ++-- backend/src/services/org-admin/org-admin-service.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/src/server/routes/v1/org-admin-router.ts b/backend/src/server/routes/v1/org-admin-router.ts index 71f636452..5339d91b5 100644 --- a/backend/src/server/routes/v1/org-admin-router.ts +++ b/backend/src/server/routes/v1/org-admin-router.ts @@ -61,14 +61,14 @@ export const registerOrgAdminRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const { membership, isExistingMember } = await server.services.orgAdmin.accessProject({ + const { membership } = await server.services.orgAdmin.accessProject({ actorOrgId: req.permission.orgId, actorAuthMethod: req.permission.authMethod, actorId: req.permission.id, actor: req.permission.type, projectId: req.params.projectId }); - if (!isExistingMember && req.auth.authMode === AuthMode.JWT) { + if (req.auth.authMode === AuthMode.JWT) { await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, projectId: req.params.projectId, diff --git a/backend/src/services/org-admin/org-admin-service.ts b/backend/src/services/org-admin/org-admin-service.ts index 89b516dcd..aef1342b4 100644 --- a/backend/src/services/org-admin/org-admin-service.ts +++ b/backend/src/services/org-admin/org-admin-service.ts @@ -169,7 +169,7 @@ export const orgAdminServiceFactory = ({ }, tx ); - return projectMembership; + return newProjectMembership; }); return { isExistingMember: false, membership: updatedMembership }; }; From b97bbe5beb8d627ca2cc9a86452c19da680ed1b4 Mon Sep 17 00:00:00 2001 From: = Date: Fri, 2 Aug 2024 19:28:52 +0530 Subject: [PATCH 3/7] feat: text change in sidebar --- frontend/src/layouts/AppLayout/AppLayout.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index 0ec57ba0d..8a794f901 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -479,7 +479,7 @@ export const AppLayout = ({ children }: LayoutProps) => { {user?.superAdmin && ( - Admin Panel + Server Admin Panel )} @@ -758,7 +758,7 @@ export const AppLayout = ({ children }: LayoutProps) => { isSelected={router.asPath === `/org/${currentOrg?.id}/admin`} icon="system-outline-109-slider-toggle-settings" > - Admin + Admin Panel
From b4a2a477d34190cba3b3c9eae0a5eb6e1ceda67f Mon Sep 17 00:00:00 2001 From: = Date: Sat, 3 Aug 2024 14:55:30 +0530 Subject: [PATCH 4/7] 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 + } +); From 711a4179ce23ee8abd104e923cf1dcbfe7a266a7 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Sat, 3 Aug 2024 07:52:35 -0400 Subject: [PATCH 5/7] rename admin panel --- frontend/src/layouts/AppLayout/AppLayout.tsx | 2 +- frontend/src/views/OrgAdminPage/OrgAdminPage.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index 0568a5821..6a65e32e8 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -482,7 +482,7 @@ export const AppLayout = ({ children }: LayoutProps) => { )} - Admin Panel + Organization Admin Console
diff --git a/frontend/src/views/OrgAdminPage/OrgAdminPage.tsx b/frontend/src/views/OrgAdminPage/OrgAdminPage.tsx index eadd732d3..406b65c69 100644 --- a/frontend/src/views/OrgAdminPage/OrgAdminPage.tsx +++ b/frontend/src/views/OrgAdminPage/OrgAdminPage.tsx @@ -14,7 +14,7 @@ export const OrgAdminPage = () => {
-

Admin Panel

+

Organization Admin Console

setActiveTab(el as TabSections)}> From e4a21379911395e85f5739ed0d8243c2a4d06b60 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Mon, 5 Aug 2024 10:01:15 -0400 Subject: [PATCH 6/7] update permission action name for org admin console --- backend/src/ee/services/permission/org-permission.ts | 4 ++-- backend/src/services/org-admin/org-admin-service.ts | 4 ++-- frontend/src/context/OrgPermissionContext/types.ts | 2 +- .../Org/RolePage/components/OrgRoleModifySection.utils.ts | 2 +- .../OrgPermissionAdminConsoleRow.tsx | 8 ++++---- .../components/OrgAdminProjects/OrgAdminProjects.tsx | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/backend/src/ee/services/permission/org-permission.ts b/backend/src/ee/services/permission/org-permission.ts index c07107912..9a5afe024 100644 --- a/backend/src/ee/services/permission/org-permission.ts +++ b/backend/src/ee/services/permission/org-permission.ts @@ -10,7 +10,7 @@ export enum OrgPermissionActions { } export enum OrgPermissionAdminConsoleAction { - GrantAccessProjects = "grant-access-projects" + AccessAllProjects = "access-all-projects" } export enum OrgPermissionSubjects { @@ -113,7 +113,7 @@ const buildAdminPermission = () => { can(OrgPermissionActions.Edit, OrgPermissionSubjects.Kms); can(OrgPermissionActions.Delete, OrgPermissionSubjects.Kms); - can(OrgPermissionAdminConsoleAction.GrantAccessProjects, OrgPermissionSubjects.AdminConsole); + can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole); return build({ conditionsMatcher }); }; diff --git a/backend/src/services/org-admin/org-admin-service.ts b/backend/src/services/org-admin/org-admin-service.ts index cfdda663d..4759db309 100644 --- a/backend/src/services/org-admin/org-admin-service.ts +++ b/backend/src/services/org-admin/org-admin-service.ts @@ -53,7 +53,7 @@ export const orgAdminServiceFactory = ({ actorOrgId ); ForbiddenError.from(permission).throwUnlessCan( - OrgPermissionAdminConsoleAction.GrantAccessProjects, + OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole ); const projects = await projectDAL.find( @@ -85,7 +85,7 @@ export const orgAdminServiceFactory = ({ actorOrgId ); ForbiddenError.from(permission).throwUnlessCan( - OrgPermissionAdminConsoleAction.GrantAccessProjects, + OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole ); diff --git a/frontend/src/context/OrgPermissionContext/types.ts b/frontend/src/context/OrgPermissionContext/types.ts index a36aaac69..9d846a065 100644 --- a/frontend/src/context/OrgPermissionContext/types.ts +++ b/frontend/src/context/OrgPermissionContext/types.ts @@ -25,7 +25,7 @@ export enum OrgPermissionSubjects { } export enum OrgPermissionAdminConsoleAction { - GrantAccessProjects = "grant-access-projects" + AccessAllProjects = "access-all-projects" } export type OrgPermissionSet = diff --git a/frontend/src/views/Org/RolePage/components/OrgRoleModifySection.utils.ts b/frontend/src/views/Org/RolePage/components/OrgRoleModifySection.utils.ts index ba5308cd9..2d83bf705 100644 --- a/frontend/src/views/Org/RolePage/components/OrgRoleModifySection.utils.ts +++ b/frontend/src/views/Org/RolePage/components/OrgRoleModifySection.utils.ts @@ -14,7 +14,7 @@ const generalPermissionSchema = z const adminConsolePermissionSchmea = z .object({ - "grant-access-projects": z.boolean().optional() + "access-all-projects": z.boolean().optional() }) .optional(); diff --git a/frontend/src/views/Org/RolePage/components/RolePermissionsSection/OrgPermissionAdminConsoleRow.tsx b/frontend/src/views/Org/RolePage/components/RolePermissionsSection/OrgPermissionAdminConsoleRow.tsx index 21d58aabb..c657aa8a4 100644 --- a/frontend/src/views/Org/RolePage/components/RolePermissionsSection/OrgPermissionAdminConsoleRow.tsx +++ b/frontend/src/views/Org/RolePage/components/RolePermissionsSection/OrgPermissionAdminConsoleRow.tsx @@ -20,7 +20,7 @@ enum Permission { } const PERMISSION_ACTIONS = [ - { action: "grant-access-projects", label: "Grant access projects" } + { action: "access-all-projects", label: "Access all organization projects" } ] as const; export const OrgPermissionAdminConsoleRow = ({ isEditable, control, setValue }: Props) => { @@ -33,7 +33,7 @@ export const OrgPermissionAdminConsoleRow = ({ isEditable, control, setValue }: }); const selectedPermissionCategory = useMemo(() => { - if (rule?.["grant-access-projects"]) { + if (rule?.["access-all-projects"]) { return Permission.Custom; } return Permission.NoAccess; @@ -63,7 +63,7 @@ export const OrgPermissionAdminConsoleRow = ({ isEditable, control, setValue }: if (val === Permission.NoAccess) { setValue( "permissions.admin-console", - { "grant-access-projects": false }, + { "access-all-projects": false }, { shouldDirty: true } ); } @@ -78,7 +78,7 @@ export const OrgPermissionAdminConsoleRow = ({ isEditable, control, setValue }: - Admin Console + Organization Admin Console