veyron/security/acl: Implement a LabelACL and a corresponding
Authorizer.
Motivation and design discussed in:
https://docs.google.com/a/google.com/document/d/1DZUu2sGKrf-b4p9JFdIbN0ULhz9Loa8reuPNKsdMKD0/edit#
Change-Id: I181a9ad8a0311e1376cb73cac560b302eeb8bded
diff --git a/security/acl/authorizer.go b/security/acl/authorizer.go
new file mode 100644
index 0000000..f4d60c9
--- /dev/null
+++ b/security/acl/authorizer.go
@@ -0,0 +1,150 @@
+package acl
+
+import (
+ "fmt"
+ "os"
+ "reflect"
+
+ "veyron.io/veyron/veyron2/security"
+)
+
+// TaggedACLAuthorizer implements an authorization policy where access is
+// granted if the remote end presents blessings included in the Access Control
+// Lists (ACLs) associated with the set of relevant tags.
+//
+// The set of relevant tags is the subset of tags associated with the
+// method (security.Context.MethodTags) that have the same type as tagType.
+// Currently, tagType.Kind must be reflect.String, i.e., only tags that are
+// named string types are supported.
+//
+// If multiple tags of tagType are associated with the method, then access is
+// granted if the peer presents blessings that match the ACLs of each one of
+// those tags. If no tags of tagType are associated with the method, then
+// access is denied.
+//
+// If the TaggedACLMap provided is nil, then a nil authorizer is returned.
+//
+// Sample usage:
+//
+// (1) Attach tags to methods in the VDL (eg. myservice.vdl)
+// package myservice
+//
+// type MyTag string
+// const (
+// ReadAccess = MyTag("R")
+// WriteAccess = MyTag("W")
+// )
+//
+// type MyService interface {
+// Get() ([]string, error) {ReadAccess}
+// GetIndex(int) (string, error) {ReadAccess}
+//
+// Set([]string) error {WriteAccess}
+// SetIndex(int, string) error {WriteAccess}
+//
+// GetAndSet([]string) ([]string, error) {ReadAccess, WriteAccess}
+// }
+//
+// (2) Setup the ipc.Dispatcher to use the TaggedACLAuthorizer
+// import (
+// "reflect"
+// "veyron.io/veyron/veyron/security/acl"
+//
+// "veyron.io/veyron/veyron2/ipc"
+// "veyron.io/veyron/veyron2/security"
+// )
+//
+// type dispatcher struct{}
+// func (d dispatcher) Lookup(suffix, method) (ipc.Invoker, security.Authorizer, error) {
+// acl := acl.TaggedACLMap{
+// "R": acl.ACL{In: []security.BlessingPattern{"alice/friends/...", "alice/family/..."} },
+// "W": acl.ACL{In: []security.BlessingPattern{"alice/family/...", "alice/colleagues/..." } },
+// }
+// typ := reflect.TypeOf(ReadAccess) // equivalently, reflect.TypeOf(WriteAccess)
+// return newInvoker(), acl.TaggedACLAuthorizer(acl, typ), nil
+// }
+//
+// With the above dispatcher, the server will grant access to a peer with the blessing
+// "alice/friend/bob" access only to the "Get" and "GetIndex" methods. A peer presenting
+// the blessing "alice/colleague/carol" will get access only to the "Set" and "SetIndex"
+// methods. A peer presenting "alice/family/mom" will get access to all methods, even
+// GetAndSet - which requires that the blessing appear in the ACLs for both the
+// ReadAccess and WriteAccess tags.
+func TaggedACLAuthorizer(acls TaggedACLMap, tagType reflect.Type) (security.Authorizer, error) {
+ if tagType.Kind() != reflect.String {
+ return nil, fmt.Errorf("tag type(%v) must be backed by a string not %v", tagType, tagType.Kind())
+ }
+ return &authorizer{acls, tagType}, nil
+}
+
+// TaggedACLAuthorizerFromFile applies the same authorization policy as
+// TaggedACLAuthorizer, with the TaggedACLMap to be used sourced from a file named
+// filename.
+//
+// Changes to the file are monitored and affect subsequent calls to Authorize.
+// Currently, this is achieved by re-reading the file on every call to
+// Authorize.
+// TODO(ashankar,ataly): Use inotify or a similar mechanism to watch for
+// changes.
+func TaggedACLAuthorizerFromFile(filename string, tagType reflect.Type) (security.Authorizer, error) {
+ if tagType.Kind() != reflect.String {
+ return nil, fmt.Errorf("tag type(%v) must be backed by a string not %v", tagType, tagType.Kind())
+ }
+ return &fileAuthorizer{filename, tagType}, nil
+}
+
+type authorizer struct {
+ acls TaggedACLMap
+ tagType reflect.Type
+}
+
+func (a *authorizer) Authorize(ctx security.Context) error {
+ // "Self-RPCs" are always authorized.
+ if l, r := ctx.LocalBlessings(), ctx.RemoteBlessings(); l != nil && r != nil && reflect.DeepEqual(l.PublicKey(), r.PublicKey()) {
+ return nil
+ }
+ var blessings []string
+ if ctx.RemoteBlessings() != nil {
+ blessings = ctx.RemoteBlessings().ForContext(ctx)
+ }
+ grant := false
+ for _, tag := range ctx.MethodTags() {
+ if v := reflect.ValueOf(tag); v.Type() == a.tagType {
+ if acl, exists := a.acls[v.String()]; !exists || !acl.Includes(blessings...) {
+ return errACLMatch(blessings)
+ }
+ grant = true
+ }
+ }
+ if grant {
+ return nil
+ }
+ return errACLMatch(blessings)
+}
+
+type fileAuthorizer struct {
+ filename string
+ tagType reflect.Type
+}
+
+func (a *fileAuthorizer) Authorize(ctx security.Context) error {
+ acl, err := loadTaggedACLMapFromFile(a.filename)
+ if err != nil {
+ // TODO(ashankar): Information leak?
+ return fmt.Errorf("failed to read ACL from file: %v", err)
+ }
+ return (&authorizer{acl, a.tagType}).Authorize(ctx)
+}
+
+func loadTaggedACLMapFromFile(filename string) (TaggedACLMap, error) {
+ file, err := os.Open(filename)
+ if err != nil {
+ return nil, err
+ }
+ defer file.Close()
+ return ReadTaggedACLMap(file)
+}
+
+func errACLMatch(blessings []string) error {
+ return fmt.Errorf("%v does not match ACL", blessings)
+}