From 3805662a1834dd65c2b317156b2ce4e1ee2a3950 Mon Sep 17 00:00:00 2001 From: Shinnok Date: Wed, 25 Oct 2023 17:26:18 +0300 Subject: [PATCH] fix(messenger_communities): block messages and reactions to token gated or spectated communities (#4064) Which specifies that if a user is not a community member & a chat member, he can't post, react or pin messages in that chat. Notes: - also fix&cleanup associated failing tests. - refactor Community.CanPost() to reflect the new requirement. - grant code is not fully implemented and is to be removed later. Fixes https://github.com/status-im/status-desktop/issues/11915 --- protocol/communities/community.go | 68 +++---- protocol/communities/community_test.go | 6 +- protocol/communities_messenger_test.go | 21 +- protocol/messenger.go | 4 +- protocol/messenger_activity_center_test.go | 211 +++++---------------- protocol/messenger_handler.go | 30 ++- 6 files changed, 106 insertions(+), 234 deletions(-) diff --git a/protocol/communities/community.go b/protocol/communities/community.go index 94c8b0c7e..1d8e226d4 100644 --- a/protocol/communities/community.go +++ b/protocol/communities/community.go @@ -1722,72 +1722,54 @@ func (o *Community) VerifyGrantSignature(data []byte) (*protobuf.Grant, error) { func (o *Community) CanPost(pk *ecdsa.PublicKey, chatID string, grantBytes []byte) (bool, error) { if o.config.CommunityDescription.Chats == nil { - o.config.Logger.Debug("canPost, no-chats") + o.config.Logger.Debug("Community.CanPost: no-chats") return false, nil } chat, ok := o.config.CommunityDescription.Chats[chatID] if !ok { - o.config.Logger.Debug("canPost, no chat with id", zap.String("chat-id", chatID)) + o.config.Logger.Debug("Community.CanPost: no chat with id", zap.String("chat-id", chatID)) return false, nil } - // creator can always post + // community creator can always post, return immediately if common.IsPubKeyEqual(pk, o.config.ID) { return true, nil } - // if banned cannot post if o.isBanned(pk) { + o.config.Logger.Debug("Community.CanPost: user is banned", zap.String("chat-id", chatID)) return false, nil } - // If both the chat & the org have no permissions, the user is allowed to post - if o.config.CommunityDescription.Permissions.Access == protobuf.CommunityPermissions_NO_MEMBERSHIP && chat.Permissions.Access == protobuf.CommunityPermissions_NO_MEMBERSHIP { - return true, nil + if o.config.CommunityDescription.Members == nil { + o.config.Logger.Debug("Community.CanPost: no members in org", zap.String("chat-id", chatID)) + return false, nil } - if chat.Permissions.Access != protobuf.CommunityPermissions_NO_MEMBERSHIP { - if chat.Members == nil { - o.config.Logger.Debug("canPost, no members in chat", zap.String("chat-id", chatID)) + // If community member, also check chat membership next + _, ok = o.config.CommunityDescription.Members[common.PubkeyToHex(pk)] + if !ok { + o.config.Logger.Debug("Community.CanPost: not a community member", zap.String("chat-id", chatID)) + return false, nil + } + + if chat.Members == nil { + o.config.Logger.Debug("Community.CanPost: no members in chat", zap.String("chat-id", chatID)) + return false, nil + } + + // Need to also be a chat member to post + if !o.IsMemberInChat(pk, chatID) { + if grantBytes == nil { + o.config.Logger.Debug("Community.CanPost: not a chat member:", zap.String("chat-id", chatID)) return false, nil } - - _, ok := chat.Members[common.PubkeyToHex(pk)] - // If member, we stop here - if ok { - return true, nil - } - - // If not a member, and not grant, we return - if !ok && grantBytes == nil { - o.config.Logger.Debug("canPost, not a member in chat", zap.String("chat-id", chatID)) - return false, nil - } - - // Otherwise we verify the grant return o.canPostWithGrant(pk, chatID, grantBytes) } - // Chat has no membership, check org permissions - if o.config.CommunityDescription.Members == nil { - o.config.Logger.Debug("canPost, no members in org", zap.String("chat-id", chatID)) - return false, nil - } - - // If member, they can post - _, ok = o.config.CommunityDescription.Members[common.PubkeyToHex(pk)] - if ok { - return true, nil - } - - // Not a member and no grant, can't post - if !ok && grantBytes == nil { - o.config.Logger.Debug("canPost, not a member in org", zap.String("chat-id", chatID), zap.String("pubkey", common.PubkeyToHex(pk))) - return false, nil - } - - return o.canPostWithGrant(pk, chatID, grantBytes) + // all conditions satisfied, user can post after all + return true, nil } func (o *Community) canPostWithGrant(pk *ecdsa.PublicKey, chatID string, grantBytes []byte) (bool, error) { diff --git a/protocol/communities/community_test.go b/protocol/communities/community_test.go index 91b0ff731..19ac93ca9 100644 --- a/protocol/communities/community_test.go +++ b/protocol/communities/community_test.go @@ -511,7 +511,7 @@ func (s *CommunitySuite) TestCanPost() { name: "no-membership org with no-membeship chat", config: s.configNoMembershipOrgNoMembershipChat(), member: notMember, - canPost: true, + canPost: false, }, { name: "no-membership org with invitation only chat-not-a-member", @@ -529,7 +529,7 @@ func (s *CommunitySuite) TestCanPost() { name: "no-membership org with invitation only chat-not-a-member valid grant", config: s.configNoMembershipOrgInvitationOnlyChat(), member: notMember, - canPost: true, + canPost: false, grant: validGrant, }, { @@ -555,7 +555,7 @@ func (s *CommunitySuite) TestCanPost() { name: "membership org with no-membership chat not-a-member valid grant", config: s.configOnRequestOrgNoMembershipChat(), member: notMember, - canPost: true, + canPost: false, grant: validGrant, }, { diff --git a/protocol/communities_messenger_test.go b/protocol/communities_messenger_test.go index e097531cc..3e151fc29 100644 --- a/protocol/communities_messenger_test.go +++ b/protocol/communities_messenger_test.go @@ -442,18 +442,29 @@ func (s *MessengerCommunitiesSuite) TestCommunityContactCodeAdvertisement() { func (s *MessengerCommunitiesSuite) TestPostToCommunityChat() { community, chat := s.createCommunity() - s.advertiseCommunityTo(community, s.owner, s.alice) - s.joinCommunity(community, s.owner, s.alice) - - ctx := context.Background() - chatID := chat.ID inputMessage := common.NewMessage() inputMessage.ChatId = chatID inputMessage.ContentType = protobuf.ChatMessage_TEXT_PLAIN inputMessage.Text = "some text" + ctx := context.Background() + + s.advertiseCommunityTo(community, s.owner, s.alice) + + // Send message without even spectating fails _, err := s.alice.SendChatMessage(ctx, inputMessage) + s.Require().Error(err) + + // Sending a message without joining fails + _, err = s.alice.SpectateCommunity(community.ID()) + s.Require().NoError(err) + _, err = s.alice.SendChatMessage(ctx, inputMessage) + s.Require().Error(err) + + // Sending should work now + s.joinCommunity(community, s.owner, s.alice) + _, err = s.alice.SendChatMessage(ctx, inputMessage) s.Require().NoError(err) var response *MessengerResponse diff --git a/protocol/messenger.go b/protocol/messenger.go index 8a98682a7..f7a6d3c0e 100644 --- a/protocol/messenger.go +++ b/protocol/messenger.go @@ -2137,10 +2137,8 @@ func (m *Messenger) dispatchMessage(ctx context.Context, rawMessage common.RawMe return rawMessage, err } - // We allow emoji reactions by anyone - if rawMessage.MessageType != protobuf.ApplicationMetadataMessage_EMOJI_REACTION && !canPost { + if !canPost { m.logger.Error("can't post on chat", zap.String("chat-id", chat.ID), zap.String("chat-name", chat.Name)) - return rawMessage, errors.New("can't post on chat") } diff --git a/protocol/messenger_activity_center_test.go b/protocol/messenger_activity_center_test.go index 541b2929c..92fd23dd2 100644 --- a/protocol/messenger_activity_center_test.go +++ b/protocol/messenger_activity_center_test.go @@ -12,6 +12,7 @@ import ( "github.com/status-im/status-go/eth-node/crypto" "github.com/status-im/status-go/eth-node/types" "github.com/status-im/status-go/protocol/common" + "github.com/status-im/status-go/protocol/communities" "github.com/status-im/status-go/protocol/protobuf" "github.com/status-im/status-go/protocol/requests" "github.com/status-im/status-go/protocol/tt" @@ -23,6 +24,19 @@ func TestMessengerActivityCenterMessageSuite(t *testing.T) { suite.Run(t, new(MessengerActivityCenterMessageSuite)) } +func (s *MessengerActivityCenterMessageSuite) createCommunity(owner *Messenger) (*communities.Community, *Chat) { + return createCommunity(&s.Suite, owner) +} + +func (s *MessengerActivityCenterMessageSuite) advertiseCommunityTo(community *communities.Community, owner *Messenger, user *Messenger) { + advertiseCommunityTo(&s.Suite, community, owner, user) +} + +func (s *MessengerActivityCenterMessageSuite) joinCommunity(community *communities.Community, owner *Messenger, user *Messenger) { + request := &requests.RequestToJoinCommunity{CommunityID: community.ID()} + joinCommunity(&s.Suite, community, owner, user, request) +} + type MessengerActivityCenterMessageSuite struct { suite.Suite m *Messenger // main instance of Messenger @@ -117,75 +131,35 @@ func (s *MessengerActivityCenterMessageSuite) TestDeleteOneToOneChat() { } func (s *MessengerActivityCenterMessageSuite) TestEveryoneMentionTag() { - - description := &requests.CreateCommunity{ - Membership: protobuf.CommunityPermissions_NO_MEMBERSHIP, - Name: "status", - Color: "#ffffff", - Description: "status community description", - } - alice := s.m bob := s.newMessenger() _, err := bob.Start() s.Require().NoError(err) defer bob.Shutdown() // nolint: errcheck - // Create an community chat - response, err := bob.CreateCommunity(description, true) - s.Require().NoError(err) - s.Require().Len(response.Communities(), 1) - - community := response.Communities()[0] + // Create a community + community, chat := s.createCommunity(bob) s.Require().NotNil(community) - - chat := CreateOneToOneChat(common.PubkeyToHex(&alice.identity.PublicKey), &alice.identity.PublicKey, bob.transport) - - // bob sends a community message - inputMessage := common.NewMessage() - inputMessage.ChatId = chat.ID - inputMessage.Text = "some text" - inputMessage.CommunityID = community.IDString() - - err = bob.SaveChat(chat) - s.Require().NoError(err) - _, err = bob.SendChatMessage(context.Background(), inputMessage) - s.Require().NoError(err) - - _, err = WaitOnMessengerResponse( - alice, - func(r *MessengerResponse) bool { return len(r.Communities()) == 1 }, - "no messages", - ) - - s.Require().NoError(err) + s.Require().NotNil(chat) // Alice joins the community - response, err = alice.JoinCommunity(context.Background(), community.ID(), false) - s.Require().NoError(err) - s.Require().NotNil(response) - s.Require().Len(response.Communities(), 1) - s.Require().True(response.Communities()[0].Joined()) - s.Require().Len(response.Chats(), 1) + s.advertiseCommunityTo(community, bob, alice) + s.joinCommunity(community, bob, alice) - defaultCommunityChatID := response.Chats()[0].ID - - // bob sends a community message - inputMessage = common.NewMessage() - inputMessage.ChatId = defaultCommunityChatID + // alice sends a community message + inputMessage := common.NewMessage() + inputMessage.ChatId = chat.ID + inputMessage.ContentType = protobuf.ChatMessage_TEXT_PLAIN inputMessage.Text = "Good news, @" + common.EveryoneMentionTag + " !" - inputMessage.CommunityID = community.IDString() - response, err = alice.SendChatMessage(context.Background(), inputMessage) + response, err := alice.SendChatMessage(context.Background(), inputMessage) s.Require().NoError(err) - s.Require().Len(response.Messages(), 1) - s.Require().True(response.Messages()[0].Mentioned) response, err = WaitOnMessengerResponse( bob, - func(r *MessengerResponse) bool { return len(r.Messages()) == 1 }, + func(r *MessengerResponse) bool { return len(r.Messages()) >= 1 }, "no messages", ) @@ -199,14 +173,6 @@ func (s *MessengerActivityCenterMessageSuite) TestEveryoneMentionTag() { } func (s *MessengerActivityCenterMessageSuite) TestReplyWithImage() { - - description := &requests.CreateCommunity{ - Membership: protobuf.CommunityPermissions_NO_MEMBERSHIP, - Name: "status", - Color: "#ffffff", - Description: "status community description", - } - alice := s.m bob := s.newMessenger() _, err := bob.Start() @@ -218,73 +184,36 @@ func (s *MessengerActivityCenterMessageSuite) TestReplyWithImage() { s.Require().NoError(err) s.Require().NotNil(mediaServer) s.Require().NoError(mediaServer.Start()) - alice.httpServer = mediaServer - // Create an community chat - response, err := bob.CreateCommunity(description, true) - s.Require().NoError(err) - s.Require().Len(response.Communities(), 1) - - community := response.Communities()[0] + // Create a community + community, chat := s.createCommunity(bob) s.Require().NotNil(community) - - chat := CreateOneToOneChat(common.PubkeyToHex(&alice.identity.PublicKey), &alice.identity.PublicKey, bob.transport) - - // bob sends a community message - inputMessage := common.NewMessage() - inputMessage.ChatId = chat.ID - inputMessage.Text = "some text" - inputMessage.CommunityID = community.IDString() - - err = bob.SaveChat(chat) - s.Require().NoError(err) - _, err = bob.SendChatMessage(context.Background(), inputMessage) - s.Require().NoError(err) - - _, err = WaitOnMessengerResponse( - alice, - func(r *MessengerResponse) bool { return len(r.Communities()) == 1 }, - "no messages", - ) - - s.Require().NoError(err) + s.Require().NotNil(chat) // Alice joins the community - response, err = alice.JoinCommunity(context.Background(), community.ID(), false) - s.Require().NoError(err) - s.Require().NotNil(response) - s.Require().Len(response.Communities(), 1) - s.Require().True(response.Communities()[0].Joined()) - s.Require().Len(response.Chats(), 1) + s.advertiseCommunityTo(community, bob, alice) + s.joinCommunity(community, bob, alice) - defaultCommunityChat := response.Chats()[0] - - defaultCommunityChatID := defaultCommunityChat.ID - - // bob sends a community message - inputMessage = common.NewMessage() - inputMessage.ChatId = defaultCommunityChatID + // Alice sends a community message + inputMessage := common.NewMessage() + inputMessage.ChatId = chat.ID + inputMessage.ContentType = protobuf.ChatMessage_TEXT_PLAIN inputMessage.Text = "test message" - inputMessage.CommunityID = community.IDString() - response, err = alice.SendChatMessage(context.Background(), inputMessage) + response, err := alice.SendChatMessage(context.Background(), inputMessage) s.Require().NoError(err) - s.Require().Len(response.Messages(), 1) - response, err = WaitOnMessengerResponse( bob, func(r *MessengerResponse) bool { return len(r.Messages()) == 1 }, "no messages", ) - s.Require().NoError(err) - s.Require().Len(response.Messages(), 1) - // bob sends a community message - inputMessage, err = buildImageWithAlbumIDMessage(*defaultCommunityChat, "0x34") + // bob sends a reply with an image + inputMessage, err = buildImageWithAlbumIDMessage(*chat, "0x34") s.Require().NoError(err) inputMessage.Text = "test message reply" @@ -292,25 +221,22 @@ func (s *MessengerActivityCenterMessageSuite) TestReplyWithImage() { response, err = bob.SendChatMessage(context.Background(), inputMessage) s.Require().NoError(err) - s.Require().Len(response.Messages(), 2) - response, err = WaitOnMessengerResponse( alice, func(r *MessengerResponse) bool { return len(r.Messages()) == 2 }, "no messages", ) - s.Require().NoError(err) s.Require().Len(response.ActivityCenterNotifications(), 1) + // verify the new message var newMessage *common.Message for _, m := range response.Messages() { if m.Text == "test message reply" { newMessage = m } } - s.Require().NotNil(newMessage) s.Require().Equal(protobuf.ChatMessage_IMAGE, newMessage.ContentType) s.Require().NotEmpty(newMessage.ImageLocalURL) @@ -331,57 +257,19 @@ func (s *MessengerActivityCenterMessageSuite) TestReplyWithImage() { } func (s *MessengerActivityCenterMessageSuite) TestMuteCommunityActivityCenterNotifications() { - - description := &requests.CreateCommunity{ - Membership: protobuf.CommunityPermissions_NO_MEMBERSHIP, - Name: "status", - Color: "#ffffff", - Description: "status community description", - } - alice := s.m bob := s.newMessenger() _, err := bob.Start() s.Require().NoError(err) - // Create an community chat - response, err := bob.CreateCommunity(description, true) - s.Require().NoError(err) - s.Require().Len(response.Communities(), 1) - - community := response.Communities()[0] + // Create a community + community, chat := s.createCommunity(bob) s.Require().NotNil(community) - - chat := CreateOneToOneChat(common.PubkeyToHex(&alice.identity.PublicKey), &alice.identity.PublicKey, bob.transport) - - // bob sends a community message - inputMessage := common.NewMessage() - inputMessage.ChatId = chat.ID - inputMessage.Text = "some text" - inputMessage.CommunityID = community.IDString() - - err = bob.SaveChat(chat) - s.Require().NoError(err) - _, err = bob.SendChatMessage(context.Background(), inputMessage) - s.Require().NoError(err) - - _, err = WaitOnMessengerResponse( - alice, - func(r *MessengerResponse) bool { return len(r.Communities()) == 1 }, - "no messages", - ) - - s.Require().NoError(err) + s.Require().NotNil(chat) // Alice joins the community - response, err = alice.JoinCommunity(context.Background(), community.ID(), true) - s.Require().NoError(err) - s.Require().NotNil(response) - s.Require().Len(response.Communities(), 1) - s.Require().True(response.Communities()[0].Joined()) - s.Require().Len(response.Chats(), 1) - - defaultCommunityChatID := response.Chats()[0].ID + s.advertiseCommunityTo(community, bob, alice) + s.joinCommunity(community, bob, alice) // Bob mutes the community time, err := bob.MuteAllCommunityChats(&requests.MuteCommunity{ @@ -396,16 +284,14 @@ func (s *MessengerActivityCenterMessageSuite) TestMuteCommunityActivityCenterNot s.Require().True(bobCommunity.Muted()) // alice sends a community message - inputMessage = common.NewMessage() - inputMessage.ChatId = defaultCommunityChatID + inputMessage := common.NewMessage() + inputMessage.ChatId = chat.ID + inputMessage.ContentType = protobuf.ChatMessage_TEXT_PLAIN inputMessage.Text = "Good news, @" + common.EveryoneMentionTag + " !" - inputMessage.CommunityID = community.IDString() - response, err = alice.SendChatMessage(context.Background(), inputMessage) + response, err := alice.SendChatMessage(context.Background(), inputMessage) s.Require().NoError(err) - s.Require().Len(response.Messages(), 1) - s.Require().True(response.Messages()[0].Mentioned) response, err = WaitOnMessengerResponse( @@ -414,10 +300,9 @@ func (s *MessengerActivityCenterMessageSuite) TestMuteCommunityActivityCenterNot "no messages", ) + // Bob still receives it, but no AC notif s.Require().NoError(err) - s.Require().Len(response.Messages(), 1) - s.Require().True(response.Messages()[0].Mentioned) s.Require().Len(response.ActivityCenterNotifications(), 0) } diff --git a/protocol/messenger_handler.go b/protocol/messenger_handler.go index 6db760847..8b92bbb3b 100644 --- a/protocol/messenger_handler.go +++ b/protocol/messenger_handler.go @@ -2625,31 +2625,27 @@ func (m *Messenger) matchChatEntity(chatEntity common.ChatEntity) (*Chat, error) return nil, errors.New("not an community chat") } - var emojiReaction bool - var pinMessage bool - // We allow emoji reactions from anyone - switch chatEntity.(type) { - case *EmojiReaction: - emojiReaction = true - case *common.PinMessage: - pinMessage = true - } - canPost, err := m.communitiesManager.CanPost(chatEntity.GetSigPubKey(), chat.CommunityID, chat.CommunityChatID(), chatEntity.GetGrant()) if err != nil { return nil, err } - community, err := m.communitiesManager.GetByIDString(chat.CommunityID) - if err != nil { - return nil, err + if !canPost { + return nil, errors.New("user can't post in community") } - hasPermission := community.IsPrivilegedMember(chatEntity.GetSigPubKey()) - pinMessageAllowed := community.AllowsAllMembersToPinMessage() + _, isPinMessage := chatEntity.(*common.PinMessage) + if isPinMessage { + community, err := m.communitiesManager.GetByIDString(chat.CommunityID) + if err != nil { + return nil, err + } - if (pinMessage && !hasPermission && !pinMessageAllowed) || (!emojiReaction && !canPost) { - return nil, errors.New("user can't post") + hasPermission := community.IsPrivilegedMember(chatEntity.GetSigPubKey()) + pinMessageAllowed := community.AllowsAllMembersToPinMessage() + if !hasPermission && !pinMessageAllowed { + return nil, errors.New("user can't pin message") + } } return chat, nil