services/security/roled: Add Glob support

This Glob implementation only shows the objects that the caller has
access to. For directories, the caller only sees the ones that contains
at least one object that is accessible.

Change-Id: I3988dd597d7c2976dcd3207739dfd2410732a7fa
diff --git a/services/role/roled/internal/discharger.go b/services/role/roled/internal/discharger.go
index 43b2cd9..0ac7fd9 100644
--- a/services/role/roled/internal/discharger.go
+++ b/services/role/roled/internal/discharger.go
@@ -26,7 +26,9 @@
 
 }
 
-type dischargerImpl struct{}
+type dischargerImpl struct {
+	serverConfig *serverConfig
+}
 
 func (dischargerImpl) Discharge(call rpc.ServerCall, caveat security.Caveat, impetus security.DischargeImpetus) (security.Discharge, error) {
 	details := caveat.ThirdPartyDetails()
@@ -58,3 +60,7 @@
 	}
 	return discharge, nil
 }
+
+func (d *dischargerImpl) GlobChildren__(call rpc.ServerCall) (<-chan string, error) {
+	return globChildren(call.Context(), d.serverConfig)
+}
diff --git a/services/role/roled/internal/dispatcher.go b/services/role/roled/internal/dispatcher.go
index 7081124..1d31cc0 100644
--- a/services/role/roled/internal/dispatcher.go
+++ b/services/role/roled/internal/dispatcher.go
@@ -32,33 +32,37 @@
 // service for the third-party caveats attached to the role blessings returned
 // by the role service.
 func NewDispatcher(configRoot, dischargerLocation string) rpc.Dispatcher {
-	return &dispatcher{configRoot, dischargerLocation}
+	return &dispatcher{&serverConfig{configRoot, dischargerLocation}}
+}
+
+type serverConfig struct {
+	root               string
+	dischargerLocation string
 }
 
 type dispatcher struct {
-	configRoot         string
-	dischargerLocation string
+	config *serverConfig
 }
 
 func (d *dispatcher) Lookup(suffix string) (interface{}, security.Authorizer, error) {
 	if len(suffix) == 0 {
-		return discharger.DischargerServer(&dischargerImpl{}), &openAuthorizer{}, nil
+		return discharger.DischargerServer(&dischargerImpl{d.config}), &openAuthorizer{}, nil
 	}
-	fileName := filepath.Join(d.configRoot, filepath.FromSlash(suffix+".conf"))
-	if !strings.HasPrefix(fileName, d.configRoot) {
+	fileName := filepath.Join(d.config.root, filepath.FromSlash(suffix+".conf"))
+	if !strings.HasPrefix(fileName, d.config.root) {
 		// Guard against ".." in the suffix that could be used to read
 		// files outside of the config root.
 		return nil, nil, verror.New(verror.ErrNoExistOrNoAccess, nil)
 	}
-	config, err := loadExpandedConfig(fileName, nil)
+	roleConfig, err := loadExpandedConfig(fileName, nil)
 	if err != nil && !os.IsNotExist(err) {
 		// The config file exists, but we failed to read it for some
 		// reason. This is likely a server configuration error.
-		vlog.Errorf("loadConfig(%q, %q): %v", d.configRoot, suffix, err)
+		vlog.Errorf("loadConfig(%q, %q): %v", d.config.root, suffix, err)
 		return nil, nil, verror.Convert(verror.ErrInternal, nil, err)
 	}
-	obj := &roleService{role: suffix, config: config, dischargerLocation: d.dischargerLocation}
-	return role.RoleServer(obj), &authorizer{config}, nil
+	obj := &roleService{serverConfig: d.config, role: suffix, roleConfig: roleConfig}
+	return role.RoleServer(obj), &authorizer{roleConfig}, nil
 }
 
 type openAuthorizer struct{}
@@ -72,19 +76,31 @@
 }
 
 func (a *authorizer) Authorize(ctx *context.T) error {
+	if security.GetCall(ctx).Method() == "__Glob" {
+		// The Glob implementation only shows objects that the caller
+		// has access to. So this blanket approval is OK.
+		return nil
+	}
 	if a.config == nil {
 		return verror.New(verror.ErrNoExistOrNoAccess, ctx)
 	}
 	remoteBlessingNames, _ := security.RemoteBlessingNames(ctx)
 
-	for _, pattern := range a.config.Members {
-		if pattern.MatchedBy(remoteBlessingNames...) {
-			return nil
-		}
+	if hasAccess(a.config, remoteBlessingNames) {
+		return nil
 	}
 	return verror.New(verror.ErrNoExistOrNoAccess, ctx)
 }
 
+func hasAccess(c *Config, blessingNames []string) bool {
+	for _, pattern := range c.Members {
+		if pattern.MatchedBy(blessingNames...) {
+			return true
+		}
+	}
+	return false
+}
+
 func loadExpandedConfig(fileName string, seenFiles map[string]struct{}) (*Config, error) {
 	if seenFiles == nil {
 		seenFiles = make(map[string]struct{})
diff --git a/services/role/roled/internal/glob.go b/services/role/roled/internal/glob.go
new file mode 100644
index 0000000..224b5f6
--- /dev/null
+++ b/services/role/roled/internal/glob.go
@@ -0,0 +1,86 @@
+// Copyright 2015 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 internal
+
+import (
+	"os"
+	"path/filepath"
+	"strings"
+
+	"v.io/v23/context"
+	"v.io/v23/security"
+	"v.io/v23/verror"
+)
+
+func globChildren(ctx *context.T, serverConfig *serverConfig) (<-chan string, error) {
+	n := findRoles(ctx, serverConfig.root)
+	suffix := security.GetCall(ctx).Suffix()
+	if len(suffix) > 0 {
+		n = n.find(strings.Split(suffix, "/"), false)
+	}
+	if n == nil {
+		return nil, verror.New(verror.ErrNoExistOrNoAccess, ctx)
+	}
+	ch := make(chan string, len(n.children))
+	for c := range n.children {
+		ch <- c
+	}
+	close(ch)
+	return ch, nil
+}
+
+// findRoles finds all the roles to which the caller has access.
+func findRoles(ctx *context.T, root string) *node {
+	blessingNames, _ := security.RemoteBlessingNames(ctx)
+	tree := newNode()
+	filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
+		if info.IsDir() || !strings.HasSuffix(path, ".conf") {
+			return nil
+		}
+		c, err := loadExpandedConfig(path, nil)
+		if err != nil {
+			return nil
+		}
+		if !hasAccess(c, blessingNames) {
+			return nil
+		}
+		relPath, err := filepath.Rel(root, path)
+		if err != nil {
+			return nil
+		}
+		tree.find(strings.Split(strings.TrimSuffix(relPath, ".conf"), string(filepath.Separator)), true)
+		return nil
+	})
+	return tree
+}
+
+type node struct {
+	children map[string]*node
+}
+
+func newNode() *node {
+	return &node{children: make(map[string]*node)}
+}
+
+func (n *node) find(names []string, create bool) *node {
+	for {
+		if len(names) == 0 {
+			return n
+		}
+		if next, ok := n.children[names[0]]; ok {
+			n = next
+			names = names[1:]
+			continue
+		}
+		if create {
+			nn := newNode()
+			n.children[names[0]] = nn
+			n = nn
+			names = names[1:]
+			continue
+		}
+		return nil
+	}
+}
diff --git a/services/role/roled/internal/role.go b/services/role/roled/internal/role.go
index 7b1f129..ee578d4 100644
--- a/services/role/roled/internal/role.go
+++ b/services/role/roled/internal/role.go
@@ -24,9 +24,9 @@
 )
 
 type roleService struct {
-	role               string
-	config             *Config
-	dischargerLocation string
+	serverConfig *serverConfig
+	role         string
+	roleConfig   *Config
 }
 
 func (i *roleService) SeekBlessings(call rpc.ServerCall) (security.Blessings, error) {
@@ -40,13 +40,17 @@
 		return security.Blessings{}, verror.New(verror.ErrNoAccess, ctx)
 	}
 
