[permissions] Improve performances using a cache for userWorkspaces roles (#11587)
In this PR we are
- introducing a cached map `{ userworkspaceId: roleId } `to reduce calls
to get a userWorkspace's role (we were having N+1 around that with
combinedFindMany queries and generally having a lot of avoidable
queries)
- using the roles permissions cache (`{ roleId: { objectNameSingular:
{ canRead: bool, canUpdate: bool, ...} } `) in Permissions V1's
userHasObjectPermission, in order to 1) improve performances to avoid
calls to get roles 2) start using our permissions cache
This commit is contained in:
@ -5,7 +5,7 @@ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadat
|
||||
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 { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { WorkspaceRolesPermissionsCacheModule } from 'src/engine/metadata-modules/workspace-roles-permissions-cache/workspace-roles-permissions-cache.module';
|
||||
import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@ -13,7 +13,7 @@ import { WorkspaceRolesPermissionsCacheModule } from 'src/engine/metadata-module
|
||||
[ObjectPermissionEntity, RoleEntity, ObjectMetadataEntity],
|
||||
'metadata',
|
||||
),
|
||||
WorkspaceRolesPermissionsCacheModule,
|
||||
WorkspacePermissionsCacheModule,
|
||||
],
|
||||
providers: [ObjectPermissionService],
|
||||
exports: [ObjectPermissionService],
|
||||
|
||||
@ -12,7 +12,7 @@ import {
|
||||
PermissionsExceptionMessage,
|
||||
} from 'src/engine/metadata-modules/permissions/permissions.exception';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { WorkspaceRolesPermissionsCacheService } from 'src/engine/metadata-modules/workspace-roles-permissions-cache/workspace-roles-permissions-cache.service';
|
||||
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
|
||||
|
||||
export class ObjectPermissionService {
|
||||
constructor(
|
||||
@ -22,7 +22,7 @@ export class ObjectPermissionService {
|
||||
private readonly roleRepository: Repository<RoleEntity>,
|
||||
@InjectRepository(ObjectMetadataEntity, 'metadata')
|
||||
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
||||
private readonly workspaceRolesPermissionsCacheService: WorkspaceRolesPermissionsCacheService,
|
||||
private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService,
|
||||
) {}
|
||||
|
||||
public async upsertObjectPermission({
|
||||
@ -54,9 +54,10 @@ export class ObjectPermissionService {
|
||||
throw new Error('Failed to upsert object permission');
|
||||
}
|
||||
|
||||
await this.workspaceRolesPermissionsCacheService.recomputeRolesPermissionsCache(
|
||||
await this.workspacePermissionsCacheService.recomputeRolesPermissionsCache(
|
||||
{
|
||||
workspaceId,
|
||||
roleIds: [input.roleId],
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@ import { PermissionsService } from 'src/engine/metadata-modules/permissions/perm
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { UserWorkspaceRoleEntity } from 'src/engine/metadata-modules/role/user-workspace-role.entity';
|
||||
import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module';
|
||||
import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@ -14,6 +15,7 @@ import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.
|
||||
FeatureFlagModule,
|
||||
TypeOrmModule.forFeature([UserWorkspace], 'core'),
|
||||
UserRoleModule,
|
||||
WorkspacePermissionsCacheModule,
|
||||
],
|
||||
providers: [PermissionsService],
|
||||
exports: [PermissionsService],
|
||||
|
||||
@ -7,18 +7,24 @@ import {
|
||||
AuthException,
|
||||
AuthExceptionCode,
|
||||
} from 'src/engine/core-modules/auth/auth.exception';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants';
|
||||
import {
|
||||
PermissionsException,
|
||||
PermissionsExceptionCode,
|
||||
PermissionsExceptionMessage,
|
||||
} from 'src/engine/metadata-modules/permissions/permissions.exception';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
|
||||
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
|
||||
|
||||
@Injectable()
|
||||
export class PermissionsService {
|
||||
constructor(private readonly userRoleService: UserRoleService) {}
|
||||
constructor(
|
||||
private readonly userRoleService: UserRoleService,
|
||||
private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
) {}
|
||||
|
||||
public async getUserWorkspacePermissions({
|
||||
userWorkspaceId,
|
||||
@ -124,11 +130,21 @@ export class PermissionsService {
|
||||
return true;
|
||||
}
|
||||
|
||||
const settingPermissions = roleOfUserWorkspace.settingPermissions ?? [];
|
||||
const isPermissionsV2Enabled =
|
||||
await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IsPermissionsV2Enabled,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
return settingPermissions.some(
|
||||
(settingPermission) => settingPermission.setting === setting,
|
||||
);
|
||||
if (isPermissionsV2Enabled) {
|
||||
const settingPermissions = roleOfUserWorkspace.settingPermissions ?? [];
|
||||
|
||||
return settingPermissions.some(
|
||||
(settingPermission) => settingPermission.setting === setting,
|
||||
);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async userHasObjectRecordsPermission({
|
||||
@ -142,6 +158,18 @@ export class PermissionsService {
|
||||
requiredPermission: PermissionsOnAllObjectRecords;
|
||||
isExecutedByApiKey: boolean;
|
||||
}): Promise<boolean> {
|
||||
const isPermissionsV2Enabled =
|
||||
await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IsPermissionsV2Enabled,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (isPermissionsV2Enabled) {
|
||||
throw new Error(
|
||||
'This should not be called once Permissions V2 is enabled',
|
||||
);
|
||||
}
|
||||
|
||||
if (isExecutedByApiKey) {
|
||||
return true;
|
||||
}
|
||||
@ -153,31 +181,51 @@ export class PermissionsService {
|
||||
);
|
||||
}
|
||||
|
||||
const [roleOfUserWorkspace] = await this.userRoleService
|
||||
.getRolesByUserWorkspaces({
|
||||
userWorkspaceIds: [userWorkspaceId],
|
||||
const roleIdOfUserWorkspace =
|
||||
await this.userRoleService.getRoleIdForUserWorkspace({
|
||||
userWorkspaceId,
|
||||
workspaceId,
|
||||
})
|
||||
.then((roles) => roles?.get(userWorkspaceId) ?? []);
|
||||
});
|
||||
|
||||
const roleColumn =
|
||||
this.getRoleColumnForRequiredPermission(requiredPermission);
|
||||
if (!isDefined(roleIdOfUserWorkspace)) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.NO_ROLE_FOUND_FOR_USER_WORKSPACE,
|
||||
PermissionsExceptionCode.NO_ROLE_FOUND_FOR_USER_WORKSPACE,
|
||||
);
|
||||
}
|
||||
|
||||
return roleOfUserWorkspace?.[roleColumn] === true;
|
||||
const { data: rolesPermissions } =
|
||||
await this.workspacePermissionsCacheService.getRolesPermissionsFromCache({
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
const rolePermissionsForUserWorkspaceRole =
|
||||
rolesPermissions[roleIdOfUserWorkspace];
|
||||
|
||||
const objectPermissionKey =
|
||||
this.getObjectPermissionKeyForRequiredPermission(requiredPermission);
|
||||
|
||||
// until permissions V2 is enabled all objects have the same permission values deriving from role, ex role.canReadAllObjectRecords
|
||||
const objectPermissionValue =
|
||||
rolePermissionsForUserWorkspaceRole[
|
||||
Object.keys(rolePermissionsForUserWorkspaceRole)[0]
|
||||
]?.[objectPermissionKey];
|
||||
|
||||
return objectPermissionValue === true;
|
||||
}
|
||||
|
||||
private getRoleColumnForRequiredPermission(
|
||||
private getObjectPermissionKeyForRequiredPermission(
|
||||
requiredPermission: PermissionsOnAllObjectRecords,
|
||||
): keyof RoleEntity {
|
||||
) {
|
||||
switch (requiredPermission) {
|
||||
case PermissionsOnAllObjectRecords.READ_ALL_OBJECT_RECORDS:
|
||||
return 'canReadAllObjectRecords';
|
||||
return 'canRead';
|
||||
case PermissionsOnAllObjectRecords.UPDATE_ALL_OBJECT_RECORDS:
|
||||
return 'canUpdateAllObjectRecords';
|
||||
return 'canUpdate';
|
||||
case PermissionsOnAllObjectRecords.SOFT_DELETE_ALL_OBJECT_RECORDS:
|
||||
return 'canSoftDeleteAllObjectRecords';
|
||||
return 'canSoftDelete';
|
||||
case PermissionsOnAllObjectRecords.DESTROY_ALL_OBJECT_RECORDS:
|
||||
return 'canDestroyAllObjectRecords';
|
||||
return 'canDestroy';
|
||||
default:
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.UNKNOWN_REQUIRED_PERMISSION,
|
||||
|
||||
@ -13,7 +13,7 @@ import { RoleService } from 'src/engine/metadata-modules/role/role.service';
|
||||
import { UserWorkspaceRoleEntity } from 'src/engine/metadata-modules/role/user-workspace-role.entity';
|
||||
import { SettingPermissionModule } from 'src/engine/metadata-modules/setting-permission/setting-permission.module';
|
||||
import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module';
|
||||
import { WorkspaceRolesPermissionsCacheModule } from 'src/engine/metadata-modules/workspace-roles-permissions-cache/workspace-roles-permissions-cache.module';
|
||||
import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@ -25,7 +25,7 @@ import { WorkspaceRolesPermissionsCacheModule } from 'src/engine/metadata-module
|
||||
FeatureFlagModule,
|
||||
ObjectPermissionModule,
|
||||
SettingPermissionModule,
|
||||
WorkspaceRolesPermissionsCacheModule,
|
||||
WorkspacePermissionsCacheModule,
|
||||
],
|
||||
providers: [RoleService, RoleResolver],
|
||||
exports: [RoleService],
|
||||
|
||||
@ -20,7 +20,7 @@ import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { UserWorkspaceRoleEntity } from 'src/engine/metadata-modules/role/user-workspace-role.entity';
|
||||
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
|
||||
import { isArgDefinedIfProvidedOrThrow } from 'src/engine/metadata-modules/utils/is-arg-defined-if-provided-or-throw.util';
|
||||
import { WorkspaceRolesPermissionsCacheService } from 'src/engine/metadata-modules/workspace-roles-permissions-cache/workspace-roles-permissions-cache.service';
|
||||
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
|
||||
|
||||
export class RoleService {
|
||||
constructor(
|
||||
@ -31,7 +31,7 @@ export class RoleService {
|
||||
@InjectRepository(UserWorkspaceRoleEntity, 'metadata')
|
||||
private readonly userWorkspaceRoleRepository: Repository<UserWorkspaceRoleEntity>,
|
||||
private readonly userRoleService: UserRoleService,
|
||||
private readonly workspaceRolesPermissionsCacheService: WorkspaceRolesPermissionsCacheService,
|
||||
private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService,
|
||||
) {}
|
||||
|
||||
public async getWorkspaceRoles(workspaceId: string): Promise<RoleEntity[]> {
|
||||
@ -69,7 +69,7 @@ export class RoleService {
|
||||
}): Promise<RoleEntity> {
|
||||
await this.validateRoleInput({ input, workspaceId });
|
||||
|
||||
const role = this.roleRepository.save({
|
||||
const role = await this.roleRepository.save({
|
||||
label: input.label,
|
||||
description: input.description,
|
||||
icon: input.icon,
|
||||
@ -82,11 +82,10 @@ export class RoleService {
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
await this.workspaceRolesPermissionsCacheService.recomputeRolesPermissionsCache(
|
||||
{
|
||||
workspaceId,
|
||||
},
|
||||
);
|
||||
await this.workspacePermissionsCacheService.recomputeRolesPermissionsCache({
|
||||
workspaceId,
|
||||
roleIds: [role.id],
|
||||
});
|
||||
|
||||
return role;
|
||||
}
|
||||
@ -128,11 +127,10 @@ export class RoleService {
|
||||
...input.update,
|
||||
});
|
||||
|
||||
await this.workspaceRolesPermissionsCacheService.recomputeRolesPermissionsCache(
|
||||
{
|
||||
workspaceId,
|
||||
},
|
||||
);
|
||||
await this.workspacePermissionsCacheService.recomputeRolesPermissionsCache({
|
||||
workspaceId,
|
||||
roleIds: [input.id],
|
||||
});
|
||||
|
||||
return { ...existingRole, ...updatedRole };
|
||||
}
|
||||
@ -196,11 +194,9 @@ export class RoleService {
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
await this.workspaceRolesPermissionsCacheService.recomputeRolesPermissionsCache(
|
||||
{
|
||||
workspaceId,
|
||||
},
|
||||
);
|
||||
await this.workspacePermissionsCacheService.recomputeRolesPermissionsCache({
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
return roleId;
|
||||
}
|
||||
@ -317,10 +313,11 @@ export class RoleService {
|
||||
workspaceId: string;
|
||||
defaultRoleId: string;
|
||||
}): Promise<void> {
|
||||
const userWorkspaceIds = await this.getUserWorkspaceIdsForRole(
|
||||
roleId,
|
||||
workspaceId,
|
||||
);
|
||||
const userWorkspaceIds =
|
||||
await this.userRoleService.getUserWorkspaceIdsAssignedToRole(
|
||||
roleId,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
userWorkspaceIds.map((userWorkspaceId) =>
|
||||
@ -333,24 +330,6 @@ export class RoleService {
|
||||
);
|
||||
}
|
||||
|
||||
private async getUserWorkspaceIdsForRole(
|
||||
roleId: string,
|
||||
workspaceId: string,
|
||||
): Promise<string[]> {
|
||||
return this.userWorkspaceRoleRepository
|
||||
.find({
|
||||
where: {
|
||||
roleId: roleId,
|
||||
workspaceId,
|
||||
},
|
||||
})
|
||||
.then((userWorkspaceRoles) =>
|
||||
userWorkspaceRoles.map(
|
||||
(userWorkspaceRole) => userWorkspaceRole.userWorkspaceId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private async getRole(
|
||||
roleId: string,
|
||||
workspaceId: string,
|
||||
|
||||
@ -5,11 +5,13 @@ import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-works
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { UserWorkspaceRoleEntity } from 'src/engine/metadata-modules/role/user-workspace-role.entity';
|
||||
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
|
||||
import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([RoleEntity, UserWorkspaceRoleEntity], 'metadata'),
|
||||
TypeOrmModule.forFeature([UserWorkspace], 'core'),
|
||||
WorkspacePermissionsCacheModule,
|
||||
],
|
||||
providers: [UserRoleService],
|
||||
exports: [UserRoleService],
|
||||
|
||||
@ -12,6 +12,7 @@ import {
|
||||
} from 'src/engine/metadata-modules/permissions/permissions.exception';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { UserWorkspaceRoleEntity } from 'src/engine/metadata-modules/role/user-workspace-role.entity';
|
||||
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
|
||||
@ -24,6 +25,7 @@ export class UserRoleService {
|
||||
@InjectRepository(UserWorkspace, 'core')
|
||||
private readonly userWorkspaceRepository: Repository<UserWorkspace>,
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService,
|
||||
) {}
|
||||
|
||||
public async assignRoleToUserWorkspace({
|
||||
@ -56,6 +58,12 @@ export class UserRoleService {
|
||||
workspaceId,
|
||||
id: Not(newUserWorkspaceRole.id),
|
||||
});
|
||||
|
||||
await this.workspacePermissionsCacheService.recomputeUserWorkspaceRoleMapCache(
|
||||
{
|
||||
workspaceId,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public async getRoleIdForUserWorkspace({
|
||||
@ -69,11 +77,14 @@ export class UserRoleService {
|
||||
return;
|
||||
}
|
||||
|
||||
const userWorkspaceRole = await this.userWorkspaceRoleRepository.findOne({
|
||||
where: { userWorkspaceId, workspaceId },
|
||||
});
|
||||
const userWorkspaceRoleMap =
|
||||
await this.workspacePermissionsCacheService.getUserWorkspaceRoleMapFromCache(
|
||||
{
|
||||
workspaceId,
|
||||
},
|
||||
);
|
||||
|
||||
return userWorkspaceRole?.roleId;
|
||||
return userWorkspaceRoleMap.data[userWorkspaceId];
|
||||
}
|
||||
|
||||
public async getRolesByUserWorkspaces({
|
||||
@ -125,21 +136,13 @@ export class UserRoleService {
|
||||
roleId: string,
|
||||
workspaceId: string,
|
||||
): Promise<WorkspaceMemberWorkspaceEntity[]> {
|
||||
const userWorkspaceRoles = await this.userWorkspaceRoleRepository.find({
|
||||
where: {
|
||||
roleId,
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
const userWorkspaceIdsWithRole =
|
||||
await this.getUserWorkspaceIdsAssignedToRole(roleId, workspaceId);
|
||||
|
||||
const userIds = await this.userWorkspaceRepository
|
||||
.find({
|
||||
where: {
|
||||
id: In(
|
||||
userWorkspaceRoles.map(
|
||||
(userWorkspaceRole) => userWorkspaceRole.userWorkspaceId,
|
||||
),
|
||||
),
|
||||
id: In(userWorkspaceIdsWithRole),
|
||||
},
|
||||
})
|
||||
.then((userWorkspaces) =>
|
||||
@ -161,6 +164,22 @@ export class UserRoleService {
|
||||
return workspaceMembers;
|
||||
}
|
||||
|
||||
public async getUserWorkspaceIdsAssignedToRole(
|
||||
roleId: string,
|
||||
workspaceId: string,
|
||||
): Promise<string[]> {
|
||||
const userWorkspaceRoleMap =
|
||||
await this.workspacePermissionsCacheService.getUserWorkspaceRoleMapFromCache(
|
||||
{
|
||||
workspaceId,
|
||||
},
|
||||
);
|
||||
|
||||
return Object.entries(userWorkspaceRoleMap.data)
|
||||
.filter(([_, roleIdFromMap]) => roleIdFromMap === roleId)
|
||||
.map(([userWorkspaceId]) => userWorkspaceId);
|
||||
}
|
||||
|
||||
public async validateUserWorkspaceIsNotUniqueAdminOrThrow({
|
||||
userWorkspaceId,
|
||||
workspaceId,
|
||||
|
||||
@ -10,6 +10,8 @@ import { TwentyORMExceptionCode } from 'src/engine/twenty-orm/exceptions/twenty-
|
||||
import { getFromCacheWithRecompute } from 'src/engine/utils/get-data-from-cache-with-recompute.util';
|
||||
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
||||
|
||||
const FEATURE_FLAG_MAP = 'Feature flag map';
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceFeatureFlagsMapCacheService {
|
||||
logger = new Logger(WorkspaceFeatureFlagsMapCacheService.name);
|
||||
@ -45,7 +47,7 @@ export class WorkspaceFeatureFlagsMapCacheService {
|
||||
workspaceId,
|
||||
),
|
||||
recomputeCache: (params) => this.recomputeFeatureFlagsMapCache(params),
|
||||
cachedEntityName: 'Feature flag map',
|
||||
cachedEntityName: FEATURE_FLAG_MAP,
|
||||
exceptionCode: TwentyORMExceptionCode.FEATURE_FLAG_MAP_VERSION_NOT_FOUND,
|
||||
});
|
||||
}
|
||||
|
||||
@ -0,0 +1 @@
|
||||
export type UserWorkspaceRoleMap = Record<string, string>;
|
||||
@ -0,0 +1,156 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { ObjectRecordsPermissionsByRoleId } from 'twenty-shared/types';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator';
|
||||
import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service';
|
||||
import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum';
|
||||
import { UserWorkspaceRoleMap } from 'src/engine/metadata-modules/workspace-permissions-cache/types/user-workspace-role-map.type';
|
||||
import { WorkspaceCacheKeys } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
||||
|
||||
const TTL_INFINITE = 0;
|
||||
|
||||
@Injectable()
|
||||
export class WorkspacePermissionsCacheStorageService {
|
||||
logger = new Logger(WorkspacePermissionsCacheStorageService.name);
|
||||
|
||||
constructor(
|
||||
@InjectCacheStorage(CacheStorageNamespace.EngineWorkspace)
|
||||
private readonly cacheStorageService: CacheStorageService,
|
||||
) {}
|
||||
|
||||
async setRolesPermissionsVersion(workspaceId: string): Promise<string> {
|
||||
const rolesPermissionsVersion = v4();
|
||||
|
||||
await this.cacheStorageService.set<string>(
|
||||
`${WorkspaceCacheKeys.MetadataPermissionsRolesPermissionsVersion}:${workspaceId}`,
|
||||
rolesPermissionsVersion,
|
||||
TTL_INFINITE,
|
||||
);
|
||||
|
||||
return rolesPermissionsVersion;
|
||||
}
|
||||
|
||||
async setRolesPermissions(
|
||||
workspaceId: string,
|
||||
permissions: ObjectRecordsPermissionsByRoleId,
|
||||
): Promise<{
|
||||
newRolesPermissionsVersion: string;
|
||||
}> {
|
||||
const [, newRolesPermissionsVersion] = await Promise.all([
|
||||
this.cacheStorageService.set<ObjectRecordsPermissionsByRoleId>(
|
||||
`${WorkspaceCacheKeys.MetadataPermissionsRolesPermissions}:${workspaceId}`,
|
||||
permissions,
|
||||
TTL_INFINITE,
|
||||
),
|
||||
this.setRolesPermissionsVersion(workspaceId),
|
||||
]);
|
||||
|
||||
return { newRolesPermissionsVersion };
|
||||
}
|
||||
|
||||
getRolesPermissions(
|
||||
workspaceId: string,
|
||||
): Promise<ObjectRecordsPermissionsByRoleId | undefined> {
|
||||
return this.cacheStorageService.get<ObjectRecordsPermissionsByRoleId>(
|
||||
`${WorkspaceCacheKeys.MetadataPermissionsRolesPermissions}:${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
getRolesPermissionsVersion(workspaceId: string): Promise<string | undefined> {
|
||||
return this.cacheStorageService.get<string>(
|
||||
`${WorkspaceCacheKeys.MetadataPermissionsRolesPermissionsVersion}:${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
addRolesPermissionsOngoingCachingLock(workspaceId: string) {
|
||||
return this.cacheStorageService.set<boolean>(
|
||||
`${WorkspaceCacheKeys.MetadataPermissionsRolesPermissionsOngoingCachingLock}:${workspaceId}`,
|
||||
true,
|
||||
1_000 * 60, // 1 minute
|
||||
);
|
||||
}
|
||||
|
||||
removeRolesPermissionsOngoingCachingLock(workspaceId: string) {
|
||||
return this.cacheStorageService.del(
|
||||
`${WorkspaceCacheKeys.MetadataPermissionsRolesPermissionsOngoingCachingLock}:${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
getRolesPermissionsOngoingCachingLock(
|
||||
workspaceId: string,
|
||||
): Promise<boolean | undefined> {
|
||||
return this.cacheStorageService.get<boolean>(
|
||||
`${WorkspaceCacheKeys.MetadataPermissionsRolesPermissionsOngoingCachingLock}:${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
async setUserWorkspaceRoleMap(
|
||||
workspaceId: string,
|
||||
userWorkspaceRoleMap: UserWorkspaceRoleMap,
|
||||
): Promise<{
|
||||
newUserWorkspaceRoleMapVersion: string;
|
||||
}> {
|
||||
const [, newUserWorkspaceRoleMapVersion] = await Promise.all([
|
||||
this.cacheStorageService.set<UserWorkspaceRoleMap>(
|
||||
`${WorkspaceCacheKeys.MetadataPermissionsUserWorkspaceRoleMap}:${workspaceId}`,
|
||||
userWorkspaceRoleMap,
|
||||
TTL_INFINITE,
|
||||
),
|
||||
this.setUserWorkspaceRoleMapVersion(workspaceId),
|
||||
]);
|
||||
|
||||
return { newUserWorkspaceRoleMapVersion };
|
||||
}
|
||||
|
||||
async setUserWorkspaceRoleMapVersion(workspaceId: string) {
|
||||
const userWorkspaceRoleMapVersion = v4();
|
||||
|
||||
await this.cacheStorageService.set<string>(
|
||||
`${WorkspaceCacheKeys.MetadataPermissionsUserWorkspaceRoleMapVersion}:${workspaceId}`,
|
||||
userWorkspaceRoleMapVersion,
|
||||
TTL_INFINITE,
|
||||
);
|
||||
|
||||
return userWorkspaceRoleMapVersion;
|
||||
}
|
||||
|
||||
getUserWorkspaceRoleMap(
|
||||
workspaceId: string,
|
||||
): Promise<Record<string, string> | undefined> {
|
||||
return this.cacheStorageService.get<Record<string, string>>(
|
||||
`${WorkspaceCacheKeys.MetadataPermissionsUserWorkspaceRoleMap}:${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
getUserWorkspaceRoleMapVersion(
|
||||
workspaceId: string,
|
||||
): Promise<string | undefined> {
|
||||
return this.cacheStorageService.get<string>(
|
||||
`${WorkspaceCacheKeys.MetadataPermissionsUserWorkspaceRoleMapVersion}:${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
addUserWorkspaceRoleMapOngoingCachingLock(workspaceId: string) {
|
||||
return this.cacheStorageService.set<boolean>(
|
||||
`${WorkspaceCacheKeys.MetadataPermissionsUserWorkspaceRoleMapOngoingCachingLock}:${workspaceId}`,
|
||||
true,
|
||||
1_000 * 60, // 1 minute
|
||||
);
|
||||
}
|
||||
|
||||
removeUserWorkspaceRoleMapOngoingCachingLock(workspaceId: string) {
|
||||
return this.cacheStorageService.del(
|
||||
`${WorkspaceCacheKeys.MetadataPermissionsUserWorkspaceRoleMapOngoingCachingLock}:${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
getUserWorkspaceRoleMapOngoingCachingLock(
|
||||
workspaceId: string,
|
||||
): Promise<boolean | undefined> {
|
||||
return this.cacheStorageService.get<boolean>(
|
||||
`${WorkspaceCacheKeys.MetadataPermissionsUserWorkspaceRoleMapOngoingCachingLock}:${workspaceId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { UserWorkspaceRoleEntity } from 'src/engine/metadata-modules/role/user-workspace-role.entity';
|
||||
import { WorkspacePermissionsCacheStorageService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache-storage.service';
|
||||
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
|
||||
|
||||
import { WorkspacePermissionsCacheService } from './workspace-permissions-cache.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Workspace], 'core'),
|
||||
TypeOrmModule.forFeature(
|
||||
[ObjectMetadataEntity, RoleEntity, UserWorkspaceRoleEntity],
|
||||
'metadata',
|
||||
),
|
||||
WorkspaceCacheStorageModule,
|
||||
FeatureFlagModule,
|
||||
],
|
||||
providers: [
|
||||
WorkspacePermissionsCacheService,
|
||||
WorkspacePermissionsCacheStorageService,
|
||||
],
|
||||
exports: [
|
||||
WorkspacePermissionsCacheService,
|
||||
WorkspacePermissionsCacheStorageService,
|
||||
],
|
||||
})
|
||||
export class WorkspacePermissionsCacheModule {}
|
||||
@ -0,0 +1,318 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import {
|
||||
ObjectRecordsPermissions,
|
||||
ObjectRecordsPermissionsByRoleId,
|
||||
} from 'twenty-shared/types';
|
||||
import { In, Repository } from 'typeorm';
|
||||
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { UserWorkspaceRoleEntity } from 'src/engine/metadata-modules/role/user-workspace-role.entity';
|
||||
import { UserWorkspaceRoleMap } from 'src/engine/metadata-modules/workspace-permissions-cache/types/user-workspace-role-map.type';
|
||||
import { WorkspacePermissionsCacheStorageService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache-storage.service';
|
||||
import { TwentyORMExceptionCode } from 'src/engine/twenty-orm/exceptions/twenty-orm.exception';
|
||||
import { getFromCacheWithRecompute } from 'src/engine/utils/get-data-from-cache-with-recompute.util';
|
||||
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
||||
|
||||
type CacheResult<T, U> = {
|
||||
version: T;
|
||||
data: U;
|
||||
};
|
||||
|
||||
const USER_WORKSPACE_ROLE_MAP = 'User workspace role map';
|
||||
const ROLES_PERMISSIONS = 'Roles permissions';
|
||||
|
||||
@Injectable()
|
||||
export class WorkspacePermissionsCacheService {
|
||||
logger = new Logger(WorkspacePermissionsCacheService.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(ObjectMetadataEntity, 'metadata')
|
||||
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
||||
@InjectRepository(RoleEntity, 'metadata')
|
||||
private readonly roleRepository: Repository<RoleEntity>,
|
||||
@InjectRepository(UserWorkspaceRoleEntity, 'metadata')
|
||||
private readonly userWorkspaceRoleRepository: Repository<UserWorkspaceRoleEntity>,
|
||||
private readonly workspacePermissionsCacheStorageService: WorkspacePermissionsCacheStorageService,
|
||||
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
) {}
|
||||
|
||||
async recomputeRolesPermissionsCache({
|
||||
workspaceId,
|
||||
ignoreLock = false,
|
||||
roleIds,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
ignoreLock?: boolean;
|
||||
roleIds?: string[];
|
||||
}): Promise<void> {
|
||||
const isPermissionsV2Enabled =
|
||||
await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IsPermissionsV2Enabled,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const isAlreadyCaching =
|
||||
await this.workspacePermissionsCacheStorageService.getRolesPermissionsOngoingCachingLock(
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (!ignoreLock && isAlreadyCaching) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.workspacePermissionsCacheStorageService.addRolesPermissionsOngoingCachingLock(
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
let currentRolesPermissions: ObjectRecordsPermissionsByRoleId | undefined =
|
||||
undefined;
|
||||
|
||||
if (roleIds) {
|
||||
currentRolesPermissions =
|
||||
await this.workspacePermissionsCacheStorageService.getRolesPermissions(
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
const recomputedRolesPermissions =
|
||||
await this.getObjectRecordPermissionsForRoles({
|
||||
workspaceId,
|
||||
isPermissionsV2Enabled,
|
||||
roleIds,
|
||||
});
|
||||
|
||||
const freshObjectRecordsPermissionsByRoleId = roleIds
|
||||
? { ...currentRolesPermissions, ...recomputedRolesPermissions }
|
||||
: recomputedRolesPermissions;
|
||||
|
||||
await this.workspacePermissionsCacheStorageService.setRolesPermissions(
|
||||
workspaceId,
|
||||
freshObjectRecordsPermissionsByRoleId,
|
||||
);
|
||||
|
||||
await this.workspacePermissionsCacheStorageService.removeRolesPermissionsOngoingCachingLock(
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
async recomputeUserWorkspaceRoleMapCache({
|
||||
workspaceId,
|
||||
ignoreLock = false,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
ignoreLock?: boolean;
|
||||
}): Promise<void> {
|
||||
const isAlreadyCaching =
|
||||
await this.workspacePermissionsCacheStorageService.getUserWorkspaceRoleMapOngoingCachingLock(
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (!ignoreLock && isAlreadyCaching) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.workspacePermissionsCacheStorageService.addUserWorkspaceRoleMapOngoingCachingLock(
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const freshUserWorkspaceRoleMap =
|
||||
await this.getUserWorkspaceRoleMapFromDatabase({
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
await this.workspacePermissionsCacheStorageService.setUserWorkspaceRoleMap(
|
||||
workspaceId,
|
||||
freshUserWorkspaceRoleMap,
|
||||
);
|
||||
|
||||
await this.workspacePermissionsCacheStorageService.removeUserWorkspaceRoleMapOngoingCachingLock(
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
async getRolesPermissionsFromCache({
|
||||
workspaceId,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
}): Promise<CacheResult<string, ObjectRecordsPermissionsByRoleId>> {
|
||||
return getFromCacheWithRecompute<string, ObjectRecordsPermissionsByRoleId>({
|
||||
workspaceId,
|
||||
getCacheData: () =>
|
||||
this.workspacePermissionsCacheStorageService.getRolesPermissions(
|
||||
workspaceId,
|
||||
),
|
||||
getCacheVersion: () =>
|
||||
this.workspacePermissionsCacheStorageService.getRolesPermissionsVersion(
|
||||
workspaceId,
|
||||
),
|
||||
recomputeCache: (params) => this.recomputeRolesPermissionsCache(params),
|
||||
cachedEntityName: ROLES_PERMISSIONS,
|
||||
exceptionCode: TwentyORMExceptionCode.ROLES_PERMISSIONS_VERSION_NOT_FOUND,
|
||||
});
|
||||
}
|
||||
|
||||
async getUserWorkspaceRoleMapFromCache({
|
||||
workspaceId,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
}): Promise<CacheResult<string, UserWorkspaceRoleMap>> {
|
||||
return getFromCacheWithRecompute<string, UserWorkspaceRoleMap>({
|
||||
workspaceId,
|
||||
getCacheData: () =>
|
||||
this.workspacePermissionsCacheStorageService.getUserWorkspaceRoleMap(
|
||||
workspaceId,
|
||||
),
|
||||
getCacheVersion: () =>
|
||||
this.workspacePermissionsCacheStorageService.getUserWorkspaceRoleMapVersion(
|
||||
workspaceId,
|
||||
),
|
||||
recomputeCache: (params) =>
|
||||
this.recomputeUserWorkspaceRoleMapCache(params),
|
||||
cachedEntityName: USER_WORKSPACE_ROLE_MAP,
|
||||
exceptionCode:
|
||||
TwentyORMExceptionCode.USER_WORKSPACE_ROLE_MAP_VERSION_NOT_FOUND,
|
||||
});
|
||||
}
|
||||
|
||||
private async getObjectRecordPermissionsForRoles({
|
||||
workspaceId,
|
||||
isPermissionsV2Enabled,
|
||||
roleIds,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
isPermissionsV2Enabled: boolean;
|
||||
roleIds?: string[];
|
||||
}): Promise<ObjectRecordsPermissionsByRoleId> {
|
||||
let roles: RoleEntity[] = [];
|
||||
|
||||
roles = await this.roleRepository.find({
|
||||
where: {
|
||||
workspaceId,
|
||||
...(roleIds ? { id: In(roleIds) } : {}),
|
||||
},
|
||||
relations: ['objectPermissions'],
|
||||
});
|
||||
|
||||
const workspaceObjectMetadataNameIdMap =
|
||||
await this.getWorkspaceObjectMetadataNameIdMap(workspaceId);
|
||||
|
||||
const permissionsByRoleId: ObjectRecordsPermissionsByRoleId = {};
|
||||
|
||||
for (const role of roles) {
|
||||
const objectRecordsPermissions: ObjectRecordsPermissions = {};
|
||||
|
||||
for (const objectMetadataNameSingular of Object.keys(
|
||||
workspaceObjectMetadataNameIdMap,
|
||||
)) {
|
||||
let canRead = role.canReadAllObjectRecords;
|
||||
let canUpdate = role.canUpdateAllObjectRecords;
|
||||
let canSoftDelete = role.canSoftDeleteAllObjectRecords;
|
||||
let canDestroy = role.canDestroyAllObjectRecords;
|
||||
|
||||
if (isPermissionsV2Enabled) {
|
||||
const objectRecordPermissionsOverride = role.objectPermissions.find(
|
||||
(objectPermission) =>
|
||||
objectPermission.objectMetadataId ===
|
||||
workspaceObjectMetadataNameIdMap[objectMetadataNameSingular],
|
||||
);
|
||||
|
||||
canRead =
|
||||
objectRecordPermissionsOverride?.canReadObjectRecords ?? canRead;
|
||||
canUpdate =
|
||||
objectRecordPermissionsOverride?.canUpdateObjectRecords ??
|
||||
canUpdate;
|
||||
canSoftDelete =
|
||||
objectRecordPermissionsOverride?.canSoftDeleteObjectRecords ??
|
||||
canSoftDelete;
|
||||
canDestroy =
|
||||
objectRecordPermissionsOverride?.canDestroyObjectRecords ??
|
||||
canDestroy;
|
||||
}
|
||||
|
||||
objectRecordsPermissions[objectMetadataNameSingular] = {
|
||||
canRead,
|
||||
canUpdate,
|
||||
canSoftDelete,
|
||||
canDestroy,
|
||||
};
|
||||
}
|
||||
|
||||
permissionsByRoleId[role.id] = objectRecordsPermissions;
|
||||
}
|
||||
|
||||
return permissionsByRoleId;
|
||||
}
|
||||
|
||||
private async getWorkspaceObjectMetadataNameIdMap(
|
||||
workspaceId: string,
|
||||
): Promise<Record<string, string>> {
|
||||
let workspaceObjectMetadataMap: Record<string, string> = {};
|
||||
const metadataVersion =
|
||||
await this.workspaceCacheStorageService.getMetadataVersion(workspaceId);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private async getUserWorkspaceRoleMapFromDatabase({
|
||||
workspaceId,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
}): Promise<UserWorkspaceRoleMap> {
|
||||
const userWorkspaceRoleMap = await this.userWorkspaceRoleRepository.find({
|
||||
where: {
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
return userWorkspaceRoleMap.reduce((acc, userWorkspaceRole) => {
|
||||
acc[userWorkspaceRole.userWorkspaceId] = userWorkspaceRole.roleId;
|
||||
|
||||
return acc;
|
||||
}, {} as UserWorkspaceRoleMap);
|
||||
}
|
||||
}
|
||||
@ -1,20 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
|
||||
|
||||
import { WorkspaceRolesPermissionsCacheService } from './workspace-roles-permissions-cache.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Workspace], 'core'),
|
||||
TypeOrmModule.forFeature([ObjectMetadataEntity, RoleEntity], 'metadata'),
|
||||
WorkspaceCacheStorageModule,
|
||||
],
|
||||
providers: [WorkspaceRolesPermissionsCacheService],
|
||||
exports: [WorkspaceRolesPermissionsCacheService],
|
||||
})
|
||||
export class WorkspaceRolesPermissionsCacheModule {}
|
||||
@ -1,128 +0,0 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import {
|
||||
ObjectRecordsPermissions,
|
||||
ObjectRecordsPermissionsByRoleId,
|
||||
} from 'twenty-shared/types';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceRolesPermissionsCacheService {
|
||||
logger = new Logger(WorkspaceRolesPermissionsCacheService.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(ObjectMetadataEntity, 'metadata')
|
||||
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
||||
@InjectRepository(RoleEntity, 'metadata')
|
||||
private readonly roleRepository: Repository<RoleEntity>,
|
||||
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
|
||||
) {}
|
||||
|
||||
async recomputeRolesPermissionsCache({
|
||||
workspaceId,
|
||||
ignoreLock = false,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
ignoreLock?: boolean;
|
||||
}): Promise<void> {
|
||||
const isAlreadyCaching =
|
||||
await this.workspaceCacheStorageService.getRolesPermissionsOngoingCachingLock(
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (!ignoreLock && isAlreadyCaching) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.workspaceCacheStorageService.addRolesPermissionsOngoingCachingLock(
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const freshObjectRecordsPermissionsByRoleId =
|
||||
await this.getObjectRecordPermissionsForRoles({
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
await this.workspaceCacheStorageService.setRolesPermissions(
|
||||
workspaceId,
|
||||
freshObjectRecordsPermissionsByRoleId,
|
||||
);
|
||||
|
||||
await this.workspaceCacheStorageService.removeRolesPermissionsOngoingCachingLock(
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
private async getObjectRecordPermissionsForRoles({
|
||||
workspaceId,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
}): Promise<ObjectRecordsPermissionsByRoleId> {
|
||||
const roles = await this.roleRepository.find({
|
||||
where: {
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
const workspaceObjectMetadataNames =
|
||||
await this.getWorkspaceObjectMetadataNames(workspaceId);
|
||||
|
||||
const permissionsByRoleId: ObjectRecordsPermissionsByRoleId = {};
|
||||
|
||||
for (const role of roles) {
|
||||
const objectRecordsPermissions: ObjectRecordsPermissions = {};
|
||||
|
||||
for (const objectMetadataNameSingular of workspaceObjectMetadataNames) {
|
||||
objectRecordsPermissions[objectMetadataNameSingular] = {
|
||||
canRead: role.canReadAllObjectRecords,
|
||||
canUpdate: role.canUpdateAllObjectRecords,
|
||||
canSoftDelete: role.canSoftDeleteAllObjectRecords,
|
||||
canDestroy: role.canDestroyAllObjectRecords,
|
||||
};
|
||||
}
|
||||
|
||||
permissionsByRoleId[role.id] = objectRecordsPermissions;
|
||||
}
|
||||
|
||||
return permissionsByRoleId;
|
||||
}
|
||||
|
||||
private async getWorkspaceObjectMetadataNames(
|
||||
workspaceId: string,
|
||||
): Promise<string[]> {
|
||||
let workspaceObjectMetadataNames: string[] = [];
|
||||
const metadataVersion =
|
||||
await this.workspaceCacheStorageService.getMetadataVersion(workspaceId);
|
||||
|
||||
if (metadataVersion) {
|
||||
const objectMetadataMaps =
|
||||
await this.workspaceCacheStorageService.getObjectMetadataMaps(
|
||||
workspaceId,
|
||||
metadataVersion,
|
||||
);
|
||||
|
||||
workspaceObjectMetadataNames = Object.values(
|
||||
objectMetadataMaps?.byId ?? {},
|
||||
).map((objectMetadata) => objectMetadata.nameSingular);
|
||||
}
|
||||
|
||||
if (!metadataVersion || workspaceObjectMetadataNames.length === 0) {
|
||||
const workspaceObjectMetadata = await this.objectMetadataRepository.find({
|
||||
where: {
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
workspaceObjectMetadataNames = workspaceObjectMetadata.map(
|
||||
(objectMetadata) => objectMetadata.nameSingular,
|
||||
);
|
||||
}
|
||||
|
||||
return workspaceObjectMetadataNames;
|
||||
}
|
||||
}
|
||||
@ -18,16 +18,16 @@ export class WorkspaceDataSource extends DataSource {
|
||||
readonly manager: WorkspaceEntityManager;
|
||||
featureFlagMapVersion: string;
|
||||
featureFlagMap: FeatureFlagMap;
|
||||
rolesPermissionsVersion?: string;
|
||||
permissionsPerRoleId?: ObjectRecordsPermissionsByRoleId;
|
||||
rolesPermissionsVersion: string;
|
||||
permissionsPerRoleId: ObjectRecordsPermissionsByRoleId;
|
||||
|
||||
constructor(
|
||||
internalContext: WorkspaceInternalContext,
|
||||
options: DataSourceOptions,
|
||||
featureFlagMapVersion: string,
|
||||
featureFlagMap: FeatureFlagMap,
|
||||
rolesPermissionsVersion?: string,
|
||||
permissionsPerRoleId?: ObjectRecordsPermissionsByRoleId,
|
||||
rolesPermissionsVersion: string,
|
||||
permissionsPerRoleId: ObjectRecordsPermissionsByRoleId,
|
||||
) {
|
||||
super(options);
|
||||
this.internalContext = internalContext;
|
||||
|
||||
@ -13,4 +13,5 @@ export enum TwentyORMExceptionCode {
|
||||
WORKSPACE_SCHEMA_NOT_FOUND = 'WORKSPACE_SCHEMA_NOT_FOUND',
|
||||
ROLES_PERMISSIONS_VERSION_NOT_FOUND = 'ROLES_PERMISSIONS_VERSION_NOT_FOUND',
|
||||
FEATURE_FLAG_MAP_VERSION_NOT_FOUND = 'FEATURE_FLAG_MAP_VERSION_NOT_FOUND',
|
||||
USER_WORKSPACE_ROLE_MAP_VERSION_NOT_FOUND = 'USER_WORKSPACE_ROLE_MAP_VERSION_NOT_FOUND',
|
||||
}
|
||||
|
||||
@ -7,12 +7,12 @@ import { EntitySchema } from 'typeorm';
|
||||
import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface';
|
||||
import { NodeEnvironment } from 'src/engine/core-modules/twenty-config/interfaces/node-environment.interface';
|
||||
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { WorkspaceFeatureFlagsMapCacheService } from 'src/engine/metadata-modules/workspace-feature-flags-map-cache/workspace-feature-flags-map-cache.service';
|
||||
import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service';
|
||||
import { WorkspaceRolesPermissionsCacheService } from 'src/engine/metadata-modules/workspace-roles-permissions-cache/workspace-roles-permissions-cache.service';
|
||||
import { WorkspacePermissionsCacheStorageService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache-storage.service';
|
||||
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
|
||||
import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource';
|
||||
import {
|
||||
TwentyORMException,
|
||||
@ -40,7 +40,8 @@ export class WorkspaceDatasourceFactory {
|
||||
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
|
||||
private readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService,
|
||||
private readonly entitySchemaFactory: EntitySchemaFactory,
|
||||
private readonly workspaceRolesPermissionsCacheService: WorkspaceRolesPermissionsCacheService,
|
||||
private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService,
|
||||
private readonly workspacePermissionsCacheStorageService: WorkspacePermissionsCacheStorageService,
|
||||
private readonly workspaceFeatureFlagsMapCacheService: WorkspaceFeatureFlagsMapCacheService,
|
||||
) {}
|
||||
|
||||
@ -60,15 +61,11 @@ export class WorkspaceDatasourceFactory {
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
const isPermissionsV2Enabled =
|
||||
cachedFeatureFlagMap[FeatureFlagKey.IsPermissionsV2Enabled];
|
||||
|
||||
const {
|
||||
data: cachedRolesPermissions,
|
||||
version: cachedRolesPermissionsVersion,
|
||||
} = await this.getRolesPermissionsFromCache({
|
||||
workspaceId,
|
||||
isPermissionsV2Enabled,
|
||||
});
|
||||
|
||||
if (
|
||||
@ -196,13 +193,11 @@ export class WorkspaceDatasourceFactory {
|
||||
throw new Error(`Failed to create WorkspaceDataSource for ${cacheKey}`);
|
||||
}
|
||||
|
||||
if (isPermissionsV2Enabled) {
|
||||
await this.updateWorkspaceDataSourceRolesPermissionsIfNeeded({
|
||||
workspaceDataSource,
|
||||
cachedRolesPermissionsVersion,
|
||||
cachedRolesPermissions,
|
||||
});
|
||||
}
|
||||
await this.updateWorkspaceDataSourceRolesPermissionsIfNeeded({
|
||||
workspaceDataSource,
|
||||
cachedRolesPermissionsVersion,
|
||||
cachedRolesPermissions,
|
||||
});
|
||||
|
||||
await this.updateWorkspaceDataSourceFeatureFlagsMapIfNeeded({
|
||||
workspaceDataSource,
|
||||
@ -215,33 +210,21 @@ export class WorkspaceDatasourceFactory {
|
||||
|
||||
private async getRolesPermissionsFromCache({
|
||||
workspaceId,
|
||||
isPermissionsV2Enabled,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
isPermissionsV2Enabled?: boolean;
|
||||
}): Promise<
|
||||
CacheResult<
|
||||
string | undefined,
|
||||
ObjectRecordsPermissionsByRoleId | undefined
|
||||
>
|
||||
> {
|
||||
if (!isPermissionsV2Enabled) {
|
||||
return { version: undefined, data: undefined };
|
||||
}
|
||||
|
||||
return getFromCacheWithRecompute<
|
||||
string | undefined,
|
||||
ObjectRecordsPermissionsByRoleId | undefined
|
||||
>({
|
||||
}): Promise<CacheResult<string, ObjectRecordsPermissionsByRoleId>> {
|
||||
return getFromCacheWithRecompute<string, ObjectRecordsPermissionsByRoleId>({
|
||||
workspaceId,
|
||||
getCacheData: () =>
|
||||
this.workspaceCacheStorageService.getRolesPermissions(workspaceId),
|
||||
this.workspacePermissionsCacheStorageService.getRolesPermissions(
|
||||
workspaceId,
|
||||
),
|
||||
getCacheVersion: () =>
|
||||
this.workspaceCacheStorageService.getRolesPermissionsVersionFromCache(
|
||||
this.workspacePermissionsCacheStorageService.getRolesPermissionsVersion(
|
||||
workspaceId,
|
||||
),
|
||||
recomputeCache: (params) =>
|
||||
this.workspaceRolesPermissionsCacheService.recomputeRolesPermissionsCache(
|
||||
this.workspacePermissionsCacheService.recomputeRolesPermissionsCache(
|
||||
params,
|
||||
),
|
||||
cachedEntityName: 'Roles permissions',
|
||||
@ -281,8 +264,8 @@ export class WorkspaceDatasourceFactory {
|
||||
cachedRolesPermissions,
|
||||
}: {
|
||||
workspaceDataSource: WorkspaceDataSource;
|
||||
cachedRolesPermissionsVersion: string | undefined;
|
||||
cachedRolesPermissions: ObjectRecordsPermissionsByRoleId | undefined;
|
||||
cachedRolesPermissionsVersion: string;
|
||||
cachedRolesPermissions: ObjectRecordsPermissionsByRoleId;
|
||||
}): Promise<void> {
|
||||
this.updateWorkspaceDataSourceIfNeeded({
|
||||
workspaceDataSource,
|
||||
|
||||
@ -8,7 +8,7 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi
|
||||
import { UserWorkspaceRoleEntity } from 'src/engine/metadata-modules/role/user-workspace-role.entity';
|
||||
import { WorkspaceFeatureFlagsMapCacheModule } from 'src/engine/metadata-modules/workspace-feature-flags-map-cache/workspace-feature-flags-map-cache.module';
|
||||
import { WorkspaceMetadataCacheModule } from 'src/engine/metadata-modules/workspace-metadata-cache/workspace-metadata-cache.module';
|
||||
import { WorkspaceRolesPermissionsCacheModule } from 'src/engine/metadata-modules/workspace-roles-permissions-cache/workspace-roles-permissions-cache.module';
|
||||
import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module';
|
||||
import { entitySchemaFactories } from 'src/engine/twenty-orm/factories';
|
||||
import { EntitySchemaFactory } from 'src/engine/twenty-orm/factories/entity-schema.factory';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
@ -26,8 +26,8 @@ import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/
|
||||
WorkspaceCacheStorageModule,
|
||||
WorkspaceMetadataCacheModule,
|
||||
PermissionsModule,
|
||||
WorkspaceRolesPermissionsCacheModule,
|
||||
WorkspaceFeatureFlagsMapCacheModule,
|
||||
WorkspacePermissionsCacheModule,
|
||||
FeatureFlagModule,
|
||||
],
|
||||
providers: [
|
||||
|
||||
@ -2,9 +2,7 @@ import { Injectable } from '@nestjs/common';
|
||||
|
||||
import crypto from 'crypto';
|
||||
|
||||
import { ObjectRecordsPermissionsByRoleId } from 'twenty-shared/types';
|
||||
import { EntitySchemaOptions } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface';
|
||||
|
||||
@ -23,12 +21,15 @@ export enum WorkspaceCacheKeys {
|
||||
MetadataObjectMetadataMaps = 'metadata:object-metadata-maps',
|
||||
MetadataObjectMetadataOngoingCachingLock = 'metadata:object-metadata-ongoing-caching-lock',
|
||||
MetadataVersion = 'metadata:workspace-metadata-version',
|
||||
MetadataRolesPermissions = 'metadata:roles-permissions',
|
||||
MetadataRolesPermissionsVersion = 'metadata:roles-permissions-version',
|
||||
MetadataRolesPermissionsOngoingCachingLock = 'metadata:roles-permissions-ongoing-caching-lock',
|
||||
FeatureFlagMap = 'feature-flag-map',
|
||||
FeatureFlagMapVersion = 'feature-flag-map-version',
|
||||
FeatureFlagMap = 'feature-flag:feature-flag-map',
|
||||
FeatureFlagMapVersion = 'feature-flag:feature-flag-map-version',
|
||||
FeatureFlagMapOngoingCachingLock = 'feature-flag-map-ongoing-caching-lock',
|
||||
MetadataPermissionsRolesPermissions = 'metadata:permissions:roles-permissions',
|
||||
MetadataPermissionsRolesPermissionsVersion = 'metadata:permissions:roles-permissions-version',
|
||||
MetadataPermissionsRolesPermissionsOngoingCachingLock = 'metadata:permissions:roles-permissions-ongoing-caching-lock',
|
||||
MetadataPermissionsUserWorkspaceRoleMap = 'metadata:permissions:user-workspace-role-map',
|
||||
MetadataPermissionsUserWorkspaceRoleMapVersion = 'metadata:permissions:user-workspace-role-map-version',
|
||||
MetadataPermissionsUserWorkspaceRoleMapOngoingCachingLock = 'metadata:permissions:user-workspace-role-map-ongoing-caching-lock',
|
||||
}
|
||||
|
||||
const TTL_INFINITE = 0;
|
||||
@ -186,74 +187,6 @@ export class WorkspaceCacheStorageService {
|
||||
);
|
||||
}
|
||||
|
||||
getRolesPermissionsVersionFromCache(
|
||||
workspaceId: string,
|
||||
): Promise<string | undefined> {
|
||||
return this.cacheStorageService.get<string>(
|
||||
`${WorkspaceCacheKeys.MetadataRolesPermissionsVersion}:${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
async setRolesPermissionsVersion(workspaceId: string): Promise<string> {
|
||||
const rolesPermissionsVersion = v4();
|
||||
|
||||
await this.cacheStorageService.set<string>(
|
||||
`${WorkspaceCacheKeys.MetadataRolesPermissionsVersion}:${workspaceId}`,
|
||||
rolesPermissionsVersion,
|
||||
TTL_INFINITE,
|
||||
);
|
||||
|
||||
return rolesPermissionsVersion;
|
||||
}
|
||||
|
||||
async setRolesPermissions(
|
||||
workspaceId: string,
|
||||
permissions: ObjectRecordsPermissionsByRoleId,
|
||||
): Promise<{
|
||||
newRolesPermissionsVersion: string;
|
||||
}> {
|
||||
const [, newRolesPermissionsVersion] = await Promise.all([
|
||||
this.cacheStorageService.set<ObjectRecordsPermissionsByRoleId>(
|
||||
`${WorkspaceCacheKeys.MetadataRolesPermissions}:${workspaceId}`,
|
||||
permissions,
|
||||
TTL_INFINITE,
|
||||
),
|
||||
this.setRolesPermissionsVersion(workspaceId),
|
||||
]);
|
||||
|
||||
return { newRolesPermissionsVersion };
|
||||
}
|
||||
|
||||
getRolesPermissions(
|
||||
workspaceId: string,
|
||||
): Promise<ObjectRecordsPermissionsByRoleId | undefined> {
|
||||
return this.cacheStorageService.get<ObjectRecordsPermissionsByRoleId>(
|
||||
`${WorkspaceCacheKeys.MetadataRolesPermissions}:${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
addRolesPermissionsOngoingCachingLock(workspaceId: string) {
|
||||
return this.cacheStorageService.set<boolean>(
|
||||
`${WorkspaceCacheKeys.MetadataRolesPermissionsOngoingCachingLock}:${workspaceId}`,
|
||||
true,
|
||||
1_000 * 60, // 1 minute
|
||||
);
|
||||
}
|
||||
|
||||
removeRolesPermissionsOngoingCachingLock(workspaceId: string) {
|
||||
return this.cacheStorageService.del(
|
||||
`${WorkspaceCacheKeys.MetadataRolesPermissionsOngoingCachingLock}:${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
getRolesPermissionsOngoingCachingLock(
|
||||
workspaceId: string,
|
||||
): Promise<boolean | undefined> {
|
||||
return this.cacheStorageService.get<boolean>(
|
||||
`${WorkspaceCacheKeys.MetadataRolesPermissionsOngoingCachingLock}:${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
getFeatureFlagsMapVersionFromCache(
|
||||
workspaceId: string,
|
||||
): Promise<string | undefined> {
|
||||
@ -341,15 +274,27 @@ export class WorkspaceCacheStorageService {
|
||||
);
|
||||
|
||||
await this.cacheStorageService.del(
|
||||
`${WorkspaceCacheKeys.MetadataRolesPermissions}:${workspaceId}`,
|
||||
`${WorkspaceCacheKeys.MetadataPermissionsRolesPermissions}:${workspaceId}`,
|
||||
);
|
||||
|
||||
await this.cacheStorageService.del(
|
||||
`${WorkspaceCacheKeys.MetadataRolesPermissionsVersion}:${workspaceId}`,
|
||||
`${WorkspaceCacheKeys.MetadataPermissionsRolesPermissionsVersion}:${workspaceId}`,
|
||||
);
|
||||
|
||||
await this.cacheStorageService.del(
|
||||
`${WorkspaceCacheKeys.MetadataRolesPermissionsOngoingCachingLock}:${workspaceId}`,
|
||||
`${WorkspaceCacheKeys.MetadataPermissionsRolesPermissionsOngoingCachingLock}:${workspaceId}`,
|
||||
);
|
||||
|
||||
await this.cacheStorageService.del(
|
||||
`${WorkspaceCacheKeys.MetadataPermissionsUserWorkspaceRoleMap}:${workspaceId}`,
|
||||
);
|
||||
|
||||
await this.cacheStorageService.del(
|
||||
`${WorkspaceCacheKeys.MetadataPermissionsUserWorkspaceRoleMapVersion}:${workspaceId}`,
|
||||
);
|
||||
|
||||
await this.cacheStorageService.del(
|
||||
`${WorkspaceCacheKeys.MetadataPermissionsUserWorkspaceRoleMapOngoingCachingLock}:${workspaceId}`,
|
||||
);
|
||||
|
||||
await this.cacheStorageService.del(
|
||||
|
||||
Reference in New Issue
Block a user