any-sync/data/aclstatebuilder.go
2022-06-28 15:37:09 +02:00

182 lines
5.0 KiB
Go

package data
import (
"fmt"
"github.com/anytypeio/go-anytype-infrastructure-experiments/data/threadmodels"
"github.com/anytypeio/go-anytype-infrastructure-experiments/pb"
)
type ACLStateBuilder struct {
tree *Tree
aclState *ACLState
identity string
key threadmodels.EncryptionPrivKey
}
type decreasedPermissionsParameters struct {
users []*pb.ACLChangeUserPermissionChange
startChange string
}
func NewACLStateBuilder(
tree *Tree,
identity string,
key threadmodels.EncryptionPrivKey,
decoder threadmodels.SigningPubKeyDecoder) (*ACLStateBuilder, error) {
root := tree.Root()
if !root.IsSnapshot {
return nil, fmt.Errorf("root should always be a snapshot")
}
snapshot := root.Content.GetAclData().GetAclSnapshot()
state, err := NewACLStateFromSnapshot(snapshot, identity, key, decoder)
if err != nil {
return nil, fmt.Errorf("could not build aclState from snapshot: %w", err)
}
return &ACLStateBuilder{
tree: tree,
aclState: state,
identity: identity,
key: key,
}, nil
}
func (sb *ACLStateBuilder) Build() (*ACLState, error) {
state, _, err := sb.BuildBefore("")
return state, err
}
// TODO: we can probably have only one state builder, because we can build both at the same time
func (sb *ACLStateBuilder) BuildBefore(beforeId string) (*ACLState, bool, error) {
var (
err error
startChange = sb.tree.root
foundId bool
idSeenMap = make(map[string][]*Change)
decreasedPermissions *decreasedPermissionsParameters
)
idSeenMap[startChange.Content.Identity] = append(idSeenMap[startChange.Content.Identity], startChange)
if startChange.Content.GetChangesData() != nil {
key, exists := sb.aclState.userReadKeys[startChange.Content.CurrentReadKeyHash]
if !exists {
return nil, false, fmt.Errorf("no first snapshot")
}
err = startChange.DecryptContents(key)
if err != nil {
return nil, false, fmt.Errorf("failed to decrypt contents of first snapshot")
}
}
if beforeId == startChange.Id {
return sb.aclState, true, nil
}
for {
// TODO: we should optimize this method to just remember last state of iterator and not iterate from the start and skip if nothing was removed from the tree
sb.tree.iterateSkip(sb.tree.root, startChange, func(c *Change) (isContinue bool) {
defer func() {
if err == nil {
startChange = c
} else if err != ErrDocumentForbidden {
log.Errorf("marking change %s as invalid: %v", c.Id, err)
sb.tree.RemoveInvalidChange(c.Id)
}
}()
// not applying root change
if c.Id == startChange.Id {
return true
}
idSeenMap[c.Content.Identity] = append(idSeenMap[c.Content.Identity], c)
if c.Content.GetAclData() != nil {
err = sb.aclState.ApplyChange(c.Id, c.Content)
if err != nil {
return false
}
// if we have some users who have less permissions now
users := sb.aclState.GetPermissionDecreasedUsers(c.Content)
if len(users) > 0 {
decreasedPermissions = &decreasedPermissionsParameters{
users: users,
startChange: c.Id,
}
return false
}
}
// the user can't make changes
if !sb.aclState.HasPermission(c.Content.Identity, pb.ACLChange_Writer) && !sb.aclState.HasPermission(c.Content.Identity, pb.ACLChange_Admin) {
err = fmt.Errorf("user %s cannot make changes", c.Content.Identity)
return false
}
// decrypting contents on the fly
if c.Content.GetChangesData() != nil {
key, exists := sb.aclState.userReadKeys[c.Content.CurrentReadKeyHash]
if !exists {
err = fmt.Errorf("failed to find key with hash: %d", c.Content.CurrentReadKeyHash)
return false
}
err = c.DecryptContents(key)
if err != nil {
err = fmt.Errorf("failed to decrypt contents for hash: %d", c.Content.CurrentReadKeyHash)
return false
}
}
if c.Id == beforeId {
foundId = true
return false
}
return true
})
// if we have users with decreased permissions
if decreasedPermissions != nil {
var removed bool
validChanges := sb.tree.dfs(decreasedPermissions.startChange)
for _, permChange := range decreasedPermissions.users {
seenChanges := idSeenMap[permChange.Identity]
for _, seen := range seenChanges {
// if we find some invalid changes
if _, exists := validChanges[seen.Id]; !exists {
// if the user didn't have enough permission to make changes
if seen.IsACLChange() || permChange.Permissions > pb.ACLChange_Writer {
removed = true
sb.tree.RemoveInvalidChange(seen.Id)
}
}
}
}
decreasedPermissions = nil
if removed {
// starting from the beginning but with updated tree
return sb.BuildBefore(beforeId)
}
} else if err == nil {
// we can finish the acl state building process
break
}
// the user is forbidden to access the document
if err == ErrDocumentForbidden {
return nil, foundId, err
}
// otherwise we have to continue from the change which we had
err = nil
}
return sb.aclState, foundId, err
}