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 {