Files
twenty/packages/twenty-server/src/engine/metadata-modules/object-permission/object-permission.service.ts
T
Weiko 1eb2e44058 Refactor workspace cache service (#16208)
## Context
We've recently introduced a new workspace cache service which now acts
as a cache access and local storage for all workspace related data,
deprecating the individual specific services.
- Better performance through multiple caching/fetching strategies
- Consistent data access patterns across the codebase
- Reduced redis queries through MGET/MSET/PIPELINE with multiple cache
keys
2025-12-01 17:08:21 +01:00

281 lines
9.3 KiB
TypeScript

import { InjectRepository } from '@nestjs/typeorm';
import { msg } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { In, Repository } from 'typeorm';
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 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';
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 workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
) {}
public async upsertObjectPermissions({
workspaceId,
input,
}: {
workspaceId: string;
input: UpsertObjectPermissionsInput;
}): Promise<ObjectPermissionEntity[]> {
try {
const role = await this.getRoleOrThrow({
roleId: input.roleId,
workspaceId,
});
await this.validateRoleIsEditableOrThrow({
role,
});
await this.validateObjectPermissionsReadAndWriteConsistencyOrThrow({
objectPermissions: input.objectPermissions,
roleWithObjectPermissions: role,
});
const { flatObjectMetadataMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatObjectMetadataMaps'],
},
);
input.objectPermissions.forEach((objectPermission) => {
const objectMetadataForObjectPermission =
flatObjectMetadataMaps.byId[objectPermission.objectMetadataId];
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;
}
}
private async validateObjectPermissionsReadAndWriteConsistencyOrThrow({
objectPermissions: newObjectPermissions,
roleWithObjectPermissions,
}: {
objectPermissions: ObjectPermissionInput[];
roleWithObjectPermissions: RoleEntity;
}) {
const existingObjectPermissions =
roleWithObjectPermissions.objectPermissions;
for (const newObjectPermission of newObjectPermissions) {
const existingObjectRecordPermission = existingObjectPermissions.find(
(objectPermission) =>
objectPermission.objectMetadataId ===
newObjectPermission.objectMetadataId,
);
const hasReadPermissionAfterUpdate =
newObjectPermission.canReadObjectRecords ??
existingObjectRecordPermission?.canReadObjectRecords ??
roleWithObjectPermissions.canReadAllObjectRecords;
if (hasReadPermissionAfterUpdate === false) {
const hasUpdatePermissionAfterUpdate =
newObjectPermission.canUpdateObjectRecords ??
existingObjectRecordPermission?.canUpdateObjectRecords ??
roleWithObjectPermissions.canUpdateAllObjectRecords;
const hasSoftDeletePermissionAfterUpdate =
newObjectPermission.canSoftDeleteObjectRecords ??
existingObjectRecordPermission?.canSoftDeleteObjectRecords ??
roleWithObjectPermissions.canSoftDeleteAllObjectRecords;
const hasDestroyPermissionAfterUpdate =
newObjectPermission.canDestroyObjectRecords ??
existingObjectRecordPermission?.canDestroyObjectRecords ??
roleWithObjectPermissions.canDestroyAllObjectRecords;
if (
hasUpdatePermissionAfterUpdate ||
hasSoftDeletePermissionAfterUpdate ||
hasDestroyPermissionAfterUpdate
) {
throw new PermissionsException(
PermissionsExceptionMessage.CANNOT_GIVE_WRITING_PERMISSION_ON_NON_READABLE_OBJECT,
PermissionsExceptionCode.CANNOT_GIVE_WRITING_PERMISSION_ON_NON_READABLE_OBJECT,
{
userFriendlyMessage: msg`You cannot grant edit permissions without also granting read permissions. Please enable read access first.`,
},
);
}
}
}
}
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.`,
},
);
}
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.`,
},
);
}
}
}