diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index 79ca91918f..a058857076 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -731,8 +731,11 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = ct <- getContactViaMember db cxt user m liftIO $ setNewContactMemberConnRequest db user m cReq liftIO $ (ct,) <$> getGroupLinkId db user gInfo - sendGrpInvitation ct m groupLinkId - toView $ CEvtSentGroupInvitation user gInfo ct m + if memberRole' membership >= GRAdmin + then do + sendGrpInvitation ct m groupLinkId + toView $ CEvtSentGroupInvitation user gInfo ct m + else messageError "processGroupMessage: group link host no longer has admin role" where sendGrpInvitation :: Contact -> GroupMember -> Maybe GroupLinkId -> CM () sendGrpInvitation ct GroupMember {memberId, memberRole = memRole} groupLinkId = do @@ -1535,9 +1538,12 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = Just gli@GroupLinkInfo {groupId, memberRole = gLinkMemRole} -> do -- TODO [short links] deduplicate request by xContactId? gInfo <- withStore $ \db -> getGroupInfo db cxt user groupId - if useRelays' gInfo - then messageWarning $ "processContactConnMessage (group " <> groupName' gInfo <> "): ignored direct join request from " <> displayName <> " (group uses relays)" - else do + if + | useRelays' gInfo -> + messageWarning $ "processContactConnMessage (group " <> groupName' gInfo <> "): ignored direct join request from " <> displayName <> " (group uses relays)" + | memberRole' (membership gInfo) < GRAdmin -> + messageWarning $ "processContactConnMessage (group " <> groupName' gInfo <> "): ignored join request because host is no longer admin" + | otherwise -> do acceptMember_ <- asks $ acceptMember . chatHooks . config maybe (pure $ Right (GAAccepted, gLinkMemRole)) (\am -> liftIO $ am gInfo gli p) acceptMember_ >>= \case Right (acceptance, useRole) @@ -1566,20 +1572,23 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = createRelayRequestGroup db cxt user groupRelayInv invId chatVRange initialDelay GSMemAccepted RSInvited lift $ void $ getRelayRequestWorker True xGrpRelayTest :: InvitationId -> VersionRangeChat -> ByteString -> CM () - xGrpRelayTest invId chatVRange challenge = do - privKey_ <- withAgent $ \a -> getConnLinkPrivKey a (aConnId conn) - case privKey_ of - Nothing -> eToView $ ChatError (CEInternalError "no short link key for relay address") - Just privKey -> do - let sig = C.signatureBytes $ C.sign' privKey challenge - msg = XGrpRelayTest challenge (Just sig) - subMode <- chatReadVar subscriptionMode - chatVR <- chatVersionRange - let chatV = chatVR `peerConnChatVersion` chatVRange - (cmdId, acId) <- agentAcceptContactAsync user True invId msg subMode PQSupportOff chatV - withStore $ \db -> do - Connection {connId = testCId} <- createRelayTestConnection db cxt user acId ConnAccepted chatV subMode - liftIO $ setCommandConnId db user cmdId testCId + xGrpRelayTest invId chatVRange challenge + | isTrue userChatRelay && isNothing ucGroupId_ = + withAgent (`getConnLinkPrivKey` aConnId conn) >>= \case + Nothing -> eToView $ ChatError (CEInternalError "no short link key for relay address") + Just privKey -> do + let sig = C.signatureBytes $ C.sign' privKey challenge + msg = XGrpRelayTest challenge (Just sig) + subMode <- chatReadVar subscriptionMode + chatVR <- chatVersionRange + let chatV = chatVR `peerConnChatVersion` chatVRange + (cmdId, acId) <- agentAcceptContactAsync user True invId msg subMode PQSupportOff chatV + withStore $ \db -> do + Connection {connId = testCId} <- createRelayTestConnection db cxt user acId ConnAccepted chatV subMode + liftIO $ setCommandConnId db user cmdId testCId + | otherwise = messageError "relay test sent to non-relay link" + where + User {userChatRelay} = user -- TODO [relays] owner, relays: TBC how to communicate member rejection rules from owner to relays -- TODO [relays] relay: TBC communicate rejection when memberId already exists (currently checked in createJoiningMember) memberJoinRequestViaRelay :: InvitationId -> VersionRangeChat -> Profile -> MemberId -> MemberKey -> CM () @@ -3113,7 +3122,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = where GroupMember {memberId = membershipMemId} = membership changeMemberRole gInfo' member@GroupMember {memberRole = fromRole} gEvent - | senderRole < GRAdmin || senderRole < fromRole = + | senderRole < maximum ([GRAdmin, fromRole, memRole] :: [GroupMemberRole]) = messageError "x.grp.mem.role with insufficient member permissions" $> Nothing | otherwise = do withStore' $ \db -> updateGroupMemberRole db user member memRole diff --git a/src/Simplex/Chat/Protocol.hs b/src/Simplex/Chat/Protocol.hs index f8ccaa74e7..86202fe598 100644 --- a/src/Simplex/Chat/Protocol.hs +++ b/src/Simplex/Chat/Protocol.hs @@ -932,7 +932,7 @@ parseChatMessages msg = case B.head msg of Right (compressed :: L.NonEmpty Compressed) -> case traverse decompressedSize compressed of Nothing -> [Left "compressed size not specified"] Just sizes - | sum sizes > maxDecompressedMsgLength -> [Left "decompressed size exceeds limit"] + | any (maxDecompressedMsgLength <) sizes || maxDecompressedMsgLength < sum sizes -> [Left "decompressed size exceeds limit"] | otherwise -> concatMap (either (\e -> [Left e]) parseUncompressed' . decompress1) compressed parseUncompressed' "" = [Left "empty string"] parseUncompressed' s = parseUncompressed (B.head s) s diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt index 803e012773..e7188f3cc2 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt @@ -1134,6 +1134,19 @@ Query: Plan: SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) +Query: + UPDATE group_members + SET member_role = 'owner' + WHERE member_category = 'user' + AND group_id IN ( + SELECT group_id FROM groups WHERE local_display_name = 'team' + ) + +Plan: +SEARCH group_members USING INDEX idx_group_members_group_id_index_in_group (group_id=?) +LIST SUBQUERY 1 +SCAN groups USING COVERING INDEX sqlite_autoindex_groups_1 + Query: DELETE FROM chat_item_reactions WHERE contact_id = ? AND shared_msg_id = ? AND reaction_sent = ? AND reaction = ? diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs index 82906110c6..c8cd1c5f30 100644 --- a/tests/ChatTests/Groups.hs +++ b/tests/ChatTests/Groups.hs @@ -81,6 +81,7 @@ chatGroupTests = do it "group live message" testGroupLiveMessage it "update group profile" testUpdateGroupProfile it "update member role" testUpdateMemberRole + it "check owner role change" testOwnerRoleChange it "group description is shown as the first message to new members" testGroupDescription it "moderate message of another group member" testGroupModerate it "moderate own message (should process as deletion)" testGroupModerateOwn @@ -108,6 +109,7 @@ chatGroupTests = do it "invitee incognito" testGroupLinkInviteeIncognito it "incognito - join/invite" testGroupLinkIncognitoJoinInvite it "group link member role" testGroupLinkMemberRole + it "demotion does not remove group link" testGroupLinkDemotedAdmin it "host profile received" testGroupLinkHostProfileReceived it "existing contact merged" testGroupLinkExistingContactMerged describe "group links - member screening" $ do @@ -1608,6 +1610,37 @@ testUpdateMemberRole = alice ##> "/mr team alice admin" alice <## "bad chat command: can't change role for self" +testOwnerRoleChange :: HasCallStack => TestParams -> IO () +testOwnerRoleChange = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + createGroup3 "team" alice bob cath + void $ withCCTransaction cath $ \db -> + DB.execute_ + db + [sql| + UPDATE group_members + SET member_role = 'owner' + WHERE member_category = 'user' + AND group_id IN ( + SELECT group_id FROM groups WHERE local_display_name = 'team' + ) + |] + + cath ##> "/mr #team bob owner" + cath <## "#team: you changed the role of bob to owner" + concurrentlyN_ + [ alice <## "error: x.grp.mem.role with insufficient member permissions", + bob <## "error: x.grp.mem.role with insufficient member permissions" + ] + + bob ##> "/ms team" + bob + <### [ "alice (Alice): owner, host, connected", + "bob (Bob): admin, you, connected", + "cath (Catherine): admin, connected" + ] + testGroupDescription :: HasCallStack => TestParams -> IO () testGroupDescription = testChat4 aliceProfile bobProfile cathProfile danProfile $ \alice bob cath dan -> do connectUsers alice bob @@ -2929,6 +2962,25 @@ testGroupLinkMemberRole = bob <## "#team: cath changed your role from member to admin" alice <## "#team: cath changed the role of bob from member to admin" +testGroupLinkDemotedAdmin :: HasCallStack => TestParams -> IO () +testGroupLinkDemotedAdmin = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob _cath -> do + createGroup2' "team" alice (bob, GRAdmin) True + + bob ##> "/create link #team member" + _gLink <- getGroupLink bob "team" GRMember True + + alice ##> "/mr #team bob member" + concurrentlyN_ + [ alice <## "#team: you changed the role of bob to member", + bob <## "#team: alice changed your role from admin to member" + ] + + -- demotion does not remove bob's group link (it is preserved, usable again on re-promotion) + bob ##> "/show link #team" + void $ getGroupLink bob "team" GRMember False + testGroupLinkHostIncognito :: HasCallStack => TestParams -> IO () testGroupLinkHostIncognito = testChat2 aliceProfile bobProfile $