[permissions] Handle fieldPermissions configuration for 1N relations (#13630)
When upserting a fieldPermission on a 1 <-> N relation field, we should upsert the same fieldPermission on the relationTargetFieldMetadata to avoid inconsistencies.
This commit is contained in:
@@ -141,10 +141,12 @@ export const fieldRelationMock = getMockFieldMetadataEntity({
|
||||
nameSingular: 'relationTargetObject',
|
||||
namePlural: 'relationTargetObjects',
|
||||
} as ObjectMetadataEntity,
|
||||
relationTargetObjectMetadataId: 'relationTargetObjectId',
|
||||
relationTargetFieldMetadata: {
|
||||
id: 'relationTargetFieldId',
|
||||
name: 'relationTargetField',
|
||||
} as FieldMetadataEntity,
|
||||
relationTargetFieldMetadataId: 'relationTargetFieldId',
|
||||
isLabelSyncedWithName: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
|
||||
+74
@@ -5,9 +5,11 @@ import { ObjectRecordsPermissionsByRoleId } from 'twenty-shared/types';
|
||||
import { In, Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
fieldRelationMock,
|
||||
fieldTextMock,
|
||||
objectMetadataItemMock,
|
||||
} from 'src/engine/api/__mocks__/object-metadata-item.mock';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
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';
|
||||
@@ -27,6 +29,7 @@ describe('FieldPermissionService', () => {
|
||||
Repository<FieldPermissionEntity>
|
||||
>;
|
||||
let roleRepository: jest.Mocked<Repository<RoleEntity>>;
|
||||
let fieldMetadataRepository: jest.Mocked<Repository<FieldMetadataEntity>>;
|
||||
let workspacePermissionsCacheService: jest.Mocked<WorkspacePermissionsCacheService>;
|
||||
let workspaceCacheStorageService: jest.Mocked<WorkspaceCacheStorageService>;
|
||||
|
||||
@@ -59,6 +62,13 @@ describe('FieldPermissionService', () => {
|
||||
canDestroy: false,
|
||||
restrictedFields: {},
|
||||
},
|
||||
[fieldRelationMock.objectMetadataId]: {
|
||||
canRead: true,
|
||||
canUpdate: true,
|
||||
canSoftDelete: false,
|
||||
canDestroy: false,
|
||||
restrictedFields: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -93,6 +103,12 @@ describe('FieldPermissionService', () => {
|
||||
getObjectMetadataMapsOrThrow: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(FieldMetadataEntity, 'core'),
|
||||
useValue: {
|
||||
find: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
@@ -101,6 +117,9 @@ describe('FieldPermissionService', () => {
|
||||
getRepositoryToken(FieldPermissionEntity, 'core'),
|
||||
);
|
||||
roleRepository = module.get(getRepositoryToken(RoleEntity, 'core'));
|
||||
fieldMetadataRepository = module.get(
|
||||
getRepositoryToken(FieldMetadataEntity, 'core'),
|
||||
);
|
||||
workspacePermissionsCacheService = module.get(
|
||||
WorkspacePermissionsCacheService,
|
||||
);
|
||||
@@ -108,6 +127,10 @@ describe('FieldPermissionService', () => {
|
||||
|
||||
// Setup default mocks
|
||||
roleRepository.findOne.mockResolvedValue(mockRole);
|
||||
fieldMetadataRepository.find.mockResolvedValue([
|
||||
fieldTextMock,
|
||||
fieldRelationMock,
|
||||
]);
|
||||
workspacePermissionsCacheService.getRolesPermissionsFromCache.mockResolvedValue(
|
||||
{
|
||||
version: '1',
|
||||
@@ -132,6 +155,16 @@ describe('FieldPermissionService', () => {
|
||||
fieldIdByName: {},
|
||||
indexMetadatas: [],
|
||||
},
|
||||
[fieldRelationMock.objectMetadataId]: {
|
||||
...objectMetadataItemMock,
|
||||
id: fieldRelationMock.objectMetadataId,
|
||||
fieldsById: {
|
||||
[fieldRelationMock.id]: fieldRelationMock,
|
||||
},
|
||||
fieldIdByJoinColumnName: {},
|
||||
fieldIdByName: {},
|
||||
indexMetadatas: [],
|
||||
},
|
||||
},
|
||||
idByNameSingular: {
|
||||
testObject: testObjectMetadataId,
|
||||
@@ -268,6 +301,47 @@ describe('FieldPermissionService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('relation cases', () => {
|
||||
it('should create two field permissions when a relation field permission is created', async () => {
|
||||
const input = createUpsertInput([
|
||||
{
|
||||
canReadFieldValue: false,
|
||||
canUpdateFieldValue: false,
|
||||
fieldMetadataId: fieldRelationMock.id,
|
||||
objectMetadataId: fieldRelationMock.objectMetadataId,
|
||||
},
|
||||
]);
|
||||
|
||||
await service.upsertFieldPermissions({
|
||||
workspaceId: testWorkspaceId,
|
||||
input,
|
||||
});
|
||||
|
||||
expect(fieldPermissionsRepository.upsert).toHaveBeenCalledWith(
|
||||
[
|
||||
{
|
||||
fieldMetadataId: fieldRelationMock.id,
|
||||
objectMetadataId: fieldRelationMock.objectMetadataId,
|
||||
canReadFieldValue: false,
|
||||
canUpdateFieldValue: false,
|
||||
roleId: testRoleId,
|
||||
workspaceId: testWorkspaceId,
|
||||
},
|
||||
{
|
||||
fieldMetadataId: fieldRelationMock.relationTargetFieldMetadataId,
|
||||
objectMetadataId:
|
||||
fieldRelationMock.relationTargetObjectMetadataId,
|
||||
canReadFieldValue: false,
|
||||
canUpdateFieldValue: false,
|
||||
roleId: testRoleId,
|
||||
workspaceId: testWorkspaceId,
|
||||
},
|
||||
],
|
||||
{ conflictPaths: ['fieldMetadataId', 'roleId'] },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validation errors', () => {
|
||||
it('should throw error when canReadFieldValue is true', async () => {
|
||||
const input = createUpsertInput([
|
||||
|
||||
+136
-4
@@ -5,6 +5,11 @@ import { ObjectRecordsPermissionsByRoleId } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { In, Repository } from 'typeorm';
|
||||
|
||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||
|
||||
import { InternalServerError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { isFieldMetadataTypeRelation } from 'src/engine/metadata-modules/field-metadata/utils/is-field-metadata-type-relation.util';
|
||||
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 {
|
||||
@@ -22,6 +27,8 @@ export class FieldPermissionService {
|
||||
constructor(
|
||||
@InjectRepository(RoleEntity, 'core')
|
||||
private readonly roleRepository: Repository<RoleEntity>,
|
||||
@InjectRepository(FieldMetadataEntity, 'core')
|
||||
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
||||
@InjectRepository(FieldPermissionEntity, 'core')
|
||||
private readonly fieldPermissionsRepository: Repository<FieldPermissionEntity>,
|
||||
private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService,
|
||||
@@ -107,13 +114,39 @@ export class FieldPermissionService {
|
||||
),
|
||||
);
|
||||
|
||||
await this.fieldPermissionsRepository.upsert(fieldPermissionsToUpsert, {
|
||||
conflictPaths: ['fieldMetadataId', 'roleId'],
|
||||
});
|
||||
const fieldMetadatasForFieldPermissions =
|
||||
await this.fieldMetadataRepository.find({
|
||||
where: {
|
||||
id: In(fieldPermissions.map((fp) => fp.fieldMetadataId)),
|
||||
},
|
||||
});
|
||||
|
||||
const relatedFieldPermissionsToUpsert =
|
||||
this.computeFieldPermissionForRelationTargetFieldMetadata({
|
||||
fieldPermissions: fieldPermissionsToUpsert,
|
||||
fieldMetadatasForFieldPermissions,
|
||||
});
|
||||
|
||||
await this.fieldPermissionsRepository.upsert(
|
||||
[...fieldPermissionsToUpsert, ...relatedFieldPermissionsToUpsert],
|
||||
{
|
||||
conflictPaths: ['fieldMetadataId', 'roleId'],
|
||||
},
|
||||
);
|
||||
|
||||
if (fieldPermissionsToDeleteIds.length > 0) {
|
||||
const relatedFieldPermissionToDeleteIds =
|
||||
this.getRelatedFieldPermissionsToDeleteIds({
|
||||
allFieldPermissions: existingFieldPermissions,
|
||||
fieldPermissionsToDelete: existingFieldPermissionsToDelete,
|
||||
fieldMetadatas: fieldMetadatasForFieldPermissions,
|
||||
});
|
||||
|
||||
await this.fieldPermissionsRepository.delete({
|
||||
id: In(fieldPermissionsToDeleteIds),
|
||||
id: In([
|
||||
...fieldPermissionsToDeleteIds,
|
||||
...relatedFieldPermissionToDeleteIds,
|
||||
]),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -267,4 +300,103 @@ export class FieldPermissionService {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getRelatedFieldPermissionsToDeleteIds({
|
||||
allFieldPermissions,
|
||||
fieldPermissionsToDelete,
|
||||
fieldMetadatas,
|
||||
}: {
|
||||
allFieldPermissions: FieldPermissionEntity[];
|
||||
fieldPermissionsToDelete: FieldPermissionEntity[];
|
||||
fieldMetadatas: FieldMetadataEntity[];
|
||||
}) {
|
||||
const fieldMetadatasForFieldPermissionsToDelete = fieldMetadatas.filter(
|
||||
(fieldMetadata) =>
|
||||
fieldPermissionsToDelete.some(
|
||||
(existingFieldPermissionToDelete) =>
|
||||
existingFieldPermissionToDelete.fieldMetadataId ===
|
||||
fieldMetadata.id,
|
||||
),
|
||||
);
|
||||
|
||||
const relationTargetFieldMetadataIds: string[] = [];
|
||||
|
||||
for (const fieldMetadataForFieldPermissionToDelete of fieldMetadatasForFieldPermissionsToDelete) {
|
||||
if (
|
||||
isFieldMetadataTypeRelation(fieldMetadataForFieldPermissionToDelete)
|
||||
) {
|
||||
if (
|
||||
fieldMetadataForFieldPermissionToDelete.settings?.relationType ===
|
||||
RelationType.ONE_TO_MANY ||
|
||||
fieldMetadataForFieldPermissionToDelete.settings?.relationType ===
|
||||
RelationType.MANY_TO_ONE
|
||||
) {
|
||||
relationTargetFieldMetadataIds.push(
|
||||
fieldMetadataForFieldPermissionToDelete.relationTargetFieldMetadataId,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fieldPermissionsForRelationTargetFieldMetadataIds =
|
||||
allFieldPermissions
|
||||
.filter((fieldPermission) =>
|
||||
relationTargetFieldMetadataIds.includes(
|
||||
fieldPermission.fieldMetadataId,
|
||||
),
|
||||
)
|
||||
.map((fieldPermission) => fieldPermission.id);
|
||||
|
||||
return fieldPermissionsForRelationTargetFieldMetadataIds;
|
||||
}
|
||||
|
||||
private computeFieldPermissionForRelationTargetFieldMetadata({
|
||||
fieldPermissions,
|
||||
fieldMetadatasForFieldPermissions,
|
||||
}: {
|
||||
fieldPermissions: UpsertFieldPermissionsInput['fieldPermissions'];
|
||||
fieldMetadatasForFieldPermissions: FieldMetadataEntity[];
|
||||
}) {
|
||||
return fieldPermissions
|
||||
.map((fieldPermission) => {
|
||||
const fieldMetadata = fieldMetadatasForFieldPermissions.find(
|
||||
(fm) => fm.id === fieldPermission.fieldMetadataId,
|
||||
);
|
||||
|
||||
if (!isDefined(fieldMetadata)) {
|
||||
throw new InternalServerError(
|
||||
'Field metadata not found for field permission',
|
||||
);
|
||||
}
|
||||
|
||||
if (isFieldMetadataTypeRelation(fieldMetadata)) {
|
||||
if (
|
||||
fieldMetadata.settings?.relationType === RelationType.ONE_TO_MANY ||
|
||||
fieldMetadata.settings?.relationType === RelationType.MANY_TO_ONE
|
||||
) {
|
||||
const fieldPermissionInputHasFieldPermissionOnRelationTargetFieldMetadata =
|
||||
fieldPermissions.filter(
|
||||
(fieldPermissionInput) =>
|
||||
fieldPermissionInput.fieldMetadataId ===
|
||||
fieldMetadata.relationTargetFieldMetadataId,
|
||||
).length > 0;
|
||||
|
||||
if (
|
||||
fieldPermissionInputHasFieldPermissionOnRelationTargetFieldMetadata
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
...fieldPermission,
|
||||
objectMetadataId: fieldMetadata.relationTargetObjectMetadataId,
|
||||
fieldMetadataId: fieldMetadata.relationTargetFieldMetadataId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.filter(isDefined);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user