mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-07-02 11:21:46 +00:00
Merge branch 'master' into sh/namespace
This commit is contained in:
@@ -131,6 +131,15 @@ public func subscriberCountStr(_ count: Int64) -> String {
|
||||
: String.localizedStringWithFormat(NSLocalizedString("%d subscribers", comment: "channel subscriber count"), count)
|
||||
}
|
||||
|
||||
public func ownersContributorsCountStr(_ count: Int, withContributors: Bool) -> String {
|
||||
if withContributors {
|
||||
return String.localizedStringWithFormat(NSLocalizedString("%d owners & contributors", comment: "channel members count"), count)
|
||||
}
|
||||
return count == 1
|
||||
? String.localizedStringWithFormat(NSLocalizedString("%d owner", comment: "channel owners count"), count)
|
||||
: String.localizedStringWithFormat(NSLocalizedString("%d owners", comment: "channel owners count"), count)
|
||||
}
|
||||
|
||||
struct ChatInfoToolbar_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ChatInfoToolbar(chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []))
|
||||
|
||||
@@ -21,22 +21,29 @@ struct ChannelMembersView: View {
|
||||
let s = m.wrapped.memberStatus
|
||||
return s != .memLeft && s != .memRemoved && m.wrapped.memberRole != .relay
|
||||
}
|
||||
.sorted { $0.wrapped.memberRole > $1.wrapped.memberRole }
|
||||
let subscriberCount = groupInfo.groupSummary.publicMemberCount ?? Int64(members.count + 1)
|
||||
if groupInfo.isOwner {
|
||||
let subscriberCount = groupInfo.groupSummary.publicMemberCount ?? Int64(members.count + 1)
|
||||
List {
|
||||
Section(header: Text(subscriberCountStr(subscriberCount)).foregroundColor(theme.colors.secondary)) {
|
||||
memberRow(GMember(groupInfo.membership), user: true, showRole: true)
|
||||
ForEach(members) { member in
|
||||
memberRow(member, user: false, showRole: member.wrapped.memberRole >= .owner)
|
||||
memberRow(member, user: false, showRole: member.wrapped.memberRole >= .member)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let owners = members.filter { $0.wrapped.memberRole >= .owner }
|
||||
let contributors = members.filter { $0.wrapped.memberRole >= .member && $0.wrapped.memberStatus != .memUnknown }
|
||||
let contributorCount = contributors.count + (groupInfo.membership.memberRole >= .member ? 1 : 0)
|
||||
let withContributors = contributors.contains { $0.wrapped.memberRole < .owner }
|
||||
|| groupInfo.membership.memberRole >= .member
|
||||
List {
|
||||
Section(header: Text("Owners").foregroundColor(theme.colors.secondary)) {
|
||||
ForEach(owners) { member in
|
||||
memberRow(member, user: false, showRole: false)
|
||||
Section(header: Text(ownersContributorsCountStr(contributorCount, withContributors: withContributors)).foregroundColor(theme.colors.secondary)) {
|
||||
if groupInfo.membership.memberRole >= .member {
|
||||
memberRow(GMember(groupInfo.membership), user: true, showRole: true)
|
||||
}
|
||||
ForEach(contributors) { member in
|
||||
memberRow(member, user: false, showRole: member.wrapped.memberRole >= .moderator)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -691,7 +691,7 @@ struct GroupChatInfoView: View {
|
||||
}
|
||||
|
||||
private func channelMembersButton() -> some View {
|
||||
let label: LocalizedStringKey = groupInfo.isOwner ? "Subscribers" : "Owners"
|
||||
let label: LocalizedStringKey = groupInfo.isOwner ? "Subscribers" : "Owners & contributors"
|
||||
return NavigationLink {
|
||||
ChannelMembersView(chat: chat, groupInfo: groupInfo)
|
||||
.navigationTitle(label)
|
||||
|
||||
+5
@@ -1547,6 +1547,11 @@ fun subscriberCountStr(count: Long): String =
|
||||
if (count == 1L) String.format(generalGetString(MR.strings.channel_subscriber_count_singular), count)
|
||||
else String.format(generalGetString(MR.strings.channel_subscriber_count_plural), count)
|
||||
|
||||
fun ownersContributorsCountStr(count: Int, withContributors: Boolean): String =
|
||||
if (withContributors) String.format(generalGetString(MR.strings.channel_owners_contributors_count), count)
|
||||
else if (count == 1) String.format(generalGetString(MR.strings.channel_owner_count_singular), count)
|
||||
else String.format(generalGetString(MR.strings.channel_owner_count_plural), count)
|
||||
|
||||
@Composable
|
||||
fun ChatInfoToolbarTitle(cInfo: ChatInfo, imageSize: Dp = 40.dp, iconColor: Color = MaterialTheme.colors.secondaryVariant.mixWith(MaterialTheme.colors.onBackground, 0.97f)) {
|
||||
Row(
|
||||
|
||||
+17
-6
@@ -14,6 +14,7 @@ import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.chat.ownersContributorsCountStr
|
||||
import chat.simplex.common.views.chat.subscriberCountStr
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.res.MR
|
||||
@@ -33,6 +34,7 @@ fun ChannelMembersView(
|
||||
&& m.memberStatus != GroupMemberStatus.MemRemoved
|
||||
&& m.memberRole != GroupMemberRole.Relay
|
||||
}
|
||||
.sortedByDescending { it.memberRole }
|
||||
|
||||
ColumnWithScrollBar {
|
||||
val title = if (groupInfo.isOwner) {
|
||||
@@ -42,8 +44,8 @@ fun ChannelMembersView(
|
||||
}
|
||||
AppBarTitle(title)
|
||||
|
||||
val subscriberCount = groupInfo.groupSummary.publicMemberCount ?: (members.size + 1).toLong()
|
||||
if (groupInfo.isOwner) {
|
||||
val subscriberCount = groupInfo.groupSummary.publicMemberCount ?: (members.size + 1).toLong()
|
||||
SectionView(title = subscriberCountStr(subscriberCount)) {
|
||||
SectionItemView(minHeight = 54.dp, padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
ChannelMemberRow(groupInfo.membership, user = true, showRole = true, isChannel = groupInfo.isChannel)
|
||||
@@ -55,14 +57,23 @@ fun ChannelMembersView(
|
||||
minHeight = 54.dp,
|
||||
padding = PaddingValues(horizontal = DEFAULT_PADDING)
|
||||
) {
|
||||
ChannelMemberRow(member, user = false, showRole = member.memberRole >= GroupMemberRole.Owner, isChannel = groupInfo.isChannel)
|
||||
ChannelMemberRow(member, user = false, showRole = member.memberRole >= GroupMemberRole.Member, isChannel = groupInfo.isChannel)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val owners = members.filter { it.memberRole >= GroupMemberRole.Owner }
|
||||
SectionView(title = generalGetString(MR.strings.channel_members_section_owners)) {
|
||||
owners.forEachIndexed { index, member ->
|
||||
val contributors = members.filter { it.memberRole >= GroupMemberRole.Member && it.memberStatus != GroupMemberStatus.MemUnknown }
|
||||
val contributorCount = contributors.size + if (groupInfo.membership.memberRole >= GroupMemberRole.Member) 1 else 0
|
||||
val withContributors = contributors.any { it.memberRole < GroupMemberRole.Owner } ||
|
||||
groupInfo.membership.memberRole >= GroupMemberRole.Member
|
||||
SectionView(title = ownersContributorsCountStr(contributorCount, withContributors)) {
|
||||
if (groupInfo.membership.memberRole >= GroupMemberRole.Member) {
|
||||
SectionItemView(minHeight = 54.dp, padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
ChannelMemberRow(groupInfo.membership, user = true, showRole = true, isChannel = groupInfo.isChannel)
|
||||
}
|
||||
Divider()
|
||||
}
|
||||
contributors.forEachIndexed { index, member ->
|
||||
if (index > 0) {
|
||||
Divider()
|
||||
}
|
||||
@@ -71,7 +82,7 @@ fun ChannelMembersView(
|
||||
minHeight = 54.dp,
|
||||
padding = PaddingValues(horizontal = DEFAULT_PADDING)
|
||||
) {
|
||||
ChannelMemberRow(member, user = false, showRole = false, isChannel = groupInfo.isChannel)
|
||||
ChannelMemberRow(member, user = false, showRole = member.memberRole >= GroupMemberRole.Moderator, isChannel = groupInfo.isChannel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2976,9 +2976,12 @@
|
||||
|
||||
<!-- ChannelMembersView.kt -->
|
||||
<string name="channel_members_title_subscribers">Subscribers</string>
|
||||
<string name="channel_members_section_owners">Owners</string>
|
||||
<string name="channel_members_section_owners">Owners & contributors</string>
|
||||
<string name="channel_subscriber_count_singular">%1$d subscriber</string>
|
||||
<string name="channel_subscriber_count_plural">%1$d subscribers</string>
|
||||
<string name="channel_owner_count_singular">%1$d owner</string>
|
||||
<string name="channel_owner_count_plural">%1$d owners</string>
|
||||
<string name="channel_owners_contributors_count">%1$d owners & contributors</string>
|
||||
<string name="channel_member_you">you</string>
|
||||
|
||||
<!-- ChatRelayView.kt -->
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ cabal-version: 1.12
|
||||
-- see: https://github.com/sol/hpack
|
||||
|
||||
name: simplex-chat
|
||||
version: 7.0.0.5
|
||||
version: 7.0.0.6
|
||||
category: Web, System, Services, Cryptography
|
||||
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
||||
author: simplex.chat
|
||||
|
||||
@@ -1256,6 +1256,9 @@ redactedMemberProfile allowSimplexLinks Profile {displayName, fullName, shortDes
|
||||
isRosterRole :: GroupMemberRole -> Bool
|
||||
isRosterRole r = r == GRMember || r == GRModerator || r == GRAdmin
|
||||
|
||||
isPrivilegedRole :: GroupMemberRole -> Bool
|
||||
isPrivilegedRole r = r >= GRMember
|
||||
|
||||
-- Drop non-privileged-role entries and de-duplicate by memberId, keeping the first.
|
||||
-- Runs on the parsed roster blob.
|
||||
validateGroupRoster :: [RosterMember] -> [RosterMember]
|
||||
|
||||
@@ -3077,8 +3077,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
|
||||
|
||||
xGrpMemNew :: GroupInfo -> GroupMember -> MemberInfo -> Maybe MsgScope -> RcvMessage -> UTCTime -> CM (Maybe DeliveryJobScope)
|
||||
xGrpMemNew gInfo m memInfo@(MemberInfo memId memRole _ _ assertedKey_) msgScope_ msg brokerTs = do
|
||||
let fromRelay = useRelays' gInfo && isRelay m
|
||||
unless fromRelay $ checkHostRole m memRole
|
||||
unless (useRelays' gInfo) $ checkHostRole m memRole
|
||||
if sameMemberId memId (membership gInfo)
|
||||
then pure Nothing
|
||||
else
|
||||
@@ -3087,7 +3086,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
|
||||
-- roster-established privileged member: the relay may update the profile only,
|
||||
-- never the role or key (those are owner-authoritative via the roster, and
|
||||
-- XGrpMemNew is unsigned)
|
||||
| fromRelay && isRosterRole (memberRole' unknownMember) -> do
|
||||
| useRelays' gInfo && isPrivilegedRole (memberRole' unknownMember) -> do
|
||||
-- a member's key is immutable per memberId and identical across relays; mismatch
|
||||
-- is unambiguous relay misbehavior (role can legitimately differ across relays
|
||||
-- under multi-relay skew, so we deliberately don't warn on role)
|
||||
@@ -3101,8 +3100,8 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
|
||||
toView $ CEvtUnknownMemberAnnounced user gInfo' m unknownMember updatedMember
|
||||
memberAnnouncedToView updatedMember gInfo'
|
||||
pure $ deliveryJobScope updatedMember
|
||||
-- asserted privileged but NOT roster-established: relay conjuring a moderator
|
||||
| fromRelay && isRosterRole memRole ->
|
||||
-- asserted privileged but NOT roster-established: relay conjuring a privileged member
|
||||
| useRelays' gInfo && isPrivilegedRole memRole ->
|
||||
messageError "x.grp.mem.new: privileged role not established by roster" $> Nothing
|
||||
| otherwise -> do
|
||||
(updatedMember, gInfo') <- withStore $ \db -> do
|
||||
@@ -3120,8 +3119,8 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
|
||||
| useRelays' gInfo -> logInfo "x.grp.mem.new: member already created via another relay" $> Nothing
|
||||
| otherwise -> messageError "x.grp.mem.new error: member already exists" $> Nothing
|
||||
Left _
|
||||
-- a privileged member absent from the roster is a relay conjuring a moderator
|
||||
| fromRelay && isRosterRole memRole -> messageError "x.grp.mem.new: privileged member not established by roster" $> Nothing
|
||||
-- a privileged member absent from the roster is a relay conjuring one
|
||||
| useRelays' gInfo && isPrivilegedRole memRole -> messageError "x.grp.mem.new: privileged member not established by roster" $> Nothing
|
||||
| otherwise -> do
|
||||
(newMember, gInfo') <- withStore $ \db -> do
|
||||
newMember <- createNewGroupMember db cxt user gInfo m memInfo GCPostMember initialStatus
|
||||
|
||||
@@ -7033,6 +7033,11 @@ Query: SELECT auth_err_counter FROM connections WHERE user_id = ? AND connection
|
||||
Plan:
|
||||
SEARCH connections USING INTEGER PRIMARY KEY (rowid=?)
|
||||
|
||||
Query: SELECT c.agent_conn_id FROM connections c JOIN group_members m ON m.group_member_id = c.group_member_id WHERE m.local_display_name = ?
|
||||
Plan:
|
||||
SCAN m USING COVERING INDEX idx_group_members_user_id_local_display_name
|
||||
SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?)
|
||||
|
||||
Query: SELECT chat_item_id FROM chat_items WHERE user_id = ? AND contact_id = ? AND shared_msg_id = ? AND item_sent = ?
|
||||
Plan:
|
||||
SEARCH chat_items USING INDEX idx_chat_items_direct_shared_msg_id (user_id=? AND contact_id=? AND shared_msg_id=?)
|
||||
@@ -7233,6 +7238,10 @@ Query: SELECT max(active_order) FROM users
|
||||
Plan:
|
||||
SEARCH users
|
||||
|
||||
Query: SELECT member_id FROM group_members WHERE member_role = ? LIMIT 1
|
||||
Plan:
|
||||
SCAN group_members
|
||||
|
||||
Query: SELECT member_pub_key FROM group_members WHERE local_display_name = ?
|
||||
Plan:
|
||||
SCAN group_members
|
||||
@@ -7253,6 +7262,10 @@ Query: SELECT member_role FROM group_members WHERE local_display_name = ?
|
||||
Plan:
|
||||
SCAN group_members
|
||||
|
||||
Query: SELECT member_role, member_pub_key FROM group_members WHERE local_display_name = ?
|
||||
Plan:
|
||||
SCAN group_members
|
||||
|
||||
Query: SELECT member_status FROM group_members WHERE local_display_name = ?
|
||||
Plan:
|
||||
SCAN group_members
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE NumericUnderscores #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE PatternSynonyms #-}
|
||||
{-# LANGUAGE PostfixOperators #-}
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
@@ -16,30 +17,37 @@ import ChatTests.DBUtils
|
||||
import ChatTests.Utils
|
||||
import Control.Concurrent (threadDelay)
|
||||
import Control.Concurrent.Async (concurrently_)
|
||||
import Control.Concurrent.STM (atomically)
|
||||
import Control.Monad (forM_, void, when)
|
||||
import Control.Monad.Except (runExceptT)
|
||||
import Data.Bifunctor (second)
|
||||
import Data.ByteString (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Maybe (fromMaybe, isJust, maybeToList)
|
||||
import Data.Time (UTCTime)
|
||||
import Data.Time (UTCTime, getCurrentTime)
|
||||
import Data.Int (Int64)
|
||||
import Data.List (intercalate, isInfixOf, isSuffixOf)
|
||||
import qualified Data.Map.Strict as M
|
||||
import qualified Data.Text as T
|
||||
import Simplex.Chat.Controller (ChatConfig (..), ChatHooks (..), ChatLogLevel (..), defaultChatHooks)
|
||||
import Simplex.Chat.Controller (ChatController (ChatController, smpAgent), ChatConfig (..), ChatHooks (..), ChatLogLevel (..), defaultChatHooks)
|
||||
import Simplex.Chat.Library.Internal (uniqueMsgMentions, updatedMentionNames)
|
||||
import Simplex.Chat.Markdown (parseMaybeMarkdownList)
|
||||
import Simplex.Chat.Messages (CIMention (..), CIMentionMember (..), ChatItemId)
|
||||
import Simplex.Chat.Messages.Batch (encodeBinaryBatch, encodeFwdElement)
|
||||
import Simplex.Chat.Messages.CIContent (publicGroupNoE2EText)
|
||||
import Simplex.Chat.Options
|
||||
import Simplex.Chat.Protocol (MsgMention (..), MsgContent (..), msgContentText)
|
||||
import Simplex.Chat.Protocol (ChatMessage (ChatMessage), ChatMsgEvent (XGrpMemNew), FwdSender (FwdMember), GrpMsgForward (GrpMsgForward), MsgMention (..), MsgContent (..), VerifiedMsg (VMUnsigned), msgContentText)
|
||||
import Simplex.Chat.Types
|
||||
import Simplex.Chat.Types.MemberRelations (MemberRelation (..), getRelation, setRelation)
|
||||
import Simplex.Chat.Types.Shared (GroupMemberRole (..), GroupAcceptance (..))
|
||||
import Simplex.Messaging.Agent (sendMessages, vrValue)
|
||||
import Simplex.Messaging.Agent.Env.SQLite
|
||||
import Simplex.Messaging.Agent.RetryInterval
|
||||
import qualified Simplex.Messaging.Agent.Store.DB as DB
|
||||
import Simplex.Messaging.Agent.Store.DB (Binary (..))
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Crypto.Ratchet (pattern PQEncOff)
|
||||
import Simplex.Messaging.Protocol (MsgFlags (..))
|
||||
import Simplex.Messaging.Server.Env.STM hiding (subscriptions)
|
||||
import Simplex.Messaging.Transport
|
||||
import Simplex.Messaging.Version
|
||||
@@ -295,6 +303,7 @@ chatGroupTests = do
|
||||
it "removed moderator drops from the roster cache" testChannelRemovedModeratorRefreshesRoster
|
||||
it "role transitions update the roster (mod <-> admin, admin -> non-roster)" testChannelRoleTransitionsUpdateRoster
|
||||
it "malicious relay cannot downgrade or re-key a roster-established moderator via XGrpMemNew" testChannelRelayCannotDowngradeRosterMember
|
||||
it "malicious relay cannot forge a privileged member via XGrpMemNew forwarded as the owner" testChannelRelayCannotForgePrivilegedMember
|
||||
it "should add relay to channel with roster (relay caches roster before joinable)" testChannelAddRelayWithRoster
|
||||
it "roster blob spanning multiple chunks reassembles" testChannelRosterMultipartReassembly
|
||||
it "corrupted roster blob is rejected on digest mismatch" testChannelRosterDigestMismatchRejected
|
||||
@@ -9371,6 +9380,13 @@ testChannels2RelaysIncognito ps =
|
||||
frank <# ("#team " <> danIncognito <> "> > hi")
|
||||
frank <## " + 👍"
|
||||
|
||||
alice `hasContactProfiles` ["alice", "bob", "cath", T.pack danIncognito, "eve", "frank"]
|
||||
bob `hasContactProfiles` ["alice", "bob", T.pack danIncognito, "eve", "frank"]
|
||||
cath `hasContactProfiles` ["alice", "cath", T.pack danIncognito, "eve", "frank"]
|
||||
dan `hasContactProfiles` ["alice", "bob", "cath", "dan", T.pack danIncognito]
|
||||
eve `hasContactProfiles` ["alice", "bob", "cath", T.pack danIncognito, "eve"]
|
||||
frank `hasContactProfiles` ["alice", "bob", "cath", T.pack danIncognito, "frank"]
|
||||
|
||||
testChannelUpdateProfileSigned :: HasCallStack => TestParams -> IO ()
|
||||
testChannelUpdateProfileSigned ps =
|
||||
withNewTestChat ps "alice" aliceProfile $ \alice ->
|
||||
@@ -9906,6 +9922,68 @@ testChannelRelayCannotDowngradeRosterMember ps =
|
||||
[Only k] -> pure k
|
||||
_ -> fail $ "expected one row for " <> T.unpack name
|
||||
|
||||
testChannelRelayCannotForgePrivilegedMember :: HasCallStack => TestParams -> IO ()
|
||||
testChannelRelayCannotForgePrivilegedMember ps =
|
||||
withNewTestChat ps "alice" aliceProfile $ \alice ->
|
||||
withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob ->
|
||||
withNewTestChat ps "cath" cathProfile $ \cath -> do
|
||||
(shortLink, fullLink) <- prepareChannel1Relay "team" alice bob
|
||||
memberJoinChannel "team" [bob] [alice] shortLink fullLink cath
|
||||
threadDelay 1000000
|
||||
-- the forged attribution only resolves to a privileged author if the victim already holds the
|
||||
-- owner at GROwner (established via the group link on join) - this documents and guards that premise
|
||||
checkMemberRow cath "alice" (Just "owner")
|
||||
ownerMemId <- ownerMemberId bob
|
||||
connId <- relayConnIdToMember bob "cath"
|
||||
-- the malicious relay forges the announcement, choosing the new member's role and signing key
|
||||
g <- C.newRandom
|
||||
kp <- atomically $ C.generateKeyPair g
|
||||
ts <- getCurrentTime
|
||||
let ChatController {smpAgent = bobAgent} = chatController bob
|
||||
attackerPub = fst kp :: C.PublicKeyEd25519
|
||||
forgedMemId = MemberId "forgedadmin1"
|
||||
forgedProfile = (aliceProfile :: Profile) {displayName = "forgery", fullName = "Forgery"}
|
||||
memInfo =
|
||||
MemberInfo
|
||||
{ memberId = forgedMemId,
|
||||
memberRole = GRAdmin,
|
||||
v = Nothing,
|
||||
profile = forgedProfile,
|
||||
memberKey = Just (MemberKey attackerPub)
|
||||
}
|
||||
chatMsg = ChatMessage chatInitialVRange Nothing (XGrpMemNew memInfo Nothing)
|
||||
fwd = GrpMsgForward (FwdMember ownerMemId "alice") ts
|
||||
body = encodeBinaryBatch [encodeFwdElement fwd (VMUnsigned chatMsg)]
|
||||
sent <- runExceptT $ sendMessages bobAgent [(connId, PQEncOff, MsgFlags False, vrValue body)]
|
||||
either (fail . show) (const $ pure ()) sent
|
||||
-- secure: the victim rejects the forged privileged announcement instead of storing it
|
||||
cath <##. "error: x.grp.mem.new: privileged member not established by roster"
|
||||
forged <- forgedMemberRows cath "forgery"
|
||||
forged `shouldBe` []
|
||||
where
|
||||
ownerMemberId :: TestCC -> IO MemberId
|
||||
ownerMemberId cc = do
|
||||
rows <- withCCTransaction cc $ \db ->
|
||||
DB.query db "SELECT member_id FROM group_members WHERE member_role = ? LIMIT 1" (Only ("owner" :: T.Text)) :: IO [Only ByteString]
|
||||
case rows of
|
||||
[Only mid] -> pure (MemberId mid)
|
||||
_ -> fail "expected exactly one owner member on the relay"
|
||||
relayConnIdToMember :: TestCC -> T.Text -> IO ByteString
|
||||
relayConnIdToMember cc name = do
|
||||
rows <- withCCTransaction cc $ \db ->
|
||||
DB.query
|
||||
db
|
||||
"SELECT c.agent_conn_id FROM connections c JOIN group_members m ON m.group_member_id = c.group_member_id WHERE m.local_display_name = ?"
|
||||
(Only name) ::
|
||||
IO [Only ByteString]
|
||||
case rows of
|
||||
(Only connId : _) -> pure connId
|
||||
_ -> fail $ "no relay connection to member " <> T.unpack name
|
||||
forgedMemberRows :: TestCC -> T.Text -> IO [(T.Text, Maybe ByteString)]
|
||||
forgedMemberRows cc name =
|
||||
withCCTransaction cc $ \db ->
|
||||
DB.query db "SELECT member_role, member_pub_key FROM group_members WHERE local_display_name = ?" (Only name)
|
||||
|
||||
testChannelRemoveMemberSigned :: HasCallStack => TestParams -> IO ()
|
||||
testChannelRemoveMemberSigned ps =
|
||||
withNewTestChat ps "alice" aliceProfile $ \alice ->
|
||||
|
||||
Reference in New Issue
Block a user