Migrate object permission to syncable entity (#18609)
Closes [#2223](https://github.com/twentyhq/core-team-issues/issues/2223)
This commit is contained in:
+115
-134
@@ -1,58 +1,67 @@
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
|
||||
import { type Repository } from 'typeorm';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { type UpsertObjectPermissionsInput } from 'src/engine/metadata-modules/object-permission/dtos/upsert-object-permissions.input';
|
||||
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 {
|
||||
PermissionsException,
|
||||
PermissionsExceptionCode,
|
||||
PermissionsExceptionMessage,
|
||||
} from 'src/engine/metadata-modules/permissions/permissions.exception';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
|
||||
|
||||
const createMockFlatRoleMaps = (roleId: string, isEditable: boolean) => ({
|
||||
byUniversalIdentifier: {
|
||||
[roleId]: {
|
||||
id: roleId,
|
||||
universalIdentifier: roleId,
|
||||
isEditable,
|
||||
canReadAllObjectRecords: true,
|
||||
canUpdateAllObjectRecords: true,
|
||||
canSoftDeleteAllObjectRecords: false,
|
||||
canDestroyAllObjectRecords: false,
|
||||
},
|
||||
},
|
||||
universalIdentifierById: { [roleId]: roleId },
|
||||
byId: { [roleId]: { id: roleId, universalIdentifier: roleId, isEditable } },
|
||||
});
|
||||
|
||||
const createMockFlatObjectMetadataMaps = (
|
||||
objectMetadataId: string,
|
||||
isSystem: boolean,
|
||||
) => ({
|
||||
byUniversalIdentifier: {
|
||||
[objectMetadataId]: {
|
||||
id: objectMetadataId,
|
||||
universalIdentifier: objectMetadataId,
|
||||
isSystem,
|
||||
},
|
||||
},
|
||||
universalIdentifierById: { [objectMetadataId]: objectMetadataId },
|
||||
byId: { [objectMetadataId]: { id: objectMetadataId, isSystem } },
|
||||
});
|
||||
|
||||
describe('ObjectPermissionService', () => {
|
||||
let service: ObjectPermissionService;
|
||||
let objectPermissionRepository: jest.Mocked<
|
||||
Repository<ObjectPermissionEntity>
|
||||
>;
|
||||
let roleRepository: jest.Mocked<Repository<RoleEntity>>;
|
||||
let workspaceCacheService: jest.Mocked<WorkspaceCacheService>;
|
||||
let workspaceManyOrAllFlatEntityMapsCacheService: jest.Mocked<WorkspaceManyOrAllFlatEntityMapsCacheService>;
|
||||
let workspaceMigrationValidateBuildAndRunService: jest.Mocked<WorkspaceMigrationValidateBuildAndRunService>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
ObjectPermissionService,
|
||||
{
|
||||
provide: getRepositoryToken(ObjectPermissionEntity),
|
||||
provide: ApplicationService,
|
||||
useValue: {
|
||||
upsert: jest.fn(),
|
||||
find: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(RoleEntity),
|
||||
useValue: {
|
||||
findOne: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(ObjectMetadataEntity),
|
||||
useValue: {
|
||||
find: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: WorkspaceCacheService,
|
||||
useValue: {
|
||||
invalidate: jest.fn(),
|
||||
invalidateAndRecompute: jest.fn(),
|
||||
findWorkspaceTwentyStandardAndCustomApplicationOrThrow: jest
|
||||
.fn()
|
||||
.mockResolvedValue({
|
||||
workspaceCustomFlatApplication: {
|
||||
id: 'app-id',
|
||||
universalIdentifier: 'app-universal-id',
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -61,18 +70,22 @@ describe('ObjectPermissionService', () => {
|
||||
getOrRecomputeManyOrAllFlatEntityMaps: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: WorkspaceMigrationValidateBuildAndRunService,
|
||||
useValue: {
|
||||
validateBuildAndRunWorkspaceMigration: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<ObjectPermissionService>(ObjectPermissionService);
|
||||
objectPermissionRepository = module.get(
|
||||
getRepositoryToken(ObjectPermissionEntity),
|
||||
);
|
||||
roleRepository = module.get(getRepositoryToken(RoleEntity));
|
||||
workspaceCacheService = module.get(WorkspaceCacheService);
|
||||
workspaceManyOrAllFlatEntityMapsCacheService = module.get(
|
||||
WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
);
|
||||
workspaceMigrationValidateBuildAndRunService = module.get(
|
||||
WorkspaceMigrationValidateBuildAndRunService,
|
||||
);
|
||||
});
|
||||
|
||||
describe('upsertObjectPermissions', () => {
|
||||
@@ -81,18 +94,7 @@ describe('ObjectPermissionService', () => {
|
||||
const systemObjectMetadataId = 'system-object-id';
|
||||
const customObjectMetadataId = 'custom-object-id';
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock role validation
|
||||
roleRepository.findOne.mockResolvedValue({
|
||||
id: roleId,
|
||||
workspaceId,
|
||||
isEditable: true,
|
||||
objectPermissions: [],
|
||||
} as unknown as RoleEntity);
|
||||
});
|
||||
|
||||
it('should throw PermissionsException when trying to add object permission on system object', async () => {
|
||||
// Arrange
|
||||
const input: UpsertObjectPermissionsInput = {
|
||||
roleId,
|
||||
objectPermissions: [
|
||||
@@ -106,31 +108,21 @@ describe('ObjectPermissionService', () => {
|
||||
],
|
||||
};
|
||||
|
||||
// Mock flat object metadata maps with a system object
|
||||
workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps.mockResolvedValue(
|
||||
{
|
||||
flatObjectMetadataMaps: {
|
||||
byUniversalIdentifier: {
|
||||
[systemObjectMetadataId]: {
|
||||
id: systemObjectMetadataId,
|
||||
isSystem: true,
|
||||
workspaceId,
|
||||
fieldIds: [],
|
||||
indexMetadataIds: [],
|
||||
viewIds: [],
|
||||
universalIdentifier: systemObjectMetadataId,
|
||||
applicationId: null,
|
||||
} as any,
|
||||
},
|
||||
universalIdentifierById: {
|
||||
[systemObjectMetadataId]: systemObjectMetadataId,
|
||||
},
|
||||
universalIdentifiersByApplicationId: {},
|
||||
flatObjectPermissionMaps: {
|
||||
byUniversalIdentifier: {},
|
||||
universalIdentifierById: {},
|
||||
byId: {},
|
||||
},
|
||||
flatRoleMaps: createMockFlatRoleMaps(roleId, true),
|
||||
flatObjectMetadataMaps: createMockFlatObjectMetadataMaps(
|
||||
systemObjectMetadataId,
|
||||
true,
|
||||
),
|
||||
} as any,
|
||||
);
|
||||
|
||||
// Act & Assert
|
||||
await expect(
|
||||
service.upsertObjectPermissions({
|
||||
workspaceId,
|
||||
@@ -143,15 +135,12 @@ describe('ObjectPermissionService', () => {
|
||||
),
|
||||
);
|
||||
|
||||
// Verify that upsert was never called
|
||||
expect(objectPermissionRepository.upsert).not.toHaveBeenCalled();
|
||||
expect(
|
||||
workspaceCacheService.invalidateAndRecompute,
|
||||
workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration,
|
||||
).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should successfully create object permission for custom (non-system) object', async () => {
|
||||
// Arrange
|
||||
const input: UpsertObjectPermissionsInput = {
|
||||
roleId,
|
||||
objectPermissions: [
|
||||
@@ -165,82 +154,70 @@ describe('ObjectPermissionService', () => {
|
||||
],
|
||||
};
|
||||
|
||||
// Mock flat object metadata maps with a custom object
|
||||
workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps.mockResolvedValue(
|
||||
{
|
||||
flatObjectMetadataMaps: {
|
||||
byUniversalIdentifier: {
|
||||
[customObjectMetadataId]: {
|
||||
id: customObjectMetadataId,
|
||||
isSystem: false,
|
||||
workspaceId,
|
||||
fieldIds: [],
|
||||
indexMetadataIds: [],
|
||||
viewIds: [],
|
||||
universalIdentifier: customObjectMetadataId,
|
||||
applicationId: null,
|
||||
} as any,
|
||||
},
|
||||
universalIdentifierById: {
|
||||
[customObjectMetadataId]: customObjectMetadataId,
|
||||
},
|
||||
universalIdentifiersByApplicationId: {},
|
||||
},
|
||||
} as any,
|
||||
);
|
||||
|
||||
// Mock successful upsert
|
||||
const mockObjectPermission = {
|
||||
const permissionUniversalId = 'permission-universal-id';
|
||||
const freshFlatObjectPermission = {
|
||||
id: 'permission-id',
|
||||
universalIdentifier: permissionUniversalId,
|
||||
roleId,
|
||||
roleUniversalIdentifier: roleId,
|
||||
objectMetadataId: customObjectMetadataId,
|
||||
workspaceId,
|
||||
objectMetadataUniversalIdentifier: customObjectMetadataId,
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
canDestroyObjectRecords: false,
|
||||
} as ObjectPermissionEntity;
|
||||
};
|
||||
|
||||
objectPermissionRepository.upsert.mockResolvedValue({
|
||||
generatedMaps: [{ id: 'permission-id' }],
|
||||
identifiers: [],
|
||||
raw: [],
|
||||
});
|
||||
workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps
|
||||
.mockResolvedValueOnce({
|
||||
flatObjectPermissionMaps: {
|
||||
byUniversalIdentifier: {},
|
||||
universalIdentifierById: {},
|
||||
byId: {},
|
||||
},
|
||||
flatRoleMaps: createMockFlatRoleMaps(roleId, true),
|
||||
flatObjectMetadataMaps: createMockFlatObjectMetadataMaps(
|
||||
customObjectMetadataId,
|
||||
false,
|
||||
),
|
||||
} as any)
|
||||
.mockResolvedValueOnce({
|
||||
flatObjectPermissionMaps: {
|
||||
byUniversalIdentifier: {
|
||||
[permissionUniversalId]: freshFlatObjectPermission,
|
||||
},
|
||||
universalIdentifierById: {
|
||||
[freshFlatObjectPermission.id]: permissionUniversalId,
|
||||
},
|
||||
byId: {
|
||||
[freshFlatObjectPermission.id]: freshFlatObjectPermission,
|
||||
},
|
||||
},
|
||||
} as any);
|
||||
|
||||
objectPermissionRepository.find.mockResolvedValue([mockObjectPermission]);
|
||||
workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration.mockResolvedValue(
|
||||
{ status: 'success' } as any,
|
||||
);
|
||||
|
||||
// Act
|
||||
const result = await service.upsertObjectPermissions({
|
||||
workspaceId,
|
||||
input,
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual([mockObjectPermission]);
|
||||
expect(objectPermissionRepository.upsert).toHaveBeenCalledWith(
|
||||
[
|
||||
{
|
||||
objectMetadataId: customObjectMetadataId,
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
canDestroyObjectRecords: false,
|
||||
roleId,
|
||||
workspaceId,
|
||||
},
|
||||
],
|
||||
{
|
||||
conflictPaths: ['objectMetadataId', 'roleId'],
|
||||
},
|
||||
);
|
||||
expect(workspaceCacheService.invalidateAndRecompute).toHaveBeenCalledWith(
|
||||
workspaceId,
|
||||
['rolesPermissions'],
|
||||
);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toMatchObject({
|
||||
objectMetadataId: customObjectMetadataId,
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
canDestroyObjectRecords: false,
|
||||
});
|
||||
expect(
|
||||
workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration,
|
||||
).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw PermissionsException when object metadata is not found', async () => {
|
||||
// Arrange
|
||||
const input: UpsertObjectPermissionsInput = {
|
||||
roleId,
|
||||
objectPermissions: [
|
||||
@@ -254,18 +231,22 @@ describe('ObjectPermissionService', () => {
|
||||
],
|
||||
};
|
||||
|
||||
// Mock empty flat object metadata maps
|
||||
workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps.mockResolvedValue(
|
||||
{
|
||||
flatObjectPermissionMaps: {
|
||||
byUniversalIdentifier: {},
|
||||
universalIdentifierById: {},
|
||||
byId: {},
|
||||
},
|
||||
flatRoleMaps: createMockFlatRoleMaps(roleId, true),
|
||||
flatObjectMetadataMaps: {
|
||||
byUniversalIdentifier: {},
|
||||
universalIdentifierById: {},
|
||||
universalIdentifiersByApplicationId: {},
|
||||
byId: {},
|
||||
},
|
||||
} as any,
|
||||
);
|
||||
|
||||
// Act & Assert
|
||||
await expect(
|
||||
service.upsertObjectPermissions({
|
||||
workspaceId,
|
||||
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
export type CreateObjectPermissionInput = {
|
||||
roleId: string;
|
||||
objectMetadataId: string;
|
||||
canReadObjectRecords?: boolean;
|
||||
canUpdateObjectRecords?: boolean;
|
||||
canSoftDeleteObjectRecords?: boolean;
|
||||
canDestroyObjectRecords?: boolean;
|
||||
universalIdentifier?: string;
|
||||
};
|
||||
+2
-2
@@ -13,7 +13,7 @@ import {
|
||||
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { WorkspaceRelatedEntity } from 'src/engine/workspace-manager/types/workspace-related-entity';
|
||||
import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
|
||||
|
||||
@Entity('objectPermission')
|
||||
@Unique('IDX_OBJECT_PERMISSION_OBJECT_METADATA_ID_ROLE_ID_UNIQUE', [
|
||||
@@ -21,7 +21,7 @@ import { WorkspaceRelatedEntity } from 'src/engine/workspace-manager/types/works
|
||||
'roleId',
|
||||
])
|
||||
@Index('IDX_OBJECT_PERMISSION_WORKSPACE_ID_ROLE_ID', ['workspaceId', 'roleId'])
|
||||
export class ObjectPermissionEntity extends WorkspaceRelatedEntity {
|
||||
export class ObjectPermissionEntity extends SyncableEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
|
||||
+5
-1
@@ -1,6 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
@@ -10,6 +11,7 @@ import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permi
|
||||
import { ObjectPermissionService } from 'src/engine/metadata-modules/object-permission/object-permission.service';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
|
||||
import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -20,8 +22,10 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache
|
||||
FieldPermissionEntity,
|
||||
FieldMetadataEntity,
|
||||
]),
|
||||
WorkspaceManyOrAllFlatEntityMapsCacheModule,
|
||||
WorkspaceCacheModule,
|
||||
ApplicationModule,
|
||||
WorkspaceMigrationModule,
|
||||
WorkspaceManyOrAllFlatEntityMapsCacheModule,
|
||||
],
|
||||
providers: [ObjectPermissionService, FieldPermissionService],
|
||||
exports: [ObjectPermissionService, FieldPermissionService],
|
||||
|
||||
+248
-205
@@ -1,35 +1,33 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { In, Repository } from 'typeorm';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { type FlatObjectPermission } from 'src/engine/metadata-modules/flat-object-permission/types/flat-object-permission.type';
|
||||
import { fromCreateObjectPermissionInputToUniversalFlatObjectPermission } from 'src/engine/metadata-modules/flat-object-permission/utils/from-create-object-permission-input-to-universal-flat-object-permission.util';
|
||||
import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type';
|
||||
import {
|
||||
type ObjectPermissionInput,
|
||||
type UpsertObjectPermissionsInput,
|
||||
} from 'src/engine/metadata-modules/object-permission/dtos/upsert-object-permissions.input';
|
||||
import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-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 { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception';
|
||||
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
|
||||
import { type UniversalFlatObjectPermission } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-object-permission.type';
|
||||
|
||||
@Injectable()
|
||||
export class ObjectPermissionService {
|
||||
constructor(
|
||||
@InjectRepository(ObjectPermissionEntity)
|
||||
private readonly objectPermissionRepository: Repository<ObjectPermissionEntity>,
|
||||
@InjectRepository(RoleEntity)
|
||||
private readonly roleRepository: Repository<RoleEntity>,
|
||||
@InjectRepository(ObjectMetadataEntity)
|
||||
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
|
||||
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
private readonly applicationService: ApplicationService,
|
||||
) {}
|
||||
|
||||
public async upsertObjectPermissions({
|
||||
@@ -38,144 +36,269 @@ export class ObjectPermissionService {
|
||||
}: {
|
||||
workspaceId: string;
|
||||
input: UpsertObjectPermissionsInput;
|
||||
}): Promise<ObjectPermissionEntity[]> {
|
||||
try {
|
||||
const role = await this.getRoleOrThrow({
|
||||
roleId: input.roleId,
|
||||
workspaceId,
|
||||
}): Promise<FlatObjectPermission[]> {
|
||||
const { flatObjectPermissionMaps, flatRoleMaps, flatObjectMetadataMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: [
|
||||
'flatObjectPermissionMaps',
|
||||
'flatRoleMaps',
|
||||
'flatObjectMetadataMaps',
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
const roleUniversalIdentifier =
|
||||
flatRoleMaps.universalIdentifierById[input.roleId];
|
||||
const flatRole = isDefined(roleUniversalIdentifier)
|
||||
? flatRoleMaps.byUniversalIdentifier[roleUniversalIdentifier]
|
||||
: undefined;
|
||||
|
||||
if (!isDefined(flatRole)) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.ROLE_NOT_FOUND,
|
||||
PermissionsExceptionCode.ROLE_NOT_FOUND,
|
||||
{
|
||||
userFriendlyMessage: msg`The role you are trying to modify could not be found.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const currentObjectPermissionsForRole = Object.values(
|
||||
flatObjectPermissionMaps.byUniversalIdentifier,
|
||||
).filter(
|
||||
(op): op is FlatObjectPermission =>
|
||||
isDefined(op) && op.roleUniversalIdentifier === roleUniversalIdentifier,
|
||||
);
|
||||
|
||||
this.validateObjectPermissionsReadAndWriteConsistencyOrThrow({
|
||||
objectPermissions: input.objectPermissions,
|
||||
flatRole,
|
||||
currentObjectPermissionsForRole,
|
||||
});
|
||||
|
||||
const flatApplication =
|
||||
await this.getFlatApplicationForWorkspace(workspaceId);
|
||||
|
||||
const desiredByObjectMetadataId = new Map(
|
||||
input.objectPermissions.map((op) => [op.objectMetadataId, op]),
|
||||
);
|
||||
|
||||
for (const desired of input.objectPermissions) {
|
||||
const objectMetadata = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: desired.objectMetadataId,
|
||||
flatEntityMaps: flatObjectMetadataMaps,
|
||||
});
|
||||
|
||||
await this.validateRoleIsEditableOrThrow({
|
||||
role,
|
||||
});
|
||||
|
||||
await this.validateObjectPermissionsReadAndWriteConsistencyOrThrow({
|
||||
objectPermissions: input.objectPermissions,
|
||||
roleWithObjectPermissions: role,
|
||||
});
|
||||
|
||||
const { flatObjectMetadataMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
if (!isDefined(objectMetadata)) {
|
||||
throw new PermissionsException(
|
||||
'Object metadata id not found',
|
||||
PermissionsExceptionCode.OBJECT_METADATA_NOT_FOUND,
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatObjectMetadataMaps'],
|
||||
userFriendlyMessage: msg`The object you are trying to set permissions for could not be found. It may have been deleted.`,
|
||||
},
|
||||
);
|
||||
|
||||
input.objectPermissions.forEach((objectPermission) => {
|
||||
const objectMetadataForObjectPermission =
|
||||
findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: objectPermission.objectMetadataId,
|
||||
flatEntityMaps: flatObjectMetadataMaps,
|
||||
});
|
||||
|
||||
if (!isDefined(objectMetadataForObjectPermission)) {
|
||||
throw new PermissionsException(
|
||||
'Object metadata id not found',
|
||||
PermissionsExceptionCode.OBJECT_METADATA_NOT_FOUND,
|
||||
{
|
||||
userFriendlyMessage: msg`The object you are trying to set permissions for could not be found. It may have been deleted.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (objectMetadataForObjectPermission.isSystem === true) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.CANNOT_ADD_OBJECT_PERMISSION_ON_SYSTEM_OBJECT,
|
||||
PermissionsExceptionCode.CANNOT_ADD_OBJECT_PERMISSION_ON_SYSTEM_OBJECT,
|
||||
{
|
||||
userFriendlyMessage: msg`You cannot set permissions on system objects as they are managed by the platform.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const objectPermissions = input.objectPermissions.map(
|
||||
(objectPermission) => ({
|
||||
...objectPermission,
|
||||
roleId: input.roleId,
|
||||
workspaceId,
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await this.objectPermissionRepository.upsert(
|
||||
objectPermissions,
|
||||
{
|
||||
conflictPaths: ['objectMetadataId', 'roleId'],
|
||||
},
|
||||
);
|
||||
|
||||
const objectPermissionId = result.generatedMaps?.[0]?.id;
|
||||
|
||||
if (!isDefined(objectPermissionId)) {
|
||||
throw new Error('Failed to upsert object permission');
|
||||
}
|
||||
|
||||
await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [
|
||||
'rolesPermissions',
|
||||
]);
|
||||
|
||||
return this.objectPermissionRepository.find({
|
||||
where: {
|
||||
roleId: input.roleId,
|
||||
objectMetadataId: In(
|
||||
input.objectPermissions.map(
|
||||
(objectPermission) => objectPermission.objectMetadataId,
|
||||
),
|
||||
),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
await this.handleForeignKeyError({
|
||||
error,
|
||||
roleId: input.roleId,
|
||||
workspaceId,
|
||||
objectMetadataIds: input.objectPermissions.map(
|
||||
(objectPermission) => objectPermission.objectMetadataId,
|
||||
),
|
||||
});
|
||||
|
||||
throw error;
|
||||
if (objectMetadata.isSystem === true) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.CANNOT_ADD_OBJECT_PERMISSION_ON_SYSTEM_OBJECT,
|
||||
PermissionsExceptionCode.CANNOT_ADD_OBJECT_PERMISSION_ON_SYSTEM_OBJECT,
|
||||
{
|
||||
userFriendlyMessage: msg`You cannot set permissions on system objects as they are managed by the platform.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const flatEntityToCreate: (UniversalFlatObjectPermission & {
|
||||
id: string;
|
||||
})[] = [];
|
||||
const flatEntityToUpdate: UniversalFlatObjectPermission[] = [];
|
||||
const flatEntityToDelete: UniversalFlatObjectPermission[] = [];
|
||||
|
||||
const currentByObjectMetadataId = new Map(
|
||||
currentObjectPermissionsForRole.map((op) => [op.objectMetadataId, op]),
|
||||
);
|
||||
|
||||
for (const desired of input.objectPermissions) {
|
||||
const current = currentByObjectMetadataId.get(desired.objectMetadataId);
|
||||
|
||||
if (!isDefined(current)) {
|
||||
flatEntityToCreate.push(
|
||||
fromCreateObjectPermissionInputToUniversalFlatObjectPermission({
|
||||
createObjectPermissionInput: {
|
||||
roleId: input.roleId,
|
||||
objectMetadataId: desired.objectMetadataId,
|
||||
canReadObjectRecords: desired.canReadObjectRecords,
|
||||
canUpdateObjectRecords: desired.canUpdateObjectRecords,
|
||||
canSoftDeleteObjectRecords: desired.canSoftDeleteObjectRecords,
|
||||
canDestroyObjectRecords: desired.canDestroyObjectRecords,
|
||||
},
|
||||
flatApplication,
|
||||
flatRoleMaps,
|
||||
flatObjectMetadataMaps,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
const effectiveCanRead =
|
||||
desired.canReadObjectRecords ?? current.canReadObjectRecords;
|
||||
const effectiveCanUpdate =
|
||||
desired.canUpdateObjectRecords ?? current.canUpdateObjectRecords;
|
||||
const effectiveCanSoftDelete =
|
||||
desired.canSoftDeleteObjectRecords ??
|
||||
current.canSoftDeleteObjectRecords;
|
||||
const effectiveCanDestroy =
|
||||
desired.canDestroyObjectRecords ?? current.canDestroyObjectRecords;
|
||||
|
||||
const canChanged =
|
||||
effectiveCanRead !== current.canReadObjectRecords ||
|
||||
effectiveCanUpdate !== current.canUpdateObjectRecords ||
|
||||
effectiveCanSoftDelete !== current.canSoftDeleteObjectRecords ||
|
||||
effectiveCanDestroy !== current.canDestroyObjectRecords;
|
||||
|
||||
if (canChanged) {
|
||||
const now = new Date().toISOString();
|
||||
flatEntityToUpdate.push({
|
||||
universalIdentifier: current.universalIdentifier,
|
||||
applicationUniversalIdentifier:
|
||||
current.applicationUniversalIdentifier,
|
||||
roleUniversalIdentifier: current.roleUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier:
|
||||
current.objectMetadataUniversalIdentifier,
|
||||
canReadObjectRecords: effectiveCanRead,
|
||||
canUpdateObjectRecords: effectiveCanUpdate,
|
||||
canSoftDeleteObjectRecords: effectiveCanSoftDelete,
|
||||
canDestroyObjectRecords: effectiveCanDestroy,
|
||||
createdAt: current.createdAt,
|
||||
updatedAt: now,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const current of currentObjectPermissionsForRole) {
|
||||
if (!desiredByObjectMetadataId.has(current.objectMetadataId)) {
|
||||
flatEntityToDelete.push({
|
||||
universalIdentifier: current.universalIdentifier,
|
||||
applicationUniversalIdentifier:
|
||||
current.applicationUniversalIdentifier,
|
||||
roleUniversalIdentifier: current.roleUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier:
|
||||
current.objectMetadataUniversalIdentifier,
|
||||
canReadObjectRecords: current.canReadObjectRecords,
|
||||
canUpdateObjectRecords: current.canUpdateObjectRecords,
|
||||
canSoftDeleteObjectRecords: current.canSoftDeleteObjectRecords,
|
||||
canDestroyObjectRecords: current.canDestroyObjectRecords,
|
||||
createdAt: current.createdAt,
|
||||
updatedAt: current.updatedAt,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
flatEntityToCreate.length === 0 &&
|
||||
flatEntityToUpdate.length === 0 &&
|
||||
flatEntityToDelete.length === 0
|
||||
) {
|
||||
const unchanged = currentObjectPermissionsForRole.filter((op) =>
|
||||
desiredByObjectMetadataId.has(op.objectMetadataId),
|
||||
);
|
||||
return unchanged;
|
||||
}
|
||||
|
||||
const buildAndRunResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
objectPermission: {
|
||||
flatEntityToCreate,
|
||||
flatEntityToUpdate,
|
||||
flatEntityToDelete,
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
isSystemBuild: false,
|
||||
applicationUniversalIdentifier: flatApplication.universalIdentifier,
|
||||
},
|
||||
);
|
||||
|
||||
if (buildAndRunResult.status === 'fail') {
|
||||
throw new WorkspaceMigrationBuilderException(
|
||||
buildAndRunResult,
|
||||
'Validation errors occurred while upserting object permissions',
|
||||
);
|
||||
}
|
||||
|
||||
const { flatObjectPermissionMaps: freshFlatObjectPermissionMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatObjectPermissionMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
const resultObjectPermissions = Object.values(
|
||||
freshFlatObjectPermissionMaps.byUniversalIdentifier,
|
||||
).filter(
|
||||
(op): op is FlatObjectPermission =>
|
||||
isDefined(op) && op.roleUniversalIdentifier === roleUniversalIdentifier,
|
||||
);
|
||||
|
||||
const desiredObjectMetadataIds = new Set(
|
||||
input.objectPermissions.map((op) => op.objectMetadataId),
|
||||
);
|
||||
const filtered = resultObjectPermissions.filter((op) =>
|
||||
desiredObjectMetadataIds.has(op.objectMetadataId),
|
||||
);
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
private async validateObjectPermissionsReadAndWriteConsistencyOrThrow({
|
||||
private validateObjectPermissionsReadAndWriteConsistencyOrThrow({
|
||||
objectPermissions: newObjectPermissions,
|
||||
roleWithObjectPermissions,
|
||||
flatRole,
|
||||
currentObjectPermissionsForRole,
|
||||
}: {
|
||||
objectPermissions: ObjectPermissionInput[];
|
||||
roleWithObjectPermissions: RoleEntity;
|
||||
}) {
|
||||
const existingObjectPermissions =
|
||||
roleWithObjectPermissions.objectPermissions;
|
||||
|
||||
flatRole: Pick<
|
||||
FlatRole,
|
||||
| 'canReadAllObjectRecords'
|
||||
| 'canUpdateAllObjectRecords'
|
||||
| 'canSoftDeleteAllObjectRecords'
|
||||
| 'canDestroyAllObjectRecords'
|
||||
>;
|
||||
currentObjectPermissionsForRole: FlatObjectPermission[];
|
||||
}): void {
|
||||
for (const newObjectPermission of newObjectPermissions) {
|
||||
const existingObjectRecordPermission = existingObjectPermissions.find(
|
||||
(objectPermission) =>
|
||||
objectPermission.objectMetadataId ===
|
||||
newObjectPermission.objectMetadataId,
|
||||
);
|
||||
const existingObjectRecordPermission =
|
||||
currentObjectPermissionsForRole.find(
|
||||
(objectPermission) =>
|
||||
objectPermission.objectMetadataId ===
|
||||
newObjectPermission.objectMetadataId,
|
||||
);
|
||||
|
||||
const hasReadPermissionAfterUpdate =
|
||||
newObjectPermission.canReadObjectRecords ??
|
||||
existingObjectRecordPermission?.canReadObjectRecords ??
|
||||
roleWithObjectPermissions.canReadAllObjectRecords;
|
||||
flatRole.canReadAllObjectRecords;
|
||||
|
||||
if (hasReadPermissionAfterUpdate === false) {
|
||||
const hasUpdatePermissionAfterUpdate =
|
||||
newObjectPermission.canUpdateObjectRecords ??
|
||||
existingObjectRecordPermission?.canUpdateObjectRecords ??
|
||||
roleWithObjectPermissions.canUpdateAllObjectRecords;
|
||||
flatRole.canUpdateAllObjectRecords;
|
||||
|
||||
const hasSoftDeletePermissionAfterUpdate =
|
||||
newObjectPermission.canSoftDeleteObjectRecords ??
|
||||
existingObjectRecordPermission?.canSoftDeleteObjectRecords ??
|
||||
roleWithObjectPermissions.canSoftDeleteAllObjectRecords;
|
||||
flatRole.canSoftDeleteAllObjectRecords;
|
||||
|
||||
const hasDestroyPermissionAfterUpdate =
|
||||
newObjectPermission.canDestroyObjectRecords ??
|
||||
existingObjectRecordPermission?.canDestroyObjectRecords ??
|
||||
roleWithObjectPermissions.canDestroyAllObjectRecords;
|
||||
flatRole.canDestroyAllObjectRecords;
|
||||
|
||||
if (
|
||||
hasUpdatePermissionAfterUpdate ||
|
||||
@@ -194,91 +317,11 @@ export class ObjectPermissionService {
|
||||
}
|
||||
}
|
||||
|
||||
private async handleForeignKeyError({
|
||||
error,
|
||||
roleId,
|
||||
workspaceId,
|
||||
objectMetadataIds,
|
||||
}: {
|
||||
error: Error;
|
||||
roleId: string;
|
||||
workspaceId: string;
|
||||
objectMetadataIds: string[];
|
||||
}) {
|
||||
if (error.message.includes('violates foreign key constraint')) {
|
||||
const role = await this.roleRepository.findOne({
|
||||
where: {
|
||||
id: roleId,
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!isDefined(role)) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.ROLE_NOT_FOUND,
|
||||
PermissionsExceptionCode.ROLE_NOT_FOUND,
|
||||
{
|
||||
userFriendlyMessage: msg`The role you are trying to modify could not be found. It may have been deleted or you may not have access to it.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const objectMetadata = await this.objectMetadataRepository.find({
|
||||
where: {
|
||||
workspaceId,
|
||||
id: In(objectMetadataIds),
|
||||
},
|
||||
});
|
||||
|
||||
if (objectMetadata.length !== objectMetadataIds.length) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.OBJECT_METADATA_NOT_FOUND,
|
||||
PermissionsExceptionCode.OBJECT_METADATA_NOT_FOUND,
|
||||
{
|
||||
userFriendlyMessage: msg`One or more objects you are trying to set permissions for could not be found. They may have been deleted.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async getRoleOrThrow({
|
||||
roleId,
|
||||
workspaceId,
|
||||
}: {
|
||||
roleId: string;
|
||||
workspaceId: string;
|
||||
}) {
|
||||
const role = await this.roleRepository.findOne({
|
||||
where: {
|
||||
id: roleId,
|
||||
workspaceId,
|
||||
},
|
||||
relations: ['objectPermissions'],
|
||||
});
|
||||
|
||||
if (!isDefined(role)) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.ROLE_NOT_FOUND,
|
||||
PermissionsExceptionCode.ROLE_NOT_FOUND,
|
||||
{
|
||||
userFriendlyMessage: msg`The role you are trying to modify could not be found. It may have been deleted or you may not have access to it.`,
|
||||
},
|
||||
private async getFlatApplicationForWorkspace(workspaceId: string) {
|
||||
const { workspaceCustomFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
);
|
||||
}
|
||||
|
||||
return role;
|
||||
}
|
||||
|
||||
private async validateRoleIsEditableOrThrow({ role }: { role: RoleEntity }) {
|
||||
if (!role.isEditable) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.ROLE_NOT_EDITABLE,
|
||||
PermissionsExceptionCode.ROLE_NOT_EDITABLE,
|
||||
{
|
||||
userFriendlyMessage: msg`This role cannot be modified because it is a system role. Only custom roles can be edited.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
return workspaceCustomFlatApplication;
|
||||
}
|
||||
}
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { type FlatObjectPermission } from 'src/engine/metadata-modules/flat-object-permission/types/flat-object-permission.type';
|
||||
import { type ObjectPermissionDTO } from 'src/engine/metadata-modules/object-permission/dtos/object-permission.dto';
|
||||
|
||||
export const fromFlatObjectPermissionToObjectPermissionDto = (
|
||||
flatObjectPermission: FlatObjectPermission,
|
||||
): ObjectPermissionDTO => ({
|
||||
objectMetadataId: flatObjectPermission.objectMetadataId,
|
||||
canReadObjectRecords: flatObjectPermission.canReadObjectRecords,
|
||||
canUpdateObjectRecords: flatObjectPermission.canUpdateObjectRecords,
|
||||
canSoftDeleteObjectRecords: flatObjectPermission.canSoftDeleteObjectRecords,
|
||||
canDestroyObjectRecords: flatObjectPermission.canDestroyObjectRecords,
|
||||
});
|
||||
Reference in New Issue
Block a user