blob: f68e64eb3266b7f6c9f6605a5034409a4f6babe6 [file] [log] [blame]
// 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
flagSyncgroup string
)
func init() {
cmdAcl.Flags.StringVar(&flagTarget, "target", "db", "The access controller type to act on (service, db, database, cx, collection, sg, syncgroup).")
cmdAcl.Flags.StringVar(&flagCollection, "cx", "", "The collection to act on, in format <blessing>,<collection>. (note: requires -target=collection or cx)")
cmdAcl.Flags.StringVar(&flagSyncgroup, "sg", "", "The syncgroup to act on, in format <blessing>,<syncgroup>. (note: requires -target=syncgroup or sg)")
}
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
} else if perms == nil {
return nil
}
if err := ac.SetPermissions(ctx, perms, version); err != nil {
return fmt.Errorf("error setting permissions: %q", err)
}
return nil
case "cx", "collection":
bdb, err := db.BeginBatch(ctx, wire.BatchOptions{})
if err != nil {
return err
}
defer bdb.Abort(ctx)
id, err := wire.ParseId(flagCollection)
if err != nil {
return err
}
collection := bdb.CollectionForId(id)
perms, err := collection.GetPermissions(ctx)
if err != nil {
return err
}
if perms, err = callback(perms); err != nil {
return err
} else if perms == nil {
return nil
}
if err = collection.SetPermissions(ctx, perms); err != nil {
return err
}
return bdb.Commit(ctx)
case "sg", "syncgroup":
id, err := wire.ParseId(flagSyncgroup)
if err != nil {
return err
}
sg := db.SyncgroupForId(id)
spec, version, err := sg.GetSpec(ctx)
if err != nil {
return err
}
if spec.Perms, err = callback(spec.Perms); err != nil {
return err
} else if spec.Perms == nil {
return nil
}
sg.SetSpec(ctx, spec, version)
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
}