diff --git a/apps/server/src/collaboration/extensions/authentication.extension.ts b/apps/server/src/collaboration/extensions/authentication.extension.ts index 852c304..bbe06e8 100644 --- a/apps/server/src/collaboration/extensions/authentication.extension.ts +++ b/apps/server/src/collaboration/extensions/authentication.extension.ts @@ -2,12 +2,18 @@ import { Extension, onAuthenticatePayload } from '@hocuspocus/server'; import { Injectable, UnauthorizedException } from '@nestjs/common'; import { TokenService } from '../../core/auth/services/token.service'; import { UserRepo } from '@docmost/db/repos/user/user.repo'; +import { PageRepo } from '@docmost/db/repos/page/page.repo'; +import { SpaceMemberRepo } from '@docmost/db/repos/space/space-member.repo'; +import { findHighestUserSpaceRole } from '@docmost/db/repos/space/utils'; +import { SpaceRole } from '../../helpers/types/permission'; @Injectable() export class AuthenticationExtension implements Extension { constructor( private tokenService: TokenService, private userRepo: UserRepo, + private pageRepo: PageRepo, + private readonly spaceMemberRepo: SpaceMemberRepo, ) {} async onAuthenticate(data: onAuthenticatePayload) { @@ -30,8 +36,25 @@ export class AuthenticationExtension implements Extension { throw new UnauthorizedException(); } - //TODO: Check if the page exists and verify user permissions for page. - // if all fails, abort connection + const page = await this.pageRepo.findById(documentName); + if (!page) { + throw new UnauthorizedException('Page not found'); + } + + const userSpaceRoles = await this.spaceMemberRepo.getUserSpaceRoles( + user.id, + page.spaceId, + ); + + const userSpaceRole = findHighestUserSpaceRole(userSpaceRoles); + + if (!userSpaceRole) { + throw new UnauthorizedException(); + } + + if (userSpaceRole === SpaceRole.READER) { + data.connection.readOnly = true; + } return { user, diff --git a/apps/server/src/core/casl/abilities/space-ability.factory.ts b/apps/server/src/core/casl/abilities/space-ability.factory.ts index b9db85e..d75c58d 100644 --- a/apps/server/src/core/casl/abilities/space-ability.factory.ts +++ b/apps/server/src/core/casl/abilities/space-ability.factory.ts @@ -46,6 +46,7 @@ function buildSpaceAdminAbility() { ); can(SpaceCaslAction.Manage, SpaceCaslSubject.Settings); can(SpaceCaslAction.Manage, SpaceCaslSubject.Member); + can(SpaceCaslAction.Manage, SpaceCaslSubject.Page); return build(); } @@ -55,6 +56,7 @@ function buildSpaceWriterAbility() { ); can(SpaceCaslAction.Read, SpaceCaslSubject.Settings); can(SpaceCaslAction.Read, SpaceCaslSubject.Member); + can(SpaceCaslAction.Manage, SpaceCaslSubject.Page); return build(); } @@ -64,5 +66,6 @@ function buildSpaceReaderAbility() { ); can(SpaceCaslAction.Read, SpaceCaslSubject.Settings); can(SpaceCaslAction.Read, SpaceCaslSubject.Member); + can(SpaceCaslAction.Read, SpaceCaslSubject.Page); return build(); } diff --git a/apps/server/src/core/casl/interfaces/space-ability.type.ts b/apps/server/src/core/casl/interfaces/space-ability.type.ts index e5eca73..407500b 100644 --- a/apps/server/src/core/casl/interfaces/space-ability.type.ts +++ b/apps/server/src/core/casl/interfaces/space-ability.type.ts @@ -8,8 +8,10 @@ export enum SpaceCaslAction { export enum SpaceCaslSubject { Settings = 'settings', Member = 'member', + Page = 'page', } export type SpaceAbility = | [SpaceCaslAction, SpaceCaslSubject.Settings] - | [SpaceCaslAction, SpaceCaslSubject.Member]; + | [SpaceCaslAction, SpaceCaslSubject.Member] + | [SpaceCaslAction, SpaceCaslSubject.Page]; diff --git a/apps/server/src/core/page/page.controller.ts b/apps/server/src/core/page/page.controller.ts index 84a088d..2c86c08 100644 --- a/apps/server/src/core/page/page.controller.ts +++ b/apps/server/src/core/page/page.controller.ts @@ -5,6 +5,8 @@ import { HttpCode, HttpStatus, UseGuards, + ForbiddenException, + NotFoundException, } from '@nestjs/common'; import { PageService } from './services/page.service'; import { CreatePageDto } from './dto/create-page.dto'; @@ -18,19 +20,38 @@ import { JwtAuthGuard } from '../../guards/jwt-auth.guard'; import { PaginationOptions } from '@docmost/db/pagination/pagination-options'; import { User, Workspace } from '@docmost/db/types/entity.types'; import { SidebarPageDto } from './dto/sidebar-page.dto'; +import { + SpaceCaslAction, + SpaceCaslSubject, +} from '../casl/interfaces/space-ability.type'; +import SpaceAbilityFactory from '../casl/abilities/space-ability.factory'; +import { PageRepo } from '@docmost/db/repos/page/page.repo'; @UseGuards(JwtAuthGuard) @Controller('pages') export class PageController { constructor( private readonly pageService: PageService, + private readonly pageRepo: PageRepo, private readonly pageHistoryService: PageHistoryService, + private readonly spaceAbility: SpaceAbilityFactory, ) {} @HttpCode(HttpStatus.OK) @Post('/info') - async getPage(@Body() pageIdDto: PageIdDto) { - return this.pageService.findById(pageIdDto.pageId); + async getPage(@Body() pageIdDto: PageIdDto, @AuthUser() user: User) { + const page = await this.pageRepo.findById(pageIdDto.pageId); + + if (!page) { + throw new NotFoundException('Page not found'); + } + + const ability = await this.spaceAbility.createForUser(user, page.spaceId); + if (ability.cannot(SpaceCaslAction.Read, SpaceCaslSubject.Page)) { + throw new ForbiddenException(); + } + + return page; } @HttpCode(HttpStatus.OK) @@ -40,12 +61,31 @@ export class PageController { @AuthUser() user: User, @AuthWorkspace() workspace: Workspace, ) { + const ability = await this.spaceAbility.createForUser( + user, + createPageDto.spaceId, + ); + if (ability.cannot(SpaceCaslAction.Create, SpaceCaslSubject.Page)) { + throw new ForbiddenException(); + } + return this.pageService.create(user.id, workspace.id, createPageDto); } @HttpCode(HttpStatus.OK) @Post('update') async update(@Body() updatePageDto: UpdatePageDto, @AuthUser() user: User) { + const page = await this.pageRepo.findById(updatePageDto.pageId); + + if (!page) { + throw new NotFoundException('Page not found'); + } + + const ability = await this.spaceAbility.createForUser(user, page.spaceId); + if (ability.cannot(SpaceCaslAction.Edit, SpaceCaslSubject.Page)) { + throw new ForbiddenException(); + } + return this.pageService.update( updatePageDto.pageId, updatePageDto, @@ -55,7 +95,17 @@ export class PageController { @HttpCode(HttpStatus.OK) @Post('delete') - async delete(@Body() pageIdDto: PageIdDto) { + async delete(@Body() pageIdDto: PageIdDto, @AuthUser() user: User) { + const page = await this.pageRepo.findById(pageIdDto.pageId); + + if (!page) { + throw new NotFoundException('Page not found'); + } + + const ability = await this.spaceAbility.createForUser(user, page.spaceId); + if (ability.cannot(SpaceCaslAction.Read, SpaceCaslSubject.Page)) { + throw new ForbiddenException(); + } await this.pageService.forceDelete(pageIdDto.pageId); } @@ -70,7 +120,15 @@ export class PageController { async getRecentSpacePages( @Body() spaceIdDto: SpaceIdDto, @Body() pagination: PaginationOptions, + @AuthUser() user: User, ) { + const ability = await this.spaceAbility.createForUser( + user, + spaceIdDto.spaceId, + ); + if (ability.cannot(SpaceCaslAction.Read, SpaceCaslSubject.Page)) { + throw new ForbiddenException(); + } return this.pageService.getRecentSpacePages(spaceIdDto.spaceId, pagination); } @@ -80,14 +138,33 @@ export class PageController { async getPageHistory( @Body() dto: PageIdDto, @Body() pagination: PaginationOptions, + @AuthUser() user: User, ) { + const page = await this.pageRepo.findById(dto.pageId); + const ability = await this.spaceAbility.createForUser(user, page.spaceId); + if (ability.cannot(SpaceCaslAction.Read, SpaceCaslSubject.Page)) { + throw new ForbiddenException(); + } + return this.pageHistoryService.findHistoryByPageId(dto.pageId, pagination); } @HttpCode(HttpStatus.OK) - @Post('/history/details') - async getPageHistoryInfo(@Body() dto: PageHistoryIdDto) { - return this.pageHistoryService.findById(dto.historyId); + @Post('/history/info') + async getPageHistoryInfo( + @Body() dto: PageHistoryIdDto, + @AuthUser() user: User, + ) { + const history = await this.pageHistoryService.findById(dto.historyId); + + const ability = await this.spaceAbility.createForUser( + user, + history.spaceId, + ); + if (ability.cannot(SpaceCaslAction.Read, SpaceCaslSubject.Page)) { + throw new ForbiddenException(); + } + return history; } @HttpCode(HttpStatus.OK) @@ -95,19 +172,43 @@ export class PageController { async getSidebarPages( @Body() dto: SidebarPageDto, @Body() pagination: PaginationOptions, + @AuthUser() user: User, ) { + const ability = await this.spaceAbility.createForUser(user, dto.spaceId); + console.log(ability.can(SpaceCaslAction.Read, SpaceCaslSubject.Page)); + if (ability.cannot(SpaceCaslAction.Read, SpaceCaslSubject.Page)) { + throw new ForbiddenException(); + } return this.pageService.getSidebarPages(dto, pagination); } @HttpCode(HttpStatus.OK) @Post('move') - async movePage(@Body() movePageDto: MovePageDto) { - return this.pageService.movePage(movePageDto); + async movePage(@Body() dto: MovePageDto, @AuthUser() user: User) { + const movedPage = await this.pageRepo.findById(dto.pageId); + if (!movedPage) { + throw new NotFoundException('Moved page not found'); + } + + const ability = await this.spaceAbility.createForUser( + user, + movedPage.spaceId, + ); + if (ability.cannot(SpaceCaslAction.Edit, SpaceCaslSubject.Page)) { + throw new ForbiddenException(); + } + + return this.pageService.movePage(dto, movedPage); } @HttpCode(HttpStatus.OK) @Post('/breadcrumbs') - async getPageBreadcrumbs(@Body() dto: PageIdDto) { + async getPageBreadcrumbs(@Body() dto: PageIdDto, @AuthUser() user: User) { + const page = await this.pageRepo.findById(dto.pageId); + const ability = await this.spaceAbility.createForUser(user, page.spaceId); + if (ability.cannot(SpaceCaslAction.Read, SpaceCaslSubject.Page)) { + throw new ForbiddenException(); + } return this.pageService.getPageBreadCrumbs(dto.pageId); } } diff --git a/apps/server/src/core/page/services/page.service.ts b/apps/server/src/core/page/services/page.service.ts index 6db5af4..24ddef6 100644 --- a/apps/server/src/core/page/services/page.service.ts +++ b/apps/server/src/core/page/services/page.service.ts @@ -42,12 +42,12 @@ export class PageService { ): Promise { // check if parent page exists if (createPageDto.parentPageId) { - // TODO: make sure parent page belongs to same space and user has permissions - // make sure user has permission to parent. const parentPage = await this.pageRepo.findById( createPageDto.parentPageId, ); - if (!parentPage) throw new NotFoundException('Parent page not found'); + + if (!parentPage || parentPage.spaceId !== createPageDto.spaceId) + throw new NotFoundException('Parent page not found'); } let pagePosition: string; @@ -59,7 +59,6 @@ export class PageService { .orderBy('position', 'desc') .limit(1); - // todo: simplify code if (createPageDto.parentPageId) { // check for children of this page const lastPage = await lastPageQuery @@ -186,7 +185,7 @@ export class PageService { return result; } - async movePage(dto: MovePageDto) { + async movePage(dto: MovePageDto, movedPage: Page) { // validate position value by attempting to generate a key try { generateJitteredKeyBetween(dto.position, null); @@ -194,9 +193,6 @@ export class PageService { throw new BadRequestException('Invalid move position'); } - const movedPage = await this.pageRepo.findById(dto.pageId); - if (!movedPage) throw new NotFoundException('Moved page not found'); - let parentPageId: string; if (movedPage.parentPageId === dto.parentPageId) { parentPageId = undefined; @@ -204,7 +200,9 @@ export class PageService { // changing the page's parent if (dto.parentPageId) { const parentPage = await this.pageRepo.findById(dto.parentPageId); - if (!parentPage) throw new NotFoundException('Parent page not found'); + if (!parentPage || parentPage.spaceId !== movedPage.spaceId) { + throw new NotFoundException('Parent page not found'); + } } parentPageId = dto.parentPageId; } @@ -216,10 +214,6 @@ export class PageService { }, dto.pageId, ); - - // TODO - // check for duplicates? - // permissions } async getPageBreadCrumbs(childPageId: string) { diff --git a/apps/server/src/kysely/repos/space/space-member.repo.ts b/apps/server/src/kysely/repos/space/space-member.repo.ts index b5a4dbe..503f8de 100644 --- a/apps/server/src/kysely/repos/space/space-member.repo.ts +++ b/apps/server/src/kysely/repos/space/space-member.repo.ts @@ -179,7 +179,7 @@ export class SpaceMemberRepo { ) .execute(); - if (roles.length < 1) { + if (!roles || roles.length === 0) { return undefined; } return roles;