[permissions] Enforce object-records permission checks in resolvers (#10304)

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

- enforcing object-records permission checks in resolvers for now. we
will move the logic to a lower level asap
- add integration tests that will still be useful when we have moved the
logic
- introduce guest seeded role to test limited permissions on
object-records
This commit is contained in:
Marie
2025-02-19 11:21:03 +01:00
committed by GitHub
parent 33178fa8b2
commit 861face2a8
48 changed files with 1372 additions and 144 deletions

View File

@ -16,6 +16,8 @@ export enum PermissionsExceptionCode {
WORKSPACE_MEMBER_NOT_FOUND = 'WORKSPACE_MEMBER_NOT_FOUND',
ROLE_NOT_FOUND = 'ROLE_NOT_FOUND',
CANNOT_UNASSIGN_LAST_ADMIN = 'CANNOT_UNASSIGN_LAST_ADMIN',
UNKNOWN_OPERATION_NAME = 'UNKNOWN_OPERATION_NAME',
UNKNOWN_REQUIRED_PERMISSION = 'UNKNOWN_REQUIRED_PERMISSION',
}
export enum PermissionsExceptionMessage {
@ -28,4 +30,6 @@ export enum PermissionsExceptionMessage {
WORKSPACE_MEMBER_NOT_FOUND = 'Workspace member not found',
ROLE_NOT_FOUND = 'Role not found',
CANNOT_UNASSIGN_LAST_ADMIN = 'Cannot unassign last admin',
UNKNOWN_OPERATION_NAME = 'Unknown operation name, cannot determine required permission',
UNKNOWN_REQUIRED_PERMISSION = 'Unknown required permission',
}

View File

@ -3,6 +3,12 @@ import { Injectable } from '@nestjs/common';
import { PermissionsOnAllObjectRecords, SettingsFeatures } from 'twenty-shared';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
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';
@Injectable()
@ -86,7 +92,49 @@ export class PermissionsService {
return false;
}
public async userHasObjectRecordsPermission({
userWorkspaceId,
workspaceId,
requiredPermission,
}: {
userWorkspaceId: string;
workspaceId: string;
requiredPermission: PermissionsOnAllObjectRecords;
}): Promise<boolean> {
const [roleOfUserWorkspace] = await this.userRoleService
.getRolesByUserWorkspaces({
userWorkspaceIds: [userWorkspaceId],
workspaceId,
})
.then((roles) => roles?.get(userWorkspaceId) ?? []);
const roleColumn =
this.getRoleColumnForRequiredPermission(requiredPermission);
return roleOfUserWorkspace?.[roleColumn] === true;
}
public async isPermissionsEnabled(): Promise<boolean> {
return this.environmentService.get('PERMISSIONS_ENABLED') === true;
}
private getRoleColumnForRequiredPermission(
requiredPermission: PermissionsOnAllObjectRecords,
): keyof RoleEntity {
switch (requiredPermission) {
case PermissionsOnAllObjectRecords.READ_ALL_OBJECT_RECORDS:
return 'canReadAllObjectRecords';
case PermissionsOnAllObjectRecords.UPDATE_ALL_OBJECT_RECORDS:
return 'canUpdateAllObjectRecords';
case PermissionsOnAllObjectRecords.SOFT_DELETE_ALL_OBJECT_RECORDS:
return 'canSoftDeleteAllObjectRecords';
case PermissionsOnAllObjectRecords.DESTROY_ALL_OBJECT_RECORDS:
return 'canDestroyAllObjectRecords';
default:
throw new PermissionsException(
PermissionsExceptionMessage.UNKNOWN_REQUIRED_PERMISSION,
PermissionsExceptionCode.UNKNOWN_REQUIRED_PERMISSION,
);
}
}
}

View File

@ -0,0 +1,24 @@
import {
ForbiddenError,
InternalServerError,
NotFoundError,
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import {
PermissionsException,
PermissionsExceptionCode,
} from 'src/engine/metadata-modules/permissions/permissions.exception';
export const permissionGraphqlApiExceptionHandler = (
error: PermissionsException,
) => {
switch (error.code) {
case PermissionsExceptionCode.PERMISSION_DENIED:
case PermissionsExceptionCode.CANNOT_UNASSIGN_LAST_ADMIN:
throw new ForbiddenError(error.message);
case PermissionsExceptionCode.ROLE_NOT_FOUND:
case PermissionsExceptionCode.USER_WORKSPACE_NOT_FOUND:
throw new NotFoundError(error.message);
default:
throw new InternalServerError(error.message);
}
};

View File

@ -1,27 +1,11 @@
import { Catch, ExceptionFilter } from '@nestjs/common';
import {
ForbiddenError,
InternalServerError,
NotFoundError,
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import {
PermissionsException,
PermissionsExceptionCode,
} from 'src/engine/metadata-modules/permissions/permissions.exception';
import { PermissionsException } from 'src/engine/metadata-modules/permissions/permissions.exception';
import { permissionGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/permissions/utils/permission-graphql-api-exception-handler.util';
@Catch(PermissionsException)
export class PermissionsGraphqlApiExceptionFilter implements ExceptionFilter {
catch(exception: PermissionsException) {
switch (exception.code) {
case PermissionsExceptionCode.PERMISSION_DENIED:
case PermissionsExceptionCode.CANNOT_UNASSIGN_LAST_ADMIN:
throw new ForbiddenError(exception.message);
case PermissionsExceptionCode.ROLE_NOT_FOUND:
case PermissionsExceptionCode.USER_WORKSPACE_NOT_FOUND:
throw new NotFoundError(exception.message);
default:
throw new InternalServerError(exception.message);
}
return permissionGraphqlApiExceptionHandler(exception);
}
}

View File

@ -56,4 +56,23 @@ export class RoleService {
workspaceId,
});
}
// Only used for dev seeding and testing
public async createGuestRole({
workspaceId,
}: {
workspaceId: string;
}): Promise<RoleEntity> {
return this.roleRepository.save({
label: 'Guest',
description: 'Guest role',
canUpdateAllSettings: false,
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: false,
canSoftDeleteAllObjectRecords: false,
canDestroyAllObjectRecords: false,
isEditable: false,
workspaceId,
});
}
}

View File

@ -109,7 +109,7 @@ export class UserRoleService {
}: {
userWorkspaceIds: string[];
workspaceId: string;
}): Promise<Map<string, RoleDTO[]>> {
}): Promise<Map<string, RoleEntity[]>> {
if (!userWorkspaceIds.length) {
return new Map();
}
@ -128,7 +128,7 @@ export class UserRoleService {
return new Map();
}
const rolesMap = new Map<string, RoleDTO[]>();
const rolesMap = new Map<string, RoleEntity[]>();
for (const userWorkspaceId of userWorkspaceIds) {
const userWorkspaceRolesOfUserWorkspace = allUserWorkspaceRoles.filter(