From 852a5beb3984f100c0a96b0161f1ddef76850c57 Mon Sep 17 00:00:00 2001 From: Jonathan Rainville Date: Wed, 1 May 2024 13:27:31 -0400 Subject: [PATCH] feat_: limit number of members in a community and number of pending requests (#5107) * feat(community): limit nb of requests to join and members Needed for https://github.com/status-im/status-desktop/issues/14532 * chore: simplify TestRequestAccessAgain * chore: add a test for the member limit --- protocol/communities/community.go | 7 ++ protocol/communities/manager.go | 28 ++++++- protocol/communities/persistence.go | 9 +++ protocol/communities_messenger_test.go | 103 +++++++++++++++++++------ 4 files changed, 121 insertions(+), 26 deletions(-) diff --git a/protocol/communities/community.go b/protocol/communities/community.go index a29f43bc1..0e3779347 100644 --- a/protocol/communities/community.go +++ b/protocol/communities/community.go @@ -1001,6 +1001,13 @@ func (o *Community) Edit(description *protobuf.CommunityDescription) { o.config.CommunityDescription.AdminSettings.PinMessageAllMembersEnabled = description.AdminSettings.PinMessageAllMembersEnabled } +func (o *Community) EditPermissionAccess(permissionAccess protobuf.CommunityPermissions_Access) { + o.config.CommunityDescription.Permissions.Access = permissionAccess + if o.IsControlNode() { + o.increaseClock() + } +} + func (o *Community) Join() { o.config.Joined = true o.config.JoinedAt = time.Now().Unix() diff --git a/protocol/communities/manager.go b/protocol/communities/manager.go index 331aee986..6c0f1f78d 100644 --- a/protocol/communities/manager.go +++ b/protocol/communities/manager.go @@ -59,6 +59,9 @@ var pieceLength = 100 * 1024 const maxArchiveSizeInBytes = 30000000 +var maxNbMembers = 5000 +var maxNbPendingRequestedMembers = 100 + var memberPermissionsCheckInterval = 1 * time.Hour var validateInterval = 2 * time.Minute @@ -66,6 +69,12 @@ var validateInterval = 2 * time.Minute func SetValidateInterval(duration time.Duration) { validateInterval = duration } +func SetMaxNbMembers(maxNb int) { + maxNbMembers = maxNb +} +func SetMaxNbPendingRequestedMembers(maxNb int) { + maxNbPendingRequestedMembers = maxNb +} // errors var ( @@ -1293,7 +1302,7 @@ func (m *Manager) EditCommunity(request *requests.EditCommunity) (*Community, er newDescription, err := request.ToCommunityDescription() if err != nil { - return nil, fmt.Errorf("Can't create community description: %v", err) + return nil, fmt.Errorf("can't create community description: %v", err) } // If permissions weren't explicitly set on original request, use existing ones @@ -2645,6 +2654,14 @@ func (m *Manager) HandleCommunityRequestToJoin(signer *ecdsa.PublicKey, receiver return nil, nil, err } + nbPendingRequestsToJoin, err := m.persistence.GetNumberOfPendingRequestsToJoin(community.ID()) + if err != nil { + return nil, nil, err + } + if nbPendingRequestsToJoin >= maxNbPendingRequestedMembers { + return nil, nil, errors.New("max number of requests to join reached") + } + requestToJoin := &RequestToJoin{ PublicKey: common.PubkeyToHex(signer), Clock: request.Clock, @@ -2737,6 +2754,15 @@ func (m *Manager) HandleCommunityRequestToJoin(signer *ecdsa.PublicKey, receiver } } + // Check if we reached the limit, if we did, change the community setting to be On Request + if community.AutoAccept() && community.MembersCount() >= maxNbMembers { + community.EditPermissionAccess(protobuf.CommunityPermissions_MANUAL_ACCEPT) + err = m.saveAndPublish(community) + if err != nil { + return nil, nil, err + } + } + // If user is already a member, then accept request automatically // It may happen when member removes itself from community and then tries to rejoin // More specifically, CommunityRequestToLeave may be delivered later than CommunityRequestToJoin, or not delivered at all diff --git a/protocol/communities/persistence.go b/protocol/communities/persistence.go index 2ea3aa940..aac58afed 100644 --- a/protocol/communities/persistence.go +++ b/protocol/communities/persistence.go @@ -842,6 +842,15 @@ func (p *Persistence) GetRequestToJoin(id []byte) (*RequestToJoin, error) { return request, nil } +func (p *Persistence) GetNumberOfPendingRequestsToJoin(communityID types.HexBytes) (int, error) { + var count int + err := p.db.QueryRow(`SELECT count(1) FROM communities_requests_to_join WHERE community_id = ? AND state = ?`, communityID, RequestToJoinStatePending).Scan(&count) + if err != nil { + return 0, err + } + return count, nil +} + func (p *Persistence) GetRequestToJoinByPkAndCommunityID(pk string, communityID []byte) (*RequestToJoin, error) { request := &RequestToJoin{} err := p.db.QueryRow(`SELECT id,public_key,clock,ens_name,customization_color,chat_id,community_id,state FROM communities_requests_to_join WHERE public_key = ? AND community_id = ?`, pk, communityID).Scan(&request.ID, &request.PublicKey, &request.Clock, &request.ENSName, &request.CustomizationColor, &request.ChatID, &request.CommunityID, &request.State) diff --git a/protocol/communities_messenger_test.go b/protocol/communities_messenger_test.go index c08748e05..b349ac2d5 100644 --- a/protocol/communities_messenger_test.go +++ b/protocol/communities_messenger_test.go @@ -9,6 +9,7 @@ import ( "fmt" "io/ioutil" "os" + "reflect" "strings" "testing" "time" @@ -1658,31 +1659,7 @@ func (s *MessengerCommunitiesSuite) TestRequestAccessAgain() { community := response.Communities()[0] - chat := CreateOneToOneChat(common.PubkeyToHex(&s.alice.identity.PublicKey), &s.alice.identity.PublicKey, s.alice.transport) - - s.Require().NoError(s.bob.SaveChat(chat)) - - message := buildTestMessage(*chat) - message.CommunityID = community.IDString() - - // We send a community link to alice - response, err = s.bob.SendChatMessage(context.Background(), message) - s.Require().NoError(err) - s.Require().NotNil(response) - - // Retrieve community link & community - err = tt.RetryWithBackOff(func() error { - response, err = s.alice.RetrieveAll() - if err != nil { - return err - } - if len(response.Communities()) == 0 { - return errors.New("message not received") - } - return nil - }) - - s.Require().NoError(err) + s.advertiseCommunityTo(community, s.bob, s.alice) request := &requests.RequestToJoinCommunity{CommunityID: community.ID()} // We try to join the org @@ -3514,6 +3491,82 @@ func (s *MessengerCommunitiesSuite) TestCommunityBanUserRequestToJoin() { s.Require().ErrorContains(err, "can't request access") } +func (s *MessengerCommunitiesSuite) TestCommunityMaxNumberOfMembers() { + john := s.newMessenger() + _, err := john.Start() + s.Require().NoError(err) + + defer TearDownMessenger(&s.Suite, john) + + // Bring back the original values + defer communities.SetMaxNbMembers(5000) + defer communities.SetMaxNbPendingRequestedMembers(100) + + community, _ := s.createCommunity() + + communities.SetMaxNbMembers(2) + communities.SetMaxNbPendingRequestedMembers(1) + + s.advertiseCommunityTo(community, s.owner, s.alice) + s.advertiseCommunityTo(community, s.owner, s.bob) + s.advertiseCommunityTo(community, s.owner, john) + + // Alice joins the community correctly + s.joinCommunity(community, s.owner, s.alice) + + // Bob also tries to join, but he will be put in the requests to join to approve and won't join + request := &requests.RequestToJoinCommunity{CommunityID: community.ID()} + response, err := s.bob.RequestToJoinCommunity(request) + s.Require().NoError(err) + s.Require().NotNil(response) + s.Require().Len(response.RequestsToJoinCommunity(), 1) + requestID := response.RequestsToJoinCommunity()[0].ID + + response, err = WaitOnMessengerResponse( + s.owner, + func(r *MessengerResponse) bool { + for _, req := range r.RequestsToJoinCommunity() { + if reflect.DeepEqual(req.ID, requestID) { + return true + } + } + return false + }, + "no request to join", + ) + s.Require().NoError(err) + s.Require().Len(response.RequestsToJoinCommunity(), 1) + s.Require().Equal(communities.RequestToJoinStatePending, response.RequestsToJoinCommunity()[0].State) + + // We confirm that there are still 2 members only and the access setting is now manual + updatedCommunity, err := s.owner.communitiesManager.GetByID(community.ID()) + s.Require().NoError(err) + s.Require().Len(updatedCommunity.Members(), 2) + s.Require().Equal(protobuf.CommunityPermissions_MANUAL_ACCEPT, updatedCommunity.Permissions().Access) + + // John also tries to join, but he his request will be ignored as it exceeds the max number of pending requests + requestJohn := &requests.RequestToJoinCommunity{CommunityID: community.ID()} + response, err = john.RequestToJoinCommunity(requestJohn) + s.Require().NoError(err) + s.Require().NotNil(response) + s.Require().Len(response.RequestsToJoinCommunity(), 1) + requestJohnID := response.RequestsToJoinCommunity()[0].ID + + _, err = WaitOnMessengerResponse( + s.owner, + func(r *MessengerResponse) bool { + for _, req := range r.RequestsToJoinCommunity() { + if reflect.DeepEqual(req.ID, requestJohnID) { + return true + } + } + return false + }, + "no request to join", + ) + s.Require().Error(err) +} + func (s *MessengerCommunitiesSuite) TestHandleImport() { community, chat := s.createCommunity()