diff --git a/data/aclstatebuilder.go b/data/aclstatebuilder.go index 0f42b9a2..2dbe7e5d 100644 --- a/data/aclstatebuilder.go +++ b/data/aclstatebuilder.go @@ -33,8 +33,7 @@ func (sb *ACLStateBuilder) Build() (*ACLState, error) { return state, err } -func (sb *ACLStateBuilder) Init( - tree *Tree) error { +func (sb *ACLStateBuilder) Init(tree *Tree) error { root := tree.Root() if !root.IsSnapshot { return fmt.Errorf("root should always be a snapshot") diff --git a/data/acltreebuilder.go b/data/acltreebuilder.go index 1308ec43..a5f994e7 100644 --- a/data/acltreebuilder.go +++ b/data/acltreebuilder.go @@ -128,7 +128,7 @@ func (tb *ACLTreeBuilder) getACLHeads(heads []string) (aclTreeHeads []string, er } precedingHeads, err := tb.getPrecedingACLHeads(head) if err != nil { - return nil, err + continue } for _, aclHead := range precedingHeads { diff --git a/data/document.go b/data/document.go index 908479d3..2888f862 100644 --- a/data/document.go +++ b/data/document.go @@ -2,11 +2,15 @@ package data import ( "fmt" + "github.com/anytypeio/go-anytype-infrastructure-experiments/data/pb" "github.com/anytypeio/go-anytype-infrastructure-experiments/data/threadmodels" + "github.com/anytypeio/go-anytype-infrastructure-experiments/util/slice" + "github.com/gogo/protobuf/proto" ) type AccountData struct { Identity string + SignKey threadmodels.SigningPrivKey EncKey threadmodels.EncryptionPrivKey } @@ -33,6 +37,12 @@ const ( UpdateResultRebuild ) +type CreateChangePayload struct { + ChangesData proto.Marshaler + ACLData *pb.ACLChangeACLData + Id string // TODO: this is just for testing, because id should be created automatically from content +} + func NewDocument( thread threadmodels.Thread, stateProvider InitialStateProvider, @@ -53,6 +63,59 @@ func NewDocument( } } +func (d *Document) Create(payload *CreateChangePayload) error { + // TODO: add snapshot creation logic + marshalled, err := payload.ChangesData.Marshal() + if err != nil { + return err + } + + encrypted, err := d.docContext.aclState.userReadKeys[d.docContext.aclState.currentReadKeyHash]. + Encrypt(marshalled) + if err != nil { + return err + } + + aclChange := &pb.ACLChange{ + TreeHeadIds: d.docContext.fullTree.Heads(), + AclHeadIds: d.getACLHeads(), + SnapshotBaseId: d.docContext.fullTree.RootId(), + AclData: payload.ACLData, + ChangesData: encrypted, + CurrentReadKeyHash: d.docContext.aclState.currentReadKeyHash, + Timestamp: 0, + Identity: d.accountData.Identity, + } + + // TODO: add CID creation logic based on content + ch := NewChange(payload.Id, aclChange) + ch.DecryptedDocumentChange = marshalled + + fullMarshalledChange, err := proto.Marshal(aclChange) + if err != nil { + return err + } + signature, err := d.accountData.SignKey.Sign(fullMarshalledChange) + if err != nil { + return err + } + d.docContext.fullTree.AddFast(ch) + + err = d.thread.AddChange(&threadmodels.RawChange{ + Payload: marshalled, + Signature: signature, + Id: payload.Id, + }) + if err != nil { + return err + } + + d.thread.SetHeads([]string{ch.Id}) + d.thread.SetMaybeHeads([]string{ch.Id}) + + return nil +} + func (d *Document) Update(changes ...*threadmodels.RawChange) (DocumentState, UpdateResult, error) { var treeChanges []*Change @@ -133,6 +196,35 @@ func (d *Document) Build() (DocumentState, error) { return d.build(false) } +// TODO: this should not be the responsibility of Document, move it somewhere else after testing +func (d *Document) getACLHeads() []string { + var aclTreeHeads []string + for _, head := range d.docContext.fullTree.Heads() { + if slice.FindPos(aclTreeHeads, head) != -1 { // do not scan known heads + continue + } + precedingHeads := d.getPrecedingACLHeads(head) + + for _, aclHead := range precedingHeads { + if slice.FindPos(aclTreeHeads, aclHead) != -1 { + continue + } + aclTreeHeads = append(aclTreeHeads, aclHead) + } + } + return aclTreeHeads +} + +func (d *Document) getPrecedingACLHeads(head string) []string { + headChange := d.docContext.fullTree.attached[head] + + if headChange.Content.GetAclData() != nil { + return []string{head} + } else { + return headChange.Content.AclHeadIds + } +} + func (d *Document) build(fromStart bool) (DocumentState, error) { d.treeBuilder.Init() d.aclTreeBuilder.Init() @@ -143,6 +235,7 @@ func (d *Document) build(fromStart bool) (DocumentState, error) { return nil, err } + // TODO: remove this from context as this is used only to validate snapshot d.docContext.aclTree, err = d.aclTreeBuilder.Build() if err != nil { return nil, err diff --git a/data/documentstatebuilder.go b/data/documentstatebuilder.go index 241c3acc..c0cb55a9 100644 --- a/data/documentstatebuilder.go +++ b/data/documentstatebuilder.go @@ -64,6 +64,7 @@ func (d *documentStateBuilder) build() (s DocumentState, err error) { } func (d *documentStateBuilder) appendFrom(fromId string, init DocumentState) (s DocumentState, err error) { + // TODO: we should do something like state copy probably s = init d.tree.Iterate(fromId, func(c *Change) (isContinue bool) { if c.Id == fromId { diff --git a/data/threadbuilder/userjoinexampleupdate.yml b/data/threadbuilder/userjoinexampleupdate.yml new file mode 100644 index 00000000..80f4fcda --- /dev/null +++ b/data/threadbuilder/userjoinexampleupdate.yml @@ -0,0 +1,133 @@ +thread: + author: A +changes: + - id: A.1.1 + identity: A + aclSnapshot: + userStates: + - identity: A + encryptionKey: key.Enc.A + encryptedReadKeys: [key.Read.1] + permission: admin + snapshot: + text: "some text" + aclChanges: + - userAdd: + identity: A + permission: admin + encryptionKey: key.Enc.A + encryptedReadKeys: [key.Read.1] + changes: + - textAppend: + text: "some text" + readKey: key.Read.1 + - id: A.1.2 + identity: A + aclChanges: + - userInvite: + acceptKey: key.Sign.Onetime1 + encryptionKey: key.Enc.Onetime1 + encryptedReadKeys: [key.Read.1] + permissions: writer + - userAdd: + identity: C + permission: reader + encryptionKey: key.Enc.C + encryptedReadKeys: [ key.Read.1 ] + readKey: key.Read.1 + - id: A.1.3 + identity: A + changes: + - textAppend: + text: "second" + readKey: key.Read.1 + - id: B.1.1 + identity: B + aclChanges: + - userJoin: + identity: B + encryptionKey: key.Enc.B + acceptSignature: key.Sign.Onetime1 + inviteId: A.1.2 + encryptedReadKeys: [key.Read.1] + readKey: key.Read.1 + - id: B.1.2 + identity: B + changes: + - textAppend: + text: "first" + readKey: key.Read.1 + - id: C.1.1 + identity: C + changes: + - textAppend: + text: "third" + readKey: key.Read.1 +keys: + Enc: + - A + - B + - C + - D + - Onetime1 + Sign: + - A + - B + - C + - D + - Onetime1 + Read: + - 1 +graph: + - id: A.1.1 + baseSnapshot: A.1.1 + - id: A.1.2 + baseSnapshot: A.1.1 + aclHeads: [A.1.1] + treeHeads: [A.1.1] + - id: B.1.1 + baseSnapshot: A.1.1 + aclHeads: [A.1.2] + treeHeads: [A.1.2] + - id: B.1.2 + baseSnapshot: A.1.1 + aclHeads: [B.1.1] + treeHeads: [B.1.1] + - id: A.1.3 # this should be invalid, because it is based on one of the invalid changes + baseSnapshot: A.1.1 + aclHeads: [B.1.1] + treeHeads: [B.1.2, C.1.1] + - id: C.1.1 # this should be invalid, because C is a reader + baseSnapshot: A.1.1 + aclHeads: [B.1.1] + treeHeads: [B.1.1] +maybeHeads: + - "A.1.3" +updatedChanges: + - id: B.1.3 + identity: B + changes: + - textAppend: + text: "second" + readKey: key.Read.1 + - id: A.1.4 + identity: A + aclChanges: + - userAdd: + identity: D + permission: writer + encryptionKey: key.Enc.D + encryptedReadKeys: [ key.Read.1 ] + changes: + - textAppend: + text: "second" + readKey: key.Read.1 +updatedGraph: + - id: B.1.3 + baseSnapshot: A.1.1 + aclHeads: [ B.1.1 ] + treeHeads: [ B.1.2 ] + - id: A.1.4 + baseSnapshot: A.1.1 + aclHeads: [ B.1.1 ] + treeHeads: [ B.1.3 ]