Add HighestRole & ordered roles to permission response

This commit adds HighestRole & a list of permissions in order of
importance to the CheckPermissionToJoinResponse.

This simplify client code so that it doesn't need to be calculated on
the client.
This commit is contained in:
Andrea Maria Piana 2024-02-10 11:42:16 +00:00
parent 8fa31df498
commit daef5c56e2
4 changed files with 280 additions and 40 deletions

View File

@ -0,0 +1,151 @@
package communities
import (
"encoding/json"
"sort"
gethcommon "github.com/ethereum/go-ethereum/common"
"github.com/status-im/status-go/protocol/protobuf"
)
type CheckPermissionsResponse struct {
Satisfied bool `json:"satisfied"`
Permissions map[string]*PermissionTokenCriteriaResult `json:"permissions"`
ValidCombinations []*AccountChainIDsCombination `json:"validCombinations"`
}
type CheckPermissionToJoinResponse = CheckPermissionsResponse
type HighestRoleResponse struct {
Role protobuf.CommunityTokenPermission_Type `json:"type"`
Satisfied bool `json:"satisfied"`
Criteria []*PermissionTokenCriteriaResult `json:"criteria"`
}
var joiningRoleOrders = map[protobuf.CommunityTokenPermission_Type]int{
protobuf.CommunityTokenPermission_BECOME_MEMBER: 1,
protobuf.CommunityTokenPermission_BECOME_ADMIN: 2,
protobuf.CommunityTokenPermission_BECOME_TOKEN_MASTER: 3,
protobuf.CommunityTokenPermission_BECOME_TOKEN_OWNER: 4,
}
type ByRoleDesc []*HighestRoleResponse
func (a ByRoleDesc) Len() int { return len(a) }
func (a ByRoleDesc) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByRoleDesc) Less(i, j int) bool {
return joiningRoleOrders[a[i].Role] > joiningRoleOrders[a[j].Role]
}
type rolesAndHighestRole struct {
Roles []*HighestRoleResponse
HighestRole *HighestRoleResponse
}
func calculateRolesAndHighestRole(permissions map[string]*PermissionTokenCriteriaResult) *rolesAndHighestRole {
item := &rolesAndHighestRole{}
byRoleMap := make(map[protobuf.CommunityTokenPermission_Type]*HighestRoleResponse)
for _, p := range permissions {
if joiningRoleOrders[p.Role] == 0 {
continue
}
if byRoleMap[p.Role] == nil {
byRoleMap[p.Role] = &HighestRoleResponse{
Role: p.Role,
}
}
satisfied := true
for _, tr := range p.TokenRequirements {
if !tr.Satisfied {
satisfied = false
break
}
}
if satisfied {
byRoleMap[p.Role].Satisfied = true
// we prepend
byRoleMap[p.Role].Criteria = append([]*PermissionTokenCriteriaResult{p}, byRoleMap[p.Role].Criteria...)
} else {
// we append then
byRoleMap[p.Role].Criteria = append(byRoleMap[p.Role].Criteria, p)
}
}
if byRoleMap[protobuf.CommunityTokenPermission_BECOME_MEMBER] == nil {
byRoleMap[protobuf.CommunityTokenPermission_BECOME_MEMBER] = &HighestRoleResponse{Satisfied: true, Role: protobuf.CommunityTokenPermission_BECOME_MEMBER}
}
for _, p := range byRoleMap {
item.Roles = append(item.Roles, p)
}
sort.Sort(ByRoleDesc(item.Roles))
for _, r := range item.Roles {
if r.Satisfied {
item.HighestRole = r
break
}
}
return item
}
func (c *CheckPermissionsResponse) MarshalJSON() ([]byte, error) {
type CheckPermissionsTypeAlias struct {
Satisfied bool `json:"satisfied"`
Permissions map[string]*PermissionTokenCriteriaResult `json:"permissions"`
ValidCombinations []*AccountChainIDsCombination `json:"validCombinations"`
Roles []*HighestRoleResponse `json:"roles"`
HighestRole *HighestRoleResponse `json:"highestRole"`
}
c.calculateSatisfied()
item := &CheckPermissionsTypeAlias{
Satisfied: c.Satisfied,
Permissions: c.Permissions,
ValidCombinations: c.ValidCombinations,
}
rolesAndHighestRole := calculateRolesAndHighestRole(c.Permissions)
item.Roles = rolesAndHighestRole.Roles
item.HighestRole = rolesAndHighestRole.HighestRole
return json.Marshal(item)
}
type TokenRequirementResponse struct {
Satisfied bool `json:"satisfied"`
TokenCriteria *protobuf.TokenCriteria `json:"criteria"`
}
type PermissionTokenCriteriaResult struct {
Role protobuf.CommunityTokenPermission_Type `json:"roles"`
TokenRequirements []TokenRequirementResponse `json:"tokenRequirement"`
Criteria []bool `json:"criteria"`
}
type AccountChainIDsCombination struct {
Address gethcommon.Address `json:"address"`
ChainIDs []uint64 `json:"chainIds"`
}
func (c *CheckPermissionsResponse) calculateSatisfied() {
if len(c.Permissions) == 0 {
c.Satisfied = true
return
}
c.Satisfied = false
for _, p := range c.Permissions {
satisfied := true
for _, criteria := range p.Criteria {
if !criteria {
satisfied = false
break
}
}
if satisfied {
c.Satisfied = true
return
}
}
}

