From 1d2bb3b72dfbba9da9cb5a6d252b58fbf1c9a709 Mon Sep 17 00:00:00 2001 From: Tomasz Palys Date: Fri, 20 Sep 2024 15:38:57 +0200 Subject: [PATCH] [lib] Set roles and permissions in thick threads based on parent permissions Summary: When we create a thick thread, we should check if the viewer is a member and set the role and permissions accordingly. This case can happen when we receive a create sidebar operation. When a user leaves a sidebar, we should also update their permissions. https://linear.app/comm/issue/ENG-9318/sidebar-joining-issues Test Plan: Create a thick sidebar without the viewer and check if the join button is visible. Join and leave the sidebar - the button should reappear. Created a new sidebar in a thread with three users and added one of them. Checked if the added user can send messages and if the second user can join the sidebar. Reviewers: kamil, ashoat Reviewed By: ashoat Differential Revision: https://phab.comm.dev/D13418 --- lib/permissions/thread-permissions.js | 10 ++ lib/shared/dm-ops/add-members-spec.js | 17 ++++ .../add-viewer-to-thread-members-spec.js | 4 +- lib/shared/dm-ops/create-sidebar-spec.js | 2 +- lib/shared/dm-ops/create-thread-spec.js | 98 +++++++++++++++++-- lib/shared/dm-ops/join-thread-spec.js | 19 +++- lib/shared/dm-ops/leave-thread-spec.js | 34 +++++++ 7 files changed, 170 insertions(+), 14 deletions(-) diff --git a/lib/permissions/thread-permissions.js b/lib/permissions/thread-permissions.js index 06d4ead3b5..ee0783f07e 100644 --- a/lib/permissions/thread-permissions.js +++ b/lib/permissions/thread-permissions.js @@ -466,6 +466,10 @@ function getThickThreadRolePermissionsBlob( threadType: ThickThreadType, ): ThreadRolePermissionsBlob { invariant(threadTypeIsThick(threadType), 'ThreadType should be thick'); + const openDescendantKnowOf = OPEN_DESCENDANT + threadPermissions.KNOW_OF; + const openDescendantVisible = OPEN_DESCENDANT + threadPermissions.VISIBLE; + const openChildJoinThread = OPEN_CHILD + threadPermissions.JOIN_THREAD; + const basePermissions = { [threadPermissions.KNOW_OF]: true, [threadPermissions.VISIBLE]: true, @@ -493,6 +497,9 @@ function getThickThreadRolePermissionsBlob( ...basePermissions, [threadPermissions.EDIT_ENTRIES]: true, [threadPermissions.CREATE_SIDEBARS]: true, + [openDescendantKnowOf]: true, + [openDescendantVisible]: true, + [openChildJoinThread]: true, }; } return { @@ -501,6 +508,9 @@ function getThickThreadRolePermissionsBlob( [threadPermissions.CREATE_SIDEBARS]: true, [threadPermissions.ADD_MEMBERS]: true, [threadPermissions.LEAVE_THREAD]: true, + [openDescendantKnowOf]: true, + [openDescendantVisible]: true, + [openChildJoinThread]: true, }; } diff --git a/lib/shared/dm-ops/add-members-spec.js b/lib/shared/dm-ops/add-members-spec.js index 77e7c148ed..0d5f465dde 100644 --- a/lib/shared/dm-ops/add-members-spec.js +++ b/lib/shared/dm-ops/add-members-spec.js @@ -70,10 +70,27 @@ const addMembersSpec: DMOperationSpec = Object.freeze({ roleIsDefaultRole(role), )?.id; invariant(defaultRoleID, 'Default role ID must exist'); + + const parentThreadID = currentThreadInfo.parentThreadID; + const parentThreadInfo = parentThreadID + ? utilities.threadInfos[parentThreadID] + : null; + if (parentThreadID && !parentThreadInfo) { + console.log( + `Parent thread with ID ${parentThreadID} was expected while adding ` + + 'thread members but is missing from the store', + ); + } + invariant( + !parentThreadInfo || parentThreadInfo.thick, + 'Parent thread should be thick', + ); + const { membershipPermissions } = createRoleAndPermissionForThickThreads( currentThreadInfo.type, currentThreadInfo.id, defaultRoleID, + parentThreadInfo, ); const memberTimestamps = { ...currentThreadInfo.timestamps.members }; diff --git a/lib/shared/dm-ops/add-viewer-to-thread-members-spec.js b/lib/shared/dm-ops/add-viewer-to-thread-members-spec.js index c1d978febf..a6f84df289 100644 --- a/lib/shared/dm-ops/add-viewer-to-thread-members-spec.js +++ b/lib/shared/dm-ops/add-viewer-to-thread-members-spec.js @@ -55,7 +55,7 @@ const addViewerToThreadMembersSpec: DMOperationSpec { const { time, messageID, addedUserIDs, existingThreadDetails } = dmOperation; - const { viewerID, threadInfos } = utilities; + const { threadInfos } = utilities; const { rawMessageInfo } = createAddViewerToThreadMembersMessageDataWithInfoFromDMOp(dmOperation); @@ -115,7 +115,7 @@ const addViewerToThreadMembersSpec: DMOperationSpec = containingThreadID: parentThreadID, timestamps: createThreadTimestamps(time, allMemberIDs), }, - viewerID, + utilities, ); const { sidebarSourceMessageInfo, createSidebarMessageInfo } = diff --git a/lib/shared/dm-ops/create-thread-spec.js b/lib/shared/dm-ops/create-thread-spec.js index ebe70bc33f..25eb92e428 100644 --- a/lib/shared/dm-ops/create-thread-spec.js +++ b/lib/shared/dm-ops/create-thread-spec.js @@ -1,5 +1,6 @@ // @flow +import invariant from 'invariant'; import uuid from 'uuid'; import type { @@ -11,6 +12,7 @@ import { getAllThreadPermissions, makePermissionsBlob, getThickThreadRolePermissionsBlob, + makePermissionsForChildrenBlob, } from '../../permissions/thread-permissions.js'; import type { CreateThickRawThreadInfoInput, @@ -34,16 +36,58 @@ import { generatePendingThreadColor } from '../color-utils.js'; import { rawMessageInfoFromMessageData } from '../message-utils.js'; import { createThreadTimestamps } from '../thread-utils.js'; +function createPermissionsInfo( + threadID: string, + threadType: ThickThreadType, + isMember: boolean, + parentThreadInfo: ?ThickRawThreadInfo, +): ThreadPermissionsInfo { + let rolePermissions = null; + if (isMember) { + rolePermissions = getThickThreadRolePermissionsBlob(threadType); + } + + let permissionsFromParent = null; + if (parentThreadInfo) { + const parentThreadRolePermissions = getThickThreadRolePermissionsBlob( + parentThreadInfo.type, + ); + const parentPermissionsBlob = makePermissionsBlob( + parentThreadRolePermissions, + null, + parentThreadInfo.id, + parentThreadInfo.type, + ); + permissionsFromParent = makePermissionsForChildrenBlob( + parentPermissionsBlob, + ); + } + + return getAllThreadPermissions( + makePermissionsBlob( + rolePermissions, + permissionsFromParent, + threadID, + threadType, + ), + threadID, + ); +} + function createRoleAndPermissionForThickThreads( threadType: ThickThreadType, threadID: string, roleID: string, + parentThreadInfo: ?ThickRawThreadInfo, ): { +role: RoleInfo, +membershipPermissions: ThreadPermissionsInfo } { const rolePermissions = getThickThreadRolePermissionsBlob(threadType); - const membershipPermissions = getAllThreadPermissions( - makePermissionsBlob(rolePermissions, null, threadID, threadType), + const membershipPermissions = createPermissionsInfo( threadID, + threadType, + true, + parentThreadInfo, ); + const role: RoleInfo = { ...minimallyEncodeRoleInfo({ id: roleID, @@ -62,7 +106,7 @@ function createRoleAndPermissionForThickThreads( type MutableThickRawThreadInfo = { ...ThickRawThreadInfo }; function createThickRawThreadInfo( input: CreateThickRawThreadInfoInput, - viewerID: string, + utilities: ProcessDMOperationUtilities, ): MutableThickRawThreadInfo { const { threadID, @@ -86,8 +130,38 @@ function createThickRawThreadInfo( const memberIDs = allMemberIDsWithSubscriptions.map(({ id }) => id); const threadColor = color ?? generatePendingThreadColor(memberIDs); + const parentThreadInfo = parentThreadID + ? utilities.threadInfos[parentThreadID] + : null; + if (parentThreadID && !parentThreadInfo) { + console.log( + `Parent thread with ID ${parentThreadID} was expected while creating ` + + 'thick thread but is missing from the store', + ); + } + invariant( + !parentThreadInfo || parentThreadInfo.thick, + 'Parent thread should be thick', + ); + const { membershipPermissions, role } = - createRoleAndPermissionForThickThreads(threadType, threadID, roleID); + createRoleAndPermissionForThickThreads( + threadType, + threadID, + roleID, + parentThreadInfo, + ); + + const viewerIsMember = allMemberIDsWithSubscriptions.some( + member => member.id === utilities.viewerID, + ); + const viewerRoleID = viewerIsMember ? role.id : null; + const viewerMembershipPermissions = createPermissionsInfo( + threadID, + threadType, + viewerIsMember, + parentThreadInfo, + ); const newThread: MutableThickRawThreadInfo = { thick: true, @@ -101,9 +175,12 @@ function createThickRawThreadInfo( ({ id: memberID, subscription }) => minimallyEncodeMemberInfo({ id: memberID, - role: role.id, - permissions: membershipPermissions, - isSender: memberID === viewerID, + role: memberID === utilities.viewerID ? viewerRoleID : role.id, + permissions: + memberID === utilities.viewerID + ? viewerMembershipPermissions + : membershipPermissions, + isSender: memberID === utilities.viewerID, subscription, }), ), @@ -111,8 +188,8 @@ function createThickRawThreadInfo( [role.id]: role, }, currentUser: minimallyEncodeThreadCurrentUserInfo({ - role: role.id, - permissions: membershipPermissions, + role: viewerRoleID, + permissions: viewerMembershipPermissions, subscription: joinThreadSubscription, unread, }), @@ -189,7 +266,7 @@ const createThreadSpec: DMOperationSpec = unread: creatorID !== viewerID, timestamps: createThreadTimestamps(time, allMemberIDs), }, - viewerID, + utilities, ); const { rawMessageInfo } = @@ -222,4 +299,5 @@ export { createThickRawThreadInfo, createThreadSpec, createRoleAndPermissionForThickThreads, + createPermissionsInfo, }; diff --git a/lib/shared/dm-ops/join-thread-spec.js b/lib/shared/dm-ops/join-thread-spec.js index e3358947ed..927b8086e7 100644 --- a/lib/shared/dm-ops/join-thread-spec.js +++ b/lib/shared/dm-ops/join-thread-spec.js @@ -118,7 +118,7 @@ const joinThreadSpec: DMOperationSpec = Object.freeze({ members: memberTimestamps, }, }, - viewerID, + utilities, ); updateInfos.push({ type: updateTypes.JOIN_THREAD, @@ -135,10 +135,27 @@ const joinThreadSpec: DMOperationSpec = Object.freeze({ roleIsDefaultRole(role), )?.id; invariant(defaultRoleID, 'Default role ID must exist'); + + const parentThreadID = existingThreadDetails.parentThreadID; + const parentThreadInfo = parentThreadID + ? utilities.threadInfos[parentThreadID] + : null; + if (parentThreadID && !parentThreadInfo) { + console.log( + `Parent thread with ID ${parentThreadID} was expected while joining ` + + 'thick thread but is missing from the store', + ); + } + invariant( + !parentThreadInfo || parentThreadInfo.thick, + 'Parent thread should be thick', + ); + const { membershipPermissions } = createRoleAndPermissionForThickThreads( currentThreadInfo.type, currentThreadInfo.id, defaultRoleID, + parentThreadInfo, ); const member = minimallyEncodeMemberInfo({ diff --git a/lib/shared/dm-ops/leave-thread-spec.js b/lib/shared/dm-ops/leave-thread-spec.js index d1ee50b4b9..9a975deefb 100644 --- a/lib/shared/dm-ops/leave-thread-spec.js +++ b/lib/shared/dm-ops/leave-thread-spec.js @@ -3,6 +3,7 @@ import invariant from 'invariant'; import uuid from 'uuid'; +import { createPermissionsInfo } from './create-thread-spec.js'; import type { DMOperationSpec, ProcessDMOperationUtilities, @@ -10,6 +11,7 @@ import type { import type { DMLeaveThreadOperation } from '../../types/dm-ops.js'; import { messageTypes } from '../../types/message-types-enum.js'; import type { ThickRawThreadInfo } from '../../types/minimally-encoded-thread-permissions-types.js'; +import { minimallyEncodeThreadCurrentUserInfo } from '../../types/minimally-encoded-thread-permissions-types.js'; import { threadTypes } from '../../types/thread-types-enum.js'; import type { RawThreadInfos } from '../../types/thread-types.js'; import { updateTypes } from '../../types/update-types-enum.js'; @@ -121,9 +123,41 @@ const leaveThreadSpec: DMOperationSpec = Object.freeze({ ...memberTimestamps[editorID], isMember: time, }; + + let currentUser = threadInfo.currentUser; + if (editorID === viewerID) { + const parentThreadID = threadInfo.parentThreadID; + const parentThreadInfo = parentThreadID + ? utilities.threadInfos[parentThreadID] + : null; + if (parentThreadID && !parentThreadInfo) { + console.log( + `Parent thread with ID ${parentThreadID} was expected while ` + + 'leaving a thread but is missing from the store', + ); + } + invariant( + parentThreadInfo?.thick, + 'Parent thread should be present and thick', + ); + const viewerMembershipPermissions = createPermissionsInfo( + threadID, + threadInfo.type, + false, + parentThreadInfo, + ); + const { minimallyEncoded, permissions, ...currentUserInfo } = currentUser; + currentUser = minimallyEncodeThreadCurrentUserInfo({ + ...currentUserInfo, + role: null, + permissions: viewerMembershipPermissions, + }); + } + const updatedThreadInfo = { ...threadInfo, members: threadInfo.members.filter(member => member.id !== editorID), + currentUser, timestamps: { ...threadInfo.timestamps, members: memberTimestamps,