[permissions] Add object records permissions to role entity (#10255)

Closes https://github.com/twentyhq/core-team-issues/issues/388

- Add object records-related permissions to role entity
- Add it to queriable `currentUserWorkspace` (used in FE)
This commit is contained in:
Marie
2025-02-17 18:32:39 +01:00
committed by GitHub
parent 5b4cb4bd2c
commit cb3bd1353a
22 changed files with 255 additions and 60 deletions

View File

@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common';
import { SettingsFeatures } from 'twenty-shared';
import { PermissionsOnAllObjectRecords, SettingsFeatures } from 'twenty-shared';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
@ -12,13 +12,21 @@ export class PermissionsService {
private readonly userRoleService: UserRoleService,
) {}
public async getUserWorkspaceSettingsPermissions({
public async getUserWorkspacePermissions({
userWorkspaceId,
workspaceId,
}: {
userWorkspaceId: string;
}): Promise<Record<SettingsFeatures, boolean>> {
workspaceId: string;
}): Promise<{
settingsPermissions: Record<SettingsFeatures, boolean>;
objectRecordsPermissions: Record<PermissionsOnAllObjectRecords, boolean>;
}> {
const [roleOfUserWorkspace] = await this.userRoleService
.getRolesByUserWorkspaces([userWorkspaceId])
.getRolesByUserWorkspaces({
userWorkspaceIds: [userWorkspaceId],
workspaceId,
})
.then((roles) => roles?.get(userWorkspaceId) ?? []);
let hasPermissionOnSettingFeature = false;
@ -27,24 +35,48 @@ export class PermissionsService {
hasPermissionOnSettingFeature = true;
}
return Object.keys(SettingsFeatures).reduce(
const settingsPermissionsMap = Object.keys(SettingsFeatures).reduce(
(acc, feature) => ({
...acc,
[feature]: hasPermissionOnSettingFeature,
}),
{} as Record<SettingsFeatures, boolean>,
);
const objectRecordsPermissionsMap: Record<
PermissionsOnAllObjectRecords,
boolean
> = {
[PermissionsOnAllObjectRecords.READ_ALL_OBJECT_RECORDS]:
roleOfUserWorkspace?.canReadAllObjectRecords ?? false,
[PermissionsOnAllObjectRecords.UPDATE_ALL_OBJECT_RECORDS]:
roleOfUserWorkspace?.canUpdateAllObjectRecords ?? false,
[PermissionsOnAllObjectRecords.SOFT_DELETE_ALL_OBJECT_RECORDS]:
roleOfUserWorkspace?.canSoftDeleteAllObjectRecords ?? false,
[PermissionsOnAllObjectRecords.DESTROY_ALL_OBJECT_RECORDS]:
roleOfUserWorkspace?.canDestroyAllObjectRecords ?? false,
};
return {
settingsPermissions: settingsPermissionsMap,
objectRecordsPermissions: objectRecordsPermissionsMap,
};
}
public async userHasWorkspaceSettingPermission({
userWorkspaceId,
workspaceId,
_setting,
}: {
userWorkspaceId: string;
workspaceId: string;
_setting: SettingsFeatures;
}): Promise<boolean> {
const [roleOfUserWorkspace] = await this.userRoleService
.getRolesByUserWorkspaces([userWorkspaceId])
.getRolesByUserWorkspaces({
userWorkspaceIds: [userWorkspaceId],
workspaceId,
})
.then((roles) => roles?.get(userWorkspaceId) ?? []);
if (roleOfUserWorkspace?.canUpdateAllSettings === true) {

View File

@ -13,9 +13,6 @@ export class RoleDTO {
@Field({ nullable: false })
label: string;
@Field({ nullable: false })
canUpdateAllSettings: boolean;
@Field({ nullable: true })
description: string;
@ -27,4 +24,19 @@ export class RoleDTO {
@Field(() => [WorkspaceMember], { nullable: true })
workspaceMembers?: WorkspaceMember[];
@Field({ nullable: false })
canUpdateAllSettings: boolean;
@Field({ nullable: false })
canReadAllObjectRecords: boolean;
@Field({ nullable: false })
canUpdateAllObjectRecords: boolean;
@Field({ nullable: false })
canSoftDeleteAllObjectRecords: boolean;
@Field({ nullable: false })
canDestroyAllObjectRecords: boolean;
}

View File

@ -21,6 +21,18 @@ export class RoleEntity {
@Column({ nullable: false, default: false })
canUpdateAllSettings: boolean;
@Column({ nullable: false, default: false })
canReadAllObjectRecords: boolean;
@Column({ nullable: false, default: false })
canUpdateAllObjectRecords: boolean;
@Column({ nullable: false, default: false })
canSoftDeleteAllObjectRecords: boolean;
@Column({ nullable: false, default: false })
canDestroyAllObjectRecords: boolean;
@Column({ nullable: true, type: 'text' })
description: string;

View File

@ -38,13 +38,17 @@ export class RoleResolver {
return roles.map((role) => ({
id: role.id,
label: role.label,
canUpdateAllSettings: role.canUpdateAllSettings,
description: role.description,
workspaceId: role.workspaceId,
createdAt: role.createdAt,
updatedAt: role.updatedAt,
isEditable: role.isEditable,
userWorkspaceRoles: role.userWorkspaceRoles,
canUpdateAllSettings: role.canUpdateAllSettings,
canReadAllObjectRecords: role.canReadAllObjectRecords,
canUpdateAllObjectRecords: role.canUpdateAllObjectRecords,
canSoftDeleteAllObjectRecords: role.canSoftDeleteAllObjectRecords,
canDestroyAllObjectRecords: role.canDestroyAllObjectRecords,
}));
}
@ -81,7 +85,10 @@ export class RoleResolver {
}
const roles = await this.userRoleService
.getRolesByUserWorkspaces([userWorkspace.id])
.getRolesByUserWorkspaces({
userWorkspaceIds: [userWorkspace.id],
workspaceId: workspace.id,
})
.then(
(rolesByUserWorkspaces) =>
rolesByUserWorkspaces?.get(userWorkspace.id) ?? [],

View File

@ -30,6 +30,10 @@ export class RoleService {
label: ADMIN_ROLE_LABEL,
description: 'Admin role',
canUpdateAllSettings: true,
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: true,
canSoftDeleteAllObjectRecords: true,
canDestroyAllObjectRecords: true,
isEditable: false,
workspaceId,
});
@ -44,6 +48,10 @@ export class RoleService {
label: MEMBER_ROLE_LABEL,
description: 'Member role',
canUpdateAllSettings: false,
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: true,
canSoftDeleteAllObjectRecords: true,
canDestroyAllObjectRecords: true,
isEditable: false,
workspaceId,
});

View File

@ -60,7 +60,10 @@ export class UserRoleService {
);
}
const roles = await this.getRolesByUserWorkspaces([userWorkspace.id]);
const roles = await this.getRolesByUserWorkspaces({
userWorkspaceIds: [userWorkspace.id],
workspaceId,
});
const currentRole = roles.get(userWorkspace.id)?.[0];
@ -88,8 +91,10 @@ export class UserRoleService {
workspaceId: string;
}): Promise<void> {
await this.validatesUserWorkspaceIsNotLastAdminIfUnassigningAdminRoleOrThrow(
userWorkspaceId,
workspaceId,
{
userWorkspaceId,
workspaceId,
},
);
await this.userWorkspaceRoleRepository.delete({
@ -98,9 +103,13 @@ export class UserRoleService {
});
}
public async getRolesByUserWorkspaces(
userWorkspaceIds: string[],
): Promise<Map<string, RoleDTO[]>> {
public async getRolesByUserWorkspaces({
userWorkspaceIds,
workspaceId,
}: {
userWorkspaceIds: string[];
workspaceId: string;
}): Promise<Map<string, RoleDTO[]>> {
if (!userWorkspaceIds.length) {
return new Map();
}
@ -108,6 +117,7 @@ export class UserRoleService {
const allUserWorkspaceRoles = await this.userWorkspaceRoleRepository.find({
where: {
userWorkspaceId: In(userWorkspaceIds),
workspaceId,
},
relations: {
role: true,
@ -176,11 +186,17 @@ export class UserRoleService {
return workspaceMembers;
}
private async validatesUserWorkspaceIsNotLastAdminIfUnassigningAdminRoleOrThrow(
userWorkspaceId: string,
workspaceId: string,
): Promise<void> {
const roles = await this.getRolesByUserWorkspaces([userWorkspaceId]);
private async validatesUserWorkspaceIsNotLastAdminIfUnassigningAdminRoleOrThrow({
userWorkspaceId,
workspaceId,
}: {
userWorkspaceId: string;
workspaceId: string;
}): Promise<void> {
const roles = await this.getRolesByUserWorkspaces({
userWorkspaceIds: [userWorkspaceId],
workspaceId,
});
const currentRoles = roles.get(userWorkspaceId);