feat: introduce `CheckChannelPermissions` API

Similar to `CheckPermissionToJoin()` we now get
a `CheckChannelPermissions()` API.

It will rely on the same `PermissionResponse` types, but gives
information about both `ViewOnlyPermissions` and
`ViewAndPostPermissions`.
This commit is contained in:
Pascal Precht 2023-06-12 17:17:37 +02:00 committed by Follow the white rabbit
parent 49187cc553
commit 7938297606
7 changed files with 438 additions and 17 deletions

View File

@ -1497,6 +1497,25 @@ func (o *Community) TokenPermissionsByType(permissionType protobuf.CommunityToke
return permissions
}
func (o *Community) ChannelTokenPermissionsByType(channelID string, permissionType protobuf.CommunityTokenPermission_Type) []*protobuf.CommunityTokenPermission {
permissions := make([]*protobuf.CommunityTokenPermission, 0)
for _, tokenPermission := range o.TokenPermissions() {
if tokenPermission.Type == permissionType && includes(tokenPermission.ChatIds, channelID) {
permissions = append(permissions, tokenPermission)
}
}
return permissions
}
func includes(channelIDs []string, channelID string) bool {
for _, id := range channelIDs {
if id == channelID {
return true
}
}
return false
}
func (o *Community) AddTokenPermission(permission *protobuf.CommunityTokenPermission) (*CommunityChanges, error) {
o.mutex.Lock()
defer o.mutex.Unlock()

View File

@ -803,6 +803,49 @@ func (s *CommunitySuite) TestChatIDs() {
s.Require().Len(chatIDs, 1)
}
func (s *CommunitySuite) TestChannelTokenPermissionsByType() {
org := s.buildCommunity(&s.identity.PublicKey)
viewOnlyPermissions := []*protobuf.CommunityTokenPermission{
&protobuf.CommunityTokenPermission{
Id: "some-id",
Type: protobuf.CommunityTokenPermission_CAN_VIEW_CHANNEL,
TokenCriteria: make([]*protobuf.TokenCriteria, 0),
ChatIds: []string{"some-chat-id"},
},
}
viewAndPostPermissions := []*protobuf.CommunityTokenPermission{
&protobuf.CommunityTokenPermission{
Id: "some-other-id",
Type: protobuf.CommunityTokenPermission_CAN_VIEW_AND_POST_CHANNEL,
TokenCriteria: make([]*protobuf.TokenCriteria, 0),
ChatIds: []string{"some-chat-id-2"},
},
}
for _, viewOnlyPermission := range viewOnlyPermissions {
_, err := org.AddTokenPermission(viewOnlyPermission)
s.Require().NoError(err)
}
for _, viewAndPostPermission := range viewAndPostPermissions {
_, err := org.AddTokenPermission(viewAndPostPermission)
s.Require().NoError(err)
}
result := org.ChannelTokenPermissionsByType("some-chat-id", protobuf.CommunityTokenPermission_CAN_VIEW_CHANNEL)
s.Require().Len(result, 1)
s.Require().Equal(result[0].Id, viewOnlyPermissions[0].Id)
s.Require().Equal(result[0].TokenCriteria, viewOnlyPermissions[0].TokenCriteria)
s.Require().Equal(result[0].ChatIds, viewOnlyPermissions[0].ChatIds)
result = org.ChannelTokenPermissionsByType("some-chat-id-2", protobuf.CommunityTokenPermission_CAN_VIEW_AND_POST_CHANNEL)
s.Require().Len(result, 1)
s.Require().Equal(result[0].Id, viewAndPostPermissions[0].Id)
s.Require().Equal(result[0].TokenCriteria, viewAndPostPermissions[0].TokenCriteria)
s.Require().Equal(result[0].ChatIds, viewAndPostPermissions[0].ChatIds)
}
func (s *CommunitySuite) emptyCommunityDescription() *protobuf.CommunityDescription {
return &protobuf.CommunityDescription{
Permissions: &protobuf.CommunityPermissions{},

View File

@ -1550,12 +1550,7 @@ func (m *Manager) CheckPermissionToJoin(id []byte, addresses []gethcommon.Addres
}
accountsAndChainIDs := combineAddressesAndChainIDs(addresses, allChainIDs)
hasPermission, err := m.checkPermissionToJoin(permissionsToJoin, accountsAndChainIDs, false)
if err != nil {
return nil, err
}
return hasPermission, nil
return m.checkPermissionToJoin(permissionsToJoin, accountsAndChainIDs, false)
}
func (m *Manager) AcceptRequestToJoin(request *requests.AcceptRequestToJoinCommunity) (*Community, error) {
@ -1882,13 +1877,15 @@ func (m *Manager) HandleCommunityRequestToJoin(signer *ecdsa.PublicKey, request
return requestToJoin, nil
}
type CheckPermissionToJoinResponse struct {
Satisfied bool `json:"satisfied"`
Permissions map[string]*CheckPermissionToJoinResult `json:"permissions"`
ValidCombinations []*AccountChainIDsCombination `json:"validCombinations"`
type CheckPermissionsResponse struct {
Satisfied bool `json:"satisfied"`
Permissions map[string]*PermissionTokenCriteriaResult `json:"permissions"`
ValidCombinations []*AccountChainIDsCombination `json:"validCombinations"`
}
type CheckPermissionToJoinResult struct {
type CheckPermissionToJoinResponse = CheckPermissionsResponse
type PermissionTokenCriteriaResult struct {
Criteria []bool `json:"criteria"`
}
@ -1936,14 +1933,18 @@ func calculateChainIDsSet(accountsAndChainIDs []*AccountChainIDsCombination, req
return revealedAccountsChainIDs
}
// checkPermissionToJoin will retrieve balances and check whether the user has
func (m *Manager) checkPermissionToJoin(permissions []*protobuf.CommunityTokenPermission, accountsAndChainIDs []*AccountChainIDsCombination, shortcircuit bool) (*CheckPermissionToJoinResponse, error) {
return m.checkPermissions(permissions, accountsAndChainIDs, shortcircuit)
}
// checkPermissions will retrieve balances and check whether the user has
// permission to join the community, if shortcircuit is true, it will stop as soon
// as we know the answer
func (m *Manager) checkPermissionToJoin(permissions []*protobuf.CommunityTokenPermission, accountsAndChainIDs []*AccountChainIDsCombination, shortcircuit bool) (*CheckPermissionToJoinResponse, error) {
func (m *Manager) checkPermissions(permissions []*protobuf.CommunityTokenPermission, accountsAndChainIDs []*AccountChainIDsCombination, shortcircuit bool) (*CheckPermissionsResponse, error) {
response := &CheckPermissionToJoinResponse{
response := &CheckPermissionsResponse{
Satisfied: false,
Permissions: make(map[string]*CheckPermissionToJoinResult),
Permissions: make(map[string]*PermissionTokenCriteriaResult),
ValidCombinations: make([]*AccountChainIDsCombination, 0),
}
@ -2004,7 +2005,7 @@ func (m *Manager) checkPermissionToJoin(permissions []*protobuf.CommunityTokenPe
for _, tokenPermission := range permissions {
permissionRequirementsMet := true
response.Permissions[tokenPermission.Id] = &CheckPermissionToJoinResult{}
response.Permissions[tokenPermission.Id] = &PermissionTokenCriteriaResult{}
// There can be multiple token requirements per permission.
// If only one is not met, the entire permission is marked
@ -2252,6 +2253,87 @@ func (m *Manager) getOwnedENS(addresses []gethcommon.Address) ([]string, error)
return ownedENS, nil
}
func (m *Manager) CheckChannelPermissions(communityID types.HexBytes, chatID string, addresses []gethcommon.Address) (*CheckChannelPermissionsResponse, error) {
community, err := m.GetByID(communityID)
if err != nil {
return nil, err
}
if chatID == "" {
return nil, errors.New(fmt.Sprintf("couldn't check channel permissions, invalid chat id: %s", chatID))
}
viewOnlyPermissions := community.ChannelTokenPermissionsByType(chatID, protobuf.CommunityTokenPermission_CAN_VIEW_CHANNEL)
viewAndPostPermissions := community.ChannelTokenPermissionsByType(chatID, protobuf.CommunityTokenPermission_CAN_VIEW_AND_POST_CHANNEL)
allChainIDs, err := m.tokenManager.GetAllChainIDs()
if err != nil {
return nil, err
}
accountsAndChainIDs := combineAddressesAndChainIDs(addresses, allChainIDs)
return m.checkChannelPermissions(viewOnlyPermissions, viewAndPostPermissions, accountsAndChainIDs, false)
}
type CheckChannelPermissionsResponse struct {
ViewOnlyPermissions *CheckChannelViewOnlyPermissionsResult `json:"viewOnlyPermissions"`
ViewAndPostPermissions *CheckChannelViewAndPostPermissionsResult `json:"viewAndPostPermissions"`
}
type CheckChannelViewOnlyPermissionsResult struct {
Satisfied bool `json:"satisfied"`
Permissions map[string]*PermissionTokenCriteriaResult `json:"permissions"`
}
type CheckChannelViewAndPostPermissionsResult struct {
Satisfied bool `json:"satisfied"`
Permissions map[string]*PermissionTokenCriteriaResult `json:"permissions"`
}
func (m *Manager) checkChannelPermissions(viewOnlyPermissions []*protobuf.CommunityTokenPermission, viewAndPostPermissions []*protobuf.CommunityTokenPermission, accountsAndChainIDs []*AccountChainIDsCombination, shortcircuit bool) (*CheckChannelPermissionsResponse, error) {
response := &CheckChannelPermissionsResponse{
ViewOnlyPermissions: &CheckChannelViewOnlyPermissionsResult{
Satisfied: false,
Permissions: make(map[string]*PermissionTokenCriteriaResult),
},
ViewAndPostPermissions: &CheckChannelViewAndPostPermissionsResult{
Satisfied: false,
Permissions: make(map[string]*PermissionTokenCriteriaResult),
},
}
viewOnlyPermissionsResponse, err := m.checkPermissions(viewOnlyPermissions, accountsAndChainIDs, shortcircuit)
if err != nil {
return nil, err
}
viewAndPostPermissionsResponse, err := m.checkPermissions(viewAndPostPermissions, accountsAndChainIDs, shortcircuit)
if err != nil {
return nil, err
}
hasViewOnlyPermissions := len(viewOnlyPermissions) > 0
hasViewAndPostPermissions := len(viewAndPostPermissions) > 0
if (hasViewAndPostPermissions && !hasViewOnlyPermissions) || (hasViewOnlyPermissions && hasViewAndPostPermissions && viewAndPostPermissionsResponse.Satisfied) {
response.ViewOnlyPermissions.Satisfied = viewAndPostPermissionsResponse.Satisfied
} else {
response.ViewOnlyPermissions.Satisfied = viewOnlyPermissionsResponse.Satisfied
}
response.ViewOnlyPermissions.Permissions = viewOnlyPermissionsResponse.Permissions
if (hasViewOnlyPermissions && !viewOnlyPermissionsResponse.Satisfied) ||
(hasViewOnlyPermissions && !hasViewAndPostPermissions) {
response.ViewAndPostPermissions.Satisfied = false
} else {
response.ViewAndPostPermissions.Satisfied = viewAndPostPermissionsResponse.Satisfied
}
response.ViewAndPostPermissions.Permissions = viewAndPostPermissionsResponse.Permissions
return response, nil
}
func (m *Manager) HandleCommunityRequestToJoinResponse(signer *ecdsa.PublicKey, request *protobuf.CommunityRequestToJoinResponse) (*RequestToJoin, error) {
pkString := common.PubkeyToHex(&m.identity.PublicKey)

View File

@ -110,7 +110,7 @@ func (m *testTokenManager) GetBalancesByChain(ctx context.Context, accounts, tok
return m.response, nil
}
func (s *ManagerSuite) TestRetrieveTokens() {
func (s *ManagerSuite) setupManagerForTokenPermissions() (*Manager, *testTokenManager) {
db, err := appdatabase.InitializeDB(sqlite.InMemoryPath, "", sqlite.ReducedKDFIterationsNumber)
s.NoError(err, "creating sqlite db instance")
err = sqlite.Migrate(db)
@ -134,6 +134,12 @@ func (s *ManagerSuite) TestRetrieveTokens() {
s.Require().NoError(err)
s.Require().NoError(m.Start())
return m, tm
}
func (s *ManagerSuite) TestRetrieveTokens() {
m, tm := s.setupManagerForTokenPermissions()
var chainID uint64 = 5
contractAddresses := make(map[uint64]string)
contractAddresses[chainID] = "0x3d6afaa395c31fcd391fe3d562e75fe9e8ec7e6a"
@ -804,6 +810,226 @@ func (s *ManagerSuite) TestUnseedHistoryArchiveTorrent() {
s.Require().Equal(ok, false)
}
func (s *ManagerSuite) TestCheckChannelPermissions_NoPermissions() {
m, tm := s.setupManagerForTokenPermissions()
var chainID uint64 = 5
contractAddresses := make(map[uint64]string)
contractAddresses[chainID] = "0x3d6afaa395c31fcd391fe3d562e75fe9e8ec7e6a"
accountChainIDsCombination := []*AccountChainIDsCombination{
&AccountChainIDsCombination{
Address: gethcommon.HexToAddress("0xD6b912e09E797D291E8D0eA3D3D17F8000e01c32"),
ChainIDs: []uint64{chainID},
},
}
var viewOnlyPermissions = make([]*protobuf.CommunityTokenPermission, 0)
var viewAndPostPermissions = make([]*protobuf.CommunityTokenPermission, 0)
tm.setResponse(chainID, accountChainIDsCombination[0].Address, gethcommon.HexToAddress(contractAddresses[chainID]), 0)
resp, err := m.checkChannelPermissions(viewOnlyPermissions, viewAndPostPermissions, accountChainIDsCombination, false)
s.Require().NoError(err)
s.Require().NotNil(resp)
// Both viewOnly and viewAndPost permissions are expected to be satisfied
// because we call `checkChannelPermissions()` with no permissions to check
s.Require().True(resp.ViewOnlyPermissions.Satisfied)
s.Require().True(resp.ViewAndPostPermissions.Satisfied)
}
func (s *ManagerSuite) TestCheckChannelPermissions_ViewOnlyPermissions() {
m, tm := s.setupManagerForTokenPermissions()
var chainID uint64 = 5
contractAddresses := make(map[uint64]string)
contractAddresses[chainID] = "0x3d6afaa395c31fcd391fe3d562e75fe9e8ec7e6a"
var decimals uint64 = 18
accountChainIDsCombination := []*AccountChainIDsCombination{
&AccountChainIDsCombination{
Address: gethcommon.HexToAddress("0xD6b912e09E797D291E8D0eA3D3D17F8000e01c32"),
ChainIDs: []uint64{chainID},
},
}
var tokenCriteria = []*protobuf.TokenCriteria{
&protobuf.TokenCriteria{
ContractAddresses: contractAddresses,
Symbol: "STT",
Type: protobuf.CommunityTokenType_ERC20,
Name: "Status Test Token",
Amount: "1.000000000000000000",
Decimals: decimals,
},
}
var viewOnlyPermissions = []*protobuf.CommunityTokenPermission{
&protobuf.CommunityTokenPermission{
Id: "some-id",
Type: protobuf.CommunityTokenPermission_CAN_VIEW_CHANNEL,
TokenCriteria: tokenCriteria,
ChatIds: []string{"test-channel-id", "test-channel-id-2"},
},
}
var viewAndPostPermissions = make([]*protobuf.CommunityTokenPermission, 0)
tm.setResponse(chainID, accountChainIDsCombination[0].Address, gethcommon.HexToAddress(contractAddresses[chainID]), 0)
resp, err := m.checkChannelPermissions(viewOnlyPermissions, viewAndPostPermissions, accountChainIDsCombination, false)
s.Require().NoError(err)
s.Require().NotNil(resp)
s.Require().False(resp.ViewOnlyPermissions.Satisfied)
// if viewOnly permissions are not satisfied then viewAndPost
// permissions shouldn't be satisfied either
s.Require().False(resp.ViewAndPostPermissions.Satisfied)
// Set response to exactly the right one
tm.setResponse(chainID, accountChainIDsCombination[0].Address, gethcommon.HexToAddress(contractAddresses[chainID]), int64(1*math.Pow(10, float64(decimals))))
resp, err = m.checkChannelPermissions(viewOnlyPermissions, viewAndPostPermissions, accountChainIDsCombination, false)
s.Require().NoError(err)
s.Require().NotNil(resp)
s.Require().True(resp.ViewOnlyPermissions.Satisfied)
s.Require().False(resp.ViewAndPostPermissions.Satisfied)
}
func (s *ManagerSuite) TestCheckChannelPermissions_ViewAndPostPermissions() {
m, tm := s.setupManagerForTokenPermissions()
var chainID uint64 = 5
contractAddresses := make(map[uint64]string)
contractAddresses[chainID] = "0x3d6afaa395c31fcd391fe3d562e75fe9e8ec7e6a"
var decimals uint64 = 18
accountChainIDsCombination := []*AccountChainIDsCombination{
&AccountChainIDsCombination{
Address: gethcommon.HexToAddress("0xD6b912e09E797D291E8D0eA3D3D17F8000e01c32"),
ChainIDs: []uint64{chainID},
},
}
var tokenCriteria = []*protobuf.TokenCriteria{
&protobuf.TokenCriteria{
ContractAddresses: contractAddresses,
Symbol: "STT",
Type: protobuf.CommunityTokenType_ERC20,
Name: "Status Test Token",
Amount: "1.000000000000000000",
Decimals: decimals,
},
}
var viewAndPostPermissions = []*protobuf.CommunityTokenPermission{
&protobuf.CommunityTokenPermission{
Id: "some-id",
Type: protobuf.CommunityTokenPermission_CAN_VIEW_CHANNEL,
TokenCriteria: tokenCriteria,
ChatIds: []string{"test-channel-id", "test-channel-id-2"},
},
}
var viewOnlyPermissions = make([]*protobuf.CommunityTokenPermission, 0)
tm.setResponse(chainID, accountChainIDsCombination[0].Address, gethcommon.HexToAddress(contractAddresses[chainID]), 0)
resp, err := m.checkChannelPermissions(viewOnlyPermissions, viewAndPostPermissions, accountChainIDsCombination, false)
s.Require().NoError(err)
s.Require().NotNil(resp)
s.Require().False(resp.ViewAndPostPermissions.Satisfied)
// viewOnly permissions are flagged as not satisfied because we have no viewOnly
// permissions on this channel and the viewAndPost permission is not satisfied either
s.Require().False(resp.ViewOnlyPermissions.Satisfied)
// Set response to exactly the right one
tm.setResponse(chainID, accountChainIDsCombination[0].Address, gethcommon.HexToAddress(contractAddresses[chainID]), int64(1*math.Pow(10, float64(decimals))))
resp, err = m.checkChannelPermissions(viewOnlyPermissions, viewAndPostPermissions, accountChainIDsCombination, false)
s.Require().NoError(err)
s.Require().NotNil(resp)
s.Require().True(resp.ViewAndPostPermissions.Satisfied)
// if viewAndPost is satisfied then viewOnly should be automatically satisfied
s.Require().True(resp.ViewOnlyPermissions.Satisfied)
}
func (s *ManagerSuite) TestCheckChannelPermissions_ViewAndPostPermissionsCombination() {
m, tm := s.setupManagerForTokenPermissions()
var chainID uint64 = 5
contractAddresses := make(map[uint64]string)
contractAddresses[chainID] = "0x3d6afaa395c31fcd391fe3d562e75fe9e8ec7e6a"
var decimals uint64 = 18
accountChainIDsCombination := []*AccountChainIDsCombination{
&AccountChainIDsCombination{
Address: gethcommon.HexToAddress("0xD6b912e09E797D291E8D0eA3D3D17F8000e01c32"),
ChainIDs: []uint64{chainID},
},
}
var viewOnlyTokenCriteria = []*protobuf.TokenCriteria{
&protobuf.TokenCriteria{
ContractAddresses: contractAddresses,
Symbol: "STT",
Type: protobuf.CommunityTokenType_ERC20,
Name: "Status Test Token",
Amount: "1.000000000000000000",
Decimals: decimals,
},
}
var viewOnlyPermissions = []*protobuf.CommunityTokenPermission{
&protobuf.CommunityTokenPermission{
Id: "some-id",
Type: protobuf.CommunityTokenPermission_CAN_VIEW_CHANNEL,
TokenCriteria: viewOnlyTokenCriteria,
ChatIds: []string{"test-channel-id", "test-channel-id-2"},
},
}
testContractAddresses := make(map[uint64]string)
testContractAddresses[chainID] = "0x123"
// Set up token criteria that won't be satisfied
var viewAndPostTokenCriteria = []*protobuf.TokenCriteria{
&protobuf.TokenCriteria{
ContractAddresses: testContractAddresses,
Symbol: "TEST",
Type: protobuf.CommunityTokenType_ERC20,
Name: "TEST token",
Amount: "1.000000000000000000",
Decimals: decimals,
},
}
var viewAndPostPermissions = []*protobuf.CommunityTokenPermission{
&protobuf.CommunityTokenPermission{
Id: "some-id",
Type: protobuf.CommunityTokenPermission_CAN_VIEW_CHANNEL,
TokenCriteria: viewAndPostTokenCriteria,
ChatIds: []string{"test-channel-id", "test-channel-id-2"},
},
}
// Set response for viewOnly permissions
tm.setResponse(chainID, accountChainIDsCombination[0].Address, gethcommon.HexToAddress(contractAddresses[chainID]), int64(1*math.Pow(10, float64(decimals))))
// Set resopnse for viewAndPost permissions
tm.setResponse(chainID, accountChainIDsCombination[0].Address, gethcommon.HexToAddress(testContractAddresses[chainID]), 0)
resp, err := m.checkChannelPermissions(viewOnlyPermissions, viewAndPostPermissions, accountChainIDsCombination, false)
s.Require().NoError(err)
s.Require().NotNil(resp)
// viewOnly permission should be satisfied, even though viewAndPost is not satisfied
s.Require().True(resp.ViewOnlyPermissions.Satisfied)
s.Require().False(resp.ViewAndPostPermissions.Satisfied)
}
func buildTorrentConfig() params.TorrentConfig {
torrentConfig := params.TorrentConfig{
Enabled: true,

View File

@ -3856,6 +3856,25 @@ func (m *Messenger) CheckPermissionsToJoinCommunity(request *requests.CheckPermi
return m.communitiesManager.CheckPermissionToJoin(request.CommunityID, addresses)
}
func (m *Messenger) CheckCommunityChannelPermissions(request *requests.CheckCommunityChannelPermissions) (*communities.CheckChannelPermissionsResponse, error) {
if err := request.Validate(); err != nil {
return nil, err
}
accounts, err := m.settings.GetAccounts()
if err != nil {
return nil, err
}
var addresses []gethcommon.Address
for _, a := range accounts {
addresses = append(addresses, gethcommon.HexToAddress(a.Address.Hex()))
}
return m.communitiesManager.CheckChannelPermissions(request.CommunityID, request.ChatID, addresses)
}
func chunkSlice[T comparable](slice []T, chunkSize int) [][]T {
var chunks [][]T
for i := 0; i < len(slice); i += chunkSize {

View File

@ -0,0 +1,28 @@
package requests
import (
"errors"
"github.com/status-im/status-go/eth-node/types"
)
var (
ErrCheckCommunityChannelPermissionsInvalidID = errors.New("check-community-channel-permissions: invalid id")
ErrCheckCommunityChannelPermissionsInvalidChatID = errors.New("check-community-channel-permissions: invalid chat id")
)
type CheckCommunityChannelPermissions struct {
CommunityID types.HexBytes
ChatID string
}
func (u *CheckCommunityChannelPermissions) Validate() error {
if len(u.CommunityID) == 0 {
return ErrCheckCommunityChannelPermissionsInvalidID
}
if len(u.ChatID) == 0 {
return ErrCheckCommunityChannelPermissionsInvalidChatID
}
return nil
}

View File

@ -1329,6 +1329,10 @@ func (api *PublicAPI) CheckPermissionsToJoinCommunity(request *requests.CheckPer
return api.service.messenger.CheckPermissionsToJoinCommunity(request)
}
func (api *PublicAPI) CheckCommunityChannelPermissions(request *requests.CheckCommunityChannelPermissions) (*communities.CheckChannelPermissionsResponse, error) {
return api.service.messenger.CheckCommunityChannelPermissions(request)
}
func (api *PublicAPI) Messenger() *protocol.Messenger {
return api.service.messenger
}