[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:
@ -199,6 +199,7 @@ export abstract class GraphqlQueryBaseResolverService<
|
||||
await this.permissionsService.userHasWorkspaceSettingPermission({
|
||||
userWorkspaceId: authContext.userWorkspaceId,
|
||||
_setting: permissionRequired,
|
||||
workspaceId: authContext.workspace.id,
|
||||
});
|
||||
|
||||
if (!userHasPermission) {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
|
||||
|
||||
import { IDField } from '@ptc-org/nestjs-query-graphql';
|
||||
import { SettingsFeatures } from 'twenty-shared';
|
||||
import { PermissionsOnAllObjectRecords, SettingsFeatures } from 'twenty-shared';
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
@ -25,6 +25,10 @@ registerEnumType(SettingsFeatures, {
|
||||
name: 'SettingsFeatures',
|
||||
});
|
||||
|
||||
registerEnumType(PermissionsOnAllObjectRecords, {
|
||||
name: 'PermissionsOnAllObjectRecords',
|
||||
});
|
||||
|
||||
@Entity({ name: 'userWorkspace', schema: 'core' })
|
||||
@ObjectType()
|
||||
@Unique('IndexOnUserIdAndWorkspaceIdUnique', ['userId', 'workspaceId'])
|
||||
@ -75,4 +79,7 @@ export class UserWorkspace {
|
||||
|
||||
@Field(() => [SettingsFeatures], { nullable: true })
|
||||
settingsPermissions?: SettingsFeatures[];
|
||||
|
||||
@Field(() => [PermissionsOnAllObjectRecords], { nullable: true })
|
||||
objectRecordsPermissions?: PermissionsOnAllObjectRecords[];
|
||||
}
|
||||
|
||||
@ -13,7 +13,7 @@ import crypto from 'crypto';
|
||||
|
||||
import { GraphQLJSONObject } from 'graphql-type-json';
|
||||
import { FileUpload, GraphQLUpload } from 'graphql-upload';
|
||||
import { SettingsFeatures } from 'twenty-shared';
|
||||
import { PermissionsOnAllObjectRecords, SettingsFeatures } from 'twenty-shared';
|
||||
import { In, Repository } from 'typeorm';
|
||||
|
||||
import { SupportDriver } from 'src/engine/core-modules/environment/interfaces/support.interface';
|
||||
@ -113,16 +113,23 @@ export class UserResolver {
|
||||
if (!currentUserWorkspace) {
|
||||
throw new Error('Current user workspace not found');
|
||||
}
|
||||
const permissions =
|
||||
await this.permissionsService.getUserWorkspaceSettingsPermissions({
|
||||
const { settingsPermissions, objectRecordsPermissions } =
|
||||
await this.permissionsService.getUserWorkspacePermissions({
|
||||
userWorkspaceId: currentUserWorkspace.id,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
const permittedFeatures: SettingsFeatures[] = (
|
||||
Object.keys(permissions) as SettingsFeatures[]
|
||||
).filter((feature) => permissions[feature] === true);
|
||||
Object.keys(settingsPermissions) as SettingsFeatures[]
|
||||
).filter((feature) => settingsPermissions[feature] === true);
|
||||
|
||||
const permittedObjectRecordsPermissions = (
|
||||
Object.keys(objectRecordsPermissions) as PermissionsOnAllObjectRecords[]
|
||||
).filter((permission) => objectRecordsPermissions[permission] === true);
|
||||
|
||||
currentUserWorkspace.settingsPermissions = permittedFeatures;
|
||||
currentUserWorkspace.objectRecordsPermissions =
|
||||
permittedObjectRecordsPermissions;
|
||||
user.currentUserWorkspace = currentUserWorkspace;
|
||||
}
|
||||
|
||||
@ -216,9 +223,12 @@ export class UserResolver {
|
||||
);
|
||||
|
||||
rolesByUserWorkspaces =
|
||||
await this.userRoleService.getRolesByUserWorkspaces(
|
||||
userWorkspaces.map((userWorkspace) => userWorkspace.id),
|
||||
);
|
||||
await this.userRoleService.getRolesByUserWorkspaces({
|
||||
userWorkspaceIds: userWorkspaces.map(
|
||||
(userWorkspace) => userWorkspace.id,
|
||||
),
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
}
|
||||
|
||||
for (const workspaceMemberEntity of workspaceMemberEntities) {
|
||||
@ -254,6 +264,11 @@ export class UserResolver {
|
||||
description: roleEntity.description,
|
||||
isEditable: roleEntity.isEditable,
|
||||
userWorkspaceRoles: roleEntity.userWorkspaceRoles,
|
||||
canReadAllObjectRecords: roleEntity.canReadAllObjectRecords,
|
||||
canUpdateAllObjectRecords: roleEntity.canUpdateAllObjectRecords,
|
||||
canSoftDeleteAllObjectRecords:
|
||||
roleEntity.canSoftDeleteAllObjectRecords,
|
||||
canDestroyAllObjectRecords: roleEntity.canDestroyAllObjectRecords,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@ -152,11 +152,13 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
|
||||
await this.validateSecurityPermissions({
|
||||
payload,
|
||||
userWorkspaceId,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
await this.validateWorkspacePermissions({
|
||||
payload,
|
||||
userWorkspaceId,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
}
|
||||
|
||||
@ -378,9 +380,11 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
|
||||
private async validateSecurityPermissions({
|
||||
payload,
|
||||
userWorkspaceId,
|
||||
workspaceId,
|
||||
}: {
|
||||
payload: Partial<Workspace>;
|
||||
userWorkspaceId?: string;
|
||||
workspaceId: string;
|
||||
}) {
|
||||
if (
|
||||
'isGoogleAuthEnabled' in payload ||
|
||||
@ -396,6 +400,7 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
|
||||
await this.permissionsService.userHasWorkspaceSettingPermission({
|
||||
userWorkspaceId,
|
||||
_setting: SettingsFeatures.SECURITY,
|
||||
workspaceId: workspaceId,
|
||||
});
|
||||
|
||||
if (!userHasPermission) {
|
||||
@ -410,9 +415,11 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
|
||||
private async validateWorkspacePermissions({
|
||||
payload,
|
||||
userWorkspaceId,
|
||||
workspaceId,
|
||||
}: {
|
||||
payload: Partial<Workspace>;
|
||||
userWorkspaceId?: string;
|
||||
workspaceId: string;
|
||||
}) {
|
||||
if (
|
||||
'displayName' in payload ||
|
||||
@ -427,6 +434,7 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
|
||||
const userHasPermission =
|
||||
await this.permissionsService.userHasWorkspaceSettingPermission({
|
||||
userWorkspaceId,
|
||||
workspaceId,
|
||||
_setting: SettingsFeatures.WORKSPACE,
|
||||
});
|
||||
|
||||
|
||||
@ -47,6 +47,7 @@ export const SettingsPermissionsGuard = (
|
||||
await this.permissionsService.userHasWorkspaceSettingPermission({
|
||||
userWorkspaceId,
|
||||
_setting: requiredPermission,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
if (hasPermission === true) {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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) ?? [],
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -4,6 +4,11 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { DEV_SEED_USER_WORKSPACE_IDS } from 'src/database/typeorm-seeds/core/user-workspaces';
|
||||
import {
|
||||
SEED_ACME_WORKSPACE_ID,
|
||||
SEED_APPLE_WORKSPACE_ID,
|
||||
} from 'src/database/typeorm-seeds/core/workspaces';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
@ -42,6 +47,7 @@ export class WorkspaceManagerService {
|
||||
private readonly userWorkspaceRepository: Repository<UserWorkspace>,
|
||||
private readonly roleService: RoleService,
|
||||
private readonly userRoleService: UserRoleService,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@ -261,20 +267,34 @@ export class WorkspaceManagerService {
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
await this.userRoleService.assignRoleToUserWorkspace({
|
||||
workspaceId,
|
||||
userWorkspaceId: DEV_SEED_USER_WORKSPACE_IDS.TIM,
|
||||
roleId: adminRole.id,
|
||||
});
|
||||
let adminUserWorkspaceId: string | undefined;
|
||||
let memberUserWorkspaceId: string | undefined;
|
||||
|
||||
if (workspaceId === SEED_APPLE_WORKSPACE_ID) {
|
||||
adminUserWorkspaceId = DEV_SEED_USER_WORKSPACE_IDS.TIM;
|
||||
memberUserWorkspaceId = DEV_SEED_USER_WORKSPACE_IDS.JONY;
|
||||
} else if (workspaceId === SEED_ACME_WORKSPACE_ID) {
|
||||
adminUserWorkspaceId = DEV_SEED_USER_WORKSPACE_IDS.TIM_ACME;
|
||||
}
|
||||
|
||||
if (adminUserWorkspaceId) {
|
||||
await this.userRoleService.assignRoleToUserWorkspace({
|
||||
workspaceId,
|
||||
userWorkspaceId: adminUserWorkspaceId,
|
||||
roleId: adminRole.id,
|
||||
});
|
||||
}
|
||||
|
||||
const memberRole = await this.roleService.createMemberRole({
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
await this.userRoleService.assignRoleToUserWorkspace({
|
||||
workspaceId,
|
||||
userWorkspaceId: DEV_SEED_USER_WORKSPACE_IDS.JONY,
|
||||
roleId: memberRole.id,
|
||||
});
|
||||
if (memberUserWorkspaceId) {
|
||||
await this.userRoleService.assignRoleToUserWorkspace({
|
||||
workspaceId,
|
||||
userWorkspaceId: memberUserWorkspaceId,
|
||||
roleId: memberRole.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user