From 56c1cfe7a9a3313c5599f16905f07a7701925585 Mon Sep 17 00:00:00 2001 From: Philipinho <16838612+Philipinho@users.noreply.github.com> Date: Wed, 7 Jan 2026 18:38:29 +0000 Subject: [PATCH] restriction info --- .../core/page/page-permission.controller.ts | 11 ++- .../page/services/page-permission.service.ts | 92 +++++++++++++++++++ 2 files changed, 102 insertions(+), 1 deletion(-) diff --git a/apps/server/src/core/page/page-permission.controller.ts b/apps/server/src/core/page/page-permission.controller.ts index 38dddba1d..956ac2598 100644 --- a/apps/server/src/core/page/page-permission.controller.ts +++ b/apps/server/src/core/page/page-permission.controller.ts @@ -91,7 +91,7 @@ export class PagePermissionController { } @HttpCode(HttpStatus.OK) - @Post('list') + @Post('members') async getPagePermissions( @Body() dto: PageIdDto, @Body() pagination: PaginationOptions, @@ -103,6 +103,15 @@ export class PagePermissionController { pagination, ); } + + @HttpCode(HttpStatus.OK) + @Post('info') + async getPageRestrictionInfo( + @Body() dto: PageIdDto, + @AuthUser() user: User, + ) { + return this.pagePermissionService.getPageRestrictionInfo(dto.pageId, user); + } } function validateMemberIds(dto: { userIds?: string[]; groupIds?: string[] }) { diff --git a/apps/server/src/core/page/services/page-permission.service.ts b/apps/server/src/core/page/services/page-permission.service.ts index 762cc282b..4c2f1866b 100644 --- a/apps/server/src/core/page/services/page-permission.service.ts +++ b/apps/server/src/core/page/services/page-permission.service.ts @@ -26,6 +26,18 @@ import { SpaceCaslSubject, } from '../../casl/interfaces/space-ability.type'; +export type PageRestrictionInfo = { + id: string; + title: string; + hasDirectRestriction: boolean; + hasInheritedRestriction: boolean; + userAccess: { + canView: boolean; + canEdit: boolean; + canManage: boolean; + }; +}; + @Injectable() export class PagePermissionService { constructor( @@ -342,6 +354,86 @@ export class PagePermissionService { ); } + /** + * Get page restriction info for the current user. + * + * Security: User must be a space member. Returns 404 for pages the user cannot view + * to avoid leaking existence of restricted pages. + */ + async getPageRestrictionInfo( + pageId: string, + authUser: User, + ): Promise { + const page = await this.pageRepo.findById(pageId); + if (!page) { + throw new NotFoundException('Page not found'); + } + + const ability = await this.spaceAbility.createForUser( + authUser, + page.spaceId, + ); + + if (ability.cannot(SpaceCaslAction.Read, SpaceCaslSubject.Page)) { + throw new ForbiddenException(); + } + + const [hasDirectRestriction, hasAnyRestriction, canView, canEdit] = + await Promise.all([ + this.pagePermissionRepo.findPageAccessByPageId(pageId).then((r) => !!r), + this.pagePermissionRepo.hasRestrictedAncestor(pageId), + this.canViewPage(authUser.id, pageId), + this.canEditPage(authUser.id, pageId), + ]); + + // Security: return 404 to avoid leaking existence of restricted pages + if (!canView) { + throw new NotFoundException('Page not found'); + } + + const hasInheritedRestriction = hasAnyRestriction && !hasDirectRestriction; + + // Determine if user can manage permissions + const canManage = this.computeCanManage(ability, canEdit, canView); + + return { + id: page.id, + title: page.title, + hasDirectRestriction, + hasInheritedRestriction, + userAccess: { + canView, + canEdit, + canManage, + }, + }; + } + + /** + * Compute if user can manage page permissions based on precomputed access values. + * Mirrors validateWriteAccess logic without throwing. + */ + private computeCanManage( + ability: Awaited>, + canEdit: boolean, + canView: boolean, + ): boolean { + if (ability.cannot(SpaceCaslAction.Edit, SpaceCaslSubject.Page)) { + return false; + } + + if (canEdit) { + return true; + } + + const isSpaceAdmin = ability.can( + SpaceCaslAction.Manage, + SpaceCaslSubject.Page, + ); + + return isSpaceAdmin && canView; + } + async validateLastWriter(pageAccessId: string): Promise { const writerCount = await this.pagePermissionRepo.countWritersByPageAccessId(pageAccessId);