From 248e4a7f24690dacfaad2a0bea3b68482a18d64f Mon Sep 17 00:00:00 2001 From: Pascal Precht <445106+0x-r4bbit@users.noreply.github.com> Date: Wed, 2 Aug 2023 14:04:47 +0200 Subject: [PATCH] refactor: EventSenders forward RequestToJoin decision to control node This is a bigger change in how community membership requests are handled among admins, token masters, owners, and control nodes. Prior to this commit, all privileged users, also known as `EventSenders`, were able to accept and reject community membership requests and those changes would be applied by all users. This commit changes this behaviour such that: 1. EventSenders can make a decision (accept, reject), but merely forward their decision to the control node, which ultimately has to confirm it 2. EventSenders are no longer removing or adding members to and from communities 3. When an eventsender signaled a decision, the membership request will enter a pending state (acceptedPending or rejectedPending) 4. Once a decision was made by one eventsender, no other eventsender can override that decision This implementation is covered with a bunch of tests: - Ensure that decision made by event sender is shared with other event senders - `testAcceptMemberRequestToJoinResponseSharedWithOtherEventSenders()` - `testRejectMemberRequestToJoinResponseSharedWithOtherEventSenders()` - Ensure memebrship request stays pending, until control node has confirmed decision by event senders - `testAcceptMemberRequestToJoinNotConfirmedByControlNode()` - `testRejectMemberRequestToJoinNotConfirmedByControlNode()` - Ensure that decision made by event sender cannot be overriden by other event senders - `testEventSenderCannotOverrideRequestToJoinState()` These test cases live in three test suites for different event sender types respectively - `OwnerWithoutCommunityKeyCommunityEventsSuite` - `TokenMasterCommunityEventsSuite` - `AdminCommunityEventsSuite` In addition to the changes mentioned above, there's also a smaller changes that ensures membership requests to *not* attached revealed wallet addresses when the requests are sent to event senders (in addition to control nodes). Requests send to a control node will still include revealed addresses as the control node needs them to verify token permissions. This commit does not yet handle the case of event senders attempting to kick and ban members. Similar to accepting and rejecting membership requests, kicking and banning need a new pending state. However, we don't track such state in local databases yet so those two cases will be handled in future commit to not have this commit grow larger. --- protocol/activity_center.go | 2 + protocol/communities/community.go | 32 +- protocol/communities/community_event.go | 21 - .../communities/community_event_message.go | 2 +- protocol/communities/manager.go | 366 ++++++++++---- protocol/communities/persistence.go | 8 + protocol/communities/request_to_join.go | 6 + ...events_owner_without_community_key_test.go | 52 +- .../communities_events_token_master_test.go | 52 +- protocol/communities_events_utils_test.go | 465 ++++++++++++++++-- protocol/communities_messenger_admin_test.go | 53 +- protocol/messenger_communities.go | 60 ++- protocol/messenger_handler.go | 8 +- 13 files changed, 942 insertions(+), 185 deletions(-) diff --git a/protocol/activity_center.go b/protocol/activity_center.go index 2219f9786..45b558db9 100644 --- a/protocol/activity_center.go +++ b/protocol/activity_center.go @@ -37,6 +37,8 @@ const ( ActivityCenterMembershipStatusPending ActivityCenterMembershipStatusAccepted ActivityCenterMembershipStatusDeclined + ActivityCenterMembershipStatusAcceptedPending + ActivityCenterMembershipStatusDeclinedPending ) type ActivityCenterQueryParamsRead uint diff --git a/protocol/communities/community.go b/protocol/communities/community.go index 9ee6cc71e..86b03ee60 100644 --- a/protocol/communities/community.go +++ b/protocol/communities/community.go @@ -753,7 +753,6 @@ func (o *Community) RemoveUserFromOrg(pk *ecdsa.PublicKey) (*protobuf.CommunityD } o.removeMemberFromOrg(pk) - if isControlNode { o.increaseClock() } @@ -1940,31 +1939,14 @@ func (o *Community) AddMemberWithRevealedAccounts(dbRequest *RequestToJoin, role defer o.mutex.Unlock() isControlNode := o.IsControlNode() - allowedToSendEvents := o.HasPermissionToSendCommunityEvents() - if !isControlNode && !allowedToSendEvents { + if !isControlNode { return nil, ErrNotAdmin } changes := o.addMemberWithRevealedAccounts(dbRequest.PublicKey, roles, accounts, dbRequest.Clock) - if allowedToSendEvents { - acceptedRequestsToJoin := make(map[string]*protobuf.CommunityRequestToJoin) - acceptedRequestsToJoin[dbRequest.PublicKey] = dbRequest.ToCommunityRequestToJoinProtobuf() - - adminChanges := &CommunityEventChanges{ - CommunityChanges: changes, - AcceptedRequestsToJoin: acceptedRequestsToJoin, - } - err := o.addNewCommunityEvent(o.ToCommunityRequestToJoinAcceptCommunityEvent(adminChanges)) - if err != nil { - return nil, err - } - } - - if isControlNode { - o.increaseClock() - } + o.increaseClock() return changes, nil } @@ -2130,16 +2112,6 @@ func (o *Community) deleteChat(chatID string) *CommunityChanges { return changes } -func (o *Community) addCommunityMember(pk *ecdsa.PublicKey, member *protobuf.CommunityMember) { - - if o.config.CommunityDescription.Members == nil { - o.config.CommunityDescription.Members = make(map[string]*protobuf.CommunityMember) - } - - memberKey := common.PubkeyToHex(pk) - o.config.CommunityDescription.Members[memberKey] = member -} - func (o *Community) addTokenPermission(permission *protobuf.CommunityTokenPermission) (*CommunityChanges, error) { if o.config.CommunityDescription.TokenPermissions == nil { o.config.CommunityDescription.TokenPermissions = make(map[string]*protobuf.CommunityTokenPermission) diff --git a/protocol/communities/community_event.go b/protocol/communities/community_event.go index 3cc5a5860..e4de01eac 100644 --- a/protocol/communities/community_event.go +++ b/protocol/communities/community_event.go @@ -160,7 +160,6 @@ func (o *Community) ToCommunityRequestToJoinAcceptCommunityEvent(changes *Commun return &CommunityEvent{ CommunityEventClock: o.NewCommunityEventClock(), Type: protobuf.CommunityEvent_COMMUNITY_REQUEST_TO_JOIN_ACCEPT, - MembersAdded: changes.MembersAdded, AcceptedRequestsToJoin: changes.AcceptedRequestsToJoin, } } @@ -323,26 +322,6 @@ func (o *Community) updateCommunityDescriptionByCommunityEvent(communityEvent Co return err } - case protobuf.CommunityEvent_COMMUNITY_REQUEST_TO_JOIN_ACCEPT: - for pkString, addedMember := range communityEvent.MembersAdded { - pk, err := common.HexToPubkey(pkString) - if err != nil { - return err - } - if !o.HasMember(pk) { - o.addCommunityMember(pk, addedMember) - } - } - - case protobuf.CommunityEvent_COMMUNITY_REQUEST_TO_JOIN_REJECT: - for pkString := range communityEvent.RejectedRequestsToJoin { - pk, err := common.HexToPubkey(pkString) - if err != nil { - return err - } - o.removeMemberFromOrg(pk) - } - case protobuf.CommunityEvent_COMMUNITY_MEMBER_KICK: pk, err := common.HexToPubkey(communityEvent.MemberToAction) if err != nil { diff --git a/protocol/communities/community_event_message.go b/protocol/communities/community_event_message.go index 30c0fb033..c37823805 100644 --- a/protocol/communities/community_event_message.go +++ b/protocol/communities/community_event_message.go @@ -192,7 +192,7 @@ func validateCommunityEvent(communityEvent *CommunityEvent) error { } case protobuf.CommunityEvent_COMMUNITY_REQUEST_TO_JOIN_ACCEPT: - if len(communityEvent.MembersAdded) == 0 { + if communityEvent.AcceptedRequestsToJoin == nil { return errors.New("invalid community request to join accepted event") } diff --git a/protocol/communities/manager.go b/protocol/communities/manager.go index 7b20d1926..b0269be8d 100644 --- a/protocol/communities/manager.go +++ b/protocol/communities/manager.go @@ -287,6 +287,8 @@ type Subscription struct { DownloadingHistoryArchivesFinishedSignal *signal.DownloadingHistoryArchivesFinishedSignal ImportingHistoryArchiveMessagesSignal *signal.ImportingHistoryArchiveMessagesSignal CommunityEventsMessage *CommunityEventsMessage + AcceptedRequestsToJoin []types.HexBytes + RejectedRequestsToJoin []types.HexBytes } type CommunityResponse struct { @@ -1353,42 +1355,8 @@ func (m *Manager) HandleCommunityEventsMessage(signer *ecdsa.PublicKey, message func (m *Manager) handleAdditionalAdminChanges(community *Community) error { - saveOrUpdateRequestToJoin := func(signer string, request *protobuf.CommunityRequestToJoin, state RequestToJoinState) error { - requestToJoin := &RequestToJoin{ - PublicKey: signer, - Clock: request.Clock, - ENSName: request.EnsName, - CommunityID: request.CommunityId, - State: state, - RevealedAccounts: request.RevealedAccounts, - } - - requestToJoin.CalculateID() - - existingRequestToJoin, err := m.persistence.GetRequestToJoin(requestToJoin.ID) - if err != nil && err != sql.ErrNoRows { - return err - } - - if existingRequestToJoin != nil { - // node already knows about this request to join, so let's compare clocks - // and update it if necessary - if existingRequestToJoin.Clock <= requestToJoin.Clock { - pk, err := common.HexToPubkey(existingRequestToJoin.PublicKey) - if err != nil { - return err - } - err = m.persistence.SetRequestToJoinState(common.PubkeyToHex(pk), community.ID(), state) - if err != nil { - return err - } - } - } else { - err := m.persistence.SaveRequestToJoin(requestToJoin) - if err != nil { - return err - } - } + if !(community.IsControlNode() || community.HasPermissionToSendCommunityEvents()) { + // we're a normal user/member node, so there's nothing for us to do here return nil } @@ -1396,19 +1364,15 @@ func (m *Manager) handleAdditionalAdminChanges(community *Community) error { communityEvent := &community.config.EventsData.Events[i] switch communityEvent.Type { case protobuf.CommunityEvent_COMMUNITY_REQUEST_TO_JOIN_ACCEPT: - for signer, request := range communityEvent.AcceptedRequestsToJoin { - err := saveOrUpdateRequestToJoin(signer, request, RequestToJoinStateAccepted) - if err != nil { - return err - } + err := m.handleCommunityEventRequestAccepted(community, communityEvent) + if err != nil { + return err } case protobuf.CommunityEvent_COMMUNITY_REQUEST_TO_JOIN_REJECT: - for signer, request := range communityEvent.RejectedRequestsToJoin { - err := saveOrUpdateRequestToJoin(signer, request, RequestToJoinStateDeclined) - if err != nil { - return err - } + err := m.handleCommunityEventRequestRejected(community, communityEvent) + if err != nil { + return err } default: @@ -1417,6 +1381,132 @@ func (m *Manager) handleAdditionalAdminChanges(community *Community) error { return nil } +func (m *Manager) saveOrUpdateRequestToJoin(signer string, communityID types.HexBytes, requestToJoin *RequestToJoin) (bool, error) { + updated := false + + existingRequestToJoin, err := m.persistence.GetRequestToJoin(requestToJoin.ID) + if err != nil && err != sql.ErrNoRows { + return updated, err + } + + if existingRequestToJoin != nil { + // node already knows about this request to join, so let's compare clocks + // and update it if necessary + if existingRequestToJoin.Clock <= requestToJoin.Clock { + pk, err := common.HexToPubkey(existingRequestToJoin.PublicKey) + if err != nil { + return updated, err + } + err = m.persistence.SetRequestToJoinState(common.PubkeyToHex(pk), communityID, requestToJoin.State) + if err != nil { + return updated, err + } + updated = true + } + } else { + err := m.persistence.SaveRequestToJoin(requestToJoin) + if err != nil { + return updated, err + } + } + return updated, nil +} + +func (m *Manager) handleCommunityEventRequestAccepted(community *Community, communityEvent *CommunityEvent) error { + requestToJoinState := RequestToJoinStateAccepted + if community.HasPermissionToSendCommunityEvents() { + // if we're an admin and we receive this admin event, we know the state is `pending` + requestToJoinState = RequestToJoinStateAcceptedPending + } + + acceptedRequestsToJoin := make([]types.HexBytes, 0) + + for signer, request := range communityEvent.AcceptedRequestsToJoin { + requestToJoin := &RequestToJoin{ + PublicKey: signer, + Clock: request.Clock, + ENSName: request.EnsName, + CommunityID: request.CommunityId, + State: requestToJoinState, + } + requestToJoin.CalculateID() + + if community.HasPermissionToSendCommunityEvents() { + existingRequestToJoin, err := m.persistence.GetRequestToJoin(requestToJoin.ID) + if err != nil && err != sql.ErrNoRows { + return err + } + if existingRequestToJoin.MarkedAsPendingByPrivilegedAccount() { + // the request is already in some pending state so we won't override it again + continue + } + } + + requestUpdated, err := m.saveOrUpdateRequestToJoin(signer, community.ID(), requestToJoin) + if err != nil { + return err + } + + if community.IsControlNode() && requestUpdated { + // We only collect requestToJoinIDs which had a state update. + // If there wasn't a state update, it means we've seen the request for the first time, + // which means we don't have revealed addresses here (as they aren't propagated by + // admin nodes), so we don't want to trigger an `AcceptRequestToJoin` in such cases. + acceptedRequestsToJoin = append(acceptedRequestsToJoin, requestToJoin.ID) + } + } + if community.IsControlNode() { + m.publish(&Subscription{AcceptedRequestsToJoin: acceptedRequestsToJoin}) + } + return nil +} + +func (m *Manager) handleCommunityEventRequestRejected(community *Community, communityEvent *CommunityEvent) error { + requestToJoinState := RequestToJoinStateDeclined + if community.HasPermissionToSendCommunityEvents() { + // if we're an admin and we receive this admin event, we want to see the same + // state that the other admin has decided for + requestToJoinState = RequestToJoinStateDeclinedPending + } + + rejectedRequestsToJoin := make([]types.HexBytes, 0) + + for signer, request := range communityEvent.RejectedRequestsToJoin { + requestToJoin := &RequestToJoin{ + PublicKey: signer, + Clock: request.Clock, + ENSName: request.EnsName, + CommunityID: request.CommunityId, + State: requestToJoinState, + } + requestToJoin.CalculateID() + + if community.HasPermissionToSendCommunityEvents() { + existingRequestToJoin, err := m.persistence.GetRequestToJoin(requestToJoin.ID) + if err != nil && err != sql.ErrNoRows { + return err + } + if existingRequestToJoin.MarkedAsPendingByPrivilegedAccount() { + // the request is already in some pending state so we won't override it again + continue + } + } + + requestUpdated, err := m.saveOrUpdateRequestToJoin(signer, community.ID(), requestToJoin) + if err != nil { + return err + } + if community.IsControlNode() && requestUpdated { + rejectedRequestsToJoin = append(rejectedRequestsToJoin, requestToJoin.ID) + } + } + + if community.IsControlNode() { + m.publish(&Subscription{RejectedRequestsToJoin: rejectedRequestsToJoin}) + } + return nil +} + // markRequestToJoin marks all the pending requests to join as completed // if we are members func (m *Manager) markRequestToJoin(pk *ecdsa.PublicKey, community *Community) error { @@ -1430,6 +1520,10 @@ func (m *Manager) markRequestToJoinAsCanceled(pk *ecdsa.PublicKey, community *Co return m.persistence.SetRequestToJoinState(common.PubkeyToHex(pk), community.ID(), RequestToJoinStateCanceled) } +func (m *Manager) markRequestToJoinAsAcceptedPending(pk *ecdsa.PublicKey, community *Community) error { + return m.persistence.SetRequestToJoinState(common.PubkeyToHex(pk), community.ID(), RequestToJoinStateAcceptedPending) +} + func (m *Manager) DeletePendingRequestToJoin(request *RequestToJoin) error { community, err := m.GetByID(request.CommunityID) if err != nil { @@ -1575,54 +1669,84 @@ func (m *Manager) AcceptRequestToJoin(request *requests.AcceptRequestToJoinCommu return nil, err } - community, err := m.GetByID(dbRequest.CommunityID) - if err != nil { - return nil, err - } - - revealedAccounts, err := m.persistence.GetRequestToJoinRevealedAddresses(dbRequest.ID) - if err != nil { - return nil, err - } - - permissionsSatisfied, role, err := m.accountsSatisfyPermissionsToJoin(community, revealedAccounts) - if err != nil { - return nil, err - } - - if !permissionsSatisfied { - return community, ErrNoPermissionToJoin - } - - memberRoles := []protobuf.CommunityMember_Roles{} - if role != protobuf.CommunityMember_ROLE_NONE { - memberRoles = []protobuf.CommunityMember_Roles{role} - } - pk, err := common.HexToPubkey(dbRequest.PublicKey) if err != nil { return nil, err } - _, err = community.AddMemberWithRevealedAccounts(dbRequest, memberRoles, revealedAccounts) + community, err := m.GetByID(dbRequest.CommunityID) if err != nil { return nil, err } - channels, err := m.accountsSatisfyPermissionsToJoinChannels(community, revealedAccounts) - if err != nil { - return nil, err - } + if community.HasPermissionToSendCommunityEvents() { + if dbRequest.MarkedAsPendingByPrivilegedAccount() { + // if the request is in any pending state, it means our admin node has either + // already made a decision in the past, or previously received a decision by + // another admin, which in both cases means we're not allowed to override this + // state again + return nil, errors.New("request to join is already in pending state") + } - for channelID := range channels { - _, err = community.AddMemberToChat(channelID, pk, memberRoles) + // admins do not perform permission checks, they merely mark the + // request as accepted (pending) and forward their decision to the control node + acceptedRequestsToJoin := make(map[string]*protobuf.CommunityRequestToJoin) + acceptedRequestsToJoin[dbRequest.PublicKey] = dbRequest.ToCommunityRequestToJoinProtobuf() + + adminChanges := &CommunityEventChanges{ + AcceptedRequestsToJoin: acceptedRequestsToJoin, + } + + err := community.addNewCommunityEvent(community.ToCommunityRequestToJoinAcceptCommunityEvent(adminChanges)) if err != nil { return nil, err } + + if err := m.markRequestToJoinAsAcceptedPending(pk, community); err != nil { + return nil, err + } } - if err := m.markRequestToJoin(pk, community); err != nil { - return nil, err + if community.IsControlNode() { + revealedAccounts, err := m.persistence.GetRequestToJoinRevealedAddresses(dbRequest.ID) + if err != nil { + return nil, err + } + + permissionsSatisfied, role, err := m.accountsSatisfyPermissionsToJoin(community, revealedAccounts) + if err != nil { + return nil, err + } + + if !permissionsSatisfied { + return community, ErrNoPermissionToJoin + } + + memberRoles := []protobuf.CommunityMember_Roles{} + if role != protobuf.CommunityMember_ROLE_NONE { + memberRoles = []protobuf.CommunityMember_Roles{role} + } + + _, err = community.AddMemberWithRevealedAccounts(dbRequest, memberRoles, revealedAccounts) + if err != nil { + return nil, err + } + + channels, err := m.accountsSatisfyPermissionsToJoinChannels(community, revealedAccounts) + if err != nil { + return nil, err + } + + for channelID := range channels { + _, err = community.AddMemberToChat(channelID, pk, memberRoles) + if err != nil { + return nil, err + } + } + + if err := m.markRequestToJoin(pk, community); err != nil { + return nil, err + } } err = m.saveAndPublish(community) @@ -1648,7 +1772,11 @@ func (m *Manager) DeclineRequestToJoin(request *requests.DeclineRequestToJoinCom return err } - err = m.persistence.SetRequestToJoinState(dbRequest.PublicKey, dbRequest.CommunityID, RequestToJoinStateDeclined) + requestToJoinState := RequestToJoinStateDeclined + if community.HasPermissionToSendCommunityEvents() { + requestToJoinState = RequestToJoinStateDeclinedPending + } + err = m.persistence.SetRequestToJoinState(dbRequest.PublicKey, dbRequest.CommunityID, requestToJoinState) if err != nil { return err } @@ -1727,7 +1855,7 @@ func (m *Manager) HandleCommunityRequestToJoin(signer *ecdsa.PublicKey, request } // don't process request as admin if community is configured as auto-accept - if community.HasPermissionToSendCommunityEvents() && community.AcceptRequestToJoinAutomatically() { + if !community.IsControlNode() && community.AcceptRequestToJoinAutomatically() { return nil, errors.New("ignoring request to join, community is set to auto-accept") } @@ -1759,11 +1887,29 @@ func (m *Manager) HandleCommunityRequestToJoin(signer *ecdsa.PublicKey, request requestToJoin.CalculateID() - if err := m.persistence.SaveRequestToJoin(requestToJoin); err != nil { + existingRequestToJoin, err := m.persistence.GetRequestToJoin(requestToJoin.ID) + if err != nil && err != sql.ErrNoRows { return nil, err } - if len(request.RevealedAccounts) > 0 { + if existingRequestToJoin != nil { + // request to join was already processed by an admin and waits to get + // confirmation for its decision + // + // we're only interested in immediately declining any declined/pending + // requests here, because if it's accepted/pending, we still need to perform + // some checks + if existingRequestToJoin.State == RequestToJoinStateDeclinedPending { + requestToJoin.State = RequestToJoinStateDeclined + return requestToJoin, nil + } + } else { + if err := m.persistence.SaveRequestToJoin(requestToJoin); err != nil { + return nil, err + } + } + + if len(request.RevealedAccounts) > 0 && community.IsControlNode() { // verify if revealed addresses indeed belong to requester for _, revealedAccount := range request.RevealedAccounts { recoverParams := account.RecoverParams{ @@ -1784,7 +1930,7 @@ func (m *Manager) HandleCommunityRequestToJoin(signer *ecdsa.PublicKey, request } // Save revealed addresses + signatures so they can later be added - // to the community member list when the request is accepted + // to the control node's local table of known revealed addresses err = m.persistence.SaveRequestToJoinRevealedAddresses(requestToJoin) if err != nil { return nil, err @@ -1796,22 +1942,42 @@ func (m *Manager) HandleCommunityRequestToJoin(signer *ecdsa.PublicKey, request // More specifically, CommunityRequestToLeave may be delivered later than CommunityRequestToJoin, or not delivered at all acceptAutomatically := community.AcceptRequestToJoinAutomatically() || community.HasMember(signer) if acceptAutomatically { - err = m.markRequestToJoin(signer, community) - if err != nil { - return nil, err + if community.IsControlNode() { + err = m.markRequestToJoin(signer, community) + if err != nil { + return nil, err + } + // Don't check permissions here, + // it will be done further in the processing pipeline. + requestToJoin.State = RequestToJoinStateAccepted + } else { + err = m.markRequestToJoinAsAcceptedPending(signer, community) + if err != nil { + return nil, err + } + requestToJoin.State = RequestToJoinStateAcceptedPending } - // Don't check permissions here, - // it will be done further in the processing pipeline. - requestToJoin.State = RequestToJoinStateAccepted return requestToJoin, nil } - permissionsSatisfied, _, err := m.accountsSatisfyPermissionsToJoin(community, request.RevealedAccounts) - if err != nil { - return nil, err - } - if !permissionsSatisfied { - requestToJoin.State = RequestToJoinStateDeclined + if community.IsControlNode() && len(request.RevealedAccounts) > 0 { + permissionsSatisfied, _, err := m.accountsSatisfyPermissionsToJoin(community, request.RevealedAccounts) + if err != nil { + return nil, err + } + if !permissionsSatisfied { + requestToJoin.State = RequestToJoinStateDeclined + } + if permissionsSatisfied && existingRequestToJoin.State == RequestToJoinStateAcceptedPending { + err = m.markRequestToJoin(signer, community) + if err != nil { + return nil, err + } + // if the request to join was already accepted by another admin, + // we mark it as accepted so it won't be in pending state, even if the community + // is not set to auto-accept + requestToJoin.State = RequestToJoinStateAccepted + } } return requestToJoin, nil @@ -2801,6 +2967,14 @@ func (m *Manager) AcceptedRequestsToJoinForCommunity(id types.HexBytes) ([]*Requ return m.persistence.AcceptedRequestsToJoinForCommunity(id) } +func (m *Manager) AcceptedPendingRequestsToJoinForCommunity(id types.HexBytes) ([]*RequestToJoin, error) { + return m.persistence.AcceptedPendingRequestsToJoinForCommunity(id) +} + +func (m *Manager) DeclinedPendingRequestsToJoinForCommunity(id types.HexBytes) ([]*RequestToJoin, error) { + return m.persistence.DeclinedPendingRequestsToJoinForCommunity(id) +} + func (m *Manager) CanPost(pk *ecdsa.PublicKey, communityID string, chatID string, grant []byte) (bool, error) { community, err := m.GetByIDString(communityID) if err != nil { diff --git a/protocol/communities/persistence.go b/protocol/communities/persistence.go index f62e7ee0e..21fba16b9 100644 --- a/protocol/communities/persistence.go +++ b/protocol/communities/persistence.go @@ -749,6 +749,14 @@ func (p *Persistence) AcceptedRequestsToJoinForCommunity(id []byte) ([]*RequestT return p.RequestsToJoinForCommunityWithState(id, RequestToJoinStateAccepted) } +func (p *Persistence) AcceptedPendingRequestsToJoinForCommunity(id []byte) ([]*RequestToJoin, error) { + return p.RequestsToJoinForCommunityWithState(id, RequestToJoinStateAcceptedPending) +} + +func (p *Persistence) DeclinedPendingRequestsToJoinForCommunity(id []byte) ([]*RequestToJoin, error) { + return p.RequestsToJoinForCommunityWithState(id, RequestToJoinStateDeclinedPending) +} + func (p *Persistence) SetRequestToJoinState(pk string, communityID []byte, state RequestToJoinState) error { _, err := p.db.Exec(`UPDATE communities_requests_to_join SET state = ? WHERE community_id = ? AND public_key = ?`, state, communityID, pk) return err diff --git a/protocol/communities/request_to_join.go b/protocol/communities/request_to_join.go index b8830a637..9c7478b4c 100644 --- a/protocol/communities/request_to_join.go +++ b/protocol/communities/request_to_join.go @@ -16,6 +16,8 @@ const ( RequestToJoinStateDeclined RequestToJoinStateAccepted RequestToJoinStateCanceled + RequestToJoinStateAcceptedPending + RequestToJoinStateDeclinedPending ) type RequestToJoin struct { @@ -72,6 +74,10 @@ func (r *RequestToJoin) Empty() bool { return len(r.ID)+len(r.PublicKey)+int(r.Clock)+len(r.ENSName)+len(r.ChatID)+len(r.CommunityID)+int(r.State) == 0 } +func (r *RequestToJoin) MarkedAsPendingByPrivilegedAccount() bool { + return r.State == RequestToJoinStateAcceptedPending || r.State == RequestToJoinStateDeclinedPending +} + func AddTimeoutToRequestToJoinClock(clock uint64) (uint64, error) { requestToJoinClock, err := strconv.ParseInt(fmt.Sprint(clock), 10, 64) if err != nil { diff --git a/protocol/communities_events_owner_without_community_key_test.go b/protocol/communities_events_owner_without_community_key_test.go index 6ad196451..4401d642c 100644 --- a/protocol/communities_events_owner_without_community_key_test.go +++ b/protocol/communities_events_owner_without_community_key_test.go @@ -198,22 +198,70 @@ func (s *OwnerWithoutCommunityKeyCommunityEventsSuite) TestOwnerCannotDeleteBeco s.Require().Nil(response) } +func (s *OwnerWithoutCommunityKeyCommunityEventsSuite) TestOwnerAcceptMemberRequestToJoinResponseSharedWithOtherEventSenders() { + additionalOwner := s.newMessenger() + community := setUpOnRequestCommunityAndRoles(s, protobuf.CommunityMember_ROLE_OWNER, []*Messenger{additionalOwner}) + // set up additional user that will send request to join + user := s.newMessenger() + testAcceptMemberRequestToJoinResponseSharedWithOtherEventSenders(s, community, user, additionalOwner) +} + +func (s *OwnerWithoutCommunityKeyCommunityEventsSuite) TestOwnerAcceptMemberRequestToJoinNotConfirmedByControlNode() { + community := setUpOnRequestCommunityAndRoles(s, protobuf.CommunityMember_ROLE_OWNER, []*Messenger{}) + // set up additional user that will send request to join + user := s.newMessenger() + testAcceptMemberRequestToJoinNotConfirmedByControlNode(s, community, user) +} + func (s *OwnerWithoutCommunityKeyCommunityEventsSuite) TestOwnerAcceptMemberRequestToJoin() { - community := setUpOnRequestCommunityAndRoles(s, protobuf.CommunityMember_ROLE_OWNER) + community := setUpOnRequestCommunityAndRoles(s, protobuf.CommunityMember_ROLE_OWNER, []*Messenger{}) // set up additional user that will send request to join user := s.newMessenger() testAcceptMemberRequestToJoin(s, community, user) } +func (s *OwnerWithoutCommunityKeyCommunityEventsSuite) TestOwnerRejectMemberRequestToJoinResponseSharedWithOtherEventSenders() { + additionalOwner := s.newMessenger() + community := setUpOnRequestCommunityAndRoles(s, protobuf.CommunityMember_ROLE_OWNER, []*Messenger{additionalOwner}) + // set up additional user that will send request to join + user := s.newMessenger() + testAcceptMemberRequestToJoinResponseSharedWithOtherEventSenders(s, community, user, additionalOwner) +} + +func (s *OwnerWithoutCommunityKeyCommunityEventsSuite) TestOwnerRejectMemberRequestToJoinNotConfirmedByControlNode() { + community := setUpOnRequestCommunityAndRoles(s, protobuf.CommunityMember_ROLE_OWNER, []*Messenger{}) + // set up additional user that will send request to join + user := s.newMessenger() + testRejectMemberRequestToJoinNotConfirmedByControlNode(s, community, user) +} + func (s *OwnerWithoutCommunityKeyCommunityEventsSuite) TestOwnerRejectMemberRequestToJoin() { - community := setUpOnRequestCommunityAndRoles(s, protobuf.CommunityMember_ROLE_OWNER) + community := setUpOnRequestCommunityAndRoles(s, protobuf.CommunityMember_ROLE_OWNER, []*Messenger{}) // set up additional user that will send request to join user := s.newMessenger() testRejectMemberRequestToJoin(s, community, user) } +func (s *OwnerWithoutCommunityKeyCommunityEventsSuite) TestOwnerRequestToJoinStateCannotBeOverridden() { + additionalOwner := s.newMessenger() + community := setUpOnRequestCommunityAndRoles(s, protobuf.CommunityMember_ROLE_OWNER, []*Messenger{additionalOwner}) + + // set up additional user that will send request to join + user := s.newMessenger() + testEventSenderCannotOverrideRequestToJoinState(s, community, user, additionalOwner) +} + +func (s *OwnerWithoutCommunityKeyCommunityEventsSuite) TestOwnerControlNodeHandlesMultipleEventSenderRequestToJoinDecisions() { + additionalOwner := s.newMessenger() + community := setUpOnRequestCommunityAndRoles(s, protobuf.CommunityMember_ROLE_OWNER, []*Messenger{additionalOwner}) + + // set up additional user that will send request to join + user := s.newMessenger() + testControlNodeHandlesMultipleEventSenderRequestToJoinDecisions(s, community, user, additionalOwner) +} + func (s *OwnerWithoutCommunityKeyCommunityEventsSuite) TestOwnerCreateEditDeleteCategories() { community := setUpCommunityAndRoles(s, protobuf.CommunityMember_ROLE_OWNER) testCreateEditDeleteCategories(s, community) diff --git a/protocol/communities_events_token_master_test.go b/protocol/communities_events_token_master_test.go index 37aa72662..37a090abb 100644 --- a/protocol/communities_events_token_master_test.go +++ b/protocol/communities_events_token_master_test.go @@ -124,20 +124,68 @@ func (s *TokenMasterCommunityEventsSuite) TestTokenMasterCannotDeleteBecomeAdmin testEventSenderCannotDeleteBecomeAdminPermission(s, community) } +func (s *TokenMasterCommunityEventsSuite) TestTokenMasterAcceptMemberRequestToJoinNotConfirmedByControlNode() { + community := setUpOnRequestCommunityAndRoles(s, protobuf.CommunityMember_ROLE_TOKEN_MASTER, []*Messenger{}) + // set up additional user that will send request to join + user := s.newMessenger() + testAcceptMemberRequestToJoinNotConfirmedByControlNode(s, community, user) +} + func (s *TokenMasterCommunityEventsSuite) TestTokenMasterAcceptMemberRequestToJoin() { - community := setUpOnRequestCommunityAndRoles(s, protobuf.CommunityMember_ROLE_TOKEN_MASTER) + community := setUpOnRequestCommunityAndRoles(s, protobuf.CommunityMember_ROLE_TOKEN_MASTER, []*Messenger{}) // set up additional user that will send request to join user := s.newMessenger() testAcceptMemberRequestToJoin(s, community, user) } +func (s *TokenMasterCommunityEventsSuite) TestTokenMasterAcceptMemberRequestToJoinResponseSharedWithOtherEventSenders() { + additionalTokenMaster := s.newMessenger() + community := setUpOnRequestCommunityAndRoles(s, protobuf.CommunityMember_ROLE_TOKEN_MASTER, []*Messenger{additionalTokenMaster}) + // set up additional user that will send request to join + user := s.newMessenger() + testAcceptMemberRequestToJoinResponseSharedWithOtherEventSenders(s, community, user, additionalTokenMaster) +} + +func (s *TokenMasterCommunityEventsSuite) TestTokenMasterRejectMemberRequestToJoinResponseSharedWithOtherEventSenders() { + additionalTokenMaster := s.newMessenger() + community := setUpOnRequestCommunityAndRoles(s, protobuf.CommunityMember_ROLE_TOKEN_MASTER, []*Messenger{additionalTokenMaster}) + // set up additional user that will send request to join + user := s.newMessenger() + testRejectMemberRequestToJoinResponseSharedWithOtherEventSenders(s, community, user, additionalTokenMaster) +} + +func (s *TokenMasterCommunityEventsSuite) TestTokenMasterRejectMemberRequestToJoinNotConfirmedByControlNode() { + community := setUpOnRequestCommunityAndRoles(s, protobuf.CommunityMember_ROLE_TOKEN_MASTER, []*Messenger{}) + // set up additional user that will send request to join + user := s.newMessenger() + testRejectMemberRequestToJoinNotConfirmedByControlNode(s, community, user) +} + func (s *TokenMasterCommunityEventsSuite) TestTokenMasterRejectMemberRequestToJoin() { - community := setUpOnRequestCommunityAndRoles(s, protobuf.CommunityMember_ROLE_TOKEN_MASTER) + community := setUpOnRequestCommunityAndRoles(s, protobuf.CommunityMember_ROLE_TOKEN_MASTER, []*Messenger{}) // set up additional user that will send request to join user := s.newMessenger() testRejectMemberRequestToJoin(s, community, user) } +func (s *TokenMasterCommunityEventsSuite) TestTokenMasterRequestToJoinStateCannotBeOverridden() { + additionalTokenMaster := s.newMessenger() + community := setUpOnRequestCommunityAndRoles(s, protobuf.CommunityMember_ROLE_TOKEN_MASTER, []*Messenger{additionalTokenMaster}) + + // set up additional user that will send request to join + user := s.newMessenger() + testEventSenderCannotOverrideRequestToJoinState(s, community, user, additionalTokenMaster) +} + +func (s *TokenMasterCommunityEventsSuite) TestTokenMasterControlNodeHandlesMultipleEventSenderRequestToJoinDecisions() { + additionalTokenMaster := s.newMessenger() + community := setUpOnRequestCommunityAndRoles(s, protobuf.CommunityMember_ROLE_TOKEN_MASTER, []*Messenger{additionalTokenMaster}) + + // set up additional user that will send request to join + user := s.newMessenger() + testControlNodeHandlesMultipleEventSenderRequestToJoinDecisions(s, community, user, additionalTokenMaster) +} + func (s *TokenMasterCommunityEventsSuite) TestTokenMasterCreateEditDeleteCategories() { community := setUpCommunityAndRoles(s, protobuf.CommunityMember_ROLE_TOKEN_MASTER) testCreateEditDeleteCategories(s, community) diff --git a/protocol/communities_events_utils_test.go b/protocol/communities_events_utils_test.go index d01a242eb..d36c83aee 100644 --- a/protocol/communities_events_utils_test.go +++ b/protocol/communities_events_utils_test.go @@ -338,7 +338,7 @@ func assertCheckTokenPermissionCreated(s *suite.Suite, community *communities.Co s.Require().Equal(permissions[0].TokenCriteria[0].Decimals, uint64(18)) } -func setUpOnRequestCommunityAndRoles(base CommunityEventsTestsInterface, role protobuf.CommunityMember_Roles) *communities.Community { +func setUpOnRequestCommunityAndRoles(base CommunityEventsTestsInterface, role protobuf.CommunityMember_Roles, additionalEventSenders []*Messenger) *communities.Community { tcs2, err := base.GetControlNode().communitiesManager.All() s := base.GetSuite() s.Require().NoError(err, "eventSender.communitiesManager.All") @@ -365,9 +365,20 @@ func setUpOnRequestCommunityAndRoles(base CommunityEventsTestsInterface, role pr checkPermissionGranted := func(response *MessengerResponse) error { return checkRolePermissionInResponse(response, base.GetEventSender().IdentityPublicKey(), role) } - waitOnMessengerResponse(s, WaitCommunityCondition, checkPermissionGranted, base.GetMember()) + for _, eventSender := range additionalEventSenders { + advertiseCommunityTo(s, community, base.GetControlNode(), eventSender) + joinOnRequestCommunity(s, community, base.GetControlNode(), eventSender) + + grantPermission(s, community, base.GetControlNode(), eventSender, role) + checkPermissionGranted = func(response *MessengerResponse) error { + return checkRolePermissionInResponse(response, eventSender.IdentityPublicKey(), role) + } + waitOnMessengerResponse(s, WaitCommunityCondition, checkPermissionGranted, base.GetMember()) + waitOnMessengerResponse(s, WaitCommunityCondition, checkPermissionGranted, base.GetEventSender()) + } + return community } @@ -761,7 +772,8 @@ func testAcceptMemberRequestToJoin(base CommunityEventsTestsInterface, community s.Require().NoError(err) s.Require().NotNil(response) s.Require().Len(response.RequestsToJoinCommunity, 1) - _ = response.RequestsToJoinCommunity[0] + + sentRequest := response.RequestsToJoinCommunity[0] // event sender receives request to join response, err = WaitOnMessengerResponse( @@ -772,19 +784,107 @@ func testAcceptMemberRequestToJoin(base CommunityEventsTestsInterface, community s.Require().NoError(err) s.Require().Len(response.RequestsToJoinCommunity, 1) - receivedRequest := response.RequestsToJoinCommunity[0] + // event sender has not accepted request yet + eventSenderCommunity, err := base.GetEventSender().GetCommunityByID(community.ID()) + s.Require().NoError(err) + s.Require().False(eventSenderCommunity.HasMember(&user.identity.PublicKey)) + + acceptRequestToJoin := &requests.AcceptRequestToJoinCommunity{ID: sentRequest.ID} + response, err = base.GetEventSender().AcceptRequestToJoinCommunity(acceptRequestToJoin) + s.Require().NoError(err) + s.Require().NotNil(response) + s.Require().Len(response.Communities(), 1) + // we don't expect `user` to be a member already, because `eventSender` merely + // forwards its accept decision to the control node + s.Require().False(response.Communities()[0].HasMember(&user.identity.PublicKey)) + + // user receives community admin event without being a member yet + response, err = WaitOnMessengerResponse( + user, + func(r *MessengerResponse) bool { return len(r.Communities()) > 0 }, + "user did not receive community request to join response", + ) + s.Require().NoError(err) + s.Require().Len(response.Communities(), 1) + // `user` should not be part of the community yet, we need to wait for + // the control node to confirm `eventSender`s decision + s.Require().False(response.Communities()[0].HasMember(&user.identity.PublicKey)) + + // control node receives community event with accepted membership request + response, err = WaitOnMessengerResponse( + base.GetControlNode(), + func(r *MessengerResponse) bool { return len(r.Communities()) > 0 }, + "control node did not receive community request to join response", + ) + s.Require().NoError(err) + s.Require().Len(response.Communities(), 1) + + // at this point, the request to join is marked as accepted by control node + acceptedRequests, err := base.GetControlNode().AcceptedRequestsToJoinForCommunity(community.ID()) + s.Require().NoError(err) + // we expect 3 here (1 event senders, 1 member + 1 from user) + s.Require().Len(acceptedRequests, 3) + s.Require().Equal(acceptedRequests[2].PublicKey, common.PubkeyToHex(&user.identity.PublicKey)) + + // user receives updated community + _, err = WaitOnMessengerResponse( + user, + func(r *MessengerResponse) bool { + return len(r.Communities()) > 0 && r.Communities()[0].HasMember(&user.identity.PublicKey) + }, + "alice did not receive community request to join response", + ) + s.Require().NoError(err) +} + +func testAcceptMemberRequestToJoinNotConfirmedByControlNode(base CommunityEventsTestsInterface, community *communities.Community, user *Messenger) { + // set up additional user that will send request to join + _, err := user.Start() + + s := base.GetSuite() + + s.Require().NoError(err) + defer user.Shutdown() // nolint: errcheck + + advertiseCommunityTo(s, community, base.GetControlNode(), user) + + // user sends request to join + requestToJoin := &requests.RequestToJoinCommunity{CommunityID: community.ID()} + response, err := user.RequestToJoinCommunity(requestToJoin) + s.Require().NoError(err) + s.Require().NotNil(response) + s.Require().Len(response.RequestsToJoinCommunity, 1) + + sentRequest := response.RequestsToJoinCommunity[0] + + // event sender receives request to join + response, err = WaitOnMessengerResponse( + base.GetEventSender(), + func(r *MessengerResponse) bool { return len(r.RequestsToJoinCommunity) > 0 }, + "event sender did not receive community request to join", + ) + s.Require().NoError(err) + s.Require().Len(response.RequestsToJoinCommunity, 1) // event sender has not accepted request yet eventSenderCommunity, err := base.GetEventSender().GetCommunityByID(community.ID()) s.Require().NoError(err) s.Require().False(eventSenderCommunity.HasMember(&user.identity.PublicKey)) - acceptRequestToJoin := &requests.AcceptRequestToJoinCommunity{ID: receivedRequest.ID} + acceptRequestToJoin := &requests.AcceptRequestToJoinCommunity{ID: sentRequest.ID} response, err = base.GetEventSender().AcceptRequestToJoinCommunity(acceptRequestToJoin) s.Require().NoError(err) s.Require().NotNil(response) s.Require().Len(response.Communities(), 1) - s.Require().True(response.Communities()[0].HasMember(&user.identity.PublicKey)) + // we don't expect `user` to be a member already, because `eventSender` merely + // forwards its accept decision to the control node + s.Require().False(response.Communities()[0].HasMember(&user.identity.PublicKey)) + + // at this point, the request to join is in accepted/pending state + acceptedPendingRequests, err := base.GetEventSender().AcceptedPendingRequestsToJoinForCommunity(community.ID()) + s.Require().NoError(err) + s.Require().Len(acceptedPendingRequests, 1) + s.Require().Equal(acceptedPendingRequests[0].PublicKey, common.PubkeyToHex(&user.identity.PublicKey)) // user receives request to join response response, err = WaitOnMessengerResponse( @@ -794,32 +894,179 @@ func testAcceptMemberRequestToJoin(base CommunityEventsTestsInterface, community ) s.Require().NoError(err) s.Require().Len(response.Communities(), 1) - s.Require().True(response.Communities()[0].HasMember(&user.identity.PublicKey)) + // `user` should not be part of the community yet, because control node + // hasn't confirmed the decistion yet + s.Require().False(response.Communities()[0].HasMember(&user.identity.PublicKey)) +} - // control node receives updated community - response, err = WaitOnMessengerResponse( - base.GetControlNode(), - func(r *MessengerResponse) bool { return len(r.Communities()) > 0 }, - "control node did not receive community request to join response", +func testAcceptMemberRequestToJoinResponseSharedWithOtherEventSenders(base CommunityEventsTestsInterface, community *communities.Community, user *Messenger, additionalEventSender *Messenger) { + // set up additional user that will send request to join + _, err := user.Start() + + s := base.GetSuite() + + s.Require().NoError(err) + defer user.Shutdown() // nolint: errcheck + + advertiseCommunityTo(s, community, base.GetControlNode(), user) + + // user sends request to join + requestToJoin := &requests.RequestToJoinCommunity{CommunityID: community.ID()} + response, err := user.RequestToJoinCommunity(requestToJoin) + s.Require().NoError(err) + s.Require().NotNil(response) + s.Require().Len(response.RequestsToJoinCommunity, 1) + + sentRequest := response.RequestsToJoinCommunity[0] + + // event sender receives request to join + _, err = WaitOnMessengerResponse( + base.GetEventSender(), + func(r *MessengerResponse) bool { return len(r.RequestsToJoinCommunity) > 0 }, + "event sender did not receive community request to join", ) s.Require().NoError(err) - s.Require().Len(response.Communities(), 1) - requests, err := base.GetControlNode().AcceptedRequestsToJoinForCommunity(community.ID()) - // there's now two requests to join (event sender and member) + 1 from user - s.Require().NoError(err) - s.Require().Len(requests, 3) - s.Require().True(response.Communities()[0].HasMember(&user.identity.PublicKey)) - - // member receives updated community - response, err = WaitOnMessengerResponse( - base.GetMember(), - func(r *MessengerResponse) bool { return len(r.Communities()) > 0 }, - "alice did not receive community request to join response", + // event sender 2 receives request to join + _, err = WaitOnMessengerResponse( + additionalEventSender, + func(r *MessengerResponse) bool { return len(r.RequestsToJoinCommunity) > 0 }, + "event sender did not receive community request to join", ) s.Require().NoError(err) + + // event sender 1 accepts request + acceptRequestToJoin := &requests.AcceptRequestToJoinCommunity{ID: sentRequest.ID} + response, err = base.GetEventSender().AcceptRequestToJoinCommunity(acceptRequestToJoin) + s.Require().NoError(err) + s.Require().NotNil(response) s.Require().Len(response.Communities(), 1) - s.Require().True(response.Communities()[0].HasMember(&user.identity.PublicKey)) + + // event sender 2 receives decision of other event sender + _, err = WaitOnMessengerResponse( + additionalEventSender, + func(r *MessengerResponse) bool { return len(r.Communities()) > 0 }, + "event sender did not receive community request to join", + ) + s.Require().NoError(err) + + // at this point, the request to join is in accepted/pending state for event sender 2 + acceptedPendingRequests, err := additionalEventSender.AcceptedPendingRequestsToJoinForCommunity(community.ID()) + s.Require().NoError(err) + s.Require().Len(acceptedPendingRequests, 1) + s.Require().Equal(acceptedPendingRequests[0].PublicKey, common.PubkeyToHex(&user.identity.PublicKey)) +} + +func testRejectMemberRequestToJoinResponseSharedWithOtherEventSenders(base CommunityEventsTestsInterface, community *communities.Community, user *Messenger, additionalEventSender *Messenger) { + // set up additional user that will send request to join + _, err := user.Start() + + s := base.GetSuite() + + s.Require().NoError(err) + defer user.Shutdown() // nolint: errcheck + + advertiseCommunityTo(s, community, base.GetControlNode(), user) + + // user sends request to join + requestToJoin := &requests.RequestToJoinCommunity{CommunityID: community.ID()} + response, err := user.RequestToJoinCommunity(requestToJoin) + s.Require().NoError(err) + s.Require().NotNil(response) + s.Require().Len(response.RequestsToJoinCommunity, 1) + + sentRequest := response.RequestsToJoinCommunity[0] + + // event sender receives request to join + response, err = WaitOnMessengerResponse( + base.GetEventSender(), + func(r *MessengerResponse) bool { return len(r.RequestsToJoinCommunity) > 0 }, + "event sender did not receive community request to join", + ) + s.Require().NoError(err) + s.Require().Len(response.RequestsToJoinCommunity, 1) + + // event sender 2 receives request to join + response, err = WaitOnMessengerResponse( + additionalEventSender, + func(r *MessengerResponse) bool { return len(r.RequestsToJoinCommunity) > 0 }, + "event sender did not receive community request to join", + ) + s.Require().NoError(err) + s.Require().Len(response.RequestsToJoinCommunity, 1) + + rejectRequestToJoin := &requests.DeclineRequestToJoinCommunity{ID: sentRequest.ID} + response, err = base.GetEventSender().DeclineRequestToJoinCommunity(rejectRequestToJoin) + s.Require().NoError(err) + s.Require().NotNil(response) + + // event sender 2 receives decision of other event sender + _, err = WaitOnMessengerResponse( + additionalEventSender, + func(r *MessengerResponse) bool { return len(r.Communities()) > 0 }, + "event sender did not receive community request to join", + ) + s.Require().NoError(err) + + // at this point, the request to join is in declined/pending state for event sender 2 + rejectedPendingRequests, err := additionalEventSender.DeclinedPendingRequestsToJoinForCommunity(community.ID()) + s.Require().NoError(err) + s.Require().Len(rejectedPendingRequests, 1) + s.Require().Equal(rejectedPendingRequests[0].PublicKey, common.PubkeyToHex(&user.identity.PublicKey)) +} + +func testRejectMemberRequestToJoinNotConfirmedByControlNode(base CommunityEventsTestsInterface, community *communities.Community, user *Messenger) { + // set up additional user that will send request to join + _, err := user.Start() + + s := base.GetSuite() + + s.Require().NoError(err) + defer user.Shutdown() // nolint: errcheck + + advertiseCommunityTo(s, community, base.GetControlNode(), user) + + // user sends request to join + requestToJoin := &requests.RequestToJoinCommunity{CommunityID: community.ID()} + response, err := user.RequestToJoinCommunity(requestToJoin) + s.Require().NoError(err) + s.Require().NotNil(response) + s.Require().Len(response.RequestsToJoinCommunity, 1) + + sentRequest := response.RequestsToJoinCommunity[0] + + // event sender receives request to join + response, err = WaitOnMessengerResponse( + base.GetEventSender(), + func(r *MessengerResponse) bool { return len(r.RequestsToJoinCommunity) > 0 }, + "event sender did not receive community request to join", + ) + s.Require().NoError(err) + s.Require().Len(response.RequestsToJoinCommunity, 1) + + // event sender has not accepted request + eventSenderCommunity, err := base.GetEventSender().GetCommunityByID(community.ID()) + s.Require().NoError(err) + s.Require().False(eventSenderCommunity.HasMember(&user.identity.PublicKey)) + + declineRequestToJoin := &requests.DeclineRequestToJoinCommunity{ID: sentRequest.ID} + response, err = base.GetEventSender().DeclineRequestToJoinCommunity(declineRequestToJoin) + s.Require().NoError(err) + s.Require().NotNil(response) + + // at this point, the request to join is in decline/pending state + declinedPendingRequests, err := base.GetEventSender().DeclinedPendingRequestsToJoinForCommunity(community.ID()) + s.Require().NoError(err) + s.Require().Len(declinedPendingRequests, 1) + s.Require().Equal(declinedPendingRequests[0].PublicKey, common.PubkeyToHex(&user.identity.PublicKey)) + + // user won't receive anything + _, err = WaitOnMessengerResponse( + user, + func(r *MessengerResponse) bool { return len(r.Communities()) == 0 }, + "user did not receive community request to join response", + ) + s.Require().NoError(err) } func testRejectMemberRequestToJoin(base CommunityEventsTestsInterface, community *communities.Community, user *Messenger) { @@ -838,6 +1085,8 @@ func testRejectMemberRequestToJoin(base CommunityEventsTestsInterface, community s.Require().NotNil(response) s.Require().Len(response.RequestsToJoinCommunity, 1) + sentRequest := response.RequestsToJoinCommunity[0] + // event sender receives request to join response, err = WaitOnMessengerResponse( base.GetEventSender(), @@ -856,15 +1105,13 @@ func testRejectMemberRequestToJoin(base CommunityEventsTestsInterface, community s.Require().NoError(err) s.Require().Len(response.RequestsToJoinCommunity, 1) - receivedRequest := response.RequestsToJoinCommunity[0] - // event sender has not accepted request yet eventSenderCommunity, err := base.GetEventSender().GetCommunityByID(community.ID()) s.Require().NoError(err) s.Require().False(eventSenderCommunity.HasMember(&user.identity.PublicKey)) // event sender rejects request to join - rejectRequestToJoin := &requests.DeclineRequestToJoinCommunity{ID: receivedRequest.ID} + rejectRequestToJoin := &requests.DeclineRequestToJoinCommunity{ID: sentRequest.ID} _, err = base.GetEventSender().DeclineRequestToJoinCommunity(rejectRequestToJoin) s.Require().NoError(err) @@ -886,6 +1133,168 @@ func testRejectMemberRequestToJoin(base CommunityEventsTestsInterface, community s.Require().NoError(err) } +func testEventSenderCannotOverrideRequestToJoinState(base CommunityEventsTestsInterface, community *communities.Community, user *Messenger, additionalEventSender *Messenger) { + _, err := user.Start() + + s := base.GetSuite() + s.Require().NoError(err) + defer user.Shutdown() // nolint: errcheck + + advertiseCommunityTo(s, community, base.GetControlNode(), user) + + // user sends request to join + requestToJoin := &requests.RequestToJoinCommunity{CommunityID: community.ID()} + response, err := user.RequestToJoinCommunity(requestToJoin) + s.Require().NoError(err) + s.Require().NotNil(response) + s.Require().Len(response.RequestsToJoinCommunity, 1) + + sentRequest := response.RequestsToJoinCommunity[0] + + // event sender receives request to join + _, err = WaitOnMessengerResponse( + base.GetEventSender(), + func(r *MessengerResponse) bool { return len(r.RequestsToJoinCommunity) > 0 }, + "event sender did not receive community request to join", + ) + s.Require().NoError(err) + + // event sender 2 receives request to join + _, err = WaitOnMessengerResponse( + additionalEventSender, + func(r *MessengerResponse) bool { return len(r.RequestsToJoinCommunity) > 0 }, + "event sender did not receive community request to join", + ) + s.Require().NoError(err) + s.Require().Len(response.RequestsToJoinCommunity, 1) + + // request is pending for event sener 2 + pendingRequests, err := additionalEventSender.PendingRequestsToJoinForCommunity(community.ID()) + s.Require().NoError(err) + s.Require().NotNil(pendingRequests) + s.Require().Len(pendingRequests, 1) + + // event sender 1 rejects request to join + rejectRequestToJoin := &requests.DeclineRequestToJoinCommunity{ID: sentRequest.ID} + _, err = base.GetEventSender().DeclineRequestToJoinCommunity(rejectRequestToJoin) + s.Require().NoError(err) + + // request to join is now marked as rejected pending for event sender 1 + rejectedPendingRequests, err := base.GetEventSender().DeclinedPendingRequestsToJoinForCommunity(community.ID()) + s.Require().NoError(err) + s.Require().NotNil(rejectedPendingRequests) + s.Require().Len(rejectedPendingRequests, 1) + + // event sender 2 receives event sender 1's decision + _, err = WaitOnMessengerResponse( + additionalEventSender, + func(r *MessengerResponse) bool { return len(r.Communities()) > 0 }, + "event sender did not receive community request to join", + ) + s.Require().NoError(err) + + // request to join is now marked as rejected pending for event sender 2 + rejectedPendingRequests, err = additionalEventSender.DeclinedPendingRequestsToJoinForCommunity(community.ID()) + s.Require().NoError(err) + s.Require().NotNil(rejectedPendingRequests) + s.Require().Len(rejectedPendingRequests, 1) + + // event sender 2 should not be able to override that pending state + acceptRequestToJoin := &requests.AcceptRequestToJoinCommunity{ID: sentRequest.ID} + _, err = additionalEventSender.AcceptRequestToJoinCommunity(acceptRequestToJoin) + s.Require().Error(err) +} + +func testControlNodeHandlesMultipleEventSenderRequestToJoinDecisions(base CommunityEventsTestsInterface, community *communities.Community, user *Messenger, additionalEventSender *Messenger) { + _, err := user.Start() + + s := base.GetSuite() + s.Require().NoError(err) + defer user.Shutdown() // nolint: errcheck + + advertiseCommunityTo(s, community, base.GetControlNode(), user) + + // user sends request to join + requestToJoin := &requests.RequestToJoinCommunity{CommunityID: community.ID()} + response, err := user.RequestToJoinCommunity(requestToJoin) + s.Require().NoError(err) + s.Require().NotNil(response) + s.Require().Len(response.RequestsToJoinCommunity, 1) + + sentRequest := response.RequestsToJoinCommunity[0] + + // event sender receives request to join + _, err = WaitOnMessengerResponse( + base.GetEventSender(), + func(r *MessengerResponse) bool { return len(r.RequestsToJoinCommunity) > 0 }, + "event sender did not receive community request to join", + ) + s.Require().NoError(err) + + // event sender 2 receives request to join + _, err = WaitOnMessengerResponse( + additionalEventSender, + func(r *MessengerResponse) bool { return len(r.RequestsToJoinCommunity) > 0 }, + "event sender did not receive community request to join", + ) + s.Require().NoError(err) + + // control node receives request to join + _, err = WaitOnMessengerResponse( + base.GetControlNode(), + func(r *MessengerResponse) bool { return len(r.RequestsToJoinCommunity) > 0 }, + "event sender did not receive community request to join", + ) + s.Require().NoError(err) + + // event sender 1 rejects request to join + rejectRequestToJoin := &requests.DeclineRequestToJoinCommunity{ID: sentRequest.ID} + _, err = base.GetEventSender().DeclineRequestToJoinCommunity(rejectRequestToJoin) + s.Require().NoError(err) + // request to join is now marked as rejected pending for event sender 1 + rejectedPendingRequests, err := base.GetEventSender().DeclinedPendingRequestsToJoinForCommunity(community.ID()) + s.Require().NoError(err) + s.Require().NotNil(rejectedPendingRequests) + s.Require().Len(rejectedPendingRequests, 1) + + // control node receives event sender 1's and 2's decision + _, err = WaitOnMessengerResponse( + base.GetControlNode(), + func(r *MessengerResponse) bool { return len(r.Communities()) > 0 }, + "control node did not receive event senders decision", + ) + s.Require().NoError(err) + // request to join is now marked as rejected + rejectedRequests, err := base.GetControlNode().DeclinedRequestsToJoinForCommunity(community.ID()) + s.Require().NoError(err) + s.Require().NotNil(rejectedRequests) + s.Require().Len(rejectedRequests, 1) + + // event sender 2 accepts request to join + acceptRequestToJoin := &requests.AcceptRequestToJoinCommunity{ID: sentRequest.ID} + _, err = additionalEventSender.AcceptRequestToJoinCommunity(acceptRequestToJoin) + s.Require().NoError(err) + // request to join is now marked as accepted pending for event sender 2 + acceptedPendingRequests, err := additionalEventSender.AcceptedPendingRequestsToJoinForCommunity(community.ID()) + s.Require().NoError(err) + s.Require().NotNil(acceptedPendingRequests) + s.Require().Len(acceptedPendingRequests, 1) + + // control node now receives event sender 2's decision + _, err = WaitOnMessengerResponse( + base.GetControlNode(), + func(r *MessengerResponse) bool { return len(r.Communities()) > 0 }, + "control node did not receive event senders decision", + ) + s.Require().NoError(err) + rejectedRequests, err = base.GetControlNode().DeclinedRequestsToJoinForCommunity(community.ID()) + s.Require().NoError(err) + s.Require().NotNil(rejectedRequests) + s.Require().Len(rejectedRequests, 1) + // we expect user's request to join still to be rejected + s.Require().Equal(rejectedRequests[0].PublicKey, common.PubkeyToHex(&user.identity.PublicKey)) +} + func testCreateEditDeleteCategories(base CommunityEventsTestsInterface, community *communities.Community) { newCategory := &requests.CreateCommunityCategory{ CommunityID: community.ID(), diff --git a/protocol/communities_messenger_admin_test.go b/protocol/communities_messenger_admin_test.go index f18cb1c9d..aeb7fcd4d 100644 --- a/protocol/communities_messenger_admin_test.go +++ b/protocol/communities_messenger_admin_test.go @@ -128,22 +128,71 @@ func (s *AdminCommunityEventsSuite) TestAdminCannotDeleteBecomeAdminPermission() testEventSenderCannotDeleteBecomeAdminPermission(s, community) } +func (s *AdminCommunityEventsSuite) TestAdminAcceptMemberRequestToJoinResponseSharedWithOtherEventSenders() { + additionalAdmin := s.newMessenger() + community := setUpOnRequestCommunityAndRoles(s, protobuf.CommunityMember_ROLE_ADMIN, []*Messenger{additionalAdmin}) + // set up additional user that will send request to join + user := s.newMessenger() + testAcceptMemberRequestToJoinResponseSharedWithOtherEventSenders(s, community, user, additionalAdmin) +} + +func (s *AdminCommunityEventsSuite) TestAdminAcceptMemberRequestToJoinNotConfirmedByControlNode() { + community := setUpOnRequestCommunityAndRoles(s, protobuf.CommunityMember_ROLE_ADMIN, []*Messenger{}) + // set up additional user that will send request to join + user := s.newMessenger() + testAcceptMemberRequestToJoinNotConfirmedByControlNode(s, community, user) +} + func (s *AdminCommunityEventsSuite) TestAdminAcceptMemberRequestToJoin() { - community := setUpOnRequestCommunityAndRoles(s, protobuf.CommunityMember_ROLE_ADMIN) + community := setUpOnRequestCommunityAndRoles(s, protobuf.CommunityMember_ROLE_ADMIN, []*Messenger{}) // set up additional user that will send request to join user := s.newMessenger() testAcceptMemberRequestToJoin(s, community, user) } +func (s *AdminCommunityEventsSuite) TestAdminRejectMemberRequestToJoinResponseSharedWithOtherEventSenders() { + additionalAdmin := s.newMessenger() + community := setUpOnRequestCommunityAndRoles(s, protobuf.CommunityMember_ROLE_ADMIN, []*Messenger{additionalAdmin}) + // set up additional user that will send request to join + user := s.newMessenger() + testRejectMemberRequestToJoinResponseSharedWithOtherEventSenders(s, community, user, additionalAdmin) +} + +func (s *AdminCommunityEventsSuite) TestAdminRejectMemberRequestToJoinNotConfirmedByControlNode() { + community := setUpOnRequestCommunityAndRoles(s, protobuf.CommunityMember_ROLE_ADMIN, []*Messenger{}) + + // set up additional user that will send request to join + user := s.newMessenger() + testRejectMemberRequestToJoinNotConfirmedByControlNode(s, community, user) +} + func (s *AdminCommunityEventsSuite) TestAdminRejectMemberRequestToJoin() { - community := setUpOnRequestCommunityAndRoles(s, protobuf.CommunityMember_ROLE_ADMIN) + community := setUpOnRequestCommunityAndRoles(s, protobuf.CommunityMember_ROLE_ADMIN, []*Messenger{}) // set up additional user that will send request to join user := s.newMessenger() testRejectMemberRequestToJoin(s, community, user) } +func (s *AdminCommunityEventsSuite) TestAdminRequestToJoinStateCannotBeOverridden() { + additionalAdmin := s.newMessenger() + community := setUpOnRequestCommunityAndRoles(s, protobuf.CommunityMember_ROLE_ADMIN, []*Messenger{additionalAdmin}) + + // set up additional user that will send request to join + user := s.newMessenger() + testEventSenderCannotOverrideRequestToJoinState(s, community, user, additionalAdmin) +} + +func (s *AdminCommunityEventsSuite) TestAdminControlNodeHandlesMultipleEventSenderRequestToJoinDecisions() { + additionalAdmin := s.newMessenger() + community := setUpOnRequestCommunityAndRoles(s, protobuf.CommunityMember_ROLE_ADMIN, []*Messenger{additionalAdmin}) + + // set up additional user that will send request to join + user := s.newMessenger() + testControlNodeHandlesMultipleEventSenderRequestToJoinDecisions(s, community, user, additionalAdmin) +} + func (s *AdminCommunityEventsSuite) TestAdminCreateEditDeleteCategories() { community := setUpCommunityAndRoles(s, protobuf.CommunityMember_ROLE_ADMIN) testCreateEditDeleteCategories(s, community) diff --git a/protocol/messenger_communities.go b/protocol/messenger_communities.go index b5af457d3..03ca69a6a 100644 --- a/protocol/messenger_communities.go +++ b/protocol/messenger_communities.go @@ -254,6 +254,33 @@ func (m *Messenger) handleCommunitiesSubscription(c chan *communities.Subscripti } } + if sub.AcceptedRequestsToJoin != nil { + for _, requestID := range sub.AcceptedRequestsToJoin { + accept := &requests.AcceptRequestToJoinCommunity{ + ID: requestID, + } + _, err := m.AcceptRequestToJoinCommunity(accept) + if err != nil { + m.logger.Warn("failed to accept request to join ", zap.Error(err)) + } + // TODO INFORM ADMINS + } + } + + if sub.RejectedRequestsToJoin != nil { + for _, requestID := range sub.RejectedRequestsToJoin { + reject := &requests.DeclineRequestToJoinCommunity{ + ID: requestID, + } + _, err := m.DeclineRequestToJoinCommunity(reject) + if err != nil { + m.logger.Warn("failed to decline request to join ", zap.Error(err)) + } + + // TODO INFORM ADMINS + } + } + case <-ticker.C: // If we are not online, we don't even try if !m.online() { @@ -951,7 +978,15 @@ func (m *Messenger) RequestToJoinCommunity(request *requests.RequestToJoinCommun } if !community.AcceptRequestToJoinAutomatically() { - // send request to join also to community privileged members + // send request to join also to community admins but without revealed addresses + requestToJoinProto.RevealedAccounts = make([]*protobuf.RevealedAccount, 0) + payload, err = proto.Marshal(requestToJoinProto) + if err != nil { + return nil, err + } + + rawMessage.Payload = payload + privilegedMembers := community.GetPrivilegedMembers() for _, privilegedMember := range privilegedMembers { _, err := m.sender.SendPrivate(context.Background(), privilegedMember, &rawMessage) @@ -1320,6 +1355,9 @@ func (m *Messenger) AcceptRequestToJoinCommunity(request *requests.AcceptRequest if notification != nil { notification.MembershipStatus = ActivityCenterMembershipStatusAccepted + if community.HasPermissionToSendCommunityEvents() { + notification.MembershipStatus = ActivityCenterMembershipStatusAcceptedPending + } notification.Read = true notification.Accepted = true notification.UpdatedAt = m.getCurrentTimeInMillis() @@ -1353,7 +1391,19 @@ func (m *Messenger) DeclineRequestToJoinCommunity(request *requests.DeclineReque response := &MessengerResponse{} if notification != nil { + dbRequest, err := m.communitiesManager.GetRequestToJoin(request.ID) + if err != nil { + return nil, err + } + community, err := m.communitiesManager.GetByID(dbRequest.CommunityID) + if err != nil { + return nil, err + } + notification.MembershipStatus = ActivityCenterMembershipStatusDeclined + if community.HasPermissionToSendCommunityEvents() { + notification.MembershipStatus = ActivityCenterMembershipStatusDeclinedPending + } notification.Read = true notification.Dismissed = true notification.UpdatedAt = m.getCurrentTimeInMillis() @@ -1972,6 +2022,14 @@ func (m *Messenger) AcceptedRequestsToJoinForCommunity(id types.HexBytes) ([]*co return m.communitiesManager.AcceptedRequestsToJoinForCommunity(id) } +func (m *Messenger) AcceptedPendingRequestsToJoinForCommunity(id types.HexBytes) ([]*communities.RequestToJoin, error) { + return m.communitiesManager.AcceptedPendingRequestsToJoinForCommunity(id) +} + +func (m *Messenger) DeclinedPendingRequestsToJoinForCommunity(id types.HexBytes) ([]*communities.RequestToJoin, error) { + return m.communitiesManager.DeclinedPendingRequestsToJoinForCommunity(id) +} + func (m *Messenger) RemoveUserFromCommunity(id types.HexBytes, pkString string) (*MessengerResponse, error) { publicKey, err := common.HexToPubkey(pkString) if err != nil { diff --git a/protocol/messenger_handler.go b/protocol/messenger_handler.go index 8fec7842d..9146a6800 100644 --- a/protocol/messenger_handler.go +++ b/protocol/messenger_handler.go @@ -1384,13 +1384,15 @@ func (m *Messenger) HandleCommunityRequestToJoin(state *ReceivedMessageState, si return err } - if requestToJoin.State == communities.RequestToJoinStateAccepted { + if requestToJoin.State == communities.RequestToJoinStateAccepted || requestToJoin.State == communities.RequestToJoinStateAcceptedPending { accept := &requests.AcceptRequestToJoinCommunity{ ID: requestToJoin.ID, } _, err = m.AcceptRequestToJoinCommunity(accept) if err != nil { if err == communities.ErrNoPermissionToJoin { + // only control node will end up here as it's the only one that + // performed token permission checks requestToJoin.State = communities.RequestToJoinStateDeclined } else { return err @@ -1398,7 +1400,9 @@ func (m *Messenger) HandleCommunityRequestToJoin(state *ReceivedMessageState, si } } - if requestToJoin.State == communities.RequestToJoinStateDeclined { + declinedOrDeclinedPending := requestToJoin.State == communities.RequestToJoinStateDeclined || requestToJoin.State == communities.RequestToJoinStateDeclinedPending + + if declinedOrDeclinedPending { cancel := &requests.DeclineRequestToJoinCommunity{ ID: requestToJoin.ID, }