-	extensions := extensions(i.config, i.role, members)
-	caveats, err := caveats(ctx, i.config)
+	extensions := extensions(i.roleConfig, i.role, members)
+	caveats, err := caveats(ctx, i.roleConfig)
 	if err != nil {
 		return security.Blessings{}, err
 	}
 
-	return createBlessings(ctx, i.config, v23.GetPrincipal(ctx), extensions, caveats, i.dischargerLocation)
+	return createBlessings(ctx, i.roleConfig, v23.GetPrincipal(ctx), extensions, caveats, i.serverConfig.dischargerLocation)
+}
+
+func (i *roleService) GlobChildren__(call rpc.ServerCall) (<-chan string, error) {
+	return globChildren(call.Context(), i.serverConfig)
 }
 
 // filterNonMembers returns only the blessing names that are authorized members
@@ -58,7 +62,7 @@
 		// blessings. We need to know exactly which names matched.
 		// These names will be used later to construct the role
 		// blessings.
-		for _, pattern := range i.config.Members {
+		for _, pattern := range i.roleConfig.Members {
 			if pattern.MatchedBy(name) {
 				results = append(results, name)
 				break
diff --git a/services/role/roled/internal/role_test.go b/services/role/roled/internal/role_test.go
index 023e15b..4c479e3 100644
--- a/services/role/roled/internal/role_test.go
+++ b/services/role/roled/internal/role_test.go
@@ -40,7 +40,7 @@
 		Members: []security.BlessingPattern{
 			"root/users/user1/_role",
 			"root/users/user2/_role",
-			"root/users/user3", // _role/A implied
+			"root/users/user3", // _role implied
 		},
 		Extend: true,
 	}
@@ -123,6 +123,59 @@
 	}
 }
 
