core: fix group link use after admin demotion (#7111)

* Fix group link use after admin demotion

* fix: group role change

* size limit

* fix

* allow delete

* do not remove link

* query plan

* relay test

* refactor

---------

Co-authored-by: Paul Bottinelli <paul.bottinelli@trailofbits.com>
Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
This commit is contained in:
Evgeny
2026-06-21 23:36:15 +01:00
committed by GitHub
parent 687661313f
commit 6cde614e51
4 changed files with 95 additions and 21 deletions
+29 -20
View File
@@ -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
+1 -1
View File
@@ -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
@@ -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 = ?
+52
View File
@@ -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 $