diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index 7594156ed..62f1d58dc 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -1085,7 +1085,7 @@ export type MutationUpdateWorkspaceFeatureFlagArgs = { export type MutationUpdateWorkspaceMemberRoleArgs = { - roleId?: InputMaybe; + roleId: Scalars['String']; workspaceMemberId: Scalars['String']; }; @@ -2368,7 +2368,7 @@ export type RoleFragmentFragment = { __typename?: 'Role', id: string, label: str export type UpdateWorkspaceMemberRoleMutationVariables = Exact<{ workspaceMemberId: Scalars['String']; - roleId?: InputMaybe; + roleId: Scalars['String']; }>; @@ -4229,7 +4229,7 @@ export type UpdateLabPublicFeatureFlagMutationHookResult = ReturnType; export type UpdateLabPublicFeatureFlagMutationOptions = Apollo.BaseMutationOptions; export const UpdateWorkspaceMemberRoleDocument = gql` - mutation UpdateWorkspaceMemberRole($workspaceMemberId: String!, $roleId: String) { + mutation UpdateWorkspaceMemberRole($workspaceMemberId: String!, $roleId: String!) { updateWorkspaceMemberRole( workspaceMemberId: $workspaceMemberId roleId: $roleId diff --git a/packages/twenty-front/src/modules/settings/roles/graphql/mutations/updateWorkspaceMemberRoleMutation.ts b/packages/twenty-front/src/modules/settings/roles/graphql/mutations/updateWorkspaceMemberRoleMutation.ts index eaa0ec675..7ca807766 100644 --- a/packages/twenty-front/src/modules/settings/roles/graphql/mutations/updateWorkspaceMemberRoleMutation.ts +++ b/packages/twenty-front/src/modules/settings/roles/graphql/mutations/updateWorkspaceMemberRoleMutation.ts @@ -7,7 +7,7 @@ export const UPDATE_WORKSPACE_MEMBER_ROLE = gql` ${ROLE_FRAGMENT} mutation UpdateWorkspaceMemberRole( $workspaceMemberId: String! - $roleId: String + $roleId: String! ) { updateWorkspaceMemberRole( workspaceMemberId: $workspaceMemberId diff --git a/packages/twenty-front/src/pages/settings/roles/components/RoleAssignment.tsx b/packages/twenty-front/src/pages/settings/roles/components/RoleAssignment.tsx index 2d52713ac..2ff1662a0 100644 --- a/packages/twenty-front/src/pages/settings/roles/components/RoleAssignment.tsx +++ b/packages/twenty-front/src/pages/settings/roles/components/RoleAssignment.tsx @@ -24,7 +24,6 @@ import { useUpdateWorkspaceMemberRoleMutation, } from '~/generated/graphql'; import { useNavigateSettings } from '~/hooks/useNavigateSettings'; -import { RoleAssignmentConfirmationModalMode } from '~/pages/settings/roles/types/RoleAssignmentConfirmationModalMode'; import { RoleAssignmentConfirmationModalSelectedWorkspaceMember } from '~/pages/settings/roles/types/RoleAssignmentConfirmationModalSelectedWorkspaceMember'; import { RoleAssignmentConfirmationModal } from './RoleAssignmentConfirmationModal'; import { RoleAssignmentTableHeader } from './RoleAssignmentTableHeader'; @@ -73,8 +72,8 @@ export const RoleAssignment = ({ role }: RoleAssignmentProps) => { refetchQueries: [GetRolesDocument], }); - const [modalMode, setModalMode] = - useState(null); + const [confirmationModalIsOpen, setConfirmationModalIsOpen] = + useState(false); const [selectedWorkspaceMember, setSelectedWorkspaceMember] = useState( null, @@ -110,7 +109,7 @@ export const RoleAssignment = ({ role }: RoleAssignmentProps) => { }); const handleModalClose = () => { - setModalMode(null); + setConfirmationModalIsOpen(false); setSelectedWorkspaceMember(null); }; @@ -122,26 +121,17 @@ export const RoleAssignment = ({ role }: RoleAssignmentProps) => { name: `${workspaceMember.name.firstName} ${workspaceMember.name.lastName}`, role: existingRole, }); - setModalMode('assign'); + setConfirmationModalIsOpen(true); closeDropdown(); }; - const handleRemoveClick = (workspaceMember: WorkspaceMember) => { - setSelectedWorkspaceMember({ - id: workspaceMember.id, - name: `${workspaceMember.name.firstName} ${workspaceMember.name.lastName}`, - role: workspaceMemberRoleMap.get(workspaceMember.id), - }); - setModalMode('remove'); - }; - const handleConfirm = async () => { - if (!selectedWorkspaceMember || !modalMode) return; + if (!selectedWorkspaceMember || !confirmationModalIsOpen) return; await updateWorkspaceMemberRole({ variables: { workspaceMemberId: selectedWorkspaceMember.id, - roleId: modalMode === 'assign' ? role.id : null, + roleId: role.id, }, }); @@ -183,7 +173,6 @@ export const RoleAssignment = ({ role }: RoleAssignmentProps) => { handleRemoveClick(workspaceMember)} /> ))} @@ -222,9 +211,8 @@ export const RoleAssignment = ({ role }: RoleAssignmentProps) => { /> - {modalMode && selectedWorkspaceMember && ( + {confirmationModalIsOpen && selectedWorkspaceMember && ( void; @@ -14,21 +12,15 @@ type RoleAssignmentConfirmationModalProps = { }; export const RoleAssignmentConfirmationModal = ({ - mode, selectedWorkspaceMember, isOpen, onClose, onConfirm, onRoleClick, }: RoleAssignmentConfirmationModalProps) => { - const isAssignMode = mode === 'assign'; - const hasExistingRole = !!selectedWorkspaceMember.role; - const workspaceMemberName = selectedWorkspaceMember.name; - const title = isAssignMode - ? t`Assign ${workspaceMemberName}?` - : t`Remove ${workspaceMemberName}?`; + const title = t`Assign ${workspaceMemberName}?`; return ( } onConfirmClick={onConfirm} - deleteButtonText={isAssignMode ? t`Confirm` : t`Remove`} - confirmButtonAccent={isAssignMode && !hasExistingRole ? 'blue' : 'danger'} + deleteButtonText={t`Confirm`} + confirmButtonAccent="blue" /> ); }; diff --git a/packages/twenty-front/src/pages/settings/roles/components/RoleAssignmentConfirmationModalSubtitle.tsx b/packages/twenty-front/src/pages/settings/roles/components/RoleAssignmentConfirmationModalSubtitle.tsx index a7afa0d60..ff15f5a33 100644 --- a/packages/twenty-front/src/pages/settings/roles/components/RoleAssignmentConfirmationModalSubtitle.tsx +++ b/packages/twenty-front/src/pages/settings/roles/components/RoleAssignmentConfirmationModalSubtitle.tsx @@ -2,7 +2,6 @@ import { SettingsCard } from '@/settings/components/SettingsCard'; import styled from '@emotion/styled'; import { t } from '@lingui/core/macro'; import { IconUser } from 'twenty-ui'; -import { RoleAssignmentConfirmationModalMode } from '~/pages/settings/roles/types/RoleAssignmentConfirmationModalMode'; import { RoleAssignmentConfirmationModalSelectedWorkspaceMember } from '~/pages/settings/roles/types/RoleAssignmentConfirmationModalSelectedWorkspaceMember'; const StyledSettingsCardContainer = styled.div` @@ -10,40 +9,29 @@ const StyledSettingsCardContainer = styled.div` `; type RoleAssignmentConfirmationModalSubtitleProps = { - mode: RoleAssignmentConfirmationModalMode; selectedWorkspaceMember: RoleAssignmentConfirmationModalSelectedWorkspaceMember; onRoleClick: (roleId: string) => void; }; export const RoleAssignmentConfirmationModalSubtitle = ({ - mode, selectedWorkspaceMember, onRoleClick, }: RoleAssignmentConfirmationModalSubtitleProps) => { - const isAssignMode = mode === 'assign'; - const hasExistingRole = !!selectedWorkspaceMember.role; - const workspaceMemberName = selectedWorkspaceMember.name; - if (isAssignMode && hasExistingRole) { - return ( - <> - {t`${workspaceMemberName} will be unassigned from the following role:`} - - } - onClick={() => - selectedWorkspaceMember.role && - onRoleClick(selectedWorkspaceMember.role.id) - } - /> - - - ); - } - - return isAssignMode - ? t`Are you sure you want to assign this role?` - : t`This member will be unassigned from this role.`; + return ( + <> + {t`${workspaceMemberName} will be unassigned from the following role:`} + + } + onClick={() => + selectedWorkspaceMember.role && + onRoleClick(selectedWorkspaceMember.role.id) + } + /> + + + ); }; diff --git a/packages/twenty-front/src/pages/settings/roles/components/RoleAssignmentTableRow.tsx b/packages/twenty-front/src/pages/settings/roles/components/RoleAssignmentTableRow.tsx index fc2f20b42..6d36de04b 100644 --- a/packages/twenty-front/src/pages/settings/roles/components/RoleAssignmentTableRow.tsx +++ b/packages/twenty-front/src/pages/settings/roles/components/RoleAssignmentTableRow.tsx @@ -2,13 +2,7 @@ import { Table } from '@/ui/layout/table/components/Table'; import { TableCell } from '@/ui/layout/table/components/TableCell'; import { TableRow } from '@/ui/layout/table/components/TableRow'; import styled from '@emotion/styled'; -import { t } from '@lingui/core/macro'; -import { - Avatar, - IconButton, - IconTrash, - OverflowingTextWithTooltip, -} from 'twenty-ui'; +import { Avatar, OverflowingTextWithTooltip } from 'twenty-ui'; import { WorkspaceMember } from '~/generated-metadata/graphql'; const StyledTable = styled(Table)` @@ -21,27 +15,13 @@ const StyledIconWrapper = styled.div` margin-right: ${({ theme }) => theme.spacing(2)}; `; -const StyledButtonContainer = styled.div` - align-items: center; - display: flex; - flex-direction: row; - margin-left: ${({ theme }) => theme.spacing(3)}; -`; - type RoleAssignmentTableRowProps = { workspaceMember: WorkspaceMember; - onRemove: (workspaceMemberId: string) => void; }; export const RoleAssignmentTableRow = ({ workspaceMember, - onRemove, }: RoleAssignmentTableRowProps) => { - const handleRemoveClick = (event: React.MouseEvent) => { - event.stopPropagation(); - onRemove(workspaceMember.id); - }; - return ( @@ -62,17 +42,6 @@ export const RoleAssignmentTableRow = ({ - - - - - ); diff --git a/packages/twenty-front/src/pages/settings/roles/types/RoleAssignmentConfirmationModalMode.ts b/packages/twenty-front/src/pages/settings/roles/types/RoleAssignmentConfirmationModalMode.ts deleted file mode 100644 index 360473454..000000000 --- a/packages/twenty-front/src/pages/settings/roles/types/RoleAssignmentConfirmationModalMode.ts +++ /dev/null @@ -1 +0,0 @@ -export type RoleAssignmentConfirmationModalMode = 'assign' | 'remove'; diff --git a/packages/twenty-server/src/engine/metadata-modules/permissions/permissions.exception.ts b/packages/twenty-server/src/engine/metadata-modules/permissions/permissions.exception.ts index 2535b8c36..42535a62b 100644 --- a/packages/twenty-server/src/engine/metadata-modules/permissions/permissions.exception.ts +++ b/packages/twenty-server/src/engine/metadata-modules/permissions/permissions.exception.ts @@ -18,6 +18,7 @@ export enum PermissionsExceptionCode { CANNOT_UNASSIGN_LAST_ADMIN = 'CANNOT_UNASSIGN_LAST_ADMIN', UNKNOWN_OPERATION_NAME = 'UNKNOWN_OPERATION_NAME', UNKNOWN_REQUIRED_PERMISSION = 'UNKNOWN_REQUIRED_PERMISSION', + CANNOT_UPDATE_SELF_ROLE = 'CANNOT_UPDATE_SELF_ROLE', } export enum PermissionsExceptionMessage { @@ -29,7 +30,8 @@ export enum PermissionsExceptionMessage { USER_WORKSPACE_ALREADY_HAS_ROLE = 'User workspace already has role', WORKSPACE_MEMBER_NOT_FOUND = 'Workspace member not found', ROLE_NOT_FOUND = 'Role not found', - CANNOT_UNASSIGN_LAST_ADMIN = 'Cannot unassign last admin', + CANNOT_UNASSIGN_LAST_ADMIN = 'Cannot unassign admin role from last admin of the workspace', UNKNOWN_OPERATION_NAME = 'Unknown operation name, cannot determine required permission', UNKNOWN_REQUIRED_PERMISSION = 'Unknown required permission', + CANNOT_UPDATE_SELF_ROLE = 'Cannot update self role', } diff --git a/packages/twenty-server/src/engine/metadata-modules/permissions/utils/permission-graphql-api-exception-handler.util.ts b/packages/twenty-server/src/engine/metadata-modules/permissions/utils/permission-graphql-api-exception-handler.util.ts index 60614a396..d960f0ade 100644 --- a/packages/twenty-server/src/engine/metadata-modules/permissions/utils/permission-graphql-api-exception-handler.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/permissions/utils/permission-graphql-api-exception-handler.util.ts @@ -14,6 +14,7 @@ export const permissionGraphqlApiExceptionHandler = ( switch (error.code) { case PermissionsExceptionCode.PERMISSION_DENIED: case PermissionsExceptionCode.CANNOT_UNASSIGN_LAST_ADMIN: + case PermissionsExceptionCode.CANNOT_UPDATE_SELF_ROLE: throw new ForbiddenError(error.message); case PermissionsExceptionCode.ROLE_NOT_FOUND: case PermissionsExceptionCode.USER_WORKSPACE_NOT_FOUND: diff --git a/packages/twenty-server/src/engine/metadata-modules/role/role.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/role/role.resolver.ts index b913d8273..15f8b22d0 100644 --- a/packages/twenty-server/src/engine/metadata-modules/role/role.resolver.ts +++ b/packages/twenty-server/src/engine/metadata-modules/role/role.resolver.ts @@ -8,14 +8,18 @@ import { Resolver, } from '@nestjs/graphql'; -import { isDefined } from 'twenty-shared'; - import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service'; import { WorkspaceMember } from 'src/engine/core-modules/user/dtos/workspace-member.dto'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { AuthWorkspaceMemberId } from 'src/engine/decorators/auth/auth-workspace-member-id.decorator'; import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; import { SettingsPermissionsGuard } from 'src/engine/guards/settings-permissions.guard'; import { SettingsPermissions } from 'src/engine/metadata-modules/permissions/constants/settings-permissions.constants'; +import { + PermissionsException, + PermissionsExceptionCode, + PermissionsExceptionMessage, +} from 'src/engine/metadata-modules/permissions/permissions.exception'; import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter'; import { RoleDTO } from 'src/engine/metadata-modules/role/dtos/role.dto'; import { RoleService } from 'src/engine/metadata-modules/role/role.service'; @@ -41,9 +45,17 @@ export class RoleResolver { async updateWorkspaceMemberRole( @AuthWorkspace() workspace: Workspace, @Args('workspaceMemberId') workspaceMemberId: string, - @Args('roleId', { type: () => String, nullable: true }) - roleId: string | null, + @Args('roleId', { type: () => String }) roleId: string, + @AuthWorkspaceMemberId() + updatorWorkspaceMemberId: string, ): Promise { + if (updatorWorkspaceMemberId === workspaceMemberId) { + throw new PermissionsException( + PermissionsExceptionMessage.CANNOT_UPDATE_SELF_ROLE, + PermissionsExceptionCode.CANNOT_UPDATE_SELF_ROLE, + ); + } + const workspaceMember = await this.userWorkspaceService.getWorkspaceMemberOrThrow({ workspaceMemberId, @@ -56,18 +68,11 @@ export class RoleResolver { workspaceId: workspace.id, }); - if (!isDefined(roleId)) { - await this.userRoleService.unassignAllRolesFromUserWorkspace({ - userWorkspaceId: userWorkspace.id, - workspaceId: workspace.id, - }); - } else { - await this.userRoleService.assignRoleToUserWorkspace({ - userWorkspaceId: userWorkspace.id, - workspaceId: workspace.id, - roleId, - }); - } + await this.userRoleService.assignRoleToUserWorkspace({ + userWorkspaceId: userWorkspace.id, + workspaceId: workspace.id, + roleId, + }); const roles = await this.userRoleService .getRolesByUserWorkspaces({ diff --git a/packages/twenty-server/src/engine/metadata-modules/user-role/user-role.service.ts b/packages/twenty-server/src/engine/metadata-modules/user-role/user-role.service.ts index ebc6a6939..fbc653063 100644 --- a/packages/twenty-server/src/engine/metadata-modules/user-role/user-role.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/user-role/user-role.service.ts @@ -1,14 +1,15 @@ import { InjectRepository } from '@nestjs/typeorm'; import { isDefined } from 'twenty-shared'; -import { In, Repository } from 'typeorm'; +import { In, Not, Repository } from 'typeorm'; import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; +import { ADMIN_ROLE_LABEL } from 'src/engine/metadata-modules/permissions/constants/admin-role-label.constants'; import { PermissionsException, PermissionsExceptionCode, + PermissionsExceptionMessage, } from 'src/engine/metadata-modules/permissions/permissions.exception'; -import { RoleDTO } from 'src/engine/metadata-modules/role/dtos/role.dto'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { UserWorkspaceRoleEntity } from 'src/engine/metadata-modules/role/user-workspace-role.entity'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; @@ -34,72 +35,22 @@ export class UserRoleService { userWorkspaceId: string; roleId: string; }): Promise { - const userWorkspace = await this.userWorkspaceRepository.findOne({ - where: { - id: userWorkspaceId, - }, - }); - - if (!isDefined(userWorkspace)) { - throw new PermissionsException( - 'User workspace not found', - PermissionsExceptionCode.USER_WORKSPACE_NOT_FOUND, - ); - } - - const role = await this.roleRepository.findOne({ - where: { - id: roleId, - }, - }); - - if (!isDefined(role)) { - throw new PermissionsException( - 'Role not found', - PermissionsExceptionCode.ROLE_NOT_FOUND, - ); - } - - const roles = await this.getRolesByUserWorkspaces({ - userWorkspaceIds: [userWorkspace.id], + await this.validateAssignRoleInput({ + userWorkspaceId, workspaceId, - }); - - const currentRole = roles.get(userWorkspace.id)?.[0]; - - if (currentRole?.id === roleId) { - return; - } - - await this.unassignAllRolesFromUserWorkspace({ - userWorkspaceId: userWorkspace.id, - workspaceId, - }); - - await this.userWorkspaceRoleRepository.save({ roleId, - userWorkspaceId: userWorkspace.id, + }); + + const newUserWorkspaceRole = await this.userWorkspaceRoleRepository.save({ + roleId, + userWorkspaceId, workspaceId, }); - } - - public async unassignAllRolesFromUserWorkspace({ - userWorkspaceId, - workspaceId, - }: { - userWorkspaceId: string; - workspaceId: string; - }): Promise { - await this.validatesUserWorkspaceIsNotLastAdminIfUnassigningAdminRoleOrThrow( - { - userWorkspaceId, - workspaceId, - }, - ); await this.userWorkspaceRoleRepository.delete({ userWorkspaceId, workspaceId, + id: Not(newUserWorkspaceRole.id), }); } @@ -186,34 +137,64 @@ export class UserRoleService { return workspaceMembers; } - private async validatesUserWorkspaceIsNotLastAdminIfUnassigningAdminRoleOrThrow({ + private async validateAssignRoleInput({ userWorkspaceId, workspaceId, + roleId, }: { userWorkspaceId: string; workspaceId: string; - }): Promise { + roleId: string; + }) { + const userWorkspace = await this.userWorkspaceRepository.findOne({ + where: { + id: userWorkspaceId, + }, + }); + + if (!isDefined(userWorkspace)) { + throw new PermissionsException( + 'User workspace not found', + PermissionsExceptionCode.USER_WORKSPACE_NOT_FOUND, + ); + } + + const role = await this.roleRepository.findOne({ + where: { + id: roleId, + }, + }); + + if (!isDefined(role)) { + throw new PermissionsException( + 'Role not found', + PermissionsExceptionCode.ROLE_NOT_FOUND, + ); + } + const roles = await this.getRolesByUserWorkspaces({ - userWorkspaceIds: [userWorkspaceId], + userWorkspaceIds: [userWorkspace.id], workspaceId, }); - const currentRoles = roles.get(userWorkspaceId); + const currentRole = roles.get(userWorkspace.id)?.[0]; - const adminRole = currentRoles?.find( - (role: RoleDTO) => role.isEditable === false, - ); + if (currentRole?.id === roleId) { + return; + } - if (isDefined(adminRole)) { - const workspaceMembersWithAdminRole = - await this.getWorkspaceMembersAssignedToRole(adminRole.id, workspaceId); + if (!(currentRole?.label === ADMIN_ROLE_LABEL)) { + return; + } - if (workspaceMembersWithAdminRole.length === 1) { - throw new PermissionsException( - `Cannot unassign admin role as there is only one admin in the workspace`, - PermissionsExceptionCode.CANNOT_UNASSIGN_LAST_ADMIN, - ); - } + const workspaceMembersWithAdminRole = + await this.getWorkspaceMembersAssignedToRole(currentRole.id, workspaceId); + + if (workspaceMembersWithAdminRole.length === 1) { + throw new PermissionsException( + PermissionsExceptionMessage.CANNOT_UNASSIGN_LAST_ADMIN, + PermissionsExceptionCode.CANNOT_UNASSIGN_LAST_ADMIN, + ); } } } diff --git a/packages/twenty-server/test/integration/graphql/suites/settings-permissions/roles.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/settings-permissions/roles.integration-spec.ts index f57a7456d..c1082a7b8 100644 --- a/packages/twenty-server/test/integration/graphql/suites/settings-permissions/roles.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/settings-permissions/roles.integration-spec.ts @@ -3,8 +3,10 @@ import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graph import { updateFeatureFlagFactory } from 'test/integration/graphql/utils/update-feature-flag-factory.util'; import { SEED_APPLE_WORKSPACE_ID } from 'src/database/typeorm-seeds/core/workspaces'; +import { DEV_SEED_WORKSPACE_MEMBER_IDS } from 'src/database/typeorm-seeds/workspace/workspace-members'; import { ErrorCode } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; import { PermissionsExceptionMessage } from 'src/engine/metadata-modules/permissions/permissions.exception'; + const client = request(`http://localhost:${APP_PORT}`); describe('roles permissions', () => { @@ -55,44 +57,46 @@ describe('roles permissions', () => { expect(resp.status).toBe(200); expect(resp.body.errors).toBeUndefined(); expect(resp.body.data.getRoles).toHaveLength(3); - expect(resp.body.data.getRoles).toEqual([ - { - label: 'Guest', - workspaceMembers: [ - { - id: '20202020-1553-45c6-a028-5a9064cce07f', - name: { - firstName: 'Phil', - lastName: 'Schiler', + expect(resp.body.data.getRoles).toEqual( + expect.arrayContaining([ + { + label: 'Guest', + workspaceMembers: [ + { + id: '20202020-1553-45c6-a028-5a9064cce07f', + name: { + firstName: 'Phil', + lastName: 'Schiler', + }, }, - }, - ], - }, - { - label: 'Admin', - workspaceMembers: [ - { - id: '20202020-0687-4c41-b707-ed1bfca972a7', - name: { - firstName: 'Tim', - lastName: 'Apple', + ], + }, + { + label: 'Admin', + workspaceMembers: [ + { + id: '20202020-0687-4c41-b707-ed1bfca972a7', + name: { + firstName: 'Tim', + lastName: 'Apple', + }, }, - }, - ], - }, - { - label: 'Member', - workspaceMembers: [ - { - id: '20202020-77d5-4cb6-b60a-f4a835a85d61', - name: { - firstName: 'Jony', - lastName: 'Ive', + ], + }, + { + label: 'Member', + workspaceMembers: [ + { + id: '20202020-77d5-4cb6-b60a-f4a835a85d61', + name: { + firstName: 'Jony', + lastName: 'Ive', + }, }, - }, - ], - }, - ]); + ], + }, + ]), + ); }); it('should throw a permission error when user does not have permission (member role)', async () => { const query = { @@ -129,7 +133,7 @@ describe('roles permissions', () => { }); describe('updateWorkspaceMemberRole', () => { - it('should throw a permission error when user does not have permission (member role)', async () => { + it('should throw a permission error when user does not have permission to update roles (member role)', async () => { const query = { query: ` mutation UpdateWorkspaceMemberRole { @@ -154,5 +158,106 @@ describe('roles permissions', () => { expect(res.body.errors[0].extensions.code).toBe(ErrorCode.FORBIDDEN); }); }); + + it('should throw a permission error when tries to update their own role (admin role)', async () => { + const query = { + query: ` + mutation UpdateWorkspaceMemberRole { + updateWorkspaceMemberRole(workspaceMemberId: "${DEV_SEED_WORKSPACE_MEMBER_IDS.TIM}", roleId: "test-role-id") { + id + } + } + `, + }; + + await client + .post('/graphql') + .set('Authorization', `Bearer ${ADMIN_ACCESS_TOKEN}`) + .send(query) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeNull(); + expect(res.body.errors).toBeDefined(); + expect(res.body.errors[0].message).toBe( + PermissionsExceptionMessage.CANNOT_UPDATE_SELF_ROLE, + ); + expect(res.body.errors[0].extensions.code).toBe(ErrorCode.FORBIDDEN); + }); + }); + + it('should allow to update role when user has permission (admin role)', async () => { + // Arrange + const getRolesQuery = { + query: ` + query GetRoles { + getRoles { + id + label + } + } + `, + }; + + const resp = await client + .post('/graphql') + .set('Authorization', `Bearer ${ADMIN_ACCESS_TOKEN}`) + .send(getRolesQuery); + + const memberRoleId = resp.body.data.getRoles.find( + (role) => role.label === 'Member', + ).id; + + const guestRoleId = resp.body.data.getRoles.find( + (role) => role.label === 'Guest', + ).id; + + const updateRoleQuery = { + query: ` + mutation UpdateWorkspaceMemberRole { + updateWorkspaceMemberRole(workspaceMemberId: "${DEV_SEED_WORKSPACE_MEMBER_IDS.PHIL}", roleId: "${memberRoleId}") { + id + } + } + `, + }; + + // Act and assert + await client + .post('/graphql') + .set('Authorization', `Bearer ${ADMIN_ACCESS_TOKEN}`) + .send(updateRoleQuery) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + expect(res.body.data.updateWorkspaceMemberRole.id).toBe( + DEV_SEED_WORKSPACE_MEMBER_IDS.PHIL, + ); + }); + + // Clean + const rollbackRoleUpdateQuery = { + query: ` + mutation UpdateWorkspaceMemberRole { + updateWorkspaceMemberRole(workspaceMemberId: "${DEV_SEED_WORKSPACE_MEMBER_IDS.PHIL}", roleId: "${guestRoleId}") { + id + } + } + `, + }; + + await client + .post('/graphql') + .set('Authorization', `Bearer ${ADMIN_ACCESS_TOKEN}`) + .send(rollbackRoleUpdateQuery) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + expect(res.body.data.updateWorkspaceMemberRole.id).toBe( + DEV_SEED_WORKSPACE_MEMBER_IDS.PHIL, + ); + }); + }); }); });