+func TestGlob(t *testing.T) {
+	ctx, shutdown := v23.Init()
+	defer shutdown()
+
+	workdir, err := ioutil.TempDir("", "test-role-server-")
+	if err != nil {
+		t.Fatal("ioutil.TempDir failed: %v", err)
+	}
+	defer os.RemoveAll(workdir)
+	os.Mkdir(filepath.Join(workdir, "sub1"), 0700)
+	os.Mkdir(filepath.Join(workdir, "sub1", "sub2"), 0700)
+	os.Mkdir(filepath.Join(workdir, "sub3"), 0700)
+
+	// Role that user1 has access to.
+	roleAConf := irole.Config{Members: []security.BlessingPattern{"root/user1"}}
+	irole.WriteConfig(t, roleAConf, filepath.Join(workdir, "A.conf"))
+	irole.WriteConfig(t, roleAConf, filepath.Join(workdir, "sub1/B.conf"))
+	irole.WriteConfig(t, roleAConf, filepath.Join(workdir, "sub1/C.conf"))
+	irole.WriteConfig(t, roleAConf, filepath.Join(workdir, "sub1/sub2/D.conf"))
+
+	// Role that user2 has access to.
+	roleBConf := irole.Config{Members: []security.BlessingPattern{"root/user2"}}
+	irole.WriteConfig(t, roleBConf, filepath.Join(workdir, "sub1/sub2/X.conf"))
+
+	root := testutil.NewIDProvider("root")
+	user1 := newPrincipalContext(t, ctx, root, "user1/_role")
+	user2 := newPrincipalContext(t, ctx, root, "user2/_role")
+	user3 := newPrincipalContext(t, ctx, root, "user3/_role")
+	addr := newRoleServer(t, newPrincipalContext(t, ctx, root, "roles"), workdir)
+
+	testcases := []struct {
+		user    *context.T
+		name    string
+		pattern string
+		results []string
+	}{
+		{user1, "", "*", []string{"A", "sub1"}},
+		{user1, "sub1", "*", []string{"B", "C", "sub2"}},
+		{user1, "sub1/sub2", "*", []string{"D"}},
+		{user1, "", "...", []string{"", "A", "sub1", "sub1/B", "sub1/C", "sub1/sub2", "sub1/sub2/D"}},
+		{user2, "", "*", []string{"sub1"}},
+		{user2, "", "...", []string{"", "sub1", "sub1/sub2", "sub1/sub2/X"}},
+		{user3, "", "*", []string{}},
+		{user3, "", "...", []string{""}},
+	}
+	for i, tc := range testcases {
+		matches, _, _ := testutil.GlobName(tc.user, naming.Join(addr, tc.name), tc.pattern)
+		if !reflect.DeepEqual(matches, tc.results) {
+			t.Errorf("unexpected results for tc #%d. Got %q, expected %q", i, matches, tc.results)
+		}
+	}
+}
+
 func newPrincipalContext(t *testing.T, ctx *context.T, root *testutil.IDProvider, names ...string) *context.T {
 	principal := testutil.NewPrincipal()
 	var blessings []security.Blessings