diff --git a/pkg/acl/tree/aclstate.go b/pkg/acl/tree/aclstate.go index f2016bb2..85cf3a90 100644 --- a/pkg/acl/tree/aclstate.go +++ b/pkg/acl/tree/aclstate.go @@ -50,11 +50,18 @@ func newACLState() *ACLState { } } -func (st *ACLState) applyChange(change *aclpb.Change) (err error) { +func (st *ACLState) applyChange(changeWrapper *Change) (err error) { + change := changeWrapper.Content aclData := &aclpb.ACLChangeACLData{} - err = proto.Unmarshal(change.ChangesData, aclData) - if err != nil { - return + + if changeWrapper.DecryptedModel != nil { + aclData = changeWrapper.DecryptedModel.(*aclpb.ACLChangeACLData) + } else { + err = proto.Unmarshal(change.ChangesData, aclData) + if err != nil { + return + } + changeWrapper.DecryptedModel = aclData } defer func() { diff --git a/pkg/acl/tree/aclstatebuilder.go b/pkg/acl/tree/aclstatebuilder.go new file mode 100644 index 00000000..681198b7 --- /dev/null +++ b/pkg/acl/tree/aclstatebuilder.go @@ -0,0 +1,79 @@ +package tree + +import ( + "fmt" + "github.com/anytypeio/go-anytype-infrastructure-experiments/pkg/acl/account" + "github.com/anytypeio/go-anytype-infrastructure-experiments/pkg/acl/aclchanges/aclpb" + "github.com/anytypeio/go-anytype-infrastructure-experiments/util/keys/asymmetric/encryptionkey" + "github.com/anytypeio/go-anytype-infrastructure-experiments/util/keys/asymmetric/signingkey" +) + +type aclStateBuilder struct { + tree *Tree + identity string + key encryptionkey.PrivKey + decoder signingkey.PubKeyDecoder +} + +func newACLStateBuilderWithIdentity(decoder signingkey.PubKeyDecoder, accountData *account.AccountData) *aclStateBuilder { + return &aclStateBuilder{ + decoder: decoder, + identity: accountData.Identity, + key: accountData.EncKey, + } +} + +func newACLStateBuilder() *aclStateBuilder { + return &aclStateBuilder{} +} + +func (sb *aclStateBuilder) Init(tree *Tree) error { + sb.tree = tree + return nil +} + +func (sb *aclStateBuilder) Build() (*ACLState, error) { + state, _, err := sb.BuildBefore("") + return state, err +} + +func (sb *aclStateBuilder) BuildBefore(beforeId string) (*ACLState, bool, error) { + var ( + err error + startChange = sb.tree.root + state *ACLState + foundId = false + ) + + if sb.decoder != nil { + state = newACLStateWithIdentity(sb.identity, sb.key, sb.decoder) + } else { + state = newACLState() + } + + if beforeId == startChange.Id { + return state, true, nil + } + + sb.tree.IterateSkip(sb.tree.root.Id, startChange.Id, func(c *Change) (isContinue bool) { + err = state.applyChange(c) + if err != nil { + return false + } + + // the user can't make changes + if !state.hasPermission(c.Content.Identity, aclpb.ACLChange_Writer) && !state.hasPermission(c.Content.Identity, aclpb.ACLChange_Admin) { + err = fmt.Errorf("user %s cannot make changes", c.Content.Identity) + return false + } + + if c.Id == beforeId { + foundId = true + return false + } + + return true + }) + + return state, foundId, err +} diff --git a/pkg/acl/tree/acltree.go b/pkg/acl/tree/acltree.go new file mode 100644 index 00000000..4b658477 --- /dev/null +++ b/pkg/acl/tree/acltree.go @@ -0,0 +1,466 @@ +package tree + +import ( + "context" + "errors" + "github.com/anytypeio/go-anytype-infrastructure-experiments/pkg/acl/account" + "github.com/anytypeio/go-anytype-infrastructure-experiments/pkg/acl/aclchanges/aclpb" + "github.com/anytypeio/go-anytype-infrastructure-experiments/pkg/acl/treestorage" + "github.com/anytypeio/go-anytype-infrastructure-experiments/pkg/acl/treestorage/treepb" + "go.uber.org/zap" + "sync" +) + +type AddResultSummary int + +var ErrTreeWithoutIdentity = errors.New("acl tree is created without identity") + +const ( + AddResultSummaryNothing AddResultSummary = iota + AddResultSummaryAppend + AddResultSummaryRebuild +) + +type AddResult struct { + OldHeads []string + Heads []string + Added []*aclpb.RawChange + // TODO: add summary for changes + Summary AddResultSummary +} + +type TreeUpdateListener interface { + Update(tree ACLTree) + Rebuild(tree ACLTree) +} + +type NoOpListener struct{} + +func (n NoOpListener) Update(tree ACLTree) {} + +func (n NoOpListener) Rebuild(tree ACLTree) {} + +type RWLocker interface { + sync.Locker + RLock() + RUnlock() +} + +var ErrNoCommonSnapshot = errors.New("trees doesn't have a common snapshot") + +type ACLTree interface { + RWLocker + ID() string + Header() *treepb.TreeHeader + ACLState() *ACLState + AddContent(ctx context.Context, f func(builder ChangeBuilder) error) (*aclpb.RawChange, error) + AddRawChanges(ctx context.Context, changes ...*aclpb.RawChange) (AddResult, error) + Heads() []string + Root() *Change + Iterate(func(change *Change) bool) + IterateFrom(string, func(change *Change) bool) + HasChange(string) bool + SnapshotPath() []string + ChangesAfterCommonSnapshot(snapshotPath []string) ([]*aclpb.RawChange, error) + Storage() treestorage.TreeStorage + DebugDump() (string, error) + + Close() error +} + +type aclTree struct { + treeStorage treestorage.TreeStorage + accountData *account.AccountData + updateListener TreeUpdateListener + + id string + header *treepb.TreeHeader + tree *Tree + aclState *ACLState + + treeBuilder *treeBuilder + aclStateBuilder *aclStateBuilder + changeBuilder *changeBuilder + + sync.RWMutex +} + +func BuildACLTreeWithIdentity( + t treestorage.TreeStorage, + acc *account.AccountData, + listener TreeUpdateListener) (ACLTree, error) { + treeBuilder := newTreeBuilder(t, acc.Decoder) + aclStateBuilder := newACLStateBuilderWithIdentity(acc.Decoder, acc) + changeBuilder := newChangeBuilder() + + aclTree := &aclTree{ + treeStorage: t, + accountData: acc, + tree: nil, + aclState: nil, + treeBuilder: treeBuilder, + aclStateBuilder: aclStateBuilder, + changeBuilder: changeBuilder, + updateListener: listener, + } + err := aclTree.rebuildFromStorage() + if err != nil { + return nil, err + } + err = aclTree.removeOrphans() + if err != nil { + return nil, err + } + err = t.SetHeads(aclTree.Heads()) + if err != nil { + return nil, err + } + aclTree.id, err = t.TreeID() + if err != nil { + return nil, err + } + aclTree.header, err = t.Header() + if err != nil { + return nil, err + } + + listener.Rebuild(aclTree) + + return aclTree, nil +} + +func BuildACLTree(t treestorage.TreeStorage) { + // TODO: Add logic for building without identity +} + +func (a *aclTree) removeOrphans() error { + // removing attached or invalid orphans + var toRemove []string + + orphans, err := a.treeStorage.Orphans() + if err != nil { + return err + } + for _, orphan := range orphans { + if _, exists := a.tree.attached[orphan]; exists { + toRemove = append(toRemove, orphan) + } + if _, exists := a.tree.invalidChanges[orphan]; exists { + toRemove = append(toRemove, orphan) + } + } + return a.treeStorage.RemoveOrphans(toRemove...) +} + +func (a *aclTree) rebuildFromStorage() (err error) { + a.treeBuilder.Init() + + a.tree, err = a.treeBuilder.Build(true) + if err != nil { + return err + } + + err = a.aclStateBuilder.Init(a.tree) + if err != nil { + return err + } + + a.aclState, err = a.aclStateBuilder.Build() + if err != nil { + return err + } + + return nil +} + +func (a *aclTree) ID() string { + return a.id +} + +func (a *aclTree) Header() *treepb.TreeHeader { + return a.header +} + +func (a *aclTree) ACLState() *ACLState { + return a.aclState +} + +func (a *aclTree) Storage() treestorage.TreeStorage { + return a.treeStorage +} + +func (a *aclTree) AddContent(ctx context.Context, build func(builder ChangeBuilder) error) (*aclpb.RawChange, error) { + if a.accountData == nil { + return nil, ErrTreeWithoutIdentity + } + + defer func() { + // TODO: should this be called in a separate goroutine to prevent accidental cycles (tree->updater->tree) + a.updateListener.Update(a) + }() + + a.changeBuilder.Init(a.aclState, a.tree, a.accountData) + err := build(a.changeBuilder) + if err != nil { + return nil, err + } + + ch, marshalled, err := a.changeBuilder.BuildAndApply() + if err != nil { + return nil, err + } + a.tree.AddFast(ch) + rawCh := &aclpb.RawChange{ + Payload: marshalled, + Signature: ch.Signature(), + Id: ch.Id, + } + + err = a.treeStorage.AddRawChange(rawCh) + if err != nil { + return nil, err + } + + err = a.treeStorage.SetHeads([]string{ch.Id}) + if err != nil { + return nil, err + } + return rawCh, nil +} + +func (a *aclTree) AddRawChanges(ctx context.Context, rawChanges ...*aclpb.RawChange) (AddResult, error) { + // TODO: make proper error handling, because there are a lot of corner cases where this will break + var err error + var mode Mode + + var changes []*Change // TODO: = addChangesBuf[:0] ... + for _, ch := range rawChanges { + change, err := NewFromRawChange(ch) + // TODO: think what if we will have incorrect signatures on rawChanges, how everything will work + if err != nil { + continue + } + changes = append(changes, change) + } + + defer func() { + if err != nil { + return + } + + err = a.removeOrphans() + if err != nil { + return + } + + err = a.treeStorage.SetHeads(a.tree.Heads()) + if err != nil { + return + } + + switch mode { + case Append: + a.updateListener.Update(a) + case Rebuild: + a.updateListener.Rebuild(a) + default: + break + } + }() + + getAddedChanges := func() []*aclpb.RawChange { + var added []*aclpb.RawChange + for _, ch := range rawChanges { + if _, exists := a.tree.attached[ch.Id]; exists { + added = append(added, ch) + } + } + return added + } + + for _, ch := range changes { + err = a.treeStorage.AddChange(ch) + if err != nil { + return AddResult{}, err + } + err = a.treeStorage.AddOrphans(ch.Id) + if err != nil { + return AddResult{}, err + } + } + + prevHeads := a.tree.Heads() + mode = a.tree.Add(changes...) + switch mode { + case Nothing: + return AddResult{ + OldHeads: prevHeads, + Heads: prevHeads, + Summary: AddResultSummaryNothing, + }, nil + + case Rebuild: + err = a.rebuildFromStorage() + if err != nil { + return AddResult{}, err + } + + return AddResult{ + OldHeads: prevHeads, + Heads: a.tree.Heads(), + Added: getAddedChanges(), + Summary: AddResultSummaryRebuild, + }, nil + default: + // just rebuilding the state from start without reloading everything from tree storage + // as an optimization we could've started from current heads, but I didn't implement that + a.aclState, err = a.aclStateBuilder.Build() + if err != nil { + return AddResult{}, err + } + + return AddResult{ + OldHeads: prevHeads, + Heads: a.tree.Heads(), + Added: getAddedChanges(), + Summary: AddResultSummaryAppend, + }, nil + } +} + +func (a *aclTree) Iterate(f func(change *Change) bool) { + a.tree.Iterate(a.tree.RootId(), f) +} + +func (a *aclTree) IterateFrom(s string, f func(change *Change) bool) { + a.tree.Iterate(s, f) +} + +func (a *aclTree) HasChange(s string) bool { + _, attachedExists := a.tree.attached[s] + _, unattachedExists := a.tree.unAttached[s] + _, invalidExists := a.tree.invalidChanges[s] + return attachedExists || unattachedExists || invalidExists +} + +func (a *aclTree) Heads() []string { + return a.tree.Heads() +} + +func (a *aclTree) Root() *Change { + return a.tree.Root() +} + +func (a *aclTree) Close() error { + return nil +} + +func (a *aclTree) SnapshotPath() []string { + // TODO: think about caching this + + var path []string + // TODO: think that the user may have not all of the snapshots locally + currentSnapshotId := a.tree.RootId() + for currentSnapshotId != "" { + sn, err := a.treeBuilder.loadChange(currentSnapshotId) + if err != nil { + break + } + path = append(path, currentSnapshotId) + currentSnapshotId = sn.SnapshotId + } + return path +} + +func (a *aclTree) ChangesAfterCommonSnapshot(theirPath []string) ([]*aclpb.RawChange, error) { + // TODO: think about when the clients will have their full acl tree and thus full snapshots + // but no changes after some of the snapshots + + var ( + isNewDocument = len(theirPath) == 0 + ourPath = a.SnapshotPath() + // by default returning everything we have + commonSnapshot = ourPath[len(ourPath)-1] // TODO: root snapshot, probably it is better to have a specific method in treestorage + err error + ) + + // if this is non-empty request + if !isNewDocument { + commonSnapshot, err = a.commonSnapshotForTwoPaths(ourPath, theirPath) + if err != nil { + return nil, err + } + } + var rawChanges []*aclpb.RawChange + // using custom load function to skip verification step and save raw changes + load := func(id string) (*Change, error) { + raw, err := a.treeStorage.GetChange(context.Background(), id) + if err != nil { + return nil, err + } + + aclChange, err := a.treeBuilder.makeUnverifiedACLChange(raw) + if err != nil { + return nil, err + } + + ch := NewChange(id, aclChange) + rawChanges = append(rawChanges, raw) + return ch, nil + } + // we presume that we have everything after the common snapshot, though this may not be the case in case of clients and only ACL tree changes + log.With( + zap.Strings("heads", a.tree.Heads()), + zap.String("breakpoint", commonSnapshot), + zap.String("id", a.id)). + Debug("getting all changes from common snapshot") + _, err = a.treeBuilder.dfs(a.tree.Heads(), commonSnapshot, load) + if err != nil { + return nil, err + } + if isNewDocument { + // adding snapshot to raw changes + _, err = load(commonSnapshot) + if err != nil { + return nil, err + } + } + log.With( + zap.Int("len(changes)", len(rawChanges)), + zap.String("id", a.id)). + Debug("returning all changes after common snapshot") + + return rawChanges, nil +} + +func (a *aclTree) DebugDump() (string, error) { + return a.tree.Graph(ACLDescriptionParser) +} + +func (a *aclTree) commonSnapshotForTwoPaths(ourPath []string, theirPath []string) (string, error) { + var i int + var j int + log.With(zap.Strings("our path", ourPath), zap.Strings("their path", theirPath)). + Debug("finding common snapshot for two paths") +OuterLoop: + // find starting point from the right + for i = len(ourPath) - 1; i >= 0; i-- { + for j = len(theirPath) - 1; j >= 0; j-- { + // most likely there would be only one comparison, because mostly the snapshot path will start from the root for nodes + if ourPath[i] == theirPath[j] { + break OuterLoop + } + } + } + if i < 0 || j < 0 { + return "", ErrNoCommonSnapshot + } + // find last common element of the sequence moving from right to left + for i >= 0 && j >= 0 { + if ourPath[i] == theirPath[j] { + i-- + j-- + } + } + return ourPath[i+1], nil +} diff --git a/pkg/acl/tree/change.go b/pkg/acl/tree/change.go index 1d232133..9fcd2006 100644 --- a/pkg/acl/tree/change.go +++ b/pkg/acl/tree/change.go @@ -22,12 +22,18 @@ type Change struct { Id string SnapshotId string IsSnapshot bool - DecryptedChange []byte + DecryptedChange []byte // TODO: check if we need it + DecryptedModel interface{} Content *aclpb.Change Sign []byte } +func (ch *Change) ProtoChange() *aclpb.ACLChange { + //TODO implement me + panic("implement me") +} + func (ch *Change) DecryptContents(key *symmetric.Key) error { // if the document is already decrypted if ch.Content.CurrentReadKeyHash == 0 { @@ -65,10 +71,6 @@ func NewChange(id string, ch *aclpb.Change) *Change { } } -func (ch *Change) ProtoChange() *aclpb.Change { - return ch.Content -} - func (ch *Change) DecryptedChangeContent() []byte { return ch.DecryptedChange } diff --git a/pkg/acl/tree/changebuilder.go b/pkg/acl/tree/changebuilder.go new file mode 100644 index 00000000..8ecfaafb --- /dev/null +++ b/pkg/acl/tree/changebuilder.go @@ -0,0 +1,163 @@ +package tree + +import ( + "github.com/anytypeio/go-anytype-infrastructure-experiments/pkg/acl/account" + "github.com/anytypeio/go-anytype-infrastructure-experiments/pkg/acl/aclchanges/aclpb" + "github.com/anytypeio/go-anytype-infrastructure-experiments/util/cid" + "github.com/anytypeio/go-anytype-infrastructure-experiments/util/keys/asymmetric/encryptionkey" + "github.com/anytypeio/go-anytype-infrastructure-experiments/util/keys/symmetric" + "github.com/gogo/protobuf/proto" + "hash/fnv" + "time" +) + +type MarshalledChange = []byte + +type ACLChangeBuilder interface { + UserAdd(identity string, encryptionKey encryptionkey.PubKey, permissions aclpb.ACLChangeUserPermissions) error + AddId(id string) // TODO: this is only for testing + SetMakeSnapshot(bool) // TODO: who should decide this? probably ACLTree so we can delete it +} + +type ChangeBuilder interface { + ACLChangeBuilder + AddChangeContent(marshaler proto.Marshaler) // user code should be responsible for making regular snapshots +} + +type changeBuilder struct { + aclState *ACLState + tree *Tree + acc *account.AccountData + + aclData *aclpb.ACLChangeACLData + changeContent proto.Marshaler + id string + makeSnapshot bool + readKey *symmetric.Key + readKeyHash uint64 +} + +func newChangeBuilder() *changeBuilder { + return &changeBuilder{} +} + +func (c *changeBuilder) Init(state *ACLState, tree *Tree, acc *account.AccountData) { + c.aclState = state + c.tree = tree + c.acc = acc + + c.aclData = &aclpb.ACLChangeACLData{} + // setting read key for further encryption etc + if state.currentReadKeyHash == 0 { + c.readKey, _ = symmetric.NewRandom() + + hasher := fnv.New64() + hasher.Write(c.readKey.Bytes()) + c.readKeyHash = hasher.Sum64() + } else { + c.readKey = c.aclState.userReadKeys[c.aclState.currentReadKeyHash] + c.readKeyHash = c.aclState.currentReadKeyHash + } +} + +func (c *changeBuilder) AddId(id string) { + c.id = id +} + +func (c *changeBuilder) SetMakeSnapshot(b bool) { + c.makeSnapshot = b +} + +func (c *changeBuilder) UserAdd(identity string, encryptionKey encryptionkey.PubKey, permissions aclpb.ACLChangeUserPermissions) error { + var allKeys []*symmetric.Key + if c.aclState.currentReadKeyHash != 0 { + for _, key := range c.aclState.userReadKeys { + allKeys = append(allKeys, key) + } + } else { + allKeys = append(allKeys, c.readKey) + } + + var encryptedKeys [][]byte + for _, k := range allKeys { + res, err := encryptionKey.Encrypt(k.Bytes()) + if err != nil { + return err + } + + encryptedKeys = append(encryptedKeys, res) + } + rawKey, err := encryptionKey.Raw() + if err != nil { + return err + } + ch := &aclpb.ACLChangeACLContentValue{ + Value: &aclpb.ACLChangeACLContentValueValueOfUserAdd{ + UserAdd: &aclpb.ACLChangeUserAdd{ + Identity: identity, + EncryptionKey: rawKey, + EncryptedReadKeys: encryptedKeys, + Permissions: permissions, + }, + }, + } + c.aclData.AclContent = append(c.aclData.AclContent, ch) + return nil +} + +func (c *changeBuilder) BuildAndApply() (*Change, []byte, error) { + aclChange := &aclpb.ACLChange{ + TreeHeadIds: c.tree.Heads(), + AclHeadIds: c.tree.ACLHeads(), + SnapshotBaseId: c.tree.RootId(), + AclData: c.aclData, + CurrentReadKeyHash: c.readKeyHash, + Timestamp: int64(time.Now().Nanosecond()), + Identity: c.acc.Identity, + } + err := c.aclState.applyChange(aclChange) + if err != nil { + return nil, nil, err + } + + if c.makeSnapshot { + c.aclData.AclSnapshot = c.aclState.makeSnapshot() + } + + var marshalled []byte + if c.changeContent != nil { + marshalled, err = c.changeContent.Marshal() + if err != nil { + return nil, nil, err + } + + encrypted, err := c.aclState.userReadKeys[c.aclState.currentReadKeyHash]. + Encrypt(marshalled) + if err != nil { + return nil, nil, err + } + aclChange.ChangesData = encrypted + } + + fullMarshalledChange, err := proto.Marshal(aclChange) + if err != nil { + return nil, nil, err + } + signature, err := c.acc.SignKey.Sign(fullMarshalledChange) + if err != nil { + return nil, nil, err + } + id, err := cid.NewCIDFromBytes(fullMarshalledChange) + if err != nil { + return nil, nil, err + } + ch := NewChange(id, aclChange) + ch.DecryptedDocumentChange = marshalled + ch.Sign = signature + + return ch, fullMarshalledChange, nil +} + +func (c *changeBuilder) AddChangeContent(marshaler proto.Marshaler) { + c.changeContent = marshaler +} diff --git a/pkg/acl/tree/descriptionparser.go b/pkg/acl/tree/descriptionparser.go new file mode 100644 index 00000000..92587c40 --- /dev/null +++ b/pkg/acl/tree/descriptionparser.go @@ -0,0 +1,45 @@ +package tree + +import ( + "fmt" + "github.com/anytypeio/go-anytype-infrastructure-experiments/pkg/acl/aclchanges/aclpb" + "github.com/gogo/protobuf/proto" + "strings" + "unicode" +) + +type DescriptionParser interface { + ParseChange(*Change) ([]string, error) +} + +var ACLDescriptionParser = aclDescriptionParser{} + +type aclDescriptionParser struct{} + +func (a aclDescriptionParser) ParseChange(changeWrapper *Change) (res []string, err error) { + change := changeWrapper.Content + aclData := &aclpb.ACLChangeACLData{} + + if changeWrapper.DecryptedModel != nil { + aclData = changeWrapper.DecryptedModel.(*aclpb.ACLChangeACLData) + } else { + err = proto.Unmarshal(change.ChangesData, aclData) + if err != nil { + return + } + } + + var chSymbs []string + for _, chc := range aclData.AclContent { + tp := fmt.Sprintf("%T", chc.Value) + tp = strings.Replace(tp, "ACLChangeACLContentValueValueOf", "", 1) + res := "" + for _, ts := range tp { + if unicode.IsUpper(ts) { + res += string(ts) + } + } + chSymbs = append(chSymbs, res) + } + return chSymbs, nil +} diff --git a/pkg/acl/tree/tree.go b/pkg/acl/tree/tree.go index 1c6cd1fe..ef8cc23d 100644 --- a/pkg/acl/tree/tree.go +++ b/pkg/acl/tree/tree.go @@ -4,7 +4,6 @@ import ( "bytes" "crypto/md5" "fmt" - "github.com/anytypeio/go-anytype-infrastructure-experiments/util/slice" "sort" ) @@ -289,34 +288,6 @@ func (t *Tree) updateHeads() { sort.Strings(t.metaHeadIds) } -func (t *Tree) ACLHeads() []string { - var aclTreeHeads []string - for _, head := range t.Heads() { - if slice.FindPos(aclTreeHeads, head) != -1 { // do not scan known heads - continue - } - precedingHeads := t.getPrecedingACLHeads(head) - - for _, aclHead := range precedingHeads { - if slice.FindPos(aclTreeHeads, aclHead) != -1 { - continue - } - aclTreeHeads = append(aclTreeHeads, aclHead) - } - } - return aclTreeHeads -} - -func (t *Tree) getPrecedingACLHeads(head string) []string { - headChange := t.attached[head] - - if headChange.Content.GetAclData() != nil { - return []string{head} - } else { - return headChange.Content.AclHeadIds - } -} - func (t *Tree) iterate(start *Change, f func(c *Change) (isContinue bool)) { it := newIterator() defer freeIterator(it) diff --git a/pkg/acl/tree/treebuilder.go b/pkg/acl/tree/treebuilder.go index 44da7c6d..b243c0b4 100644 --- a/pkg/acl/tree/treebuilder.go +++ b/pkg/acl/tree/treebuilder.go @@ -223,8 +223,8 @@ func (tb *treeBuilder) makeVerifiedChange(change *aclpb.RawChange) (aclChange *a return } -func (tb *treeBuilder) makeUnverifiedACLChange(change *aclpb.RawChange) (aclChange *aclpb.ACLChange, err error) { - aclChange = new(aclpb.ACLChange) +func (tb *treeBuilder) makeUnverifiedACLChange(change *aclpb.RawChange) (aclChange *aclpb.Change, err error) { + aclChange = new(aclpb.Change) err = proto.Unmarshal(change.Payload, aclChange) return } diff --git a/pkg/acl/tree/treegraph.go b/pkg/acl/tree/treegraph.go new file mode 100644 index 00000000..67cd42bd --- /dev/null +++ b/pkg/acl/tree/treegraph.go @@ -0,0 +1,11 @@ +//go:build ((!linux && !darwin) || android || ios || nographviz) && !amd64 +// +build !linux,!darwin android ios nographviz +// +build !amd64 + +package tree + +import "fmt" + +func (t *Tree) Graph(parser DescriptionParser) (data []string, err error) { + return "", fmt.Errorf("not supported") +} diff --git a/pkg/acl/tree/treegraph_nix.go b/pkg/acl/tree/treegraph_nix.go new file mode 100644 index 00000000..c506c9b3 --- /dev/null +++ b/pkg/acl/tree/treegraph_nix.go @@ -0,0 +1,121 @@ +//go:build (linux || darwin) && !android && !ios && !nographviz && (amd64 || arm64) +// +build linux darwin +// +build !android +// +build !ios +// +build !nographviz +// +build amd64 arm64 + +package tree + +import ( + "bytes" + "fmt" + "github.com/goccy/go-graphviz" + "github.com/goccy/go-graphviz/cgraph" + "strings" + "time" +) + +func (t *Tree) Graph(parser DescriptionParser) (data string, err error) { + var order = make(map[string]string) + var seq = 0 + t.Iterate(t.RootId(), func(c *Change) (isContinue bool) { + v := order[c.Id] + if v == "" { + order[c.Id] = fmt.Sprint(seq) + } else { + order[c.Id] = fmt.Sprintf("%s,%d", v, seq) + } + seq++ + return true + }) + g := graphviz.New() + defer g.Close() + graph, err := g.Graph() + if err != nil { + return + } + defer func() { + err = graph.Close() + }() + var nodes = make(map[string]*cgraph.Node) + var addChange = func(c *Change) error { + n, e := graph.CreateNode(c.Id) + if e != nil { + return e + } + n.SetStyle(cgraph.FilledNodeStyle) + nodes[c.Id] = n + ord := order[c.Id] + if ord == "" { + ord = "miss" + } + chSymbs, err := parser.ParseChange(c) + if err != nil { + return err + } + + shortId := c.Id + label := fmt.Sprintf("Id: %s\nOrd: %s\nTime: %s\nChanges: %s\n", + shortId, + ord, + time.Unix(c.Content.Timestamp, 0).Format("02.01.06 15:04:05"), + strings.Join(chSymbs, ","), + ) + n.SetLabel(label) + return nil + } + for _, c := range t.attached { + if err = addChange(c); err != nil { + return + } + } + for _, c := range t.unAttached { + if err = addChange(c); err != nil { + return + } + } + var getNode = func(id string) (*cgraph.Node, error) { + if n, ok := nodes[id]; ok { + return n, nil + } + n, err := graph.CreateNode(fmt.Sprintf("%s: not in Tree", id)) + if err != nil { + return nil, err + } + nodes[id] = n + return n, nil + } + var addLinks = func(c *Change) error { + for _, prevId := range c.PreviousIds { + self, e := getNode(c.Id) + if e != nil { + return e + } + prev, e := getNode(prevId) + if e != nil { + return e + } + _, e = graph.CreateEdge("", self, prev) + if e != nil { + return e + } + } + return nil + } + for _, c := range t.attached { + if err = addLinks(c); err != nil { + return + } + } + for _, c := range t.unAttached { + if err = addLinks(c); err != nil { + return + } + } + var buf bytes.Buffer + if err = g.Render(graph, "dot", &buf); err != nil { + return + } + return buf.String(), nil +} diff --git a/pkg/acl/tree/treeiterator.go b/pkg/acl/tree/treeiterator.go new file mode 100644 index 00000000..2bc1db58 --- /dev/null +++ b/pkg/acl/tree/treeiterator.go @@ -0,0 +1,158 @@ +package tree + +import "sync" + +var itPool = &sync.Pool{ + New: func() interface{} { + return &iterator{} + }, +} + +func newIterator() *iterator { + return itPool.Get().(*iterator) +} + +func freeIterator(i *iterator) { + itPool.Put(i) +} + +type iterator struct { + compBuf []*Change + queue []*Change + doneMap map[*Change]struct{} + breakpoint *Change + f func(c *Change) bool +} + +func (i *iterator) iterateSkip(start *Change, skipBefore *Change, f func(c *Change) (isContinue bool)) { + skipping := true + i.iterate(start, func(c *Change) (isContinue bool) { + if skipping && c != skipBefore { + return true + } + skipping = false + return f(c) + }) +} + +func (i *iterator) iterate(start *Change, f func(c *Change) (isContinue bool)) { + if start == nil { + return + } + // reset + i.queue = i.queue[:0] + i.compBuf = i.compBuf[:0] + i.doneMap = make(map[*Change]struct{}) + i.queue = append(i.queue, start) + i.breakpoint = nil + i.f = f + + for len(i.queue) > 0 { + c := i.queue[0] + i.queue = i.queue[1:] + nl := len(c.Next) + if nl == 1 { + if !i.iterateLin(c) { + return + } + if i.breakpoint != nil { + i.toQueue(i.breakpoint) + i.breakpoint = nil + } + } else { + _, done := i.doneMap[c] + if !done { + if !f(c) { + return + } + i.doneMap[c] = struct{}{} + } + if nl != 0 { + for _, next := range c.Next { + i.toQueue(next) + } + } + } + } +} + +func (i *iterator) iterateLin(c *Change) bool { + for len(c.Next) == 1 { + _, done := i.doneMap[c] + if !done { + if !i.f(c) { + return false + } + i.doneMap[c] = struct{}{} + } + + c = c.Next[0] + if len(c.PreviousIds) > 1 { + break + } + } + if len(c.Next) == 0 && len(c.PreviousIds) <= 1 { + if !i.f(c) { + return false + } + i.doneMap[c] = struct{}{} + } else { + i.breakpoint = c + } + + return true +} + +func (i *iterator) comp(c1, c2 *Change) uint8 { + if c1.Id == c2.Id { + return 0 + } + i.compBuf = i.compBuf[:0] + i.compBuf = append(i.compBuf, c1.Next...) + var uniq = make(map[*Change]struct{}) + var appendUniqueToBuf = func(next []*Change) { + for _, n := range next { + if _, ok := uniq[n]; !ok { + i.compBuf = append(i.compBuf, n) + uniq[n] = struct{}{} + } + } + } + var used int + for len(i.compBuf)-used > 0 { + l := len(i.compBuf) - used + for _, n := range i.compBuf[used:] { + delete(uniq, n) + if n.Id == c2.Id { + return 1 + } else { + appendUniqueToBuf(n.Next) + } + } + used += l + } + return 2 +} + +func (i *iterator) toQueue(c *Change) { + var pos = -1 +For: + for idx, qc := range i.queue { + switch i.comp(c, qc) { + // exists + case 0: + return + // + case 1: + pos = idx + break For + } + } + if pos == -1 { + i.queue = append(i.queue, c) + } else if pos == 0 { + i.queue = append([]*Change{c}, i.queue...) + } else { + i.queue = append(i.queue[:pos], append([]*Change{c}, i.queue[pos:]...)...) + } +}