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
This commit is contained in:
Jonathan Rainville 2024-05-01 13:27:31 -04:00 committed by GitHub
parent 2fb6d615fd
commit 852a5beb39
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 121 additions and 26 deletions

View File

@ -1001,6 +1001,13 @@ func (o *Community) Edit(description *protobuf.CommunityDescription) {
o.config.CommunityDescription.AdminSettings.PinMessageAllMembersEnabled = description.AdminSettings.PinMessageAllMembersEnabled 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() { func (o *Community) Join() {
o.config.Joined = true o.config.Joined = true
o.config.JoinedAt = time.Now().Unix() o.config.JoinedAt = time.Now().Unix()

View File

@ -59,6 +59,9 @@ var pieceLength = 100 * 1024
const maxArchiveSizeInBytes = 30000000 const maxArchiveSizeInBytes = 30000000
var maxNbMembers = 5000
var maxNbPendingRequestedMembers = 100
var memberPermissionsCheckInterval = 1 * time.Hour var memberPermissionsCheckInterval = 1 * time.Hour
var validateInterval = 2 * time.Minute var validateInterval = 2 * time.Minute
@ -66,6 +69,12 @@ var validateInterval = 2 * time.Minute
func SetValidateInterval(duration time.Duration) { func SetValidateInterval(duration time.Duration) {
validateInterval = duration validateInterval = duration
} }
func SetMaxNbMembers(maxNb int) {
maxNbMembers = maxNb
}
func SetMaxNbPendingRequestedMembers(maxNb int) {
maxNbPendingRequestedMembers = maxNb
}
// errors // errors
var ( var (
@ -1293,7 +1302,7 @@ func (m *Manager) EditCommunity(request *requests.EditCommunity) (*Community, er
newDescription, err := request.ToCommunityDescription() newDescription, err := request.ToCommunityDescription()
if err != nil { 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 // 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 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{ requestToJoin := &RequestToJoin{
PublicKey: common.PubkeyToHex(signer), PublicKey: common.PubkeyToHex(signer),
Clock: request.Clock, 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 // If user is already a member, then accept request automatically
// It may happen when member removes itself from community and then tries to rejoin // 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 // More specifically, CommunityRequestToLeave may be delivered later than CommunityRequestToJoin, or not delivered at all

View File

@ -842,6 +842,15 @@ func (p *Persistence) GetRequestToJoin(id []byte) (*RequestToJoin, error) {
return request, nil 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) { func (p *Persistence) GetRequestToJoinByPkAndCommunityID(pk string, communityID []byte) (*RequestToJoin, error) {
request := &RequestToJoin{} 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) 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)

View File

@ -9,6 +9,7 @@ import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"reflect"
"strings" "strings"
"testing" "testing"
"time" "time"
@ -1658,31 +1659,7 @@ func (s *MessengerCommunitiesSuite) TestRequestAccessAgain() {
community := response.Communities()[0] community := response.Communities()[0]
chat := CreateOneToOneChat(common.PubkeyToHex(&s.alice.identity.PublicKey), &s.alice.identity.PublicKey, s.alice.transport) s.advertiseCommunityTo(community, s.bob, s.alice)
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)
request := &requests.RequestToJoinCommunity{CommunityID: community.ID()} request := &requests.RequestToJoinCommunity{CommunityID: community.ID()}
// We try to join the org // We try to join the org
@ -3514,6 +3491,82 @@ func (s *MessengerCommunitiesSuite) TestCommunityBanUserRequestToJoin() {
s.Require().ErrorContains(err, "can't request access") 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() { func (s *MessengerCommunitiesSuite) TestHandleImport() {
community, chat := s.createCommunity() community, chat := s.createCommunity()