Syncbase: Set visibility on syncgroup advertisments and update
the advertisements whenever the read ACL of the syncgroup changes.
MultiPart: 1/2
Change-Id: I0ddb28cdf75ff5553902868d5726075c20105a5c
diff --git a/security/pattern.go b/security/pattern.go
index 154f350..3303d1a 100644
--- a/security/pattern.go
+++ b/security/pattern.go
@@ -8,6 +8,7 @@
"fmt"
"regexp"
"strings"
+
"v.io/v23/naming"
)
diff --git a/services/syncbase/.api b/services/syncbase/.api
index bb7b6a8..126e685 100644
--- a/services/syncbase/.api
+++ b/services/syncbase/.api
@@ -7,6 +7,11 @@
pkg syncbase, const BlobFetchStateFetching BlobFetchState
pkg syncbase, const BlobFetchStateLocating BlobFetchState
pkg syncbase, const BlobFetchStatePending BlobFetchState
+pkg syncbase, const DiscoveryAttrDatabaseBlessing ideal-string
+pkg syncbase, const DiscoveryAttrDatabaseName ideal-string
+pkg syncbase, const DiscoveryAttrPeer ideal-string
+pkg syncbase, const DiscoveryAttrSyncgroupBlessing ideal-string
+pkg syncbase, const DiscoveryAttrSyncgroupName ideal-string
pkg syncbase, const NullBlobRef BlobRef
pkg syncbase, const ResolverTypeAppResolves ResolverType
pkg syncbase, const ResolverTypeDefer ResolverType
diff --git a/services/syncbase/service.vdl b/services/syncbase/service.vdl
index 153e2da..0ee0f0b 100644
--- a/services/syncbase/service.vdl
+++ b/services/syncbase/service.vdl
@@ -12,11 +12,13 @@
import (
"time"
- "v.io/v23/security/access"
- "v.io/v23/services/permissions"
+ "v.io/v23/security/access"
+ "v.io/v23/services/permissions"
"v.io/v23/services/watch"
)
+
+
// NOTE(sadovsky): Various methods below may end up needing additional options.
// TODO(sadovsky): Move "DevMode" methods elsewhere, so that they are completely
diff --git a/services/syncbase/syncbase.vdl.go b/services/syncbase/syncbase.vdl.go
index fb4df9a..246c668 100644
--- a/services/syncbase/syncbase.vdl.go
+++ b/services/syncbase/syncbase.vdl.go
@@ -2777,6 +2777,22 @@
const BlobDevTypeLeaf = int32(2) // Blobs migrate from leaves, which have less storage (examples: a camera, phone)
const NullBlobRef = BlobRef("")
+// DiscoveryAttrPeer is the globally unique identifier of the advertised syncbase.
+const DiscoveryAttrPeer = "p"
+
+// DiscoveryAttrSyncgroupName is the name of the advertised syncgroup.
+const DiscoveryAttrSyncgroupName = "s"
+
+// DiscoveryAttrSyncgroupBlessing is the blessing of the creator of the syncgroup.
+const DiscoveryAttrSyncgroupBlessing = "sb"
+
+// DiscoveryAttrDatabaseName is the name component of a database ID, that this syncgroup is a part of.
+const DiscoveryAttrDatabaseName = "d"
+
+// DiscoveryAttrDatabaseBlessing is the app blessing component of a database ID,
+// that this syncgroup is a part of.
+const DiscoveryAttrDatabaseBlessing = "db"
+
//////////////////////////////////////////////////
// Error definitions
diff --git a/services/syncbase/types.vdl b/services/syncbase/types.vdl
index fa114eb..57b9326 100644
--- a/services/syncbase/types.vdl
+++ b/services/syncbase/types.vdl
@@ -404,3 +404,18 @@
// Note: FromSync is always false for initial state Changes.
FromSync bool
}
+
+// Types of discovery service attributes.
+const (
+ // DiscoveryAttrPeer is the globally unique identifier of the advertised syncbase.
+ DiscoveryAttrPeer = "p"
+ // DiscoveryAttrSyncgroupName is the name of the advertised syncgroup.
+ DiscoveryAttrSyncgroupName = "s"
+ // DiscoveryAttrSyncgroupBlessing is the blessing of the creator of the syncgroup.
+ DiscoveryAttrSyncgroupBlessing = "sb"
+ // DiscoveryAttrDatabaseName is the name component of a database ID, that this syncgroup is a part of.
+ DiscoveryAttrDatabaseName = "d"
+ // DiscoveryAttrDatabaseBlessing is the app blessing component of a database ID,
+ // that this syncgroup is a part of.
+ DiscoveryAttrDatabaseBlessing = "db"
+)
diff --git a/syncbase/.api b/syncbase/.api
index ecea5e1..78e1422 100644
--- a/syncbase/.api
+++ b/syncbase/.api
@@ -1,5 +1,6 @@
pkg syncbase, const DeleteChange ChangeType
pkg syncbase, const PutChange ChangeType
+pkg syncbase, func NewDiscovery(*context.T) (discovery.T, error)
pkg syncbase, func NewService(string) Service
pkg syncbase, func NewValue(*context.T, interface{}) (*Value, error)
pkg syncbase, func Prefix(string) PrefixRange
@@ -12,6 +13,8 @@
pkg syncbase, method (*ConflictRow) VDLRead(vdl.Decoder) error
pkg syncbase, method (*ConflictRowSet) VDLRead(vdl.Decoder) error
pkg syncbase, method (*ConflictScanSet) VDLRead(vdl.Decoder) error
+pkg syncbase, method (*Discovery) Advertise(*context.T, *discovery.Advertisement, []security.BlessingPattern) (<-chan struct{}, error)
+pkg syncbase, method (*Discovery) Scan(*context.T, string) (<-chan discovery.Update, error)
pkg syncbase, method (*Resolution) VDLRead(vdl.Decoder) error
pkg syncbase, method (*ResolvedRow) VDLRead(vdl.Decoder) error
pkg syncbase, method (*Value) Get(interface{}) error
@@ -128,6 +131,7 @@
pkg syncbase, type DatabaseHandle interface, GetResumeMarker(*context.T) (watch.ResumeMarker, error)
pkg syncbase, type DatabaseHandle interface, Id() wire.Id
pkg syncbase, type DatabaseHandle interface, ListCollections(*context.T) ([]wire.Id, error)
+pkg syncbase, type Discovery struct
pkg syncbase, type PrefixRange interface { Limit, Prefix, Start }
pkg syncbase, type PrefixRange interface, Limit() string
pkg syncbase, type PrefixRange interface, Prefix() string
diff --git a/syncbase/discovery.go b/syncbase/discovery.go
new file mode 100644
index 0000000..fb7a70c
--- /dev/null
+++ b/syncbase/discovery.go
@@ -0,0 +1,173 @@
+// 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 syncbase
+
+import (
+ "strings"
+
+ "v.io/v23"
+ "v.io/v23/context"
+ "v.io/v23/discovery"
+ "v.io/v23/security"
+)
+
+const visibilityKey = "vis"
+
+// Discovery implements v.io/v23/discovery.T for syncbase based
+// applications.
+// TODO(mattr): Actually this is not syncbase specific. At some
+// point we should just replace the result of v23.NewDiscovery
+// with this.
+type Discovery struct {
+ nhDiscovery discovery.T
+ // TODO(mattr): Add global discovery.
+}
+
+// NewDiscovery creates a new syncbase discovery object.
+func NewDiscovery(ctx *context.T) (discovery.T, error) {
+ nhDiscovery, err := v23.NewDiscovery(ctx)
+ if err != nil {
+ return nil, err
+ }
+ return &Discovery{nhDiscovery: nhDiscovery}, nil
+}
+
+// Scan implements v.io/v23/discovery/T.Scan.
+func (d *Discovery) Scan(ctx *context.T, query string) (<-chan discovery.Update, error) {
+ nhUpdates, err := d.nhDiscovery.Scan(ctx, query)
+ if err != nil {
+ return nil, err
+ }
+
+ // Currently setting visibility on the neighborhood discovery
+ // service turns IBE encryption on. We currently don't have the
+ // infrastructure support for IBE, so that would make our advertisements
+ // unreadable by everyone.
+ // Instead we add the visibility list to the attributes of the advertisement
+ // and filter on the client side. This is a temporary measure until
+ // IBE is set up. See v.io/i/1345.
+ updates := make(chan discovery.Update)
+ go func() {
+ for u := range nhUpdates {
+ patterns := splitPatterns(u.Attribute(visibilityKey))
+ if len(patterns) > 0 && !matchesPatterns(ctx, patterns) {
+ continue
+ }
+ updates <- update{u}
+ }
+ close(updates)
+ }()
+
+ return updates, nil
+}
+
+// Advertise implements v.io/v23/discovery/T.Advertise.
+func (d *Discovery) Advertise(ctx *context.T, ad *discovery.Advertisement, visibility []security.BlessingPattern) (<-chan struct{}, error) {
+ // Currently setting visibility on the neighborhood discovery
+ // service turns IBE encryption on. We currently don't have the
+ // infrastructure support for IBE, so that would make our advertisements
+ // unreadable by everyone.
+ // Instead we add the visibility list to the attributes of the advertisement
+ // and filter on the client side. This is a temporary measure until
+ // IBE is set up. See v.io/i/1345.
+ adCopy := *ad
+ if len(visibility) > 0 {
+ adCopy.Attributes = make(discovery.Attributes, len(ad.Attributes)+1)
+ for k, v := range ad.Attributes {
+ adCopy.Attributes[k] = v
+ }
+ patterns := joinPatterns(visibility)
+ adCopy.Attributes[visibilityKey] = patterns
+ }
+ return d.nhDiscovery.Advertise(ctx, &adCopy, nil)
+}
+
+func matchesPatterns(ctx *context.T, patterns []security.BlessingPattern) bool {
+ p := v23.GetPrincipal(ctx)
+ blessings := p.BlessingStore().PeerBlessings()
+ for _, b := range blessings {
+ names := security.BlessingNames(p, b)
+ for _, pattern := range patterns {
+ if pattern.MatchedBy(names...) {
+ return true
+ }
+ }
+ }
+ return false
+}
+
+// update wraps the discovery.Update to remove the visibility attribute which we add.
+type update struct {
+ discovery.Update
+}
+
+func (u update) Attribute(name string) string {
+ if name == visibilityKey {
+ return ""
+ }
+ return u.Update.Attribute(name)
+}
+
+func (u update) Advertisement() discovery.Advertisement {
+ cp := u.Update.Advertisement()
+ orig := cp.Attributes
+ cp.Attributes = make(discovery.Attributes, len(orig))
+ for k, v := range orig {
+ if k != visibilityKey {
+ cp.Attributes[k] = v
+ }
+ }
+ return cp
+}
+
+// blessingSeparator is used to join multiple blessings into a
+// single string.
+// Note that comma cannot appear in blessings, see:
+// v.io/v23/security/certificate.go
+const blessingsSeparator = ','
+
+// joinPatterns concatenates the elements of a to create a single string.
+// The string can be split again with SplitPatterns.
+func joinPatterns(a []security.BlessingPattern) string {
+ if len(a) == 0 {
+ return ""
+ }
+ if len(a) == 1 {
+ return string(a[0])
+ }
+ n := (len(a) - 1)
+ for i := 0; i < len(a); i++ {
+ n += len(a[i])
+ }
+
+ b := make([]byte, n)
+ bp := copy(b, a[0])
+ for _, s := range a[1:] {
+ b[bp] = blessingsSeparator
+ bp++
+ bp += copy(b[bp:], s)
+ }
+ return string(b)
+}
+
+// splitPatterns splits BlessingPatterns that were joined with
+// JoinBlessingPattern.
+func splitPatterns(patterns string) []security.BlessingPattern {
+ if patterns == "" {
+ return nil
+ }
+ n := strings.Count(patterns, string(blessingsSeparator)) + 1
+ out := make([]security.BlessingPattern, n)
+ last, start := 0, 0
+ for i, r := range patterns {
+ if r == blessingsSeparator {
+ out[last] = security.BlessingPattern(patterns[start:i])
+ last++
+ start = i + 1
+ }
+ }
+ out[last] = security.BlessingPattern(patterns[start:])
+ return out
+}
diff --git a/syncbase/discovery_pattern_test.go b/syncbase/discovery_pattern_test.go
new file mode 100644
index 0000000..1d61faa
--- /dev/null
+++ b/syncbase/discovery_pattern_test.go
@@ -0,0 +1,36 @@
+// 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 syncbase
+
+import (
+ "reflect"
+ "testing"
+
+ "v.io/v23/security"
+)
+
+func TestJoinSplitPatterns(t *testing.T) {
+ cases := []struct {
+ patterns []security.BlessingPattern
+ joined string
+ }{
+ {nil, ""},
+ {[]security.BlessingPattern{"a", "b"}, "a,b"},
+ {[]security.BlessingPattern{"a:b:c", "d:e:f"}, "a:b:c,d:e:f"},
+ {[]security.BlessingPattern{"alpha:one", "alpha:two", "alpha:three"}, "alpha:one,alpha:two,alpha:three"},
+ }
+ for _, c := range cases {
+ if got := joinPatterns(c.patterns); got != c.joined {
+ t.Errorf("%#v, got %q, wanted %q", c.patterns, got, c.joined)
+ }
+ if got := splitPatterns(c.joined); !reflect.DeepEqual(got, c.patterns) {
+ t.Errorf("%q, got %#v, wanted %#v", c.joined, got, c.patterns)
+ }
+ }
+ // Special case, Joining an empty non-nil list results in empty string.
+ if got := joinPatterns([]security.BlessingPattern{}); got != "" {
+ t.Errorf("Joining empty list: got %q, want %q", got, "")
+ }
+}
diff --git a/syncbase/discovery_test.go b/syncbase/discovery_test.go
new file mode 100644
index 0000000..7e9c151
--- /dev/null
+++ b/syncbase/discovery_test.go
@@ -0,0 +1,166 @@
+// 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 syncbase_test
+
+import (
+ "fmt"
+ "reflect"
+ "testing"
+ "time"
+
+ "v.io/v23"
+ "v.io/v23/context"
+ "v.io/v23/discovery"
+ "v.io/v23/security"
+ wire "v.io/v23/services/syncbase"
+ "v.io/v23/syncbase"
+ "v.io/v23/verror"
+ tu "v.io/x/ref/services/syncbase/testutil"
+ "v.io/x/ref/test/testutil"
+)
+
+func TestSyncgroupDiscovery(t *testing.T) {
+ _, ctx, sName, rootp, cleanup := tu.SetupOrDieCustom(
+ "client1", "server", perms("root:client1"))
+ defer cleanup()
+ d := tu.CreateDatabase(t, ctx, syncbase.NewService(sName), "d")
+ collection1 := wire.Id{"v.io:u:sam", "c1"}
+ collection2 := wire.Id{"v.io:u:sam", "c2"}
+ tu.CreateCollection(t, ctx, d, collection1.Name)
+ tu.CreateCollection(t, ctx, d, collection2.Name)
+
+ c1Updates, err := scanAs(ctx, rootp, "client1")
+ if err != nil {
+ panic(err)
+ }
+ c2Updates, err := scanAs(ctx, rootp, "client2")
+ if err != nil {
+ panic(err)
+ }
+
+ sgId := wire.Id{Name: "sg1", Blessing: "b1"}
+ spec := wire.SyncgroupSpec{
+ Description: "test syncgroup sg1",
+ Perms: perms("root:server", "root:client1"),
+ Collections: []wire.Id{collection1},
+ }
+ createSyncgroup(t, ctx, d, sgId, spec, verror.ID(""))
+
+ // First update is for syncbase, and not a specific syncgroup.
+ u := <-c1Updates
+ attrs := u.Advertisement().Attributes
+ peer := attrs[wire.DiscoveryAttrPeer]
+ if peer == "" || len(attrs) != 1 {
+ t.Errorf("Got %v, expected only a peer name.", attrs)
+ }
+ // Client2 should see the same.
+ if err := expect(c2Updates, find, discovery.Attributes{wire.DiscoveryAttrPeer: peer}); err != nil {
+ t.Error(err)
+ }
+
+ sg1Attrs := discovery.Attributes{
+ wire.DiscoveryAttrDatabaseName: "d",
+ wire.DiscoveryAttrDatabaseBlessing: "v.io:a:xyz",
+ wire.DiscoveryAttrSyncgroupName: "sg1",
+ wire.DiscoveryAttrSyncgroupBlessing: "b1",
+ }
+ sg2Attrs := discovery.Attributes{
+ wire.DiscoveryAttrDatabaseName: "d",
+ wire.DiscoveryAttrDatabaseBlessing: "v.io:a:xyz",
+ wire.DiscoveryAttrSyncgroupName: "sg2",
+ wire.DiscoveryAttrSyncgroupBlessing: "b1",
+ }
+
+ // Then we should see an update for the created syncgroup.
+ if err := expect(c1Updates, find, sg1Attrs); err != nil {
+ t.Error(err)
+ }
+
+ // Now update the spec to add client2 to the permissions.
+ spec.Perms = perms("root:server", "root:client1", "root:client2")
+ if err := d.SyncgroupForId(sgId).SetSpec(ctx, spec, ""); err != nil {
+ t.Fatalf("sg.SetSpec failed: %v", err)
+ }
+
+ // Client1 should see a lost and a found message.
+ if err := expect(c1Updates, both, sg1Attrs); err != nil {
+ t.Error(err)
+ }
+ // Client2 should just now see the found message.
+ if err := expect(c2Updates, find, sg1Attrs); err != nil {
+ t.Error(err)
+ }
+
+ // Now create a second syncgroup.
+ sg2Id := wire.Id{Name: "sg2", Blessing: "b1"}
+ spec2 := wire.SyncgroupSpec{
+ Description: "test syncgroup sg2",
+ Perms: perms("root:server", "root:client1", "root:client2"),
+ Collections: []wire.Id{collection2},
+ }
+ createSyncgroup(t, ctx, d, sg2Id, spec2, verror.ID(""))
+
+ // Both clients should see the new syncgroup.
+ if err := expect(c1Updates, find, sg2Attrs); err != nil {
+ t.Error(err)
+ }
+ if err := expect(c2Updates, find, sg2Attrs); err != nil {
+ t.Error(err)
+ }
+
+ spec2.Perms = perms("root:server", "root:client1")
+ if err := d.SyncgroupForId(sg2Id).SetSpec(ctx, spec2, ""); err != nil {
+ t.Fatalf("sg.SetSpec failed: %v", err)
+ }
+ if err := expect(c2Updates, lose, sg2Attrs); err != nil {
+ t.Error(err)
+ }
+}
+
+func scanAs(ctx *context.T, rootp security.Principal, as string) (<-chan discovery.Update, error) {
+ idp := testutil.IDProviderFromPrincipal(rootp)
+ p := testutil.NewPrincipal()
+ if err := idp.Bless(p, as); err != nil {
+ return nil, err
+ }
+ ctx, err := v23.WithPrincipal(ctx, p)
+ if err != nil {
+ return nil, err
+ }
+ dis, err := syncbase.NewDiscovery(ctx)
+ if err != nil {
+ return nil, err
+ }
+ return dis.Scan(ctx, `v.InterfaceName="v.io/x/ref/services/syncbase/server/interfaces/Sync"`)
+}
+
+const (
+ lose = "lose"
+ find = "find"
+ both = "both"
+)
+
+func expect(ch <-chan discovery.Update, typ string, want discovery.Attributes) error {
+ select {
+ case u := <-ch:
+ if (u.IsLost() && typ == find) || (!u.IsLost() && typ == lose) {
+ return fmt.Errorf("IsLost mismatch. Got %v, wanted %v", u, typ)
+ }
+ got := u.Advertisement().Attributes
+ if !reflect.DeepEqual(got, want) {
+ return fmt.Errorf("got %v, want %v", got, want)
+ }
+ if typ == both {
+ typ = lose
+ if u.IsLost() {
+ typ = find
+ }
+ return expect(ch, typ, want)
+ }
+ return nil
+ case <-time.After(2 * time.Second):
+ return fmt.Errorf("timed out")
+ }
+}