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")
+	}
+}