server: page permissions

This commit is contained in:
Philipinho
2024-04-22 02:25:03 +01:00
parent 3462c7fdbc
commit 28ec542ed6
6 changed files with 149 additions and 26 deletions

View File

@ -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,

View File

@ -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();
}

View File

@ -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];

View File

@ -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);
}
}

View File

@ -42,12 +42,12 @@ export class PageService {
): Promise<Page> {
// 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) {

View File

@ -179,7 +179,7 @@ export class SpaceMemberRepo {
)
.execute();
if (roles.length < 1) {
if (!roles || roles.length === 0) {
return undefined;
}
return roles;