Handle restricted objects #1 refactor permissions map + return object permissions from gql (#12313)

## Context
- Introduced objectPermissions in currentUserWorkspace which uses role
permissions from cache so we can fetch granular permissions from the API
- Refactored cached role permissions to map permissions with object
metadata id instead of object metadata name singular to be more flexible

New Cache
<img width="574" alt="Screenshot 2025-05-27 at 11 59 06"
src="https://github.com/user-attachments/assets/1a090134-1b8a-4681-a630-29f1472178bd"
/>

GQL
<img width="977" alt="Screenshot 2025-05-27 at 11 58 53"
src="https://github.com/user-attachments/assets/3b9a82b0-6019-4a25-a6e2-a9e0fb4bb8a0"
/>


Next steps: Use the updated API in the FE to fetch granular permissions
and update useHasObjectReadOnlyPermission hook
This commit is contained in:
Weiko
2025-05-27 17:42:26 +02:00
committed by GitHub
parent 651ad38e79
commit 8051646567
14 changed files with 143 additions and 93 deletions

View File

@ -2,12 +2,6 @@ import { Field, ObjectType } from '@nestjs/graphql';
@ObjectType('ObjectPermission')
export class ObjectPermissionDTO {
@Field({ nullable: false })
id: string;
@Field({ nullable: false })
roleId: string;
@Field({ nullable: false })
objectMetadataId: string;

View File

@ -1,6 +1,7 @@
import { Injectable } from '@nestjs/common';
import { PermissionsOnAllObjectRecords } from 'twenty-shared/constants';
import { ObjectRecordsPermissions } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import {
@ -26,6 +27,64 @@ export class PermissionsService {
private readonly featureFlagService: FeatureFlagService,
) {}
public async getUserWorkspacePermissionsV2({
userWorkspaceId,
workspaceId,
}: {
userWorkspaceId: string;
workspaceId: string;
}): Promise<{
settingsPermissions: Record<SettingPermissionType, boolean>;
objectRecordsPermissions: ObjectRecordsPermissions;
}> {
const [roleOfUserWorkspace] = await this.userRoleService
.getRolesByUserWorkspaces({
userWorkspaceIds: [userWorkspaceId],
workspaceId,
})
.then((roles) => roles?.get(userWorkspaceId) ?? []);
let hasPermissionOnSettingFeature = false;
if (!isDefined(roleOfUserWorkspace)) {
throw new PermissionsException(
PermissionsExceptionMessage.NO_ROLE_FOUND_FOR_USER_WORKSPACE,
PermissionsExceptionCode.NO_ROLE_FOUND_FOR_USER_WORKSPACE,
);
}
if (roleOfUserWorkspace.canUpdateAllSettings === true) {
hasPermissionOnSettingFeature = true;
}
const settingPermissions = roleOfUserWorkspace.settingPermissions ?? [];
const settingsPermissionsMap = Object.keys(SettingPermissionType).reduce(
(acc, feature) => ({
...acc,
[feature]:
hasPermissionOnSettingFeature ||
settingPermissions.some(
(settingPermission) => settingPermission.setting === feature,
),
}),
{} as Record<SettingPermissionType, boolean>,
);
const { data: rolesPermissions } =
await this.workspacePermissionsCacheService.getRolesPermissionsFromCache({
workspaceId,
});
const objectRecordsPermissions =
rolesPermissions[roleOfUserWorkspace.id] ?? {};
return {
settingsPermissions: settingsPermissionsMap,
objectRecordsPermissions: objectRecordsPermissions,
};
}
public async getUserWorkspacePermissions({
userWorkspaceId,
workspaceId,

View File

@ -247,17 +247,15 @@ export class WorkspacePermissionsCacheService {
relations: ['objectPermissions'],
});
const workspaceObjectMetadataNameIdMap =
await this.getWorkspaceObjectMetadataNameIdMap(workspaceId);
const workspaceObjectMetadataIds =
await this.getWorkspaceObjectMetadataIds(workspaceId);
const permissionsByRoleId: ObjectRecordsPermissionsByRoleId = {};
for (const role of roles) {
const objectRecordsPermissions: ObjectRecordsPermissions = {};
for (const objectMetadataNameSingular of Object.keys(
workspaceObjectMetadataNameIdMap,
)) {
for (const objectMetadataId of workspaceObjectMetadataIds) {
let canRead = role.canReadAllObjectRecords;
let canUpdate = role.canUpdateAllObjectRecords;
let canSoftDelete = role.canSoftDeleteAllObjectRecords;
@ -266,8 +264,7 @@ export class WorkspacePermissionsCacheService {
if (isPermissionsV2Enabled) {
const objectRecordPermissionsOverride = role.objectPermissions.find(
(objectPermission) =>
objectPermission.objectMetadataId ===
workspaceObjectMetadataNameIdMap[objectMetadataNameSingular],
objectPermission.objectMetadataId === objectMetadataId,
);
canRead =
@ -283,7 +280,7 @@ export class WorkspacePermissionsCacheService {
canDestroy;
}
objectRecordsPermissions[objectMetadataNameSingular] = {
objectRecordsPermissions[objectMetadataId] = {
canRead,
canUpdate,
canSoftDelete,
@ -297,53 +294,17 @@ export class WorkspacePermissionsCacheService {
return permissionsByRoleId;
}
private async getWorkspaceObjectMetadataNameIdMap(
private async getWorkspaceObjectMetadataIds(
workspaceId: string,
): Promise<Record<string, string>> {
let workspaceObjectMetadataMap: Record<string, string> = {};
const metadataVersion =
await this.workspaceCacheStorageService.getMetadataVersion(workspaceId);
): Promise<string[]> {
const workspaceObjectMetadata = await this.objectMetadataRepository.find({
where: {
workspaceId,
},
select: ['id'],
});
if (metadataVersion) {
const objectMetadataMaps =
await this.workspaceCacheStorageService.getObjectMetadataMaps(
workspaceId,
metadataVersion,
);
workspaceObjectMetadataMap = Object.values(
objectMetadataMaps?.byId ?? {},
).reduce(
(acc, objectMetadata) => {
acc[objectMetadata.nameSingular] = objectMetadata.id;
return acc;
},
{} as Record<string, string>,
);
}
if (
!metadataVersion ||
Object.keys(workspaceObjectMetadataMap).length === 0
) {
const workspaceObjectMetadata = await this.objectMetadataRepository.find({
where: {
workspaceId,
},
});
workspaceObjectMetadataMap = workspaceObjectMetadata.reduce(
(acc, objectMetadata) => {
acc[objectMetadata.nameSingular] = objectMetadata.id;
return acc;
},
{} as Record<string, string>,
);
}
return workspaceObjectMetadataMap;
return workspaceObjectMetadata.map((objectMetadata) => objectMetadata.id);
}
private async getUserWorkspaceRoleMapFromDatabase({