services/security/roled/internal: Allow imports from other roles

This change makes it possible to share member sets between roles, which
should make managing the member sets a lot easier.

The config now has a ImportMembers files that contains the names of the
other roles to import from. Example:

role1.conf:
  Members = [ A, B, C ]

role2.conf:
  ImportMembers = [ role1 ]

Then role2's members are A, B, C.

Change-Id: Ibcad056f2735d5071cecb454639cde0aca5664c8
diff --git a/services/security/roled/internal/config.vdl b/services/security/roled/internal/config.vdl
index cffe6b4..734ca95 100644
--- a/services/security/roled/internal/config.vdl
+++ b/services/security/roled/internal/config.vdl
@@ -9,6 +9,11 @@
 // Config contains the attributes of the role, and the list of members who have
 // access to it.
 type Config struct {
+	// List of role objects, relative to this role, from which to import
+	// the set of members. File path notation like "." and ".." may be used.
+	// The set of members who have access to this role is the union of this
+	// role's members and those of all the imported roles.
+	ImportMembers []string
 	// Blessings that match at least one of the patterns in this set are
 	// allowed to act on behalf of the role.
 	Members []security.BlessingPattern
diff --git a/services/security/roled/internal/config.vdl.go b/services/security/roled/internal/config.vdl.go
index 258fed3..6e10437 100644
--- a/services/security/roled/internal/config.vdl.go
+++ b/services/security/roled/internal/config.vdl.go
@@ -18,6 +18,11 @@
 // Config contains the attributes of the role, and the list of members who have
 // access to it.
 type Config struct {
+	// List of role objects, relative to this role, from which to import
+	// the set of members. File path notation like "." and ".." may be used.
+	// The set of members who have access to this role is the union of this
+	// role's members and those of all the imported roles.
+	ImportMembers []string
 	// Blessings that match at least one of the patterns in this set are
 	// allowed to act on behalf of the role.
 	Members []security.BlessingPattern
diff --git a/services/security/roled/internal/dispatcher.go b/services/security/roled/internal/dispatcher.go
index d01e8a2..22478b9 100644
--- a/services/security/roled/internal/dispatcher.go
+++ b/services/security/roled/internal/dispatcher.go
@@ -49,11 +49,11 @@
 		// files outside of the config root.
 		return nil, nil, verror.New(verror.ErrNoExistOrNoAccess, nil)
 	}
-	config, err := loadConfig(fileName)
+	config, 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): %v", fileName, err)
+		vlog.Errorf("loadConfig(%q, %q): %v", d.configRoot, suffix, err)
 		return nil, nil, verror.Convert(verror.ErrInternal, nil, err)
 	}
 	obj := &roleService{role: suffix, config: config, dischargerLocation: d.dischargerLocation}
@@ -84,6 +84,36 @@
 	return verror.New(verror.ErrNoExistOrNoAccess, ctx)
 }
 
+func loadExpandedConfig(fileName string, seenFiles map[string]struct{}) (*Config, error) {
+	if seenFiles == nil {
+		seenFiles = make(map[string]struct{})
+	}
+	if _, seen := seenFiles[fileName]; seen {
+		return nil, nil
+	}
+	seenFiles[fileName] = struct{}{}
+	c, err := loadConfig(fileName)
+	if err != nil {
+		return nil, err
+	}
+	parentDir := filepath.Dir(fileName)
+	for _, imp := range c.ImportMembers {
+		f := filepath.Join(parentDir, filepath.FromSlash(imp+".conf"))
+		ic, err := loadExpandedConfig(f, seenFiles)
+		if err != nil {
+			vlog.Errorf("loadExpandedConfig(%q) failed: %v", f, err)
+			continue
+		}
+		if ic == nil {
+			continue
+		}
+		c.Members = append(c.Members, ic.Members...)
+	}
+	c.ImportMembers = nil
+	dedupMembers(c)
+	return c, nil
+}
+
 func loadConfig(fileName string) (*Config, error) {
 	contents, err := ioutil.ReadFile(fileName)
 	if err != nil {
@@ -100,3 +130,14 @@
 	}
 	return &c, nil
 }
