[permissions] Update updateRole logic + disallow self role-assignment (#10476)
In this PR - updateWorkspaceMemberRole api was changed to stop allowing null as a valid value for roleId. it is not possible anymore to just unassign a role from a user. instead it is only possible to assign a different role to a user, which will unassign them from their previous role. For this reason in the FE the bins icons next to the workspaceMember on a role page were removed - updateWorkspaceMemberRole will throw if a user attempts to update their own role - tests tests tests!
This commit is contained in:
@ -1085,7 +1085,7 @@ export type MutationUpdateWorkspaceFeatureFlagArgs = {
|
|||||||
|
|
||||||
|
|
||||||
export type MutationUpdateWorkspaceMemberRoleArgs = {
|
export type MutationUpdateWorkspaceMemberRoleArgs = {
|
||||||
roleId?: InputMaybe<Scalars['String']>;
|
roleId: Scalars['String'];
|
||||||
workspaceMemberId: Scalars['String'];
|
workspaceMemberId: Scalars['String'];
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -2368,7 +2368,7 @@ export type RoleFragmentFragment = { __typename?: 'Role', id: string, label: str
|
|||||||
|
|
||||||
export type UpdateWorkspaceMemberRoleMutationVariables = Exact<{
|
export type UpdateWorkspaceMemberRoleMutationVariables = Exact<{
|
||||||
workspaceMemberId: Scalars['String'];
|
workspaceMemberId: Scalars['String'];
|
||||||
roleId?: InputMaybe<Scalars['String']>;
|
roleId: Scalars['String'];
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
||||||
@ -4229,7 +4229,7 @@ export type UpdateLabPublicFeatureFlagMutationHookResult = ReturnType<typeof use
|
|||||||
export type UpdateLabPublicFeatureFlagMutationResult = Apollo.MutationResult<UpdateLabPublicFeatureFlagMutation>;
|
export type UpdateLabPublicFeatureFlagMutationResult = Apollo.MutationResult<UpdateLabPublicFeatureFlagMutation>;
|
||||||
export type UpdateLabPublicFeatureFlagMutationOptions = Apollo.BaseMutationOptions<UpdateLabPublicFeatureFlagMutation, UpdateLabPublicFeatureFlagMutationVariables>;
|
export type UpdateLabPublicFeatureFlagMutationOptions = Apollo.BaseMutationOptions<UpdateLabPublicFeatureFlagMutation, UpdateLabPublicFeatureFlagMutationVariables>;
|
||||||
export const UpdateWorkspaceMemberRoleDocument = gql`
|
export const UpdateWorkspaceMemberRoleDocument = gql`
|
||||||
mutation UpdateWorkspaceMemberRole($workspaceMemberId: String!, $roleId: String) {
|
mutation UpdateWorkspaceMemberRole($workspaceMemberId: String!, $roleId: String!) {
|
||||||
updateWorkspaceMemberRole(
|
updateWorkspaceMemberRole(
|
||||||
workspaceMemberId: $workspaceMemberId
|
workspaceMemberId: $workspaceMemberId
|
||||||
roleId: $roleId
|
roleId: $roleId
|
||||||
|
|||||||
@ -7,7 +7,7 @@ export const UPDATE_WORKSPACE_MEMBER_ROLE = gql`
|
|||||||
${ROLE_FRAGMENT}
|
${ROLE_FRAGMENT}
|
||||||
mutation UpdateWorkspaceMemberRole(
|
mutation UpdateWorkspaceMemberRole(
|
||||||
$workspaceMemberId: String!
|
$workspaceMemberId: String!
|
||||||
$roleId: String
|
$roleId: String!
|
||||||
) {
|
) {
|
||||||
updateWorkspaceMemberRole(
|
updateWorkspaceMemberRole(
|
||||||
workspaceMemberId: $workspaceMemberId
|
workspaceMemberId: $workspaceMemberId
|
||||||
|
|||||||
@ -24,7 +24,6 @@ import {
|
|||||||
useUpdateWorkspaceMemberRoleMutation,
|
useUpdateWorkspaceMemberRoleMutation,
|
||||||
} from '~/generated/graphql';
|
} from '~/generated/graphql';
|
||||||
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
||||||
import { RoleAssignmentConfirmationModalMode } from '~/pages/settings/roles/types/RoleAssignmentConfirmationModalMode';
|
|
||||||
import { RoleAssignmentConfirmationModalSelectedWorkspaceMember } from '~/pages/settings/roles/types/RoleAssignmentConfirmationModalSelectedWorkspaceMember';
|
import { RoleAssignmentConfirmationModalSelectedWorkspaceMember } from '~/pages/settings/roles/types/RoleAssignmentConfirmationModalSelectedWorkspaceMember';
|
||||||
import { RoleAssignmentConfirmationModal } from './RoleAssignmentConfirmationModal';
|
import { RoleAssignmentConfirmationModal } from './RoleAssignmentConfirmationModal';
|
||||||
import { RoleAssignmentTableHeader } from './RoleAssignmentTableHeader';
|
import { RoleAssignmentTableHeader } from './RoleAssignmentTableHeader';
|
||||||
@ -73,8 +72,8 @@ export const RoleAssignment = ({ role }: RoleAssignmentProps) => {
|
|||||||
refetchQueries: [GetRolesDocument],
|
refetchQueries: [GetRolesDocument],
|
||||||
});
|
});
|
||||||
|
|
||||||
const [modalMode, setModalMode] =
|
const [confirmationModalIsOpen, setConfirmationModalIsOpen] =
|
||||||
useState<RoleAssignmentConfirmationModalMode | null>(null);
|
useState<boolean>(false);
|
||||||
const [selectedWorkspaceMember, setSelectedWorkspaceMember] =
|
const [selectedWorkspaceMember, setSelectedWorkspaceMember] =
|
||||||
useState<RoleAssignmentConfirmationModalSelectedWorkspaceMember | null>(
|
useState<RoleAssignmentConfirmationModalSelectedWorkspaceMember | null>(
|
||||||
null,
|
null,
|
||||||
@ -110,7 +109,7 @@ export const RoleAssignment = ({ role }: RoleAssignmentProps) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const handleModalClose = () => {
|
const handleModalClose = () => {
|
||||||
setModalMode(null);
|
setConfirmationModalIsOpen(false);
|
||||||
setSelectedWorkspaceMember(null);
|
setSelectedWorkspaceMember(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -122,26 +121,17 @@ export const RoleAssignment = ({ role }: RoleAssignmentProps) => {
|
|||||||
name: `${workspaceMember.name.firstName} ${workspaceMember.name.lastName}`,
|
name: `${workspaceMember.name.firstName} ${workspaceMember.name.lastName}`,
|
||||||
role: existingRole,
|
role: existingRole,
|
||||||
});
|
});
|
||||||
setModalMode('assign');
|
setConfirmationModalIsOpen(true);
|
||||||
closeDropdown();
|
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 () => {
|
const handleConfirm = async () => {
|
||||||
if (!selectedWorkspaceMember || !modalMode) return;
|
if (!selectedWorkspaceMember || !confirmationModalIsOpen) return;
|
||||||
|
|
||||||
await updateWorkspaceMemberRole({
|
await updateWorkspaceMemberRole({
|
||||||
variables: {
|
variables: {
|
||||||
workspaceMemberId: selectedWorkspaceMember.id,
|
workspaceMemberId: selectedWorkspaceMember.id,
|
||||||
roleId: modalMode === 'assign' ? role.id : null,
|
roleId: role.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -183,7 +173,6 @@ export const RoleAssignment = ({ role }: RoleAssignmentProps) => {
|
|||||||
<RoleAssignmentTableRow
|
<RoleAssignmentTableRow
|
||||||
key={workspaceMember.id}
|
key={workspaceMember.id}
|
||||||
workspaceMember={workspaceMember}
|
workspaceMember={workspaceMember}
|
||||||
onRemove={() => handleRemoveClick(workspaceMember)}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Table>
|
</Table>
|
||||||
@ -222,9 +211,8 @@ export const RoleAssignment = ({ role }: RoleAssignmentProps) => {
|
|||||||
/>
|
/>
|
||||||
</StyledBottomSection>
|
</StyledBottomSection>
|
||||||
|
|
||||||
{modalMode && selectedWorkspaceMember && (
|
{confirmationModalIsOpen && selectedWorkspaceMember && (
|
||||||
<RoleAssignmentConfirmationModal
|
<RoleAssignmentConfirmationModal
|
||||||
mode={modalMode}
|
|
||||||
selectedWorkspaceMember={selectedWorkspaceMember}
|
selectedWorkspaceMember={selectedWorkspaceMember}
|
||||||
isOpen={true}
|
isOpen={true}
|
||||||
onClose={handleModalClose}
|
onClose={handleModalClose}
|
||||||
|
|||||||
@ -1,11 +1,9 @@
|
|||||||
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
|
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
|
||||||
import { t } from '@lingui/core/macro';
|
import { t } from '@lingui/core/macro';
|
||||||
import { RoleAssignmentConfirmationModalSubtitle } from '~/pages/settings/roles/components/RoleAssignmentConfirmationModalSubtitle';
|
import { RoleAssignmentConfirmationModalSubtitle } from '~/pages/settings/roles/components/RoleAssignmentConfirmationModalSubtitle';
|
||||||
import { RoleAssignmentConfirmationModalMode } from '~/pages/settings/roles/types/RoleAssignmentConfirmationModalMode';
|
|
||||||
import { RoleAssignmentConfirmationModalSelectedWorkspaceMember } from '~/pages/settings/roles/types/RoleAssignmentConfirmationModalSelectedWorkspaceMember';
|
import { RoleAssignmentConfirmationModalSelectedWorkspaceMember } from '~/pages/settings/roles/types/RoleAssignmentConfirmationModalSelectedWorkspaceMember';
|
||||||
|
|
||||||
type RoleAssignmentConfirmationModalProps = {
|
type RoleAssignmentConfirmationModalProps = {
|
||||||
mode: RoleAssignmentConfirmationModalMode;
|
|
||||||
selectedWorkspaceMember: RoleAssignmentConfirmationModalSelectedWorkspaceMember;
|
selectedWorkspaceMember: RoleAssignmentConfirmationModalSelectedWorkspaceMember;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
@ -14,21 +12,15 @@ type RoleAssignmentConfirmationModalProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const RoleAssignmentConfirmationModal = ({
|
export const RoleAssignmentConfirmationModal = ({
|
||||||
mode,
|
|
||||||
selectedWorkspaceMember,
|
selectedWorkspaceMember,
|
||||||
isOpen,
|
isOpen,
|
||||||
onClose,
|
onClose,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
onRoleClick,
|
onRoleClick,
|
||||||
}: RoleAssignmentConfirmationModalProps) => {
|
}: RoleAssignmentConfirmationModalProps) => {
|
||||||
const isAssignMode = mode === 'assign';
|
|
||||||
const hasExistingRole = !!selectedWorkspaceMember.role;
|
|
||||||
|
|
||||||
const workspaceMemberName = selectedWorkspaceMember.name;
|
const workspaceMemberName = selectedWorkspaceMember.name;
|
||||||
|
|
||||||
const title = isAssignMode
|
const title = t`Assign ${workspaceMemberName}?`;
|
||||||
? t`Assign ${workspaceMemberName}?`
|
|
||||||
: t`Remove ${workspaceMemberName}?`;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConfirmationModal
|
<ConfirmationModal
|
||||||
@ -37,14 +29,13 @@ export const RoleAssignmentConfirmationModal = ({
|
|||||||
title={title}
|
title={title}
|
||||||
subtitle={
|
subtitle={
|
||||||
<RoleAssignmentConfirmationModalSubtitle
|
<RoleAssignmentConfirmationModalSubtitle
|
||||||
mode={mode}
|
|
||||||
selectedWorkspaceMember={selectedWorkspaceMember}
|
selectedWorkspaceMember={selectedWorkspaceMember}
|
||||||
onRoleClick={onRoleClick}
|
onRoleClick={onRoleClick}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
onConfirmClick={onConfirm}
|
onConfirmClick={onConfirm}
|
||||||
deleteButtonText={isAssignMode ? t`Confirm` : t`Remove`}
|
deleteButtonText={t`Confirm`}
|
||||||
confirmButtonAccent={isAssignMode && !hasExistingRole ? 'blue' : 'danger'}
|
confirmButtonAccent="blue"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import { SettingsCard } from '@/settings/components/SettingsCard';
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { t } from '@lingui/core/macro';
|
import { t } from '@lingui/core/macro';
|
||||||
import { IconUser } from 'twenty-ui';
|
import { IconUser } from 'twenty-ui';
|
||||||
import { RoleAssignmentConfirmationModalMode } from '~/pages/settings/roles/types/RoleAssignmentConfirmationModalMode';
|
|
||||||
import { RoleAssignmentConfirmationModalSelectedWorkspaceMember } from '~/pages/settings/roles/types/RoleAssignmentConfirmationModalSelectedWorkspaceMember';
|
import { RoleAssignmentConfirmationModalSelectedWorkspaceMember } from '~/pages/settings/roles/types/RoleAssignmentConfirmationModalSelectedWorkspaceMember';
|
||||||
|
|
||||||
const StyledSettingsCardContainer = styled.div`
|
const StyledSettingsCardContainer = styled.div`
|
||||||
@ -10,40 +9,29 @@ const StyledSettingsCardContainer = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
type RoleAssignmentConfirmationModalSubtitleProps = {
|
type RoleAssignmentConfirmationModalSubtitleProps = {
|
||||||
mode: RoleAssignmentConfirmationModalMode;
|
|
||||||
selectedWorkspaceMember: RoleAssignmentConfirmationModalSelectedWorkspaceMember;
|
selectedWorkspaceMember: RoleAssignmentConfirmationModalSelectedWorkspaceMember;
|
||||||
onRoleClick: (roleId: string) => void;
|
onRoleClick: (roleId: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RoleAssignmentConfirmationModalSubtitle = ({
|
export const RoleAssignmentConfirmationModalSubtitle = ({
|
||||||
mode,
|
|
||||||
selectedWorkspaceMember,
|
selectedWorkspaceMember,
|
||||||
onRoleClick,
|
onRoleClick,
|
||||||
}: RoleAssignmentConfirmationModalSubtitleProps) => {
|
}: RoleAssignmentConfirmationModalSubtitleProps) => {
|
||||||
const isAssignMode = mode === 'assign';
|
|
||||||
const hasExistingRole = !!selectedWorkspaceMember.role;
|
|
||||||
|
|
||||||
const workspaceMemberName = selectedWorkspaceMember.name;
|
const workspaceMemberName = selectedWorkspaceMember.name;
|
||||||
|
|
||||||
if (isAssignMode && hasExistingRole) {
|
return (
|
||||||
return (
|
<>
|
||||||
<>
|
{t`${workspaceMemberName} will be unassigned from the following role:`}
|
||||||
{t`${workspaceMemberName} will be unassigned from the following role:`}
|
<StyledSettingsCardContainer>
|
||||||
<StyledSettingsCardContainer>
|
<SettingsCard
|
||||||
<SettingsCard
|
title={selectedWorkspaceMember.role?.label || ''}
|
||||||
title={selectedWorkspaceMember.role?.label || ''}
|
Icon={<IconUser />}
|
||||||
Icon={<IconUser />}
|
onClick={() =>
|
||||||
onClick={() =>
|
selectedWorkspaceMember.role &&
|
||||||
selectedWorkspaceMember.role &&
|
onRoleClick(selectedWorkspaceMember.role.id)
|
||||||
onRoleClick(selectedWorkspaceMember.role.id)
|
}
|
||||||
}
|
/>
|
||||||
/>
|
</StyledSettingsCardContainer>
|
||||||
</StyledSettingsCardContainer>
|
</>
|
||||||
</>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return isAssignMode
|
|
||||||
? t`Are you sure you want to assign this role?`
|
|
||||||
: t`This member will be unassigned from this role.`;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,13 +2,7 @@ import { Table } from '@/ui/layout/table/components/Table';
|
|||||||
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { t } from '@lingui/core/macro';
|
import { Avatar, OverflowingTextWithTooltip } from 'twenty-ui';
|
||||||
import {
|
|
||||||
Avatar,
|
|
||||||
IconButton,
|
|
||||||
IconTrash,
|
|
||||||
OverflowingTextWithTooltip,
|
|
||||||
} from 'twenty-ui';
|
|
||||||
import { WorkspaceMember } from '~/generated-metadata/graphql';
|
import { WorkspaceMember } from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
const StyledTable = styled(Table)`
|
const StyledTable = styled(Table)`
|
||||||
@ -21,27 +15,13 @@ const StyledIconWrapper = styled.div`
|
|||||||
margin-right: ${({ theme }) => theme.spacing(2)};
|
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 = {
|
type RoleAssignmentTableRowProps = {
|
||||||
workspaceMember: WorkspaceMember;
|
workspaceMember: WorkspaceMember;
|
||||||
onRemove: (workspaceMemberId: string) => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RoleAssignmentTableRow = ({
|
export const RoleAssignmentTableRow = ({
|
||||||
workspaceMember,
|
workspaceMember,
|
||||||
onRemove,
|
|
||||||
}: RoleAssignmentTableRowProps) => {
|
}: RoleAssignmentTableRowProps) => {
|
||||||
const handleRemoveClick = (event: React.MouseEvent) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
onRemove(workspaceMember.id);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledTable>
|
<StyledTable>
|
||||||
<TableRow gridAutoColumns="150px 1fr 1fr">
|
<TableRow gridAutoColumns="150px 1fr 1fr">
|
||||||
@ -62,17 +42,6 @@ export const RoleAssignmentTableRow = ({
|
|||||||
<TableCell>
|
<TableCell>
|
||||||
<OverflowingTextWithTooltip text={workspaceMember.userEmail} />
|
<OverflowingTextWithTooltip text={workspaceMember.userEmail} />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell align={'right'}>
|
|
||||||
<StyledButtonContainer>
|
|
||||||
<IconButton
|
|
||||||
onClick={handleRemoveClick}
|
|
||||||
variant="tertiary"
|
|
||||||
size="medium"
|
|
||||||
Icon={IconTrash}
|
|
||||||
aria-label={t`Remove`}
|
|
||||||
/>
|
|
||||||
</StyledButtonContainer>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</StyledTable>
|
</StyledTable>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
export type RoleAssignmentConfirmationModalMode = 'assign' | 'remove';
|
|
||||||
@ -18,6 +18,7 @@ export enum PermissionsExceptionCode {
|
|||||||
CANNOT_UNASSIGN_LAST_ADMIN = 'CANNOT_UNASSIGN_LAST_ADMIN',
|
CANNOT_UNASSIGN_LAST_ADMIN = 'CANNOT_UNASSIGN_LAST_ADMIN',
|
||||||
UNKNOWN_OPERATION_NAME = 'UNKNOWN_OPERATION_NAME',
|
UNKNOWN_OPERATION_NAME = 'UNKNOWN_OPERATION_NAME',
|
||||||
UNKNOWN_REQUIRED_PERMISSION = 'UNKNOWN_REQUIRED_PERMISSION',
|
UNKNOWN_REQUIRED_PERMISSION = 'UNKNOWN_REQUIRED_PERMISSION',
|
||||||
|
CANNOT_UPDATE_SELF_ROLE = 'CANNOT_UPDATE_SELF_ROLE',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum PermissionsExceptionMessage {
|
export enum PermissionsExceptionMessage {
|
||||||
@ -29,7 +30,8 @@ export enum PermissionsExceptionMessage {
|
|||||||
USER_WORKSPACE_ALREADY_HAS_ROLE = 'User workspace already has role',
|
USER_WORKSPACE_ALREADY_HAS_ROLE = 'User workspace already has role',
|
||||||
WORKSPACE_MEMBER_NOT_FOUND = 'Workspace member not found',
|
WORKSPACE_MEMBER_NOT_FOUND = 'Workspace member not found',
|
||||||
ROLE_NOT_FOUND = 'Role 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_OPERATION_NAME = 'Unknown operation name, cannot determine required permission',
|
||||||
UNKNOWN_REQUIRED_PERMISSION = 'Unknown required permission',
|
UNKNOWN_REQUIRED_PERMISSION = 'Unknown required permission',
|
||||||
|
CANNOT_UPDATE_SELF_ROLE = 'Cannot update self role',
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,6 +14,7 @@ export const permissionGraphqlApiExceptionHandler = (
|
|||||||
switch (error.code) {
|
switch (error.code) {
|
||||||
case PermissionsExceptionCode.PERMISSION_DENIED:
|
case PermissionsExceptionCode.PERMISSION_DENIED:
|
||||||
case PermissionsExceptionCode.CANNOT_UNASSIGN_LAST_ADMIN:
|
case PermissionsExceptionCode.CANNOT_UNASSIGN_LAST_ADMIN:
|
||||||
|
case PermissionsExceptionCode.CANNOT_UPDATE_SELF_ROLE:
|
||||||
throw new ForbiddenError(error.message);
|
throw new ForbiddenError(error.message);
|
||||||
case PermissionsExceptionCode.ROLE_NOT_FOUND:
|
case PermissionsExceptionCode.ROLE_NOT_FOUND:
|
||||||
case PermissionsExceptionCode.USER_WORKSPACE_NOT_FOUND:
|
case PermissionsExceptionCode.USER_WORKSPACE_NOT_FOUND:
|
||||||
|
|||||||
@ -8,14 +8,18 @@ import {
|
|||||||
Resolver,
|
Resolver,
|
||||||
} from '@nestjs/graphql';
|
} from '@nestjs/graphql';
|
||||||
|
|
||||||
import { isDefined } from 'twenty-shared';
|
|
||||||
|
|
||||||
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
|
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 { WorkspaceMember } from 'src/engine/core-modules/user/dtos/workspace-member.dto';
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
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 { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||||
import { SettingsPermissionsGuard } from 'src/engine/guards/settings-permissions.guard';
|
import { SettingsPermissionsGuard } from 'src/engine/guards/settings-permissions.guard';
|
||||||
import { SettingsPermissions } from 'src/engine/metadata-modules/permissions/constants/settings-permissions.constants';
|
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 { 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 { RoleDTO } from 'src/engine/metadata-modules/role/dtos/role.dto';
|
||||||
import { RoleService } from 'src/engine/metadata-modules/role/role.service';
|
import { RoleService } from 'src/engine/metadata-modules/role/role.service';
|
||||||
@ -41,9 +45,17 @@ export class RoleResolver {
|
|||||||
async updateWorkspaceMemberRole(
|
async updateWorkspaceMemberRole(
|
||||||
@AuthWorkspace() workspace: Workspace,
|
@AuthWorkspace() workspace: Workspace,
|
||||||
@Args('workspaceMemberId') workspaceMemberId: string,
|
@Args('workspaceMemberId') workspaceMemberId: string,
|
||||||
@Args('roleId', { type: () => String, nullable: true })
|
@Args('roleId', { type: () => String }) roleId: string,
|
||||||
roleId: string | null,
|
@AuthWorkspaceMemberId()
|
||||||
|
updatorWorkspaceMemberId: string,
|
||||||
): Promise<WorkspaceMember> {
|
): Promise<WorkspaceMember> {
|
||||||
|
if (updatorWorkspaceMemberId === workspaceMemberId) {
|
||||||
|
throw new PermissionsException(
|
||||||
|
PermissionsExceptionMessage.CANNOT_UPDATE_SELF_ROLE,
|
||||||
|
PermissionsExceptionCode.CANNOT_UPDATE_SELF_ROLE,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const workspaceMember =
|
const workspaceMember =
|
||||||
await this.userWorkspaceService.getWorkspaceMemberOrThrow({
|
await this.userWorkspaceService.getWorkspaceMemberOrThrow({
|
||||||
workspaceMemberId,
|
workspaceMemberId,
|
||||||
@ -56,18 +68,11 @@ export class RoleResolver {
|
|||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!isDefined(roleId)) {
|
await this.userRoleService.assignRoleToUserWorkspace({
|
||||||
await this.userRoleService.unassignAllRolesFromUserWorkspace({
|
userWorkspaceId: userWorkspace.id,
|
||||||
userWorkspaceId: userWorkspace.id,
|
workspaceId: workspace.id,
|
||||||
workspaceId: workspace.id,
|
roleId,
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
await this.userRoleService.assignRoleToUserWorkspace({
|
|
||||||
userWorkspaceId: userWorkspace.id,
|
|
||||||
workspaceId: workspace.id,
|
|
||||||
roleId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const roles = await this.userRoleService
|
const roles = await this.userRoleService
|
||||||
.getRolesByUserWorkspaces({
|
.getRolesByUserWorkspaces({
|
||||||
|
|||||||
@ -1,14 +1,15 @@
|
|||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
import { isDefined } from 'twenty-shared';
|
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 { 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 {
|
import {
|
||||||
PermissionsException,
|
PermissionsException,
|
||||||
PermissionsExceptionCode,
|
PermissionsExceptionCode,
|
||||||
|
PermissionsExceptionMessage,
|
||||||
} from 'src/engine/metadata-modules/permissions/permissions.exception';
|
} 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 { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||||
import { UserWorkspaceRoleEntity } from 'src/engine/metadata-modules/role/user-workspace-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';
|
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||||
@ -34,72 +35,22 @@ export class UserRoleService {
|
|||||||
userWorkspaceId: string;
|
userWorkspaceId: string;
|
||||||
roleId: string;
|
roleId: string;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const userWorkspace = await this.userWorkspaceRepository.findOne({
|
await this.validateAssignRoleInput({
|
||||||
where: {
|
userWorkspaceId,
|
||||||
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],
|
|
||||||
workspaceId,
|
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,
|
roleId,
|
||||||
userWorkspaceId: userWorkspace.id,
|
});
|
||||||
|
|
||||||
|
const newUserWorkspaceRole = await this.userWorkspaceRoleRepository.save({
|
||||||
|
roleId,
|
||||||
|
userWorkspaceId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
public async unassignAllRolesFromUserWorkspace({
|
|
||||||
userWorkspaceId,
|
|
||||||
workspaceId,
|
|
||||||
}: {
|
|
||||||
userWorkspaceId: string;
|
|
||||||
workspaceId: string;
|
|
||||||
}): Promise<void> {
|
|
||||||
await this.validatesUserWorkspaceIsNotLastAdminIfUnassigningAdminRoleOrThrow(
|
|
||||||
{
|
|
||||||
userWorkspaceId,
|
|
||||||
workspaceId,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.userWorkspaceRoleRepository.delete({
|
await this.userWorkspaceRoleRepository.delete({
|
||||||
userWorkspaceId,
|
userWorkspaceId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
|
id: Not(newUserWorkspaceRole.id),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -186,34 +137,64 @@ export class UserRoleService {
|
|||||||
return workspaceMembers;
|
return workspaceMembers;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async validatesUserWorkspaceIsNotLastAdminIfUnassigningAdminRoleOrThrow({
|
private async validateAssignRoleInput({
|
||||||
userWorkspaceId,
|
userWorkspaceId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
|
roleId,
|
||||||
}: {
|
}: {
|
||||||
userWorkspaceId: string;
|
userWorkspaceId: string;
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
}): Promise<void> {
|
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({
|
const roles = await this.getRolesByUserWorkspaces({
|
||||||
userWorkspaceIds: [userWorkspaceId],
|
userWorkspaceIds: [userWorkspace.id],
|
||||||
workspaceId,
|
workspaceId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const currentRoles = roles.get(userWorkspaceId);
|
const currentRole = roles.get(userWorkspace.id)?.[0];
|
||||||
|
|
||||||
const adminRole = currentRoles?.find(
|
if (currentRole?.id === roleId) {
|
||||||
(role: RoleDTO) => role.isEditable === false,
|
return;
|
||||||
);
|
}
|
||||||
|
|
||||||
if (isDefined(adminRole)) {
|
if (!(currentRole?.label === ADMIN_ROLE_LABEL)) {
|
||||||
const workspaceMembersWithAdminRole =
|
return;
|
||||||
await this.getWorkspaceMembersAssignedToRole(adminRole.id, workspaceId);
|
}
|
||||||
|
|
||||||
if (workspaceMembersWithAdminRole.length === 1) {
|
const workspaceMembersWithAdminRole =
|
||||||
throw new PermissionsException(
|
await this.getWorkspaceMembersAssignedToRole(currentRole.id, workspaceId);
|
||||||
`Cannot unassign admin role as there is only one admin in the workspace`,
|
|
||||||
PermissionsExceptionCode.CANNOT_UNASSIGN_LAST_ADMIN,
|
if (workspaceMembersWithAdminRole.length === 1) {
|
||||||
);
|
throw new PermissionsException(
|
||||||
}
|
PermissionsExceptionMessage.CANNOT_UNASSIGN_LAST_ADMIN,
|
||||||
|
PermissionsExceptionCode.CANNOT_UNASSIGN_LAST_ADMIN,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 { updateFeatureFlagFactory } from 'test/integration/graphql/utils/update-feature-flag-factory.util';
|
||||||
|
|
||||||
import { SEED_APPLE_WORKSPACE_ID } from 'src/database/typeorm-seeds/core/workspaces';
|
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 { ErrorCode } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||||
import { PermissionsExceptionMessage } from 'src/engine/metadata-modules/permissions/permissions.exception';
|
import { PermissionsExceptionMessage } from 'src/engine/metadata-modules/permissions/permissions.exception';
|
||||||
|
|
||||||
const client = request(`http://localhost:${APP_PORT}`);
|
const client = request(`http://localhost:${APP_PORT}`);
|
||||||
|
|
||||||
describe('roles permissions', () => {
|
describe('roles permissions', () => {
|
||||||
@ -55,44 +57,46 @@ describe('roles permissions', () => {
|
|||||||
expect(resp.status).toBe(200);
|
expect(resp.status).toBe(200);
|
||||||
expect(resp.body.errors).toBeUndefined();
|
expect(resp.body.errors).toBeUndefined();
|
||||||
expect(resp.body.data.getRoles).toHaveLength(3);
|
expect(resp.body.data.getRoles).toHaveLength(3);
|
||||||
expect(resp.body.data.getRoles).toEqual([
|
expect(resp.body.data.getRoles).toEqual(
|
||||||
{
|
expect.arrayContaining([
|
||||||
label: 'Guest',
|
{
|
||||||
workspaceMembers: [
|
label: 'Guest',
|
||||||
{
|
workspaceMembers: [
|
||||||
id: '20202020-1553-45c6-a028-5a9064cce07f',
|
{
|
||||||
name: {
|
id: '20202020-1553-45c6-a028-5a9064cce07f',
|
||||||
firstName: 'Phil',
|
name: {
|
||||||
lastName: 'Schiler',
|
firstName: 'Phil',
|
||||||
|
lastName: 'Schiler',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
],
|
},
|
||||||
},
|
{
|
||||||
{
|
label: 'Admin',
|
||||||
label: 'Admin',
|
workspaceMembers: [
|
||||||
workspaceMembers: [
|
{
|
||||||
{
|
id: '20202020-0687-4c41-b707-ed1bfca972a7',
|
||||||
id: '20202020-0687-4c41-b707-ed1bfca972a7',
|
name: {
|
||||||
name: {
|
firstName: 'Tim',
|
||||||
firstName: 'Tim',
|
lastName: 'Apple',
|
||||||
lastName: 'Apple',
|
},
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
],
|
},
|
||||||
},
|
{
|
||||||
{
|
label: 'Member',
|
||||||
label: 'Member',
|
workspaceMembers: [
|
||||||
workspaceMembers: [
|
{
|
||||||
{
|
id: '20202020-77d5-4cb6-b60a-f4a835a85d61',
|
||||||
id: '20202020-77d5-4cb6-b60a-f4a835a85d61',
|
name: {
|
||||||
name: {
|
firstName: 'Jony',
|
||||||
firstName: 'Jony',
|
lastName: 'Ive',
|
||||||
lastName: 'Ive',
|
},
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
],
|
},
|
||||||
},
|
]),
|
||||||
]);
|
);
|
||||||
});
|
});
|
||||||
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 (member role)', async () => {
|
||||||
const query = {
|
const query = {
|
||||||
@ -129,7 +133,7 @@ describe('roles permissions', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('updateWorkspaceMemberRole', () => {
|
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 = {
|
const query = {
|
||||||
query: `
|
query: `
|
||||||
mutation UpdateWorkspaceMemberRole {
|
mutation UpdateWorkspaceMemberRole {
|
||||||
@ -154,5 +158,106 @@ describe('roles permissions', () => {
|
|||||||
expect(res.body.errors[0].extensions.code).toBe(ErrorCode.FORBIDDEN);
|
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,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user