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,
|
allPages,
|
||||||
rootPage.id,
|
rootPage.id,
|
||||||
userId,
|
userId,
|
||||||
|
rootPage.spaceId,
|
||||||
);
|
);
|
||||||
const accessibleIds = new Set(accessiblePages.map((p) => p.id));
|
const accessibleIds = new Set(accessiblePages.map((p) => p.id));
|
||||||
|
|
||||||
@@ -389,6 +390,7 @@ export class PageService {
|
|||||||
allPages,
|
allPages,
|
||||||
rootPage.id,
|
rootPage.id,
|
||||||
authUser.id,
|
authUser.id,
|
||||||
|
rootPage.spaceId,
|
||||||
);
|
);
|
||||||
|
|
||||||
const pageMap = new Map<string, CopyPageMapEntry>();
|
const pageMap = new Map<string, CopyPageMapEntry>();
|
||||||
@@ -683,12 +685,13 @@ export class PageService {
|
|||||||
|
|
||||||
if (result.items.length > 0) {
|
if (result.items.length > 0) {
|
||||||
const pageIds = result.items.map((p) => p.id);
|
const pageIds = result.items.map((p) => p.id);
|
||||||
const accessiblePages =
|
const accessibleIds =
|
||||||
await this.pagePermissionRepo.filterAccessiblePageIdsWithPermissions(
|
await this.pagePermissionRepo.filterAccessiblePageIds({
|
||||||
pageIds,
|
pageIds,
|
||||||
userId,
|
userId,
|
||||||
);
|
spaceId,
|
||||||
const accessibleSet = new Set(accessiblePages.map((p) => p.id));
|
});
|
||||||
|
const accessibleSet = new Set(accessibleIds);
|
||||||
result.items = result.items.filter((p) => accessibleSet.has(p.id));
|
result.items = result.items.filter((p) => accessibleSet.has(p.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -703,12 +706,12 @@ export class PageService {
|
|||||||
|
|
||||||
if (result.items.length > 0) {
|
if (result.items.length > 0) {
|
||||||
const pageIds = result.items.map((p) => p.id);
|
const pageIds = result.items.map((p) => p.id);
|
||||||
const accessiblePages =
|
const accessibleIds =
|
||||||
await this.pagePermissionRepo.filterAccessiblePageIdsWithPermissions(
|
await this.pagePermissionRepo.filterAccessiblePageIds({
|
||||||
pageIds,
|
pageIds,
|
||||||
userId,
|
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));
|
result.items = result.items.filter((p) => accessibleSet.has(p.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -727,12 +730,13 @@ export class PageService {
|
|||||||
|
|
||||||
if (result.items.length > 0) {
|
if (result.items.length > 0) {
|
||||||
const pageIds = result.items.map((p) => p.id);
|
const pageIds = result.items.map((p) => p.id);
|
||||||
const accessiblePages =
|
const accessibleIds =
|
||||||
await this.pagePermissionRepo.filterAccessiblePageIdsWithPermissions(
|
await this.pagePermissionRepo.filterAccessiblePageIds({
|
||||||
pageIds,
|
pageIds,
|
||||||
userId,
|
userId,
|
||||||
);
|
spaceId,
|
||||||
const accessibleSet = new Set(accessiblePages.map((p) => p.id));
|
});
|
||||||
|
const accessibleSet = new Set(accessibleIds);
|
||||||
result.items = result.items.filter((p) => accessibleSet.has(p.id));
|
result.items = result.items.filter((p) => accessibleSet.has(p.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -806,19 +810,18 @@ export class PageService {
|
|||||||
pages: T[],
|
pages: T[],
|
||||||
rootPageId: string,
|
rootPageId: string,
|
||||||
userId: string,
|
userId: string,
|
||||||
|
spaceId?: string,
|
||||||
): Promise<T[]> {
|
): Promise<T[]> {
|
||||||
if (pages.length === 0) return [];
|
if (pages.length === 0) return [];
|
||||||
|
|
||||||
const pageIds = pages.map((p) => p.id);
|
const pageIds = pages.map((p) => p.id);
|
||||||
const accessiblePages =
|
const accessibleIds =
|
||||||
await this.pagePermissionRepo.filterAccessiblePageIdsWithPermissions(
|
await this.pagePermissionRepo.filterAccessiblePageIds({
|
||||||
pageIds,
|
pageIds,
|
||||||
userId,
|
userId,
|
||||||
);
|
spaceId,
|
||||||
const accessibleSet = new Set(accessiblePages.map((p) => p.id));
|
});
|
||||||
|
const accessibleSet = new Set(accessibleIds);
|
||||||
// Build a map for quick lookup
|
|
||||||
const pageMap = new Map(pages.map((p) => [p.id, p]));
|
|
||||||
|
|
||||||
// Prune: include a page only if it's accessible AND its parent chain to root is included
|
// Prune: include a page only if it's accessible AND its parent chain to root is included
|
||||||
const includedIds = new Set<string>();
|
const includedIds = new Set<string>();
|
||||||
|
|||||||
@@ -122,12 +122,13 @@ export class SearchService {
|
|||||||
// Filter results by page-level permissions (if user is authenticated)
|
// Filter results by page-level permissions (if user is authenticated)
|
||||||
if (opts.userId && results.length > 0) {
|
if (opts.userId && results.length > 0) {
|
||||||
const pageIds = results.map((r: any) => r.id);
|
const pageIds = results.map((r: any) => r.id);
|
||||||
const accessiblePages =
|
const accessibleIds =
|
||||||
await this.pagePermissionRepo.filterAccessiblePageIdsWithPermissions(
|
await this.pagePermissionRepo.filterAccessiblePageIds({
|
||||||
pageIds,
|
pageIds,
|
||||||
opts.userId,
|
userId: opts.userId,
|
||||||
);
|
spaceId: searchParams.spaceId,
|
||||||
const accessibleSet = new Set(accessiblePages.map((p) => p.id));
|
});
|
||||||
|
const accessibleSet = new Set(accessibleIds);
|
||||||
results = results.filter((r: any) => accessibleSet.has(r.id));
|
results = results.filter((r: any) => accessibleSet.has(r.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,12 +226,13 @@ export class SearchService {
|
|||||||
// Filter by page-level permissions
|
// Filter by page-level permissions
|
||||||
if (pages.length > 0) {
|
if (pages.length > 0) {
|
||||||
const pageIds = pages.map((p) => p.id);
|
const pageIds = pages.map((p) => p.id);
|
||||||
const accessiblePages =
|
const accessibleIds =
|
||||||
await this.pagePermissionRepo.filterAccessiblePageIdsWithPermissions(
|
await this.pagePermissionRepo.filterAccessiblePageIds({
|
||||||
pageIds,
|
pageIds,
|
||||||
userId,
|
userId,
|
||||||
);
|
spaceId: suggestion?.spaceId,
|
||||||
const accessibleSet = new Set(accessiblePages.map((p) => p.id));
|
});
|
||||||
|
const accessibleSet = new Set(accessibleIds);
|
||||||
pages = pages.filter((p) => accessibleSet.has(p.id));
|
pages = pages.filter((p) => accessibleSet.has(p.id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -612,6 +612,83 @@ export class PagePermissionRepo {
|
|||||||
* Returns page IDs with their permission level (canEdit).
|
* Returns page IDs with their permission level (canEdit).
|
||||||
* Single query implementation for efficiency.
|
* 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(
|
async filterAccessiblePageIdsWithPermissions(
|
||||||
pageIds: string[],
|
pageIds: string[],
|
||||||
userId: string,
|
userId: string,
|
||||||
@@ -645,7 +722,6 @@ export class PagePermissionRepo {
|
|||||||
)
|
)
|
||||||
.selectFrom('pages')
|
.selectFrom('pages')
|
||||||
.select('pages.id')
|
.select('pages.id')
|
||||||
// Check if user lacks writer permission on any restricted ancestor
|
|
||||||
.select((eb) =>
|
.select((eb) =>
|
||||||
eb
|
eb
|
||||||
.case()
|
.case()
|
||||||
@@ -690,7 +766,6 @@ export class PagePermissionRepo {
|
|||||||
.as('canEdit'),
|
.as('canEdit'),
|
||||||
)
|
)
|
||||||
.where(sql<SqlBool>`pages.id = ANY(${pageIds}::uuid[])`)
|
.where(sql<SqlBool>`pages.id = ANY(${pageIds}::uuid[])`)
|
||||||
// Filter: user must have access (any permission on all restricted ancestors)
|
|
||||||
.where(({ not, exists, selectFrom }) =>
|
.where(({ not, exists, selectFrom }) =>
|
||||||
not(
|
not(
|
||||||
exists(
|
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
|
// Filter to only accessible pages if permissions are enforced
|
||||||
if (!ignorePermissions && userId) {
|
if (!ignorePermissions && userId) {
|
||||||
const accessiblePages =
|
pageMentionIds =
|
||||||
await this.pagePermissionRepo.filterAccessiblePageIdsWithPermissions(
|
await this.pagePermissionRepo.filterAccessiblePageIds({
|
||||||
pageMentionIds,
|
pageIds: pageMentionIds,
|
||||||
userId,
|
userId,
|
||||||
);
|
});
|
||||||
pageMentionIds = accessiblePages.map((p) => p.id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const pages =
|
const pages =
|
||||||
@@ -485,20 +484,14 @@ export class ExportService {
|
|||||||
): Promise<Page[]> {
|
): Promise<Page[]> {
|
||||||
if (pages.length === 0) return [];
|
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 pageIds = pages.map((p) => p.id);
|
||||||
const accessiblePages =
|
const accessibleIds =
|
||||||
await this.pagePermissionRepo.filterAccessiblePageIdsWithPermissions(
|
await this.pagePermissionRepo.filterAccessiblePageIds({
|
||||||
pageIds,
|
pageIds,
|
||||||
userId,
|
userId,
|
||||||
);
|
spaceId,
|
||||||
const accessibleSet = new Set(accessiblePages.map((p) => p.id));
|
});
|
||||||
|
const accessibleSet = new Set(accessibleIds);
|
||||||
|
|
||||||
const includedIds = new Set<string>();
|
const includedIds = new Set<string>();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user