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