View File

@ -0,0 +1,121 @@
package communities
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/status-im/status-go/protocol/protobuf"
)
func TestCalculateRolesAndHighestRole(t *testing.T) {
testCases := []struct {
name string
permissions map[string]*PermissionTokenCriteriaResult
expectedRolesOrder []protobuf.CommunityTokenPermission_Type
expectedHighestRole protobuf.CommunityTokenPermission_Type
}{
{
name: "Basic scenario with multiple permissions",
permissions: map[string]*PermissionTokenCriteriaResult{
"1": {
Role: protobuf.CommunityTokenPermission_BECOME_MEMBER,
TokenRequirements: []TokenRequirementResponse{
{Satisfied: true},
},
},
"2": {
Role: protobuf.CommunityTokenPermission_BECOME_ADMIN,
TokenRequirements: []TokenRequirementResponse{
{Satisfied: true},
},
},
"3": {
Role: protobuf.CommunityTokenPermission_BECOME_TOKEN_MASTER,
TokenRequirements: []TokenRequirementResponse{
{Satisfied: false},
},
},
"4": {
Role: protobuf.CommunityTokenPermission_BECOME_TOKEN_OWNER,
TokenRequirements: []TokenRequirementResponse{
{Satisfied: false},
},
},
},
expectedRolesOrder: []protobuf.CommunityTokenPermission_Type{protobuf.CommunityTokenPermission_BECOME_TOKEN_OWNER, protobuf.CommunityTokenPermission_BECOME_TOKEN_MASTER, protobuf.CommunityTokenPermission_BECOME_ADMIN, protobuf.CommunityTokenPermission_BECOME_MEMBER},
expectedHighestRole: protobuf.CommunityTokenPermission_BECOME_ADMIN,
},
{
name: "No member permission created",
permissions: map[string]*PermissionTokenCriteriaResult{
"2": {
Role: protobuf.CommunityTokenPermission_BECOME_ADMIN,
TokenRequirements: []TokenRequirementResponse{
{Satisfied: false},
},
},
"3": {
Role: protobuf.CommunityTokenPermission_BECOME_TOKEN_MASTER,
TokenRequirements: []TokenRequirementResponse{
{Satisfied: false},
},
},
"4": {
Role: protobuf.CommunityTokenPermission_BECOME_TOKEN_OWNER,
TokenRequirements: []TokenRequirementResponse{
{Satisfied: false},
},
},
},
expectedRolesOrder: []protobuf.CommunityTokenPermission_Type{protobuf.CommunityTokenPermission_BECOME_TOKEN_OWNER, protobuf.CommunityTokenPermission_BECOME_TOKEN_MASTER, protobuf.CommunityTokenPermission_BECOME_ADMIN, protobuf.CommunityTokenPermission_BECOME_MEMBER},
expectedHighestRole: protobuf.CommunityTokenPermission_BECOME_MEMBER,
},
{
name: "no permission satisfied",
permissions: map[string]*PermissionTokenCriteriaResult{
"1": {
Role: protobuf.CommunityTokenPermission_BECOME_MEMBER,
TokenRequirements: []TokenRequirementResponse{
{Satisfied: false},
},
},
"2": {
Role: protobuf.CommunityTokenPermission_BECOME_ADMIN,
TokenRequirements: []TokenRequirementResponse{
{Satisfied: false},
},
},
"3": {
Role: protobuf.CommunityTokenPermission_BECOME_TOKEN_MASTER,
TokenRequirements: []TokenRequirementResponse{
{Satisfied: false},
},
},
"4": {
Role: protobuf.CommunityTokenPermission_BECOME_TOKEN_OWNER,
TokenRequirements: []TokenRequirementResponse{
{Satisfied: false},
},
},
},
expectedRolesOrder: []protobuf.CommunityTokenPermission_Type{protobuf.CommunityTokenPermission_BECOME_TOKEN_OWNER, protobuf.CommunityTokenPermission_BECOME_TOKEN_MASTER, protobuf.CommunityTokenPermission_BECOME_ADMIN, protobuf.CommunityTokenPermission_BECOME_MEMBER},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := calculateRolesAndHighestRole(tc.permissions)
var actualOrder []protobuf.CommunityTokenPermission_Type
for _, r := range result.Roles {
actualOrder = append(actualOrder, r.Role)
}
if tc.expectedHighestRole == 0 {
assert.Nil(t, result.HighestRole)
} else {
assert.Equal(t, tc.expectedHighestRole, result.HighestRole.Role, "Highest role is not calculated as expected")
}
assert.Equal(t, tc.expectedRolesOrder, actualOrder, "Roles are not calculated as expected")
})
}
}

