Introduce updateWorkspaceMemberSettings and clarify product (#19441)
## Summary Introduces a dedicated **metadata** mutation to update **standard (non-custom)** workspace member settings, moves profile-related UI to use it, and aligns **workspace member** record permissions with the rest of the CRM so users cannot escalate visibility via RLS by editing their own member record. ## Product behaviour ### Profile and appearance (standard fields) - Users can still update **their own** standard workspace member fields that the product exposes in **Settings / Profile** (e.g. name, locale, color scheme, avatar flow) via the new **`updateWorkspaceMemberSettings`** mutation. - The mutation returns a **boolean**; the app **merges** the updated fields into local state so the UI stays in sync without refetching the full workspace member record. - **Locale** changes also keep **`userWorkspace`** in sync when a locale is present in the payload (including from the workspace `updateOne` path when applicable). ### Custom fields on workspace members - The dedicated metadata mutation **rejects** any **custom** workspace member field (and unknown keys). Those updates must go through the normal **object** `updateOne` pipeline, which is subject to **object- and field-level** permissions like other records. But since we don't have object- and field-level permission configuration for system objects yet, this permission is derived from Workspace member settings permission. - **Workspace member** is no longer exempt from ORM permission validation for updates merely because it is a **system** object. Users who **do not** have workspace member access (e.g. no **Workspace members** settings permission and no equivalent broad settings access on the role) **cannot** use `updateOne` on `workspaceMember` to change **custom** (or other) fields on their own row—even though that row is used for RLS predicates. - This closes a path where someone could widen what they can see by writing to fields that drive row-level rules. ### Who can change another member - Updating **another** user’s workspace member still requires **Workspace members** (or equivalent) settings permission, consistent with admin tooling.
This commit is contained in:
+1
@@ -75,6 +75,7 @@ export class WorkspaceMemberDeleteOnePostQueryHook
|
||||
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
|
||||
workspace.id,
|
||||
'workspaceMember',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
return workspaceMemberRepository.findOne({
|
||||
|
||||
+5
-42
@@ -1,22 +1,13 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { isDefined } from 'class-validator';
|
||||
import { assertIsDefinedOrThrow } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { type WorkspacePreQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
|
||||
import { type UpdateOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
|
||||
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
|
||||
import { CoreEntityCacheService } from 'src/engine/core-entity-cache/services/core-entity-cache.service';
|
||||
import {
|
||||
AuthException,
|
||||
AuthExceptionCode,
|
||||
} from 'src/engine/core-modules/auth/auth.exception';
|
||||
import { isApiKeyAuthContext } from 'src/engine/core-modules/auth/guards/is-api-key-auth-context.guard';
|
||||
import { isUserAuthContext } from 'src/engine/core-modules/auth/guards/is-user-auth-context.guard';
|
||||
import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
|
||||
import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception';
|
||||
import { WorkspaceMemberPreQueryHookService } from 'src/modules/workspace-member/query-hooks/workspace-member-pre-query-hook.service';
|
||||
|
||||
@@ -26,9 +17,7 @@ export class WorkspaceMemberUpdateOnePreQueryHook
|
||||
{
|
||||
constructor(
|
||||
private readonly workspaceMemberPreQueryHookService: WorkspaceMemberPreQueryHookService,
|
||||
@InjectRepository(UserWorkspaceEntity)
|
||||
private readonly userWorkspaceRepository: Repository<UserWorkspaceEntity>,
|
||||
private readonly coreEntityCacheService: CoreEntityCacheService,
|
||||
private readonly userWorkspaceService: UserWorkspaceService,
|
||||
) {}
|
||||
|
||||
async execute(
|
||||
@@ -45,43 +34,17 @@ export class WorkspaceMemberUpdateOnePreQueryHook
|
||||
userWorkspaceId: isUserAuthContext(authContext)
|
||||
? authContext.userWorkspaceId
|
||||
: undefined,
|
||||
workspaceMemberId: isUserAuthContext(authContext)
|
||||
? authContext.workspaceMemberId
|
||||
: undefined,
|
||||
targettedWorkspaceMemberId: payload.id,
|
||||
workspaceId: workspace.id,
|
||||
apiKey: isApiKeyAuthContext(authContext)
|
||||
? authContext.apiKey
|
||||
: undefined,
|
||||
workspaceMemberId: isUserAuthContext(authContext)
|
||||
? authContext.workspaceMemberId
|
||||
: undefined,
|
||||
},
|
||||
);
|
||||
|
||||
// TODO: remove this code once we have migrated locale update to userWorkspace update
|
||||
if (payload.data.locale && isUserAuthContext(authContext)) {
|
||||
const userWorkspace = await this.userWorkspaceRepository.findOne({
|
||||
where: {
|
||||
id: authContext.userWorkspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!isDefined(userWorkspace)) {
|
||||
throw new AuthException(
|
||||
'User workspace not found',
|
||||
AuthExceptionCode.USER_WORKSPACE_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
await this.userWorkspaceRepository.save({
|
||||
...userWorkspace,
|
||||
locale: payload.data.locale,
|
||||
});
|
||||
|
||||
await this.coreEntityCacheService.invalidate(
|
||||
'userWorkspaceEntity',
|
||||
authContext.userWorkspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
await this.workspaceMemberPreQueryHookService.completeOnboardingProfileStepIfNameProvided(
|
||||
{
|
||||
userId: isUserAuthContext(authContext)
|
||||
|
||||
Reference in New Issue
Block a user