feat: Add an expiration and periodical publishing for community grants (#5024)
* feat_: add periodical publishing for community grants feat_: Validate grant when receiving it feat_: add expiration for grants feat_: add test for grants expiration fix_: move grants test to profile showcase, fix a few bugs * feat_: use one group mesage to update grants * chore_: review fixes
This commit is contained in:
parent
3de2756660
commit
6da423fc71
|
@ -31,6 +31,9 @@ import (
|
|||
|
||||
const signatureLength = 65
|
||||
|
||||
// GrantExpirationTime interval of 7 days
|
||||
var GrantExpirationTime = 168 * time.Hour
|
||||
|
||||
type Config struct {
|
||||
PrivateKey *ecdsa.PrivateKey
|
||||
ControlNode *ecdsa.PublicKey
|
||||
|
@ -1881,6 +1884,9 @@ func (o *Community) VerifyGrantSignature(data []byte) (*protobuf.Grant, error) {
|
|||
if !bytes.Equal(grant.CommunityId, o.ID()) {
|
||||
return nil, ErrInvalidGrant
|
||||
}
|
||||
if grant.Expires < uint64(time.Now().UnixMilli()) {
|
||||
return nil, ErrGrantExpired
|
||||
}
|
||||
|
||||
extractedPublicKey, err := crypto.SigToPub(crypto.Keccak256(payload), signature)
|
||||
if err != nil {
|
||||
|
@ -1973,6 +1979,7 @@ func (o *Community) buildGrant(key *ecdsa.PublicKey, chatID string) ([]byte, err
|
|||
MemberId: crypto.CompressPubkey(key),
|
||||
ChatId: chatID,
|
||||
Clock: o.config.CommunityDescription.Clock,
|
||||
Expires: uint64(time.Now().Add(GrantExpirationTime).UnixMilli()),
|
||||
}
|
||||
marshaledGrant, err := proto.Marshal(grant)
|
||||
if err != nil {
|
||||
|
|
|
@ -27,6 +27,8 @@ var ErrNotAdmin = errors.New("no admin privileges for this community")
|
|||
var ErrNotOwner = errors.New("no owner privileges for this community")
|
||||
var ErrNotControlNode = errors.New("not a control node")
|
||||
var ErrInvalidGrant = errors.New("invalid grant")
|
||||
var ErrGrantExpired = errors.New("expired grant")
|
||||
var ErrGrantOlder = errors.New("received grant older than the current one")
|
||||
var ErrNotAuthorized = errors.New("not authorized")
|
||||
var ErrAlreadyMember = errors.New("already a member")
|
||||
var ErrAlreadyJoined = errors.New("already joined")
|
||||
|
@ -46,3 +48,4 @@ var ErrNoRevealedAccountsSignature = errors.New("revealed accounts without the s
|
|||
var ErrNoFreeSpaceForHistoryArchives = errors.New("history archive: No free space for downloading history archives")
|
||||
var ErrPermissionToJoinNotSatisfied = errors.New("permission to join not satisfied")
|
||||
var ErrBannedMemberNotFound = errors.New("banned member not found")
|
||||
var ErrGrantMemberPublicKeyIsDifferent = errors.New("grant member public key is different")
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package communities
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"database/sql"
|
||||
|
@ -862,7 +863,7 @@ func (m *Manager) CreateCommunity(request *requests.CreateCommunity, publish boo
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = m.persistence.SaveCommunityGrant(community.IDString(), grant, description.Clock)
|
||||
err = m.persistence.SaveCommunityGrant(community.IDString(), grant, uint64(time.Now().UnixMilli()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -1415,7 +1416,7 @@ func (m *Manager) ImportCommunity(key *ecdsa.PrivateKey, clock uint64) (*Communi
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = m.persistence.SaveCommunityGrant(community.IDString(), grant, community.Description().Clock)
|
||||
err = m.persistence.SaveCommunityGrant(community.IDString(), grant, uint64(time.Now().UnixMilli()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -2979,8 +2980,11 @@ func (m *Manager) HandleCommunityRequestToJoinResponse(signer *ecdsa.PublicKey,
|
|||
return nil, err
|
||||
}
|
||||
|
||||
if err = m.handleCommunityGrant(community.ID(), request.Grant, request.Clock); err != nil {
|
||||
return nil, err
|
||||
if community.Encrypted() && len(request.Grant) > 0 {
|
||||
_, err = m.HandleCommunityGrant(community, request.Grant, request.Clock)
|
||||
if err != nil && err != ErrGrantOlder && err != ErrGrantExpired {
|
||||
m.logger.Error("Error handling a community grant", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
err = m.persistence.SaveCommunity(community)
|
||||
|
@ -4870,17 +4874,26 @@ func (m *Manager) handleCommunityTokensMetadata(community *Community) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) handleCommunityGrant(communityID types.HexBytes, grant []byte, clock uint64) error {
|
||||
_, oldClock, err := m.persistence.GetCommunityGrant(communityID.String())
|
||||
func (m *Manager) HandleCommunityGrant(community *Community, grant []byte, clock uint64) (uint64, error) {
|
||||
_, oldClock, err := m.GetCommunityGrant(community.IDString())
|
||||
if err != nil {
|
||||
return err
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if oldClock >= clock {
|
||||
return nil
|
||||
return 0, ErrGrantOlder
|
||||
}
|
||||
|
||||
return m.persistence.SaveCommunityGrant(communityID.String(), grant, clock)
|
||||
verifiedGrant, err := community.VerifyGrantSignature(grant)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if !bytes.Equal(verifiedGrant.MemberId, crypto.CompressPubkey(&m.identity.PublicKey)) {
|
||||
return 0, ErrGrantMemberPublicKeyIsDifferent
|
||||
}
|
||||
|
||||
return clock - oldClock, m.persistence.SaveCommunityGrant(community.IDString(), grant, clock)
|
||||
}
|
||||
|
||||
func (m *Manager) FetchCommunityToken(community *Community, tokenMetadata *protobuf.CommunityTokenMetadata, chainID uint64, contractAddress string) (*community_token.CommunityToken, error) {
|
||||
|
|
|
@ -3,6 +3,7 @@ package encryption
|
|||
import (
|
||||
"bytes"
|
||||
"crypto/ecdsa"
|
||||
"crypto/rand"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
|
@ -422,6 +423,43 @@ func (p *Protocol) BuildHashRatchetMessage(groupID []byte, payload []byte) (*Pro
|
|||
return spec, nil
|
||||
}
|
||||
|
||||
func (p *Protocol) EncryptCommunityGrants(privateKey *ecdsa.PrivateKey, recipientGrants map[*ecdsa.PublicKey][]byte) (map[uint32][]byte, error) {
|
||||
grants := make(map[uint32][]byte)
|
||||
|
||||
for recipientKey, grant := range recipientGrants {
|
||||
sharedKey, err := generateSharedKey(privateKey, recipientKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
encryptedGrant, err := encrypt(grant, sharedKey, rand.Reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
kBytes := publicKeyMostRelevantBytes(recipientKey)
|
||||
grants[kBytes] = encryptedGrant
|
||||
}
|
||||
|
||||
return grants, nil
|
||||
}
|
||||
|
||||
func (p *Protocol) DecryptCommunityGrant(myIdentityKey *ecdsa.PrivateKey, senderKey *ecdsa.PublicKey, grants map[uint32][]byte) ([]byte, error) {
|
||||
kBytes := publicKeyMostRelevantBytes(&myIdentityKey.PublicKey)
|
||||
|
||||
ecryptedGrant, ok := grants[kBytes]
|
||||
if !ok {
|
||||
return nil, errors.New("can't find related grant in the map")
|
||||
}
|
||||
|
||||
sharedKey, err := generateSharedKey(myIdentityKey, senderKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return decrypt(ecryptedGrant, sharedKey)
|
||||
}
|
||||
|
||||
func (p *Protocol) GetKeyExMessageSpecs(groupID []byte, identity *ecdsa.PrivateKey, recipients []*ecdsa.PublicKey, forceRekey bool) ([]*ProtocolMessageSpec, error) {
|
||||
var ratchets []*HashRatchetKeyCompatibility
|
||||
var err error
|
||||
|
|
|
@ -813,6 +813,7 @@ func (m *Messenger) Start() (*MessengerResponse, error) {
|
|||
m.handleCommunitiesSubscription(m.communitiesManager.Subscribe())
|
||||
m.handleCommunitiesHistoryArchivesSubscription(m.communitiesManager.Subscribe())
|
||||
m.updateCommunitiesActiveMembersPeriodically()
|
||||
m.schedulePublishGrantsForControlledCommunities()
|
||||
m.handleENSVerificationSubscription(ensSubscription)
|
||||
m.watchConnectionChange()
|
||||
m.watchChatsAndCommunitiesToUnmute()
|
||||
|
|
|
@ -51,6 +51,12 @@ var messageArchiveInterval = 7 * 24 * time.Hour
|
|||
// 1 day interval
|
||||
var updateActiveMembersInterval = 24 * time.Hour
|
||||
|
||||
// 1 day interval
|
||||
var grantUpdateInterval = 24 * time.Hour
|
||||
|
||||
// 4 hours interval
|
||||
var grantInvokesProfileDispatchInterval = 4 * time.Hour
|
||||
|
||||
const discordTimestampLayout = time.RFC3339
|
||||
|
||||
const (
|
||||
|
@ -481,8 +487,8 @@ func (m *Messenger) handleCommunitiesSubscription(c chan *communities.Subscripti
|
|||
lastPublished = time.Now().Unix()
|
||||
|
||||
case <-m.quit:
|
||||
ticker.Stop()
|
||||
return
|
||||
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
@ -526,6 +532,134 @@ func (m *Messenger) updateCommunitiesActiveMembersPeriodically() {
|
|||
}
|
||||
|
||||
case <-m.quit:
|
||||
ticker.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (m *Messenger) HandleCommunityUpdateGrant(state *ReceivedMessageState, message *protobuf.CommunityUpdateGrant, statusMessage *v1protocol.StatusMessage) error {
|
||||
community, err := m.communitiesManager.GetByID(message.CommunityId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
grant, err := m.encryptor.DecryptCommunityGrant(m.identity, state.CurrentMessageState.PublicKey, message.Grants)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return m.handleCommunityGrant(community, grant, message.Timestamp)
|
||||
}
|
||||
|
||||
func (m *Messenger) handleCommunityGrant(community *communities.Community, grant []byte, clock uint64) error {
|
||||
difference, err := m.communitiesManager.HandleCommunityGrant(community, grant, clock)
|
||||
if err == communities.ErrGrantOlder || err == communities.ErrGrantExpired {
|
||||
// Don't log an error for these cases
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// if grant is significantly newer than the one we have, we should check the profile showcase
|
||||
if time.Duration(difference)*time.Millisecond > grantInvokesProfileDispatchInterval {
|
||||
err = m.UpdateProfileShowcaseCommunity(community)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Messenger) publishGroupGrantMessage(community *communities.Community, timestamp uint64, recipientGrants map[*ecdsa.PublicKey][]byte) error {
|
||||
grants, err := m.encryptor.EncryptCommunityGrants(community.PrivateKey(), recipientGrants)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
message := &protobuf.CommunityUpdateGrant{
|
||||
Timestamp: timestamp,
|
||||
CommunityId: community.ID(),
|
||||
Grants: grants,
|
||||
}
|
||||
|
||||
payload, err := proto.Marshal(message)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rawMessage := common.RawMessage{
|
||||
Payload: payload,
|
||||
Sender: community.PrivateKey(),
|
||||
SkipEncryptionLayer: true,
|
||||
MessageType: protobuf.ApplicationMetadataMessage_COMMUNITY_UPDATE_GRANT,
|
||||
PubsubTopic: community.PubsubTopic(),
|
||||
}
|
||||
|
||||
_, err = m.sender.SendPublic(context.Background(), community.IDString(), rawMessage)
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *Messenger) updateGrantsForControlledCommunities() {
|
||||
controlledCommunities, err := m.communitiesManager.Controlled()
|
||||
if err != nil {
|
||||
m.logger.Error("failed fetch controlled communities for grants update", zap.Error(err))
|
||||
}
|
||||
|
||||
for _, community := range controlledCommunities {
|
||||
// Skip unencrypted communities
|
||||
if !community.Encrypted() {
|
||||
continue
|
||||
}
|
||||
|
||||
memberGrants := map[*ecdsa.PublicKey][]byte{}
|
||||
for memberKey := range community.Members() {
|
||||
if memberKey == m.IdentityPublicKeyString() {
|
||||
grant, err := community.BuildGrant(m.IdentityPublicKey(), "")
|
||||
if err != nil {
|
||||
m.logger.Error("can't build own grant for controlled community", zap.Error(err))
|
||||
}
|
||||
|
||||
err = m.handleCommunityGrant(community, grant, uint64(time.Now().UnixMilli()))
|
||||
if err != nil {
|
||||
m.logger.Error("error handling grant for controlled community", zap.Error(err))
|
||||
}
|
||||
} else {
|
||||
memberPubKey, err := common.HexToPubkey(memberKey)
|
||||
if err != nil {
|
||||
m.logger.Error("Pubkey decode ", zap.Error(err))
|
||||
}
|
||||
|
||||
grant, err := community.BuildGrant(memberPubKey, "")
|
||||
if err != nil {
|
||||
m.logger.Error("can't build member's grant for controlled community", zap.Error(err))
|
||||
}
|
||||
|
||||
memberGrants[memberPubKey] = grant
|
||||
}
|
||||
}
|
||||
err = m.publishGroupGrantMessage(community, uint64(time.Now().UnixMilli()), memberGrants)
|
||||
if err != nil {
|
||||
m.logger.Error("failed to update grant for community members", zap.Error(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Messenger) schedulePublishGrantsForControlledCommunities() {
|
||||
// Send once immediately
|
||||
m.updateGrantsForControlledCommunities()
|
||||
|
||||
ticker := time.NewTicker(grantUpdateInterval)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
m.updateGrantsForControlledCommunities()
|
||||
case <-m.quit:
|
||||
ticker.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
|
@ -253,6 +253,9 @@ func (m *Messenger) dispatchToHandler(messageState *ReceivedMessageState, protoB
|
|||
case protobuf.ApplicationMetadataMessage_DELETE_COMMUNITY_MEMBER_MESSAGES:
|
||||
return m.handleDeleteCommunityMemberMessagesProtobuf(messageState, protoBytes, msg, filter)
|
||||
|
||||
case protobuf.ApplicationMetadataMessage_COMMUNITY_UPDATE_GRANT:
|
||||
return m.handleCommunityUpdateGrantProtobuf(messageState, protoBytes, msg, filter)
|
||||
|
||||
default:
|
||||
m.logger.Info("protobuf type not found", zap.String("type", string(msg.ApplicationLayer.Type)))
|
||||
return errors.New("protobuf type not found")
|
||||
|
@ -1817,3 +1820,21 @@ func (m *Messenger) handleDeleteCommunityMemberMessagesProtobuf(messageState *Re
|
|||
}
|
||||
|
||||
|
||||
func (m *Messenger) handleCommunityUpdateGrantProtobuf(messageState *ReceivedMessageState, protoBytes []byte, msg *v1protocol.StatusMessage, filter transport.Filter) error {
|
||||
m.logger.Info("handling CommunityUpdateGrant")
|
||||
|
||||
|
||||
|
||||
p := &protobuf.CommunityUpdateGrant{}
|
||||
err := proto.Unmarshal(protoBytes, p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.outputToCSV(msg.TransportLayer.Message.Timestamp, msg.ApplicationLayer.ID, messageState.CurrentMessageState.Contact.ID, filter.ContentTopic, filter.ChatID, msg.ApplicationLayer.Type, p)
|
||||
|
||||
return m.HandleCommunityUpdateGrant(messageState, p, msg)
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -118,13 +118,13 @@ func (m *Messenger) validateCommunityMembershipEntry(
|
|||
}
|
||||
|
||||
if community.Encrypted() {
|
||||
grant, err := community.VerifyGrantSignature(entry.Grant)
|
||||
verifiedGrant, err := community.VerifyGrantSignature(entry.Grant)
|
||||
if err != nil {
|
||||
m.logger.Warn("failed to verify grant signature ", zap.Error(err))
|
||||
return identity.ProfileShowcaseMembershipStatusNotAMember, nil
|
||||
return identity.ProfileShowcaseMembershipStatusUnproven, nil
|
||||
}
|
||||
|
||||
if grant != nil && bytes.Equal(grant.MemberId, crypto.CompressPubkey(contactPubKey)) {
|
||||
if bytes.Equal(verifiedGrant.MemberId, crypto.CompressPubkey(contactPubKey)) {
|
||||
return identity.ProfileShowcaseMembershipStatusProvenMember, nil
|
||||
}
|
||||
// Show as not a member if membership can't be proven
|
||||
|
@ -432,8 +432,6 @@ func (m *Messenger) GetProfileShowcaseForContact(contactID string, validate bool
|
|||
return nil, err
|
||||
}
|
||||
|
||||
// TODO: validate collectibles & assets ownership, https://github.com/status-im/status-desktop/issues/14129
|
||||
|
||||
return profileShowcase, nil
|
||||
}
|
||||
|
||||
|
@ -713,6 +711,19 @@ func (m *Messenger) DeleteProfileShowcaseWalletAccount(account *accounts.Account
|
|||
return nil
|
||||
}
|
||||
|
||||
func (m *Messenger) UpdateProfileShowcaseCommunity(community *communities.Community) error {
|
||||
profileCommunity, err := m.persistence.GetProfileShowcaseCommunityPreference(community.IDString())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if profileCommunity == nil {
|
||||
// No corresponding profile entry, exit
|
||||
return nil
|
||||
}
|
||||
return m.DispatchProfileShowcase()
|
||||
}
|
||||
|
||||
func (m *Messenger) DeleteProfileShowcaseCommunity(community *communities.Community) error {
|
||||
deleted, err := m.persistence.DeleteProfileShowcaseCommunityPreference(community.IDString())
|
||||
if err != nil {
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"errors"
|
||||
"math/big"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
"go.uber.org/zap"
|
||||
|
@ -17,6 +18,7 @@ import (
|
|||
"github.com/status-im/status-go/eth-node/types"
|
||||
"github.com/status-im/status-go/multiaccounts/accounts"
|
||||
"github.com/status-im/status-go/protocol/common"
|
||||
"github.com/status-im/status-go/protocol/communities"
|
||||
"github.com/status-im/status-go/protocol/identity"
|
||||
"github.com/status-im/status-go/protocol/protobuf"
|
||||
"github.com/status-im/status-go/protocol/requests"
|
||||
|
@ -702,5 +704,139 @@ func (s *TestMessengerProfileShowcase) TestProfileShowcaseProofOfMembershipEncry
|
|||
s.Require().Equal(profileShowcase.Communities[0].CommunityID, aliceCommunity.IDString())
|
||||
s.Require().Equal(profileShowcase.Communities[0].MembershipStatus, identity.ProfileShowcaseMembershipStatusProvenMember)
|
||||
s.Require().Equal(profileShowcase.Communities[1].CommunityID, bobCommunity.IDString())
|
||||
s.Require().Equal(profileShowcase.Communities[1].MembershipStatus, identity.ProfileShowcaseMembershipStatusNotAMember)
|
||||
s.Require().Equal(profileShowcase.Communities[1].MembershipStatus, identity.ProfileShowcaseMembershipStatusUnproven)
|
||||
}
|
||||
|
||||
// The scenario tested is as follow:
|
||||
// 1) Owner creates an encrypted community
|
||||
// 2) Bob add Alice becommes a mutual contacts
|
||||
// 3) Alice and bob join the community
|
||||
// 4) Alice presents the community in her profile showcase
|
||||
// 5) Bob gets the community from Alice's profile showcase and validates community's membership with grant
|
||||
// 6) Wait until the grant expires, Bob should not be able to validate the membership anymore (commented step)
|
||||
// 7) Owner updates the grant
|
||||
// 8) Bob should be able to validate the membership again
|
||||
func (s *TestMessengerProfileShowcase) TestProfileShowcaseCommuniesDispatchOnGrantUpdate() {
|
||||
// NOTE: smaller timeouts can lead test to be flaky
|
||||
communities.GrantExpirationTime = 500 * time.Millisecond
|
||||
grantInvokesProfileDispatchInterval = 1 * time.Millisecond
|
||||
alice := s.m
|
||||
|
||||
// Set Display name to pass shouldPublishChatIdentity check
|
||||
profileKp := accounts.GetProfileKeypairForTest(true, false, false)
|
||||
profileKp.KeyUID = alice.account.KeyUID
|
||||
profileKp.Accounts[0].KeyUID = alice.account.KeyUID
|
||||
|
||||
err := alice.settings.SaveOrUpdateKeypair(profileKp)
|
||||
s.Require().NoError(err)
|
||||
|
||||
err = alice.SetDisplayName("Alice")
|
||||
s.Require().NoError(err)
|
||||
|
||||
// 1) Owner creates an encrypted community
|
||||
owner := s.newMessengerForProfileShowcase()
|
||||
defer TearDownMessenger(&s.Suite, owner)
|
||||
|
||||
owner.communitiesManager.PermissionChecker = &testPermissionChecker{}
|
||||
|
||||
community, _ := createEncryptedCommunity(&s.Suite, owner)
|
||||
s.Require().True(community.Encrypted())
|
||||
|
||||
// 2) Bob add Alice becommes a mutual contacts
|
||||
bob := s.newMessengerForProfileShowcase()
|
||||
defer TearDownMessenger(&s.Suite, bob)
|
||||
|
||||
s.mutualContact(bob)
|
||||
|
||||
// 3) Alice and bob join the community
|
||||
advertiseCommunityTo(&s.Suite, community, owner, alice)
|
||||
advertiseCommunityTo(&s.Suite, community, owner, bob)
|
||||
request := &requests.RequestToJoinCommunity{CommunityID: community.ID()}
|
||||
joinCommunity(&s.Suite, community, owner, alice, request, "")
|
||||
|
||||
joinedCommunities, err := alice.communitiesManager.Joined()
|
||||
s.Require().NoError(err)
|
||||
s.Require().Len(joinedCommunities, 1)
|
||||
s.Require().Equal(joinedCommunities[0].IDString(), community.IDString())
|
||||
s.Require().True(joinedCommunities[0].Encrypted())
|
||||
|
||||
grant, clock, err := alice.communitiesManager.GetCommunityGrant(community.IDString())
|
||||
s.Require().NoError(err)
|
||||
s.Require().NotEqual(grant, []byte{})
|
||||
s.Require().True(clock > 0)
|
||||
|
||||
// 4) Alice presents the community in her profile showcase
|
||||
err = alice.SetProfileShowcasePreferences(&identity.ProfileShowcasePreferences{
|
||||
Communities: []*identity.ProfileShowcaseCommunityPreference{
|
||||
&identity.ProfileShowcaseCommunityPreference{
|
||||
CommunityID: community.IDString(),
|
||||
ShowcaseVisibility: identity.ProfileShowcaseVisibilityEveryone,
|
||||
Order: 0,
|
||||
},
|
||||
},
|
||||
}, false)
|
||||
s.Require().NoError(err)
|
||||
|
||||
// 5) Bob gets the community from Alice's profile showcase and validates community's membership with grant
|
||||
contactID := types.EncodeHex(crypto.FromECDSAPub(&alice.identity.PublicKey))
|
||||
_, err = WaitOnMessengerResponse(
|
||||
bob,
|
||||
func(r *MessengerResponse) bool {
|
||||
return r.updatedProfileShowcaseContactIDs[contactID] == true
|
||||
},
|
||||
"no messages",
|
||||
)
|
||||
s.Require().NoError(err)
|
||||
|
||||
profileShowcase, err := bob.GetProfileShowcaseForContact(contactID, true)
|
||||
s.Require().NoError(err)
|
||||
s.Require().Len(profileShowcase.Communities, 1)
|
||||
s.Require().Equal(community.IDString(), profileShowcase.Communities[0].CommunityID)
|
||||
s.Require().Equal(identity.ProfileShowcaseMembershipStatusProvenMember, profileShowcase.Communities[0].MembershipStatus)
|
||||
|
||||
// 6) Wait until the grant expires, Bob should not be able to validate the membership anymore
|
||||
time.Sleep(communities.GrantExpirationTime)
|
||||
|
||||
profileShowcase, err = bob.GetProfileShowcaseForContact(contactID, true)
|
||||
s.Require().NoError(err)
|
||||
s.Require().Len(profileShowcase.Communities, 1)
|
||||
s.Require().Equal(community.IDString(), profileShowcase.Communities[0].CommunityID)
|
||||
s.Require().Equal(identity.ProfileShowcaseMembershipStatusUnproven, profileShowcase.Communities[0].MembershipStatus)
|
||||
|
||||
// 7) Owner updates the grant
|
||||
owner.updateGrantsForControlledCommunities()
|
||||
|
||||
// Retrieve for grant clock update
|
||||
err = tt.RetryWithBackOff(func() error {
|
||||
_, err = alice.RetrieveAll()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, updatedClock, err := alice.communitiesManager.GetCommunityGrant(community.IDString())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if clock == updatedClock {
|
||||
return errors.New("can't recive an updated grant")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
s.Require().NoError(err)
|
||||
|
||||
// 8) Bob should be able to validate the membership again
|
||||
_, err = WaitOnMessengerResponse(
|
||||
bob,
|
||||
func(r *MessengerResponse) bool {
|
||||
return r.updatedProfileShowcaseContactIDs[contactID] == true
|
||||
},
|
||||
"no messages",
|
||||
)
|
||||
s.Require().NoError(err)
|
||||
|
||||
profileShowcase, err = bob.GetProfileShowcaseForContact(contactID, true)
|
||||
s.Require().NoError(err)
|
||||
s.Require().Len(profileShowcase.Communities, 1)
|
||||
s.Require().Equal(profileShowcase.Communities[0].CommunityID, community.IDString())
|
||||
s.Require().Equal(profileShowcase.Communities[0].MembershipStatus, identity.ProfileShowcaseMembershipStatusProvenMember)
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ const selectProfileShowcasePreferencesQuery = "SELECT clock FROM profile_showcas
|
|||
|
||||
const upsertProfileShowcaseCommunityPreferenceQuery = "INSERT OR REPLACE INTO profile_showcase_communities_preferences(community_id, visibility, sort_order) VALUES (?, ?, ?)" // #nosec G101
|
||||
const selectProfileShowcaseCommunityPreferenceQuery = "SELECT community_id, visibility, sort_order FROM profile_showcase_communities_preferences" // #nosec G101
|
||||
const selectSpecifiedShowcaseCommunityPreferenceQuery = "SELECT community_id, visibility, sort_order FROM profile_showcase_communities_preferences WHERE community_id = ?" // #nosec G101
|
||||
const deleteProfileShowcaseCommunityPreferenceQuery = "DELETE FROM profile_showcase_communities_preferences WHERE community_id = ?" // #nosec G101
|
||||
const clearProfileShowcaseCommunitiyPreferencesQuery = "DELETE FROM profile_showcase_communities_preferences" // #nosec G101
|
||||
|
||||
|
@ -99,14 +100,11 @@ func (db sqlitePersistence) saveProfileShowcaseCommunityPreference(tx *sql.Tx, c
|
|||
return err
|
||||
}
|
||||
|
||||
func (db sqlitePersistence) getProfileShowcaseCommunitiesPreferences(tx *sql.Tx) ([]*identity.ProfileShowcaseCommunityPreference, error) {
|
||||
rows, err := tx.Query(selectProfileShowcaseCommunityPreferenceQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
func (db sqlitePersistence) processProfileShowcaseCommunityPreferences(rows *sql.Rows) (result []*identity.ProfileShowcaseCommunityPreference, err error) {
|
||||
if rows == nil {
|
||||
return nil, errors.New("rows is nil")
|
||||
}
|
||||
|
||||
communities := []*identity.ProfileShowcaseCommunityPreference{}
|
||||
|
||||
for rows.Next() {
|
||||
community := &identity.ProfileShowcaseCommunityPreference{}
|
||||
|
||||
|
@ -120,9 +118,33 @@ func (db sqlitePersistence) getProfileShowcaseCommunitiesPreferences(tx *sql.Tx)
|
|||
return nil, err
|
||||
}
|
||||
|
||||
communities = append(communities, community)
|
||||
result = append(result, community)
|
||||
}
|
||||
return communities, nil
|
||||
|
||||
err = rows.Err()
|
||||
return
|
||||
}
|
||||
|
||||
func (db sqlitePersistence) getProfileShowcaseCommunitiesPreferences(tx *sql.Tx) ([]*identity.ProfileShowcaseCommunityPreference, error) {
|
||||
rows, err := tx.Query(selectProfileShowcaseCommunityPreferenceQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return db.processProfileShowcaseCommunityPreferences(rows)
|
||||
}
|
||||
|
||||
func (db sqlitePersistence) GetProfileShowcaseCommunityPreference(communityID string) (*identity.ProfileShowcaseCommunityPreference, error) {
|
||||
rows, err := db.db.Query(selectSpecifiedShowcaseCommunityPreferenceQuery, communityID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
communities, err := db.processProfileShowcaseCommunityPreferences(rows)
|
||||
if len(communities) > 0 {
|
||||
return communities[0], err
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func (db sqlitePersistence) DeleteProfileShowcaseCommunityPreference(communityID string) (bool, error) {
|
||||
|
|
|
@ -106,6 +106,7 @@ const (
|
|||
ApplicationMetadataMessage_COMMUNITY_PUBLIC_STORENODES_INFO ApplicationMetadataMessage_Type = 83
|
||||
ApplicationMetadataMessage_COMMUNITY_REEVALUATE_PERMISSIONS_REQUEST ApplicationMetadataMessage_Type = 84
|
||||
ApplicationMetadataMessage_DELETE_COMMUNITY_MEMBER_MESSAGES ApplicationMetadataMessage_Type = 85
|
||||
ApplicationMetadataMessage_COMMUNITY_UPDATE_GRANT ApplicationMetadataMessage_Type = 86
|
||||
)
|
||||
|
||||
// Enum value maps for ApplicationMetadataMessage_Type.
|
||||
|
@ -192,6 +193,7 @@ var (
|
|||
83: "COMMUNITY_PUBLIC_STORENODES_INFO",
|
||||
84: "COMMUNITY_REEVALUATE_PERMISSIONS_REQUEST",
|
||||
85: "DELETE_COMMUNITY_MEMBER_MESSAGES",
|
||||
86: "COMMUNITY_UPDATE_GRANT",
|
||||
}
|
||||
ApplicationMetadataMessage_Type_value = map[string]int32{
|
||||
"UNKNOWN": 0,
|
||||
|
@ -275,6 +277,7 @@ var (
|
|||
"COMMUNITY_PUBLIC_STORENODES_INFO": 83,
|
||||
"COMMUNITY_REEVALUATE_PERMISSIONS_REQUEST": 84,
|
||||
"DELETE_COMMUNITY_MEMBER_MESSAGES": 85,
|
||||
"COMMUNITY_UPDATE_GRANT": 86,
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -376,7 +379,7 @@ var File_application_metadata_message_proto protoreflect.FileDescriptor
|
|||
var file_application_metadata_message_proto_rawDesc = []byte{
|
||||
0x0a, 0x22, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6d, 0x65,
|
||||
0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x70,
|
||||
0x72, 0x6f, 0x74, 0x6f, 0x12, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x22, 0xc3,
|
||||
0x72, 0x6f, 0x74, 0x6f, 0x12, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x22, 0xdf,
|
||||
0x15, 0x0a, 0x1a, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65,
|
||||
0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x1c, 0x0a,
|
||||
0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c,
|
||||
|
@ -386,7 +389,7 @@ var file_application_metadata_message_proto_rawDesc = []byte{
|
|||
0x01, 0x28, 0x0e, 0x32, 0x29, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41,
|
||||
0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61,
|
||||
0x74, 0x61, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04,
|
||||
0x74, 0x79, 0x70, 0x65, 0x22, 0xad, 0x14, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a,
|
||||
0x74, 0x79, 0x70, 0x65, 0x22, 0xc9, 0x14, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a,
|
||||
0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x10, 0x0a, 0x0c, 0x43, 0x48,
|
||||
0x41, 0x54, 0x5f, 0x4d, 0x45, 0x53, 0x53, 0x41, 0x47, 0x45, 0x10, 0x01, 0x12, 0x12, 0x0a, 0x0e,
|
||||
0x43, 0x4f, 0x4e, 0x54, 0x41, 0x43, 0x54, 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x10, 0x02,
|
||||
|
@ -538,19 +541,21 @@ var file_application_metadata_message_proto_rawDesc = []byte{
|
|||
0x45, 0x5f, 0x50, 0x45, 0x52, 0x4d, 0x49, 0x53, 0x53, 0x49, 0x4f, 0x4e, 0x53, 0x5f, 0x52, 0x45,
|
||||
0x51, 0x55, 0x45, 0x53, 0x54, 0x10, 0x54, 0x12, 0x24, 0x0a, 0x20, 0x44, 0x45, 0x4c, 0x45, 0x54,
|
||||
0x45, 0x5f, 0x43, 0x4f, 0x4d, 0x4d, 0x55, 0x4e, 0x49, 0x54, 0x59, 0x5f, 0x4d, 0x45, 0x4d, 0x42,
|
||||
0x45, 0x52, 0x5f, 0x4d, 0x45, 0x53, 0x53, 0x41, 0x47, 0x45, 0x53, 0x10, 0x55, 0x22, 0x04, 0x08,
|
||||
0x0e, 0x10, 0x0e, 0x22, 0x04, 0x08, 0x41, 0x10, 0x41, 0x22, 0x04, 0x08, 0x42, 0x10, 0x42, 0x22,
|
||||
0x04, 0x08, 0x47, 0x10, 0x47, 0x2a, 0x1d, 0x53, 0x59, 0x4e, 0x43, 0x5f, 0x49, 0x4e, 0x53, 0x54,
|
||||
0x41, 0x4c, 0x4c, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x5f,
|
||||
0x43, 0x48, 0x41, 0x54, 0x2a, 0x22, 0x53, 0x59, 0x4e, 0x43, 0x5f, 0x41, 0x43, 0x54, 0x49, 0x56,
|
||||
0x45, 0x52, 0x5f, 0x4d, 0x45, 0x53, 0x53, 0x41, 0x47, 0x45, 0x53, 0x10, 0x55, 0x12, 0x1a, 0x0a,
|
||||
0x16, 0x43, 0x4f, 0x4d, 0x4d, 0x55, 0x4e, 0x49, 0x54, 0x59, 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54,
|
||||
0x45, 0x5f, 0x47, 0x52, 0x41, 0x4e, 0x54, 0x10, 0x56, 0x22, 0x04, 0x08, 0x0e, 0x10, 0x0e, 0x22,
|
||||
0x04, 0x08, 0x41, 0x10, 0x41, 0x22, 0x04, 0x08, 0x42, 0x10, 0x42, 0x22, 0x04, 0x08, 0x47, 0x10,
|
||||
0x47, 0x2a, 0x1d, 0x53, 0x59, 0x4e, 0x43, 0x5f, 0x49, 0x4e, 0x53, 0x54, 0x41, 0x4c, 0x4c, 0x41,
|
||||
0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x5f, 0x43, 0x48, 0x41, 0x54,
|
||||
0x2a, 0x22, 0x53, 0x59, 0x4e, 0x43, 0x5f, 0x41, 0x43, 0x54, 0x49, 0x56, 0x49, 0x54, 0x59, 0x5f,
|
||||
0x43, 0x45, 0x4e, 0x54, 0x45, 0x52, 0x5f, 0x4e, 0x4f, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54,
|
||||
0x49, 0x4f, 0x4e, 0x53, 0x2a, 0x27, 0x53, 0x59, 0x4e, 0x43, 0x5f, 0x41, 0x43, 0x54, 0x49, 0x56,
|
||||
0x49, 0x54, 0x59, 0x5f, 0x43, 0x45, 0x4e, 0x54, 0x45, 0x52, 0x5f, 0x4e, 0x4f, 0x54, 0x49, 0x46,
|
||||
0x49, 0x43, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x53, 0x2a, 0x27, 0x53, 0x59, 0x4e, 0x43, 0x5f, 0x41,
|
||||
0x43, 0x54, 0x49, 0x56, 0x49, 0x54, 0x59, 0x5f, 0x43, 0x45, 0x4e, 0x54, 0x45, 0x52, 0x5f, 0x4e,
|
||||
0x4f, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x54, 0x41, 0x54,
|
||||
0x45, 0x2a, 0x21, 0x43, 0x4f, 0x4d, 0x4d, 0x55, 0x4e, 0x49, 0x54, 0x59, 0x5f, 0x45, 0x56, 0x45,
|
||||
0x4e, 0x54, 0x53, 0x5f, 0x4d, 0x45, 0x53, 0x53, 0x41, 0x47, 0x45, 0x5f, 0x52, 0x45, 0x4a, 0x45,
|
||||
0x43, 0x54, 0x45, 0x44, 0x42, 0x0d, 0x5a, 0x0b, 0x2e, 0x2f, 0x3b, 0x70, 0x72, 0x6f, 0x74, 0x6f,
|
||||
0x62, 0x75, 0x66, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
0x49, 0x43, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x2a, 0x21, 0x43,
|
||||
0x4f, 0x4d, 0x4d, 0x55, 0x4e, 0x49, 0x54, 0x59, 0x5f, 0x45, 0x56, 0x45, 0x4e, 0x54, 0x53, 0x5f,
|
||||
0x4d, 0x45, 0x53, 0x53, 0x41, 0x47, 0x45, 0x5f, 0x52, 0x45, 0x4a, 0x45, 0x43, 0x54, 0x45, 0x44,
|
||||
0x42, 0x0d, 0x5a, 0x0b, 0x2e, 0x2f, 0x3b, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x62,
|
||||
0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
|
|
|
@ -104,5 +104,6 @@ message ApplicationMetadataMessage {
|
|||
COMMUNITY_PUBLIC_STORENODES_INFO = 83;
|
||||
COMMUNITY_REEVALUATE_PERMISSIONS_REQUEST = 84;
|
||||
DELETE_COMMUNITY_MEMBER_MESSAGES = 85;
|
||||
COMMUNITY_UPDATE_GRANT = 86;
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -12,6 +12,7 @@ message Grant {
|
|||
bytes member_id = 2;
|
||||
string chat_id = 3;
|
||||
uint64 clock = 4;
|
||||
uint64 expires = 5;
|
||||
}
|
||||
|
||||
message CommunityMember {
|
||||
|
@ -271,3 +272,9 @@ message DeleteCommunityMemberMessages {
|
|||
string member_id = 3;
|
||||
repeated DeleteCommunityMemberMessage messages = 4;
|
||||
}
|
||||
|
||||
message CommunityUpdateGrant {
|
||||
uint64 timestamp = 1;
|
||||
bytes community_id = 2;
|
||||
map<uint32, bytes> grants = 3;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue