[demo] Allow workspace to work with a non-system workspaceMember object (#15547)

For demo purposes, we want to work with workspaceMember has a non-system
object, allowing it to be displayed on the product, to be added custom
fields etc.
It is something we may want to do later anyway. 
This PR adds a bypassPermissionChecks on workspaceMember repository
calls. This does not provide more permissions than before for other
workspaces: workspaceMember being a system object, permissions are
already bypassed for this one. (Note that actions such inviting a new
workspaceMember, updating a workspaceMember are protected by a system
permission checked in the relevant places).

This work does **not** allow any workspace to switch their
workspaceMember object to non-system, the switch is still manual.
This commit is contained in:
Marie
2025-11-03 14:57:37 +01:00
committed by GitHub
parent 9e1c854b27
commit 0975b06a9b
12 changed files with 30 additions and 11 deletions
@@ -7,13 +7,13 @@ import { Repository } from 'typeorm';
import { buildCreatedByFromApiKey } from 'src/engine/core-modules/actor/utils/build-created-by-from-api-key.util';
import { buildCreatedByFromFullNameMetadata } from 'src/engine/core-modules/actor/utils/build-created-by-from-full-name-metadata.util';
import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception';
import { type ActorMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util';
import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type CreateInput = Record<string, any>;
@@ -119,6 +119,7 @@ export class CreatedByFromAuthContextService {
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
workspace.id,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
const workspaceMember = await workspaceMemberRepository.findOneOrFail({
@@ -40,6 +40,7 @@ export class ApprovedAccessDomainResolver {
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
currentWorkspace.id,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
const workspaceMember = await workspaceMemberRepository.findOneOrFail({
@@ -173,6 +173,7 @@ export class GoogleAPIsService {
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
workspaceId,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
const workspaceMember = await workspaceMemberRepository.findOneOrFail(
@@ -82,6 +82,7 @@ export class AccessTokenService {
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
workspaceId,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
const workspaceMember = await workspaceMemberRepository.findOne({
@@ -885,7 +885,9 @@ describe('UserWorkspaceService', () => {
expect(
twentyORMGlobalManager.getRepositoryForWorkspace,
).toHaveBeenCalledWith(workspaceId, 'workspaceMember');
).toHaveBeenCalledWith(workspaceId, 'workspaceMember', {
shouldBypassPermissionChecks: true,
});
expect(workspaceMemberRepository.findOne).toHaveBeenCalledWith({
where: {
id: workspaceMemberId,
@@ -86,6 +86,7 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspaceEntit
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
workspaceId,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
const userWorkspace = await this.userWorkspaceRepository.findOneOrFail({
@@ -314,6 +315,7 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspaceEntit
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
workspaceId,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
const workspaceMember = await workspaceMemberRepository.findOne({
@@ -1,22 +1,22 @@
import { Test, type TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { type Repository } from 'typeorm';
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
import { type Repository } from 'typeorm';
import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
import { AuthException } from 'src/engine/core-modules/auth/auth.exception';
import { UserService } from 'src/engine/core-modules/user/services/user.service';
import { UserEntity } from 'src/engine/core-modules/user/user.entity';
import { WorkspaceService } from 'src/engine/core-modules/workspace/services/workspace.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
import { AuthException } from 'src/engine/core-modules/auth/auth.exception';
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import {
PermissionsException,
PermissionsExceptionCode,
} from 'src/engine/metadata-modules/permissions/permissions.exception';
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
describe('UserService', () => {
let service: UserService;
@@ -107,7 +107,9 @@ describe('UserService', () => {
expect(
twentyORMGlobalManager.getRepositoryForWorkspace,
).toHaveBeenCalledWith('w1', 'workspaceMember');
).toHaveBeenCalledWith('w1', 'workspaceMember', {
shouldBypassPermissionChecks: true,
});
expect(mockWorkspaceMemberRepo.findOne).toHaveBeenCalledWith({
where: { userId: 'u1' },
});
@@ -4,9 +4,9 @@ import assert from 'assert';
import { msg } from '@lingui/core/macro';
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
import { assertIsDefinedOrThrow } from 'twenty-shared/utils';
import { isWorkspaceActiveOrSuspended } from 'twenty-shared/workspace';
import { IsNull, Not, Repository } from 'typeorm';
import { assertIsDefinedOrThrow } from 'twenty-shared/utils';
import {
AuthException,
@@ -46,6 +46,7 @@ export class UserService extends TypeOrmQueryService<UserEntity> {
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
workspace.id,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
return await workspaceMemberRepository.findOne({
@@ -64,6 +65,7 @@ export class UserService extends TypeOrmQueryService<UserEntity> {
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
workspace.id,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
return await workspaceMemberRepository.find({ withDeleted: withDeleted });
@@ -78,6 +80,7 @@ export class UserService extends TypeOrmQueryService<UserEntity> {
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
workspace.id,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
return await workspaceMemberRepository.find({
@@ -104,6 +107,7 @@ export class UserService extends TypeOrmQueryService<UserEntity> {
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
workspaceId,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
const workspaceMembers = await workspaceMemberRepository.find();
@@ -71,6 +71,7 @@ export class WorkflowTriggerResolver {
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
workspace.id,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
const workspaceMember = await workspaceMemberRepository.findOneOrFail({
@@ -60,6 +60,7 @@ export class WorkspaceInvitationResolver {
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
workspace.id,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
const workspaceMember = await workspaceMemberRepository.findOneOrFail({
@@ -100,6 +101,7 @@ export class WorkspaceInvitationResolver {
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
workspace.id,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
const workspaceMember = await workspaceMemberRepository.findOneOrFail({
@@ -29,6 +29,7 @@ export class ConnectedAccountListener {
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
workspaceId,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
const workspaceMember = await workspaceMemberRepository.findOneOrFail({
where: { id: workspaceMemberId },
@@ -110,6 +110,7 @@ export class MatchParticipantService<
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
workspaceId,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
const chunkSize = 200;