diff --git a/keyserver/src/fetchers/thread-permission-fetchers.js b/keyserver/src/fetchers/thread-permission-fetchers.js --- a/keyserver/src/fetchers/thread-permission-fetchers.js +++ b/keyserver/src/fetchers/thread-permission-fetchers.js @@ -28,6 +28,7 @@ ThreadRolePermissionsBlob, } from 'lib/types/thread-permission-types.js'; import type { ThreadType } from 'lib/types/thread-types-enum.js'; +import { ServerError } from 'lib/utils/errors.js'; import { values } from 'lib/utils/objects.js'; import { fetchThreadInfos } from './thread-fetchers.js'; @@ -107,7 +108,22 @@ permissions: ?ThreadPermissionsBlob, role: number, checks: $ReadOnlyArray, + predicate: 'and' | 'or' = 'and', ): boolean { + if (predicate === 'or') { + for (const check of checks) { + if (check.check === 'is_member') { + if (role > 0) { + return true; + } + } else if (check.check === 'permission') { + if (permissionLookup(permissions, check.permission)) { + return true; + } + } + } + return false; + } for (const check of checks) { if (check.check === 'is_member') { if (role <= 0) { @@ -136,6 +152,21 @@ return new Set(threadRows.map(row => row.threadID)); } +// Like checkThread, but "or" over the checks instead of "and" +async function checkThreadsOr( + viewer: Viewer, + threadIDs: $ReadOnlyArray, + checks: $ReadOnlyArray, +): Promise> { + if (viewer.isScriptViewer) { + // script viewers are all-powerful + return new Set(threadIDs); + } + + const threadRows = await getValidThreads(viewer, threadIDs, checks, 'or'); + return new Set(threadRows.map(row => row.threadID)); +} + type PartialMembershipRow = { +threadID: string, +role: number, @@ -145,7 +176,22 @@ viewer: Viewer, threadIDs: $ReadOnlyArray, checks: $ReadOnlyArray, + predicate: 'and' | 'or' = 'and', ): Promise { + if ( + predicate === 'or' && + checks.some(({ check }) => check !== 'permission') + ) { + // Handling non-permission checks for the "or" case is more complicated. + // We'd need to break the separation between isThreadValid and + // checkThreadsFrozen, since a a valid non-permission check in isThreadValid + // should cause checkThreadsFrozen to be ignored. + throw new ServerError( + 'getValidThreads only supports the "or" predicate when provided only ' + + 'permission checks', + ); + } + const query = SQL` SELECT thread AS threadID, permissions, role FROM memberships @@ -161,7 +207,7 @@ const [[result], disabledThreadIDs] = await Promise.all([ dbQuery(query), - checkThreadsFrozen(viewer, permissionsToCheck, threadIDs), + checkThreadsFrozen(viewer, permissionsToCheck, threadIDs, predicate), ]); return result @@ -172,7 +218,7 @@ })) .filter( row => - isThreadValid(row.permissions, row.role, checks) && + isThreadValid(row.permissions, row.role, checks, predicate) && !disabledThreadIDs.has(row.threadID), ); } @@ -181,12 +227,20 @@ viewer: Viewer, permissionsToCheck: $ReadOnlyArray, threadIDs: $ReadOnlyArray, + predicate: 'and' | 'or' = 'and', ): Promise<$ReadOnlySet> { const threadIDsWithDisabledPermissions = new Set(); - const permissionMightBeDisabled = permissionsToCheck.some(permission => - permissionsDisabledByBlock.has(permission), - ); + let permissionMightBeDisabled; + if (predicate === 'and') { + permissionMightBeDisabled = permissionsToCheck.some(permission => + permissionsDisabledByBlock.has(permission), + ); + } else { + permissionMightBeDisabled = permissionsToCheck.every(permission => + permissionsDisabledByBlock.has(permission), + ); + } if (!permissionMightBeDisabled) { return threadIDsWithDisabledPermissions; } @@ -463,6 +517,7 @@ viewerIsMember, viewerIsMemberOfThreads, checkThreads, + checkThreadsOr, getValidThreads, checkThread, checkIfThreadIsBlocked,