feat: add pending state for token permissions

iterates: status-im/status-desktop#11852
This commit is contained in:
Patryk Osmaczko 2023-08-17 19:14:23 +02:00 committed by osmaczko
parent a31dea7988
commit a12e87dac6
10 changed files with 465 additions and 271 deletions

View File

@ -104,26 +104,26 @@ func (o *Community) MarshalPublicAPIJSON() ([]byte, error) {
return nil, errors.New("member identity not set")
}
communityItem := struct {
ID types.HexBytes `json:"id"`
Verified bool `json:"verified"`
Chats map[string]CommunityChat `json:"chats"`
Categories map[string]CommunityCategory `json:"categories"`
Name string `json:"name"`
Description string `json:"description"`
IntroMessage string `json:"introMessage"`
OutroMessage string `json:"outroMessage"`
Tags []CommunityTag `json:"tags"`
Images map[string]images.IdentityImage `json:"images"`
Color string `json:"color"`
MembersCount int `json:"membersCount"`
EnsName string `json:"ensName"`
Link string `json:"link"`
CommunityAdminSettings CommunityAdminSettings `json:"adminSettings"`
Encrypted bool `json:"encrypted"`
BanList []string `json:"banList"`
TokenPermissions map[string]*protobuf.CommunityTokenPermission `json:"tokenPermissions"`
CommunityTokensMetadata []*protobuf.CommunityTokenMetadata `json:"communityTokensMetadata"`
ActiveMembersCount uint64 `json:"activeMembersCount"`
ID types.HexBytes `json:"id"`
Verified bool `json:"verified"`
Chats map[string]CommunityChat `json:"chats"`
Categories map[string]CommunityCategory `json:"categories"`
Name string `json:"name"`
Description string `json:"description"`
IntroMessage string `json:"introMessage"`
OutroMessage string `json:"outroMessage"`
Tags []CommunityTag `json:"tags"`
Images map[string]images.IdentityImage `json:"images"`
Color string `json:"color"`
MembersCount int `json:"membersCount"`
EnsName string `json:"ensName"`
Link string `json:"link"`
CommunityAdminSettings CommunityAdminSettings `json:"adminSettings"`
Encrypted bool `json:"encrypted"`
BanList []string `json:"banList"`
TokenPermissions map[string]*CommunityTokenPermission `json:"tokenPermissions"`
CommunityTokensMetadata []*protobuf.CommunityTokenMetadata `json:"communityTokensMetadata"`
ActiveMembersCount uint64 `json:"activeMembersCount"`
}{
ID: o.ID(),
Verified: o.config.Verified,
@ -161,7 +161,7 @@ func (o *Community) MarshalPublicAPIJSON() ([]byte, error) {
communityItem.Chats[id] = chat
}
communityItem.TokenPermissions = o.config.CommunityDescription.TokenPermissions
communityItem.TokenPermissions = o.tokenPermissions()
communityItem.MembersCount = len(o.config.CommunityDescription.Members)
communityItem.Link = fmt.Sprintf("https://join.status.im/c/0x%x", o.ID())
communityItem.IntroMessage = o.config.CommunityDescription.IntroMessage
@ -199,38 +199,38 @@ func (o *Community) MarshalJSON() ([]byte, error) {
return nil, errors.New("member identity not set")
}
communityItem := struct {
ID types.HexBytes `json:"id"`
MemberRole protobuf.CommunityMember_Roles `json:"memberRole"`
IsControlNode bool `json:"isControlNode"`
Verified bool `json:"verified"`
Joined bool `json:"joined"`
Spectated bool `json:"spectated"`
RequestedAccessAt int `json:"requestedAccessAt"`
Name string `json:"name"`
Description string `json:"description"`
IntroMessage string `json:"introMessage"`
OutroMessage string `json:"outroMessage"`
Tags []CommunityTag `json:"tags"`
Chats map[string]CommunityChat `json:"chats"`
Categories map[string]CommunityCategory `json:"categories"`
Images map[string]images.IdentityImage `json:"images"`
Permissions *protobuf.CommunityPermissions `json:"permissions"`
Members map[string]*protobuf.CommunityMember `json:"members"`
CanRequestAccess bool `json:"canRequestAccess"`
CanManageUsers bool `json:"canManageUsers"` //TODO: we can remove this
CanDeleteMessageForEveryone bool `json:"canDeleteMessageForEveryone"` //TODO: we can remove this
CanJoin bool `json:"canJoin"`
Color string `json:"color"`
RequestedToJoinAt uint64 `json:"requestedToJoinAt,omitempty"`
IsMember bool `json:"isMember"`
Muted bool `json:"muted"`
MuteTill time.Time `json:"muteTill,omitempty"`
CommunityAdminSettings CommunityAdminSettings `json:"adminSettings"`
Encrypted bool `json:"encrypted"`
BanList []string `json:"banList"`
TokenPermissions map[string]*protobuf.CommunityTokenPermission `json:"tokenPermissions"`
CommunityTokensMetadata []*protobuf.CommunityTokenMetadata `json:"communityTokensMetadata"`
ActiveMembersCount uint64 `json:"activeMembersCount"`
ID types.HexBytes `json:"id"`
MemberRole protobuf.CommunityMember_Roles `json:"memberRole"`
IsControlNode bool `json:"isControlNode"`
Verified bool `json:"verified"`
Joined bool `json:"joined"`
Spectated bool `json:"spectated"`
RequestedAccessAt int `json:"requestedAccessAt"`
Name string `json:"name"`
Description string `json:"description"`
IntroMessage string `json:"introMessage"`
OutroMessage string `json:"outroMessage"`
Tags []CommunityTag `json:"tags"`
Chats map[string]CommunityChat `json:"chats"`
Categories map[string]CommunityCategory `json:"categories"`
Images map[string]images.IdentityImage `json:"images"`
Permissions *protobuf.CommunityPermissions `json:"permissions"`
Members map[string]*protobuf.CommunityMember `json:"members"`
CanRequestAccess bool `json:"canRequestAccess"`
CanManageUsers bool `json:"canManageUsers"` //TODO: we can remove this
CanDeleteMessageForEveryone bool `json:"canDeleteMessageForEveryone"` //TODO: we can remove this
CanJoin bool `json:"canJoin"`
Color string `json:"color"`
RequestedToJoinAt uint64 `json:"requestedToJoinAt,omitempty"`
IsMember bool `json:"isMember"`
Muted bool `json:"muted"`
MuteTill time.Time `json:"muteTill,omitempty"`
CommunityAdminSettings CommunityAdminSettings `json:"adminSettings"`
Encrypted bool `json:"encrypted"`
BanList []string `json:"banList"`
TokenPermissions map[string]*CommunityTokenPermission `json:"tokenPermissions"`
CommunityTokensMetadata []*protobuf.CommunityTokenMetadata `json:"communityTokensMetadata"`
ActiveMembersCount uint64 `json:"activeMembersCount"`
}{
ID: o.ID(),
MemberRole: o.MemberRole(o.MemberIdentity()),
@ -280,7 +280,7 @@ func (o *Community) MarshalJSON() ([]byte, error) {
}
communityItem.Chats[id] = chat
}
communityItem.TokenPermissions = o.config.CommunityDescription.TokenPermissions
communityItem.TokenPermissions = o.tokenPermissions()
communityItem.Members = o.config.CommunityDescription.Members
communityItem.Permissions = o.config.CommunityDescription.Permissions
communityItem.IntroMessage = o.config.CommunityDescription.IntroMessage
@ -949,15 +949,16 @@ func (o *Community) UpdateCommunityDescription(description *protobuf.CommunityDe
return response, nil
}
// We only calculate changes if we joined/spectated the community or we requested access, otherwise not interested
if o.config.Joined || o.config.Spectated || o.config.RequestedToJoinAt > 0 {
response = EvaluateCommunityChanges(o.config.CommunityDescription, description)
response.Community = o
}
originCommunity := o.CreateDeepCopy()
o.config.CommunityDescription = description
o.config.CommunityDescriptionProtocolMessage = rawMessage
// We only calculate changes if we joined/spectated the community or we requested access, otherwise not interested
if o.config.Joined || o.config.Spectated || o.config.RequestedToJoinAt > 0 {
response = EvaluateCommunityChanges(originCommunity, o)
}
return response, nil
}
@ -1315,20 +1316,64 @@ func (o *Community) Categories() map[string]*protobuf.CommunityCategory {
return response
}
func (o *Community) TokenPermissions() map[string]*protobuf.CommunityTokenPermission {
return o.config.CommunityDescription.TokenPermissions
func (o *Community) tokenPermissions() map[string]*CommunityTokenPermission {
result := make(map[string]*CommunityTokenPermission, len(o.config.CommunityDescription.TokenPermissions))
for _, tokenPermission := range o.config.CommunityDescription.TokenPermissions {
result[tokenPermission.Id] = NewCommunityTokenPermission(tokenPermission)
}
// Non-privileged members should not see pending permissions
if o.config.EventsData == nil || !o.IsPrivilegedMember(o.MemberIdentity()) {
return result
}
processedPermissions := make(map[string]*struct{})
for _, event := range o.config.EventsData.Events {
if event.TokenPermission == nil || processedPermissions[event.TokenPermission.Id] != nil {
continue
}
processedPermissions[event.TokenPermission.Id] = &struct{}{} // first permission event wins
switch event.Type {
case protobuf.CommunityEvent_COMMUNITY_MEMBER_TOKEN_PERMISSION_CHANGE:
tokenPermission := result[event.TokenPermission.Id]
if tokenPermission != nil {
tokenPermission.State = TokenPermissionUpdatePending
} else {
tokenPermission := NewCommunityTokenPermission(event.TokenPermission)
tokenPermission.State = TokenPermissionAdditionPending
result[event.TokenPermission.Id] = tokenPermission
}
case protobuf.CommunityEvent_COMMUNITY_MEMBER_TOKEN_PERMISSION_DELETE:
tokenPermission := result[event.TokenPermission.Id]
if tokenPermission != nil {
tokenPermission.State = TokenPermissionRemovalPending
}
default:
}
}
return result
}
func (o *Community) TokenPermissions() map[string]*CommunityTokenPermission {
o.mutex.Lock()
defer o.mutex.Unlock()
return o.tokenPermissions()
}
func (o *Community) HasTokenPermissions() bool {
return o.config.CommunityDescription.TokenPermissions != nil && len(o.config.CommunityDescription.TokenPermissions) > 0
o.mutex.Lock()
defer o.mutex.Unlock()
return len(o.tokenPermissions()) > 0
}
func (o *Community) ChannelHasTokenPermissions(chatID string) bool {
if !o.HasTokenPermissions() {
return false
}
o.mutex.Lock()
defer o.mutex.Unlock()
for _, tokenPermission := range o.TokenPermissions() {
for _, tokenPermission := range o.tokenPermissions() {
if includes(tokenPermission.ChatIds, chatID) {
return true
}
@ -1337,8 +1382,8 @@ func (o *Community) ChannelHasTokenPermissions(chatID string) bool {
return false
}
func TokenPermissionsByType(permissions map[string]*protobuf.CommunityTokenPermission, permissionType protobuf.CommunityTokenPermission_Type) []*protobuf.CommunityTokenPermission {
result := make([]*protobuf.CommunityTokenPermission, 0)
func TokenPermissionsByType(permissions map[string]*CommunityTokenPermission, permissionType protobuf.CommunityTokenPermission_Type) []*CommunityTokenPermission {
result := make([]*CommunityTokenPermission, 0)
for _, tokenPermission := range permissions {
if tokenPermission.Type == permissionType {
result = append(result, tokenPermission)
@ -1347,22 +1392,24 @@ func TokenPermissionsByType(permissions map[string]*protobuf.CommunityTokenPermi
return result
}
func (o *Community) tokenPermissionByID(ID string) *protobuf.CommunityTokenPermission {
permissions := o.config.CommunityDescription.TokenPermissions
if permissions == nil {
return nil
}
return permissions[ID]
func (o *Community) tokenPermissionByID(ID string) *CommunityTokenPermission {
return o.tokenPermissions()[ID]
}
func (o *Community) TokenPermissionsByType(permissionType protobuf.CommunityTokenPermission_Type) []*protobuf.CommunityTokenPermission {
return TokenPermissionsByType(o.TokenPermissions(), permissionType)
func (o *Community) TokenPermissionByID(ID string) *CommunityTokenPermission {
o.mutex.Lock()
defer o.mutex.Unlock()
return o.tokenPermissionByID(ID)
}
func (o *Community) ChannelTokenPermissionsByType(channelID string, permissionType protobuf.CommunityTokenPermission_Type) []*protobuf.CommunityTokenPermission {
permissions := make([]*protobuf.CommunityTokenPermission, 0)
for _, tokenPermission := range o.TokenPermissions() {
func (o *Community) TokenPermissionsByType(permissionType protobuf.CommunityTokenPermission_Type) []*CommunityTokenPermission {
return TokenPermissionsByType(o.tokenPermissions(), permissionType)
}
func (o *Community) ChannelTokenPermissionsByType(channelID string, permissionType protobuf.CommunityTokenPermission_Type) []*CommunityTokenPermission {
permissions := make([]*CommunityTokenPermission, 0)
for _, tokenPermission := range o.tokenPermissions() {
if tokenPermission.Type == permissionType && includes(tokenPermission.ChatIds, channelID) {
permissions = append(permissions, tokenPermission)
}
@ -1387,57 +1434,80 @@ func (o *Community) UpsertTokenPermission(tokenPermission *protobuf.CommunityTok
o.mutex.Lock()
defer o.mutex.Unlock()
if !(o.IsControlNode() || o.hasPermissionToSendTokenPermissionCommunityEvent(protobuf.CommunityEvent_COMMUNITY_MEMBER_TOKEN_PERMISSION_CHANGE, tokenPermission.Type)) {
return nil, ErrNotAuthorized
}
changes, err := o.upsertTokenPermission(tokenPermission)
if err != nil {
return nil, err
}
if o.IsControlNode() {
changes, err := o.upsertTokenPermission(tokenPermission)
if err != nil {
return nil, err
}
o.updateEncrypted()
o.increaseClock()
} else {
return changes, nil
}
if o.hasPermissionToSendTokenPermissionCommunityEvent(protobuf.CommunityEvent_COMMUNITY_MEMBER_TOKEN_PERMISSION_CHANGE, tokenPermission.Type) {
existed := o.tokenPermissionByID(tokenPermission.Id) != nil
err := o.addNewCommunityEvent(o.ToCommunityTokenPermissionChangeCommunityEvent(tokenPermission))
if err != nil {
return nil, err
}
permission := NewCommunityTokenPermission(tokenPermission)
changes := o.emptyCommunityChanges()
if existed {
permission.State = TokenPermissionUpdatePending
changes.TokenPermissionsModified[tokenPermission.Id] = permission
} else {
permission.State = TokenPermissionAdditionPending
changes.TokenPermissionsAdded[tokenPermission.Id] = permission
}
return changes, nil
}
return changes, nil
return nil, ErrNotAuthorized
}
func (o *Community) DeleteTokenPermission(permissionID string) (*CommunityChanges, error) {
o.mutex.Lock()
defer o.mutex.Unlock()
permission, exists := o.config.CommunityDescription.TokenPermissions[permissionID]
tokenPermission, exists := o.config.CommunityDescription.TokenPermissions[permissionID]
if !exists {
return nil, ErrTokenPermissionNotFound
}
if !(o.IsControlNode() || o.hasPermissionToSendTokenPermissionCommunityEvent(protobuf.CommunityEvent_COMMUNITY_MEMBER_TOKEN_PERMISSION_DELETE, permission.Type)) {
return nil, ErrNotAuthorized
}
changes, err := o.deleteTokenPermission(permissionID)
if err != nil {
return nil, err
}
if o.IsControlNode() {
o.updateEncrypted()
o.increaseClock()
} else {
err := o.addNewCommunityEvent(o.ToCommunityTokenPermissionDeleteCommunityEvent(permission))
changes, err := o.deleteTokenPermission(permissionID)
if err != nil {
return nil, err
}
o.updateEncrypted()
o.increaseClock()
return changes, nil
}
return changes, nil
if o.hasPermissionToSendTokenPermissionCommunityEvent(protobuf.CommunityEvent_COMMUNITY_MEMBER_TOKEN_PERMISSION_DELETE, tokenPermission.Type) {
err := o.addNewCommunityEvent(o.ToCommunityTokenPermissionDeleteCommunityEvent(tokenPermission))
if err != nil {
return nil, err
}
permission := NewCommunityTokenPermission(tokenPermission)
permission.State = TokenPermissionRemovalPending
changes := o.emptyCommunityChanges()
changes.TokenPermissionsModified[permission.Id] = permission
return changes, nil
}
return nil, ErrNotAuthorized
}
func (o *Community) VerifyGrantSignature(data []byte) (*protobuf.Grant, error) {
@ -1989,9 +2059,9 @@ func (o *Community) upsertTokenPermission(permission *protobuf.CommunityTokenPer
changes := o.emptyCommunityChanges()
if existed {
changes.TokenPermissionsModified[permission.Id] = permission
changes.TokenPermissionsModified[permission.Id] = NewCommunityTokenPermission(permission)
} else {
changes.TokenPermissionsAdded[permission.Id] = permission
changes.TokenPermissionsAdded[permission.Id] = NewCommunityTokenPermission(permission)
}
return changes, nil
@ -2007,7 +2077,7 @@ func (o *Community) deleteTokenPermission(permissionID string) (*CommunityChange
changes := o.emptyCommunityChanges()
changes.TokenPermissionsRemoved[permissionID] = permission
changes.TokenPermissionsRemoved[permissionID] = NewCommunityTokenPermission(permission)
return changes, nil
}

View File

@ -17,9 +17,9 @@ type CommunityChanges struct {
MembersAdded map[string]*protobuf.CommunityMember `json:"membersAdded"`
MembersRemoved map[string]*protobuf.CommunityMember `json:"membersRemoved"`
TokenPermissionsAdded map[string]*protobuf.CommunityTokenPermission `json:"tokenPermissionsAdded"`
TokenPermissionsModified map[string]*protobuf.CommunityTokenPermission `json:"tokenPermissionsModified"`
TokenPermissionsRemoved map[string]*protobuf.CommunityTokenPermission `json:"tokenPermissionsRemoved"`
TokenPermissionsAdded map[string]*CommunityTokenPermission `json:"tokenPermissionsAdded"`
TokenPermissionsModified map[string]*CommunityTokenPermission `json:"tokenPermissionsModified"`
TokenPermissionsRemoved map[string]*CommunityTokenPermission `json:"tokenPermissionsRemoved"`
ChatsRemoved map[string]*protobuf.CommunityChat `json:"chatsRemoved"`
ChatsAdded map[string]*protobuf.CommunityChat `json:"chatsAdded"`
@ -46,9 +46,9 @@ func EmptyCommunityChanges() *CommunityChanges {
MembersAdded: make(map[string]*protobuf.CommunityMember),
MembersRemoved: make(map[string]*protobuf.CommunityMember),
TokenPermissionsAdded: make(map[string]*protobuf.CommunityTokenPermission),
TokenPermissionsModified: make(map[string]*protobuf.CommunityTokenPermission),
TokenPermissionsRemoved: make(map[string]*protobuf.CommunityTokenPermission),
TokenPermissionsAdded: make(map[string]*CommunityTokenPermission),
TokenPermissionsModified: make(map[string]*CommunityTokenPermission),
TokenPermissionsRemoved: make(map[string]*CommunityTokenPermission),
ChatsRemoved: make(map[string]*protobuf.CommunityChat),
ChatsAdded: make(map[string]*protobuf.CommunityChat),
@ -79,7 +79,35 @@ func (c *CommunityChanges) HasMemberLeft(identity string) bool {
return ok
}
func EvaluateCommunityChanges(origin, modified *protobuf.CommunityDescription) *CommunityChanges {
func EvaluateCommunityChanges(origin, modified *Community) *CommunityChanges {
changes := evaluateCommunityChangesByDescription(origin.Description(), modified.Description())
originTokenPermissions := origin.tokenPermissions()
modifiedTokenPermissions := modified.tokenPermissions()
// Check for modified or removed token permissions
for id, originPermission := range originTokenPermissions {
if modifiedPermission := modifiedTokenPermissions[id]; modifiedPermission != nil {
if !modifiedPermission.Equals(originPermission) {
changes.TokenPermissionsModified[id] = modifiedPermission
}
} else {
changes.TokenPermissionsRemoved[id] = originPermission
}
}
// Check for added token permissions
for id, permission := range modifiedTokenPermissions {
if _, ok := originTokenPermissions[id]; !ok {
changes.TokenPermissionsAdded[id] = permission
}
}
changes.Community = modified
return changes
}
func evaluateCommunityChangesByDescription(origin, modified *protobuf.CommunityDescription) *CommunityChanges {
changes := EmptyCommunityChanges()
// Check for new members at the org level
@ -229,19 +257,5 @@ func EvaluateCommunityChanges(origin, modified *protobuf.CommunityDescription) *
}
}
// Check for removed token permissions
for id, permission := range origin.TokenPermissions {
if _, ok := modified.TokenPermissions[id]; !ok {
changes.TokenPermissionsRemoved[id] = permission
}
}
// Check for added token permissions
for id, permission := range modified.TokenPermissions {
if _, ok := origin.TokenPermissions[id]; !ok {
changes.TokenPermissionsAdded[id] = permission
}
}
return changes
}

View File

@ -44,7 +44,7 @@ func EvaluateCommunityEncryptionKeyActions(origin, modified *Community) *Encrypt
}
}
changes := EvaluateCommunityChanges(origin.Description(), modified.Description())
changes := EvaluateCommunityChanges(origin, modified)
result := &EncryptionKeyActions{
CommunityKeyAction: *evaluateCommunityLevelEncryptionKeyAction(origin, modified, changes),
@ -89,7 +89,7 @@ func evaluateChannelLevelEncryptionKeyActions(origin, modified *Community, chang
return &result
}
func evaluateEncryptionKeyAction(originPermissions, modifiedPermissions []*protobuf.CommunityTokenPermission, allMembers, membersAdded, membersRemoved map[string]*protobuf.CommunityMember) *EncryptionKeyAction {
func evaluateEncryptionKeyAction(originPermissions, modifiedPermissions []*CommunityTokenPermission, allMembers, membersAdded, membersRemoved map[string]*protobuf.CommunityMember) *EncryptionKeyAction {
result := &EncryptionKeyAction{
ActionType: EncryptionKeyNone,
Members: map[string]*protobuf.CommunityMember{},

View File

@ -181,43 +181,34 @@ func (o *Community) ToAddTokenMetadataCommunityEvent(tokenMetadata *protobuf.Com
}
}
func (o *Community) UpdateCommunityByEvents(communityEventMessage *CommunityEventsMessage) (*CommunityChanges, error) {
func (o *Community) UpdateCommunityByEvents(communityEventMessage *CommunityEventsMessage) error {
o.mutex.Lock()
defer o.mutex.Unlock()
// Validate that EventsBaseCommunityDescription was signed by the control node
description, err := validateAndGetEventsMessageCommunityDescription(communityEventMessage.EventsBaseCommunityDescription, o.config.ID)
if err != nil {
return nil, err
return err
}
if description.Clock != o.config.CommunityDescription.Clock {
return nil, ErrInvalidCommunityEventClock
return ErrInvalidCommunityEventClock
}
// Create a deep copy of current community so we can update CommunityDescription by new admin events
copy := o.CreateDeepCopy()
// Merge community admin events to existing community. Admin events must be stored to the db
// Merge community events to existing community. Community events must be stored to the db
// during saving the community
o.mergeCommunityEvents(communityEventMessage)
copy.config.CommunityDescription = description
copy.config.CommunityDescriptionProtocolMessage = communityEventMessage.EventsBaseCommunityDescription
copy.config.EventsData = o.config.EventsData
o.config.CommunityDescription = description
o.config.CommunityDescriptionProtocolMessage = communityEventMessage.EventsBaseCommunityDescription
// Update the copy of the CommunityDescription by community events
err = copy.updateCommunityDescriptionByEvents()
err = o.updateCommunityDescriptionByEvents()
if err != nil {
return nil, err
return err
}
// Evaluate `CommunityChanges` data by searching a difference between `CommunityDescription`
// from the DB and `CommunityDescription` patched by community events
changes := EvaluateCommunityChanges(o.config.CommunityDescription, copy.config.CommunityDescription)
changes.Community = copy
return changes, nil
return nil
}
func (o *Community) updateCommunityDescriptionByEvents() error {
@ -246,15 +237,19 @@ func (o *Community) updateCommunityDescriptionByCommunityEvent(communityEvent Co
o.config.CommunityDescription.Tags = communityEvent.CommunityConfig.Tags
case protobuf.CommunityEvent_COMMUNITY_MEMBER_TOKEN_PERMISSION_CHANGE:
_, err := o.upsertTokenPermission(communityEvent.TokenPermission)
if err != nil {
return err
if o.IsControlNode() {
_, err := o.upsertTokenPermission(communityEvent.TokenPermission)
if err != nil {
return err
}
}
case protobuf.CommunityEvent_COMMUNITY_MEMBER_TOKEN_PERMISSION_DELETE:
_, err := o.deleteTokenPermission(communityEvent.TokenPermission.Id)
if err != nil {
return err
if o.IsControlNode() {
_, err := o.deleteTokenPermission(communityEvent.TokenPermission.Id)
if err != nil {
return err
}
}
case protobuf.CommunityEvent_COMMUNITY_CATEGORY_CREATE:

View File

@ -0,0 +1,65 @@
package communities
import (
"reflect"
"github.com/status-im/status-go/protocol/protobuf"
)
type TokenPermissionState uint8
const (
TokenPermissionApproved TokenPermissionState = iota
TokenPermissionAdditionPending
TokenPermissionUpdatePending
TokenPermissionRemovalPending
)
type CommunityTokenPermission struct {
*protobuf.CommunityTokenPermission
State TokenPermissionState `json:"state,omitempty"`
}
func NewCommunityTokenPermission(base *protobuf.CommunityTokenPermission) *CommunityTokenPermission {
return &CommunityTokenPermission{
CommunityTokenPermission: base,
State: TokenPermissionApproved,
}
}
func (p *CommunityTokenPermission) Equals(other *CommunityTokenPermission) bool {
if p.Id != other.Id ||
p.Type != other.Type ||
len(p.TokenCriteria) != len(other.TokenCriteria) ||
len(p.ChatIds) != len(other.ChatIds) ||
p.IsPrivate != other.IsPrivate ||
p.State != other.State {
return false
}
for i := range p.TokenCriteria {
if !compareTokenCriteria(p.TokenCriteria[i], other.TokenCriteria[i]) {
return false
}
}
return reflect.DeepEqual(p.ChatIds, other.ChatIds)
}
func compareTokenCriteria(a, b *protobuf.TokenCriteria) bool {
if a == nil && b == nil {
return true
}
if a == nil || b == nil {
return false
}
return a.Type == b.Type &&
a.Symbol == b.Symbol &&
a.Name == b.Name &&
a.Amount == b.Amount &&
a.EnsPattern == b.EnsPattern &&
a.Decimals == b.Decimals &&
reflect.DeepEqual(a.ContractAddresses, b.ContractAddresses) &&
reflect.DeepEqual(a.TokenIds, b.TokenIds)
}

View File

@ -1350,6 +1350,7 @@ func (m *Manager) handleCommunityDescriptionMessageCommon(community *Community,
if err != nil {
return nil, err
}
community.config.EventsData = nil
err = m.persistence.SaveCommunity(community)
if err != nil {
@ -1433,9 +1434,11 @@ func (m *Manager) HandleCommunityEventsMessage(signer *ecdsa.PublicKey, message
return nil, errors.New("user has not permissions to send events")
}
originCommunity := community.CreateDeepCopy()
eventsMessage.Events = m.validateAndFilterEvents(community, eventsMessage.Events)
changes, err := community.UpdateCommunityByEvents(eventsMessage)
err = community.UpdateCommunityByEvents(eventsMessage)
if err != nil {
if err == ErrInvalidCommunityEventClock && community.IsControlNode() {
m.publish(&Subscription{
@ -1447,7 +1450,7 @@ func (m *Manager) HandleCommunityEventsMessage(signer *ecdsa.PublicKey, message
return nil, err
}
additionalCommunityResponse, err := m.handleAdditionalAdminChanges(changes.Community)
additionalCommunityResponse, err := m.handleAdditionalAdminChanges(community)
if err != nil {
return nil, err
}
@ -1456,30 +1459,30 @@ func (m *Manager) HandleCommunityEventsMessage(signer *ecdsa.PublicKey, message
return nil, err
}
// Control node cerifies community events and publish changes
// all other nodes only apply changes to the community
if changes.Community.IsControlNode() {
changes.Community.increaseClock()
err = m.persistence.SaveCommunity(changes.Community)
// Control node applies events and publish updated CommunityDescription
if community.IsControlNode() {
community.config.EventsData = nil // clear events, they are already applied
community.increaseClock()
err = m.persistence.SaveCommunity(community)
if err != nil {
return nil, err
}
m.publish(&Subscription{Community: changes.Community})
m.publish(&Subscription{Community: community})
} else {
err = m.persistence.SaveCommunity(changes.Community)
err = m.persistence.SaveCommunity(community)
if err != nil {
return nil, err
}
err := m.persistence.SaveCommunityEvents(changes.Community)
err := m.persistence.SaveCommunityEvents(community)
if err != nil {
return nil, err
}
}
return &CommunityResponse{
Community: changes.Community,
Changes: changes,
Community: community,
Changes: EvaluateCommunityChanges(originCommunity, community),
RequestsToJoin: additionalCommunityResponse.RequestsToJoin,
}, nil
}
@ -2320,7 +2323,7 @@ func calculateChainIDsSet(accountsAndChainIDs []*AccountChainIDsCombination, req
// 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) checkPermissions(permissions []*protobuf.CommunityTokenPermission, accountsAndChainIDs []*AccountChainIDsCombination, shortcircuit bool) (*CheckPermissionsResponse, error) {
func (m *Manager) checkPermissions(permissions []*CommunityTokenPermission, accountsAndChainIDs []*AccountChainIDsCombination, shortcircuit bool) (*CheckPermissionsResponse, error) {
response := &CheckPermissionsResponse{
Satisfied: false,
@ -2661,7 +2664,7 @@ type CheckChannelViewAndPostPermissionsResult struct {
Permissions map[string]*PermissionTokenCriteriaResult `json:"permissions"`
}
func (m *Manager) checkChannelPermissions(viewOnlyPermissions []*protobuf.CommunityTokenPermission, viewAndPostPermissions []*protobuf.CommunityTokenPermission, accountsAndChainIDs []*AccountChainIDsCombination, shortcircuit bool) (*CheckChannelPermissionsResponse, error) {
func (m *Manager) checkChannelPermissions(viewOnlyPermissions []*CommunityTokenPermission, viewAndPostPermissions []*CommunityTokenPermission, accountsAndChainIDs []*AccountChainIDsCombination, shortcircuit bool) (*CheckChannelPermissionsResponse, error) {
response := &CheckChannelPermissionsResponse{
ViewOnlyPermissions: &CheckChannelViewOnlyPermissionsResult{
@ -4477,7 +4480,7 @@ func revealedAccountsToAccountsAndChainIDsCombination(revealedAccounts []*protob
return accountsAndChainIDs
}
func (m *Manager) accountsHasPrivilegedPermission(privilegedPermissions []*protobuf.CommunityTokenPermission, accounts []*AccountChainIDsCombination) bool {
func (m *Manager) accountsHasPrivilegedPermission(privilegedPermissions []*CommunityTokenPermission, accounts []*AccountChainIDsCombination) bool {
if len(privilegedPermissions) > 0 {
permissionResponse, err := m.checkPermissions(privilegedPermissions, accounts, true)
if err != nil {
@ -4551,7 +4554,7 @@ func (m *Manager) GetRevealedAddresses(communityID types.HexBytes, memberPk stri
return m.persistence.GetRequestToJoinRevealedAddresses(requestID)
}
func (m *Manager) ReevaluatePrivelegedMember(community *Community, tokenPermissions []*protobuf.CommunityTokenPermission,
func (m *Manager) ReevaluatePrivelegedMember(community *Community, tokenPermissions []*CommunityTokenPermission,
accountsAndChainIDs []*AccountChainIDsCombination, memberPubKey *ecdsa.PublicKey,
privilegedRole protobuf.CommunityMember_Roles, alreadyHasPrivilegedRole bool) (bool, error) {
@ -4672,7 +4675,7 @@ func (m *Manager) HandleCommunityTokensMetadata(community *Community) error {
return nil
}
func getPrivilegesLevel(chainID uint64, tokenAddress string, tokenPermissions map[string]*protobuf.CommunityTokenPermission) community_token.PrivilegesLevel {
func getPrivilegesLevel(chainID uint64, tokenAddress string, tokenPermissions map[string]*CommunityTokenPermission) community_token.PrivilegesLevel {
for _, permission := range tokenPermissions {
if permission.Type == protobuf.CommunityTokenPermission_BECOME_TOKEN_MASTER || permission.Type == protobuf.CommunityTokenPermission_BECOME_TOKEN_OWNER {
for _, tokenCriteria := range permission.TokenCriteria {

View File

@ -187,11 +187,13 @@ func (s *ManagerSuite) TestRetrieveTokens() {
},
}
var permissions = []*protobuf.CommunityTokenPermission{
&protobuf.CommunityTokenPermission{
Id: "some-id",
Type: protobuf.CommunityTokenPermission_BECOME_MEMBER,
TokenCriteria: tokenCriteria,
var permissions = []*CommunityTokenPermission{
&CommunityTokenPermission{
CommunityTokenPermission: &protobuf.CommunityTokenPermission{
Id: "some-id",
Type: protobuf.CommunityTokenPermission_BECOME_MEMBER,
TokenCriteria: tokenCriteria,
},
},
}
@ -234,11 +236,13 @@ func (s *ManagerSuite) TestRetrieveCollectibles() {
},
}
var permissions = []*protobuf.CommunityTokenPermission{
&protobuf.CommunityTokenPermission{
Id: "some-id",
Type: protobuf.CommunityTokenPermission_BECOME_MEMBER,
TokenCriteria: tokenCriteria,
var permissions = []*CommunityTokenPermission{
&CommunityTokenPermission{
CommunityTokenPermission: &protobuf.CommunityTokenPermission{
Id: "some-id",
Type: protobuf.CommunityTokenPermission_BECOME_MEMBER,
TokenCriteria: tokenCriteria,
},
},
}
@ -905,8 +909,8 @@ func (s *ManagerSuite) TestCheckChannelPermissions_NoPermissions() {
},
}
var viewOnlyPermissions = make([]*protobuf.CommunityTokenPermission, 0)
var viewAndPostPermissions = make([]*protobuf.CommunityTokenPermission, 0)
var viewOnlyPermissions = make([]*CommunityTokenPermission, 0)
var viewAndPostPermissions = make([]*CommunityTokenPermission, 0)
tm.setResponse(chainID, accountChainIDsCombination[0].Address, gethcommon.HexToAddress(contractAddresses[chainID]), 0)
resp, err := m.checkChannelPermissions(viewOnlyPermissions, viewAndPostPermissions, accountChainIDsCombination, false)
@ -946,16 +950,18 @@ func (s *ManagerSuite) TestCheckChannelPermissions_ViewOnlyPermissions() {
},
}
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 viewOnlyPermissions = []*CommunityTokenPermission{
&CommunityTokenPermission{
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)
var viewAndPostPermissions = make([]*CommunityTokenPermission, 0)
tm.setResponse(chainID, accountChainIDsCombination[0].Address, gethcommon.HexToAddress(contractAddresses[chainID]), 0)
resp, err := m.checkChannelPermissions(viewOnlyPermissions, viewAndPostPermissions, accountChainIDsCombination, false)
@ -1004,16 +1010,18 @@ func (s *ManagerSuite) TestCheckChannelPermissions_ViewAndPostPermissions() {
},
}
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 viewAndPostPermissions = []*CommunityTokenPermission{
&CommunityTokenPermission{
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)
var viewOnlyPermissions = make([]*CommunityTokenPermission, 0)
tm.setResponse(chainID, accountChainIDsCombination[0].Address, gethcommon.HexToAddress(contractAddresses[chainID]), 0)
resp, err := m.checkChannelPermissions(viewOnlyPermissions, viewAndPostPermissions, accountChainIDsCombination, false)
@ -1063,12 +1071,14 @@ func (s *ManagerSuite) TestCheckChannelPermissions_ViewAndPostPermissionsCombina
},
}
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"},
var viewOnlyPermissions = []*CommunityTokenPermission{
&CommunityTokenPermission{
CommunityTokenPermission: &protobuf.CommunityTokenPermission{
Id: "some-id",
Type: protobuf.CommunityTokenPermission_CAN_VIEW_CHANNEL,
TokenCriteria: viewOnlyTokenCriteria,
ChatIds: []string{"test-channel-id", "test-channel-id-2"},
},
},
}
@ -1087,12 +1097,14 @@ func (s *ManagerSuite) TestCheckChannelPermissions_ViewAndPostPermissionsCombina
},
}
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"},
var viewAndPostPermissions = []*CommunityTokenPermission{
&CommunityTokenPermission{
CommunityTokenPermission: &protobuf.CommunityTokenPermission{
Id: "some-id",
Type: protobuf.CommunityTokenPermission_CAN_VIEW_CHANNEL,
TokenCriteria: viewAndPostTokenCriteria,
ChatIds: []string{"test-channel-id", "test-channel-id-2"},
},
},
}

View File

@ -14,7 +14,7 @@ func CalculateRequestID(publicKey string, communityID types.HexBytes) types.HexB
return crypto.Keccak256([]byte(idString))
}
func ExtractTokenCriteria(permissions []*protobuf.CommunityTokenPermission) (erc20TokenCriteria map[uint64]map[string]*protobuf.TokenCriteria, erc721TokenCriteria map[uint64]map[string]*protobuf.TokenCriteria, ensTokenCriteria []string) {
func ExtractTokenCriteria(permissions []*CommunityTokenPermission) (erc20TokenCriteria map[uint64]map[string]*protobuf.TokenCriteria, erc721TokenCriteria map[uint64]map[string]*protobuf.TokenCriteria, ensTokenCriteria []string) {
erc20TokenCriteria = make(map[uint64]map[string]*protobuf.TokenCriteria)
erc721TokenCriteria = make(map[uint64]map[string]*protobuf.TokenCriteria)
ensTokenCriteria = make([]string, 0)

View File

@ -277,34 +277,44 @@ func createTestPermissionRequest(community *communities.Community, pType protobu
}
func createTokenPermission(base CommunityEventsTestsInterface, community *communities.Community, request *requests.CreateCommunityTokenPermission) (string, *requests.CreateCommunityTokenPermission) {
checkTokenPermissionCreation := func(response *MessengerResponse) error {
modifiedCommmunity, err := getModifiedCommunity(response, community.IDString())
if err != nil {
return err
}
if len(modifiedCommmunity.TokenPermissionsByType(request.Type)) == 0 {
return errors.New("new token permission was not found")
}
return nil
}
response, err := base.GetEventSender().CreateCommunityTokenPermission(request)
s := base.GetSuite()
s.Require().NoError(err)
s.Require().Nil(checkTokenPermissionCreation(response))
s.Require().Len(response.CommunityChanges, 1)
s.Require().Len(response.CommunityChanges[0].TokenPermissionsAdded, 1)
checkClientsReceivedAdminEvent(base, WaitCommunityCondition, checkTokenPermissionCreation)
addedPermission := func() *communities.CommunityTokenPermission {
for _, permission := range response.CommunityChanges[0].TokenPermissionsAdded {
return permission
}
return nil
}()
s.Require().NotNil(addedPermission)
// Permission added by event must be in pending state
s.Require().Equal(communities.TokenPermissionAdditionPending, addedPermission.State)
var tokenPermissionID string
for tokenPermissionID = range response.CommunityChanges[0].TokenPermissionsAdded {
break
responseHasApprovedTokenPermission := func(r *MessengerResponse) bool {
if len(r.Communities()) == 0 {
return false
}
receivedPermission := r.Communities()[0].TokenPermissionByID(addedPermission.Id)
return receivedPermission != nil && receivedPermission.State == communities.TokenPermissionApproved
}
s.Require().NotEqual(tokenPermissionID, "")
// Control node receives community event & approves it
_, err = WaitOnMessengerResponse(base.GetControlNode(), responseHasApprovedTokenPermission, "community with approved permission not found")
s.Require().NoError(err)
return tokenPermissionID, request
// Member receives updated community description
_, err = WaitOnMessengerResponse(base.GetMember(), responseHasApprovedTokenPermission, "community with approved permission not found")
s.Require().NoError(err)
// EventSender receives updated community description
_, err = WaitOnMessengerResponse(base.GetEventSender(), responseHasApprovedTokenPermission, "community with approved permission not found")
s.Require().NoError(err)
return addedPermission.Id, request
}
func createTestTokenPermission(base CommunityEventsTestsInterface, community *communities.Community, pType protobuf.CommunityTokenPermission_Type) (string, *requests.CreateCommunityTokenPermission) {
@ -314,54 +324,79 @@ func createTestTokenPermission(base CommunityEventsTestsInterface, community *co
func editTokenPermission(base CommunityEventsTestsInterface, community *communities.Community, request *requests.EditCommunityTokenPermission) {
s := base.GetSuite()
checkTokenPermissionEdit := func(response *MessengerResponse) error {
modifiedCommmunity, err := getModifiedCommunity(response, community.IDString())
if err != nil {
return err
}
assertCheckTokenPermissionEdited(s, modifiedCommmunity, request.CreateCommunityTokenPermission.Type)
return nil
}
response, err := base.GetEventSender().EditCommunityTokenPermission(request)
s.Require().NoError(err)
s.Require().Nil(checkTokenPermissionEdit(response))
s.Require().Len(response.CommunityChanges, 1)
s.Require().Len(response.CommunityChanges[0].TokenPermissionsModified, 1)
checkClientsReceivedAdminEvent(base, WaitCommunityCondition, checkTokenPermissionEdit)
}
editedPermission := response.CommunityChanges[0].TokenPermissionsModified[request.PermissionID]
s.Require().NotNil(editedPermission)
// Permission edited by event must be in pending state
s.Require().Equal(communities.TokenPermissionUpdatePending, editedPermission.State)
func assertCheckTokenPermissionEdited(s *suite.Suite, community *communities.Community, pType protobuf.CommunityTokenPermission_Type) {
permissions := community.TokenPermissionsByType(pType)
s.Require().Len(permissions, 1)
s.Require().Len(permissions[0].TokenCriteria, 1)
s.Require().Equal(permissions[0].TokenCriteria[0].Type, protobuf.CommunityTokenType_ERC20)
s.Require().Equal(permissions[0].TokenCriteria[0].Symbol, "UPDATED")
s.Require().Equal(permissions[0].TokenCriteria[0].Amount, "200")
s.Require().Equal(permissions[0].TokenCriteria[0].Decimals, uint64(18))
permissionSatisfyRequest := func(p *communities.CommunityTokenPermission) bool {
return request.Type == p.Type &&
request.TokenCriteria[0].Symbol == p.TokenCriteria[0].Symbol &&
request.TokenCriteria[0].Amount == p.TokenCriteria[0].Amount &&
request.TokenCriteria[0].Decimals == p.TokenCriteria[0].Decimals
}
s.Require().True(permissionSatisfyRequest(editedPermission))
responseHasApprovedEditedTokenPermission := func(r *MessengerResponse) bool {
if len(r.Communities()) == 0 {
return false
}
receivedPermission := r.Communities()[0].TokenPermissionByID(editedPermission.Id)
return receivedPermission != nil && receivedPermission.State == communities.TokenPermissionApproved &&
permissionSatisfyRequest(receivedPermission)
}
// Control node receives community event & approves it
_, err = WaitOnMessengerResponse(base.GetControlNode(), responseHasApprovedEditedTokenPermission, "community with approved permission not found")
s.Require().NoError(err)
// Member receives updated community description
_, err = WaitOnMessengerResponse(base.GetMember(), responseHasApprovedEditedTokenPermission, "community with approved permission not found")
s.Require().NoError(err)
// EventSender receives updated community description
_, err = WaitOnMessengerResponse(base.GetEventSender(), responseHasApprovedEditedTokenPermission, "community with approved permission not found")
s.Require().NoError(err)
}
func deleteTokenPermission(base CommunityEventsTestsInterface, community *communities.Community, request *requests.DeleteCommunityTokenPermission) {
checkTokenPermissionDeleted := func(response *MessengerResponse) error {
modifiedCommmunity, err := getModifiedCommunity(response, community.IDString())
if err != nil {
return err
}
if modifiedCommmunity.HasTokenPermissions() {
return errors.New("token permission was not deleted")
}
return nil
}
response, err := base.GetEventSender().DeleteCommunityTokenPermission(request)
s := base.GetSuite()
s.Require().NoError(err)
s.Require().Nil(checkTokenPermissionDeleted(response))
s.Require().Len(response.CommunityChanges, 1)
s.Require().Len(response.CommunityChanges[0].TokenPermissionsModified, 1)
checkClientsReceivedAdminEvent(base, WaitCommunityCondition, checkTokenPermissionDeleted)
removedPermission := response.CommunityChanges[0].TokenPermissionsModified[request.PermissionID]
s.Require().NotNil(removedPermission)
// Permission removed by event must be in pending state
s.Require().Equal(communities.TokenPermissionRemovalPending, removedPermission.State)
responseHasNoTokenPermission := func(r *MessengerResponse) bool {
if len(r.Communities()) == 0 {
return false
}
return r.Communities()[0].TokenPermissionByID(removedPermission.Id) == nil
}
// Control node receives community event & approves it
_, err = WaitOnMessengerResponse(base.GetControlNode(), responseHasNoTokenPermission, "community with approved permission not found")
s.Require().NoError(err)
// Member receives updated community description
_, err = WaitOnMessengerResponse(base.GetMember(), responseHasNoTokenPermission, "community with approved permission not found")
s.Require().NoError(err)
// EventSender receives updated community description
_, err = WaitOnMessengerResponse(base.GetEventSender(), responseHasNoTokenPermission, "community with approved permission not found")
s.Require().NoError(err)
}
func assertCheckTokenPermissionCreated(s *suite.Suite, community *communities.Community, pType protobuf.CommunityTokenPermission_Type) {

View File

@ -1001,7 +1001,7 @@ func (s *MessengerCommunitiesTokenPermissionsSuite) testReevaluateMemberPrivileg
s.advertiseCommunityTo(community, s.alice)
var tokenPermission *protobuf.CommunityTokenPermission
var tokenPermission *communities.CommunityTokenPermission
for _, tokenPermission = range community.TokenPermissions() {
break
}
@ -1110,8 +1110,8 @@ func (s *MessengerCommunitiesTokenPermissionsSuite) testReevaluateMemberPrivileg
s.advertiseCommunityTo(community, s.alice)
var tokenPermission *protobuf.CommunityTokenPermission
var tokenMemberPermission *protobuf.CommunityTokenPermission
var tokenPermission *communities.CommunityTokenPermission
var tokenMemberPermission *communities.CommunityTokenPermission
for _, permission := range community.TokenPermissions() {
if permission.Type == protobuf.CommunityTokenPermission_BECOME_MEMBER {
tokenMemberPermission = permission