veyron2/security, veyron/security: Move ACL authorizer to veyron/security.
ACL authorizer is the default implementation of Authorizer, and is
intended to be used by both the Veyron runtime and applications.
Change-Id: Ib17097ccb94ccc2cd629a851b9a1c9fa29ef3d27
diff --git a/security/acl_authorizer.go b/security/acl_authorizer.go
new file mode 100644
index 0000000..82b35d9
--- /dev/null
+++ b/security/acl_authorizer.go
@@ -0,0 +1,103 @@
+package security
+
+// This file provides an implementation of security.Authorizer.
+//
+// Definitions
+// * Self-RPC: An RPC request is said to be a "self-RPC" if the identities
+// at the local and remote ends are identical.
+
+import (
+ "errors"
+ "os"
+ "reflect"
+
+ "veyron2/security"
+)
+
+var (
+ errACL = errors.New("no matching ACL entry found")
+ errInvalidLabel = errors.New("label is invalid")
+ errNilID = errors.New("identity being matched is nil")
+ errNilACL = errors.New("ACL is nil")
+ nullACL security.ACL
+)
+
+// aclAuthorizer implements Authorizer.
+type aclAuthorizer security.ACL
+
+// Authorize verifies a request iff the identity at the remote end has a name authorized by
+// the aclAuthorizer's ACL for the request's label, or the request corresponds to a self-RPC.
+func (a aclAuthorizer) Authorize(ctx security.Context) error {
+ // Test if the request corresponds to a self-RPC.
+ if ctx.LocalID() != nil && ctx.RemoteID() != nil && reflect.DeepEqual(ctx.LocalID(), ctx.RemoteID()) {
+ return nil
+ }
+ // Match the aclAuthorizer's ACL.
+ return matchesACL(ctx.RemoteID(), ctx.Label(), security.ACL(a))
+}
+
+// NewACLAuthorizer creates an authorizer from the provided ACL. The
+// authorizer authorizes a request iff the identity at the remote end has a name
+// authorized by the provided ACL for the request's label, or the request
+// corresponds to a self-RPC.
+func NewACLAuthorizer(acl security.ACL) security.Authorizer { return aclAuthorizer(acl) }
+
+// fileACLAuthorizer implements Authorizer.
+type fileACLAuthorizer string
+
+// Authorize reads and decodes the fileACLAuthorizer's ACL file into a ACL and
+// then verifies the request according to an aclAuthorizer based on the ACL. If
+// reading or decoding the file fails then no requests are authorized.
+func (a fileACLAuthorizer) Authorize(ctx security.Context) error {
+ acl, err := loadACLFromFile(string(a))
+ if err != nil {
+ return err
+ }
+ return aclAuthorizer(acl).Authorize(ctx)
+}
+
+// NewFileACLAuthorizer creates an authorizer from the provided path to a file
+// containing a JSON-encoded ACL. Each call to "Authorize" involves reading and
+// decoding a ACL from the file and then authorizing the request according to the
+// ACL. The authorizer monitors the file so out of band changes to the contents of
+// the file are reflected in the ACL. If reading or decoding the file fails then
+// no requests are authorized.
+//
+// The JSON-encoding of a ACL is essentially a JSON object describing a map from
+// PrincipalPatterns to encoded LabelSets (see LabelSet.MarshalJSON).
+// Examples:
+// * `{"*" : "RW"}` encodes an ACL that allows all principals to access all methods with
+// ReadLabel or WriteLabel.
+// * `{"veyron/alice": "RW", "veyron/bob/*": "R"} encodes an ACL that allows all principals
+// matching "veyron/alice" to access methods with ReadLabel or WriteLabel,
+// and all principals matching "veyron/bob/*" to access methods with ReadLabel.
+// (Also see PublicID.Match.)
+//
+// TODO(ataly, ashankar): Instead of reading the file on each call we should use the "inotify"
+// mechanism to watch the file. Eventually we should also support ACLs stored in the Veyron
+// store.
+func NewFileACLAuthorizer(filePath string) security.Authorizer { return fileACLAuthorizer(filePath) }
+
+func matchesACL(id security.PublicID, label security.Label, acl security.ACL) error {
+ if id == nil {
+ return errNilID
+ }
+ if acl == nil {
+ return errNilACL
+ }
+ for key, labels := range acl {
+ if labels.HasLabel(label) && id.Match(key) {
+ return nil
+ }
+ }
+ return errACL
+}
+
+func loadACLFromFile(filePath string) (security.ACL, error) {
+ f, err := os.Open(filePath)
+ if err != nil {
+ return nil, err
+ }
+ defer f.Close()
+ return security.LoadACL(f)
+}
diff --git a/security/acl_authorizer_test.go b/security/acl_authorizer_test.go
new file mode 100644
index 0000000..6c54ca4
--- /dev/null
+++ b/security/acl_authorizer_test.go
@@ -0,0 +1,249 @@
+package security
+
+import (
+ "io/ioutil"
+ "os"
+ "runtime"
+ "testing"
+ "time"
+
+ "veyron2/naming"
+ "veyron2/security"
+)
+
+type authMap map[security.PublicID]security.LabelSet
+
+// context implements Context.
+type context struct {
+ localID, remoteID security.PublicID
+ discharges security.CaveatDischargeMap
+ method, name, suffix string
+ label security.Label
+}
+
+func (c *context) Method() string { return c.method }
+func (c *context) Name() string { return c.name }
+func (c *context) Suffix() string { return c.suffix }
+func (c *context) Label() security.Label { return c.label }
+func (c *context) CaveatDischarges() security.CaveatDischargeMap { return c.discharges }
+func (c *context) LocalID() security.PublicID { return c.localID }
+func (c *context) RemoteID() security.PublicID { return c.remoteID }
+func (c *context) LocalEndpoint() naming.Endpoint { return nil }
+func (c *context) RemoteEndpoint() naming.Endpoint { return nil }
+
+func saveACLToTempFile(acl security.ACL) string {
+ f, err := ioutil.TempFile("", "saved_acl")
+ if err != nil {
+ panic(err)
+ }
+ defer f.Close()
+ if err := security.SaveACL(f, acl); err != nil {
+ defer os.Remove(f.Name())
+ panic(err)
+ }
+ return f.Name()
+}
+
+func updateACLInFile(fileName string, acl security.ACL) {
+ f, err := os.OpenFile(fileName, os.O_WRONLY, 0600)
+ if err != nil {
+ panic(err)
+ }
+ defer f.Close()
+ if err := security.SaveACL(f, acl); err != nil {
+ panic(err)
+ }
+}
+
+func bless(blessee security.PublicID, blesser security.PrivateID, name string) security.PublicID {
+ blessed, err := blesser.Bless(blessee, name, 5*time.Minute, nil)
+ if err != nil {
+ panic(err)
+ }
+ return blessed
+}
+
+func derive(pub security.PublicID, priv security.PrivateID) security.PrivateID {
+ d, err := priv.Derive(pub)
+ if err != nil {
+ panic(err)
+ }
+ return d
+}
+
+func testSelfRPCs(t *testing.T, authorizer security.Authorizer) {
+ _, file, line, _ := runtime.Caller(1)
+ var (
+ veyron = security.FakePrivateID("veyron")
+ alice = security.FakePrivateID("alice")
+ veyronAlice = bless(alice.PublicID(), veyron, "alice")
+ )
+ testData := []struct {
+ localID, remoteID security.PublicID
+ isAuthorized bool
+ }{
+ {alice.PublicID(), alice.PublicID(), true},
+ {veyron.PublicID(), veyron.PublicID(), true},
+ {veyron.PublicID(), alice.PublicID(), false},
+ {veyronAlice, veyronAlice, true},
+ {veyronAlice, alice.PublicID(), false},
+ {veyronAlice, veyron.PublicID(), false},
+ }
+ for _, d := range testData {
+ ctx := &context{localID: d.localID, remoteID: d.remoteID}
+ if got, want := authorizer.Authorize(ctx), d.isAuthorized; (got == nil) != want {
+ t.Errorf("%s:%d: %+v.Authorize(&context{localID: %v, remoteID: %v}) returned error: %v, want error: %v", file, line, authorizer, d.localID, d.remoteID, got, !want)
+ }
+ }
+}
+
+func testAuthorizations(t *testing.T, authorizer security.Authorizer, authorizations authMap) {
+ _, file, line, _ := runtime.Caller(1)
+ for user, labels := range authorizations {
+ for _, l := range security.ValidLabels {
+ ctx := &context{remoteID: user, label: l}
+ if got, want := authorizer.Authorize(ctx), labels.HasLabel(l); (got == nil) != want {
+ t.Errorf("%s:%d: %+v.Authorize(&context{remoteID: %v, label: %v}) returned error: %v, want error: %v", file, line, authorizer, user, l, got, !want)
+ }
+ }
+ }
+}
+
+func testNothingPermitted(t *testing.T, authorizer security.Authorizer) {
+ _, file, line, _ := runtime.Caller(1)
+ var (
+ veyronPrivateID = security.FakePrivateID("veyron")
+ alicePrivateID = security.FakePrivateID("alice")
+ randomPrivateID = security.FakePrivateID("random")
+ veyron = veyronPrivateID.PublicID()
+ alice = alicePrivateID.PublicID()
+ random = randomPrivateID.PublicID()
+ veyronAlice = bless(alice, veyronPrivateID, "alice")
+ veyronAliceFriend = bless(random, derive(veyronAlice, alicePrivateID), "friend")
+ veyronBob = bless(random, veyronPrivateID, "bob")
+ )
+ users := []security.PublicID{
+ veyron,
+ random,
+ alice,
+
+ // Blessed principals
+ veyronAlice,
+ veyronAliceFriend,
+ veyronBob,
+ }
+ // No principal (whether the identity provider is trusted or not)
+ // should have access to any valid or invalid label.
+ for _, u := range users {
+ for _, l := range security.ValidLabels {
+ ctx := &context{remoteID: u, label: l}
+ if got := authorizer.Authorize(ctx); got == nil {
+ t.Errorf("%s:%d: %+v.Authorize(%v) returns nil, want error", file, line, authorizer, ctx)
+ }
+ }
+ invalidLabel := security.Label(3)
+ ctx := &context{remoteID: u, label: invalidLabel}
+ if got := authorizer.Authorize(ctx); got == nil {
+ t.Errorf("%s:%d: %+v.Authorize(%v) returns nil, want error", file, line, authorizer, ctx)
+ }
+ }
+}
+
+func TestACLAuthorizer(t *testing.T) {
+ const (
+ // Shorthands
+ R = security.ReadLabel
+ W = security.WriteLabel
+ A = security.AdminLabel
+ D = security.DebugLabel
+ M = security.MonitoringLabel
+ )
+ // Principals to test
+ var (
+ veyronPrivateID = security.FakePrivateID("veyron")
+ alicePrivateID = security.FakePrivateID("alice")
+ veyron = veyronPrivateID.PublicID()
+ alice = alicePrivateID.PublicID()
+ bob = security.FakePrivateID("bob").PublicID()
+
+ // Blessed principals
+ veyronAlice = bless(alice, veyronPrivateID, "alice")
+ veyronBob = bless(bob, veyronPrivateID, "bob")
+ veyronAliceFriend = bless(bob, derive(veyronAlice, alicePrivateID), "friend")
+ )
+ // Convenience function for combining Labels into a LabelSet.
+ LS := func(labels ...security.Label) security.LabelSet {
+ var ret security.LabelSet
+ for _, l := range labels {
+ ret = ret | security.LabelSet(l)
+ }
+ return ret
+ }
+
+ // ACL for testing
+ acl := security.ACL{
+ "*": LS(R),
+ "fake/veyron/alice/*": LS(W, R),
+ "fake/veyron/alice": LS(A, D, M),
+ "fake/veyron/bob": LS(D, M),
+ }
+
+ // Authorizations for the above ACL.
+ authorizations := authMap{
+ // alice and bob have only what "*" has.
+ alice: LS(R),
+ bob: LS(R),
+ // veyron and veyronAlice have R, W, A, D, M from the "veyron/alice" and
+ // "veyron/alice/*" ACL entries.
+ veyron: LS(R, W, A, D, M),
+ veyronAlice: LS(R, W, A, D, M),
+ // veyronBob has R, D, M from "*" and "veyron/bob" ACL entries.
+ veyronBob: LS(R, D, M),
+ // veyronAliceFriend has W, R from the "veyron/alice/*" ACL entry.
+ veyronAliceFriend: LS(W, R),
+ // nil PublicIDs are not authorized.
+ nil: LS(),
+ }
+ // Create an aclAuthorizer based on the ACL and verify the authorizations.
+ authorizer := NewACLAuthorizer(acl)
+ testAuthorizations(t, authorizer, authorizations)
+ testSelfRPCs(t, authorizer)
+
+ // Create a fileACLAuthorizer by saving the ACL in a file, and verify the
+ // authorizations.
+ fileName := saveACLToTempFile(acl)
+ defer os.Remove(fileName)
+ fileAuthorizer := NewFileACLAuthorizer(fileName)
+ testAuthorizations(t, fileAuthorizer, authorizations)
+ testSelfRPCs(t, fileAuthorizer)
+
+ // Modify the ACL stored in the file and verify that the authorizations appropriately
+ // change for the fileACLAuthorizer.
+ acl["fake/veyron/bob"] = LS(R, W, A, D, M)
+ updateACLInFile(fileName, acl)
+
+ authorizations[veyronBob] = LS(R, W, A, D, M)
+ testAuthorizations(t, fileAuthorizer, authorizations)
+ testSelfRPCs(t, fileAuthorizer)
+
+ // Update the ACL file with invalid contents and verify that no requests are
+ // authorized.
+ f, err := os.OpenFile(fileName, os.O_WRONLY, 0600)
+ if err != nil {
+ panic(err)
+ }
+ f.Write([]byte("invalid ACL"))
+ f.Close()
+ testNothingPermitted(t, fileAuthorizer)
+
+ // Verify that a fileACLAuthorizer based on a nonexistent file does not authorize any
+ // requests.
+ fileAuthorizer = NewFileACLAuthorizer("fileDoesNotExist")
+ testNothingPermitted(t, fileAuthorizer)
+}
+
+func TestNilACLAuthorizer(t *testing.T) {
+ authorizer := NewACLAuthorizer(nil)
+ testNothingPermitted(t, authorizer)
+ testSelfRPCs(t, authorizer)
+}
diff --git a/security/flag/flag.go b/security/flag/flag.go
index fa032b8..11dfcca 100644
--- a/security/flag/flag.go
+++ b/security/flag/flag.go
@@ -7,6 +7,8 @@
"errors"
"flag"
+ vsecurity "veyron/security"
+
"veyron2/security"
)
@@ -27,11 +29,11 @@
panic(errors.New("only one of the flags \"--acl\" or \"--acl_file\" must be provided"))
}
if len(*aclFile) != 0 {
- return security.NewFileACLAuthorizer(*aclFile)
+ return vsecurity.NewFileACLAuthorizer(*aclFile)
}
a, err := security.LoadACL(bytes.NewBufferString(*acl))
if err != nil {
return nil
}
- return security.NewACLAuthorizer(a)
+ return vsecurity.NewACLAuthorizer(a)
}
diff --git a/security/flag/flag_test.go b/security/flag/flag_test.go
index 5fa5616..03ae915 100644
--- a/security/flag/flag_test.go
+++ b/security/flag/flag_test.go
@@ -7,6 +7,7 @@
"testing"
tsecurity "veyron/lib/testutil/security"
+ vsecurity "veyron/security"
"veyron2/security"
)
@@ -45,19 +46,19 @@
},
{
flags: flagValue{"acl": "{}"},
- wantAuth: security.NewACLAuthorizer(acl1),
+ wantAuth: vsecurity.NewACLAuthorizer(acl1),
},
{
flags: flagValue{"acl": "{\"veyron/alice\":\"RW\", \"veyron/bob\": \"R\"}"},
- wantAuth: security.NewACLAuthorizer(acl2),
+ wantAuth: vsecurity.NewACLAuthorizer(acl2),
},
{
flags: flagValue{"acl": "{\"veyron/bob\":\"R\", \"veyron/alice\": \"WR\"}"},
- wantAuth: security.NewACLAuthorizer(acl2),
+ wantAuth: vsecurity.NewACLAuthorizer(acl2),
},
{
flags: flagValue{"acl_file": acl2File},
- wantAuth: security.NewFileACLAuthorizer(acl2File),
+ wantAuth: vsecurity.NewFileACLAuthorizer(acl2File),
},
{
flags: flagValue{"acl_file": acl2File, "acl": "{\"veyron/alice\":\"RW\", \"veyron/bob\": \"R\"}"},