feat: introduce channel-level encryption
- distribute ratchet keys at both community and channel levels - use explicit `HashRatchetGroupID` in ecryption layer, instead of inheriting `groupID` from `CommunityID` - populate `HashRatchetGroupID` with `CommunityID+ChannelID` for channels, and `CommunityID` for whole community - hydrate channels with members; channel members are now subset of community members - include channel permissions in periodic permissions check closes: status-im/status-desktop#10998
This commit is contained in:
parent
30da8390bd
commit
367b7722d1
|
@ -285,7 +285,7 @@ func (s *MessageSender) sendCommunity(
|
|||
// Check if it's a key exchange message. In this case we send it
|
||||
// to all the recipients
|
||||
if rawMessage.CommunityKeyExMsgType != KeyExMsgNone {
|
||||
keyExMessageSpecs, err := s.protocol.GetKeyExMessageSpecs(rawMessage.CommunityID, s.identity, rawMessage.Recipients, rawMessage.CommunityKeyExMsgType == KeyExMsgRekey)
|
||||
keyExMessageSpecs, err := s.protocol.GetKeyExMessageSpecs(rawMessage.HashRatchetGroupID, s.identity, rawMessage.Recipients, rawMessage.CommunityKeyExMsgType == KeyExMsgRekey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -307,7 +307,7 @@ func (s *MessageSender) sendCommunity(
|
|||
|
||||
// If it's a chat message, we send it on the community chat topic
|
||||
if ShouldCommunityMessageBeEncrypted(rawMessage.MessageType) {
|
||||
messageSpec, err := s.protocol.BuildHashRatchetMessage(rawMessage.CommunityID, wrappedMessage)
|
||||
messageSpec, err := s.protocol.BuildHashRatchetMessage(rawMessage.HashRatchetGroupID, wrappedMessage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -35,4 +35,5 @@ type RawMessage struct {
|
|||
CommunityKeyExMsgType CommKeyExMsgType
|
||||
Ephemeral bool
|
||||
BeforeDispatch func(*RawMessage) error
|
||||
HashRatchetGroupID []byte
|
||||
}
|
||||
|
|
|
@ -681,6 +681,20 @@ func (o *Community) GetMember(pk *ecdsa.PublicKey) *protobuf.CommunityMember {
|
|||
return o.getMember(pk)
|
||||
}
|
||||
|
||||
func (o *Community) getChatMember(pk *ecdsa.PublicKey, chatID string) *protobuf.CommunityMember {
|
||||
if !o.hasMember(pk) {
|
||||
return nil
|
||||
}
|
||||
|
||||
chat, ok := o.config.CommunityDescription.Chats[chatID]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
key := common.PubkeyToHex(pk)
|
||||
return chat.Members[key]
|
||||
}
|
||||
|
||||
func (o *Community) hasMember(pk *ecdsa.PublicKey) bool {
|
||||
|
||||
member := o.getMember(pk)
|
||||
|
@ -736,18 +750,7 @@ func (o *Community) IsMemberInChat(pk *ecdsa.PublicKey, chatID string) bool {
|
|||
o.mutex.Lock()
|
||||
defer o.mutex.Unlock()
|
||||
|
||||
if !o.hasMember(pk) {
|
||||
return false
|
||||
}
|
||||
|
||||
chat, ok := o.config.CommunityDescription.Chats[chatID]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
key := common.PubkeyToHex(pk)
|
||||
_, ok = chat.Members[key]
|
||||
return ok
|
||||
return o.getChatMember(pk, chatID) != nil
|
||||
}
|
||||
|
||||
func (o *Community) RemoveUserFromChat(pk *ecdsa.PublicKey, chatID string) (*protobuf.CommunityDescription, error) {
|
||||
|
@ -905,17 +908,27 @@ func (o *Community) AddRoleToMember(pk *ecdsa.PublicKey, role protobuf.Community
|
|||
}
|
||||
|
||||
updated := false
|
||||
member := o.getMember(pk)
|
||||
if member != nil {
|
||||
addRole := func(member *protobuf.CommunityMember) {
|
||||
roles := make(map[protobuf.CommunityMember_Roles]bool)
|
||||
roles[role] = true
|
||||
if !o.hasMemberPermission(member, roles) {
|
||||
member.Roles = append(member.Roles, role)
|
||||
o.config.CommunityDescription.Members[common.PubkeyToHex(pk)] = member
|
||||
updated = true
|
||||
}
|
||||
}
|
||||
|
||||
member := o.getMember(pk)
|
||||
if member != nil {
|
||||
addRole(member)
|
||||
}
|
||||
|
||||
for channelID := range o.chats() {
|
||||
chatMember := o.getChatMember(pk, channelID)
|
||||
if chatMember != nil {
|
||||
addRole(member)
|
||||
}
|
||||
}
|
||||
|
||||
if updated {
|
||||
o.increaseClock()
|
||||
}
|
||||
|
@ -931,8 +944,7 @@ func (o *Community) RemoveRoleFromMember(pk *ecdsa.PublicKey, role protobuf.Comm
|
|||
}
|
||||
|
||||
updated := false
|
||||
member := o.getMember(pk)
|
||||
if member != nil {
|
||||
removeRole := func(member *protobuf.CommunityMember) {
|
||||
roles := make(map[protobuf.CommunityMember_Roles]bool)
|
||||
roles[role] = true
|
||||
if o.hasMemberPermission(member, roles) {
|
||||
|
@ -943,11 +955,22 @@ func (o *Community) RemoveRoleFromMember(pk *ecdsa.PublicKey, role protobuf.Comm
|
|||
}
|
||||
}
|
||||
member.Roles = newRoles
|
||||
o.config.CommunityDescription.Members[common.PubkeyToHex(pk)] = member
|
||||
updated = true
|
||||
}
|
||||
}
|
||||
|
||||
member := o.getMember(pk)
|
||||
if member != nil {
|
||||
removeRole(member)
|
||||
}
|
||||
|
||||
for channelID := range o.chats() {
|
||||
chatMember := o.getChatMember(pk, channelID)
|
||||
if chatMember != nil {
|
||||
removeRole(member)
|
||||
}
|
||||
}
|
||||
|
||||
if updated {
|
||||
o.increaseClock()
|
||||
}
|
||||
|
@ -1328,16 +1351,20 @@ func (o *Community) ToBytes() ([]byte, error) {
|
|||
}
|
||||
|
||||
func (o *Community) Chats() map[string]*protobuf.CommunityChat {
|
||||
response := make(map[string]*protobuf.CommunityChat)
|
||||
|
||||
// Why are we checking here for nil, it should be the responsibility of the caller
|
||||
if o == nil {
|
||||
return response
|
||||
return make(map[string]*protobuf.CommunityChat)
|
||||
}
|
||||
|
||||
o.mutex.Lock()
|
||||
defer o.mutex.Unlock()
|
||||
|
||||
return o.chats()
|
||||
}
|
||||
|
||||
func (o *Community) chats() map[string]*protobuf.CommunityChat {
|
||||
response := make(map[string]*protobuf.CommunityChat)
|
||||
|
||||
if o.config != nil && o.config.CommunityDescription != nil {
|
||||
for k, v := range o.config.CommunityDescription.Chats {
|
||||
response[k] = v
|
||||
|
@ -1391,7 +1418,21 @@ func (o *Community) TokenPermissions() map[string]*protobuf.CommunityTokenPermis
|
|||
}
|
||||
|
||||
func (o *Community) HasTokenPermissions() bool {
|
||||
return len(o.config.CommunityDescription.TokenPermissions) > 0
|
||||
return o.config.CommunityDescription.TokenPermissions != nil && len(o.config.CommunityDescription.TokenPermissions) > 0
|
||||
}
|
||||
|
||||
func (o *Community) ChannelHasTokenPermissions(chatID string) bool {
|
||||
if !o.HasTokenPermissions() {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, tokenPermission := range o.TokenPermissions() {
|
||||
if includes(tokenPermission.ChatIds, chatID) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func TokenPermissionsByType(permissions map[string]*protobuf.CommunityTokenPermission, permissionType protobuf.CommunityTokenPermission_Type) []*protobuf.CommunityTokenPermission {
|
||||
|
@ -2064,6 +2105,8 @@ func (o *Community) createChat(chatID string, chat *protobuf.CommunityChat) erro
|
|||
}
|
||||
}
|
||||
|
||||
chat.Members = o.config.CommunityDescription.Members
|
||||
|
||||
o.config.CommunityDescription.Chats[chatID] = chat
|
||||
|
||||
return nil
|
||||
|
|
|
@ -63,7 +63,9 @@ func evaluateCommunityLevelEncryptionKeyAction(origin, modified *Community, chan
|
|||
func evaluateChannelLevelEncryptionKeyActions(origin, modified *Community, changes *CommunityChanges) *map[string]EncryptionKeyAction {
|
||||
result := make(map[string]EncryptionKeyAction)
|
||||
|
||||
for chatID := range modified.config.CommunityDescription.Chats {
|
||||
for channelID := range modified.config.CommunityDescription.Chats {
|
||||
chatID := modified.IDString() + channelID
|
||||
|
||||
originChannelViewOnlyPermissions := origin.ChannelTokenPermissionsByType(chatID, protobuf.CommunityTokenPermission_CAN_VIEW_CHANNEL)
|
||||
originChannelViewAndPostPermissions := origin.ChannelTokenPermissionsByType(chatID, protobuf.CommunityTokenPermission_CAN_VIEW_AND_POST_CHANNEL)
|
||||
originChannelPermissions := append(originChannelViewOnlyPermissions, originChannelViewAndPostPermissions...)
|
||||
|
@ -75,13 +77,13 @@ func evaluateChannelLevelEncryptionKeyActions(origin, modified *Community, chang
|
|||
membersAdded := make(map[string]*protobuf.CommunityMember)
|
||||
membersRemoved := make(map[string]*protobuf.CommunityMember)
|
||||
|
||||
chatChanges, ok := changes.ChatsModified[chatID]
|
||||
chatChanges, ok := changes.ChatsModified[channelID]
|
||||
if ok {
|
||||
membersAdded = chatChanges.MembersAdded
|
||||
membersRemoved = chatChanges.MembersRemoved
|
||||
}
|
||||
|
||||
result[chatID] = *evaluateEncryptionKeyAction(originChannelPermissions, modifiedChannelPermissions, modified.config.CommunityDescription.Members, membersAdded, membersRemoved)
|
||||
result[channelID] = *evaluateEncryptionKeyAction(originChannelPermissions, modifiedChannelPermissions, modified.config.CommunityDescription.Chats[channelID].Members, membersAdded, membersRemoved)
|
||||
}
|
||||
|
||||
return &result
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"github.com/status-im/status-go/eth-node/crypto"
|
||||
"github.com/status-im/status-go/eth-node/types"
|
||||
"github.com/status-im/status-go/protocol/common"
|
||||
"github.com/status-im/status-go/protocol/protobuf"
|
||||
)
|
||||
|
@ -622,7 +623,8 @@ func (s *CommunityEncryptionKeyActionSuite) TestCommunityLevelKeyActions_Permiss
|
|||
}
|
||||
|
||||
func (s *CommunityEncryptionKeyActionSuite) TestChannelLevelKeyActions() {
|
||||
chatID := "0x1234"
|
||||
channelID := "1234"
|
||||
chatID := types.EncodeHex(crypto.CompressPubkey(&s.identity.PublicKey)) + channelID
|
||||
testCases := []struct {
|
||||
name string
|
||||
originPermissions []*protobuf.CommunityTokenPermission
|
||||
|
@ -736,7 +738,7 @@ func (s *CommunityEncryptionKeyActionSuite) TestChannelLevelKeyActions() {
|
|||
for _, tc := range testCases {
|
||||
s.Run(tc.name, func() {
|
||||
origin := createTestCommunity(s.identity)
|
||||
_, err := origin.CreateChat(chatID, &protobuf.CommunityChat{
|
||||
_, err := origin.CreateChat(channelID, &protobuf.CommunityChat{
|
||||
Members: map[string]*protobuf.CommunityMember{},
|
||||
Permissions: &protobuf.CommunityPermissions{Access: protobuf.CommunityPermissions_NO_MEMBERSHIP},
|
||||
Identity: &protobuf.ChatIdentity{},
|
||||
|
@ -752,7 +754,7 @@ func (s *CommunityEncryptionKeyActionSuite) TestChannelLevelKeyActions() {
|
|||
for _, member := range tc.originMembers {
|
||||
_, err := origin.AddMember(member, []protobuf.CommunityMember_Roles{})
|
||||
s.Require().NoError(err)
|
||||
_, err = origin.AddMemberToChat(chatID, member, []protobuf.CommunityMember_Roles{})
|
||||
_, err = origin.AddMemberToChat(channelID, member, []protobuf.CommunityMember_Roles{})
|
||||
s.Require().NoError(err)
|
||||
}
|
||||
|
||||
|
@ -763,12 +765,12 @@ func (s *CommunityEncryptionKeyActionSuite) TestChannelLevelKeyActions() {
|
|||
for _, member := range tc.modifiedMembers {
|
||||
_, err := modified.AddMember(member, []protobuf.CommunityMember_Roles{})
|
||||
s.Require().NoError(err)
|
||||
_, err = modified.AddMemberToChat(chatID, member, []protobuf.CommunityMember_Roles{})
|
||||
_, err = modified.AddMemberToChat(channelID, member, []protobuf.CommunityMember_Roles{})
|
||||
s.Require().NoError(err)
|
||||
}
|
||||
|
||||
actions := EvaluateCommunityEncryptionKeyActions(origin, modified)
|
||||
channelAction, ok := actions.ChannelKeysActions[chatID]
|
||||
channelAction, ok := actions.ChannelKeysActions[channelID]
|
||||
s.Require().True(ok)
|
||||
s.Require().Equal(tc.expectedAction.ActionType, channelAction.ActionType)
|
||||
s.Require().Len(tc.expectedAction.Members, len(channelAction.Members))
|
||||
|
@ -783,8 +785,10 @@ func (s *CommunityEncryptionKeyActionSuite) TestChannelLevelKeyActions() {
|
|||
func (s *CommunityEncryptionKeyActionSuite) TestNilOrigin() {
|
||||
newCommunity := createTestCommunity(s.identity)
|
||||
|
||||
chatID := "0x1234"
|
||||
_, err := newCommunity.CreateChat(chatID, &protobuf.CommunityChat{
|
||||
channelID := "0x1234"
|
||||
chatID := types.EncodeHex(crypto.CompressPubkey(&s.identity.PublicKey)) + channelID
|
||||
|
||||
_, err := newCommunity.CreateChat(channelID, &protobuf.CommunityChat{
|
||||
Members: map[string]*protobuf.CommunityMember{},
|
||||
Permissions: &protobuf.CommunityPermissions{Access: protobuf.CommunityPermissions_NO_MEMBERSHIP},
|
||||
Identity: &protobuf.ChatIdentity{},
|
||||
|
@ -813,6 +817,6 @@ func (s *CommunityEncryptionKeyActionSuite) TestNilOrigin() {
|
|||
actions := EvaluateCommunityEncryptionKeyActions(nil, newCommunity)
|
||||
s.Require().Equal(actions.CommunityKeyAction.ActionType, EncryptionKeyAdd)
|
||||
s.Require().Len(actions.ChannelKeysActions, 1)
|
||||
s.Require().NotNil(actions.ChannelKeysActions[chatID])
|
||||
s.Require().Equal(actions.ChannelKeysActions[chatID].ActionType, EncryptionKeyAdd)
|
||||
s.Require().NotNil(actions.ChannelKeysActions[channelID])
|
||||
s.Require().Equal(actions.ChannelKeysActions[channelID].ActionType, EncryptionKeyAdd)
|
||||
}
|
||||
|
|
|
@ -62,29 +62,29 @@ var (
|
|||
)
|
||||
|
||||
type Manager struct {
|
||||
persistence *Persistence
|
||||
encryptor *encryption.Protocol
|
||||
ensSubscription chan []*ens.VerificationRecord
|
||||
subscriptions []chan *Subscription
|
||||
ensVerifier *ens.Verifier
|
||||
identity *ecdsa.PrivateKey
|
||||
accountsManager account.Manager
|
||||
tokenManager TokenManager
|
||||
collectiblesManager CollectiblesManager
|
||||
logger *zap.Logger
|
||||
stdoutLogger *zap.Logger
|
||||
transport *transport.Transport
|
||||
quit chan struct{}
|
||||
torrentConfig *params.TorrentConfig
|
||||
torrentClient *torrent.Client
|
||||
walletConfig *params.WalletConfig
|
||||
historyArchiveTasksWaitGroup sync.WaitGroup
|
||||
historyArchiveTasks sync.Map // stores `chan struct{}`
|
||||
periodicMemberPermissionsTasks sync.Map // stores `chan struct{}`
|
||||
torrentTasks map[string]metainfo.Hash
|
||||
historyArchiveDownloadTasks map[string]*HistoryArchiveDownloadTask
|
||||
stopped bool
|
||||
RekeyInterval time.Duration
|
||||
persistence *Persistence
|
||||
encryptor *encryption.Protocol
|
||||
ensSubscription chan []*ens.VerificationRecord
|
||||
subscriptions []chan *Subscription
|
||||
ensVerifier *ens.Verifier
|
||||
identity *ecdsa.PrivateKey
|
||||
accountsManager account.Manager
|
||||
tokenManager TokenManager
|
||||
collectiblesManager CollectiblesManager
|
||||
logger *zap.Logger
|
||||
stdoutLogger *zap.Logger
|
||||
transport *transport.Transport
|
||||
quit chan struct{}
|
||||
torrentConfig *params.TorrentConfig
|
||||
torrentClient *torrent.Client
|
||||
walletConfig *params.WalletConfig
|
||||
historyArchiveTasksWaitGroup sync.WaitGroup
|
||||
historyArchiveTasks sync.Map // stores `chan struct{}`
|
||||
periodicMembersReevaluationTasks sync.Map // stores `chan struct{}`
|
||||
torrentTasks map[string]metainfo.Hash
|
||||
historyArchiveDownloadTasks map[string]*HistoryArchiveDownloadTask
|
||||
stopped bool
|
||||
RekeyInterval time.Duration
|
||||
}
|
||||
|
||||
type HistoryArchiveDownloadTask struct {
|
||||
|
@ -615,16 +615,12 @@ func (m *Manager) EditCommunityTokenPermission(request *requests.EditCommunityTo
|
|||
return community, changes, nil
|
||||
}
|
||||
|
||||
func (m *Manager) CheckMemberPermissions(community *Community, removeAdmins bool) error {
|
||||
func (m *Manager) ReevaluateMembers(community *Community, removeAdmins bool) error {
|
||||
becomeMemberPermissions := community.TokenPermissionsByType(protobuf.CommunityTokenPermission_BECOME_MEMBER)
|
||||
becomeAdminPermissions := community.TokenPermissionsByType(protobuf.CommunityTokenPermission_BECOME_ADMIN)
|
||||
|
||||
adminPermissions := len(becomeAdminPermissions) > 0
|
||||
memberPermissions := len(becomeMemberPermissions) > 0
|
||||
|
||||
if !adminPermissions && !memberPermissions && !removeAdmins {
|
||||
return nil
|
||||
}
|
||||
hasMemberPermissions := len(becomeMemberPermissions) > 0
|
||||
hasAdminPermissions := len(becomeAdminPermissions) > 0
|
||||
|
||||
for memberKey, member := range community.Members() {
|
||||
memberPubKey, err := common.HexToPubkey(memberKey)
|
||||
|
@ -641,7 +637,7 @@ func (m *Manager) CheckMemberPermissions(community *Community, removeAdmins bool
|
|||
|
||||
// Check if user was not treated as an admin without wallet in open community
|
||||
// or user treated as a member without wallet in closed community
|
||||
if (!memberHasWallet && isAdmin) || (memberPermissions && !memberHasWallet) {
|
||||
if !memberHasWallet && (hasMemberPermissions || isAdmin) {
|
||||
_, err = community.RemoveUserFromOrg(memberPubKey)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -653,7 +649,7 @@ func (m *Manager) CheckMemberPermissions(community *Community, removeAdmins bool
|
|||
|
||||
// Check if user is still an admin or can become an admin and do update of member role
|
||||
removeAdminRole := false
|
||||
if adminPermissions {
|
||||
if hasAdminPermissions {
|
||||
permissionResponse, err := m.checkPermissionToJoin(becomeAdminPermissions, accountsAndChainIDs, true)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -678,21 +674,67 @@ func (m *Manager) CheckMemberPermissions(community *Community, removeAdmins bool
|
|||
isAdmin = false
|
||||
}
|
||||
|
||||
// Skip further validation if user has admin permissions or we do not have member permissions
|
||||
if isAdmin || !memberPermissions {
|
||||
if isAdmin {
|
||||
// Make sure admin is added to every channel
|
||||
for channelID := range community.Chats() {
|
||||
if !community.IsMemberInChat(memberPubKey, channelID) {
|
||||
_, err = community.AddMemberToChat(channelID, memberPubKey, []protobuf.CommunityMember_Roles{protobuf.CommunityMember_ROLE_ADMIN})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
// Skip further validation if user has admin permissions
|
||||
continue
|
||||
}
|
||||
|
||||
permissionResponse, err := m.checkPermissionToJoin(becomeMemberPermissions, accountsAndChainIDs, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !permissionResponse.Satisfied {
|
||||
_, err = community.RemoveUserFromOrg(memberPubKey)
|
||||
if hasMemberPermissions {
|
||||
permissionResponse, err := m.checkPermissionToJoin(becomeMemberPermissions, accountsAndChainIDs, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !permissionResponse.Satisfied {
|
||||
_, err = community.RemoveUserFromOrg(memberPubKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Skip channels validation if user has been removed
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Validate channel permissions
|
||||
for channelID := range community.Chats() {
|
||||
chatID := community.IDString() + channelID
|
||||
|
||||
viewOnlyPermissions := community.ChannelTokenPermissionsByType(chatID, protobuf.CommunityTokenPermission_CAN_VIEW_CHANNEL)
|
||||
viewAndPostPermissions := community.ChannelTokenPermissionsByType(chatID, protobuf.CommunityTokenPermission_CAN_VIEW_AND_POST_CHANNEL)
|
||||
|
||||
if len(viewOnlyPermissions) == 0 && len(viewAndPostPermissions) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
response, err := m.checkChannelPermissions(viewOnlyPermissions, viewAndPostPermissions, accountsAndChainIDs, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
isMemberAlreadyInChannel := community.IsMemberInChat(memberPubKey, channelID)
|
||||
|
||||
if response.ViewOnlyPermissions.Satisfied || response.ViewAndPostPermissions.Satisfied {
|
||||
if !isMemberAlreadyInChannel {
|
||||
_, err := community.AddMemberToChat(channelID, memberPubKey, []protobuf.CommunityMember_Roles{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else if isMemberAlreadyInChannel {
|
||||
_, err := community.RemoveUserFromChat(memberPubKey, channelID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -704,21 +746,13 @@ func (m *Manager) CheckMemberPermissions(community *Community, removeAdmins bool
|
|||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) CheckIfStopCheckingPermissionsPeriodically(community *Community) {
|
||||
if cancel, exists := m.periodicMemberPermissionsTasks.Load(community.IDString()); exists &&
|
||||
len(community.TokenPermissions()) == 0 {
|
||||
close(cancel.(chan struct{})) // Need to cast to the chan
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) CheckMemberPermissionsPeriodically(communityID types.HexBytes) {
|
||||
|
||||
if _, exists := m.periodicMemberPermissionsTasks.Load(communityID.String()); exists {
|
||||
func (m *Manager) ReevaluateMembersPeriodically(communityID types.HexBytes) {
|
||||
if _, exists := m.periodicMembersReevaluationTasks.Load(communityID.String()); exists {
|
||||
return
|
||||
}
|
||||
|
||||
cancel := make(chan struct{})
|
||||
m.periodicMemberPermissionsTasks.Store(communityID.String(), cancel)
|
||||
m.periodicMembersReevaluationTasks.Store(communityID.String(), cancel)
|
||||
|
||||
ticker := time.NewTicker(memberPermissionsCheckInterval)
|
||||
defer ticker.Stop()
|
||||
|
@ -729,15 +763,15 @@ func (m *Manager) CheckMemberPermissionsPeriodically(communityID types.HexBytes)
|
|||
community, err := m.GetByID(communityID)
|
||||
if err != nil {
|
||||
m.logger.Debug("can't validate member permissions, community was not found", zap.Error(err))
|
||||
m.periodicMemberPermissionsTasks.Delete(communityID.String())
|
||||
m.periodicMembersReevaluationTasks.Delete(communityID.String())
|
||||
}
|
||||
|
||||
err = m.CheckMemberPermissions(community, true)
|
||||
err = m.ReevaluateMembers(community, true)
|
||||
if err != nil {
|
||||
m.logger.Debug("failed to check member permissions", zap.Error(err))
|
||||
}
|
||||
case <-cancel:
|
||||
m.periodicMemberPermissionsTasks.Delete(communityID.String())
|
||||
m.periodicMembersReevaluationTasks.Delete(communityID.String())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -1525,6 +1559,32 @@ func (m *Manager) accountsSatisfyPermissionsToJoin(community *Community, account
|
|||
return true, false, nil
|
||||
}
|
||||
|
||||
func (m *Manager) accountsSatisfyPermissionsToJoinChannels(community *Community, accounts []*protobuf.RevealedAccount) (map[string]*protobuf.CommunityChat, error) {
|
||||
result := make(map[string]*protobuf.CommunityChat)
|
||||
|
||||
accountsAndChainIDs := revealedAccountsToAccountsAndChainIDsCombination(accounts)
|
||||
|
||||
for channelID, channel := range community.config.CommunityDescription.Chats {
|
||||
channelViewOnlyPermissions := community.ChannelTokenPermissionsByType(community.IDString()+channelID, protobuf.CommunityTokenPermission_CAN_VIEW_CHANNEL)
|
||||
channelViewAndPostPermissions := community.ChannelTokenPermissionsByType(community.IDString()+channelID, protobuf.CommunityTokenPermission_CAN_VIEW_AND_POST_CHANNEL)
|
||||
channelPermissions := append(channelViewOnlyPermissions, channelViewAndPostPermissions...)
|
||||
|
||||
if len(channelPermissions) > 0 {
|
||||
permissionResponse, err := m.checkPermissions(channelPermissions, accountsAndChainIDs, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if permissionResponse.Satisfied {
|
||||
result[channelID] = channel
|
||||
}
|
||||
} else {
|
||||
result[channelID] = channel
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (m *Manager) AcceptRequestToJoin(request *requests.AcceptRequestToJoinCommunity) (*Community, error) {
|
||||
dbRequest, err := m.persistence.GetRequestToJoin(request.ID)
|
||||
if err != nil {
|
||||
|
@ -1565,6 +1625,18 @@ func (m *Manager) AcceptRequestToJoin(request *requests.AcceptRequestToJoinCommu
|
|||
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
|
||||
}
|
||||
|
@ -2798,8 +2870,17 @@ func (m *Manager) IsEncrypted(communityID string) (bool, error) {
|
|||
}
|
||||
|
||||
return community.Encrypted(), nil
|
||||
|
||||
}
|
||||
|
||||
func (m *Manager) IsChannelEncrypted(communityID string, chatID string) (bool, error) {
|
||||
community, err := m.GetByIDString(communityID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return community.ChannelHasTokenPermissions(chatID), nil
|
||||
}
|
||||
|
||||
func (m *Manager) ShouldHandleSyncCommunity(community *protobuf.SyncCommunity) (bool, error) {
|
||||
return m.persistence.ShouldHandleSyncCommunity(community)
|
||||
}
|
||||
|
|
|
@ -30,9 +30,9 @@ func (ckd *CommunitiesKeyDistributorImpl) Distribute(community *communities.Comm
|
|||
return err
|
||||
}
|
||||
|
||||
for chatID := range keyActions.ChannelKeysActions {
|
||||
keyAction := keyActions.ChannelKeysActions[chatID]
|
||||
err := ckd.distributeKey(community.ID(), []byte(chatID), &keyAction)
|
||||
for channelID := range keyActions.ChannelKeysActions {
|
||||
keyAction := keyActions.ChannelKeysActions[channelID]
|
||||
err := ckd.distributeKey(community.ID(), []byte(community.IDString()+channelID), &keyAction)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -110,6 +110,7 @@ func (ckd *CommunitiesKeyDistributorImpl) sendKeyExchangeMessage(communityID, ha
|
|||
CommunityKeyExMsgType: msgType,
|
||||
Recipients: pubkeys,
|
||||
MessageType: protobuf.ApplicationMetadataMessage_CHAT_MESSAGE,
|
||||
HashRatchetGroupID: hashRatchetGroupID,
|
||||
}
|
||||
_, err := ckd.sender.SendCommunityMessage(context.Background(), rawMessage)
|
||||
|
||||
|
|
|
@ -767,7 +767,157 @@ func (s *MessengerCommunitiesTokenPermissionsSuite) TestBecomeMemberPermissions(
|
|||
s.Require().NoError(err)
|
||||
|
||||
// send message to channel
|
||||
msg = s.sendChatMessage(s.owner, chat.ID, "hello on encrypted community")
|
||||
msg = s.sendChatMessage(s.owner, chat.ID, "hello on encrypted community 2")
|
||||
|
||||
// bob can read the message
|
||||
response, err = WaitOnMessengerResponse(
|
||||
s.bob,
|
||||
func(r *MessengerResponse) bool {
|
||||
for _, message := range r.messages {
|
||||
if message.Text == msg.Text {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
},
|
||||
"no messages",
|
||||
)
|
||||
s.Require().NoError(err)
|
||||
s.Require().Len(response.Messages(), 1)
|
||||
s.Require().Equal(msg.Text, response.Messages()[0].Text)
|
||||
}
|
||||
|
||||
func (s *MessengerCommunitiesTokenPermissionsSuite) TestViewChannelPermissions() {
|
||||
community, chat := s.createCommunity()
|
||||
|
||||
// bob joins the community
|
||||
s.advertiseCommunityTo(community, s.bob)
|
||||
s.joinCommunity(community, s.bob, bobPassword, []string{})
|
||||
|
||||
// send message to the channel
|
||||
msg := s.sendChatMessage(s.owner, chat.ID, "hello on open community")
|
||||
|
||||
// bob can read the message
|
||||
response, err := WaitOnMessengerResponse(
|
||||
s.bob,
|
||||
func(r *MessengerResponse) bool {
|
||||
for _, message := range r.messages {
|
||||
if message.Text == msg.Text {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
},
|
||||
"no messages",
|
||||
)
|
||||
s.Require().NoError(err)
|
||||
s.Require().Len(response.Messages(), 1)
|
||||
s.Require().Equal(msg.Text, response.Messages()[0].Text)
|
||||
|
||||
// setup view channel permission
|
||||
channelPermissionRequest := requests.CreateCommunityTokenPermission{
|
||||
CommunityID: community.ID(),
|
||||
Type: protobuf.CommunityTokenPermission_CAN_VIEW_CHANNEL,
|
||||
TokenCriteria: []*protobuf.TokenCriteria{
|
||||
&protobuf.TokenCriteria{
|
||||
Type: protobuf.CommunityTokenType_ERC20,
|
||||
ContractAddresses: map[uint64]string{testChainID1: "0x123"},
|
||||
Symbol: "TEST",
|
||||
Amount: "100",
|
||||
Decimals: uint64(18),
|
||||
},
|
||||
},
|
||||
ChatIds: []string{chat.ID},
|
||||
}
|
||||
|
||||
waitOnBobToBeKickedFromChannel := s.waitOnCommunitiesEvent(s.owner, func(sub *communities.Subscription) bool {
|
||||
for channelID, channel := range sub.Community.Chats() {
|
||||
if channelID == chat.CommunityChatID() && len(channel.Members) == 1 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
waitOnChannelToBeRekeyedOnceBobIsKicked := s.waitOnKeyDistribution(func(sub *CommunityAndKeyActions) bool {
|
||||
for channelID, action := range sub.keyActions.ChannelKeysActions {
|
||||
if channelID == chat.CommunityChatID() && action.ActionType == communities.EncryptionKeyRekey {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
response, err = s.owner.CreateCommunityTokenPermission(&channelPermissionRequest)
|
||||
s.Require().NoError(err)
|
||||
s.Require().Len(response.Communities(), 1)
|
||||
s.Require().True(s.owner.communitiesManager.IsChannelEncrypted(community.IDString(), chat.ID))
|
||||
|
||||
err = <-waitOnBobToBeKickedFromChannel
|
||||
s.Require().NoError(err)
|
||||
|
||||
err = <-waitOnChannelToBeRekeyedOnceBobIsKicked
|
||||
s.Require().NoError(err)
|
||||
|
||||
// send message to the channel
|
||||
msg = s.sendChatMessage(s.owner, chat.ID, "hello on closed channel")
|
||||
|
||||
// bob can't read the message
|
||||
_, err = WaitOnMessengerResponse(
|
||||
s.bob,
|
||||
func(r *MessengerResponse) bool {
|
||||
for _, message := range r.messages {
|
||||
if message.Text == msg.Text {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
},
|
||||
"no messages",
|
||||
)
|
||||
s.Require().Error(err)
|
||||
s.Require().ErrorContains(err, "no messages")
|
||||
|
||||
// make bob satisfy channel criteria
|
||||
s.makeAddressSatisfyTheCriteria(testChainID1, bobAddress, channelPermissionRequest.TokenCriteria[0])
|
||||
|
||||
waitOnChannelKeyToBeDistributedToBob := s.waitOnKeyDistribution(func(sub *CommunityAndKeyActions) bool {
|
||||
for channelID, action := range sub.keyActions.ChannelKeysActions {
|
||||
if channelID == chat.CommunityChatID() && action.ActionType == communities.EncryptionKeySendToMembers {
|
||||
for memberPubKey := range action.Members {
|
||||
if memberPubKey == common.PubkeyToHex(&s.bob.identity.PublicKey) {
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
// force owner to reevaluate channel members
|
||||
// in production it will happen automatically, by periodic check
|
||||
community, err = s.owner.communitiesManager.GetByID(community.ID())
|
||||
s.Require().NoError(err)
|
||||
err = s.owner.communitiesManager.ReevaluateMembers(community, true)
|
||||
s.Require().NoError(err)
|
||||
|
||||
err = <-waitOnChannelKeyToBeDistributedToBob
|
||||
s.Require().NoError(err)
|
||||
|
||||
// ensure key is delivered to bob before message is sent
|
||||
// FIXME: this step shouldn't be necessary as we store hash ratchet messages
|
||||
// for later, to decrypt them when the key arrives.
|
||||
// for some reason, without it, the test is flaky
|
||||
_, _ = WaitOnMessengerResponse(
|
||||
s.bob,
|
||||
func(r *MessengerResponse) bool {
|
||||
return false
|
||||
},
|
||||
"",
|
||||
)
|
||||
|
||||
// send message to the channel
|
||||
msg = s.sendChatMessage(s.owner, chat.ID, "hello on closed channel 2")
|
||||
|
||||
// bob can read the message
|
||||
response, err = WaitOnMessengerResponse(
|
||||
|
|
|
@ -822,7 +822,7 @@ func (m *Messenger) Start() (*MessengerResponse, error) {
|
|||
|
||||
for _, c := range adminCommunities {
|
||||
if c.Joined() && c.HasTokenPermissions() {
|
||||
go m.communitiesManager.CheckMemberPermissionsPeriodically(c.ID())
|
||||
go m.communitiesManager.ReevaluateMembersPeriodically(c.ID())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2063,21 +2063,36 @@ func (m *Messenger) dispatchMessage(ctx context.Context, rawMessage common.RawMe
|
|||
}
|
||||
|
||||
logger.Debug("sending community chat message", zap.String("chatName", chat.Name))
|
||||
isEncrypted, err := m.communitiesManager.IsEncrypted(chat.CommunityID)
|
||||
isCommunityEncrypted, err := m.communitiesManager.IsEncrypted(chat.CommunityID)
|
||||
if err != nil {
|
||||
return rawMessage, err
|
||||
}
|
||||
isChannelEncrypted, err := m.communitiesManager.IsChannelEncrypted(chat.CommunityID, chat.ID)
|
||||
if err != nil {
|
||||
return rawMessage, err
|
||||
}
|
||||
isEncrypted := isCommunityEncrypted || isChannelEncrypted
|
||||
if !isEncrypted {
|
||||
id, err = m.sender.SendPublic(ctx, chat.ID, rawMessage)
|
||||
if err != nil {
|
||||
return rawMessage, err
|
||||
}
|
||||
} else {
|
||||
rawMessage.CommunityID, err = types.DecodeHex(chat.CommunityID)
|
||||
|
||||
if err == nil {
|
||||
id, err = m.sender.SendCommunityMessage(ctx, rawMessage)
|
||||
if err != nil {
|
||||
return rawMessage, err
|
||||
}
|
||||
|
||||
if isChannelEncrypted {
|
||||
rawMessage.HashRatchetGroupID = []byte(chat.ID)
|
||||
} else {
|
||||
rawMessage.HashRatchetGroupID = rawMessage.CommunityID
|
||||
}
|
||||
|
||||
id, err = m.sender.SendCommunityMessage(ctx, rawMessage)
|
||||
if err != nil {
|
||||
return rawMessage, err
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return rawMessage, err
|
||||
}
|
||||
case ChatTypePrivateGroupChat:
|
||||
logger.Debug("sending group message", zap.String("chatName", chat.Name))
|
||||
|
|
|
@ -1697,12 +1697,12 @@ func (m *Messenger) CreateCommunityTokenPermission(request *requests.CreateCommu
|
|||
if community.IsControlNode() {
|
||||
// check existing member permission once, then check periodically
|
||||
go func() {
|
||||
err := m.communitiesManager.CheckMemberPermissions(community, true)
|
||||
err := m.communitiesManager.ReevaluateMembers(community, true)
|
||||
if err != nil {
|
||||
m.logger.Debug("failed to check member permissions", zap.Error(err))
|
||||
}
|
||||
|
||||
m.communitiesManager.CheckMemberPermissionsPeriodically(community.ID())
|
||||
m.communitiesManager.ReevaluateMembersPeriodically(community.ID())
|
||||
}()
|
||||
}
|
||||
|
||||
|
@ -1729,7 +1729,7 @@ func (m *Messenger) EditCommunityTokenPermission(request *requests.EditCommunity
|
|||
// We do this in a separate routine to not block this function
|
||||
if community.IsControlNode() {
|
||||
go func() {
|
||||
err := m.communitiesManager.CheckMemberPermissions(community, true)
|
||||
err := m.communitiesManager.ReevaluateMembers(community, true)
|
||||
if err != nil {
|
||||
m.logger.Debug("failed to check member permissions", zap.Error(err))
|
||||
}
|
||||
|
@ -1760,14 +1760,10 @@ func (m *Messenger) DeleteCommunityTokenPermission(request *requests.DeleteCommu
|
|||
becomeAdminPermissions := community.TokenPermissionsByType(protobuf.CommunityTokenPermission_BECOME_ADMIN)
|
||||
|
||||
// Make sure that we remove admins roles if we remove admin permissions
|
||||
err = m.communitiesManager.CheckMemberPermissions(community, len(becomeAdminPermissions) == 0)
|
||||
err = m.communitiesManager.ReevaluateMembers(community, len(becomeAdminPermissions) == 0)
|
||||
if err != nil {
|
||||
m.logger.Debug("failed to check member permissions", zap.Error(err))
|
||||
}
|
||||
|
||||
// Check if there's still permissions we need to track,
|
||||
// if not we can stop checking token criteria on-chain
|
||||
m.communitiesManager.CheckIfStopCheckingPermissionsPeriodically(community)
|
||||
}()
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue