sb: Add ACL view/modify functionality

sb acl dump shows all permissions.
sb acl get <blessing> shows permissions for a given blessing.
sb acl add/rm [<blessing> <tag>[,<tag>]*]+ allow you to change permissions.

Change-Id: Ic6ead52d9d02ac667ae21354f1cda677314fe14f
diff --git a/cmd/sb/commands/acl.go b/cmd/sb/commands/acl.go
new file mode 100644
index 0000000..d566fc9
--- /dev/null
+++ b/cmd/sb/commands/acl.go
@@ -0,0 +1,279 @@
+// Copyright 2016 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 commands
+
+import (
+	"fmt"
+	"strings"
+
+	"v.io/v23/context"
+	"v.io/v23/security"
+	"v.io/v23/security/access"
+	wire "v.io/v23/services/syncbase"
+	"v.io/v23/syncbase"
+	sbUtil "v.io/v23/syncbase/util"
+	"v.io/x/lib/cmdline"
+	"v.io/x/ref/cmd/sb/dbutil"
+)
+
+var cmdAcl = &cmdline.Command{
+	Name:     "acl",
+	Short:    "Read or mutate the ACLs",
+	Long:     `Read or mutate the ACLs`,
+	Children: []*cmdline.Command{cmdAclGet, cmdAclAdd, cmdAclRm, cmdAclDump},
+}
+
+var (
+	flagTarget     string
+	flagCollection string
+)
+
+func init() {
+	cmdAcl.Flags.StringVar(&flagTarget, "target", "db", "The access controller type to act on (service, db, collection).")
+	cmdAcl.Flags.StringVar(&flagCollection, "collection", "", "The collection to act on. (note: requires -target=collection)")
+}
+
+func updatePerms(ctx *context.T, db syncbase.Database, env *cmdline.Env, callback func(access.Permissions) (access.Permissions, error)) error {
+	switch flagTarget {
+	case "service", "db", "database":
+		var ac sbUtil.AccessController
+		if flagTarget == "service" {
+			ac = dbutil.GetService()
+		} else {
+			ac = db
+		}
+
+		perms, version, err := ac.GetPermissions(ctx)
+		if err != nil {
+			return err
+		}
+
+		if perms, err = callback(perms); err != nil {
+			return err
+		}
+
+		if perms != nil {
+			if err := ac.SetPermissions(ctx, perms, version); err != nil {
+				return fmt.Errorf("error setting permissions: %q", err)
+			}
+		}
+		return nil
+	case "collection":
+		bdb, err := db.BeginBatch(ctx, wire.BatchOptions{})
+		if err != nil {
+			return err
+		}
+		defer bdb.Abort(ctx)
+
+		collection := bdb.Collection(ctx, flagCollection)
+		perms, err := collection.GetPermissions(ctx)
+		if err != nil {
+			return err
+		}
+
+		if perms, err = callback(perms); err != nil {
+			return err
+		}
+
+		if perms != nil {
+			if err = collection.SetPermissions(ctx, perms); err != nil {
+				return err
+			}
+
+			return bdb.Commit(ctx)
+		}
+		return nil
+	default:
+		return env.UsageErrorf("target %s not recognized", flagTarget)
+	}
+
+	return nil
+}
+
+var cmdAclGet = &cmdline.Command{
+	Name:     "get",
+	Short:    "Read a blessing's permissions",
+	Long:     "Read a blessing's permissions",
+	ArgsName: "<blessing>",
+	ArgsLong: `
+<blessing> is the blessing to check permissions for.
+`,
+	Runner: SbRunner(runAclGet),
+}
+
+func runAclGet(ctx *context.T, db syncbase.Database, env *cmdline.Env, args []string) error {
+	if got := len(args); got != 1 {
+		return env.UsageErrorf("get: expected 1 arg, got %d", got)
+	}
+
+	blessing := args[0]
+	return updatePerms(ctx, db, env,
+		func(perms access.Permissions) (access.Permissions, error) {
+			// Pretty-print the groups that blessing is in
+			var groupsIn []string
+			for groupName, acl := range perms {
+				for _, b := range acl.In {
+					if b == security.BlessingPattern(blessing) {
+						groupsIn = append(groupsIn, groupName)
+						break
+					}
+				}
+			}
+			fmt.Printf("[%s]\n", strings.Join(groupsIn, ", "))
+
+			// Pretty-print the groups that blessing is blacklisted from
+			var groupsNotIn []string
+			for groupName, acl := range perms {
+				for _, b := range acl.NotIn {
+					if b == blessing {
+						groupsNotIn = append(groupsNotIn, groupName)
+						break
+					}
+				}
+			}
+			fmt.Printf("![%s]\n", strings.Join(groupsNotIn, ", "))
+
+			return nil, nil
+		},
+	)
+}
+
+var cmdAclAdd = &cmdline.Command{
+	Name:     "add",
+	Short:    "Add blessing to groups",
+	Long:     "Add blessing to groups",
+	ArgsName: "(<blessing> [!]<tag>(,[!]<tag>)* )+",
+	ArgsLong: `
+The args are sequential pairs of the form "<blessing> <taglist>"
+A taglist is made up of a series of comma-separated tags.
+The optional preface "!" puts the blessing in the blacklist instead of the whitelist.
+Note that the pair "dev.v.io:u:foo@google.com bar,baz,!bar" is legal.
+In such a case the blessing will be added to both the blacklist and the whitelist,
+with the blacklist overriding the whitelist.
+`,
+	Runner: SbRunner(runAclAdd),
+}
+
+func runAclAdd(ctx *context.T, db syncbase.Database, env *cmdline.Env, args []string) error {
+	if len(args) == 0 {
+		return env.UsageErrorf("add: expected arguments")
+	}
+
+	userToPerms, err := parseAccessList(args)
+	if err != nil {
+		return env.UsageErrorf("add: error parsing access list: %q", err)
+	}
+
+	return updatePerms(ctx, db, env,
+		func(perms access.Permissions) (access.Permissions, error) {
+			for blessing, tags := range userToPerms {
+				bp := security.BlessingPattern(blessing)
+				perms = perms.Add(bp, tags.in...)
+				perms = perms.Blacklist(blessing, tags.out...)
+			}
+			return perms, nil
+		},
+	)
+}
+
+var cmdAclRm = &cmdline.Command{
+	Name:     "rm",
+	Short:    "Remove specific permissions from the acl.",
+	Long:     "Remove specific permissions from the acl.",
+	ArgsName: "(<blessing> <tag>(,<tag>)* )+",
+	ArgsLong: `
+The args are sequential pairs of the form "<blessing> <taglist>"
+A taglist is made up of a series of comma-separated tags.
+rm removes all references to each <blessing> from all of the tags.
+`,
+	Runner: SbRunner(runAclRm),
+}
+
+func runAclRm(ctx *context.T, db syncbase.Database, env *cmdline.Env, args []string) error {
+	if len(args) == 0 {
+		return env.UsageErrorf("rm: expected arguments")
+	}
+
+	userToPerms, err := parseAccessList(args)
+	if err != nil {
+		return env.UsageErrorf("rm: failed to parse access list: %q", err)
+	}
+
+	return updatePerms(ctx, db, env,
+		func(perms access.Permissions) (access.Permissions, error) {
+			for blessing, tags := range userToPerms {
+				perms = perms.Clear(blessing, tags.in...)
+			}
+			return perms, nil
+		},
+	)
+}
+
+var cmdAclDump = &cmdline.Command{
+	Name:   "dump",
+	Short:  "Pretty-print the whole acl",
+	Long:   "Pretty-print the whole acl",
+	Runner: SbRunner(runAclDump),
+}
+
+func runAclDump(ctx *context.T, db syncbase.Database, env *cmdline.Env, args []string) error {
+	return updatePerms(ctx, db, env,
+		func(perms access.Permissions) (access.Permissions, error) {
+			fmt.Println("map[")
+			for tag, acl := range perms {
+				fmt.Printf("\t%s: %v\n", tag, acl)
+			}
+			fmt.Println("]")
+			return nil, nil
+		},
+	)
+}
+
+type userTags struct {
+	in  []string
+	out []string
+}
+
+// parseAccessList returns a map from blessing patterns to structs containing lists
+// of tags which the pattern is white/blacklisted on
+func parseAccessList(args []string) (map[string]userTags, error) {
+	if got := len(args); got%2 != 0 {
+		return nil, fmt.Errorf("incorrect number of arguments %d, must be even", got)
+	}
+
+	userToTags := make(map[string]userTags)
+	for i := 0; i < len(args); i += 2 {
+		tags, err := parseAccessTags(args[i+1])
+		if err != nil {
+			return nil, err
+		}
+		userToTags[args[i]] = tags
+	}
+
+	return userToTags, nil
+}
+
+// parseAccessTags returns two lists of tags, the first being tags which have
+// whitelisted the blessing, and the second being tags which have blacklisted it.
+func parseAccessTags(input string) (tags userTags, err error) {
+	fields := strings.Split(input, ",")
+	for _, tag := range fields {
+		blacklist := strings.HasPrefix(tag, "!")
+		if blacklist {
+			tag = tag[1:]
+		}
+		if len(tag) == 0 {
+			return userTags{}, fmt.Errorf("empty access tag in %q", input)
+		}
+
+		if blacklist {
+			tags.out = append(tags.out, tag)
+		} else {
+			tags.in = append(tags.in, tag)
+		}
+	}
+
+	return
+}
diff --git a/cmd/sb/commands/commands.go b/cmd/sb/commands/commands.go
index 21efbc1..38bf78e 100644
--- a/cmd/sb/commands/commands.go
+++ b/cmd/sb/commands/commands.go
@@ -19,6 +19,7 @@
 	cmdDump,
 	cmdMakeDemo,
 	cmdSelect,
