Fix permission - WIP

This commit is contained in:
Philipinho
2025-12-23 23:05:04 +00:00
parent 68a838606a
commit f65726ae26
2 changed files with 167 additions and 39 deletions
@@ -366,20 +366,19 @@ export class PagePermissionService {
} }
} }
/**
* Check if user has writer permission on ALL restricted ancestors of a page.
* Used for permission management operations.
*/
async hasWritePermission(userId: string, pageId: string): Promise<boolean> { async hasWritePermission(userId: string, pageId: string): Promise<boolean> {
const restrictedAncestor = const hasRestriction =
await this.pagePermissionRepo.findRestrictedAncestor(pageId); await this.pagePermissionRepo.hasRestrictedAncestor(pageId);
if (!restrictedAncestor) { if (!hasRestriction) {
return false; return false; // no restrictions, defer to space permissions
} }
const permission = await this.pagePermissionRepo.getUserPagePermission( return this.pagePermissionRepo.canUserEditPage(userId, pageId);
userId,
restrictedAncestor.pageId,
);
return permission?.role === PagePermissionRole.WRITER;
} }
async hasPageAccess(pageId: string): Promise<boolean> { async hasPageAccess(pageId: string): Promise<boolean> {
@@ -402,46 +401,34 @@ export class PagePermissionService {
/** /**
* Check if user can view a page. * Check if user can view a page.
* User must have permission (reader or writer) on EVERY restricted ancestor.
* Returns true if: * Returns true if:
* - Page has no restricted ancestor: fall back to space permission * - No ancestors are restricted (defer to space permission)
* - Page has restricted ancestor: user has reader or writer permission on that ancestor * - User has permission on all restricted ancestors
*/ */
async canViewPage(userId: string, pageId: string): Promise<boolean> { async canViewPage(userId: string, pageId: string): Promise<boolean> {
const restrictedAncestor = return this.pagePermissionRepo.canUserAccessPage(userId, pageId);
await this.pagePermissionRepo.findRestrictedAncestor(pageId);
if (!restrictedAncestor) {
return true; // no page-level restriction, defer to space permission
}
const permission = await this.pagePermissionRepo.getUserPagePermission(
userId,
restrictedAncestor.pageId,
);
return !!permission; // has any permission (reader or writer)
} }
/** /**
* Check if user can edit a page. * Check if user can edit a page.
* User must have WRITER permission on EVERY restricted ancestor.
* Returns true if: * Returns true if:
* - Page has no restricted ancestor: fall back to space permission * - No ancestors are restricted (defer to space permission)
* - Page has restricted ancestor: user has writer permission on that ancestor * - User has writer permission on all restricted ancestors
*/ */
async canEditPage(userId: string, pageId: string): Promise<boolean> { async canEditPage(userId: string, pageId: string): Promise<boolean> {
const restrictedAncestor = return this.pagePermissionRepo.canUserEditPage(userId, pageId);
await this.pagePermissionRepo.findRestrictedAncestor(pageId); }
if (!restrictedAncestor) { /**
return true; // no page-level restriction, defer to space permission * Filter page IDs to only those the user can access.
} */
async filterAccessiblePages(
const permission = await this.pagePermissionRepo.getUserPagePermission( pageIds: string[],
userId, userId: string,
restrictedAncestor.pageId, ): Promise<string[]> {
); return this.pagePermissionRepo.filterAccessiblePageIds(pageIds, userId);
return permission?.role === PagePermissionRole.WRITER;
} }
/** /**
@@ -267,4 +267,145 @@ export class PagePermissionRepo {
.orderBy('pageHierarchy.depth', 'asc') .orderBy('pageHierarchy.depth', 'asc')
.executeTakeFirst(); .executeTakeFirst();
} }
/**
* Check if user can access a page by verifying they have permission on ALL restricted ancestors.
* Returns true if:
* - No ancestors are restricted, OR
* - User has permission (reader or writer) on every restricted ancestor
*/
async canUserAccessPage(userId: string, pageId: string): Promise<boolean> {
// Find any restricted ancestor where user lacks permission
const deniedAncestor = await this.db
.selectFrom('pageHierarchy')
.innerJoin('pageAccess', 'pageAccess.pageId', 'pageHierarchy.ancestorId')
.leftJoin('pagePermissions', (join) =>
join
.onRef('pagePermissions.pageAccessId', '=', 'pageAccess.id')
.on((eb) =>
eb.or([
eb('pagePermissions.userId', '=', userId),
eb(
'pagePermissions.groupId',
'in',
eb
.selectFrom('groupUsers')
.select('groupUsers.groupId')
.where('groupUsers.userId', '=', userId),
),
]),
),
)
.select('pageAccess.pageId')
.where('pageHierarchy.descendantId', '=', pageId)
.where('pagePermissions.id', 'is', null)
.executeTakeFirst();
return !deniedAncestor;
}
/**
* Check if user can edit a page by verifying they have WRITER permission on ALL restricted ancestors.
* Returns true if:
* - No ancestors are restricted, OR
* - User has writer permission on every restricted ancestor
*/
async canUserEditPage(userId: string, pageId: string): Promise<boolean> {
// Find any restricted ancestor where user lacks writer permission
const deniedAncestor = await this.db
.selectFrom('pageHierarchy')
.innerJoin('pageAccess', 'pageAccess.pageId', 'pageHierarchy.ancestorId')
.leftJoin('pagePermissions', (join) =>
join
.onRef('pagePermissions.pageAccessId', '=', 'pageAccess.id')
.on('pagePermissions.role', '=', 'writer')
.on((eb) =>
eb.or([
eb('pagePermissions.userId', '=', userId),
eb(
'pagePermissions.groupId',
'in',
eb
.selectFrom('groupUsers')
.select('groupUsers.groupId')
.where('groupUsers.userId', '=', userId),
),
]),
),
)
.select('pageAccess.pageId')
.where('pageHierarchy.descendantId', '=', pageId)
.where('pagePermissions.id', 'is', null)
.executeTakeFirst();
return !deniedAncestor;
}
/**
* Filter a list of page IDs to only those the user can access.
* Efficient single-query implementation for bulk filtering.
*/
async filterAccessiblePageIds(
pageIds: string[],
userId: string,
): Promise<string[]> {
if (pageIds.length === 0) return [];
// For each page, count restricted ancestors vs permitted ancestors
// A page is accessible if restrictedCount == permittedCount
const results = await this.db
.selectFrom('pages')
.select('pages.id')
.where('pages.id', 'in', pageIds)
.where(({ not, exists, selectFrom }) =>
not(
exists(
selectFrom('pageHierarchy')
.innerJoin(
'pageAccess',
'pageAccess.pageId',
'pageHierarchy.ancestorId',
)
.leftJoin('pagePermissions', (join) =>
join
.onRef('pagePermissions.pageAccessId', '=', 'pageAccess.id')
.on((eb) =>
eb.or([
eb('pagePermissions.userId', '=', userId),
eb(
'pagePermissions.groupId',
'in',
eb
.selectFrom('groupUsers')
.select('groupUsers.groupId')
.where('groupUsers.userId', '=', userId),
),
]),
),
)
.select('pageAccess.pageId')
.whereRef('pageHierarchy.descendantId', '=', 'pages.id')
.where('pagePermissions.id', 'is', null),
),
),
)
.execute();
return results.map((r) => r.id);
}
/**
* Check if a page or any of its ancestors has restrictions.
* Used to determine if page-level permission checks are needed.
*/
async hasRestrictedAncestor(pageId: string): Promise<boolean> {
const result = await this.db
.selectFrom('pageHierarchy')
.innerJoin('pageAccess', 'pageAccess.pageId', 'pageHierarchy.ancestorId')
.select('pageAccess.id')
.where('pageHierarchy.descendantId', '=', pageId)
.executeTakeFirst();
return !!result;
}
} }