View File

@ -2602,45 +2602,6 @@ func (m *Manager) HandleCommunityEditSharedAddresses(signer *ecdsa.PublicKey, re
return nil
}
type CheckPermissionsResponse struct {
Satisfied bool `json:"satisfied"`
Permissions map[string]*PermissionTokenCriteriaResult `json:"permissions"`
ValidCombinations []*AccountChainIDsCombination `json:"validCombinations"`
}
type CheckPermissionToJoinResponse = CheckPermissionsResponse
type PermissionTokenCriteriaResult struct {
Criteria []bool `json:"criteria"`
}
type AccountChainIDsCombination struct {
Address gethcommon.Address `json:"address"`
ChainIDs []uint64 `json:"chainIds"`
}
func (c *CheckPermissionsResponse) calculateSatisfied() {
if len(c.Permissions) == 0 {
c.Satisfied = true
return
}
c.Satisfied = false
for _, p := range c.Permissions {
satisfied := true
for _, criteria := range p.Criteria {
if !criteria {
satisfied = false
break
}
}
if satisfied {
c.Satisfied = true
return
}
}
}
func calculateChainIDsSet(accountsAndChainIDs []*AccountChainIDsCombination, requirementsChainIDs map[uint64]bool) []uint64 {
revealedAccountsChainIDs := make([]uint64, 0)

View File

@ -263,7 +263,7 @@ func (p *DefaultPermissionChecker) CheckPermissions(permissions []*CommunityToke
for _, tokenPermission := range permissions {
permissionRequirementsMet := true
response.Permissions[tokenPermission.Id] = &PermissionTokenCriteriaResult{}
response.Permissions[tokenPermission.Id] = &PermissionTokenCriteriaResult{Role: tokenPermission.Type}
// There can be multiple token requirements per permission.
// If only one is not met, the entire permission is marked
@ -271,9 +271,12 @@ func (p *DefaultPermissionChecker) CheckPermissions(permissions []*CommunityToke
for _, tokenRequirement := range tokenPermission.TokenCriteria {
tokenRequirementMet := false
tokenRequirementResponse := TokenRequirementResponse{TokenCriteria: tokenRequirement}
if tokenRequirement.Type == protobuf.CommunityTokenType_ERC721 {
if len(ownedERC721Tokens) == 0 {
response.Permissions[tokenPermission.Id].TokenRequirements = append(response.Permissions[tokenPermission.Id].TokenRequirements, tokenRequirementResponse)
response.Permissions[tokenPermission.Id].Criteria = append(response.Permissions[tokenPermission.Id].Criteria, false)
continue
}
@ -321,6 +324,7 @@ func (p *DefaultPermissionChecker) CheckPermissions(permissions []*CommunityToke
}
} else if tokenRequirement.Type == protobuf.CommunityTokenType_ERC20 {
if len(ownedERC20TokenBalances) == 0 {
response.Permissions[tokenPermission.Id].TokenRequirements = append(response.Permissions[tokenPermission.Id].TokenRequirements, tokenRequirementResponse)
response.Permissions[tokenPermission.Id].Criteria = append(response.Permissions[tokenPermission.Id].Criteria, false)
continue
}
@ -406,6 +410,9 @@ func (p *DefaultPermissionChecker) CheckPermissions(permissions []*CommunityToke
if !tokenRequirementMet {
permissionRequirementsMet = false
}
tokenRequirementResponse.Satisfied = tokenRequirementMet
response.Permissions[tokenPermission.Id].TokenRequirements = append(response.Permissions[tokenPermission.Id].TokenRequirements, tokenRequirementResponse)
response.Permissions[tokenPermission.Id].Criteria = append(response.Permissions[tokenPermission.Id].Criteria, tokenRequirementMet)
}
// multiple permissions are treated as logical OR, meaning