+
+func dedupMembers(c *Config) {
+	members := make(map[security.BlessingPattern]struct{})
+	for _, m := range c.Members {
+		members[m] = struct{}{}
+	}
+	c.Members = []security.BlessingPattern{}
+	for m := range members {
+		c.Members = append(c.Members, m)
+	}
+}
diff --git a/services/security/roled/internal/role_internal_test.go b/services/security/roled/internal/role_internal_test.go
new file mode 100644
index 0000000..578feff
--- /dev/null
+++ b/services/security/roled/internal/role_internal_test.go
@@ -0,0 +1,77 @@
+// 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 (
+	"encoding/json"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"reflect"
+	"sort"
+	"testing"
+
+	"v.io/v23/security"
+)
+
+func TestImportMembers(t *testing.T) {
+	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, "sub"), 0700)
+
+	configs := map[string]Config{
+		"role1":     Config{Members: []security.BlessingPattern{"A", "B", "C"}},
+		"role2":     Config{Members: []security.BlessingPattern{"C", "D", "E"}},
+		"sub/role3": Config{ImportMembers: []string{"../role2"}},
+		"sub/role4": Config{ImportMembers: []string{"../role1", "../role2"}},
+		"sub/role5": Config{ImportMembers: []string{"../role1", "../role6"}},
+		"role6":     Config{ImportMembers: []string{"sub/role5"}, Members: []security.BlessingPattern{"F"}},
+	}
+	for role, config := range configs {
+		WriteConfig(t, config, filepath.Join(workdir, role+".conf"))
+	}
+
+	testcases := []struct {
+		role    string
+		members []security.BlessingPattern
+	}{
+		{"role1", []security.BlessingPattern{"A/_role", "B/_role", "C/_role"}},
+		{"role2", []security.BlessingPattern{"C/_role", "D/_role", "E/_role"}},
+		{"sub/role3", []security.BlessingPattern{"C/_role", "D/_role", "E/_role"}},
+		{"sub/role4", []security.BlessingPattern{"A/_role", "B/_role", "C/_role", "D/_role", "E/_role"}},
+		{"sub/role5", []security.BlessingPattern{"A/_role", "B/_role", "C/_role", "F/_role"}},
+		{"role6", []security.BlessingPattern{"A/_role", "B/_role", "C/_role", "F/_role"}},
+	}
+	for _, tc := range testcases {
+		c, err := loadExpandedConfig(filepath.Join(workdir, tc.role+".conf"), nil)
+		if err != nil {
+			t.Errorf("unexpected error for %q: %v", tc.role, err)
+			continue
+		}
+		sort.Sort(BlessingPatternSlice(c.Members))
+		if !reflect.DeepEqual(tc.members, c.Members) {
+			t.Errorf("unexpected results. Got %#v, expected %#v", c.Members, tc.members)
+		}
+	}
+}
+
+type BlessingPatternSlice []security.BlessingPattern
+
+func (p BlessingPatternSlice) Len() int           { return len(p) }
+func (p BlessingPatternSlice) Less(i, j int) bool { return p[i] < p[j] }
+func (p BlessingPatternSlice) Swap(i, j int)      { p[i], p[j] = p[j], p[i] }
+
+func WriteConfig(t *testing.T, config Config, fileName string) {
+	mConf, err := json.Marshal(config)
+	if err != nil {
+		t.Fatal("json.MarshalIndent failed: %v", err)
+	}
+	if err := ioutil.WriteFile(fileName, mConf, 0644); err != nil {
+		t.Fatal("ioutil.WriteFile(%q, %q) failed: %v", fileName, string(mConf), err)
+	}
+}
diff --git a/services/security/roled/internal/role_test.go b/services/security/roled/internal/role_test.go
index 583bd98..b1f5c43 100644
--- a/services/security/roled/internal/role_test.go
+++ b/services/security/roled/internal/role_test.go
@@ -5,7 +5,6 @@
 package internal_test
 
 import (
-	"encoding/json"
 	"io/ioutil"
 	"os"
 	"path/filepath"
@@ -45,7 +44,7 @@
 		},
 		Extend: true,
 	}
-	writeConfig(t, roleAConf, filepath.Join(workdir, "A.conf"))
+	irole.WriteConfig(t, roleAConf, filepath.Join(workdir, "A.conf"))
 
 	// Role B is an unrestricted role.
 	roleBConf := irole.Config{
@@ -56,7 +55,7 @@
 		Audit:  true,
 		Extend: false,
 	}
-	writeConfig(t, roleBConf, filepath.Join(workdir, "B.conf"))
+	irole.WriteConfig(t, roleBConf, filepath.Join(workdir, "B.conf"))
 
 	root := testutil.NewIDProvider("root")
 
@@ -167,16 +166,6 @@
 	return server, endpoints[0].Name()
 }
 
-func writeConfig(t *testing.T, config irole.Config, fileName string) {
-	mConf, err := json.Marshal(config)
-	if err != nil {
-		t.Fatal("json.MarshalIndent failed: %v", err)
-	}
-	if err := ioutil.WriteFile(fileName, mConf, 0644); err != nil {
-		t.Fatal("ioutil.WriteFile(%q, %q) failed: %v", fileName, string(mConf), err)
-	}
-}
-
 func callTest(t *testing.T, ctx *context.T, addr string) (blessingNames []string, rejected []security.RejectedBlessing) {
 	call, err := v23.GetClient(ctx).StartCall(ctx, addr, "Test", nil)
 	if err != nil {