diff --git a/apps/server/src/modules/authorization/index.ts b/apps/server/src/modules/authorization/index.ts index e129df2cd11..5d4583e7c97 100644 --- a/apps/server/src/modules/authorization/index.ts +++ b/apps/server/src/modules/authorization/index.ts @@ -13,3 +13,4 @@ export { } from './domain'; // Should not used anymore export { FeathersAuthorizationService } from './feathers'; +export { PermissionContextService } from './permission-context/service/permission-context.service'; diff --git a/apps/server/src/modules/authorization/permission-context/service/permission-context.service.spec.ts b/apps/server/src/modules/authorization/permission-context/service/permission-context.service.spec.ts index 09e90dd56ce..c1a0997edf0 100644 --- a/apps/server/src/modules/authorization/permission-context/service/permission-context.service.spec.ts +++ b/apps/server/src/modules/authorization/permission-context/service/permission-context.service.spec.ts @@ -1,4 +1,3 @@ -import { ObjectId } from 'bson'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { permissionContextFactory, setupEntities, userFactory } from '@shared/testing'; @@ -50,10 +49,10 @@ describe('PermissionContextService', () => { it('should call permissionContextRepo', async () => { const { user, spy } = setup(); - const oid = new ObjectId(); - const resolvedPermissions = await service.resolvePermissions(user, oid); + const id = 'TEST ID'; + const resolvedPermissions = await service.resolvePermissions(user.id, id); expect(resolvedPermissions).toEqual([]); - expect(spy).toBeCalledWith(oid); + expect(spy).toBeCalledWith(id); }); }); }); diff --git a/apps/server/src/modules/authorization/permission-context/service/permission-context.service.ts b/apps/server/src/modules/authorization/permission-context/service/permission-context.service.ts index 49244261b27..198e9aade78 100644 --- a/apps/server/src/modules/authorization/permission-context/service/permission-context.service.ts +++ b/apps/server/src/modules/authorization/permission-context/service/permission-context.service.ts @@ -1,17 +1,15 @@ import { Injectable } from '@nestjs/common'; -import { User, Permission } from '@shared/domain'; +import { User, Permission, EntityId } from '@shared/domain'; import { UserRepo, PermissionContextRepo } from '@shared/repo'; -import { ObjectId } from 'bson'; @Injectable() export class PermissionContextService { constructor(private readonly userRepo: UserRepo, private readonly permissionContextRepo: PermissionContextRepo) {} - public async resolvePermissions(user: User, contextReference: ObjectId): Promise { + public async resolvePermissions(userId: User['id'], contextReference: EntityId): Promise { // NOTE: the contextReference is the _id to a collection that needs authorization - const permissionCtxEntities = await this.permissionContextRepo.findByContextReference(contextReference); - - const permissions = permissionCtxEntities.resolvedPermissions(user); + const permissionCtxEntity = await this.permissionContextRepo.findByContextReference(contextReference); + const permissions = permissionCtxEntity.resolvedPermissions(userId); return permissions; } diff --git a/apps/server/src/modules/board/uc/board.uc.ts b/apps/server/src/modules/board/uc/board.uc.ts index 62559bd966f..6b2b9130b03 100644 --- a/apps/server/src/modules/board/uc/board.uc.ts +++ b/apps/server/src/modules/board/uc/board.uc.ts @@ -1,8 +1,8 @@ -import { forwardRef, Inject, Injectable } from '@nestjs/common'; -import { BoardExternalReference, Column, ColumnBoard, EntityId } from '@shared/domain'; +import { forwardRef, Inject, Injectable, UnauthorizedException } from '@nestjs/common'; +import { BoardExternalReference, Column, ColumnBoard, EntityId, Permission } from '@shared/domain'; import { LegacyLogger } from '@src/core/logger'; import { AuthorizationService } from '@modules/authorization/domain'; -import { Action } from '@modules/authorization'; +import { Action, PermissionContextService } from '@modules/authorization'; import { CardService, ColumnBoardService, ColumnService } from '../service'; import { BoardDoAuthorizableService } from '../service/board-do-authorizable.service'; import { BaseUc } from './base.uc'; @@ -16,17 +16,31 @@ export class BoardUc extends BaseUc { private readonly cardService: CardService, private readonly columnBoardService: ColumnBoardService, private readonly columnService: ColumnService, - private readonly logger: LegacyLogger + private readonly logger: LegacyLogger, + protected readonly permissionContextService: PermissionContextService ) { super(authorizationService, boardDoAuthorizableService); this.logger.setContext(BoardUc.name); } + private async pocCheckPermission( + userId: EntityId, + contextReference: EntityId, + permissionsToContain: Permission[] + ): Promise { + const permissions = await this.permissionContextService.resolvePermissions(userId, contextReference); + const hasPermission = permissionsToContain.every((permission) => permissions.includes(permission)); + if (!hasPermission) { + throw new UnauthorizedException(); + } + } + async findBoard(userId: EntityId, boardId: EntityId): Promise { this.logger.debug({ action: 'findBoard', userId, boardId }); + await this.pocCheckPermission(userId, boardId, [Permission.BOARD_READ]); const board = await this.columnBoardService.findById(boardId); - await this.checkPermission(userId, board, Action.read); + // await this.checkPermission(userId, board, Action.read); return board; } @@ -34,8 +48,9 @@ export class BoardUc extends BaseUc { async findBoardContext(userId: EntityId, boardId: EntityId): Promise { this.logger.debug({ action: 'findBoardContext', userId, boardId }); + await this.pocCheckPermission(userId, boardId, [Permission.BOARD_READ]); const board = await this.columnBoardService.findById(boardId); - await this.checkPermission(userId, board, Action.read); + // await this.checkPermission(userId, board, Action.read); return board.context; } diff --git a/apps/server/src/shared/domain/entity/all-entities.ts b/apps/server/src/shared/domain/entity/all-entities.ts index 9dc33c55b78..2f9563bec6a 100644 --- a/apps/server/src/shared/domain/entity/all-entities.ts +++ b/apps/server/src/shared/domain/entity/all-entities.ts @@ -46,6 +46,7 @@ import { TeamEntity, TeamUserEntity } from './team.entity'; import { UserLoginMigrationEntity } from './user-login-migration.entity'; import { User } from './user.entity'; import { VideoConference } from './video-conference.entity'; +import { PermissionContextEntity } from './permission-context.entity'; export const ALL_ENTITIES = [ Account, @@ -100,4 +101,5 @@ export const ALL_ENTITIES = [ UserLoginMigrationEntity, VideoConference, GroupEntity, + PermissionContextEntity, ]; diff --git a/apps/server/src/shared/domain/entity/permission-context.entity.spec.ts b/apps/server/src/shared/domain/entity/permission-context.entity.spec.ts index ba2c87f247e..283e9726909 100644 --- a/apps/server/src/shared/domain/entity/permission-context.entity.spec.ts +++ b/apps/server/src/shared/domain/entity/permission-context.entity.spec.ts @@ -43,7 +43,7 @@ describe('PermissionContextEntity Entity', () => { it('should resolve nested permissions', () => { const { user, permissionContext } = setup(); - const resolvedPermissions = permissionContext.resolvedPermissions(user); + const resolvedPermissions = permissionContext.resolvedPermissions(user.id); expect(resolvedPermissions.sort()).toEqual([Permission.ADD_SCHOOL_MEMBERS, Permission.ACCOUNT_EDIT].sort()); }); }); diff --git a/apps/server/src/shared/domain/entity/permission-context.entity.ts b/apps/server/src/shared/domain/entity/permission-context.entity.ts index c3402036d2b..ac55a613b0d 100644 --- a/apps/server/src/shared/domain/entity/permission-context.entity.ts +++ b/apps/server/src/shared/domain/entity/permission-context.entity.ts @@ -1,5 +1,5 @@ -import { Embeddable, Entity, Index, ManyToOne, Property } from '@mikro-orm/core'; -import { ObjectId } from 'bson'; +import { Embeddable, Entity, Index, ManyToOne, Property, Unique } from '@mikro-orm/core'; +import { ObjectId } from '@mikro-orm/mongodb'; import { BaseEntityWithTimestamps } from './base.entity'; import { Permission } from '../interface'; import { User } from './user.entity'; @@ -28,11 +28,10 @@ export interface IPermissionContextProperties { parentContext: PermissionContextEntity | null; } -// TODO: add test @Entity({ tableName: 'permission-context' }) export class PermissionContextEntity extends BaseEntityWithTimestamps { @Property() - @Index() + @Unique() contextReference: ObjectId; @Property() @@ -54,19 +53,19 @@ export class PermissionContextEntity extends BaseEntityWithTimestamps { this.userDelta = props.userDelta ?? new UserDelta([]); } - private resolveUserDelta(user: User): { + private resolveUserDelta(userId: User['id']): { includedPermissions: Permission[]; excludedPermissions: Permission[]; } { - const userDelta = this.userDelta[user.id] ?? { includedPermissions: [], excludedPermissions: [] }; + const userDelta = this.userDelta[userId] ?? { includedPermissions: [], excludedPermissions: [] }; return userDelta; } - public resolvedPermissions(user: User): Permission[] { - const parentPermissions = this.parentContext?.resolvedPermissions(user) ?? []; + public resolvedPermissions(userId: User['id']): Permission[] { + const parentPermissions = this.parentContext?.resolvedPermissions(userId) ?? []; - const userDelta = this.resolveUserDelta(user); + const userDelta = this.resolveUserDelta(userId); const finalPermissions = parentPermissions .concat(userDelta.includedPermissions) diff --git a/apps/server/src/shared/domain/interface/permission.enum.ts b/apps/server/src/shared/domain/interface/permission.enum.ts index c3f880101b5..986e16160a8 100644 --- a/apps/server/src/shared/domain/interface/permission.enum.ts +++ b/apps/server/src/shared/domain/interface/permission.enum.ts @@ -1,4 +1,14 @@ +// NOTE: we should remove enum and replace them with type of string +// to be able to group them and merge them into Permission type +// currently typescript does not allow merging of enums export enum Permission { + /** POC: BOARD PERMISSIONS */ + BOARD_READ = 'BOARD_READ', + BOARD_CREATE = 'BOARD_CREATE', + BOARD_CREATE_COLUMN = 'BOARD_CREATE_COLUMN', + BOARD_DELETE = 'BOARD_DELETE', + BOARD_UPDATE_TITLE = 'BOARD_UPDATE_TITLE', + /** POC END: BOARD PERMISSIONS */ ACCOUNT_CREATE = 'ACCOUNT_CREATE', ACCOUNT_EDIT = 'ACCOUNT_EDIT', ADD_SCHOOL_MEMBERS = 'ADD_SCHOOL_MEMBERS', diff --git a/apps/server/src/shared/repo/permission-context/permission-context.repo.ts b/apps/server/src/shared/repo/permission-context/permission-context.repo.ts index 1c3df2e645e..a13416fc38c 100644 --- a/apps/server/src/shared/repo/permission-context/permission-context.repo.ts +++ b/apps/server/src/shared/repo/permission-context/permission-context.repo.ts @@ -1,6 +1,6 @@ -import { ObjectId } from 'bson'; +import { ObjectId } from '@mikro-orm/mongodb'; import { Injectable } from '@nestjs/common'; -import { PermissionContextEntity } from '@shared/domain'; +import { EntityId, PermissionContextEntity } from '@shared/domain'; import { BaseRepo } from '../base.repo'; // TODO: add test @@ -10,7 +10,7 @@ export class PermissionContextRepo extends BaseRepo { return PermissionContextEntity; } - findByContextReference(contextReference: ObjectId): Promise { - return this._em.findOneOrFail(PermissionContextEntity, { contextReference }); + findByContextReference(contextReference: EntityId): Promise { + return this._em.findOneOrFail(PermissionContextEntity, { contextReference: new ObjectId(contextReference) }); } } diff --git a/apps/server/src/shared/testing/factory/permission-context.factory.ts b/apps/server/src/shared/testing/factory/permission-context.factory.ts index 474cb22f396..02baaa3273f 100644 --- a/apps/server/src/shared/testing/factory/permission-context.factory.ts +++ b/apps/server/src/shared/testing/factory/permission-context.factory.ts @@ -1,6 +1,6 @@ /* istanbul ignore file */ -import { ObjectId } from 'bson'; -import { PermissionContextEntity, IPermissionContextProperties, UserDelta } from '@shared/domain'; +import { ObjectId } from '@mikro-orm/mongodb'; +import { PermissionContextEntity, IPermissionContextProperties, UserDelta, EntityId } from '@shared/domain'; import { DeepPartial } from 'fishery'; import { BaseFactory } from './base.factory'; @@ -17,7 +17,7 @@ class PermissionContextFactory extends BaseFactory = { contextReference }; return this.params(params);