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
This commit is contained in:
Shinnok 2023-10-25 17:26:18 +03:00 committed by GitHub
parent 9d59d889f6
commit 3805662a18
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 106 additions and 234 deletions

View File

@ -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) {

View File

@ -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,
},
{

View File

@ -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

View File

@ -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")
}

View File

@ -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)
}

View File

@ -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