diff --git a/backend/src/ee/routes/v1/group-router.ts b/backend/src/ee/routes/v1/group-router.ts index 766ac3d93..84cffd890 100644 --- a/backend/src/ee/routes/v1/group-router.ts +++ b/backend/src/ee/routes/v1/group-router.ts @@ -2,6 +2,7 @@ import slugify from "@sindresorhus/slugify"; import { z } from "zod"; import { GroupsSchema, OrgMembershipRole, UsersSchema } from "@app/db/schemas"; +import { GROUPS } from "@app/lib/api-docs"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -12,8 +13,7 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => { onRequest: verifyAuth([AuthMode.JWT]), schema: { body: z.object({ - // TODO: update on frontend to not send organizationId - name: z.string().trim().min(1), + name: z.string().trim().min(1).describe(GROUPS.CREATE.name), slug: z .string() .min(5) @@ -21,8 +21,9 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => { .refine((v) => slugify(v) === v, { message: "Slug must be a valid slug" }) - .optional(), - role: z.string().trim().min(1).default(OrgMembershipRole.NoAccess) + .optional() + .describe(GROUPS.CREATE.slug), + role: z.string().trim().min(1).default(OrgMembershipRole.NoAccess).describe(GROUPS.CREATE.role) }), response: { 200: GroupsSchema @@ -47,19 +48,20 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => { onRequest: verifyAuth([AuthMode.JWT]), schema: { params: z.object({ - currentSlug: z.string().trim() + currentSlug: z.string().trim().describe(GROUPS.UPDATE.currentSlug) }), body: z .object({ - name: z.string().trim().min(1), + name: z.string().trim().min(1).describe(GROUPS.UPDATE.name), slug: z .string() .min(5) .max(36) .refine((v) => slugify(v) === v, { message: "Slug must be a valid slug" - }), - role: z.string().trim().min(1) + }) + .describe(GROUPS.UPDATE.slug), + role: z.string().trim().min(1).describe(GROUPS.UPDATE.role) }) .partial(), response: { @@ -81,12 +83,12 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => { }); server.route({ - url: "/:groupSlug", + url: "/:slug", method: "DELETE", onRequest: verifyAuth([AuthMode.JWT]), schema: { params: z.object({ - groupSlug: z.string().trim() + slug: z.string().trim().describe(GROUPS.DELETE.slug) }), response: { 200: GroupsSchema @@ -94,7 +96,7 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => { }, handler: async (req) => { const group = await server.services.group.deleteGroup({ - groupSlug: req.params.groupSlug, + groupSlug: req.params.slug, actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, @@ -111,7 +113,7 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => { onRequest: verifyAuth([AuthMode.JWT]), schema: { params: z.object({ - slug: z.string().trim() + slug: z.string().trim().describe(GROUPS.LIST_USERS.slug) }), response: { 200: UsersSchema.pick({ @@ -147,8 +149,8 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => { onRequest: verifyAuth([AuthMode.JWT]), schema: { params: z.object({ - slug: z.string().trim(), - username: z.string().trim() + slug: z.string().trim().describe(GROUPS.ADD_USER.slug), + username: z.string().trim().describe(GROUPS.ADD_USER.username) }), response: { 200: UsersSchema.pick({ @@ -180,8 +182,8 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => { onRequest: verifyAuth([AuthMode.JWT]), schema: { params: z.object({ - slug: z.string().trim(), - username: z.string().trim() + slug: z.string().trim().describe(GROUPS.DELETE_USER.slug), + username: z.string().trim().describe(GROUPS.DELETE_USER.username) }), response: { 200: UsersSchema.pick({ diff --git a/backend/src/ee/services/group/group-service.ts b/backend/src/ee/services/group/group-service.ts index 920c6d549..2b3e66e8d 100644 --- a/backend/src/ee/services/group/group-service.ts +++ b/backend/src/ee/services/group/group-service.ts @@ -30,7 +30,7 @@ import { TUserGroupMembershipDALFactory } from "./user-group-membership-dal"; type TGroupServiceFactoryDep = { userDAL: Pick; groupDAL: Pick; - groupProjectDAL: TGroupProjectDALFactory; + groupProjectDAL: Pick; orgDAL: Pick; userGroupMembershipDAL: Pick< TUserGroupMembershipDALFactory, diff --git a/backend/src/ee/services/permission/org-permission.ts b/backend/src/ee/services/permission/org-permission.ts index ec6410037..9fece040b 100644 --- a/backend/src/ee/services/permission/org-permission.ts +++ b/backend/src/ee/services/permission/org-permission.ts @@ -112,6 +112,7 @@ const buildMemberPermission = () => { can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace); can(OrgPermissionActions.Read, OrgPermissionSubjects.Member); can(OrgPermissionActions.Create, OrgPermissionSubjects.Member); + can(OrgPermissionActions.Read, OrgPermissionSubjects.Groups); can(OrgPermissionActions.Read, OrgPermissionSubjects.Role); can(OrgPermissionActions.Read, OrgPermissionSubjects.Settings); can(OrgPermissionActions.Read, OrgPermissionSubjects.Billing); diff --git a/backend/src/ee/services/permission/permission-dal.ts b/backend/src/ee/services/permission/permission-dal.ts index cd223399f..186f5ccca 100644 --- a/backend/src/ee/services/permission/permission-dal.ts +++ b/backend/src/ee/services/permission/permission-dal.ts @@ -145,41 +145,6 @@ export const permissionDALFactory = (db: TDbClient) => { ] }); - // const permission = sqlNestRelationships({ - // data: docs, // TODO: concat? - // key: "membershipId", - // parentMapper: ({ - // orgId, - // orgAuthEnforced, - // membershipId, - // membershipCreatedAt, - // membershipUpdatedAt, - // oldRoleField, - // // role - // }) => ({ - // orgId, - // orgAuthEnforced, - // userId, - // role: oldRoleField, - // // role, - // id: membershipId, - // projectId, - // createdAt: membershipCreatedAt, - // updatedAt: membershipUpdatedAt - // }), - // childrenMapper: [ - // { - // key: "id", - // label: "roles" as const, - // mapper: (data) => - // ProjectUserMembershipRolesSchema.extend({ - // permissions: z.unknown(), - // customRoleSlug: z.string().optional().nullable() - // }).parse(data) - // } - // ] - // }); - // when introducting cron mode change it here const activeRoles = permission?.[0]?.roles.filter( ({ isTemporary, temporaryAccessEndTime }) => diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 8736e80c6..e892233ad 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -1,3 +1,31 @@ +export const GROUPS = { + CREATE: { + name: "The name of the group to create.", + slug: "The slug of the group to create.", + role: "The role of the group to create." + }, + UPDATE: { + currentSlug: "The current slug of the group to update.", + name: "The new name of the group to update to.", + slug: "The new slug of the group to update to.", + role: "The new role of the group to update to." + }, + DELETE: { + slug: "The slug of the group to delete" + }, + LIST_USERS: { + slug: "The slug of the group to list users for" + }, + ADD_USER: { + slug: "The slug of the group to add the user to.", + username: "The username of the user to add to the group." + }, + DELETE_USER: { + slug: "The slug of the group to remove the user from.", + username: "The username of the user to remove from the group." + } +} as const; + export const IDENTITIES = { CREATE: { name: "The name of the identity to create.", @@ -79,6 +107,9 @@ export const ORGANIZATIONS = { }, GET_PROJECTS: { organizationId: "The ID of the organization to get projects from." + }, + LIST_GROUPS: { + organizationId: "The ID of the organization to list groups for." } } as const; @@ -141,6 +172,23 @@ export const PROJECTS = { }, ROLLBACK_TO_SNAPSHOT: { secretSnapshotId: "The ID of the snapshot to rollback to." + }, + ADD_GROUP_TO_PROJECT: { + projectSlug: "The slug of the project to add the group to.", + groupSlug: "The slug of the group to add to the project.", + role: "The role for the group to assume in the project." + }, + UPDATE_GROUP_IN_PROJECT: { + projectSlug: "The slug of the project to update the group in.", + groupSlug: "The slug of the group to update in the project.", + roles: "A list of roles to update the group to." + }, + REMOVE_GROUP_FROM_PROJECT: { + projectSlug: "The slug of the project to delete the group from.", + groupSlug: "The slug of the group to delete from the project." + }, + LIST_GROUPS_IN_PROJECT: { + projectSlug: "The slug of the project to list groups for." } } as const; diff --git a/backend/src/lib/types/index.ts b/backend/src/lib/types/index.ts index a586cfd3e..25a7e6465 100644 --- a/backend/src/lib/types/index.ts +++ b/backend/src/lib/types/index.ts @@ -15,6 +15,15 @@ export type TProjectPermission = { actorOrgId: string; }; +// same as TProjectPermission but with projectSlug requirement instead of projectId +export type TProjectSlugPermission = { + actor: ActorType; + actorId: string; + projectSlug: string; + actorAuthMethod: ActorAuthMethod; + actorOrgId: string; +}; + export type RequiredKeys = { [K in keyof T]-?: undefined extends T[K] ? never : K; }[keyof T]; diff --git a/backend/src/server/routes/v1/organization-router.ts b/backend/src/server/routes/v1/organization-router.ts index df6db62b7..28c4aec91 100644 --- a/backend/src/server/routes/v1/organization-router.ts +++ b/backend/src/server/routes/v1/organization-router.ts @@ -8,6 +8,7 @@ import { OrgRolesSchema, UsersSchema } from "@app/db/schemas"; +import { ORGANIZATIONS } from "@app/lib/api-docs"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -209,7 +210,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { url: "/:organizationId/groups", schema: { params: z.object({ - organizationId: z.string().trim() + organizationId: z.string().trim().describe(ORGANIZATIONS.LIST_GROUPS.organizationId) }), response: { 200: z.object({ @@ -232,7 +233,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { const groups = await server.services.org.getOrgGroups({ actor: req.permission.type, actorId: req.permission.id, - orgId: req.permission.orgId, + orgId: req.params.organizationId, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId }); diff --git a/backend/src/server/routes/v2/group-project-router.ts b/backend/src/server/routes/v2/group-project-router.ts index b9c3e548c..6d438c1ff 100644 --- a/backend/src/server/routes/v2/group-project-router.ts +++ b/backend/src/server/routes/v2/group-project-router.ts @@ -15,15 +15,26 @@ import { ProjectUserMembershipTemporaryMode } from "@app/services/project-member export const registerGroupProjectRouter = async (server: FastifyZodProvider) => { server.route({ method: "POST", - url: "/:projectId/group-memberships/:groupSlug", + url: "/:projectSlug/groups/:groupSlug", onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + description: "Add group to project", + security: [ + { + bearerAuth: [] + } + ], params: z.object({ - projectId: z.string().trim(), - groupSlug: z.string().trim() + projectSlug: z.string().trim().describe(PROJECTS.ADD_GROUP_TO_PROJECT.projectSlug), + groupSlug: z.string().trim().describe(PROJECTS.ADD_GROUP_TO_PROJECT.groupSlug) }), body: z.object({ - role: z.string().trim().min(1).default(ProjectMembershipRole.NoAccess) + role: z + .string() + .trim() + .min(1) + .default(ProjectMembershipRole.NoAccess) + .describe(PROJECTS.ADD_GROUP_TO_PROJECT.role) }), response: { 200: z.object({ @@ -32,13 +43,13 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) => } }, handler: async (req) => { - const groupMembership = await server.services.groupProject.createProjectGroup({ + const groupMembership = await server.services.groupProject.addGroupToProject({ actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, groupSlug: req.params.groupSlug, - projectId: req.params.projectId, + projectSlug: req.params.projectSlug, role: req.body.role }); return { groupMembership }; @@ -47,18 +58,18 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) => server.route({ method: "PATCH", - url: "/:projectId/group-memberships/:groupSlug", + url: "/:projectSlug/groups/:groupSlug", onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { - description: "Update project group memberships", + description: "Update group in project", security: [ { bearerAuth: [] } ], params: z.object({ - projectId: z.string().trim(), - groupSlug: z.string().trim() + projectSlug: z.string().trim().describe(PROJECTS.UPDATE_GROUP_IN_PROJECT.projectSlug), + groupSlug: z.string().trim().describe(PROJECTS.UPDATE_GROUP_IN_PROJECT.groupSlug) }), body: z.object({ roles: z @@ -78,6 +89,7 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) => ]) ) .min(1) + .describe(PROJECTS.UPDATE_GROUP_IN_PROJECT.roles) }), response: { 200: z.object({ @@ -86,13 +98,13 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) => } }, handler: async (req) => { - const roles = await server.services.groupProject.updateProjectGroup({ + const roles = await server.services.groupProject.updateGroupInProject({ actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, groupSlug: req.params.groupSlug, - projectId: req.params.projectId, + projectSlug: req.params.projectSlug, roles: req.body.roles }); return { roles }; @@ -101,18 +113,18 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) => server.route({ method: "DELETE", - url: "/:projectId/group-memberships/:groupSlug", + url: "/:projectSlug/groups/:groupSlug", onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { - description: "Delete project group memberships", + description: "Remove group from project", security: [ { bearerAuth: [] } ], params: z.object({ - projectId: z.string().trim().describe(PROJECTS.DELETE_IDENTITY_MEMBERSHIP.projectId), - groupSlug: z.string().trim().describe(PROJECTS.DELETE_IDENTITY_MEMBERSHIP.identityId) + projectSlug: z.string().trim().describe(PROJECTS.REMOVE_GROUP_FROM_PROJECT.projectSlug), + groupSlug: z.string().trim().describe(PROJECTS.REMOVE_GROUP_FROM_PROJECT.groupSlug) }), response: { 200: z.object({ @@ -121,13 +133,13 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) => } }, handler: async (req) => { - const groupMembership = await server.services.groupProject.deleteProjectGroup({ + const groupMembership = await server.services.groupProject.removeGroupFromProject({ actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, groupSlug: req.params.groupSlug, - projectId: req.params.projectId + projectSlug: req.params.projectSlug }); return { groupMembership }; } @@ -135,17 +147,17 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) => server.route({ method: "GET", - url: "/:projectId/group-memberships", + url: "/:projectSlug/groups", onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { - description: "Return project group memberships", + description: "Return list of groups in project", security: [ { bearerAuth: [] } ], params: z.object({ - projectId: z.string().trim().describe(PROJECTS.LIST_IDENTITY_MEMBERSHIPS.projectId) + projectSlug: z.string().trim().describe(PROJECTS.LIST_GROUPS_IN_PROJECT.projectSlug) }), response: { 200: z.object({ @@ -176,12 +188,12 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) => } }, handler: async (req) => { - const groupMemberships = await server.services.groupProject.listProjectGroup({ + const groupMemberships = await server.services.groupProject.listGroupsInProject({ actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, - projectId: req.params.projectId + projectSlug: req.params.projectSlug }); return { groupMemberships }; } diff --git a/backend/src/services/group-project/group-project-service.ts b/backend/src/services/group-project/group-project-service.ts index b09f6594f..9b8d698a5 100644 --- a/backend/src/services/group-project/group-project-service.ts +++ b/backend/src/services/group-project/group-project-service.ts @@ -33,7 +33,7 @@ type TGroupProjectServiceFactoryDep = { "create" | "transaction" | "insertMany" | "delete" >; userGroupMembershipDAL: TUserGroupMembershipDALFactory; - projectDAL: Pick; + projectDAL: Pick; projectKeyDAL: Pick; projectRoleDAL: Pick; projectBotDAL: TProjectBotDALFactory; @@ -54,19 +54,25 @@ export const groupProjectServiceFactory = ({ projectRoleDAL, permissionService }: TGroupProjectServiceFactoryDep) => { - const createProjectGroup = async ({ + const addGroupToProject = async ({ groupSlug, actor, actorId, actorOrgId, actorAuthMethod, - projectId, + projectSlug, role }: TCreateProjectGroupDTO) => { + const project = await projectDAL.findOne({ + slug: projectSlug + }); + + if (!project) throw new BadRequestError({ message: `Failed to find project with slug ${projectSlug}` }); + const { permission } = await permissionService.getProjectPermission( actor, actorId, - projectId, + project.id, actorAuthMethod, actorOrgId ); @@ -75,14 +81,12 @@ export const groupProjectServiceFactory = ({ const group = await groupDAL.findOne({ orgId: actorOrgId, slug: groupSlug }); if (!group) throw new BadRequestError({ message: `Failed to find group with slug ${groupSlug}` }); - const existingGroup = await groupProjectDAL.findOne({ groupId: group.id, projectId }); + const existingGroup = await groupProjectDAL.findOne({ groupId: group.id, projectId: project.id }); if (existingGroup) throw new BadRequestError({ - message: `Group with slug ${groupSlug} already exists in project with id ${projectId}` + message: `Group with slug ${groupSlug} already exists in project with id ${project.id}` }); - const project = await projectDAL.findById(projectId); - const { permission: rolePermission, role: customRole } = await permissionService.getProjectPermissionByRole( role, project.id @@ -119,10 +123,10 @@ export const groupProjectServiceFactory = ({ // share project key with users in group that have not // individually been added to the project and that are not part of // other groups that are in the project - const groupMembers = await userGroupMembershipDAL.findGroupMembersNotInProject(group.id, projectId); + const groupMembers = await userGroupMembershipDAL.findGroupMembersNotInProject(group.id, project.id); if (groupMembers.length) { - const ghostUser = await projectDAL.findProjectGhostUser(projectId); + const ghostUser = await projectDAL.findProjectGhostUser(project.id); if (!ghostUser) { throw new BadRequestError({ @@ -130,7 +134,7 @@ export const groupProjectServiceFactory = ({ }); } - const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, projectId); + const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, project.id); if (!ghostUserLatestKey) { throw new BadRequestError({ @@ -138,7 +142,7 @@ export const groupProjectServiceFactory = ({ }); } - const bot = await projectBotDAL.findOne({ projectId }); + const bot = await projectBotDAL.findOne({ projectId: project.id }); if (!bot) { throw new BadRequestError({ @@ -168,7 +172,7 @@ export const groupProjectServiceFactory = ({ nonce, senderId: ghostUser.id, receiverId: id, - projectId + projectId: project.id }; }); @@ -178,8 +182,8 @@ export const groupProjectServiceFactory = ({ return projectGroup; }; - const updateProjectGroup = async ({ - projectId, + const updateGroupInProject = async ({ + projectSlug, groupSlug, roles, actor, @@ -187,10 +191,16 @@ export const groupProjectServiceFactory = ({ actorAuthMethod, actorOrgId }: TUpdateProjectGroupDTO) => { + const project = await projectDAL.findOne({ + slug: projectSlug + }); + + if (!project) throw new BadRequestError({ message: `Failed to find project with slug ${projectSlug}` }); + const { permission } = await permissionService.getProjectPermission( actor, actorId, - projectId, + project.id, actorAuthMethod, actorOrgId ); @@ -199,12 +209,12 @@ export const groupProjectServiceFactory = ({ const group = await groupDAL.findOne({ orgId: actorOrgId, slug: groupSlug }); if (!group) throw new BadRequestError({ message: `Failed to find group with slug ${groupSlug}` }); - const projectGroup = await groupProjectDAL.findOne({ groupId: group.id, projectId }); + const projectGroup = await groupProjectDAL.findOne({ groupId: group.id, projectId: project.id }); if (!projectGroup) throw new BadRequestError({ message: `Failed to find group with slug ${groupSlug}` }); const { permission: groupRolePermission } = await permissionService.getProjectPermissionByRole( projectGroup.role, - projectId + project.id ); const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, groupRolePermission); @@ -217,7 +227,7 @@ export const groupProjectServiceFactory = ({ const hasCustomRole = Boolean(customInputRoles.length); const customRoles = hasCustomRole ? await projectRoleDAL.find({ - projectId, + projectId: project.id, $in: { slug: customInputRoles.map(({ role }) => role) } }) : []; @@ -257,69 +267,87 @@ export const groupProjectServiceFactory = ({ return updatedRoles; }; - const deleteProjectGroup = async ({ + const removeGroupFromProject = async ({ + projectSlug, groupSlug, actorId, actor, actorOrgId, - actorAuthMethod, - projectId + actorAuthMethod }: TDeleteProjectGroupDTO) => { + const project = await projectDAL.findOne({ + slug: projectSlug + }); + + if (!project) throw new BadRequestError({ message: `Failed to find project with slug ${projectSlug}` }); + const group = await groupDAL.findOne({ orgId: actorOrgId, slug: groupSlug }); if (!group) throw new BadRequestError({ message: `Failed to find group with slug ${groupSlug}` }); - const groupProjectMembership = await groupProjectDAL.findOne({ groupId: group.id, projectId }); + const groupProjectMembership = await groupProjectDAL.findOne({ groupId: group.id, projectId: project.id }); if (!groupProjectMembership) throw new BadRequestError({ message: `Failed to find group with slug ${groupSlug}` }); const { permission } = await permissionService.getProjectPermission( actor, actorId, - projectId, + project.id, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Groups); const { permission: groupRolePermission } = await permissionService.getProjectPermissionByRole( groupProjectMembership.role, - projectId + project.id ); const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, groupRolePermission); if (!hasRequiredPriviledges) throw new ForbiddenRequestError({ message: "Failed to delete more privileged group" }); - const groupMembers = await userGroupMembershipDAL.findGroupMembersNotInProject(group.id, projectId); + const groupMembers = await userGroupMembershipDAL.findGroupMembersNotInProject(group.id, project.id); if (groupMembers.length) { await projectKeyDAL.delete({ - projectId, + projectId: project.id, $in: { receiverId: groupMembers.map(({ user: { id } }) => id) } }); } - const [deletedGroup] = await groupProjectDAL.delete({ groupId: group.id, projectId }); + const [deletedGroup] = await groupProjectDAL.delete({ groupId: group.id, projectId: project.id }); return deletedGroup; }; - const listProjectGroup = async ({ projectId, actor, actorId, actorAuthMethod, actorOrgId }: TListProjectGroupDTO) => { + const listGroupsInProject = async ({ + projectSlug, + actor, + actorId, + actorAuthMethod, + actorOrgId + }: TListProjectGroupDTO) => { + const project = await projectDAL.findOne({ + slug: projectSlug + }); + + if (!project) throw new BadRequestError({ message: `Failed to find project with slug ${projectSlug}` }); + const { permission } = await permissionService.getProjectPermission( actor, actorId, - projectId, + project.id, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Groups); - const groupMemberhips = await groupProjectDAL.findByProjectId(projectId); + const groupMemberhips = await groupProjectDAL.findByProjectId(project.id); return groupMemberhips; }; return { - createProjectGroup, - updateProjectGroup, - deleteProjectGroup, - listProjectGroup + addGroupToProject, + updateGroupInProject, + removeGroupFromProject, + listGroupsInProject }; }; diff --git a/backend/src/services/group-project/group-project-types.ts b/backend/src/services/group-project/group-project-types.ts index 4579fe122..c867b75c0 100644 --- a/backend/src/services/group-project/group-project-types.ts +++ b/backend/src/services/group-project/group-project-types.ts @@ -1,11 +1,11 @@ -import { TProjectPermission } from "@app/lib/types"; +import { TProjectSlugPermission } from "@app/lib/types"; import { ProjectUserMembershipTemporaryMode } from "../project-membership/project-membership-types"; export type TCreateProjectGroupDTO = { groupSlug: string; role: string; -} & TProjectPermission; +} & TProjectSlugPermission; export type TUpdateProjectGroupDTO = { roles: ( @@ -22,10 +22,10 @@ export type TUpdateProjectGroupDTO = { } )[]; groupSlug: string; -} & TProjectPermission; +} & TProjectSlugPermission; export type TDeleteProjectGroupDTO = { groupSlug: string; -} & TProjectPermission; +} & TProjectSlugPermission; -export type TListProjectGroupDTO = TProjectPermission; +export type TListProjectGroupDTO = TProjectSlugPermission; diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts index fab94edf6..c03fe1748 100644 --- a/backend/src/services/org/org-service.ts +++ b/backend/src/services/org/org-service.ts @@ -118,7 +118,8 @@ export const orgServiceFactory = ({ }; const getOrgGroups = async ({ actor, actorId, orgId, actorAuthMethod, actorOrgId }: TGetOrgGroupsDTO) => { - await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Groups); const groups = await groupDAL.findByOrgId(orgId); return groups; }; diff --git a/backend/src/services/org/org-types.ts b/backend/src/services/org/org-types.ts index 0efc7ffe1..5e1e7c6ae 100644 --- a/backend/src/services/org/org-types.ts +++ b/backend/src/services/org/org-types.ts @@ -51,7 +51,10 @@ export type TFindAllWorkspacesDTO = { }; export type TUpdateOrgDTO = { + orgId: string; data: Partial<{ name: string; slug: string; authEnforced: boolean; scimEnabled: boolean }>; } & TOrgPermission; -export type TGetOrgGroupsDTO = TOrgPermission; +export type TGetOrgGroupsDTO = { + orgId: string; +} & TOrgPermission; diff --git a/frontend/src/hooks/api/workspace/queries.tsx b/frontend/src/hooks/api/workspace/queries.tsx index 0da76f7f9..2122ab81a 100644 --- a/frontend/src/hooks/api/workspace/queries.tsx +++ b/frontend/src/hooks/api/workspace/queries.tsx @@ -40,7 +40,7 @@ export const workspaceKeys = { getWorkspaceIdentityMemberships: (workspaceId: string) => [{ workspaceId }, "workspace-identity-memberships"] as const, getWorkspaceGroupMemberships: (workspaceId: string) => - [{ workspaceId }, "workspace-group-memberships"] as const + [{ workspaceId }, "workspace-groups"] as const }; const fetchWorkspaceById = async (workspaceId: string) => { @@ -460,25 +460,25 @@ export const useAddGroupToWorkspace = () => { return useMutation({ mutationFn: async ({ groupSlug, - workspaceId, + projectSlug, role }: { groupSlug: string; - workspaceId: string; + projectSlug: string; role?: string; }) => { const { data: { groupMembership } } = await apiRequest.post( - `/api/v2/workspace/${workspaceId}/group-memberships/${groupSlug}`, + `/api/v2/workspace/${projectSlug}/groups/${groupSlug}`, { role } ); return groupMembership; }, - onSuccess: (_, { workspaceId }) => { - queryClient.invalidateQueries(workspaceKeys.getWorkspaceGroupMemberships(workspaceId)); + onSuccess: (_, { projectSlug }) => { + queryClient.invalidateQueries(workspaceKeys.getWorkspaceGroupMemberships(projectSlug)); } }); }; @@ -486,20 +486,19 @@ export const useAddGroupToWorkspace = () => { export const useUpdateGroupWorkspaceRole = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ groupSlug, workspaceId, roles }: TUpdateWorkspaceGroupRoleDTO) => { + mutationFn: async ({ groupSlug, projectSlug, roles }: TUpdateWorkspaceGroupRoleDTO) => { const { data: { groupMembership } } = await apiRequest.patch( - `/api/v2/workspace/${workspaceId}/group-memberships/${groupSlug}`, - { + `/api/v2/workspace/${projectSlug}/groups/${groupSlug}`, { roles } ); return groupMembership; }, - onSuccess: (_, { workspaceId }) => { - queryClient.invalidateQueries(workspaceKeys.getWorkspaceGroupMemberships(workspaceId)); + onSuccess: (_, { projectSlug }) => { + queryClient.invalidateQueries(workspaceKeys.getWorkspaceGroupMemberships(projectSlug)); } }); }; @@ -509,32 +508,32 @@ export const useDeleteGroupFromWorkspace = () => { return useMutation({ mutationFn: async ({ groupSlug, - workspaceId + projectSlug }: { groupSlug: string; - workspaceId: string; + projectSlug: string; }) => { const { data: { groupMembership } } = await apiRequest.delete( - `/api/v2/workspace/${workspaceId}/group-memberships/${groupSlug}` + `/api/v2/workspace/${projectSlug}/groups/${groupSlug}` ); return groupMembership; }, - onSuccess: (_, { workspaceId }) => { - queryClient.invalidateQueries(workspaceKeys.getWorkspaceGroupMemberships(workspaceId)); + onSuccess: (_, { projectSlug }) => { + queryClient.invalidateQueries(workspaceKeys.getWorkspaceGroupMemberships(projectSlug)); } }); }; -export const useGetWorkspaceGroupMemberships = (workspaceId: string) => { +export const useGetWorkspaceGroupMemberships = (projectSlug: string) => { return useQuery({ - queryKey: workspaceKeys.getWorkspaceGroupMemberships(workspaceId), + queryKey: workspaceKeys.getWorkspaceGroupMemberships(projectSlug), queryFn: async () => { const { data: { groupMemberships } } = await apiRequest.get<{ groupMemberships: TGroupMembership[] }>( - `/api/v2/workspace/${workspaceId}/group-memberships` + `/api/v2/workspace/${projectSlug}/groups` ); return groupMemberships; }, diff --git a/frontend/src/hooks/api/workspace/types.ts b/frontend/src/hooks/api/workspace/types.ts index 00f4ec4fe..8be9beed0 100644 --- a/frontend/src/hooks/api/workspace/types.ts +++ b/frontend/src/hooks/api/workspace/types.ts @@ -114,7 +114,7 @@ export type TUpdateWorkspaceIdentityRoleDTO = { export type TUpdateWorkspaceGroupRoleDTO = { groupSlug: string; - workspaceId: string; + projectSlug: string; roles: ( | { role: string; diff --git a/frontend/src/views/Project/MembersPage/components/GroupsTab/components/GroupsSection/GroupModal.tsx b/frontend/src/views/Project/MembersPage/components/GroupsTab/components/GroupsSection/GroupModal.tsx index 333f69cd8..4e5b4d7ac 100644 --- a/frontend/src/views/Project/MembersPage/components/GroupsTab/components/GroupsSection/GroupModal.tsx +++ b/frontend/src/views/Project/MembersPage/components/GroupsTab/components/GroupsSection/GroupModal.tsx @@ -48,7 +48,7 @@ export const GroupModal = ({ const workspaceId = currentWorkspace?.id || ""; const { data: groups } = useGetOrganizationGroups(orgId); - const { data: groupMemberships } = useGetWorkspaceGroupMemberships(workspaceId); + const { data: groupMemberships } = useGetWorkspaceGroupMemberships(currentWorkspace?.slug || ""); const { data: roles } = useGetProjectRoles(workspaceId); @@ -76,7 +76,7 @@ export const GroupModal = ({ const onFormSubmit = async ({ slug, role }: FormData) => { try { await addGroupToWorkspaceMutateAsync({ - workspaceId, + projectSlug: currentWorkspace?.slug || "", groupSlug: slug, role: role || undefined }); diff --git a/frontend/src/views/Project/MembersPage/components/GroupsTab/components/GroupsSection/GroupRoles.tsx b/frontend/src/views/Project/MembersPage/components/GroupsTab/components/GroupsSection/GroupRoles.tsx index 8812d9c9a..1384c5819 100644 --- a/frontend/src/views/Project/MembersPage/components/GroupsTab/components/GroupsSection/GroupRoles.tsx +++ b/frontend/src/views/Project/MembersPage/components/GroupsTab/components/GroupsSection/GroupRoles.tsx @@ -252,7 +252,7 @@ export const GroupRoles = ({ try { await updateGroupWorkspaceRole.mutateAsync({ - workspaceId, + projectSlug: currentWorkspace?.slug || "", groupSlug, roles: selectedRoles }); diff --git a/frontend/src/views/Project/MembersPage/components/GroupsTab/components/GroupsSection/GroupsSection.tsx b/frontend/src/views/Project/MembersPage/components/GroupsTab/components/GroupsSection/GroupsSection.tsx index 5fa69b433..77c9bc4d2 100644 --- a/frontend/src/views/Project/MembersPage/components/GroupsTab/components/GroupsSection/GroupsSection.tsx +++ b/frontend/src/views/Project/MembersPage/components/GroupsTab/components/GroupsSection/GroupsSection.tsx @@ -17,8 +17,6 @@ import { GroupTable } from "./GroupsTable"; export const GroupsSection = () => { const { currentWorkspace } = useWorkspace(); - const workspaceId = currentWorkspace?.id ?? ""; - const { mutateAsync: deleteMutateAsync } = useDeleteGroupFromWorkspace(); const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ @@ -31,7 +29,7 @@ export const GroupsSection = () => { try { await deleteMutateAsync({ groupSlug, - workspaceId + projectSlug: currentWorkspace?.slug || "" }); createNotification({ diff --git a/frontend/src/views/Project/MembersPage/components/GroupsTab/components/GroupsSection/GroupsTable.tsx b/frontend/src/views/Project/MembersPage/components/GroupsTab/components/GroupsSection/GroupsTable.tsx index 436b402b8..4958178d6 100644 --- a/frontend/src/views/Project/MembersPage/components/GroupsTab/components/GroupsSection/GroupsTable.tsx +++ b/frontend/src/views/Project/MembersPage/components/GroupsTab/components/GroupsSection/GroupsTable.tsx @@ -33,7 +33,7 @@ type Props = { export const GroupTable = ({ handlePopUpOpen }: Props) => { const { currentWorkspace } = useWorkspace(); - const { data, isLoading } = useGetWorkspaceGroupMemberships(currentWorkspace?.id || ""); + const { data, isLoading } = useGetWorkspaceGroupMemberships(currentWorkspace?.slug || ""); return (