mirror of
https://github.com/docmost/docmost.git
synced 2026-05-07 06:23:06 +08:00
better permissions filtering
This commit is contained in:
@@ -286,6 +286,7 @@ export class PageService {
|
||||
allPages,
|
||||
rootPage.id,
|
||||
userId,
|
||||
rootPage.spaceId,
|
||||
);
|
||||
const accessibleIds = new Set(accessiblePages.map((p) => p.id));
|
||||
|
||||
@@ -389,6 +390,7 @@ export class PageService {
|
||||
allPages,
|
||||
rootPage.id,
|
||||
authUser.id,
|
||||
rootPage.spaceId,
|
||||
);
|
||||
|
||||
const pageMap = new Map<string, CopyPageMapEntry>();
|
||||
@@ -683,12 +685,13 @@ export class PageService {
|
||||
|
||||
if (result.items.length > 0) {
|
||||
const pageIds = result.items.map((p) => p.id);
|
||||
const accessiblePages =
|
||||
await this.pagePermissionRepo.filterAccessiblePageIdsWithPermissions(
|
||||
const accessibleIds =
|
||||
await this.pagePermissionRepo.filterAccessiblePageIds({
|
||||
pageIds,
|
||||
userId,
|
||||
);
|
||||
const accessibleSet = new Set(accessiblePages.map((p) => p.id));
|
||||
spaceId,
|
||||
});
|
||||
const accessibleSet = new Set(accessibleIds);
|
||||
result.items = result.items.filter((p) => accessibleSet.has(p.id));
|
||||
}
|
||||
|
||||
@@ -703,12 +706,12 @@ export class PageService {
|
||||
|
||||
if (result.items.length > 0) {
|
||||
const pageIds = result.items.map((p) => p.id);
|
||||
const accessiblePages =
|
||||
await this.pagePermissionRepo.filterAccessiblePageIdsWithPermissions(
|
||||
const accessibleIds =
|
||||
await this.pagePermissionRepo.filterAccessiblePageIds({
|
||||
pageIds,
|
||||
userId,
|
||||
);
|
||||
const accessibleSet = new Set(accessiblePages.map((p) => p.id));
|
||||
});
|
||||
const accessibleSet = new Set(accessibleIds);
|
||||
result.items = result.items.filter((p) => accessibleSet.has(p.id));
|
||||
}
|
||||
|
||||
@@ -727,12 +730,13 @@ export class PageService {
|
||||
|
||||
if (result.items.length > 0) {
|
||||
const pageIds = result.items.map((p) => p.id);
|
||||
const accessiblePages =
|
||||
await this.pagePermissionRepo.filterAccessiblePageIdsWithPermissions(
|
||||
const accessibleIds =
|
||||
await this.pagePermissionRepo.filterAccessiblePageIds({
|
||||
pageIds,
|
||||
userId,
|
||||
);
|
||||
const accessibleSet = new Set(accessiblePages.map((p) => p.id));
|
||||
spaceId,
|
||||
});
|
||||
const accessibleSet = new Set(accessibleIds);
|
||||
result.items = result.items.filter((p) => accessibleSet.has(p.id));
|
||||
}
|
||||
|
||||
@@ -806,19 +810,18 @@ export class PageService {
|
||||
pages: T[],
|
||||
rootPageId: string,
|
||||
userId: string,
|
||||
spaceId?: string,
|
||||
): Promise<T[]> {
|
||||
if (pages.length === 0) return [];
|
||||
|
||||
const pageIds = pages.map((p) => p.id);
|
||||
const accessiblePages =
|
||||
await this.pagePermissionRepo.filterAccessiblePageIdsWithPermissions(
|
||||
const accessibleIds =
|
||||
await this.pagePermissionRepo.filterAccessiblePageIds({
|
||||
pageIds,
|
||||
userId,
|
||||
);
|
||||
const accessibleSet = new Set(accessiblePages.map((p) => p.id));
|
||||
|
||||
// Build a map for quick lookup
|
||||
const pageMap = new Map(pages.map((p) => [p.id, p]));
|
||||
spaceId,
|
||||
});
|
||||
const accessibleSet = new Set(accessibleIds);
|
||||
|
||||
// Prune: include a page only if it's accessible AND its parent chain to root is included
|
||||
const includedIds = new Set<string>();
|
||||
|
||||
@@ -122,12 +122,13 @@ export class SearchService {
|
||||
// Filter results by page-level permissions (if user is authenticated)
|
||||
if (opts.userId && results.length > 0) {
|
||||
const pageIds = results.map((r: any) => r.id);
|
||||
const accessiblePages =
|
||||
await this.pagePermissionRepo.filterAccessiblePageIdsWithPermissions(
|
||||
const accessibleIds =
|
||||
await this.pagePermissionRepo.filterAccessiblePageIds({
|
||||
pageIds,
|
||||
opts.userId,
|
||||
);
|
||||
const accessibleSet = new Set(accessiblePages.map((p) => p.id));
|
||||
userId: opts.userId,
|
||||
spaceId: searchParams.spaceId,
|
||||
});
|
||||
const accessibleSet = new Set(accessibleIds);
|
||||
results = results.filter((r: any) => accessibleSet.has(r.id));
|
||||
}
|
||||
|
||||
@@ -225,12 +226,13 @@ export class SearchService {
|
||||
// Filter by page-level permissions
|
||||
if (pages.length > 0) {
|
||||
const pageIds = pages.map((p) => p.id);
|
||||
const accessiblePages =
|
||||
await this.pagePermissionRepo.filterAccessiblePageIdsWithPermissions(
|
||||
const accessibleIds =
|
||||
await this.pagePermissionRepo.filterAccessiblePageIds({
|
||||
pageIds,
|
||||
userId,
|
||||
);
|
||||
const accessibleSet = new Set(accessiblePages.map((p) => p.id));
|
||||
spaceId: suggestion?.spaceId,
|
||||
});
|
||||
const accessibleSet = new Set(accessibleIds);
|
||||
pages = pages.filter((p) => accessibleSet.has(p.id));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -612,6 +612,83 @@ export class PagePermissionRepo {
|
||||
* Returns page IDs with their permission level (canEdit).
|
||||
* Single query implementation for efficiency.
|
||||
*/
|
||||
async filterAccessiblePageIds(opts: {
|
||||
pageIds: string[];
|
||||
userId: string;
|
||||
spaceId?: string;
|
||||
}): Promise<string[]> {
|
||||
const { pageIds, userId, spaceId } = opts;
|
||||
if (pageIds.length === 0) return [];
|
||||
|
||||
if (spaceId) {
|
||||
const hasRestrictions = await this.hasRestrictedPagesInSpace(spaceId);
|
||||
if (!hasRestrictions) {
|
||||
return pageIds;
|
||||
}
|
||||
}
|
||||
|
||||
const results = await this.db
|
||||
.withRecursive('allAncestors', (qb) =>
|
||||
qb
|
||||
.selectFrom('pages')
|
||||
.select([
|
||||
'pages.id as pageId',
|
||||
'pages.id as ancestorId',
|
||||
'pages.parentPageId',
|
||||
])
|
||||
.where(sql<SqlBool>`pages.id = ANY(${pageIds}::uuid[])`)
|
||||
.unionAll((eb) =>
|
||||
eb
|
||||
.selectFrom('pages')
|
||||
.innerJoin(
|
||||
'allAncestors',
|
||||
'allAncestors.parentPageId',
|
||||
'pages.id',
|
||||
)
|
||||
.select([
|
||||
'allAncestors.pageId',
|
||||
'pages.id as ancestorId',
|
||||
'pages.parentPageId',
|
||||
]),
|
||||
),
|
||||
)
|
||||
.selectFrom('pages')
|
||||
.select('pages.id')
|
||||
.where(sql<SqlBool>`pages.id = ANY(${pageIds}::uuid[])`)
|
||||
.where(({ not, exists, selectFrom }) =>
|
||||
not(
|
||||
exists(
|
||||
selectFrom('allAncestors')
|
||||
.innerJoin(
|
||||
'pageAccess',
|
||||
'pageAccess.pageId',
|
||||
'allAncestors.ancestorId',
|
||||
)
|
||||
.leftJoin('pagePermissions', (join) =>
|
||||
join
|
||||
.onRef('pagePermissions.pageAccessId', '=', 'pageAccess.id')
|
||||
.on((eb) =>
|
||||
eb.or([
|
||||
eb('pagePermissions.userId', '=', userId),
|
||||
eb(
|
||||
'pagePermissions.groupId',
|
||||
'in',
|
||||
this.userGroupIdsSubquery(eb, userId),
|
||||
),
|
||||
]),
|
||||
),
|
||||
)
|
||||
.select('pageAccess.pageId')
|
||||
.whereRef('allAncestors.pageId', '=', 'pages.id')
|
||||
.where('pagePermissions.id', 'is', null),
|
||||
),
|
||||
),
|
||||
)
|
||||
.execute();
|
||||
|
||||
return results.map((r) => r.id);
|
||||
}
|
||||
|
||||
async filterAccessiblePageIdsWithPermissions(
|
||||
pageIds: string[],
|
||||
userId: string,
|
||||
@@ -645,7 +722,6 @@ export class PagePermissionRepo {
|
||||
)
|
||||
.selectFrom('pages')
|
||||
.select('pages.id')
|
||||
// Check if user lacks writer permission on any restricted ancestor
|
||||
.select((eb) =>
|
||||
eb
|
||||
.case()
|
||||
@@ -690,7 +766,6 @@ export class PagePermissionRepo {
|
||||
.as('canEdit'),
|
||||
)
|
||||
.where(sql<SqlBool>`pages.id = ANY(${pageIds}::uuid[])`)
|
||||
// Filter: user must have access (any permission on all restricted ancestors)
|
||||
.where(({ not, exists, selectFrom }) =>
|
||||
not(
|
||||
exists(
|
||||
|
||||
+1
-1
Submodule apps/server/src/ee updated: 05d3f55c78...9da4770025
@@ -380,12 +380,11 @@ export class ExportService {
|
||||
|
||||
// Filter to only accessible pages if permissions are enforced
|
||||
if (!ignorePermissions && userId) {
|
||||
const accessiblePages =
|
||||
await this.pagePermissionRepo.filterAccessiblePageIdsWithPermissions(
|
||||
pageMentionIds,
|
||||
pageMentionIds =
|
||||
await this.pagePermissionRepo.filterAccessiblePageIds({
|
||||
pageIds: pageMentionIds,
|
||||
userId,
|
||||
);
|
||||
pageMentionIds = accessiblePages.map((p) => p.id);
|
||||
});
|
||||
}
|
||||
|
||||
const pages =
|
||||
@@ -485,20 +484,14 @@ export class ExportService {
|
||||
): Promise<Page[]> {
|
||||
if (pages.length === 0) return [];
|
||||
|
||||
// skip heavy filtering if no restrictions exist in this space
|
||||
const hasRestrictions =
|
||||
await this.pagePermissionRepo.hasRestrictedPagesInSpace(spaceId);
|
||||
if (!hasRestrictions) {
|
||||
return pages;
|
||||
}
|
||||
|
||||
const pageIds = pages.map((p) => p.id);
|
||||
const accessiblePages =
|
||||
await this.pagePermissionRepo.filterAccessiblePageIdsWithPermissions(
|
||||
const accessibleIds =
|
||||
await this.pagePermissionRepo.filterAccessiblePageIds({
|
||||
pageIds,
|
||||
userId,
|
||||
);
|
||||
const accessibleSet = new Set(accessiblePages.map((p) => p.id));
|
||||
spaceId,
|
||||
});
|
||||
const accessibleSet = new Set(accessibleIds);
|
||||
|
||||
const includedIds = new Set<string>();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user