[field-level permissions] Upsert fieldPermission + use fieldPermission to compute permissions (#13050)
In this PR
- introduction of fieldPermission entity
- addition of upsertFieldPermission in role resolver
- computing of permissions taking fieldPermission into account. In order
to limit what is stored in Redis we only store fields restrictions. For
instance for objectMetadata with id XXX with a restriction on field with
id YYY we store:
`"XXX":{"canRead":true,"canUpdate":false,"canSoftDelete":false,"canDestroy":false,"restrictedFields":{"YYY":{"canRead":false,"canUpdate":null}}}`
---------
Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
This commit is contained in:
@ -678,6 +678,7 @@ export type FeatureFlagDto = {
|
||||
export enum FeatureFlagKey {
|
||||
IS_AIRTABLE_INTEGRATION_ENABLED = 'IS_AIRTABLE_INTEGRATION_ENABLED',
|
||||
IS_AI_ENABLED = 'IS_AI_ENABLED',
|
||||
IS_FIELDS_PERMISSIONS_ENABLED = 'IS_FIELDS_PERMISSIONS_ENABLED',
|
||||
IS_IMAP_ENABLED = 'IS_IMAP_ENABLED',
|
||||
IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED',
|
||||
IS_MORPH_RELATION_ENABLED = 'IS_MORPH_RELATION_ENABLED',
|
||||
@ -766,6 +767,23 @@ export enum FieldMetadataType {
|
||||
UUID = 'UUID'
|
||||
}
|
||||
|
||||
export type FieldPermission = {
|
||||
__typename?: 'FieldPermission';
|
||||
canReadFieldValue?: Maybe<Scalars['Boolean']>;
|
||||
canUpdateFieldValue?: Maybe<Scalars['Boolean']>;
|
||||
fieldMetadataId: Scalars['String'];
|
||||
id: Scalars['String'];
|
||||
objectMetadataId: Scalars['String'];
|
||||
roleId: Scalars['String'];
|
||||
};
|
||||
|
||||
export type FieldPermissionInput = {
|
||||
canReadFieldValue?: InputMaybe<Scalars['Boolean']>;
|
||||
canUpdateFieldValue?: InputMaybe<Scalars['Boolean']>;
|
||||
fieldMetadataId: Scalars['String'];
|
||||
objectMetadataId: Scalars['String'];
|
||||
};
|
||||
|
||||
export enum FileFolder {
|
||||
Attachment = 'Attachment',
|
||||
PersonPicture = 'PersonPicture',
|
||||
@ -1076,6 +1094,7 @@ export type Mutation = {
|
||||
uploadImage: SignedFileDto;
|
||||
uploadProfilePicture: SignedFileDto;
|
||||
uploadWorkspaceLogo: SignedFileDto;
|
||||
upsertFieldPermissions: Array<FieldPermission>;
|
||||
upsertObjectPermissions: Array<ObjectPermission>;
|
||||
upsertSettingPermissions: Array<SettingPermission>;
|
||||
userLookupAdminPanel: UserLookup;
|
||||
@ -1494,6 +1513,11 @@ export type MutationUploadWorkspaceLogoArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationUpsertFieldPermissionsArgs = {
|
||||
upsertFieldPermissionsInput: UpsertFieldPermissionsInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationUpsertObjectPermissionsArgs = {
|
||||
upsertObjectPermissionsInput: UpsertObjectPermissionsInput;
|
||||
};
|
||||
@ -2484,6 +2508,11 @@ export type UpdateWorkspaceInput = {
|
||||
subdomain?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type UpsertFieldPermissionsInput = {
|
||||
fieldPermissions: Array<FieldPermissionInput>;
|
||||
roleId: Scalars['String'];
|
||||
};
|
||||
|
||||
export type UpsertObjectPermissionsInput = {
|
||||
objectPermissions: Array<ObjectPermissionInput>;
|
||||
roleId: Scalars['String'];
|
||||
|
||||
@ -642,6 +642,7 @@ export type FeatureFlagDto = {
|
||||
export enum FeatureFlagKey {
|
||||
IS_AIRTABLE_INTEGRATION_ENABLED = 'IS_AIRTABLE_INTEGRATION_ENABLED',
|
||||
IS_AI_ENABLED = 'IS_AI_ENABLED',
|
||||
IS_FIELDS_PERMISSIONS_ENABLED = 'IS_FIELDS_PERMISSIONS_ENABLED',
|
||||
IS_IMAP_ENABLED = 'IS_IMAP_ENABLED',
|
||||
IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED',
|
||||
IS_MORPH_RELATION_ENABLED = 'IS_MORPH_RELATION_ENABLED',
|
||||
@ -730,6 +731,23 @@ export enum FieldMetadataType {
|
||||
UUID = 'UUID'
|
||||
}
|
||||
|
||||
export type FieldPermission = {
|
||||
__typename?: 'FieldPermission';
|
||||
canReadFieldValue?: Maybe<Scalars['Boolean']>;
|
||||
canUpdateFieldValue?: Maybe<Scalars['Boolean']>;
|
||||
fieldMetadataId: Scalars['String'];
|
||||
id: Scalars['String'];
|
||||
objectMetadataId: Scalars['String'];
|
||||
roleId: Scalars['String'];
|
||||
};
|
||||
|
||||
export type FieldPermissionInput = {
|
||||
canReadFieldValue?: InputMaybe<Scalars['Boolean']>;
|
||||
canUpdateFieldValue?: InputMaybe<Scalars['Boolean']>;
|
||||
fieldMetadataId: Scalars['String'];
|
||||
objectMetadataId: Scalars['String'];
|
||||
};
|
||||
|
||||
export enum FileFolder {
|
||||
Attachment = 'Attachment',
|
||||
PersonPicture = 'PersonPicture',
|
||||
@ -1027,6 +1045,7 @@ export type Mutation = {
|
||||
uploadImage: SignedFileDto;
|
||||
uploadProfilePicture: SignedFileDto;
|
||||
uploadWorkspaceLogo: SignedFileDto;
|
||||
upsertFieldPermissions: Array<FieldPermission>;
|
||||
upsertObjectPermissions: Array<ObjectPermission>;
|
||||
upsertSettingPermissions: Array<SettingPermission>;
|
||||
userLookupAdminPanel: UserLookup;
|
||||
@ -1405,6 +1424,11 @@ export type MutationUploadWorkspaceLogoArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationUpsertFieldPermissionsArgs = {
|
||||
upsertFieldPermissionsInput: UpsertFieldPermissionsInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationUpsertObjectPermissionsArgs = {
|
||||
upsertObjectPermissionsInput: UpsertObjectPermissionsInput;
|
||||
};
|
||||
@ -2322,6 +2346,11 @@ export type UpdateWorkspaceInput = {
|
||||
subdomain?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type UpsertFieldPermissionsInput = {
|
||||
fieldPermissions: Array<FieldPermissionInput>;
|
||||
roleId: Scalars['String'];
|
||||
};
|
||||
|
||||
export type UpsertObjectPermissionsInput = {
|
||||
objectPermissions: Array<ObjectPermissionInput>;
|
||||
roleId: Scalars['String'];
|
||||
|
||||
@ -0,0 +1,19 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddIndexOnObjectPermission1751890088507
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'AddIndexOnObjectPermission1751890088507';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_OBJECT_PERMISSION_WORKSPACE_ID_ROLE_ID" ON "core"."objectPermission" ("workspaceId", "roleId") `,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "core"."IDX_OBJECT_PERMISSION_WORKSPACE_ID_ROLE_ID"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddFieldPermission1751993324990 implements MigrationInterface {
|
||||
name = 'AddFieldPermission1751993324990';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "core"."fieldPermission" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "roleId" uuid NOT NULL, "objectMetadataId" uuid NOT NULL, "fieldMetadataId" uuid NOT NULL, "canReadFieldValue" boolean, "canUpdateFieldValue" boolean, "workspaceId" uuid NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "IDX_FIELD_PERMISSION_FIELD_METADATA_ID_ROLE_ID_UNIQUE" UNIQUE ("fieldMetadataId", "roleId"), CONSTRAINT "PK_d7bb911e4f9b1b5e3bfcfdd1c4b" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_FIELD_PERMISSION_WORKSPACE_ID_ROLE_ID" ON "core"."fieldPermission" ("workspaceId", "roleId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."fieldPermission" ADD CONSTRAINT "FK_bbf16a91f5a10199e5b18c019ba" FOREIGN KEY ("roleId") REFERENCES "core"."role"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."fieldPermission" ADD CONSTRAINT "FK_dc8e552397f5e44d175fedf752a" FOREIGN KEY ("objectMetadataId") REFERENCES "core"."objectMetadata"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."fieldPermission" ADD CONSTRAINT "FK_d5c47a26fe71648894d05da3d3a" FOREIGN KEY ("fieldMetadataId") REFERENCES "core"."fieldMetadata"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."fieldPermission" ADD CONSTRAINT "FK_2763aee5614b54019d692333fe1" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."fieldPermission" DROP CONSTRAINT "FK_2763aee5614b54019d692333fe1"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."fieldPermission" DROP CONSTRAINT "FK_d5c47a26fe71648894d05da3d3a"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."fieldPermission" DROP CONSTRAINT "FK_dc8e552397f5e44d175fedf752a"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."fieldPermission" DROP CONSTRAINT "FK_bbf16a91f5a10199e5b18c019ba"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "core"."IDX_FIELD_PERMISSION_WORKSPACE_ID_ROLE_ID"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "core"."fieldPermission"`);
|
||||
}
|
||||
}
|
||||
@ -9,4 +9,5 @@ export enum FeatureFlagKey {
|
||||
IS_MORPH_RELATION_ENABLED = 'IS_MORPH_RELATION_ENABLED',
|
||||
IS_WORKFLOW_FILTERING_ENABLED = 'IS_WORKFLOW_FILTERING_ENABLED',
|
||||
IS_RELATION_CONNECT_ENABLED = 'IS_RELATION_CONNECT_ENABLED',
|
||||
IS_FIELDS_PERMISSIONS_ENABLED = 'IS_FIELDS_PERMISSIONS_ENABLED',
|
||||
}
|
||||
|
||||
@ -20,6 +20,7 @@ import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadat
|
||||
import { FieldStandardOverridesDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-standard-overrides.dto';
|
||||
import { IndexFieldMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-field-metadata.entity';
|
||||
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';
|
||||
|
||||
@Entity('fieldMetadata')
|
||||
// max length of index is 63 characters
|
||||
@ -148,4 +149,10 @@ export class FieldMetadataEntity<
|
||||
|
||||
@UpdateDateColumn({ type: 'timestamptz' })
|
||||
updatedAt: Date;
|
||||
|
||||
@OneToMany(
|
||||
() => FieldPermissionEntity,
|
||||
(fieldPermission: FieldPermissionEntity) => fieldPermission.fieldMetadata,
|
||||
)
|
||||
fieldPermissions: Relation<FieldPermissionEntity[]>;
|
||||
}
|
||||
|
||||
@ -25,7 +25,7 @@ import {
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { IsValidMetadataName } from 'src/engine/decorators/metadata/is-valid-metadata-name.decorator';
|
||||
import { IndexFieldMetadataDTO } from 'src/engine/metadata-modules/index-metadata/dtos/index-field-metadata.dto';
|
||||
import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types';
|
||||
import { ObjectMetadataDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-metadata.dto';
|
||||
|
||||
registerEnumType(IndexType, {
|
||||
|
||||
@ -13,13 +13,9 @@ import {
|
||||
} from 'typeorm';
|
||||
|
||||
import { IndexFieldMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-field-metadata.entity';
|
||||
import { IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
|
||||
export enum IndexType {
|
||||
BTREE = 'BTREE',
|
||||
GIN = 'GIN',
|
||||
}
|
||||
|
||||
@Unique('IDX_INDEX_METADATA_NAME_WORKSPACE_ID_OBJECT_METADATA_ID_UNIQUE', [
|
||||
'name',
|
||||
'workspaceId',
|
||||
|
||||
@ -6,10 +6,8 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
import { QueryRunner, Repository } from 'typeorm';
|
||||
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import {
|
||||
IndexMetadataEntity,
|
||||
IndexType,
|
||||
} from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types';
|
||||
import { generateDeterministicIndexName } from 'src/engine/metadata-modules/index-metadata/utils/generate-deterministic-index-name';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util';
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { IndexFieldMetadataInterface } from 'src/engine/metadata-modules/index-metadata/interfaces/index-field-metadata.interface';
|
||||
|
||||
import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types';
|
||||
|
||||
export interface IndexMetadataInterface {
|
||||
id: string;
|
||||
|
||||
@ -0,0 +1,4 @@
|
||||
export enum IndexType {
|
||||
BTREE = 'BTREE',
|
||||
GIN = 'GIN',
|
||||
}
|
||||
@ -5,7 +5,7 @@ import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metad
|
||||
import { IndexFieldMetadataInterface } from 'src/engine/metadata-modules/index-metadata/interfaces/index-field-metadata.interface';
|
||||
import { IndexMetadataInterface } from 'src/engine/metadata-modules/index-metadata/interfaces/index-metadata.interface';
|
||||
|
||||
import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types';
|
||||
import { getUniqueConstraintsFields } from 'src/engine/metadata-modules/index-metadata/utils/getUniqueConstraintsFields.util';
|
||||
|
||||
describe('getUniqueConstraintsFields', () => {
|
||||
|
||||
@ -17,6 +17,7 @@ import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-s
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { ObjectStandardOverridesDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-standard-overrides.dto';
|
||||
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';
|
||||
|
||||
@Entity('objectMetadata')
|
||||
@ -134,4 +135,13 @@ export class ObjectMetadataEntity implements ObjectMetadataInterface {
|
||||
},
|
||||
)
|
||||
objectPermissions: Relation<ObjectPermissionEntity[]>;
|
||||
|
||||
@OneToMany(
|
||||
() => FieldPermissionEntity,
|
||||
(fieldPermission: FieldPermissionEntity) => fieldPermission.objectMetadata,
|
||||
{
|
||||
cascade: true,
|
||||
},
|
||||
)
|
||||
fieldPermissions: Relation<FieldPermissionEntity[]>;
|
||||
}
|
||||
|
||||
@ -0,0 +1,22 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
@ObjectType('FieldPermission')
|
||||
export class FieldPermissionDTO {
|
||||
@Field({ nullable: false })
|
||||
id: string;
|
||||
|
||||
@Field({ nullable: false })
|
||||
objectMetadataId: string;
|
||||
|
||||
@Field({ nullable: false })
|
||||
fieldMetadataId: string;
|
||||
|
||||
@Field({ nullable: false })
|
||||
roleId: string;
|
||||
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
canReadFieldValue?: boolean | null;
|
||||
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
canUpdateFieldValue?: boolean | null;
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
import {
|
||||
ArrayMinSize,
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsUUID,
|
||||
} from 'class-validator';
|
||||
|
||||
@InputType()
|
||||
export class UpsertFieldPermissionsInput {
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
roleId: string;
|
||||
|
||||
@IsArray()
|
||||
@ArrayMinSize(1)
|
||||
@IsNotEmpty()
|
||||
@Field(() => [FieldPermissionInput])
|
||||
fieldPermissions: FieldPermissionInput[];
|
||||
}
|
||||
|
||||
@InputType()
|
||||
export class FieldPermissionInput {
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
objectMetadataId: string;
|
||||
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
fieldMetadataId: string;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
canReadFieldValue?: boolean | null;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
canUpdateFieldValue?: boolean | null;
|
||||
}
|
||||
@ -0,0 +1,585 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
|
||||
import { ObjectRecordsPermissionsByRoleId } from 'twenty-shared/types';
|
||||
import { In, Repository } from 'typeorm';
|
||||
|
||||
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
||||
|
||||
import {
|
||||
fieldTextMock,
|
||||
objectMetadataItemMock,
|
||||
} from 'src/engine/api/__mocks__/object-metadata-item.mock';
|
||||
import { UpsertFieldPermissionsInput } from 'src/engine/metadata-modules/object-permission/dtos/upsert-field-permissions.input';
|
||||
import { FieldPermissionEntity } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.entity';
|
||||
import { FieldPermissionService } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.service';
|
||||
import {
|
||||
PermissionsException,
|
||||
PermissionsExceptionCode,
|
||||
PermissionsExceptionMessage,
|
||||
} from 'src/engine/metadata-modules/permissions/permissions.exception';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
|
||||
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
||||
|
||||
describe('FieldPermissionService', () => {
|
||||
let service: FieldPermissionService;
|
||||
let fieldPermissionsRepository: jest.Mocked<
|
||||
Repository<FieldPermissionEntity>
|
||||
>;
|
||||
let roleRepository: jest.Mocked<Repository<RoleEntity>>;
|
||||
let workspacePermissionsCacheService: jest.Mocked<WorkspacePermissionsCacheService>;
|
||||
let workspaceCacheStorageService: jest.Mocked<WorkspaceCacheStorageService>;
|
||||
|
||||
const testWorkspaceId = 'test-workspace-id';
|
||||
const testRoleId = 'test-role-id';
|
||||
const testObjectMetadataId = 'test-object-metadata-id';
|
||||
const testFieldMetadataId = fieldTextMock.id;
|
||||
|
||||
const mockRole: RoleEntity = {
|
||||
id: testRoleId,
|
||||
label: 'Test Role',
|
||||
description: 'Test role for unit tests',
|
||||
canUpdateAllSettings: false,
|
||||
canReadAllObjectRecords: true,
|
||||
canUpdateAllObjectRecords: false,
|
||||
canSoftDeleteAllObjectRecords: false,
|
||||
canDestroyAllObjectRecords: false,
|
||||
workspaceId: testWorkspaceId,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
isEditable: true,
|
||||
} as RoleEntity;
|
||||
|
||||
const mockRolesPermissions: ObjectRecordsPermissionsByRoleId = {
|
||||
[testRoleId]: {
|
||||
[testObjectMetadataId]: {
|
||||
canRead: true,
|
||||
canUpdate: true,
|
||||
canSoftDelete: false,
|
||||
canDestroy: false,
|
||||
restrictedFields: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
FieldPermissionService,
|
||||
{
|
||||
provide: getRepositoryToken(RoleEntity, 'core'),
|
||||
useValue: {
|
||||
findOne: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(FieldPermissionEntity, 'core'),
|
||||
useValue: {
|
||||
find: jest.fn(),
|
||||
upsert: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: WorkspacePermissionsCacheService,
|
||||
useValue: {
|
||||
getRolesPermissionsFromCache: jest.fn(),
|
||||
recomputeRolesPermissionsCache: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: WorkspaceCacheStorageService,
|
||||
useValue: {
|
||||
getObjectMetadataMapsOrThrow: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<FieldPermissionService>(FieldPermissionService);
|
||||
fieldPermissionsRepository = module.get(
|
||||
getRepositoryToken(FieldPermissionEntity, 'core'),
|
||||
);
|
||||
roleRepository = module.get(getRepositoryToken(RoleEntity, 'core'));
|
||||
workspacePermissionsCacheService = module.get(
|
||||
WorkspacePermissionsCacheService,
|
||||
);
|
||||
workspaceCacheStorageService = module.get(WorkspaceCacheStorageService);
|
||||
|
||||
// Setup default mocks
|
||||
roleRepository.findOne.mockResolvedValue(mockRole);
|
||||
workspacePermissionsCacheService.getRolesPermissionsFromCache.mockResolvedValue(
|
||||
{
|
||||
version: '1',
|
||||
data: mockRolesPermissions,
|
||||
},
|
||||
);
|
||||
workspaceCacheStorageService.getObjectMetadataMapsOrThrow.mockResolvedValue(
|
||||
{
|
||||
byId: {
|
||||
[testObjectMetadataId]: {
|
||||
...objectMetadataItemMock,
|
||||
fieldsById: {
|
||||
[fieldTextMock.id]: {
|
||||
...fieldTextMock,
|
||||
label: 'Test Field',
|
||||
objectMetadataId: testObjectMetadataId,
|
||||
} as FieldMetadataInterface,
|
||||
},
|
||||
fieldIdByJoinColumnName: {},
|
||||
fieldIdByName: {},
|
||||
indexMetadatas: [],
|
||||
},
|
||||
},
|
||||
idByNameSingular: {
|
||||
testObject: testObjectMetadataId,
|
||||
},
|
||||
},
|
||||
);
|
||||
fieldPermissionsRepository.find.mockResolvedValue([]);
|
||||
fieldPermissionsRepository.upsert.mockResolvedValue({} as any);
|
||||
fieldPermissionsRepository.delete.mockResolvedValue({} as any);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('upsertFieldPermissions', () => {
|
||||
const createUpsertInput = (
|
||||
fieldPermissions: {
|
||||
canReadFieldValue?: boolean | null;
|
||||
canUpdateFieldValue?: boolean | null;
|
||||
objectMetadataId?: string;
|
||||
fieldMetadataId?: string;
|
||||
}[],
|
||||
): UpsertFieldPermissionsInput => ({
|
||||
roleId: testRoleId,
|
||||
fieldPermissions: fieldPermissions.map((fieldPermission) => ({
|
||||
objectMetadataId: testObjectMetadataId,
|
||||
fieldMetadataId: testFieldMetadataId,
|
||||
...fieldPermission,
|
||||
})),
|
||||
});
|
||||
|
||||
describe('successful cases', () => {
|
||||
it('should successfully upsert field permissions', async () => {
|
||||
const input = createUpsertInput([
|
||||
{
|
||||
canReadFieldValue: false,
|
||||
canUpdateFieldValue: false,
|
||||
},
|
||||
]);
|
||||
|
||||
await service.upsertFieldPermissions({
|
||||
workspaceId: testWorkspaceId,
|
||||
input,
|
||||
});
|
||||
|
||||
expect(fieldPermissionsRepository.upsert).toHaveBeenCalledWith(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
roleId: testRoleId,
|
||||
workspaceId: testWorkspaceId,
|
||||
objectMetadataId: testObjectMetadataId,
|
||||
fieldMetadataId: testFieldMetadataId,
|
||||
canReadFieldValue: false,
|
||||
canUpdateFieldValue: false,
|
||||
}),
|
||||
]),
|
||||
{
|
||||
conflictPaths: ['fieldMetadataId', 'roleId'],
|
||||
},
|
||||
);
|
||||
|
||||
expect(
|
||||
workspacePermissionsCacheService.recomputeRolesPermissionsCache,
|
||||
).toHaveBeenCalledWith({
|
||||
workspaceId: testWorkspaceId,
|
||||
roleIds: [testRoleId],
|
||||
});
|
||||
});
|
||||
|
||||
it('should delete field permissions when both canReadFieldValue and canUpdateFieldValue are null', async () => {
|
||||
const existingFieldPermission: FieldPermissionEntity = {
|
||||
id: 'existing-field-permission-id',
|
||||
roleId: testRoleId,
|
||||
objectMetadataId: testObjectMetadataId,
|
||||
fieldMetadataId: testFieldMetadataId,
|
||||
canReadFieldValue: null,
|
||||
canUpdateFieldValue: false,
|
||||
workspaceId: testWorkspaceId,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
} as unknown as FieldPermissionEntity;
|
||||
|
||||
fieldPermissionsRepository.find.mockResolvedValue([
|
||||
existingFieldPermission,
|
||||
]);
|
||||
|
||||
const input = createUpsertInput([
|
||||
{
|
||||
canUpdateFieldValue: null,
|
||||
},
|
||||
]);
|
||||
|
||||
await service.upsertFieldPermissions({
|
||||
workspaceId: testWorkspaceId,
|
||||
input,
|
||||
});
|
||||
|
||||
expect(fieldPermissionsRepository.delete).toHaveBeenCalledWith({
|
||||
id: In(['existing-field-permission-id']),
|
||||
});
|
||||
});
|
||||
|
||||
it('should not delete field permissions when one value is null and the other is false', async () => {
|
||||
const existingFieldPermission: FieldPermissionEntity = {
|
||||
id: 'existing-field-permission-id',
|
||||
roleId: testRoleId,
|
||||
objectMetadataId: testObjectMetadataId,
|
||||
fieldMetadataId: testFieldMetadataId,
|
||||
canReadFieldValue: false,
|
||||
canUpdateFieldValue: null,
|
||||
workspaceId: testWorkspaceId,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
} as FieldPermissionEntity;
|
||||
|
||||
fieldPermissionsRepository.find.mockResolvedValue([
|
||||
existingFieldPermission,
|
||||
]);
|
||||
|
||||
const input = createUpsertInput([
|
||||
{
|
||||
canUpdateFieldValue: false,
|
||||
canReadFieldValue: null,
|
||||
},
|
||||
]);
|
||||
|
||||
await service.upsertFieldPermissions({
|
||||
workspaceId: testWorkspaceId,
|
||||
input,
|
||||
});
|
||||
|
||||
expect(fieldPermissionsRepository.delete).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('validation errors', () => {
|
||||
it('should throw error when canReadFieldValue is true', async () => {
|
||||
const input = createUpsertInput([
|
||||
{
|
||||
canReadFieldValue: true,
|
||||
canUpdateFieldValue: false,
|
||||
},
|
||||
]);
|
||||
|
||||
await expect(
|
||||
service.upsertFieldPermissions({
|
||||
workspaceId: testWorkspaceId,
|
||||
input,
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
new PermissionsException(
|
||||
PermissionsExceptionMessage.ONLY_FIELD_RESTRICTION_ALLOWED,
|
||||
PermissionsExceptionCode.ONLY_FIELD_RESTRICTION_ALLOWED,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error when canUpdateFieldValue is true', async () => {
|
||||
const input = createUpsertInput([
|
||||
{
|
||||
canReadFieldValue: false,
|
||||
canUpdateFieldValue: true,
|
||||
},
|
||||
]);
|
||||
|
||||
await expect(
|
||||
service.upsertFieldPermissions({
|
||||
workspaceId: testWorkspaceId,
|
||||
input,
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
new PermissionsException(
|
||||
PermissionsExceptionMessage.ONLY_FIELD_RESTRICTION_ALLOWED,
|
||||
PermissionsExceptionCode.ONLY_FIELD_RESTRICTION_ALLOWED,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error when object metadata is not found', async () => {
|
||||
workspaceCacheStorageService.getObjectMetadataMapsOrThrow.mockResolvedValue(
|
||||
{
|
||||
byId: {},
|
||||
idByNameSingular: {},
|
||||
},
|
||||
);
|
||||
|
||||
const input = createUpsertInput([
|
||||
{
|
||||
objectMetadataId: 'non-existent-object',
|
||||
canReadFieldValue: false,
|
||||
canUpdateFieldValue: false,
|
||||
},
|
||||
]);
|
||||
|
||||
await expect(
|
||||
service.upsertFieldPermissions({
|
||||
workspaceId: testWorkspaceId,
|
||||
input,
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
new PermissionsException(
|
||||
PermissionsExceptionMessage.OBJECT_METADATA_NOT_FOUND,
|
||||
PermissionsExceptionCode.OBJECT_METADATA_NOT_FOUND,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error when trying to add field permission on system object', async () => {
|
||||
const systemObjectMetadata = {
|
||||
...objectMetadataItemMock,
|
||||
isSystem: true,
|
||||
};
|
||||
|
||||
workspaceCacheStorageService.getObjectMetadataMapsOrThrow.mockResolvedValue(
|
||||
{
|
||||
byId: {
|
||||
[testObjectMetadataId]: {
|
||||
...systemObjectMetadata,
|
||||
fieldsById: {},
|
||||
fieldIdByJoinColumnName: {},
|
||||
fieldIdByName: {},
|
||||
indexMetadatas: [],
|
||||
},
|
||||
},
|
||||
idByNameSingular: {
|
||||
testObject: testObjectMetadataId,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const input = createUpsertInput([
|
||||
{
|
||||
canReadFieldValue: false,
|
||||
canUpdateFieldValue: false,
|
||||
},
|
||||
]);
|
||||
|
||||
await expect(
|
||||
service.upsertFieldPermissions({
|
||||
workspaceId: testWorkspaceId,
|
||||
input,
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
new PermissionsException(
|
||||
PermissionsExceptionMessage.CANNOT_ADD_FIELD_PERMISSION_ON_SYSTEM_OBJECT,
|
||||
PermissionsExceptionCode.CANNOT_ADD_FIELD_PERMISSION_ON_SYSTEM_OBJECT,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error when field metadata is not found', async () => {
|
||||
const objectMetadataWithoutField = {
|
||||
...objectMetadataItemMock,
|
||||
fieldsById: {},
|
||||
fieldIdByJoinColumnName: {},
|
||||
fieldIdByName: {},
|
||||
indexMetadatas: [],
|
||||
};
|
||||
|
||||
workspaceCacheStorageService.getObjectMetadataMapsOrThrow.mockResolvedValue(
|
||||
{
|
||||
byId: {
|
||||
[testObjectMetadataId]: objectMetadataWithoutField,
|
||||
},
|
||||
idByNameSingular: {
|
||||
testObject: testObjectMetadataId,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const input = createUpsertInput([
|
||||
{
|
||||
fieldMetadataId: 'non-existent-field',
|
||||
canReadFieldValue: false,
|
||||
canUpdateFieldValue: false,
|
||||
},
|
||||
]);
|
||||
|
||||
await expect(
|
||||
service.upsertFieldPermissions({
|
||||
workspaceId: testWorkspaceId,
|
||||
input,
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
new PermissionsException(
|
||||
PermissionsExceptionMessage.FIELD_METADATA_NOT_FOUND,
|
||||
PermissionsExceptionCode.FIELD_METADATA_NOT_FOUND,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error when object permission is not found', async () => {
|
||||
workspacePermissionsCacheService.getRolesPermissionsFromCache.mockResolvedValue(
|
||||
{
|
||||
version: '1',
|
||||
data: {},
|
||||
},
|
||||
);
|
||||
|
||||
const input = createUpsertInput([
|
||||
{
|
||||
canReadFieldValue: false,
|
||||
canUpdateFieldValue: false,
|
||||
},
|
||||
]);
|
||||
|
||||
await expect(
|
||||
service.upsertFieldPermissions({
|
||||
workspaceId: testWorkspaceId,
|
||||
input,
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
new PermissionsException(
|
||||
PermissionsExceptionMessage.OBJECT_PERMISSION_NOT_FOUND,
|
||||
PermissionsExceptionCode.OBJECT_PERMISSION_NOT_FOUND,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error when object is not readable (permission wise)', async () => {
|
||||
const nonReadableObjectPermissions: ObjectRecordsPermissionsByRoleId = {
|
||||
[testRoleId]: {
|
||||
[testObjectMetadataId]: {
|
||||
canRead: false,
|
||||
canUpdate: false,
|
||||
canSoftDelete: false,
|
||||
canDestroy: false,
|
||||
restrictedFields: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
workspacePermissionsCacheService.getRolesPermissionsFromCache.mockResolvedValue(
|
||||
{
|
||||
version: '1',
|
||||
data: nonReadableObjectPermissions,
|
||||
},
|
||||
);
|
||||
|
||||
const input = createUpsertInput([
|
||||
{
|
||||
canUpdateFieldValue: false,
|
||||
},
|
||||
]);
|
||||
|
||||
await expect(
|
||||
service.upsertFieldPermissions({
|
||||
workspaceId: testWorkspaceId,
|
||||
input,
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
new PermissionsException(
|
||||
PermissionsExceptionMessage.FIELD_RESTRICTION_ONLY_ALLOWED_ON_READABLE_OBJECT,
|
||||
PermissionsExceptionCode.FIELD_RESTRICTION_ONLY_ALLOWED_ON_READABLE_OBJECT,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error when trying to restrict update on non-updatable object', async () => {
|
||||
const nonUpdatableObjectPermissions: ObjectRecordsPermissionsByRoleId =
|
||||
{
|
||||
[testRoleId]: {
|
||||
[testObjectMetadataId]: {
|
||||
canRead: true,
|
||||
canUpdate: false,
|
||||
canSoftDelete: false,
|
||||
canDestroy: false,
|
||||
restrictedFields: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
workspacePermissionsCacheService.getRolesPermissionsFromCache.mockResolvedValue(
|
||||
{
|
||||
version: '1',
|
||||
data: nonUpdatableObjectPermissions,
|
||||
},
|
||||
);
|
||||
|
||||
const input = createUpsertInput([
|
||||
{
|
||||
canUpdateFieldValue: false,
|
||||
},
|
||||
]);
|
||||
|
||||
await expect(
|
||||
service.upsertFieldPermissions({
|
||||
workspaceId: testWorkspaceId,
|
||||
input,
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
new PermissionsException(
|
||||
PermissionsExceptionMessage.FIELD_RESTRICTION_ON_UPDATE_ONLY_ALLOWED_ON_UPDATABLE_OBJECT,
|
||||
PermissionsExceptionCode.FIELD_RESTRICTION_ON_UPDATE_ONLY_ALLOWED_ON_UPDATABLE_OBJECT,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error when both canReadFieldValue and canUpdateFieldValue are null', async () => {
|
||||
const input = createUpsertInput([
|
||||
{
|
||||
canReadFieldValue: null,
|
||||
canUpdateFieldValue: null,
|
||||
},
|
||||
]);
|
||||
|
||||
await expect(
|
||||
service.upsertFieldPermissions({
|
||||
workspaceId: testWorkspaceId,
|
||||
input,
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
new PermissionsException(
|
||||
PermissionsExceptionMessage.EMPTY_FIELD_PERMISSION_NOT_ALLOWED,
|
||||
PermissionsExceptionCode.EMPTY_FIELD_PERMISSION_NOT_ALLOWED,
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('role validation errors', () => {
|
||||
it('should throw error when role is not editable', async () => {
|
||||
const nonEditableRole: RoleEntity = {
|
||||
...mockRole,
|
||||
isEditable: false,
|
||||
};
|
||||
|
||||
roleRepository.findOne.mockResolvedValue(nonEditableRole);
|
||||
|
||||
const input = createUpsertInput([
|
||||
{
|
||||
canReadFieldValue: false,
|
||||
canUpdateFieldValue: false,
|
||||
},
|
||||
]);
|
||||
|
||||
await expect(
|
||||
service.upsertFieldPermissions({
|
||||
workspaceId: testWorkspaceId,
|
||||
input,
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
new PermissionsException(
|
||||
PermissionsExceptionMessage.ROLE_NOT_EDITABLE,
|
||||
PermissionsExceptionCode.ROLE_NOT_EDITABLE,
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,92 @@
|
||||
import { ValidateBy } from 'class-validator';
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
Index,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
Relation,
|
||||
Unique,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
|
||||
@Entity('fieldPermission')
|
||||
@Unique('IDX_FIELD_PERMISSION_FIELD_METADATA_ID_ROLE_ID_UNIQUE', [
|
||||
'fieldMetadataId',
|
||||
'roleId',
|
||||
])
|
||||
@Index('IDX_FIELD_PERMISSION_WORKSPACE_ID_ROLE_ID', ['workspaceId', 'roleId'])
|
||||
export class FieldPermissionEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ nullable: false, type: 'uuid' })
|
||||
roleId: string;
|
||||
|
||||
@ManyToOne(() => RoleEntity, (role) => role.fieldPermissions, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn({ name: 'roleId' })
|
||||
role: Relation<RoleEntity>;
|
||||
|
||||
@Column({ nullable: false, type: 'uuid' })
|
||||
objectMetadataId: string;
|
||||
|
||||
@Column({ nullable: false, type: 'uuid' })
|
||||
fieldMetadataId: string;
|
||||
|
||||
@ManyToOne(
|
||||
() => ObjectMetadataEntity,
|
||||
(objectMetadata) => objectMetadata.fieldPermissions,
|
||||
{
|
||||
onDelete: 'CASCADE',
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: 'objectMetadataId' })
|
||||
objectMetadata: Relation<ObjectMetadataEntity>;
|
||||
|
||||
@ManyToOne(
|
||||
() => FieldMetadataEntity,
|
||||
(fieldMetadata) => fieldMetadata.fieldPermissions,
|
||||
{
|
||||
onDelete: 'CASCADE',
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: 'fieldMetadataId' })
|
||||
fieldMetadata: Relation<FieldMetadataEntity>;
|
||||
|
||||
@Column({ nullable: true, type: 'boolean' })
|
||||
canReadFieldValue?: boolean | null;
|
||||
|
||||
@ValidateBy({
|
||||
name: 'isFalseOrNull',
|
||||
validator: {
|
||||
validate: (value: boolean | null) => value === false || value === null,
|
||||
defaultMessage: () => 'value must be either false or null',
|
||||
},
|
||||
})
|
||||
@Column({ nullable: true, type: 'boolean' })
|
||||
canUpdateFieldValue?: boolean | null;
|
||||
|
||||
@Column({ nullable: false, type: 'uuid' })
|
||||
workspaceId: string;
|
||||
|
||||
@ManyToOne(() => Workspace, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn({ name: 'workspaceId' })
|
||||
workspace: Relation<Workspace>;
|
||||
|
||||
@CreateDateColumn({ type: 'timestamptz' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ type: 'timestamptz' })
|
||||
updatedAt: Date;
|
||||
}
|
||||
@ -0,0 +1,281 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { ObjectRecordsPermissionsByRoleId } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { In, Repository } from 'typeorm';
|
||||
|
||||
import { UpsertFieldPermissionsInput } from 'src/engine/metadata-modules/object-permission/dtos/upsert-field-permissions.input';
|
||||
import { FieldPermissionEntity } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.entity';
|
||||
import {
|
||||
PermissionsException,
|
||||
PermissionsExceptionCode,
|
||||
PermissionsExceptionMessage,
|
||||
} from 'src/engine/metadata-modules/permissions/permissions.exception';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
|
||||
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
||||
|
||||
@Injectable()
|
||||
export class FieldPermissionService {
|
||||
constructor(
|
||||
@InjectRepository(RoleEntity, 'core')
|
||||
private readonly roleRepository: Repository<RoleEntity>,
|
||||
@InjectRepository(FieldPermissionEntity, 'core')
|
||||
private readonly fieldPermissionsRepository: Repository<FieldPermissionEntity>,
|
||||
private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService,
|
||||
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
|
||||
) {}
|
||||
|
||||
public async upsertFieldPermissions({
|
||||
workspaceId,
|
||||
input,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
input: UpsertFieldPermissionsInput;
|
||||
}): Promise<FieldPermissionEntity[]> {
|
||||
const role = await this.getRoleOrThrow({
|
||||
roleId: input.roleId,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
const { data: rolesPermissions } =
|
||||
await this.workspacePermissionsCacheService.getRolesPermissionsFromCache({
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
await this.validateRoleIsEditableOrThrow({
|
||||
role,
|
||||
});
|
||||
|
||||
const { byId: objectMetadataMapsById } =
|
||||
await this.workspaceCacheStorageService.getObjectMetadataMapsOrThrow(
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const existingFieldPermissions = await this.fieldPermissionsRepository.find(
|
||||
{
|
||||
where: {
|
||||
roleId: input.roleId,
|
||||
workspaceId,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const fieldPermissionsToDeleteIds: string[] = [];
|
||||
|
||||
input.fieldPermissions.forEach((fieldPermission) => {
|
||||
this.validateFieldPermission({
|
||||
fieldPermission,
|
||||
objectMetadataMapsById,
|
||||
rolesPermissions,
|
||||
role,
|
||||
});
|
||||
|
||||
if (
|
||||
fieldPermission.canReadFieldValue === null ||
|
||||
fieldPermission.canUpdateFieldValue === null
|
||||
) {
|
||||
this.checkIfFieldPermissionShouldBeDeleted({
|
||||
fieldPermission,
|
||||
existingFieldPermissions,
|
||||
fieldPermissionsToDeleteIds,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const fieldPermissions = input.fieldPermissions.map((fieldPermission) => ({
|
||||
...fieldPermission,
|
||||
roleId: input.roleId,
|
||||
workspaceId,
|
||||
}));
|
||||
|
||||
await this.fieldPermissionsRepository.upsert(fieldPermissions, {
|
||||
conflictPaths: ['fieldMetadataId', 'roleId'],
|
||||
});
|
||||
|
||||
if (fieldPermissionsToDeleteIds.length > 0) {
|
||||
await this.fieldPermissionsRepository.delete({
|
||||
id: In(fieldPermissionsToDeleteIds),
|
||||
});
|
||||
}
|
||||
|
||||
await this.workspacePermissionsCacheService.recomputeRolesPermissionsCache({
|
||||
workspaceId,
|
||||
roleIds: [input.roleId],
|
||||
});
|
||||
|
||||
return this.fieldPermissionsRepository.find({
|
||||
where: {
|
||||
roleId: input.roleId,
|
||||
objectMetadataId: In(
|
||||
input.fieldPermissions.map(
|
||||
(fieldPermission) => fieldPermission.objectMetadataId,
|
||||
),
|
||||
),
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private validateFieldPermission({
|
||||
fieldPermission,
|
||||
objectMetadataMapsById,
|
||||
rolesPermissions,
|
||||
role,
|
||||
}: {
|
||||
fieldPermission: UpsertFieldPermissionsInput['fieldPermissions'][0];
|
||||
objectMetadataMapsById: Record<string, ObjectMetadataItemWithFieldMaps>;
|
||||
rolesPermissions: ObjectRecordsPermissionsByRoleId;
|
||||
role: RoleEntity;
|
||||
}) {
|
||||
if (
|
||||
('canUpdateFieldValue' in fieldPermission &&
|
||||
fieldPermission.canUpdateFieldValue !== null &&
|
||||
fieldPermission.canUpdateFieldValue !== false) ||
|
||||
('canReadFieldValue' in fieldPermission &&
|
||||
fieldPermission.canReadFieldValue !== null &&
|
||||
fieldPermission.canReadFieldValue !== false)
|
||||
) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.ONLY_FIELD_RESTRICTION_ALLOWED,
|
||||
PermissionsExceptionCode.ONLY_FIELD_RESTRICTION_ALLOWED,
|
||||
);
|
||||
}
|
||||
|
||||
const objectMetadataForFieldPermission =
|
||||
objectMetadataMapsById[fieldPermission.objectMetadataId];
|
||||
|
||||
if (!isDefined(objectMetadataForFieldPermission)) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.OBJECT_METADATA_NOT_FOUND,
|
||||
PermissionsExceptionCode.OBJECT_METADATA_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
if (objectMetadataForFieldPermission.isSystem === true) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.CANNOT_ADD_FIELD_PERMISSION_ON_SYSTEM_OBJECT,
|
||||
PermissionsExceptionCode.CANNOT_ADD_FIELD_PERMISSION_ON_SYSTEM_OBJECT,
|
||||
);
|
||||
}
|
||||
|
||||
const fieldMetadataForFieldPermission =
|
||||
objectMetadataForFieldPermission.fieldsById[
|
||||
fieldPermission.fieldMetadataId
|
||||
];
|
||||
|
||||
if (!isDefined(fieldMetadataForFieldPermission)) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.FIELD_METADATA_NOT_FOUND,
|
||||
PermissionsExceptionCode.FIELD_METADATA_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const rolePermissionOnObject =
|
||||
rolesPermissions?.[role.id]?.[fieldPermission.objectMetadataId];
|
||||
|
||||
if (!isDefined(rolePermissionOnObject)) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.OBJECT_PERMISSION_NOT_FOUND,
|
||||
PermissionsExceptionCode.OBJECT_PERMISSION_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
if (rolePermissionOnObject.canRead === false) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.FIELD_RESTRICTION_ONLY_ALLOWED_ON_READABLE_OBJECT,
|
||||
PermissionsExceptionCode.FIELD_RESTRICTION_ONLY_ALLOWED_ON_READABLE_OBJECT,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
rolePermissionOnObject.canUpdate === false &&
|
||||
fieldPermission.canUpdateFieldValue === false
|
||||
) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.FIELD_RESTRICTION_ON_UPDATE_ONLY_ALLOWED_ON_UPDATABLE_OBJECT,
|
||||
PermissionsExceptionCode.FIELD_RESTRICTION_ON_UPDATE_ONLY_ALLOWED_ON_UPDATABLE_OBJECT,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
fieldPermission.canUpdateFieldValue === null &&
|
||||
fieldPermission.canReadFieldValue === null
|
||||
) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.EMPTY_FIELD_PERMISSION_NOT_ALLOWED,
|
||||
PermissionsExceptionCode.EMPTY_FIELD_PERMISSION_NOT_ALLOWED,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async getRoleOrThrow({
|
||||
roleId,
|
||||
workspaceId,
|
||||
}: {
|
||||
roleId: string;
|
||||
workspaceId: string;
|
||||
}) {
|
||||
const role = await this.roleRepository.findOne({
|
||||
where: {
|
||||
id: roleId,
|
||||
workspaceId,
|
||||
},
|
||||
relations: ['objectPermissions', 'fieldPermissions'],
|
||||
});
|
||||
|
||||
if (!isDefined(role)) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.ROLE_NOT_FOUND,
|
||||
PermissionsExceptionCode.ROLE_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
return role;
|
||||
}
|
||||
|
||||
private async validateRoleIsEditableOrThrow({ role }: { role: RoleEntity }) {
|
||||
if (!role.isEditable) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.ROLE_NOT_EDITABLE,
|
||||
PermissionsExceptionCode.ROLE_NOT_EDITABLE,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private checkIfFieldPermissionShouldBeDeleted({
|
||||
fieldPermission,
|
||||
existingFieldPermissions,
|
||||
fieldPermissionsToDeleteIds,
|
||||
}: {
|
||||
fieldPermission: UpsertFieldPermissionsInput['fieldPermissions'][0];
|
||||
existingFieldPermissions: FieldPermissionEntity[];
|
||||
fieldPermissionsToDeleteIds: string[];
|
||||
}) {
|
||||
const existingFieldPermission = existingFieldPermissions.find(
|
||||
(existingFieldPermission) =>
|
||||
existingFieldPermission.fieldMetadataId ===
|
||||
fieldPermission.fieldMetadataId,
|
||||
);
|
||||
|
||||
if (existingFieldPermission) {
|
||||
const finalCanReadFieldValue =
|
||||
'canReadFieldValue' in fieldPermission
|
||||
? fieldPermission.canReadFieldValue
|
||||
: existingFieldPermission.canReadFieldValue;
|
||||
const finalCanUpdateFieldValue =
|
||||
'canUpdateFieldValue' in fieldPermission
|
||||
? fieldPermission.canUpdateFieldValue
|
||||
: existingFieldPermission.canUpdateFieldValue;
|
||||
|
||||
if (
|
||||
finalCanReadFieldValue === null &&
|
||||
finalCanUpdateFieldValue === null
|
||||
) {
|
||||
fieldPermissionsToDeleteIds.push(existingFieldPermission.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2,6 +2,7 @@ import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
Index,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
@ -18,6 +19,7 @@ import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
'objectMetadataId',
|
||||
'roleId',
|
||||
])
|
||||
@Index('IDX_OBJECT_PERMISSION_WORKSPACE_ID_ROLE_ID', ['workspaceId', 'roleId'])
|
||||
export class ObjectPermissionEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
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 { FieldPermissionService } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.service';
|
||||
import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity';
|
||||
import { ObjectPermissionService } from 'src/engine/metadata-modules/object-permission/object-permission.service';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
@ -11,13 +14,19 @@ import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature(
|
||||
[ObjectPermissionEntity, RoleEntity, ObjectMetadataEntity],
|
||||
[
|
||||
ObjectPermissionEntity,
|
||||
RoleEntity,
|
||||
ObjectMetadataEntity,
|
||||
FieldPermissionEntity,
|
||||
FieldMetadataEntity,
|
||||
],
|
||||
'core',
|
||||
),
|
||||
WorkspaceCacheStorageModule,
|
||||
WorkspacePermissionsCacheModule,
|
||||
],
|
||||
providers: [ObjectPermissionService],
|
||||
exports: [ObjectPermissionService],
|
||||
providers: [ObjectPermissionService, FieldPermissionService],
|
||||
exports: [ObjectPermissionService, FieldPermissionService],
|
||||
})
|
||||
export class ObjectPermissionModule {}
|
||||
|
||||
@ -31,10 +31,19 @@ export enum PermissionsExceptionCode {
|
||||
DEFAULT_ROLE_CANNOT_BE_DELETED = 'DEFAULT_ROLE_CANNOT_BE_DELETED',
|
||||
NO_PERMISSIONS_FOUND_IN_DATASOURCE = 'NO_PERMISSIONS_FOUND_IN_DATASOURCE',
|
||||
CANNOT_ADD_OBJECT_PERMISSION_ON_SYSTEM_OBJECT = 'CANNOT_ADD_OBJECT_PERMISSION_ON_SYSTEM_OBJECT',
|
||||
CANNOT_ADD_FIELD_PERMISSION_ON_SYSTEM_OBJECT = 'CANNOT_ADD_FIELD_PERMISSION_ON_SYSTEM_OBJECT',
|
||||
METHOD_NOT_ALLOWED = 'METHOD_NOT_ALLOWED',
|
||||
RAW_SQL_NOT_ALLOWED = 'RAW_SQL_NOT_ALLOWED',
|
||||
CANNOT_GIVE_WRITING_PERMISSION_ON_NON_READABLE_OBJECT = 'CANNOT_GIVE_WRITING_PERMISSION_ON_NON_READABLE_OBJECT',
|
||||
CANNOT_GIVE_WRITING_PERMISSION_WITHOUT_READING_PERMISSION = 'CANNOT_GIVE_WRITING_PERMISSION_WITHOUT_READING_PERMISSION',
|
||||
FIELD_METADATA_NOT_FOUND = 'FIELD_METADATA_NOT_FOUND',
|
||||
ONLY_FIELD_RESTRICTION_ALLOWED = 'ONLY_FIELD_RESTRICTION_ALLOWED',
|
||||
FIELD_RESTRICTION_ONLY_ALLOWED_ON_READABLE_OBJECT = 'FIELD_RESTRICTION_ONLY_ALLOWED_ON_READABLE_OBJECT',
|
||||
FIELD_RESTRICTION_ON_UPDATE_ONLY_ALLOWED_ON_UPDATABLE_OBJECT = 'FIELD_RESTRICTION_ON_UPDATE_ONLY_ALLOWED_ON_UPDATABLE_OBJECT',
|
||||
UPSERT_FIELD_PERMISSION_FAILED = 'UPSERT_FIELD_PERMISSION_FAILED',
|
||||
PERMISSION_NOT_FOUND = 'PERMISSION_NOT_FOUND',
|
||||
OBJECT_PERMISSION_NOT_FOUND = 'OBJECT_PERMISSION_NOT_FOUND',
|
||||
EMPTY_FIELD_PERMISSION_NOT_ALLOWED = 'EMPTY_FIELD_PERMISSION_NOT_ALLOWED',
|
||||
}
|
||||
|
||||
export enum PermissionsExceptionMessage {
|
||||
@ -60,6 +69,15 @@ export enum PermissionsExceptionMessage {
|
||||
DEFAULT_ROLE_CANNOT_BE_DELETED = 'Default role cannot be deleted',
|
||||
NO_PERMISSIONS_FOUND_IN_DATASOURCE = 'No permissions found in datasource',
|
||||
CANNOT_ADD_OBJECT_PERMISSION_ON_SYSTEM_OBJECT = 'Cannot add object permission on system object',
|
||||
CANNOT_ADD_FIELD_PERMISSION_ON_SYSTEM_OBJECT = 'Cannot add field permission on system object',
|
||||
CANNOT_GIVE_WRITING_PERMISSION_ON_NON_READABLE_OBJECT = 'Cannot give update permission to non-readable object',
|
||||
CANNOT_GIVE_WRITING_PERMISSION_WITHOUT_READING_PERMISSION = 'Cannot give writing permission without reading permission',
|
||||
FIELD_METADATA_NOT_FOUND = 'Field metadata not found',
|
||||
ONLY_FIELD_RESTRICTION_ALLOWED = 'Field permission can only introduce a restriction',
|
||||
FIELD_RESTRICTION_ONLY_ALLOWED_ON_READABLE_OBJECT = 'Field restriction only makes sense on readable object',
|
||||
FIELD_RESTRICTION_ON_UPDATE_ONLY_ALLOWED_ON_UPDATABLE_OBJECT = 'Field restriction on update only makes sense on updatable object',
|
||||
UPSERT_FIELD_PERMISSION_FAILED = 'Failed to upsert field permission',
|
||||
PERMISSION_NOT_FOUND = 'Permission not found',
|
||||
OBJECT_PERMISSION_NOT_FOUND = 'Object permission not found',
|
||||
EMPTY_FIELD_PERMISSION_NOT_ALLOWED = 'Empty field permission not allowed',
|
||||
}
|
||||
|
||||
@ -27,16 +27,24 @@ export const permissionGraphqlApiExceptionHandler = (
|
||||
case PermissionsExceptionCode.CANNOT_DELETE_LAST_ADMIN_USER:
|
||||
case PermissionsExceptionCode.ROLE_NOT_EDITABLE:
|
||||
case PermissionsExceptionCode.CANNOT_ADD_OBJECT_PERMISSION_ON_SYSTEM_OBJECT:
|
||||
case PermissionsExceptionCode.CANNOT_ADD_FIELD_PERMISSION_ON_SYSTEM_OBJECT:
|
||||
throw new ForbiddenError(error.message);
|
||||
case PermissionsExceptionCode.INVALID_ARG:
|
||||
case PermissionsExceptionCode.INVALID_SETTING:
|
||||
case PermissionsExceptionCode.CANNOT_GIVE_WRITING_PERMISSION_ON_NON_READABLE_OBJECT:
|
||||
case PermissionsExceptionCode.CANNOT_GIVE_WRITING_PERMISSION_WITHOUT_READING_PERMISSION:
|
||||
case PermissionsExceptionCode.ONLY_FIELD_RESTRICTION_ALLOWED:
|
||||
case PermissionsExceptionCode.FIELD_RESTRICTION_ONLY_ALLOWED_ON_READABLE_OBJECT:
|
||||
case PermissionsExceptionCode.FIELD_RESTRICTION_ON_UPDATE_ONLY_ALLOWED_ON_UPDATABLE_OBJECT:
|
||||
case PermissionsExceptionCode.EMPTY_FIELD_PERMISSION_NOT_ALLOWED:
|
||||
throw new UserInputError(error.message);
|
||||
case PermissionsExceptionCode.ROLE_NOT_FOUND:
|
||||
case PermissionsExceptionCode.USER_WORKSPACE_NOT_FOUND:
|
||||
case PermissionsExceptionCode.OBJECT_METADATA_NOT_FOUND:
|
||||
case PermissionsExceptionCode.FIELD_METADATA_NOT_FOUND:
|
||||
case PermissionsExceptionCode.PERMISSION_NOT_FOUND:
|
||||
throw new NotFoundError(error.message);
|
||||
case PermissionsExceptionCode.UPSERT_FIELD_PERMISSION_FAILED:
|
||||
case PermissionsExceptionCode.DEFAULT_ROLE_NOT_FOUND:
|
||||
case PermissionsExceptionCode.WORKSPACE_ID_ROLE_USER_WORKSPACE_MISMATCH:
|
||||
case PermissionsExceptionCode.TOO_MANY_ADMIN_CANDIDATES:
|
||||
@ -50,6 +58,7 @@ export const permissionGraphqlApiExceptionHandler = (
|
||||
case PermissionsExceptionCode.NO_PERMISSIONS_FOUND_IN_DATASOURCE:
|
||||
case PermissionsExceptionCode.METHOD_NOT_ALLOWED:
|
||||
case PermissionsExceptionCode.RAW_SQL_NOT_ALLOWED:
|
||||
case PermissionsExceptionCode.OBJECT_PERMISSION_NOT_FOUND:
|
||||
throw error;
|
||||
default: {
|
||||
const _exhaustiveCheck: never = error.code;
|
||||
|
||||
@ -9,6 +9,7 @@ import {
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
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 { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
|
||||
import { SettingPermissionEntity } from 'src/engine/metadata-modules/setting-permission/setting-permission.entity';
|
||||
@ -72,4 +73,10 @@ export class RoleEntity {
|
||||
(settingPermission: SettingPermissionEntity) => settingPermission.role,
|
||||
)
|
||||
settingPermissions: Relation<SettingPermissionEntity[]>;
|
||||
|
||||
@OneToMany(
|
||||
() => FieldPermissionEntity,
|
||||
(fieldPermission: FieldPermissionEntity) => fieldPermission.role,
|
||||
)
|
||||
fieldPermissions: Relation<FieldPermissionEntity[]>;
|
||||
}
|
||||
|
||||
@ -22,8 +22,11 @@ import { SettingsPermissionsGuard } from 'src/engine/guards/settings-permissions
|
||||
import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { AgentRoleService } from 'src/engine/metadata-modules/agent-role/agent-role.service';
|
||||
import { FieldPermissionDTO } from 'src/engine/metadata-modules/object-permission/dtos/field-permission.dto';
|
||||
import { ObjectPermissionDTO } from 'src/engine/metadata-modules/object-permission/dtos/object-permission.dto';
|
||||
import { UpsertFieldPermissionsInput } from 'src/engine/metadata-modules/object-permission/dtos/upsert-field-permissions.input';
|
||||
import { UpsertObjectPermissionsInput } from 'src/engine/metadata-modules/object-permission/dtos/upsert-object-permissions.input';
|
||||
import { FieldPermissionService } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.service';
|
||||
import { ObjectPermissionService } from 'src/engine/metadata-modules/object-permission/object-permission.service';
|
||||
import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants';
|
||||
import {
|
||||
@ -60,6 +63,7 @@ export class RoleResolver {
|
||||
private readonly objectPermissionService: ObjectPermissionService,
|
||||
private readonly settingPermissionService: SettingPermissionService,
|
||||
private readonly agentRoleService: AgentRoleService,
|
||||
private readonly fieldPermissionService: FieldPermissionService,
|
||||
) {}
|
||||
|
||||
@Query(() => [RoleDTO])
|
||||
@ -179,6 +183,18 @@ export class RoleResolver {
|
||||
});
|
||||
}
|
||||
|
||||
@Mutation(() => [FieldPermissionDTO])
|
||||
async upsertFieldPermissions(
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
@Args('upsertFieldPermissionsInput')
|
||||
upsertFieldPermissionsInput: UpsertFieldPermissionsInput,
|
||||
): Promise<FieldPermissionDTO[]> {
|
||||
return this.fieldPermissionService.upsertFieldPermissions({
|
||||
workspaceId: workspace.id,
|
||||
input: upsertFieldPermissionsInput,
|
||||
});
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean)
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_AI_ENABLED)
|
||||
async assignRoleToAgent(
|
||||
|
||||
@ -9,8 +9,8 @@ import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metada
|
||||
|
||||
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/constants/search-vector-field.constants';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { IndexMetadataService } from 'src/engine/metadata-modules/index-metadata/index-metadata.service';
|
||||
import { IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types';
|
||||
import { CreateObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/create-object.input';
|
||||
import { DEFAULT_LABEL_IDENTIFIER_FIELD_NAME } from 'src/engine/metadata-modules/object-metadata/object-metadata.constants';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
|
||||
@ -5,7 +5,7 @@ import {
|
||||
PrimaryGeneratedColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types';
|
||||
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-metadata/relation-on-delete-action.type';
|
||||
|
||||
export enum WorkspaceMigrationColumnActionType {
|
||||
|
||||
@ -8,10 +8,12 @@ import {
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { In, Repository } from 'typeorm';
|
||||
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants';
|
||||
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { WorkspaceFeatureFlagsMapCacheService } from 'src/engine/metadata-modules/workspace-feature-flags-map-cache/workspace-feature-flags-map-cache.service';
|
||||
import { UserWorkspaceRoleMap } from 'src/engine/metadata-modules/workspace-permissions-cache/types/user-workspace-role-map.type';
|
||||
import { WorkspacePermissionsCacheStorageService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache-storage.service';
|
||||
import { TwentyORMExceptionCode } from 'src/engine/twenty-orm/exceptions/twenty-orm.exception';
|
||||
@ -38,6 +40,7 @@ export class WorkspacePermissionsCacheService {
|
||||
@InjectRepository(RoleTargetsEntity, 'core')
|
||||
private readonly roleTargetsRepository: Repository<RoleTargetsEntity>,
|
||||
private readonly workspacePermissionsCacheStorageService: WorkspacePermissionsCacheStorageService,
|
||||
private readonly workspaceFeatureFlagsMapCacheService: WorkspaceFeatureFlagsMapCacheService,
|
||||
) {}
|
||||
|
||||
async recomputeRolesPermissionsCache({
|
||||
@ -156,7 +159,7 @@ export class WorkspacePermissionsCacheService {
|
||||
return userWorkspaceRoleMap[userWorkspaceId];
|
||||
}
|
||||
|
||||
private async getObjectRecordPermissionsForRoles({
|
||||
async getObjectRecordPermissionsForRoles({
|
||||
workspaceId,
|
||||
roleIds,
|
||||
}: {
|
||||
@ -165,12 +168,24 @@ export class WorkspacePermissionsCacheService {
|
||||
}): Promise<ObjectRecordsPermissionsByRoleId> {
|
||||
let roles: RoleEntity[] = [];
|
||||
|
||||
const workspaceFeatureFlagsMap =
|
||||
await this.workspaceFeatureFlagsMapCacheService.getWorkspaceFeatureFlagsMap(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
const isFieldPermissionsEnabled =
|
||||
workspaceFeatureFlagsMap[FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED];
|
||||
|
||||
roles = await this.roleRepository.find({
|
||||
where: {
|
||||
workspaceId,
|
||||
...(roleIds ? { id: In(roleIds) } : {}),
|
||||
},
|
||||
relations: ['objectPermissions', 'settingPermissions'],
|
||||
relations: [
|
||||
'objectPermissions',
|
||||
'settingPermissions',
|
||||
...(isFieldPermissionsEnabled ? ['fieldPermissions'] : []),
|
||||
],
|
||||
});
|
||||
|
||||
const workspaceObjectMetadataCollection =
|
||||
@ -188,6 +203,10 @@ export class WorkspacePermissionsCacheService {
|
||||
let canUpdate = role.canUpdateAllObjectRecords;
|
||||
let canSoftDelete = role.canSoftDeleteAllObjectRecords;
|
||||
let canDestroy = role.canDestroyAllObjectRecords;
|
||||
const restrictedFields: Record<
|
||||
string,
|
||||
{ canRead?: boolean | null; canUpdate?: boolean | null }
|
||||
> = {};
|
||||
|
||||
if (
|
||||
standardId &&
|
||||
@ -230,6 +249,20 @@ export class WorkspacePermissionsCacheService {
|
||||
objectRecordPermissionsOverride?.canDestroyObjectRecords,
|
||||
canDestroy,
|
||||
);
|
||||
|
||||
if (isFieldPermissionsEnabled) {
|
||||
const fieldPermissions = role.fieldPermissions.filter(
|
||||
(fieldPermission) =>
|
||||
fieldPermission.objectMetadataId === objectMetadataId,
|
||||
);
|
||||
|
||||
for (const fieldPermission of fieldPermissions) {
|
||||
restrictedFields[fieldPermission.fieldMetadataId] = {
|
||||
canRead: fieldPermission.canReadFieldValue,
|
||||
canUpdate: fieldPermission.canUpdateFieldValue,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
objectRecordsPermissions[objectMetadataId] = {
|
||||
@ -237,6 +270,7 @@ export class WorkspacePermissionsCacheService {
|
||||
canUpdate,
|
||||
canSoftDelete,
|
||||
canDestroy,
|
||||
restrictedFields,
|
||||
};
|
||||
|
||||
permissionsByRoleId[role.id] = objectRecordsPermissions;
|
||||
|
||||
@ -6,7 +6,7 @@ import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfa
|
||||
|
||||
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/constants/search-vector-field.constants';
|
||||
import { ActorMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
|
||||
import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types';
|
||||
import { DEFAULT_LABEL_IDENTIFIER_FIELD_NAME } from 'src/engine/metadata-modules/object-metadata/object-metadata.constants';
|
||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||
import { WorkspaceCustomEntity } from 'src/engine/twenty-orm/decorators/workspace-custom-entity.decorator';
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types';
|
||||
import { generateDeterministicIndexName } from 'src/engine/metadata-modules/index-metadata/utils/generate-deterministic-index-name';
|
||||
import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage';
|
||||
import { convertClassNameToObjectMetadataName } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/convert-class-to-object-metadata-name.util';
|
||||
|
||||
@ -72,6 +72,7 @@ describe('WorkspaceEntityManager', () => {
|
||||
canUpdate: false,
|
||||
canSoftDelete: false,
|
||||
canDestroy: false,
|
||||
restrictedFields: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { Gate } from 'src/engine/twenty-orm/interfaces/gate.interface';
|
||||
|
||||
import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types';
|
||||
|
||||
export interface WorkspaceIndexMetadataArgs {
|
||||
/**
|
||||
|
||||
@ -75,6 +75,7 @@ describe('WorkspaceRepository', () => {
|
||||
canUpdate: false,
|
||||
canSoftDelete: false,
|
||||
canDestroy: false,
|
||||
restrictedFields: {},
|
||||
},
|
||||
};
|
||||
mockQueryRunner = {} as QueryRunner;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types';
|
||||
import { getColumnsForIndex } from 'src/engine/twenty-orm/utils/get-default-columns-for-index.util';
|
||||
|
||||
describe('getColumnsForIndex', () => {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types';
|
||||
|
||||
export const getColumnsForIndex = (indexType?: IndexType) => {
|
||||
switch (indexType) {
|
||||
|
||||
@ -3,7 +3,7 @@ import { Injectable, Logger } from '@nestjs/common';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { QueryRunner, Table, TableColumn } from 'typeorm';
|
||||
|
||||
import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types';
|
||||
import {
|
||||
WorkspaceMigrationColumnAction,
|
||||
WorkspaceMigrationColumnActionType,
|
||||
|
||||
@ -3,10 +3,8 @@ import { Injectable, Logger } from '@nestjs/common';
|
||||
import { PartialIndexMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-index-metadata.interface';
|
||||
import { WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface';
|
||||
|
||||
import {
|
||||
IndexMetadataEntity,
|
||||
IndexType,
|
||||
} from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types';
|
||||
import { generateDeterministicIndexName } from 'src/engine/metadata-modules/index-metadata/utils/generate-deterministic-index-name';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||
|
||||
@ -10,7 +10,7 @@ import { ActorMetadata } from 'src/engine/metadata-modules/field-metadata/compos
|
||||
import { AddressMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/address.composite-type';
|
||||
import { CurrencyMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/currency.composite-type';
|
||||
import { LinksMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/links.composite-type';
|
||||
import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types';
|
||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||
import { WorkspaceDuplicateCriteria } from 'src/engine/twenty-orm/decorators/workspace-duplicate-criteria.decorator';
|
||||
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
||||
|
||||
@ -8,7 +8,7 @@ import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/i
|
||||
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/constants/search-vector-field.constants';
|
||||
import { ActorMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
|
||||
import { RichTextV2Metadata } from 'src/engine/metadata-modules/field-metadata/composite-types/rich-text-v2.composite-type';
|
||||
import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types';
|
||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
||||
import { WorkspaceFieldIndex } from 'src/engine/twenty-orm/decorators/workspace-field-index.decorator';
|
||||
|
||||
@ -8,7 +8,7 @@ import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/i
|
||||
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/constants/search-vector-field.constants';
|
||||
import { ActorMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
|
||||
import { CurrencyMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/currency.composite-type';
|
||||
import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types';
|
||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
||||
import { WorkspaceFieldIndex } from 'src/engine/twenty-orm/decorators/workspace-field-index.decorator';
|
||||
|
||||
@ -11,7 +11,7 @@ import { EmailsMetadata } from 'src/engine/metadata-modules/field-metadata/compo
|
||||
import { FullNameMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/full-name.composite-type';
|
||||
import { LinksMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/links.composite-type';
|
||||
import { PhonesMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/phones.composite-type';
|
||||
import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types';
|
||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||
import { WorkspaceDuplicateCriteria } from 'src/engine/twenty-orm/decorators/workspace-duplicate-criteria.decorator';
|
||||
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
||||
|
||||
@ -8,7 +8,7 @@ import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/i
|
||||
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/constants/search-vector-field.constants';
|
||||
import { ActorMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
|
||||
import { RichTextV2Metadata } from 'src/engine/metadata-modules/field-metadata/composite-types/rich-text-v2.composite-type';
|
||||
import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types';
|
||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
||||
import { WorkspaceFieldIndex } from 'src/engine/twenty-orm/decorators/workspace-field-index.decorator';
|
||||
|
||||
@ -7,7 +7,7 @@ import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/i
|
||||
|
||||
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/constants/search-vector-field.constants';
|
||||
import { ActorMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
|
||||
import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types';
|
||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
||||
import { WorkspaceFieldIndex } from 'src/engine/twenty-orm/decorators/workspace-field-index.decorator';
|
||||
@ -29,9 +29,9 @@ import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-o
|
||||
import { WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
|
||||
import { WorkflowWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity';
|
||||
import { WorkflowActionOutput } from 'src/modules/workflow/workflow-executor/types/workflow-action-output.type';
|
||||
import { WorkflowRunStepInfo } from 'src/modules/workflow/workflow-executor/types/workflow-run-step-info.type';
|
||||
import { WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||
import { WorkflowTrigger } from 'src/modules/workflow/workflow-trigger/types/workflow-trigger.type';
|
||||
import { WorkflowRunStepInfo } from 'src/modules/workflow/workflow-executor/types/workflow-run-step-info.type';
|
||||
|
||||
export enum WorkflowRunStatus {
|
||||
NOT_STARTED = 'NOT_STARTED',
|
||||
|
||||
@ -7,7 +7,7 @@ import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/i
|
||||
|
||||
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/constants/search-vector-field.constants';
|
||||
import { FieldMetadataComplexOption } from 'src/engine/metadata-modules/field-metadata/dtos/options.input';
|
||||
import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types';
|
||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
||||
import { WorkspaceFieldIndex } from 'src/engine/twenty-orm/decorators/workspace-field-index.decorator';
|
||||
|
||||
@ -5,29 +5,29 @@ import { RelationOnDeleteAction } from 'src/engine/metadata-modules/field-metada
|
||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||
import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface';
|
||||
|
||||
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/constants/search-vector-field.constants';
|
||||
import { ActorMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
|
||||
import { FieldMetadataComplexOption } from 'src/engine/metadata-modules/field-metadata/dtos/options.input';
|
||||
import { IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types';
|
||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||
import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
||||
import { WorkspaceFieldIndex } from 'src/engine/twenty-orm/decorators/workspace-field-index.decorator';
|
||||
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
||||
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
||||
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
||||
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
|
||||
import { WorkspaceFieldIndex } from 'src/engine/twenty-orm/decorators/workspace-field-index.decorator';
|
||||
import { WORKFLOW_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
||||
import { STANDARD_OBJECT_ICONS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-icons';
|
||||
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
||||
import {
|
||||
FieldTypeAndNameMetadata,
|
||||
getTsVectorColumnExpressionFromFields,
|
||||
} from 'src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util';
|
||||
import { FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/favorite.workspace-entity';
|
||||
import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity';
|
||||
import { WorkflowAutomatedTriggerWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-automated-trigger.workspace-entity';
|
||||
import { WorkflowRunWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity';
|
||||
import { WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
|
||||
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/constants/search-vector-field.constants';
|
||||
import {
|
||||
FieldTypeAndNameMetadata,
|
||||
getTsVectorColumnExpressionFromFields,
|
||||
} from 'src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util';
|
||||
|
||||
export enum WorkflowStatus {
|
||||
DRAFT = 'DRAFT',
|
||||
|
||||
@ -10,7 +10,7 @@ import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/i
|
||||
|
||||
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/constants/search-vector-field.constants';
|
||||
import { FullNameMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/full-name.composite-type';
|
||||
import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types';
|
||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
||||
import { WorkspaceFieldIndex } from 'src/engine/twenty-orm/decorators/workspace-field-index.decorator';
|
||||
|
||||
@ -3,6 +3,7 @@ import { deleteOneRoleOperationFactory } from 'test/integration/graphql/utils/de
|
||||
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
|
||||
import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
|
||||
|
||||
import { fieldTextMock } from 'src/engine/api/__mocks__/object-metadata-item.mock';
|
||||
import { ErrorCode } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants';
|
||||
import { PermissionsExceptionMessage } from 'src/engine/metadata-modules/permissions/permissions.exception';
|
||||
@ -537,6 +538,27 @@ describe('roles permissions', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('upsertFieldPermissions', () => {
|
||||
it('should throw a permission error when user does not have permission to upsert field permission (member role)', async () => {
|
||||
const query = {
|
||||
query: `
|
||||
mutation UpsertFieldPermissions {
|
||||
upsertFieldPermissions(upsertFieldPermissionsInput: {roleId: "${guestRoleId}", fieldPermissions: [{objectMetadataId: "${listingObjectId}", fieldMetadataId: "${fieldTextMock.id}", canReadFieldValue: false, canUpdateFieldValue: false}]}) {
|
||||
id
|
||||
roleId
|
||||
objectMetadataId
|
||||
fieldMetadataId
|
||||
canReadFieldValue
|
||||
canUpdateFieldValue
|
||||
}
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
await assertPermissionDeniedForMemberWithMemberRole({ query });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('upsertSettingPermissions', () => {
|
||||
|
||||
@ -47,6 +47,7 @@ describe('AgentToolService Integration', () => {
|
||||
canUpdate: true,
|
||||
canSoftDelete: true,
|
||||
canDestroy: true,
|
||||
restrictedFields: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -91,6 +92,7 @@ describe('AgentToolService Integration', () => {
|
||||
canUpdate: false,
|
||||
canSoftDelete: false,
|
||||
canDestroy: false,
|
||||
restrictedFields: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -168,6 +170,7 @@ describe('AgentToolService Integration', () => {
|
||||
canUpdate: true,
|
||||
canSoftDelete: true,
|
||||
canDestroy: false,
|
||||
restrictedFields: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -767,12 +770,14 @@ describe('AgentToolService Integration', () => {
|
||||
canUpdate: true,
|
||||
canSoftDelete: false,
|
||||
canDestroy: false,
|
||||
restrictedFields: {},
|
||||
},
|
||||
[secondObjectMetadata.id]: {
|
||||
canRead: true,
|
||||
canUpdate: false,
|
||||
canSoftDelete: true,
|
||||
canDestroy: false,
|
||||
restrictedFields: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@ -158,6 +158,7 @@ export const createAgentToolTestModule =
|
||||
targetRelationFields: [],
|
||||
dataSource: {} as any,
|
||||
objectPermissions: [],
|
||||
fieldPermissions: [],
|
||||
};
|
||||
|
||||
return {
|
||||
@ -207,6 +208,7 @@ export const setupBasicPermissions = (context: AgentToolTestContext) => {
|
||||
canUpdate: true,
|
||||
canSoftDelete: true,
|
||||
canDestroy: false,
|
||||
restrictedFields: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@ -1,7 +1,14 @@
|
||||
type ObjectMetadataId = string;
|
||||
export type ObjectRecordsPermissions = Record<ObjectMetadataId, {
|
||||
export type ObjectRecordsPermissions = Record<
|
||||
ObjectMetadataId,
|
||||
{
|
||||
canRead: boolean;
|
||||
canUpdate: boolean;
|
||||
canSoftDelete: boolean;
|
||||
canDestroy: boolean;
|
||||
}>;
|
||||
restrictedFields: Record<
|
||||
string,
|
||||
{ canRead?: boolean | null; canUpdate?: boolean | null }
|
||||
>;
|
||||
}
|
||||
>;
|
||||
|
||||
Reference in New Issue
Block a user