Three-way merges of access.Permissions as well as tests.
This method is not hooked up anywhere yet.
Change-Id: I5b6c17b37d7127d1d6eddd37f97a8f8fe579beb1
diff --git a/services/syncbase/vsync/cr_permissions.go b/services/syncbase/vsync/cr_permissions.go
new file mode 100644
index 0000000..105f8f3
--- /dev/null
+++ b/services/syncbase/vsync/cr_permissions.go
@@ -0,0 +1,105 @@
+// Copyright 2016 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package vsync
+
+import (
+ "v.io/v23/security"
+ "v.io/v23/security/access"
+ "v.io/x/lib/set"
+)
+
+// resolvePermissions performs fine-grained conflict resolution of Permissions.
+func resolvePermissions(local, remote, ancestor access.Permissions) access.Permissions {
+ result := access.Permissions{}
+ if ancestor == nil {
+ ancestor = access.Permissions{}
+ }
+
+ tagsSet := map[string]bool{}
+ for tag, _ := range local {
+ tagsSet[tag] = true
+ }
+ for tag, _ := range remote {
+ tagsSet[tag] = true
+ }
+ for tag, _ := range tagsSet {
+ in := toBlessingPatterns(resolveSlice(
+ fromBlessingPatterns(local[tag].In),
+ fromBlessingPatterns(remote[tag].In),
+ fromBlessingPatterns(ancestor[tag].In)))
+ notIn := resolveSlice(
+ local[tag].NotIn,
+ remote[tag].NotIn,
+ ancestor[tag].NotIn)
+
+ // TODO(fredq): remove this when it is added to Normalize
+ if len(in) != 0 || len(notIn) != 0 {
+ result[tag] = access.AccessList{In: in, NotIn: notIn}
+ }
+ }
+ return result.Normalize()
+}
+
+// resolveSlice is passed two new versions plus an ancestor and returns the merge of
+// the three by performing the following set calculation:
+// A - (A - L) - (A - R) + (L - A) + (R - A)
+// The resultant set will be the ancestor minus the elements that right and left each removed,
+// plus the elements that the right and left added.
+//
+// Since a side can only express adds and removals (and not, e.g., keep element), it isn't
+// possible for one side to add an element that the other side removed, and vice versa. So if the
+// initial set is {A} and one side adds {B} to it, it is not possible for the other side of
+// the conflict to remove {B} during the conflict, since {B} wasn't yet in the set
+// to be removed.
+func resolveSlice(local, remote, ancestor []string) []string {
+ ls := set.String.FromSlice(local)
+ rs := set.String.FromSlice(remote)
+ as := set.String.FromSlice(ancestor)
+
+ // We may be adding things to ls later, and since it will be nil if local is empty,
+ // let's be sure to allocate a map for it now.
+ if ls == nil {
+ ls = map[string]struct{}{}
+ }
+
+ // ls now has all the local elements in it.
+
+ // Add in the elements from remote that are not in ancestor.
+ for _, el := range remote {
+ if _, exists := as[el]; !exists {
+ ls[el] = struct{}{}
+ }
+ }
+ // Remove the elements that are in ancestor but not in remote.
+ for _, el := range ancestor {
+ if _, exists := rs[el]; !exists {
+ delete(ls, el)
+ }
+ }
+
+ //
+ // Convert the map back to a slice, sort it, and return it.
+ return set.String.ToSlice(ls)
+}
+
+// fromBlessingPatterns takes a slice of BlessingPatterns, which is a type alias of string, and
+// returns a slice of strings, casting each BlessingPattern to a string.
+func fromBlessingPatterns(in []security.BlessingPattern) []string {
+ result := make([]string, len(in))
+ for i, value := range in {
+ result[i] = string(value)
+ }
+ return result
+}
+
+// toBlessingPatterns takes a slice of strings and returns it as a new slice of
+// BlessingPatterns, casting each string to a BlessingPattern.
+func toBlessingPatterns(in []string) []security.BlessingPattern {
+ result := make([]security.BlessingPattern, len(in))
+ for i, value := range in {
+ result[i] = security.BlessingPattern(value)
+ }
+ return result
+}
diff --git a/services/syncbase/vsync/cr_permissions_test.go b/services/syncbase/vsync/cr_permissions_test.go
new file mode 100644
index 0000000..b17b230
--- /dev/null
+++ b/services/syncbase/vsync/cr_permissions_test.go
@@ -0,0 +1,159 @@
+// Copyright 2016 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package vsync
+
+import (
+ "github.com/stretchr/testify/assert"
+ "reflect"
+ "sort"
+ "testing"
+ "v.io/v23/security"
+ "v.io/v23/security/access"
+)
+
+func TestResolvePermissions(t *testing.T) {
+ ancestor := access.Permissions{}
+ left := access.Permissions{}
+ right := access.Permissions{}
+ expected := access.Permissions{}
+
+ ancestor["R"] = access.AccessList{In: []security.BlessingPattern{"alice", "bob"}, NotIn: []string{"bob:bad"}}
+ ancestor["W"] = access.AccessList{In: []security.BlessingPattern{"alice", "bob"}, NotIn: []string{"bob:bad"}}
+
+ left["A"] = access.AccessList{In: []security.BlessingPattern{"alice", "bob"}, NotIn: []string{"bob:bad"}}
+ left["R"] = access.AccessList{In: []security.BlessingPattern{"alice", "bob", "carol"}, NotIn: []string{"bob:bad"}}
+
+ right["R"] = access.AccessList{In: []security.BlessingPattern{"alice", "bob", "don"}, NotIn: []string{"bob:bad"}}
+ right["W"] = access.AccessList{In: []security.BlessingPattern{"eric"}, NotIn: []string{"bob:bad"}}
+ right["Z"] = access.AccessList{In: []security.BlessingPattern{"alice", "bob"}, NotIn: []string{"bob:bad"}}
+
+ expected["A"] = access.AccessList{In: []security.BlessingPattern{"alice", "bob"}, NotIn: []string{"bob:bad"}}
+ expected["R"] = access.AccessList{In: []security.BlessingPattern{"alice", "bob", "carol", "don"}, NotIn: []string{"bob:bad"}}
+ expected["W"] = access.AccessList{In: []security.BlessingPattern{"eric"}, NotIn: nil}
+ expected["Z"] = access.AccessList{In: []security.BlessingPattern{"alice", "bob"}, NotIn: []string{"bob:bad"}}
+
+ result := resolvePermissions(left, right, ancestor)
+ assertPermsEqual(t, expected, result)
+}
+
+func TestResolvePermissionsRemovals(t *testing.T) {
+ ancestor := access.Permissions{}
+ left := access.Permissions{}
+ right := access.Permissions{}
+ expected := access.Permissions{}
+
+ ancestor["R"] = access.AccessList{In: []security.BlessingPattern{"alice", "bob", "carol", "don"}, NotIn: []string{"bob:bad"}}
+ left["R"] = access.AccessList{In: []security.BlessingPattern{"alice", "bob", "carol"}, NotIn: []string{"bob:bad"}}
+ right["R"] = access.AccessList{In: []security.BlessingPattern{"alice", "bob", "don"}, NotIn: []string{"bob:bad"}}
+ expected["R"] = access.AccessList{In: []security.BlessingPattern{"alice", "bob"}, NotIn: []string{"bob:bad"}}
+ result := resolvePermissions(left, right, ancestor)
+ assertPermsEqual(t, expected, result)
+
+ ancestor["R"] = access.AccessList{In: []security.BlessingPattern{"alice", "bob"}, NotIn: []string{"bob:bad"}}
+ left["R"] = access.AccessList{In: []security.BlessingPattern{"alice", "bob", "carol"}, NotIn: []string{"bob:bad"}}
+ right["R"] = access.AccessList{In: []security.BlessingPattern{"bob"}, NotIn: []string{"bob:bad"}}
+ expected["R"] = access.AccessList{In: []security.BlessingPattern{"bob", "carol"}, NotIn: []string{"bob:bad"}}
+ result = resolvePermissions(left, right, ancestor)
+ assertPermsEqual(t, expected, result)
+}
+
+func TestResolvePermissionsDroppedTags(t *testing.T) {
+ ancestor := access.Permissions{}
+ left := access.Permissions{}
+ right := access.Permissions{}
+ expected := access.Permissions{}
+
+ ancestor["R"] = access.AccessList{In: []security.BlessingPattern{"alice", "bob", "carol", "don"}, NotIn: []string{"bob:bad"}}
+ left["R"] = access.AccessList{In: []security.BlessingPattern{"alice", "bob"}, NotIn: []string{"bob:bad"}}
+ right["R"] = access.AccessList{In: []security.BlessingPattern{"carol", "don"}, NotIn: []string{"bob:bad"}}
+ expected["R"] = access.AccessList{In: nil, NotIn: []string{"bob:bad"}}
+ result := resolvePermissions(left, right, ancestor)
+ assertPermsEqual(t, expected, result)
+
+ ancestor["R"] = access.AccessList{In: []security.BlessingPattern{"alice", "bob", "carol", "don"}, NotIn: []string{"bob:bad"}}
+ left["R"] = access.AccessList{In: []security.BlessingPattern{"alice", "bob"}, NotIn: nil}
+ right["R"] = access.AccessList{In: []security.BlessingPattern{"carol", "don"}, NotIn: nil}
+ expected = access.Permissions{}
+ result = resolvePermissions(left, right, ancestor)
+ assertPermsEqual(t, expected, result)
+}
+
+func TestResolvePermissionsNoAncestor(t *testing.T) {
+ var ancestor access.Permissions
+ left := access.Permissions{}
+ right := access.Permissions{}
+ expected := access.Permissions{}
+
+ left["A"] = access.AccessList{In: []security.BlessingPattern{"alice", "bob"}, NotIn: []string{"bob:bad"}}
+ left["R"] = access.AccessList{In: []security.BlessingPattern{"alice", "bob", "carol"}, NotIn: []string{"bob:bad"}}
+ left["W"] = access.AccessList{In: []security.BlessingPattern{"bob"}, NotIn: []string{"bob:bad"}}
+
+ right["R"] = access.AccessList{In: []security.BlessingPattern{"alice", "bob", "don"}, NotIn: []string{"bob:bad"}}
+ right["W"] = access.AccessList{In: []security.BlessingPattern{"alice", "bob", "eric"}, NotIn: []string{"bob:bad"}}
+ right["Z"] = access.AccessList{In: []security.BlessingPattern{"alice", "bob"}, NotIn: []string{"bob:bad"}}
+
+ expected["A"] = access.AccessList{In: []security.BlessingPattern{"alice", "bob"}, NotIn: []string{"bob:bad"}}
+ expected["R"] = access.AccessList{In: []security.BlessingPattern{"alice", "bob", "carol", "don"}, NotIn: []string{"bob:bad"}}
+ expected["W"] = access.AccessList{In: []security.BlessingPattern{"alice", "bob", "eric"}, NotIn: []string{"bob:bad"}}
+ expected["Z"] = access.AccessList{In: []security.BlessingPattern{"alice", "bob"}, NotIn: []string{"bob:bad"}}
+
+ result := resolvePermissions(left, right, ancestor)
+ assertPermsEqual(t, expected, result)
+}
+
+func TestResolveSlice(t *testing.T) {
+ ancestor := []string{"a", "b"}
+ left := []string{"b"}
+ right := []string{"a", "b", "e"}
+ expected := []string{"b", "e"}
+ result := resolveSlice(left, right, ancestor)
+ sort.Strings(expected)
+ sort.Strings(result)
+ assert.Equal(t, expected, result)
+
+ ancestor = []string{"a", "b", "c"}
+ left = []string{"a", "b", "d"}
+ right = []string{"b", "c", "e"}
+ expected = []string{"b", "d", "e"}
+ result = resolveSlice(left, right, ancestor)
+ sort.Strings(expected)
+ sort.Strings(result)
+ assert.Equal(t, expected, result)
+
+ // Empty left.
+ ancestor = []string{"a", "b"}
+ left = nil
+ right = []string{"b", "c", "e"}
+ expected = []string{"c", "e"}
+ result = resolveSlice(left, right, ancestor)
+ sort.Strings(expected)
+ sort.Strings(result)
+ assert.Equal(t, expected, result)
+
+ // Empty right.
+ ancestor = []string{"a", "b"}
+ left = []string{"b", "c", "e"}
+ right = nil
+ expected = []string{"c", "e"}
+ result = resolveSlice(left, right, ancestor)
+ sort.Strings(expected)
+ sort.Strings(result)
+ assert.Equal(t, expected, result)
+
+ // Empty ancestor.
+ ancestor = nil
+ left = []string{"b", "c", "e"}
+ right = []string{"a", "b"}
+ expected = []string{"a", "b", "c", "e"}
+ result = resolveSlice(left, right, ancestor)
+ sort.Strings(expected)
+ sort.Strings(result)
+ assert.Equal(t, expected, result)
+}
+
+func assertPermsEqual(t *testing.T, expected, actual access.Permissions) {
+ assert.Equal(t, expected, actual)
+ assert.True(t, reflect.DeepEqual(expected, actual))
+}
diff --git a/services/syncbase/vsync/initiator.go b/services/syncbase/vsync/initiator.go
index 6c076be..c1f3cd0 100644
--- a/services/syncbase/vsync/initiator.go
+++ b/services/syncbase/vsync/initiator.go
@@ -764,7 +764,7 @@
// a Database update to simply update the Database to the latest value.
//
// * There is a conflict and we call into the app or use a well-known policy to
-// resolve the conflict, resulting in three possibilties: (a) conflict was
+// resolve the conflict, resulting in three possibilities: (a) conflict was
// resolved by picking the local version. In this case, Database need not be
// updated, but a link is added to record the choice. (b) conflict was resolved
// by picking the remote version. In this case, Database is updated with the