450 lines
12 KiB
Go
450 lines
12 KiB
Go
package list
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"github.com/anytypeio/go-anytype-infrastructure-experiments/app/logger"
|
|
"github.com/anytypeio/go-anytype-infrastructure-experiments/pkg/acl/aclchanges/aclpb"
|
|
"github.com/anytypeio/go-anytype-infrastructure-experiments/util/keys"
|
|
"github.com/anytypeio/go-anytype-infrastructure-experiments/util/keys/asymmetric/encryptionkey"
|
|
"github.com/anytypeio/go-anytype-infrastructure-experiments/util/keys/asymmetric/signingkey"
|
|
"github.com/anytypeio/go-anytype-infrastructure-experiments/util/keys/symmetric"
|
|
"github.com/gogo/protobuf/proto"
|
|
"go.uber.org/zap"
|
|
"hash/fnv"
|
|
)
|
|
|
|
var log = logger.NewNamed("acllist")
|
|
|
|
var ErrNoSuchUser = errors.New("no such user")
|
|
var ErrFailedToDecrypt = errors.New("failed to decrypt key")
|
|
var ErrUserRemoved = errors.New("user was removed from the document")
|
|
var ErrDocumentForbidden = errors.New("your user was forbidden access to the document")
|
|
var ErrUserAlreadyExists = errors.New("user already exists")
|
|
var ErrNoSuchRecord = errors.New("no such record")
|
|
var ErrInsufficientPermissions = errors.New("insufficient permissions")
|
|
|
|
type UserPermissionPair struct {
|
|
Identity string
|
|
Permission aclpb.ACLChangeUserPermissions
|
|
}
|
|
|
|
type ACLState struct {
|
|
currentReadKeyHash uint64
|
|
userReadKeys map[uint64]*symmetric.Key
|
|
userStates map[string]*aclpb.ACLChangeUserState
|
|
userInvites map[string]*aclpb.ACLChangeUserInvite
|
|
signingPubKeyDecoder keys.Decoder
|
|
encryptionKey encryptionkey.PrivKey
|
|
identity string
|
|
permissionsAtRecord map[string][]UserPermissionPair
|
|
}
|
|
|
|
func newACLStateWithIdentity(
|
|
identity string,
|
|
encryptionKey encryptionkey.PrivKey,
|
|
decoder keys.Decoder) *ACLState {
|
|
return &ACLState{
|
|
identity: identity,
|
|
encryptionKey: encryptionKey,
|
|
userReadKeys: make(map[uint64]*symmetric.Key),
|
|
userStates: make(map[string]*aclpb.ACLChangeUserState),
|
|
userInvites: make(map[string]*aclpb.ACLChangeUserInvite),
|
|
signingPubKeyDecoder: decoder,
|
|
permissionsAtRecord: make(map[string][]UserPermissionPair),
|
|
}
|
|
}
|
|
|
|
func newACLState(decoder keys.Decoder) *ACLState {
|
|
return &ACLState{
|
|
signingPubKeyDecoder: decoder,
|
|
userReadKeys: make(map[uint64]*symmetric.Key),
|
|
userStates: make(map[string]*aclpb.ACLChangeUserState),
|
|
userInvites: make(map[string]*aclpb.ACLChangeUserInvite),
|
|
permissionsAtRecord: make(map[string][]UserPermissionPair),
|
|
}
|
|
}
|
|
|
|
func (st *ACLState) CurrentReadKeyHash() uint64 {
|
|
return st.currentReadKeyHash
|
|
}
|
|
|
|
func (st *ACLState) UserReadKeys() map[uint64]*symmetric.Key {
|
|
return st.userReadKeys
|
|
}
|
|
|
|
func (st *ACLState) PermissionsAtRecord(id string, identity string) (UserPermissionPair, error) {
|
|
permissions, ok := st.permissionsAtRecord[id]
|
|
if !ok {
|
|
return UserPermissionPair{}, ErrNoSuchRecord
|
|
}
|
|
|
|
for _, perm := range permissions {
|
|
if perm.Identity == identity {
|
|
return perm, nil
|
|
}
|
|
}
|
|
return UserPermissionPair{}, ErrNoSuchUser
|
|
}
|
|
|
|
func (st *ACLState) applyRecord(record *aclpb.Record) (err error) {
|
|
// TODO: this should be probably changed
|
|
aclData := &aclpb.ACLChangeACLData{}
|
|
|
|
err = proto.Unmarshal(record.Data, aclData)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
defer func() {
|
|
if err != nil {
|
|
return
|
|
}
|
|
st.currentReadKeyHash = record.CurrentReadKeyHash
|
|
}()
|
|
|
|
return st.applyChangeData(aclData, record.CurrentReadKeyHash, record.Identity)
|
|
}
|
|
|
|
func (st *ACLState) applyChangeAndUpdate(recordWrapper *Record) (err error) {
|
|
change := recordWrapper.Content
|
|
aclData := &aclpb.ACLChangeACLData{}
|
|
|
|
if recordWrapper.ParsedModel != nil {
|
|
aclData = recordWrapper.ParsedModel.(*aclpb.ACLChangeACLData)
|
|
} else {
|
|
err = proto.Unmarshal(change.Data, aclData)
|
|
if err != nil {
|
|
return
|
|
}
|
|
recordWrapper.ParsedModel = aclData
|
|
}
|
|
|
|
err = st.applyChangeData(aclData, recordWrapper.Content.CurrentReadKeyHash, recordWrapper.Content.Identity)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var permissions []UserPermissionPair
|
|
for _, state := range st.userStates {
|
|
permission := UserPermissionPair{
|
|
Identity: state.Identity,
|
|
Permission: state.Permissions,
|
|
}
|
|
permissions = append(permissions, permission)
|
|
}
|
|
st.permissionsAtRecord[recordWrapper.Id] = permissions
|
|
return nil
|
|
}
|
|
|
|
func (st *ACLState) applyChangeData(changeData *aclpb.ACLChangeACLData, hash uint64, identity string) (err error) {
|
|
defer func() {
|
|
if err != nil {
|
|
return
|
|
}
|
|
st.currentReadKeyHash = hash
|
|
}()
|
|
|
|
// we can't check this for the user which is joining, because it will not be in our list
|
|
// the same is for the first change to be added
|
|
skipIdentityCheck := st.isUserJoin(changeData) || (st.currentReadKeyHash == 0 && st.isUserAdd(changeData, identity))
|
|
if !skipIdentityCheck {
|
|
// we check signature when we add this to the Tree, so no need to do it here
|
|
if _, exists := st.userStates[identity]; !exists {
|
|
err = ErrNoSuchUser
|
|
return
|
|
}
|
|
|
|
if !st.hasPermission(identity, aclpb.ACLChange_Admin) {
|
|
err = fmt.Errorf("user %s must have admin permissions", identity)
|
|
return
|
|
}
|
|
}
|
|
|
|
for _, ch := range changeData.GetAclContent() {
|
|
if err = st.applyChangeContent(ch); err != nil {
|
|
log.Info("error while applying changes: %v; ignore", zap.Error(err))
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (st *ACLState) applyChangeContent(ch *aclpb.ACLChangeACLContentValue) error {
|
|
switch {
|
|
case ch.GetUserPermissionChange() != nil:
|
|
return st.applyUserPermissionChange(ch.GetUserPermissionChange())
|
|
case ch.GetUserAdd() != nil:
|
|
return st.applyUserAdd(ch.GetUserAdd())
|
|
case ch.GetUserRemove() != nil:
|
|
return st.applyUserRemove(ch.GetUserRemove())
|
|
case ch.GetUserInvite() != nil:
|
|
return st.applyUserInvite(ch.GetUserInvite())
|
|
case ch.GetUserJoin() != nil:
|
|
return st.applyUserJoin(ch.GetUserJoin())
|
|
case ch.GetUserConfirm() != nil:
|
|
return st.applyUserConfirm(ch.GetUserConfirm())
|
|
default:
|
|
return fmt.Errorf("unexpected change type: %v", ch)
|
|
}
|
|
}
|
|
|
|
func (st *ACLState) applyUserPermissionChange(ch *aclpb.ACLChangeUserPermissionChange) error {
|
|
if _, exists := st.userStates[ch.Identity]; !exists {
|
|
return ErrNoSuchUser
|
|
}
|
|
|
|
st.userStates[ch.Identity].Permissions = ch.Permissions
|
|
return nil
|
|
}
|
|
|
|
func (st *ACLState) applyUserInvite(ch *aclpb.ACLChangeUserInvite) error {
|
|
st.userInvites[ch.InviteId] = ch
|
|
return nil
|
|
}
|
|
|
|
func (st *ACLState) applyUserJoin(ch *aclpb.ACLChangeUserJoin) error {
|
|
invite, exists := st.userInvites[ch.UserInviteId]
|
|
if !exists {
|
|
return fmt.Errorf("no such invite with id %s", ch.UserInviteId)
|
|
}
|
|
|
|
if _, exists = st.userStates[ch.Identity]; exists {
|
|
return ErrUserAlreadyExists
|
|
}
|
|
|
|
// validating signature
|
|
signature := ch.GetAcceptSignature()
|
|
verificationKey, err := st.signingPubKeyDecoder.DecodeFromBytes(invite.AcceptPublicKey)
|
|
if err != nil {
|
|
return fmt.Errorf("public key verifying invite accepts is given in incorrect format: %v", err)
|
|
}
|
|
|
|
rawSignedId, err := st.signingPubKeyDecoder.DecodeFromStringIntoBytes(ch.Identity)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to decode signing identity as bytes")
|
|
}
|
|
|
|
res, err := verificationKey.(signingkey.PubKey).Verify(rawSignedId, signature)
|
|
if err != nil {
|
|
return fmt.Errorf("verification returned error: %w", err)
|
|
}
|
|
if !res {
|
|
return fmt.Errorf("signature is invalid")
|
|
}
|
|
|
|
// if ourselves -> we need to decrypt the read keys
|
|
if st.identity == ch.Identity {
|
|
for _, key := range ch.EncryptedReadKeys {
|
|
key, hash, err := st.decryptReadKeyAndHash(key)
|
|
if err != nil {
|
|
return ErrFailedToDecrypt
|
|
}
|
|
|
|
st.userReadKeys[hash] = key
|
|
}
|
|
}
|
|
|
|
// adding user to the list
|
|
userState := &aclpb.ACLChangeUserState{
|
|
Identity: ch.Identity,
|
|
EncryptionKey: ch.EncryptionKey,
|
|
EncryptedReadKeys: ch.EncryptedReadKeys,
|
|
Permissions: invite.Permissions,
|
|
IsConfirmed: true,
|
|
}
|
|
st.userStates[ch.Identity] = userState
|
|
return nil
|
|
}
|
|
|
|
func (st *ACLState) applyUserAdd(ch *aclpb.ACLChangeUserAdd) error {
|
|
if _, exists := st.userStates[ch.Identity]; exists {
|
|
return ErrUserAlreadyExists
|
|
}
|
|
|
|
st.userStates[ch.Identity] = &aclpb.ACLChangeUserState{
|
|
Identity: ch.Identity,
|
|
EncryptionKey: ch.EncryptionKey,
|
|
Permissions: ch.Permissions,
|
|
EncryptedReadKeys: ch.EncryptedReadKeys,
|
|
}
|
|
|
|
if ch.Identity == st.identity {
|
|
for _, key := range ch.EncryptedReadKeys {
|
|
key, hash, err := st.decryptReadKeyAndHash(key)
|
|
if err != nil {
|
|
return ErrFailedToDecrypt
|
|
}
|
|
|
|
st.userReadKeys[hash] = key
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (st *ACLState) applyUserRemove(ch *aclpb.ACLChangeUserRemove) error {
|
|
if ch.Identity == st.identity {
|
|
return ErrDocumentForbidden
|
|
}
|
|
|
|
if _, exists := st.userStates[ch.Identity]; !exists {
|
|
return ErrNoSuchUser
|
|
}
|
|
|
|
delete(st.userStates, ch.Identity)
|
|
|
|
for _, replace := range ch.ReadKeyReplaces {
|
|
userState, exists := st.userStates[replace.Identity]
|
|
if !exists {
|
|
continue
|
|
}
|
|
|
|
userState.EncryptedReadKeys = append(userState.EncryptedReadKeys, replace.EncryptedReadKey)
|
|
// if this is our identity then we have to decrypt the key
|
|
if replace.Identity == st.identity {
|
|
key, hash, err := st.decryptReadKeyAndHash(replace.EncryptedReadKey)
|
|
if err != nil {
|
|
return ErrFailedToDecrypt
|
|
}
|
|
|
|
st.currentReadKeyHash = hash
|
|
st.userReadKeys[st.currentReadKeyHash] = key
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (st *ACLState) applyUserConfirm(ch *aclpb.ACLChangeUserConfirm) error {
|
|
if _, exists := st.userStates[ch.Identity]; !exists {
|
|
return ErrNoSuchUser
|
|
}
|
|
|
|
userState := st.userStates[ch.Identity]
|
|
userState.IsConfirmed = true
|
|
return nil
|
|
}
|
|
|
|
func (st *ACLState) decryptReadKeyAndHash(msg []byte) (*symmetric.Key, uint64, error) {
|
|
decrypted, err := st.encryptionKey.Decrypt(msg)
|
|
if err != nil {
|
|
return nil, 0, ErrFailedToDecrypt
|
|
}
|
|
|
|
key, err := symmetric.FromBytes(decrypted)
|
|
if err != nil {
|
|
return nil, 0, ErrFailedToDecrypt
|
|
}
|
|
|
|
hasher := fnv.New64()
|
|
hasher.Write(decrypted)
|
|
return key, hasher.Sum64(), nil
|
|
}
|
|
|
|
func (st *ACLState) hasPermission(identity string, permission aclpb.ACLChangeUserPermissions) bool {
|
|
state, exists := st.userStates[identity]
|
|
if !exists {
|
|
return false
|
|
}
|
|
|
|
return state.Permissions == permission
|
|
}
|
|
|
|
func (st *ACLState) isUserJoin(data *aclpb.ACLChangeACLData) bool {
|
|
// if we have a UserJoin, then it should always be the first one applied
|
|
return data.GetAclContent() != nil && data.GetAclContent()[0].GetUserJoin() != nil
|
|
}
|
|
|
|
func (st *ACLState) isUserAdd(data *aclpb.ACLChangeACLData, identity string) bool {
|
|
// if we have a UserAdd, then it should always be the first one applied
|
|
userAdd := data.GetAclContent()[0].GetUserAdd()
|
|
return data.GetAclContent() != nil && userAdd != nil && userAdd.GetIdentity() == identity
|
|
}
|
|
|
|
func (st *ACLState) getPermissionDecreasedUsers(ch *aclpb.ACLChange) (identities []*aclpb.ACLChangeUserPermissionChange) {
|
|
// this should be called after general checks are completed
|
|
if ch.GetAclData().GetAclContent() == nil {
|
|
return nil
|
|
}
|
|
|
|
contents := ch.GetAclData().GetAclContent()
|
|
for _, c := range contents {
|
|
if c.GetUserPermissionChange() != nil {
|
|
content := c.GetUserPermissionChange()
|
|
|
|
currentState := st.userStates[content.Identity]
|
|
// the comparison works in different direction :-)
|
|
if content.Permissions > currentState.Permissions {
|
|
identities = append(identities, &aclpb.ACLChangeUserPermissionChange{
|
|
Identity: content.Identity,
|
|
Permissions: content.Permissions,
|
|
})
|
|
}
|
|
}
|
|
if c.GetUserRemove() != nil {
|
|
content := c.GetUserRemove()
|
|
identities = append(identities, &aclpb.ACLChangeUserPermissionChange{
|
|
Identity: content.Identity,
|
|
Permissions: aclpb.ACLChange_Removed,
|
|
})
|
|
}
|
|
}
|
|
|
|
return identities
|
|
}
|
|
|
|
func (st *ACLState) equal(other *ACLState) bool {
|
|
if st == nil && other == nil {
|
|
return true
|
|
}
|
|
|
|
if st == nil || other == nil {
|
|
return false
|
|
}
|
|
|
|
if st.currentReadKeyHash != other.currentReadKeyHash {
|
|
return false
|
|
}
|
|
|
|
if st.identity != other.identity {
|
|
return false
|
|
}
|
|
|
|
if len(st.userStates) != len(other.userStates) {
|
|
return false
|
|
}
|
|
|
|
for _, st := range st.userStates {
|
|
otherSt, exists := other.userStates[st.Identity]
|
|
if !exists {
|
|
return false
|
|
}
|
|
|
|
if st.Permissions != otherSt.Permissions {
|
|
return false
|
|
}
|
|
|
|
if bytes.Compare(st.EncryptionKey, otherSt.EncryptionKey) != 0 {
|
|
return false
|
|
}
|
|
}
|
|
|
|
if len(st.userInvites) != len(other.userInvites) {
|
|
return false
|
|
}
|
|
|
|
// TODO: add detailed user invites comparison + compare other stuff
|
|
return true
|
|
}
|
|
|
|
func (st *ACLState) GetUserStates() map[string]*aclpb.ACLChangeUserState {
|
|
// TODO: we should provide better API that would not allow to change this map from the outside
|
|
return st.userStates
|
|
}
|
|
|
|
func (st *ACLState) isNodeIdentity() bool {
|
|
return st.identity == ""
|
|
}
|