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: