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:
+2
-2
@@ -3,19 +3,19 @@ import { Args, Mutation, Query } from '@nestjs/graphql';
|
||||
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
|
||||
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
|
||||
import { ApprovedAccessDomainExceptionFilter } from 'src/engine/core-modules/approved-access-domain/approved-access-domain-exception-filter';
|
||||
import { ApprovedAccessDomainDTO } from 'src/engine/core-modules/approved-access-domain/dtos/approved-access-domain.dto';
|
||||
import { CreateApprovedAccessDomainInput } from 'src/engine/core-modules/approved-access-domain/dtos/create-approved-access.domain.input';
|
||||
import { DeleteApprovedAccessDomainInput } from 'src/engine/core-modules/approved-access-domain/dtos/delete-approved-access-domain.input';
|
||||
import { ValidateApprovedAccessDomainInput } from 'src/engine/core-modules/approved-access-domain/dtos/validate-approved-access-domain.input';
|
||||
import { ApprovedAccessDomainService } from 'src/engine/core-modules/approved-access-domain/services/approved-access-domain.service';
|
||||
import { type AuthContextUser } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter';
|
||||
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
|
||||
import { type AuthContextUser } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
|
||||
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
|
||||
+1
@@ -34,6 +34,7 @@ export class UpdateSubscriptionQuantityJob {
|
||||
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
|
||||
data.workspaceId,
|
||||
'workspaceMember',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
const workspaceMembersCount = await workspaceMemberRepository.count();
|
||||
|
||||
+5
-6
@@ -4,6 +4,7 @@ import { NestjsQueryGraphQLModule } from '@ptc-org/nestjs-query-graphql';
|
||||
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
|
||||
|
||||
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
|
||||
import { CoreEntityCacheModule } from 'src/engine/core-entity-cache/core-entity-cache.module';
|
||||
import { ApprovedAccessDomainModule } from 'src/engine/core-modules/approved-access-domain/approved-access-domain.module';
|
||||
import { TokenModule } from 'src/engine/core-modules/auth/token/token.module';
|
||||
import { WorkspaceDomainsModule } from 'src/engine/core-modules/domain/workspace-domains/workspace-domains.module';
|
||||
@@ -11,7 +12,6 @@ import { EnterpriseModule } from 'src/engine/core-modules/enterprise/enterprise.
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { FileModule } from 'src/engine/core-modules/file/file.module';
|
||||
import { OnboardingModule } from 'src/engine/core-modules/onboarding/onboarding.module';
|
||||
import { UploadProfilePicturePermissionGuard } from 'src/engine/core-modules/user-workspace/guards/upload-profile-picture-permission.guard';
|
||||
import { UserWorkspaceEntityCacheProviderService } from 'src/engine/core-modules/user-workspace/services/user-workspace-entity-cache-provider.service';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
|
||||
@@ -21,6 +21,7 @@ import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.ent
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { RoleValidationModule } from 'src/engine/metadata-modules/role-validation/role-validation.module';
|
||||
import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module';
|
||||
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
|
||||
@@ -35,6 +36,7 @@ import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/works
|
||||
UserWorkspaceEntity,
|
||||
WorkspaceEntity,
|
||||
RoleTargetEntity,
|
||||
RoleEntity,
|
||||
]),
|
||||
RoleValidationModule,
|
||||
NestjsQueryTypeOrmModule.forFeature([ObjectMetadataEntity]),
|
||||
@@ -51,15 +53,12 @@ import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/works
|
||||
OnboardingModule,
|
||||
EnterpriseModule,
|
||||
FeatureFlagModule,
|
||||
CoreEntityCacheModule,
|
||||
],
|
||||
services: [UserWorkspaceService],
|
||||
}),
|
||||
],
|
||||
exports: [UserWorkspaceService],
|
||||
providers: [
|
||||
UserWorkspaceService,
|
||||
UserWorkspaceEntityCacheProviderService,
|
||||
UploadProfilePicturePermissionGuard,
|
||||
],
|
||||
providers: [UserWorkspaceService, UserWorkspaceEntityCacheProviderService],
|
||||
})
|
||||
export class UserWorkspaceModule {}
|
||||
|
||||
+7
@@ -5,6 +5,7 @@ import { type DataSource, type Repository } from 'typeorm';
|
||||
|
||||
import { type ApprovedAccessDomainEntity } from 'src/engine/core-modules/approved-access-domain/approved-access-domain.entity';
|
||||
import { ApprovedAccessDomainService } from 'src/engine/core-modules/approved-access-domain/services/approved-access-domain.service';
|
||||
import { CoreEntityCacheService } from 'src/engine/core-entity-cache/services/core-entity-cache.service';
|
||||
import { AuthException } from 'src/engine/core-modules/auth/auth.exception';
|
||||
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
|
||||
import { type AuthContextUser } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
@@ -138,6 +139,12 @@ describe('UserWorkspaceService', () => {
|
||||
setOnboardingCreateProfilePending: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: CoreEntityCacheService,
|
||||
useValue: {
|
||||
invalidate: jest.fn().mockResolvedValue(undefined),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: FeatureFlagService,
|
||||
useValue: {
|
||||
|
||||
+28
@@ -7,6 +7,7 @@ import { FileFolder } from 'twenty-shared/types';
|
||||
import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
|
||||
import { IsNull, Not, type QueryRunner, type Repository } from 'typeorm';
|
||||
|
||||
import { CoreEntityCacheService } from 'src/engine/core-entity-cache/services/core-entity-cache.service';
|
||||
import { FileStorageExceptionCode } from 'src/engine/core-modules/file-storage/interfaces/file-storage-exception';
|
||||
|
||||
import { type AppTokenEntity } from 'src/engine/core-modules/app-token/app-token.entity';
|
||||
@@ -62,10 +63,37 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspaceEntit
|
||||
private readonly fileCorePictureService: FileCorePictureService,
|
||||
private readonly fileUrlService: FileUrlService,
|
||||
private readonly onboardingService: OnboardingService,
|
||||
private readonly coreEntityCacheService: CoreEntityCacheService,
|
||||
) {
|
||||
super(userWorkspaceRepository);
|
||||
}
|
||||
|
||||
async updateUserWorkspaceLocaleForUserWorkspace({
|
||||
locale,
|
||||
userWorkspaceId,
|
||||
}: {
|
||||
locale: UserWorkspaceEntity['locale'];
|
||||
userWorkspaceId: string;
|
||||
}): Promise<void> {
|
||||
const userWorkspace = await this.userWorkspaceRepository.findOne({
|
||||
where: {
|
||||
id: userWorkspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!isDefined(userWorkspace)) {
|
||||
return;
|
||||
}
|
||||
|
||||
userWorkspace.locale = locale;
|
||||
await this.userWorkspaceRepository.save(userWorkspace);
|
||||
|
||||
await this.coreEntityCacheService.invalidate(
|
||||
'userWorkspaceEntity',
|
||||
userWorkspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
async create(
|
||||
{
|
||||
userId,
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
import { IsObject, IsUUID } from 'class-validator';
|
||||
import GraphQLJSON from 'graphql-type-json';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
@InputType()
|
||||
export class UpdateWorkspaceMemberSettingsInput {
|
||||
@Field(() => UUIDScalarType)
|
||||
@IsUUID()
|
||||
workspaceMemberId: string;
|
||||
|
||||
@Field(() => GraphQLJSON)
|
||||
@IsObject()
|
||||
update: Record<string, unknown>;
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { NestjsQueryGraphQLModule } from '@ptc-org/nestjs-query-graphql';
|
||||
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
|
||||
|
||||
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
|
||||
import { CoreEntityCacheModule } from 'src/engine/core-entity-cache/core-entity-cache.module';
|
||||
import { AuditModule } from 'src/engine/core-modules/audit/audit.module';
|
||||
import { WorkspaceDomainsModule } from 'src/engine/core-modules/domain/workspace-domains/workspace-domains.module';
|
||||
import { EmailVerificationModule } from 'src/engine/core-modules/email-verification/email-verification.module';
|
||||
@@ -25,7 +26,6 @@ import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.mod
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module';
|
||||
import { CoreEntityCacheModule } from 'src/engine/core-entity-cache/core-entity-cache.module';
|
||||
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
|
||||
|
||||
import { userAutoResolverOpts } from './user.auto-resolver-opts';
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
AuthExceptionCode,
|
||||
} from 'src/engine/core-modules/auth/auth.exception';
|
||||
import { AvailableWorkspaces } from 'src/engine/core-modules/auth/dto/available-workspaces.dto';
|
||||
import { type AuthContextUser } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { OnboardingStatus } from 'src/engine/core-modules/onboarding/enums/onboarding-status.enum';
|
||||
import {
|
||||
OnboardingService,
|
||||
@@ -31,6 +32,7 @@ import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user
|
||||
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
|
||||
import { DeletedWorkspaceMemberDTO } from 'src/engine/core-modules/user/dtos/deleted-workspace-member.dto';
|
||||
import { UpdateUserEmailInput } from 'src/engine/core-modules/user/dtos/update-user-email.input';
|
||||
import { UpdateWorkspaceMemberSettingsInput } from 'src/engine/core-modules/user/dtos/update-workspace-member-settings.input';
|
||||
import { WorkspaceMemberDTO } from 'src/engine/core-modules/user/dtos/workspace-member.dto';
|
||||
import { UserService } from 'src/engine/core-modules/user/services/user.service';
|
||||
import {
|
||||
@@ -38,15 +40,16 @@ import {
|
||||
WorkspaceMemberTranspiler,
|
||||
} from 'src/engine/core-modules/user/services/workspace-member-transpiler.service';
|
||||
import { UserVarsService } from 'src/engine/core-modules/user/user-vars/services/user-vars.service';
|
||||
import { type AuthContextUser } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { UserEntity } from 'src/engine/core-modules/user/user.entity';
|
||||
import { userValidator } from 'src/engine/core-modules/user/user.validate';
|
||||
import { assertWorkspaceMemberUpdateUsesNonCustomFieldsOnly } from 'src/engine/core-modules/user/utils/assert-workspace-member-update-non-custom-fields.util';
|
||||
import { AuthProviderEnum } from 'src/engine/core-modules/workspace/types/workspace.type';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthApiKey } from 'src/engine/decorators/auth/auth-api-key.decorator';
|
||||
import { AuthProvider } from 'src/engine/decorators/auth/auth-provider.decorator';
|
||||
import { AuthUserWorkspaceId } from 'src/engine/decorators/auth/auth-user-workspace-id.decorator';
|
||||
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
|
||||
import { AuthWorkspaceMemberId } from 'src/engine/decorators/auth/auth-workspace-member-id.decorator';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { CustomPermissionGuard } from 'src/engine/guards/custom-permission.guard';
|
||||
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
|
||||
@@ -437,6 +440,106 @@ export class UserResolver {
|
||||
});
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean)
|
||||
@UseGuards(WorkspaceAuthGuard, CustomPermissionGuard)
|
||||
async updateWorkspaceMemberSettings(
|
||||
@Args('input') input: UpdateWorkspaceMemberSettingsInput,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
@AuthUserWorkspaceId({ allowUndefined: true }) userWorkspaceId?: string,
|
||||
@AuthApiKey() apiKey?: ApiKeyEntity,
|
||||
@AuthWorkspaceMemberId() authenticatedWorkspaceMemberId?: string,
|
||||
@AuthUser({ allowUndefined: true }) user?: AuthContextUser | null,
|
||||
): Promise<boolean> {
|
||||
let isUpdatingSelf =
|
||||
isDefined(authenticatedWorkspaceMemberId) &&
|
||||
authenticatedWorkspaceMemberId === input.workspaceMemberId;
|
||||
|
||||
// for signup
|
||||
if (!isUpdatingSelf && isDefined(user?.id) && !isDefined(apiKey)) {
|
||||
const targetWorkspaceMember =
|
||||
await this.userWorkspaceService.getWorkspaceMemberOrThrow({
|
||||
workspaceMemberId: input.workspaceMemberId,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
isUpdatingSelf = targetWorkspaceMember.userId === user.id;
|
||||
}
|
||||
|
||||
const canUpdateWorkspaceMember =
|
||||
isUpdatingSelf ||
|
||||
(await this.permissionsService.userHasWorkspaceSettingPermission({
|
||||
userWorkspaceId,
|
||||
workspaceId: workspace.id,
|
||||
setting: PermissionFlagType.WORKSPACE_MEMBERS,
|
||||
apiKeyId: apiKey?.id,
|
||||
}));
|
||||
|
||||
if (!canUpdateWorkspaceMember) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.PERMISSION_DENIED,
|
||||
PermissionsExceptionCode.PERMISSION_DENIED,
|
||||
{
|
||||
userFriendlyMessage: msg`You do not have permission to update this workspace member.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
assertWorkspaceMemberUpdateUsesNonCustomFieldsOnly({
|
||||
update: input.update,
|
||||
});
|
||||
|
||||
const workspaceMemberRepository =
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () =>
|
||||
this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
|
||||
workspace.id,
|
||||
'workspaceMember',
|
||||
{
|
||||
shouldBypassPermissionChecks: true,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
const workspaceMember =
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(() =>
|
||||
workspaceMemberRepository.findOne({
|
||||
where: {
|
||||
id: input.workspaceMemberId,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
if (!isDefined(workspaceMember)) {
|
||||
throw new BadRequestException('Workspace member not found');
|
||||
}
|
||||
|
||||
const workspaceMemberUpdatePayload: Partial<WorkspaceMemberWorkspaceEntity> =
|
||||
{
|
||||
id: workspaceMember.id,
|
||||
...(input.update as Partial<WorkspaceMemberWorkspaceEntity>),
|
||||
};
|
||||
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(() =>
|
||||
workspaceMemberRepository.save(workspaceMemberUpdatePayload),
|
||||
);
|
||||
|
||||
if (isDefined(input.update.locale)) {
|
||||
const targetUserWorkspace =
|
||||
await this.userWorkspaceService.getUserWorkspaceForUserOrThrow({
|
||||
userId: workspaceMember.userId,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
await this.userWorkspaceService.updateUserWorkspaceLocaleForUserWorkspace(
|
||||
{
|
||||
locale: input.update.locale as UserWorkspaceEntity['locale'],
|
||||
userWorkspaceId: targetUserWorkspace.id,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ResolveField(() => OnboardingStatus, {
|
||||
nullable: true,
|
||||
})
|
||||
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
import { UserInputError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import { assertWorkspaceMemberUpdateUsesNonCustomFieldsOnly } from 'src/engine/core-modules/user/utils/assert-workspace-member-update-non-custom-fields.util';
|
||||
|
||||
describe('assertWorkspaceMemberUpdateUsesNonCustomFieldsOnly', () => {
|
||||
it('should throw when the update payload is empty', () => {
|
||||
expect(() =>
|
||||
assertWorkspaceMemberUpdateUsesNonCustomFieldsOnly({ update: {} }),
|
||||
).toThrow(UserInputError);
|
||||
|
||||
expect(() =>
|
||||
assertWorkspaceMemberUpdateUsesNonCustomFieldsOnly({ update: {} }),
|
||||
).toThrow('Update payload cannot be empty');
|
||||
});
|
||||
|
||||
it('should not throw when all top-level keys are standard workspaceMember fields', () => {
|
||||
expect(() =>
|
||||
assertWorkspaceMemberUpdateUsesNonCustomFieldsOnly({
|
||||
update: {
|
||||
timeZone: 'Europe/Paris',
|
||||
locale: 'en',
|
||||
},
|
||||
}),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it.each(['userId', 'id'] as const)(
|
||||
'should throw when the update includes %s',
|
||||
(fieldName) => {
|
||||
expect(() =>
|
||||
assertWorkspaceMemberUpdateUsesNonCustomFieldsOnly({
|
||||
update: {
|
||||
[fieldName]: 'value',
|
||||
},
|
||||
}),
|
||||
).toThrow(UserInputError);
|
||||
|
||||
expect(() =>
|
||||
assertWorkspaceMemberUpdateUsesNonCustomFieldsOnly({
|
||||
update: {
|
||||
[fieldName]: 'value',
|
||||
},
|
||||
}),
|
||||
).toThrow(
|
||||
`Cannot update custom workspaceMember field via this endpoint: ${fieldName}`,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it('should throw when a top-level key is not in the standard field allowlist', () => {
|
||||
const unknownKey = 'notAWorkspaceMemberField';
|
||||
|
||||
expect(() =>
|
||||
assertWorkspaceMemberUpdateUsesNonCustomFieldsOnly({
|
||||
update: {
|
||||
[unknownKey]: 'value',
|
||||
},
|
||||
}),
|
||||
).toThrow(UserInputError);
|
||||
|
||||
expect(() =>
|
||||
assertWorkspaceMemberUpdateUsesNonCustomFieldsOnly({
|
||||
update: {
|
||||
[unknownKey]: 'value',
|
||||
},
|
||||
}),
|
||||
).toThrow(
|
||||
`Cannot update custom workspaceMember field via this endpoint: ${unknownKey}`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject the payload when a disallowed key is mixed with allowed keys', () => {
|
||||
const unknownKey = 'typoTimeZone';
|
||||
|
||||
expect(() =>
|
||||
assertWorkspaceMemberUpdateUsesNonCustomFieldsOnly({
|
||||
update: {
|
||||
timeZone: 'Europe/Paris',
|
||||
[unknownKey]: 'x',
|
||||
},
|
||||
}),
|
||||
).toThrow(
|
||||
`Cannot update custom workspaceMember field via this endpoint: ${unknownKey}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
|
||||
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
|
||||
|
||||
import { UserInputError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
|
||||
const WORKSPACE_MEMBER_UPDATE_DISALLOWED_FIELD_NAMES = new Set([
|
||||
'id',
|
||||
'userId',
|
||||
]);
|
||||
|
||||
const WORKSPACE_MEMBER_NON_CUSTOM_UPDATE_FIELD_ALLOWLIST = new Set<string>(
|
||||
Object.keys(STANDARD_OBJECTS.workspaceMember.fields).filter(
|
||||
(fieldName) =>
|
||||
!WORKSPACE_MEMBER_UPDATE_DISALLOWED_FIELD_NAMES.has(fieldName),
|
||||
),
|
||||
);
|
||||
|
||||
export const assertWorkspaceMemberUpdateUsesNonCustomFieldsOnly = ({
|
||||
update,
|
||||
}: {
|
||||
update: Record<string, unknown>;
|
||||
}): void => {
|
||||
const updateKeys = Object.keys(update);
|
||||
|
||||
if (updateKeys.length === 0) {
|
||||
throw new UserInputError('Update payload cannot be empty', {
|
||||
userFriendlyMessage: msg`Add at least one field to update.`,
|
||||
});
|
||||
}
|
||||
|
||||
for (const payloadKey of updateKeys) {
|
||||
if (!WORKSPACE_MEMBER_NON_CUSTOM_UPDATE_FIELD_ALLOWLIST.has(payloadKey)) {
|
||||
throw new UserInputError(
|
||||
`Cannot update custom workspaceMember field via this endpoint: ${payloadKey}`,
|
||||
{
|
||||
userFriendlyMessage: msg`"${payloadKey}" is not a valid workspace member field.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
+302
@@ -0,0 +1,302 @@
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
|
||||
import { type Repository } from 'typeorm';
|
||||
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { FieldPermissionEntity } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.entity';
|
||||
import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity';
|
||||
import { PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { WorkspaceRolesPermissionsCacheService } from 'src/engine/metadata-modules/role/services/workspace-roles-permissions-cache.service';
|
||||
import { RowLevelPermissionPredicateGroupEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate-group.entity';
|
||||
import { RowLevelPermissionPredicateEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate.entity';
|
||||
|
||||
const WORKSPACE_ID = '20202020-0000-4000-8000-000000000000';
|
||||
const ROLE_ID = '11111111-1111-4111-8111-111111111111';
|
||||
const WORKSPACE_MEMBER_OBJECT_METADATA_ID =
|
||||
'22222222-2222-4222-8222-222222222222';
|
||||
const WORKFLOW_OBJECT_METADATA_ID = '33333333-3333-4333-8333-333333333333';
|
||||
const PERSON_OBJECT_METADATA_ID = '44444444-4444-4444-8444-444444444444';
|
||||
|
||||
const createBaseRole = (
|
||||
overrides: Partial<RoleEntity> &
|
||||
Pick<RoleEntity, 'permissionFlags' | 'objectPermissions'>,
|
||||
): RoleEntity =>
|
||||
({
|
||||
id: ROLE_ID,
|
||||
label: 'Test role',
|
||||
workspaceId: WORKSPACE_ID,
|
||||
canUpdateAllSettings: false,
|
||||
canAccessAllTools: false,
|
||||
canReadAllObjectRecords: false,
|
||||
canUpdateAllObjectRecords: false,
|
||||
canSoftDeleteAllObjectRecords: false,
|
||||
canDestroyAllObjectRecords: false,
|
||||
description: null,
|
||||
icon: null,
|
||||
isEditable: true,
|
||||
canBeAssignedToUsers: true,
|
||||
canBeAssignedToAgents: true,
|
||||
canBeAssignedToApiKeys: true,
|
||||
fieldPermissions: [],
|
||||
rowLevelPermissionPredicates: [],
|
||||
rowLevelPermissionPredicateGroups: [],
|
||||
...overrides,
|
||||
}) as RoleEntity;
|
||||
|
||||
describe('WorkspaceRolesPermissionsCacheService', () => {
|
||||
let service: WorkspaceRolesPermissionsCacheService;
|
||||
let roleRepository: jest.Mocked<Pick<Repository<RoleEntity>, 'find'>>;
|
||||
let objectMetadataRepository: jest.Mocked<
|
||||
Pick<Repository<ObjectMetadataEntity>, 'find'>
|
||||
>;
|
||||
let objectPermissionRepository: jest.Mocked<
|
||||
Pick<Repository<ObjectPermissionEntity>, 'find'>
|
||||
>;
|
||||
let permissionFlagRepository: jest.Mocked<
|
||||
Pick<Repository<PermissionFlagEntity>, 'find'>
|
||||
>;
|
||||
|
||||
const workspaceObjectMetadataFixture: ObjectMetadataEntity[] = [
|
||||
{
|
||||
id: WORKSPACE_MEMBER_OBJECT_METADATA_ID,
|
||||
isSystem: true,
|
||||
universalIdentifier: STANDARD_OBJECTS.workspaceMember.universalIdentifier,
|
||||
labelIdentifierFieldMetadataId: null,
|
||||
} as ObjectMetadataEntity,
|
||||
{
|
||||
id: WORKFLOW_OBJECT_METADATA_ID,
|
||||
isSystem: true,
|
||||
universalIdentifier: STANDARD_OBJECTS.workflow.universalIdentifier,
|
||||
labelIdentifierFieldMetadataId: null,
|
||||
} as ObjectMetadataEntity,
|
||||
{
|
||||
id: PERSON_OBJECT_METADATA_ID,
|
||||
isSystem: false,
|
||||
universalIdentifier: STANDARD_OBJECTS.person.universalIdentifier,
|
||||
labelIdentifierFieldMetadataId: null,
|
||||
} as ObjectMetadataEntity,
|
||||
];
|
||||
|
||||
beforeEach(async () => {
|
||||
roleRepository = {
|
||||
find: jest.fn(),
|
||||
};
|
||||
|
||||
objectMetadataRepository = {
|
||||
find: jest.fn().mockResolvedValue(workspaceObjectMetadataFixture),
|
||||
};
|
||||
|
||||
objectPermissionRepository = {
|
||||
find: jest.fn().mockResolvedValue([]),
|
||||
};
|
||||
|
||||
permissionFlagRepository = {
|
||||
find: jest.fn().mockResolvedValue([]),
|
||||
};
|
||||
const fieldPermissionRepository = {
|
||||
find: jest.fn().mockResolvedValue([]),
|
||||
};
|
||||
const rowLevelPermissionPredicateRepository = {
|
||||
find: jest.fn().mockResolvedValue([]),
|
||||
};
|
||||
const rowLevelPermissionPredicateGroupRepository = {
|
||||
find: jest.fn().mockResolvedValue([]),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
WorkspaceRolesPermissionsCacheService,
|
||||
{
|
||||
provide: getRepositoryToken(ObjectMetadataEntity),
|
||||
useValue: objectMetadataRepository,
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(RoleEntity),
|
||||
useValue: roleRepository,
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(ObjectPermissionEntity),
|
||||
useValue: objectPermissionRepository,
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(PermissionFlagEntity),
|
||||
useValue: permissionFlagRepository,
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(FieldPermissionEntity),
|
||||
useValue: fieldPermissionRepository,
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(RowLevelPermissionPredicateEntity),
|
||||
useValue: rowLevelPermissionPredicateRepository,
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(RowLevelPermissionPredicateGroupEntity),
|
||||
useValue: rowLevelPermissionPredicateGroupRepository,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get(WorkspaceRolesPermissionsCacheService);
|
||||
});
|
||||
|
||||
describe('workspaceMember object', () => {
|
||||
it('should deny all record permissions when role has neither workspace members access nor update-all-settings', async () => {
|
||||
roleRepository.find.mockResolvedValue([
|
||||
createBaseRole({
|
||||
permissionFlags: [],
|
||||
objectPermissions: [],
|
||||
}),
|
||||
]);
|
||||
|
||||
const result = await service.computeForCache(WORKSPACE_ID);
|
||||
const workspaceMemberPermissions =
|
||||
result[ROLE_ID][WORKSPACE_MEMBER_OBJECT_METADATA_ID];
|
||||
|
||||
expect(workspaceMemberPermissions.canReadObjectRecords).toBe(true);
|
||||
expect(workspaceMemberPermissions.canUpdateObjectRecords).toBe(false);
|
||||
expect(workspaceMemberPermissions.canSoftDeleteObjectRecords).toBe(false);
|
||||
expect(workspaceMemberPermissions.canDestroyObjectRecords).toBe(false);
|
||||
});
|
||||
|
||||
it('should grant all record permissions when role has WORKSPACE_MEMBERS permission flag', async () => {
|
||||
permissionFlagRepository.find.mockResolvedValue([
|
||||
{
|
||||
roleId: ROLE_ID,
|
||||
flag: PermissionFlagType.WORKSPACE_MEMBERS,
|
||||
} as PermissionFlagEntity,
|
||||
]);
|
||||
|
||||
roleRepository.find.mockResolvedValue([
|
||||
createBaseRole({
|
||||
permissionFlags: [],
|
||||
objectPermissions: [],
|
||||
}),
|
||||
]);
|
||||
|
||||
const result = await service.computeForCache(WORKSPACE_ID);
|
||||
const workspaceMemberPermissions =
|
||||
result[ROLE_ID][WORKSPACE_MEMBER_OBJECT_METADATA_ID];
|
||||
|
||||
expect(workspaceMemberPermissions.canReadObjectRecords).toBe(true);
|
||||
expect(workspaceMemberPermissions.canUpdateObjectRecords).toBe(true);
|
||||
expect(workspaceMemberPermissions.canSoftDeleteObjectRecords).toBe(true);
|
||||
expect(workspaceMemberPermissions.canDestroyObjectRecords).toBe(true);
|
||||
});
|
||||
|
||||
it('should grant all record permissions when role has canUpdateAllSettings', async () => {
|
||||
roleRepository.find.mockResolvedValue([
|
||||
createBaseRole({
|
||||
canUpdateAllSettings: true,
|
||||
permissionFlags: [],
|
||||
objectPermissions: [],
|
||||
}),
|
||||
]);
|
||||
|
||||
const result = await service.computeForCache(WORKSPACE_ID);
|
||||
const workspaceMemberPermissions =
|
||||
result[ROLE_ID][WORKSPACE_MEMBER_OBJECT_METADATA_ID];
|
||||
|
||||
expect(workspaceMemberPermissions.canReadObjectRecords).toBe(true);
|
||||
expect(workspaceMemberPermissions.canUpdateObjectRecords).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('workflow object', () => {
|
||||
it('should deny all record permissions when role has neither workflows access nor update-all-settings', async () => {
|
||||
roleRepository.find.mockResolvedValue([
|
||||
createBaseRole({
|
||||
permissionFlags: [],
|
||||
objectPermissions: [],
|
||||
}),
|
||||
]);
|
||||
|
||||
const result = await service.computeForCache(WORKSPACE_ID);
|
||||
const workflowPermissions = result[ROLE_ID][WORKFLOW_OBJECT_METADATA_ID];
|
||||
|
||||
expect(workflowPermissions.canReadObjectRecords).toBe(false);
|
||||
expect(workflowPermissions.canUpdateObjectRecords).toBe(false);
|
||||
expect(workflowPermissions.canSoftDeleteObjectRecords).toBe(false);
|
||||
expect(workflowPermissions.canDestroyObjectRecords).toBe(false);
|
||||
});
|
||||
|
||||
it('should grant all record permissions when role has WORKFLOWS permission flag', async () => {
|
||||
permissionFlagRepository.find.mockResolvedValue([
|
||||
{
|
||||
roleId: ROLE_ID,
|
||||
flag: PermissionFlagType.WORKFLOWS,
|
||||
} as PermissionFlagEntity,
|
||||
]);
|
||||
|
||||
roleRepository.find.mockResolvedValue([
|
||||
createBaseRole({
|
||||
permissionFlags: [],
|
||||
objectPermissions: [],
|
||||
}),
|
||||
]);
|
||||
|
||||
const result = await service.computeForCache(WORKSPACE_ID);
|
||||
const workflowPermissions = result[ROLE_ID][WORKFLOW_OBJECT_METADATA_ID];
|
||||
|
||||
expect(workflowPermissions.canReadObjectRecords).toBe(true);
|
||||
expect(workflowPermissions.canUpdateObjectRecords).toBe(true);
|
||||
expect(workflowPermissions.canSoftDeleteObjectRecords).toBe(true);
|
||||
expect(workflowPermissions.canDestroyObjectRecords).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('regular object (person)', () => {
|
||||
it('should apply object permission overrides when object is not system', async () => {
|
||||
objectPermissionRepository.find.mockResolvedValue([
|
||||
{
|
||||
roleId: ROLE_ID,
|
||||
objectMetadataId: PERSON_OBJECT_METADATA_ID,
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: false,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
canDestroyObjectRecords: false,
|
||||
} as ObjectPermissionEntity,
|
||||
]);
|
||||
|
||||
roleRepository.find.mockResolvedValue([
|
||||
createBaseRole({
|
||||
permissionFlags: [],
|
||||
objectPermissions: [],
|
||||
}),
|
||||
]);
|
||||
|
||||
const result = await service.computeForCache(WORKSPACE_ID);
|
||||
const personPermissions = result[ROLE_ID][PERSON_OBJECT_METADATA_ID];
|
||||
|
||||
expect(personPermissions.canReadObjectRecords).toBe(true);
|
||||
expect(personPermissions.canUpdateObjectRecords).toBe(false);
|
||||
expect(personPermissions.canSoftDeleteObjectRecords).toBe(false);
|
||||
expect(personPermissions.canDestroyObjectRecords).toBe(false);
|
||||
});
|
||||
|
||||
it('should use role-wide CRUD defaults when no object permission row exists', async () => {
|
||||
roleRepository.find.mockResolvedValue([
|
||||
createBaseRole({
|
||||
canReadAllObjectRecords: true,
|
||||
canUpdateAllObjectRecords: true,
|
||||
canSoftDeleteAllObjectRecords: true,
|
||||
canDestroyAllObjectRecords: true,
|
||||
permissionFlags: [],
|
||||
objectPermissions: [],
|
||||
}),
|
||||
]);
|
||||
|
||||
const result = await service.computeForCache(WORKSPACE_ID);
|
||||
const personPermissions = result[ROLE_ID][PERSON_OBJECT_METADATA_ID];
|
||||
|
||||
expect(personPermissions.canReadObjectRecords).toBe(true);
|
||||
expect(personPermissions.canUpdateObjectRecords).toBe(true);
|
||||
expect(personPermissions.canSoftDeleteObjectRecords).toBe(true);
|
||||
expect(personPermissions.canDestroyObjectRecords).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
+57
-39
@@ -28,6 +28,8 @@ const WORKFLOW_STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS = [
|
||||
STANDARD_OBJECTS.workflowRun.universalIdentifier,
|
||||
STANDARD_OBJECTS.workflowVersion.universalIdentifier,
|
||||
] as const;
|
||||
const WORKSPACE_MEMBER_OBJECT_UNIVERSAL_IDENTIFIER =
|
||||
STANDARD_OBJECTS.workspaceMember.universalIdentifier;
|
||||
|
||||
@Injectable()
|
||||
@WorkspaceCache('rolesPermissions')
|
||||
@@ -137,47 +139,66 @@ export class WorkspaceRolesPermissionsCacheService extends WorkspaceCacheProvide
|
||||
let canDestroy = role.canDestroyAllObjectRecords;
|
||||
const restrictedFields: RestrictedFieldsPermissions = {};
|
||||
|
||||
if (
|
||||
const isWorkspaceMemberObject =
|
||||
universalIdentifier === WORKSPACE_MEMBER_OBJECT_UNIVERSAL_IDENTIFIER;
|
||||
const isWorkflowRelatedObject =
|
||||
WORKFLOW_STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.includes(
|
||||
universalIdentifier as (typeof WORKFLOW_STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS)[number],
|
||||
)
|
||||
) {
|
||||
const hasWorkflowsPermissions = this.hasWorkflowsPermissions(
|
||||
role,
|
||||
rolePermissionFlags,
|
||||
);
|
||||
|
||||
if (isWorkflowRelatedObject) {
|
||||
const hasWorkflowsPermissions =
|
||||
this.hasSettingsGatedObjectPermissions(
|
||||
role,
|
||||
rolePermissionFlags,
|
||||
PermissionFlagType.WORKFLOWS,
|
||||
);
|
||||
|
||||
canRead = hasWorkflowsPermissions;
|
||||
canUpdate = hasWorkflowsPermissions;
|
||||
canSoftDelete = hasWorkflowsPermissions;
|
||||
canDestroy = hasWorkflowsPermissions;
|
||||
} else {
|
||||
const objectRecordPermissionsOverride = roleObjectPermissions.find(
|
||||
(objectPermission) =>
|
||||
objectPermission.objectMetadataId === objectMetadataId,
|
||||
);
|
||||
if (isWorkspaceMemberObject) {
|
||||
const hasWorkspaceMembersPermissions =
|
||||
this.hasSettingsGatedObjectPermissions(
|
||||
role,
|
||||
rolePermissionFlags,
|
||||
PermissionFlagType.WORKSPACE_MEMBERS,
|
||||
);
|
||||
|
||||
const getPermissionValue = (
|
||||
overrideValue: boolean | undefined,
|
||||
defaultValue: boolean,
|
||||
) => (isSystem ? true : (overrideValue ?? defaultValue));
|
||||
canRead = true;
|
||||
canUpdate = hasWorkspaceMembersPermissions;
|
||||
canSoftDelete = hasWorkspaceMembersPermissions;
|
||||
canDestroy = hasWorkspaceMembersPermissions;
|
||||
} else {
|
||||
const objectRecordPermissionsOverride = roleObjectPermissions.find(
|
||||
(objectPermission) =>
|
||||
objectPermission.objectMetadataId === objectMetadataId,
|
||||
);
|
||||
|
||||
canRead = getPermissionValue(
|
||||
objectRecordPermissionsOverride?.canReadObjectRecords,
|
||||
canRead,
|
||||
);
|
||||
canUpdate = getPermissionValue(
|
||||
objectRecordPermissionsOverride?.canUpdateObjectRecords,
|
||||
canUpdate,
|
||||
);
|
||||
canSoftDelete = getPermissionValue(
|
||||
objectRecordPermissionsOverride?.canSoftDeleteObjectRecords,
|
||||
canSoftDelete,
|
||||
);
|
||||
canDestroy = getPermissionValue(
|
||||
objectRecordPermissionsOverride?.canDestroyObjectRecords,
|
||||
canDestroy,
|
||||
);
|
||||
const getPermissionValue = (
|
||||
overrideValue: boolean | undefined,
|
||||
defaultValue: boolean,
|
||||
) => (isSystem ? true : (overrideValue ?? defaultValue));
|
||||
|
||||
canRead = getPermissionValue(
|
||||
objectRecordPermissionsOverride?.canReadObjectRecords,
|
||||
canRead,
|
||||
);
|
||||
canUpdate = getPermissionValue(
|
||||
objectRecordPermissionsOverride?.canUpdateObjectRecords,
|
||||
canUpdate,
|
||||
);
|
||||
canSoftDelete = getPermissionValue(
|
||||
objectRecordPermissionsOverride?.canSoftDeleteObjectRecords,
|
||||
canSoftDelete,
|
||||
);
|
||||
canDestroy = getPermissionValue(
|
||||
objectRecordPermissionsOverride?.canDestroyObjectRecords,
|
||||
canDestroy,
|
||||
);
|
||||
}
|
||||
|
||||
const fieldPermissionsForObject = roleFieldPermissions.filter(
|
||||
(fieldPermission) =>
|
||||
@@ -246,21 +267,18 @@ export class WorkspaceRolesPermissionsCacheService extends WorkspaceCacheProvide
|
||||
return workspaceObjectMetadata;
|
||||
}
|
||||
|
||||
private hasWorkflowsPermissions(
|
||||
private hasSettingsGatedObjectPermissions(
|
||||
role: RoleEntity,
|
||||
permissionFlags: PermissionFlagEntity[],
|
||||
permissionFlagType: PermissionFlagType,
|
||||
): boolean {
|
||||
const hasWorkflowsPermissionFromRole = role.canUpdateAllSettings;
|
||||
const hasWorkflowsPermissionsFromSettingPermissions = isDefined(
|
||||
const hasPermissionFromRole = role.canUpdateAllSettings;
|
||||
const hasPermissionFromSettingPermissions = isDefined(
|
||||
permissionFlags.find(
|
||||
(permissionFlag) =>
|
||||
permissionFlag.flag === PermissionFlagType.WORKFLOWS,
|
||||
(permissionFlag) => permissionFlag.flag === permissionFlagType,
|
||||
),
|
||||
);
|
||||
|
||||
return (
|
||||
hasWorkflowsPermissionFromRole ||
|
||||
hasWorkflowsPermissionsFromSettingPermissions
|
||||
);
|
||||
return hasPermissionFromRole || hasPermissionFromSettingPermissions;
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -177,6 +177,7 @@ export class ViewQueryParamsService {
|
||||
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'workspaceMember',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
const workspaceMember = await workspaceMemberRepository.findOne({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import isEmpty from 'lodash.isempty';
|
||||
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
|
||||
import {
|
||||
type ObjectsPermissions,
|
||||
type RestrictedFieldsPermissions,
|
||||
@@ -20,6 +21,9 @@ import {
|
||||
} from 'src/engine/metadata-modules/permissions/permissions.exception';
|
||||
import { getColumnNameToFieldMetadataIdMap } from 'src/engine/twenty-orm/utils/get-column-name-to-field-metadata-id.util';
|
||||
|
||||
const WORKSPACE_MEMBER_OBJECT_UNIVERSAL_IDENTIFIER =
|
||||
STANDARD_OBJECTS.workspaceMember.universalIdentifier;
|
||||
|
||||
const getTargetEntityAndOperationType = (
|
||||
expressionMap: QueryExpressionMap,
|
||||
):
|
||||
@@ -110,8 +114,11 @@ export const validateOperationIsPermittedOrThrow = ({
|
||||
}
|
||||
|
||||
const objectMetadataIsSystem = objectMetadata.isSystem === true;
|
||||
const isWorkspaceMemberObject =
|
||||
objectMetadata.universalIdentifier ===
|
||||
WORKSPACE_MEMBER_OBJECT_UNIVERSAL_IDENTIFIER;
|
||||
|
||||
if (objectMetadataIsSystem) {
|
||||
if (objectMetadataIsSystem && !isWorkspaceMemberObject) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user