diff --git a/apps/server/src/core/share/share.controller.ts b/apps/server/src/core/share/share.controller.ts index cce95921..4b22d112 100644 --- a/apps/server/src/core/share/share.controller.ts +++ b/apps/server/src/core/share/share.controller.ts @@ -26,6 +26,7 @@ import { UpdateShareDto, } from './dto/share.dto'; import { PageRepo } from '@docmost/db/repos/page/page.repo'; +import { PagePermissionRepo } from '@docmost/db/repos/page/page-permission.repo'; import { PageAccessService } from '../page-access/page-access.service'; import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; import { Public } from '../../common/decorators/public.decorator'; @@ -42,6 +43,7 @@ export class ShareController { private readonly spaceAbility: SpaceAbilityFactory, private readonly shareRepo: ShareRepo, private readonly pageRepo: PageRepo, + private readonly pagePermissionRepo: PagePermissionRepo, private readonly pageAccessService: PageAccessService, private readonly environmentService: EnvironmentService, ) {} @@ -126,8 +128,17 @@ export class ShareController { } // User must be able to edit the page to create a share + //TODO: i dont think this is neccessary if we prevent restricted pages from getting shared + // rather, use space level permission and workspace/space level sharing restriction await this.pageAccessService.validateCanEdit(page, user); + // Prevent sharing restricted pages + const isRestricted = + await this.pagePermissionRepo.hasRestrictedAncestor(page.id); + if (isRestricted) { + throw new BadRequestException('Cannot share a restricted page'); + } + return this.shareService.createShare({ page, authUserId: user.id, diff --git a/apps/server/src/core/share/share.service.ts b/apps/server/src/core/share/share.service.ts index d065f860..82ae401e 100644 --- a/apps/server/src/core/share/share.service.ts +++ b/apps/server/src/core/share/share.service.ts @@ -19,6 +19,7 @@ import { } from '../../common/helpers/prosemirror/utils'; import { Node } from '@tiptap/pm/model'; import { ShareRepo } from '@docmost/db/repos/share/share.repo'; +import { PagePermissionRepo } from '@docmost/db/repos/page/page-permission.repo'; import { updateAttachmentAttr } from './share.util'; import { Page } from '@docmost/db/types/entity.types'; import { validate as isValidUUID } from 'uuid'; @@ -31,6 +32,7 @@ export class ShareService { constructor( private readonly shareRepo: ShareRepo, private readonly pageRepo: PageRepo, + private readonly pagePermissionRepo: PagePermissionRepo, @InjectKysely() private readonly db: KyselyDB, private readonly tokenService: TokenService, ) {} @@ -46,7 +48,13 @@ export class ShareService { includeContent: false, }); - return { share, pageTree: pageList }; + // Filter out restricted pages and their descendants + const restrictedIds = + await this.pagePermissionRepo.getRestrictedSubtreeIds(share.pageId); + const restrictedSet = new Set(restrictedIds); + const filteredPages = pageList.filter((page) => !restrictedSet.has(page.id)); + + return { share, pageTree: filteredPages }; } else { return { share, pageTree: [] }; } @@ -112,6 +120,13 @@ export class ShareService { throw new NotFoundException('Shared page not found'); } + // Block access to restricted pages + const isRestricted = + await this.pagePermissionRepo.hasRestrictedAncestor(page.id); + if (isRestricted) { + throw new NotFoundException('Shared page not found'); + } + page.content = await this.updatePublicAttachments(page); return { page, share }; diff --git a/apps/server/src/database/repos/page/page-permission.repo.ts b/apps/server/src/database/repos/page/page-permission.repo.ts index 07c5c724..e63e137f 100644 --- a/apps/server/src/database/repos/page/page-permission.repo.ts +++ b/apps/server/src/database/repos/page/page-permission.repo.ts @@ -624,4 +624,31 @@ export class PagePermissionRepo { return results.map((r) => r.parentPageId); } + + /** + * Get all page IDs within a subtree that are restricted OR are descendants of restricted pages. + * Used to filter pages from public shares - if a page is restricted, it and all its + * children should be hidden. + */ + async getRestrictedSubtreeIds(rootPageId: string): Promise { + const results = await this.db + .selectFrom('pageHierarchy as subtree') + .where('subtree.ancestorId', '=', rootPageId) + .innerJoin( + (eb) => + eb + .selectFrom('pageHierarchy as inner') + .innerJoin('pageAccess', 'pageAccess.pageId', 'inner.ancestorId') + .select('inner.descendantId as restrictedDescendant') + .distinct() + .as('restricted'), + (join) => + join.onRef('restricted.restrictedDescendant', '=', 'subtree.descendantId'), + ) + .select('subtree.descendantId') + .distinct() + .execute(); + + return results.map((r) => r.descendantId); + } }