diff --git a/protocol/communities/community.go b/protocol/communities/community.go index 5a63ac978..e46c378af 100644 --- a/protocol/communities/community.go +++ b/protocol/communities/community.go @@ -936,42 +936,67 @@ func (o *Community) BanUserFromCommunity(pk *ecdsa.PublicKey, communityBanInfo * return o.config.CommunityDescription, nil } -func (o *Community) AddRoleToMember(pk *ecdsa.PublicKey, role protobuf.CommunityMember_Roles) (*protobuf.CommunityDescription, error) { - o.mutex.Lock() - defer o.mutex.Unlock() - - if !o.IsControlNode() { - return nil, ErrNotControlNode - } - +func (o *Community) setRoleToMember(pk *ecdsa.PublicKey, role protobuf.CommunityMember_Roles, setter func(member *protobuf.CommunityMember, role protobuf.CommunityMember_Roles) bool) (*protobuf.CommunityDescription, error) { updated := false - addRole := func(member *protobuf.CommunityMember) { - roles := make(map[protobuf.CommunityMember_Roles]bool) - roles[role] = true - if !o.memberHasRoles(member, roles) { - member.Roles = append(member.Roles, role) - updated = true - } - } member := o.getMember(pk) if member != nil { - addRole(member) + updated = setter(member, role) } for channelID := range o.chats() { chatMember := o.getChatMember(pk, channelID) if chatMember != nil { - addRole(member) + _ = setter(member, role) } } if updated { o.increaseClock() } + return o.config.CommunityDescription, nil } +func (o *Community) SetRoleToMember(pk *ecdsa.PublicKey, role protobuf.CommunityMember_Roles) (*protobuf.CommunityDescription, error) { + if !o.IsControlNode() { + return nil, ErrNotControlNode + } + o.mutex.Lock() + defer o.mutex.Unlock() + + setRole := func(member *protobuf.CommunityMember, role protobuf.CommunityMember_Roles) bool { + if len(member.Roles) == 1 && member.Roles[0] == role { + return false + } + member.Roles = []protobuf.CommunityMember_Roles{role} + return true + } + + return o.setRoleToMember(pk, role, setRole) +} + +// Deprecated: roles are mutually exclusive, use SetRoleToMember instead. +func (o *Community) AddRoleToMember(pk *ecdsa.PublicKey, role protobuf.CommunityMember_Roles) (*protobuf.CommunityDescription, error) { + if !o.IsControlNode() { + return nil, ErrNotControlNode + } + o.mutex.Lock() + defer o.mutex.Unlock() + + addRole := func(member *protobuf.CommunityMember, role protobuf.CommunityMember_Roles) bool { + roles := make(map[protobuf.CommunityMember_Roles]bool) + roles[role] = true + if !o.memberHasRoles(member, roles) { + member.Roles = append(member.Roles, role) + return true + } + return false + } + + return o.setRoleToMember(pk, role, addRole) +} + func (o *Community) RemoveRoleFromMember(pk *ecdsa.PublicKey, role protobuf.CommunityMember_Roles) (*protobuf.CommunityDescription, error) { o.mutex.Lock() defer o.mutex.Unlock() diff --git a/protocol/communities/manager.go b/protocol/communities/manager.go index 0df03ac39..d797fd24c 100644 --- a/protocol/communities/manager.go +++ b/protocol/communities/manager.go @@ -180,6 +180,7 @@ func (t *HistoryArchiveDownloadTask) Cancel() { } type membersReevaluationTask struct { + lastStartTime time.Time lastSuccessTime time.Time onDemandRequestTime time.Time mutex sync.Mutex @@ -1055,38 +1056,81 @@ func (m *Manager) EditCommunityTokenPermission(request *requests.EditCommunityTo return community, changes, nil } +type reevaluateMemberRole struct { + old protobuf.CommunityMember_Roles + new protobuf.CommunityMember_Roles +} + +func (rmr reevaluateMemberRole) hasChanged() bool { + return rmr.old != rmr.new +} + +func (rmr reevaluateMemberRole) isPrivileged() bool { + return rmr.new != protobuf.CommunityMember_ROLE_NONE +} + +func (rmr reevaluateMemberRole) hasChangedToPrivileged() bool { + return rmr.hasChanged() && rmr.old == protobuf.CommunityMember_ROLE_NONE +} + +type reevaluateMembersResult struct { + membersToRemove map[string]struct{} + membersRoles map[string]*reevaluateMemberRole + membersToRemoveFromChannels map[string]map[string]struct{} + membersToAddToChannels map[string]map[string]protobuf.CommunityMember_ChannelRole +} + +func (rmr *reevaluateMembersResult) newPrivilegedRoles() (map[protobuf.CommunityMember_Roles][]*ecdsa.PublicKey, error) { + result := map[protobuf.CommunityMember_Roles][]*ecdsa.PublicKey{} + + for memberKey, roles := range rmr.membersRoles { + if roles.hasChangedToPrivileged() { + memberPubKey, err := common.HexToPubkey(memberKey) + if err != nil { + return nil, err + } + if result[roles.new] == nil { + result[roles.new] = []*ecdsa.PublicKey{} + } + result[roles.new] = append(result[roles.new], memberPubKey) + } + } + + return result, nil +} + // use it only for testing purposes func (m *Manager) ReevaluateMembers(communityID types.HexBytes) (*Community, map[protobuf.CommunityMember_Roles][]*ecdsa.PublicKey, error) { return m.reevaluateMembers(communityID) } +// First, the community is read from the database, +// then the members are reevaluated, and only then +// the community is locked and changes are applied. +// NOTE: Changes made to the same community +// while reevaluation is ongoing are respected +// and do not affect the result of this function. +// If permissions are changed in the meantime, +// they will be accommodated with the next reevaluation. func (m *Manager) reevaluateMembers(communityID types.HexBytes) (*Community, map[protobuf.CommunityMember_Roles][]*ecdsa.PublicKey, error) { - m.communityLock.Lock(communityID) - defer m.communityLock.Unlock(communityID) - community, err := m.GetByID(communityID) if err != nil { return nil, nil, err } - // TODO: Control node needs to be notified to do a permission check if TokenMasters did airdrop - // of the token which is using in a community permissions if !community.IsControlNode() { return nil, nil, ErrNotEnoughPermissions } communityPermissionsPreParsedData, channelPermissionsPreParsedData := PreParsePermissionsData(community.tokenPermissions()) - hasMemberPermissions := communityPermissionsPreParsedData[protobuf.CommunityTokenPermission_BECOME_MEMBER] != nil - - if len(channelPermissionsPreParsedData) == 0 { - community.PopulateChannelsWithAllMembers() + result := &reevaluateMembersResult{ + membersToRemove: map[string]struct{}{}, + membersRoles: map[string]*reevaluateMemberRole{}, + membersToRemoveFromChannels: map[string]map[string]struct{}{}, + membersToAddToChannels: map[string]map[string]protobuf.CommunityMember_ChannelRole{}, } - newPrivilegedRoles := make(map[protobuf.CommunityMember_Roles][]*ecdsa.PublicKey) - newPrivilegedRoles[protobuf.CommunityMember_ROLE_TOKEN_MASTER] = []*ecdsa.PublicKey{} - newPrivilegedRoles[protobuf.CommunityMember_ROLE_ADMIN] = []*ecdsa.PublicKey{} - membersAccounts, err := m.persistence.GetCommunityRequestsToJoinRevealedAddresses(community.ID()) if err != nil { return nil, nil, err @@ -1102,128 +1146,200 @@ func (m *Manager) reevaluateMembers(communityID types.HexBytes) (*Community, map continue } - isCurrentRoleTokenMaster := community.IsMemberTokenMaster(memberPubKey) - isCurrentRoleAdmin := community.IsMemberAdmin(memberPubKey) - - revealedAccount, exists := membersAccounts[memberKey] - memberHasWallet := exists - - // Check if user has privilege role without sharing the account to controlNode - // or user treated as a member without wallet in closed community - if !memberHasWallet && (hasMemberPermissions || isCurrentRoleTokenMaster || isCurrentRoleAdmin) { - _, err = community.RemoveUserFromOrg(memberPubKey) - if err != nil { - return nil, nil, err - } + revealedAccount, memberHasWallet := membersAccounts[memberKey] + if !memberHasWallet { + result.membersToRemove[memberKey] = struct{}{} continue } accountsAndChainIDs := revealedAccountsToAccountsAndChainIDsCombination(revealedAccount) - isNewRoleTokenMaster, err := m.ReevaluatePrivilegedMember( - community, - communityPermissionsPreParsedData[protobuf.CommunityTokenPermission_BECOME_TOKEN_MASTER], - accountsAndChainIDs, - memberPubKey, - protobuf.CommunityMember_ROLE_TOKEN_MASTER, isCurrentRoleTokenMaster) - - if err != nil { - return nil, nil, err + result.membersRoles[memberKey] = &reevaluateMemberRole{ + old: community.MemberRole(memberPubKey), + new: protobuf.CommunityMember_ROLE_NONE, } - if isNewRoleTokenMaster { - if !isCurrentRoleTokenMaster { - newPrivilegedRoles[protobuf.CommunityMember_ROLE_TOKEN_MASTER] = - append(newPrivilegedRoles[protobuf.CommunityMember_ROLE_TOKEN_MASTER], memberPubKey) + becomeTokenMasterPermissions := communityPermissionsPreParsedData[protobuf.CommunityTokenPermission_BECOME_TOKEN_MASTER] + if becomeTokenMasterPermissions != nil { + permissionResponse, err := m.PermissionChecker.CheckPermissions(becomeTokenMasterPermissions, accountsAndChainIDs, true) + if err != nil { + return nil, nil, err } - // Skip further validation if user has TokenMaster permissions - continue - } - isNewRoleAdmin, err := m.ReevaluatePrivilegedMember( - community, - communityPermissionsPreParsedData[protobuf.CommunityTokenPermission_BECOME_ADMIN], - accountsAndChainIDs, - memberPubKey, - protobuf.CommunityMember_ROLE_ADMIN, isCurrentRoleAdmin) - - if err != nil { - return nil, nil, err - } - - if isNewRoleAdmin { - if !isCurrentRoleAdmin { - newPrivilegedRoles[protobuf.CommunityMember_ROLE_ADMIN] = - append(newPrivilegedRoles[protobuf.CommunityMember_ROLE_ADMIN], memberPubKey) + if permissionResponse.Satisfied { + result.membersRoles[memberKey].new = protobuf.CommunityMember_ROLE_TOKEN_MASTER + // Skip further validation if user has TokenMaster permissions + continue } - // Skip further validation if user has Admin permissions - continue } - if hasMemberPermissions { - permissionResponse, err := m.PermissionChecker.CheckPermissions( - communityPermissionsPreParsedData[protobuf.CommunityTokenPermission_BECOME_MEMBER], - accountsAndChainIDs, - true) + becomeAdminPermissions := communityPermissionsPreParsedData[protobuf.CommunityTokenPermission_BECOME_ADMIN] + if becomeAdminPermissions != nil { + permissionResponse, err := m.PermissionChecker.CheckPermissions(becomeAdminPermissions, accountsAndChainIDs, true) + if err != nil { + return nil, nil, err + } + + if permissionResponse.Satisfied { + result.membersRoles[memberKey].new = protobuf.CommunityMember_ROLE_ADMIN + // Skip further validation if user has Admin permissions + continue + } + } + + becomeMemberPermissions := communityPermissionsPreParsedData[protobuf.CommunityTokenPermission_BECOME_MEMBER] + if becomeMemberPermissions != nil { + permissionResponse, err := m.PermissionChecker.CheckPermissions(becomeMemberPermissions, accountsAndChainIDs, true) if err != nil { return nil, nil, err } if !permissionResponse.Satisfied { - _, err = community.RemoveUserFromOrg(memberPubKey) - if err != nil { - return nil, nil, err - } + result.membersToRemove[memberKey] = struct{}{} // Skip channels validation if user has been removed continue } } - err = m.reevaluateMemberChannelsPermissions(community, memberPubKey, channelPermissionsPreParsedData, accountsAndChainIDs) + addToChannels, removeFromChannels, err := m.reevaluateMemberChannelsPermissions(community, memberPubKey, channelPermissionsPreParsedData, accountsAndChainIDs) if err != nil { return nil, nil, err } + result.membersToAddToChannels[memberKey] = addToChannels + result.membersToRemoveFromChannels[memberKey] = removeFromChannels + } + + newPrivilegedRoles, err := result.newPrivilegedRoles() + if err != nil { + return nil, nil, err + } + + // Note: community itself may have changed in the meantime of permissions reevaluation. + community, err = m.applyReevaluateMembersResult(communityID, result) + if err != nil { + return nil, nil, err } return community, newPrivilegedRoles, m.saveAndPublish(community) } -func (m *Manager) reevaluateMemberChannelsPermissions(community *Community, memberPubKey *ecdsa.PublicKey, - channelPermissionsPreParsedData map[string]*PreParsedCommunityPermissionsData, accountsAndChainIDs []*AccountChainIDsCombination) error { +// Apply results on the most up-to-date community. +func (m *Manager) applyReevaluateMembersResult(communityID types.HexBytes, result *reevaluateMembersResult) (*Community, error) { + m.communityLock.Lock(communityID) + defer m.communityLock.Unlock(communityID) - if len(channelPermissionsPreParsedData) == 0 { - return nil + community, err := m.GetByID(communityID) + if err != nil { + return nil, err } + if !community.IsControlNode() { + return nil, ErrNotEnoughPermissions + } + + // Remove members. + for memberKey := range result.membersToRemove { + memberPubKey, err := common.HexToPubkey(memberKey) + if err != nil { + return nil, err + } + _, err = community.RemoveUserFromOrg(memberPubKey) + if err != nil { + return nil, err + } + } + + // Ensure members have proper roles. + for memberKey, roles := range result.membersRoles { + memberPubKey, err := common.HexToPubkey(memberKey) + if err != nil { + return nil, err + } + + if !community.HasMember(memberPubKey) { + continue + } + + _, err = community.SetRoleToMember(memberPubKey, roles.new) + if err != nil { + return nil, err + } + + // Ensure privileged members can post in all chats. + if roles.isPrivileged() { + for channelID := range community.Chats() { + _, err = community.AddMemberToChat(channelID, memberPubKey, []protobuf.CommunityMember_Roles{roles.new}, protobuf.CommunityMember_CHANNEL_ROLE_POSTER) + if err != nil { + return nil, err + } + } + } + } + + // Remove members from channels. + for memberKey, channels := range result.membersToRemoveFromChannels { + memberPubKey, err := common.HexToPubkey(memberKey) + if err != nil { + return nil, err + } + + for channelID := range channels { + _, err = community.RemoveUserFromChat(memberPubKey, channelID) + if err != nil { + return nil, err + } + } + } + + // Add unprivileged members to channels. + for memberKey, channels := range result.membersToAddToChannels { + memberPubKey, err := common.HexToPubkey(memberKey) + if err != nil { + return nil, err + } + + if !community.HasMember(memberPubKey) { + continue + } + + for channelID, channelRole := range channels { + _, err = community.AddMemberToChat(channelID, memberPubKey, []protobuf.CommunityMember_Roles{protobuf.CommunityMember_ROLE_NONE}, channelRole) + if err != nil { + return nil, err + } + } + } + + return community, nil +} + +func (m *Manager) reevaluateMemberChannelsPermissions(community *Community, memberPubKey *ecdsa.PublicKey, + channelPermissionsPreParsedData map[string]*PreParsedCommunityPermissionsData, accountsAndChainIDs []*AccountChainIDsCombination) (map[string]protobuf.CommunityMember_ChannelRole, map[string]struct{}, error) { + + addToChannels := map[string]protobuf.CommunityMember_ChannelRole{} + removeFromChannels := map[string]struct{}{} + // check which permissions we satisfy and which not channelPermissionsCheckResult, err := m.checkChannelsPermissions(channelPermissionsPreParsedData, accountsAndChainIDs, true) if err != nil { - return err + return nil, nil, err } for channelID := range community.Chats() { - chatID := community.ChatID(channelID) - isMemberAlreadyInChannel := community.IsMemberInChat(memberPubKey, channelID) + channelPermissionsCheckResult, hasChannelPermission := channelPermissionsCheckResult[community.ChatID(channelID)] - channelPermissionsCheckResult, exists := channelPermissionsCheckResult[chatID] - - // if channel permissions were removed member must be added back - if !exists { - if !isMemberAlreadyInChannel { - _, err := community.AddMemberToChat(channelID, memberPubKey, []protobuf.CommunityMember_Roles{}, protobuf.CommunityMember_CHANNEL_ROLE_POSTER) - if err != nil { - return err - } - } + // ensure member is added if channel has no permissions + if !hasChannelPermission { + addToChannels[channelID] = protobuf.CommunityMember_CHANNEL_ROLE_POSTER continue } - viewAndPostSatisfied, viewAndPosPermissionExists := channelPermissionsCheckResult[protobuf.CommunityTokenPermission_CAN_VIEW_AND_POST_CHANNEL] + viewAndPostSatisfied, viewAndPostPermissionExists := channelPermissionsCheckResult[protobuf.CommunityTokenPermission_CAN_VIEW_AND_POST_CHANNEL] viewOnlySatisfied, viewOnlyPermissionExists := channelPermissionsCheckResult[protobuf.CommunityTokenPermission_CAN_VIEW_CHANNEL] satisfied := false channelRole := protobuf.CommunityMember_CHANNEL_ROLE_VIEWER - if viewAndPosPermissionExists && viewAndPostSatisfied { + if viewAndPostPermissionExists && viewAndPostSatisfied { satisfied = viewAndPostSatisfied channelRole = protobuf.CommunityMember_CHANNEL_ROLE_POSTER } else if !satisfied && viewOnlyPermissionExists { @@ -1231,19 +1347,13 @@ func (m *Manager) reevaluateMemberChannelsPermissions(community *Community, memb } if satisfied { - // Add the member back to the chat member list in case the role changed (it replaces the previous values) - _, err := community.AddMemberToChat(channelID, memberPubKey, []protobuf.CommunityMember_Roles{}, channelRole) - if err != nil { - return err - } - } else if !satisfied && isMemberAlreadyInChannel { - _, err := community.RemoveUserFromChat(memberPubKey, channelID) - if err != nil { - return err - } + addToChannels[channelID] = channelRole + } else { + removeFromChannels[channelID] = struct{}{} } } - return nil + + return addToChannels, removeFromChannels, nil } func (m *Manager) checkChannelsPermissions(channelsPermissionsPreParsedData map[string]*PreParsedCommunityPermissionsData, accountsAndChainIDs []*AccountChainIDsCombination, shortcircuit bool) (map[string]map[protobuf.CommunityTokenPermission_Type]bool, error) { @@ -1302,7 +1412,7 @@ func (m *Manager) reevaluateMembersLoop(communityID types.HexBytes, reevaluateOn } if !task.lastSuccessTime.Before(time.Now().Add(-memberPermissionsCheckInterval)) && - !task.lastSuccessTime.Before(task.onDemandRequestTime) { + !task.lastStartTime.Before(task.onDemandRequestTime) { return false } @@ -1327,6 +1437,10 @@ func (m *Manager) reevaluateMembersLoop(communityID types.HexBytes, reevaluateOn return nil } + task.mutex.Lock() + task.lastStartTime = time.Now() + task.mutex.Unlock() + err = m.reevaluateCommunityMembersPermissions(communityID) if err != nil { if errors.Is(err, ErrOrgNotFound) { @@ -1338,9 +1452,10 @@ func (m *Manager) reevaluateMembersLoop(communityID types.HexBytes, reevaluateOn } task.mutex.Lock() - defer task.mutex.Unlock() task.lastSuccessTime = time.Now() + task.mutex.Unlock() + m.logger.Info("reevaluation finished", zap.String("communityID", communityID.String()), zap.Duration("elapsed", task.lastSuccessTime.Sub(task.lastStartTime))) return nil } @@ -5285,55 +5400,6 @@ func (m *Manager) GetRevealedAddresses(communityID types.HexBytes, memberPk stri return response, err } -func (m *Manager) ReevaluatePrivilegedMember(community *Community, permissionsData *PreParsedCommunityPermissionsData, - accountsAndChainIDs []*AccountChainIDsCombination, memberPubKey *ecdsa.PublicKey, - privilegedRole protobuf.CommunityMember_Roles, alreadyHasPrivilegedRole bool) (bool, error) { - - hasPrivilegedRolePermissions := permissionsData != nil - removeCurrentRole := false - - if hasPrivilegedRolePermissions { - permissionResponse, err := m.PermissionChecker.CheckPermissions(permissionsData, accountsAndChainIDs, true) - if err != nil { - m.logger.Warn("check privileged permission failed: %v", zap.Error(err)) - return alreadyHasPrivilegedRole, err - } else if permissionResponse.Satisfied && !alreadyHasPrivilegedRole { - _, err = community.AddRoleToMember(memberPubKey, privilegedRole) - if err != nil { - return alreadyHasPrivilegedRole, err - } - alreadyHasPrivilegedRole = true - } else if !permissionResponse.Satisfied && alreadyHasPrivilegedRole { - removeCurrentRole = true - alreadyHasPrivilegedRole = false - } - } - - // Remove privileged role if user does not pass role permissions check or - // Community does not have permissions but user has a role - if removeCurrentRole || (!hasPrivilegedRolePermissions && alreadyHasPrivilegedRole) { - _, err := community.RemoveRoleFromMember(memberPubKey, privilegedRole) - if err != nil { - return alreadyHasPrivilegedRole, err - } - alreadyHasPrivilegedRole = false - } - - if alreadyHasPrivilegedRole { - // Make sure privileged user is added to every channel - for channelID := range community.Chats() { - if !community.IsMemberInChat(memberPubKey, channelID) { - _, err := community.AddMemberToChat(channelID, memberPubKey, []protobuf.CommunityMember_Roles{privilegedRole}, protobuf.CommunityMember_CHANNEL_ROLE_POSTER) - if err != nil { - return alreadyHasPrivilegedRole, err - } - } - } - } - - return alreadyHasPrivilegedRole, nil -} - func (m *Manager) handleCommunityTokensMetadata(community *Community) error { communityID := community.IDString() communityTokens := community.CommunityTokensMetadata() diff --git a/protocol/communities_messenger_token_permissions_test.go b/protocol/communities_messenger_token_permissions_test.go index ab8c103d1..03e3c120f 100644 --- a/protocol/communities_messenger_token_permissions_test.go +++ b/protocol/communities_messenger_token_permissions_test.go @@ -1606,16 +1606,16 @@ func (s *MessengerCommunitiesTokenPermissionsSuite) testReevaluateMemberPrivileg }, } + waitOnCommunityPermissionCreated := waitOnCommunitiesEvent(s.owner, func(sub *communities.Subscription) bool { + return sub.Community.HasTokenPermissions() + }) + response, err := s.owner.CreateCommunityTokenPermission(createTokenPermission) s.Require().NoError(err) s.Require().NotNil(response) s.Require().Len(response.Communities(), 1) s.Require().True(response.Communities()[0].HasTokenPermissions()) - waitOnCommunityPermissionCreated := waitOnCommunitiesEvent(s.owner, func(sub *communities.Subscription) bool { - return sub.Community.HasTokenPermissions() - }) - err = <-waitOnCommunityPermissionCreated s.Require().NoError(err) @@ -1663,6 +1663,13 @@ func (s *MessengerCommunitiesTokenPermissionsSuite) testReevaluateMemberPrivileg PermissionID: tokenPermission.Id, } + waitOnPermissionsReevaluated = waitOnCommunitiesEvent(s.owner, func(sub *communities.Subscription) bool { + if sub.Community == nil { + return false + } + return !checkRoleBasedOnThePermissionType(permissionType, &s.alice.identity.PublicKey, sub.Community) + }) + response, err = s.owner.DeleteCommunityTokenPermission(deleteTokenPermission) s.Require().NoError(err) s.Require().NotNil(response) @@ -1673,13 +1680,6 @@ func (s *MessengerCommunitiesTokenPermissionsSuite) testReevaluateMemberPrivileg s.Require().NoError(err) s.Require().False(community.HasTokenPermissions()) - waitOnPermissionsReevaluated = waitOnCommunitiesEvent(s.owner, func(sub *communities.Subscription) bool { - if sub.Community == nil { - return false - } - return !checkRoleBasedOnThePermissionType(permissionType, &s.alice.identity.PublicKey, sub.Community) - }) - err = s.owner.communitiesManager.ForceMembersReevaluation(community.ID()) s.Require().NoError(err)