[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:
Marie
2025-02-25 15:20:07 +01:00
committed by GitHub
parent 2247d3fa91
commit 9fe5c96d56
12 changed files with 253 additions and 224 deletions

View File

@ -1085,7 +1085,7 @@ export type MutationUpdateWorkspaceFeatureFlagArgs = {
export type MutationUpdateWorkspaceMemberRoleArgs = {
roleId?: InputMaybe<Scalars['String']>;
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<Scalars['String']>;
roleId: Scalars['String'];
}>;
@ -4229,7 +4229,7 @@ export type UpdateLabPublicFeatureFlagMutationHookResult = ReturnType<typeof use
export type UpdateLabPublicFeatureFlagMutationResult = Apollo.MutationResult<UpdateLabPublicFeatureFlagMutation>;
export type UpdateLabPublicFeatureFlagMutationOptions = Apollo.BaseMutationOptions<UpdateLabPublicFeatureFlagMutation, UpdateLabPublicFeatureFlagMutationVariables>;
export const UpdateWorkspaceMemberRoleDocument = gql`
mutation UpdateWorkspaceMemberRole($workspaceMemberId: String!, $roleId: String) {
mutation UpdateWorkspaceMemberRole($workspaceMemberId: String!, $roleId: String!) {
updateWorkspaceMemberRole(
workspaceMemberId: $workspaceMemberId
roleId: $roleId

View File

@ -7,7 +7,7 @@ export const UPDATE_WORKSPACE_MEMBER_ROLE = gql`
${ROLE_FRAGMENT}
mutation UpdateWorkspaceMemberRole(
$workspaceMemberId: String!
$roleId: String
$roleId: String!
) {
updateWorkspaceMemberRole(
workspaceMemberId: $workspaceMemberId

View File

@ -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<RoleAssignmentConfirmationModalMode | null>(null);
const [confirmationModalIsOpen, setConfirmationModalIsOpen] =
useState<boolean>(false);
const [selectedWorkspaceMember, setSelectedWorkspaceMember] =
useState<RoleAssignmentConfirmationModalSelectedWorkspaceMember | null>(
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) => {
<RoleAssignmentTableRow
key={workspaceMember.id}
workspaceMember={workspaceMember}
onRemove={() => handleRemoveClick(workspaceMember)}
/>
))}
</Table>
@ -222,9 +211,8 @@ export const RoleAssignment = ({ role }: RoleAssignmentProps) => {
/>
</StyledBottomSection>
{modalMode && selectedWorkspaceMember && (
{confirmationModalIsOpen && selectedWorkspaceMember && (
<RoleAssignmentConfirmationModal
mode={modalMode}
selectedWorkspaceMember={selectedWorkspaceMember}
isOpen={true}
onClose={handleModalClose}

View File

@ -1,11 +1,9 @@
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { t } from '@lingui/core/macro';
import { RoleAssignmentConfirmationModalSubtitle } from '~/pages/settings/roles/components/RoleAssignmentConfirmationModalSubtitle';
import { RoleAssignmentConfirmationModalMode } from '~/pages/settings/roles/types/RoleAssignmentConfirmationModalMode';
import { RoleAssignmentConfirmationModalSelectedWorkspaceMember } from '~/pages/settings/roles/types/RoleAssignmentConfirmationModalSelectedWorkspaceMember';
type RoleAssignmentConfirmationModalProps = {
mode: RoleAssignmentConfirmationModalMode;
selectedWorkspaceMember: RoleAssignmentConfirmationModalSelectedWorkspaceMember;
isOpen: boolean;
onClose: () => 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 (
<ConfirmationModal
@ -37,14 +29,13 @@ export const RoleAssignmentConfirmationModal = ({
title={title}
subtitle={
<RoleAssignmentConfirmationModalSubtitle
mode={mode}
selectedWorkspaceMember={selectedWorkspaceMember}
onRoleClick={onRoleClick}
/>
}
onConfirmClick={onConfirm}
deleteButtonText={isAssignMode ? t`Confirm` : t`Remove`}
confirmButtonAccent={isAssignMode && !hasExistingRole ? 'blue' : 'danger'}
deleteButtonText={t`Confirm`}
confirmButtonAccent="blue"
/>
);
};

View File

@ -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:`}
<StyledSettingsCardContainer>
<SettingsCard
title={selectedWorkspaceMember.role?.label || ''}
Icon={<IconUser />}
onClick={() =>
selectedWorkspaceMember.role &&
onRoleClick(selectedWorkspaceMember.role.id)
}
/>
</StyledSettingsCardContainer>
</>
);
}
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:`}
<StyledSettingsCardContainer>
<SettingsCard
title={selectedWorkspaceMember.role?.label || ''}
Icon={<IconUser />}
onClick={() =>
selectedWorkspaceMember.role &&
onRoleClick(selectedWorkspaceMember.role.id)
}
/>
</StyledSettingsCardContainer>
</>
);
};

View File

@ -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 (
<StyledTable>
<TableRow gridAutoColumns="150px 1fr 1fr">
@ -62,17 +42,6 @@ export const RoleAssignmentTableRow = ({
<TableCell>
<OverflowingTextWithTooltip text={workspaceMember.userEmail} />
</TableCell>
<TableCell align={'right'}>
<StyledButtonContainer>
<IconButton
onClick={handleRemoveClick}
variant="tertiary"
size="medium"
Icon={IconTrash}
aria-label={t`Remove`}
/>
</StyledButtonContainer>
</TableCell>
</TableRow>
</StyledTable>
);

View File

@ -1 +0,0 @@
export type RoleAssignmentConfirmationModalMode = 'assign' | 'remove';