diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-delete-one.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-delete-one.handler.ts index 88bdcf737d1..ed1e60c7867 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-delete-one.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-delete-one.handler.ts @@ -29,7 +29,9 @@ export class RestApiDeleteOneHandler extends RestApiBaseHandler { select: selectOptions, }); - await repository.delete(recordId); + const columnsToReturnForDelete: string[] = []; + + await repository.delete(recordId, undefined, columnsToReturnForDelete); return this.formatResult({ operation: 'delete', diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service.ts index 78771b6a00a..fe83f1b5f92 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service.ts @@ -289,6 +289,9 @@ export class FieldMetadataService extends TypeOrmQueryService( workspaceId, 'view', + { + shouldBypassPermissionChecks: true, + }, ); await viewRepository.delete({ diff --git a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.ts b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.ts index 4bd6a219390..4eaa89652b7 100644 --- a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.ts +++ b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.ts @@ -583,6 +583,7 @@ export class WorkspaceEntityManager extends EntityManager { targetOrEntity: EntityTarget, criteria: unknown, permissionOptions?: PermissionOptions, + selectedColumns: string[] | '*' = '*', ): Promise { if ( criteria === undefined || @@ -611,6 +612,7 @@ export class WorkspaceEntityManager extends EntityManager { .delete() .from(targetOrEntity) .whereInIds(criteria) + .returning(selectedColumns) .execute(); } else { return this.createQueryBuilder( @@ -622,6 +624,7 @@ export class WorkspaceEntityManager extends EntityManager { .delete() .from(targetOrEntity) .where(criteria) + .returning(selectedColumns) .execute(); } } @@ -630,6 +633,7 @@ export class WorkspaceEntityManager extends EntityManager { targetOrEntity: EntityTarget, criteria: unknown, permissionOptions?: PermissionOptions, + selectedColumns: string[] | '*' = '*', ): Promise { // if user passed empty criteria or empty list of criterias, then throw an error if ( @@ -659,6 +663,7 @@ export class WorkspaceEntityManager extends EntityManager { .softDelete() .from(targetOrEntity) .whereInIds(criteria) + .returning(selectedColumns) .execute(); } else { return this.createQueryBuilder( @@ -670,6 +675,7 @@ export class WorkspaceEntityManager extends EntityManager { .softDelete() .from(targetOrEntity) .where(criteria) + .returning(selectedColumns) .execute(); } } @@ -678,6 +684,7 @@ export class WorkspaceEntityManager extends EntityManager { targetOrEntity: EntityTarget, criteria: unknown, permissionOptions?: PermissionOptions, + selectedColumns: string[] | '*' = '*', ): Promise { // if user passed empty criteria or empty list of criterias, then throw an error if ( @@ -707,6 +714,7 @@ export class WorkspaceEntityManager extends EntityManager { .restore() .from(targetOrEntity) .whereInIds(criteria) + .returning(selectedColumns) .execute(); } else { return this.createQueryBuilder( @@ -718,6 +726,7 @@ export class WorkspaceEntityManager extends EntityManager { .restore() .from(targetOrEntity) .where(criteria) + .returning(selectedColumns) .execute(); } } @@ -1237,6 +1246,10 @@ export class WorkspaceEntityManager extends EntityManager { objectMetadataItem: ObjectMetadataItemWithFieldMaps; permissionOptionsFromArgs: PermissionOptions | undefined; }): Entity[] { + if (permissionOptionsFromArgs?.shouldBypassPermissionChecks === true) { + return formattedResult; + } + const restrictedFields = permissionOptionsFromArgs?.objectRecordsPermissions?.[ objectMetadataItem.id diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/__tests__/workspace.repository.spec.ts b/packages/twenty-server/src/engine/twenty-orm/repository/__tests__/workspace.repository.spec.ts index a081ef00ae9..9ade08866aa 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/__tests__/workspace.repository.spec.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/__tests__/workspace.repository.spec.ts @@ -248,6 +248,7 @@ describe('WorkspaceRepository', () => { shouldBypassPermissionChecks: false, objectRecordsPermissions: mockObjectRecordsPermissions, }, + undefined, ); expect(result).toEqual(expectedResult); }); diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/permissions.utils.ts b/packages/twenty-server/src/engine/twenty-orm/repository/permissions.utils.ts index 63b5419dec5..2b440b2d037 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/permissions.utils.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/permissions.utils.ts @@ -332,7 +332,7 @@ const getSelectedColumnsFromExpressionMap = ({ operationType, ) ) { - if (isEmpty(expressionMap.returning)) { + if (!isDefined(expressionMap.returning)) { throw new InternalServerError( 'Returning columns are not set for update query', ); diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts b/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts index 0487a78c31f..78b6f505690 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts @@ -348,6 +348,7 @@ export class WorkspaceRepository< | ObjectId[] | FindOptionsWhere, entityManager?: WorkspaceEntityManager, + selectedColumns?: string[] | '*', ): Promise { const manager = entityManager || this.manager; @@ -360,7 +361,12 @@ export class WorkspaceRepository< objectRecordsPermissions: this.objectRecordsPermissions, }; - return manager.delete(this.target, criteria, permissionOptions); + return manager.delete( + this.target, + criteria, + permissionOptions, + selectedColumns, + ); } override softRemove>( @@ -431,6 +437,7 @@ export class WorkspaceRepository< | ObjectId[] | FindOptionsWhere, entityManager?: WorkspaceEntityManager, + selectedColumns?: string[], ): Promise { const manager = entityManager || this.manager; @@ -443,7 +450,12 @@ export class WorkspaceRepository< objectRecordsPermissions: this.objectRecordsPermissions, }; - return manager.softDelete(this.target, criteria, permissionOptions); + return manager.softDelete( + this.target, + criteria, + permissionOptions, + selectedColumns, + ); } /** @@ -517,6 +529,7 @@ export class WorkspaceRepository< | ObjectId[] | FindOptionsWhere, entityManager?: WorkspaceEntityManager, + selectedColumns?: string[], ): Promise { const manager = entityManager || this.manager; @@ -529,7 +542,12 @@ export class WorkspaceRepository< objectRecordsPermissions: this.objectRecordsPermissions, }; - return manager.restore(this.target, criteria, permissionOptions); + return manager.restore( + this.target, + criteria, + permissionOptions, + selectedColumns, + ); } /** diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/services/dev-seeder-permissions.service.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/services/dev-seeder-permissions.service.ts index f9abe62973d..aaa02563e4a 100644 --- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/services/dev-seeder-permissions.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/services/dev-seeder-permissions.service.ts @@ -7,6 +7,7 @@ import { Repository } from 'typeorm'; import { TypeORMService } from 'src/database/typeorm/typeorm.service'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +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 { RoleService } from 'src/engine/metadata-modules/role/role.service'; import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service'; @@ -30,6 +31,7 @@ export class DevSeederPermissionsService { private readonly objectMetadataRepository: Repository, private readonly typeORMService: TypeORMService, private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService, + private readonly fieldPermissionService: FieldPermissionService, ) {} public async initPermissions(workspaceId: string) { @@ -175,6 +177,28 @@ export class DevSeederPermissionsService { }, }); + const personObjectMetadata = + await this.objectMetadataRepository.findOneOrFail({ + where: { + nameSingular: 'person', + workspaceId, + }, + relations: { + fields: true, + }, + }); + + const companyObjectMetadata = + await this.objectMetadataRepository.findOneOrFail({ + where: { + nameSingular: 'company', + workspaceId, + }, + relations: { + fields: true, + }, + }); + await this.objectPermissionService.upsertObjectPermissions({ workspaceId, input: { @@ -198,6 +222,47 @@ export class DevSeederPermissionsService { }, }); + const personCityFieldMetadata = personObjectMetadata.fields.find( + (field) => field.name === 'city', + ); + + if (!personCityFieldMetadata) { + throw new Error('Person city field metadata not found'); + } + + const companyLinkedinLinkFieldMetadata = companyObjectMetadata.fields.find( + (field) => field.name === 'linkedinLink', + ); + + if (!companyLinkedinLinkFieldMetadata) { + throw new Error('Company linkedin link field metadata not found'); + } + + const readOnlyOnPersonCityFieldPermission = { + objectMetadataId: personObjectMetadata.id, + fieldMetadataId: personCityFieldMetadata.id, + canReadFieldValue: null, + canUpdateFieldValue: false, + }; + + const noReadOnCompanyLinkedinLinkFieldPermission = { + objectMetadataId: companyObjectMetadata.id, + fieldMetadataId: companyLinkedinLinkFieldMetadata.id, + canReadFieldValue: false, + canUpdateFieldValue: false, + }; + + await this.fieldPermissionService.upsertFieldPermissions({ + workspaceId, + input: { + roleId: customRole.id, + fieldPermissions: [ + readOnlyOnPersonCityFieldPermission, + noReadOnCompanyLinkedinLinkFieldPermission, + ], + }, + }); + return customRole; } } diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts index 68f7c1d6104..821424a5974 100644 --- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts @@ -75,6 +75,11 @@ export const seedFeatureFlags = async ( workspaceId: workspaceId, value: true, }, + { + key: FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED, + workspaceId: workspaceId, + value: true, + }, ]) .execute(); }; diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/delete-record.workflow-action.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/delete-record.workflow-action.ts index b08fc29378d..908ea88b0a4 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/delete-record.workflow-action.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/delete-record.workflow-action.ts @@ -92,7 +92,13 @@ export class DeleteRecordWorkflowAction implements WorkflowAction { ); } - await repository.softDelete(workflowActionInput.objectRecordId); + const columnsToReturnForSoftDelete: string[] = []; + + await repository.softDelete( + workflowActionInput.objectRecordId, + undefined, + columnsToReturnForSoftDelete, + ); return { result: objectRecord,