+	cmdAcl,
 }
 
 var (
@@ -37,21 +38,31 @@
 type sbHandler func(ctx *context.T, db syncbase.Database, env *cmdline.Env, args []string) error
 
 func SbRunner(handler sbHandler) cmdline.Runner {
-	return v23cmd.RunnerFuncWithInit(func(ctx *context.T, env *cmdline.Env, args []string) error {
-		db := commandDb // Set in shell handler.
-		if db == nil {
-			var err error
-			if db, err = dbutil.OpenDB(ctx); err != nil {
-				return err
+	return v23cmd.RunnerFuncWithInit(
+		func(ctx *context.T, env *cmdline.Env, args []string) error {
+			db := commandDb // Set in shell handler.
+			if db == nil {
+				var err error
+				if db, err = dbutil.OpenDB(ctx); err != nil {
+					return err
+				}
 			}
-		}
-		return handler(ctx, db, env, args)
-	}, func() (*context.T, v23.Shutdown, error) {
-		if commandCtx != nil {
-			return commandCtx, func() {}, nil
-		}
-		return v23.TryInit()
-	})
+			return handler(ctx, db, env, args)
+		},
+		sbInit)
+}
+
+func sbInit() (*context.T, v23.Shutdown, error) {
+	if commandCtx != nil {
+		return commandCtx, func() {}, nil
+	}
+	return v23.TryInit()
+}
+
+type sbHandlerPlain func(ctx *context.T, env *cmdline.Env, args []string) error
+
+func sbRunnerPlain(handler sbHandlerPlain) cmdline.Runner {
+	return v23cmd.RunnerFuncWithInit(handler, sbInit)
 }
 
 func GetCommand(name string) (*cmdline.Command, error) {
diff --git a/cmd/sb/dbutil/dbutil.go b/cmd/sb/dbutil/dbutil.go
index 330444b..34f0e33 100644
--- a/cmd/sb/dbutil/dbutil.go
+++ b/cmd/sb/dbutil/dbutil.go
@@ -16,7 +16,6 @@
 var (
 	flagCreateIfAbsent bool
 	flagService        string
-	flagBlessing       string
 	flagDbId           string
 )
 
@@ -26,6 +25,10 @@
 	flag.StringVar(&flagDbId, "db", "", "Id of database to connect to.")
 }
 
+func GetService() syncbase.Service {
+	return syncbase.NewService(flagService)
+}
+
 // OpenDB is a user-friendly wrapper for openDB.
 func OpenDB(ctx *context.T) (syncbase.Database, error) {
 	// Open a connection to syncbase
diff --git a/cmd/sb/doc.go b/cmd/sb/doc.go
index a43af09..1a5ee03 100644
--- a/cmd/sb/doc.go
+++ b/cmd/sb/doc.go
@@ -17,6 +17,7 @@
    dump        Print a dump of the database
    make-demo   Populate the db with dummy data
    select      Display particular rows, or parts of rows
+   acl         Read or mutate the ACLs
    help        Display help for commands or topics
 
 The global flags are: