package main
import "encoding/json"
import "flag"
import "fmt"
import "os"
import "text/tabwriter"
import "oscarkilo.com/okg/who"
func runGroup(cfg *Config, args []string) error {
if len(args) == 0 {
return fmt.Errorf(
"usage: okg group SUBCOMMAND ... " +
"(try `okg --help`)")
}
switch args[0] {
case "list":
return runGroupList(cfg, args[1:])
case "create":
return runGroupCreate(cfg, args[1:])
case "add-member":
return runGroupAddMember(cfg, args[1:])
case "remove-member":
return runGroupRemoveMember(cfg, args[1:])
case "members":
return runGroupMembers(cfg, args[1:])
case "delete":
return runGroupDelete(cfg, args[1:])
default:
return fmt.Errorf(
"unknown group subcommand: %s", args[0])
}
}
func runGroupList(cfg *Config, args []string) error {
fs := flag.NewFlagSet("group list", flag.ContinueOnError)
asJSON := fs.Bool("json", false, "output raw JSON")
if err := fs.Parse(args); err != nil {
return err
}
c, err := newWhoClient(cfg)
if err != nil {
return err
}
groups, err := c.ListGroups()
if err != nil {
return err
}
if *asJSON {
buf, err := json.MarshalIndent(groups, "", " ")
if err != nil {
return err
}
fmt.Println(string(buf))
return nil
}
tw := tabwriter.NewWriter(os.Stdout, 0, 2, 2, ' ', 0)
fmt.Fprintln(tw, "USERNAME\tFULL NAME\tOWNER")
for _, g := range groups {
fmt.Fprintf(tw, "%s\t%s\t%s\n",
g.Username, g.Name, g.OwnerUsername)
}
return tw.Flush()
}
func runGroupCreate(cfg *Config, args []string) error {
fs := flag.NewFlagSet(
"group create", flag.ContinueOnError)
fullName := fs.String("full-name", "",
"display name (default: NAME)")
owner := fs.String("owner", "",
"owner username (default: caller)")
if err := fs.Parse(args); err != nil {
return err
}
positional := fs.Args()
if len(positional) != 1 {
return fmt.Errorf("usage: okg group create NAME")
}
name := positional[0]
c, err := newWhoClient(cfg)
if err != nil {
return err
}
if *owner == "" {
me, err := c.GetProfile()
if err != nil {
return fmt.Errorf("resolve caller: %v", err)
}
*owner = me.Username
}
displayName := *fullName
if displayName == "" {
displayName = name
}
if err := c.CreateGroup(who.CreateGroupRequest{
Username: name,
Name: displayName,
OwnerUsername: *owner,
}); err != nil {
return err
}
fmt.Printf(
"Created group %s (owner: %s)\n", name, *owner)
return nil
}
func runGroupAddMember(cfg *Config, args []string) error {
fs := flag.NewFlagSet(
"group add-member", flag.ContinueOnError)
if err := fs.Parse(args); err != nil {
return err
}
positional := fs.Args()
if len(positional) < 2 {
return fmt.Errorf(
"usage: okg group add-member GROUP USER [USER ...]")
}
group := positional[0]
members := positional[1:]
c, err := newWhoClient(cfg)
if err != nil {
return err
}
if err := c.JoinGroups(who.JoinGroupsRequest{
GroupUsernames: []string{group},
MemberUsernames: members,
}); err != nil {
return err
}
fmt.Printf(
"Added %d member(s) to %s\n", len(members), group)
return nil
}
func runGroupRemoveMember(cfg *Config, args []string) error {
fs := flag.NewFlagSet(
"group remove-member", flag.ContinueOnError)
if err := fs.Parse(args); err != nil {
return err
}
positional := fs.Args()
if len(positional) < 2 {
return fmt.Errorf(
"usage: okg group remove-member " +
"GROUP USER [USER ...]")
}
groupName := positional[0]
members := positional[1:]
c, err := newWhoClient(cfg)
if err != nil {
return err
}
// /groups/leave wants owids, not usernames. Resolve the
// group's owid via ListGroups, then the members' owids via
// GroupMembers. Two round-trips on top of the actual leave
// calls; shared across all members in this invocation.
groupOwid, err := resolveGroupOwid(c, groupName)
if err != nil {
return err
}
ms, err := c.GroupMembers(groupOwid)
if err != nil {
return err
}
username2owid := make(map[string]string)
for owid, name := range ms.Usernames {
username2owid[name] = owid
}
for _, m := range members {
memberOwid, ok := username2owid[m]
if !ok {
return fmt.Errorf(
"user %q is not a member of %s", m, groupName)
}
if err := c.LeaveGroup(who.LeaveGroupRequest{
GroupOwid: groupOwid,
MemberOwid: memberOwid,
}); err != nil {
return fmt.Errorf(
"remove %s from %s: %v", m, groupName, err)
}
}
fmt.Printf(
"Removed %d member(s) from %s\n",
len(members), groupName)
return nil
}
func runGroupMembers(cfg *Config, args []string) error {
fs := flag.NewFlagSet(
"group members", flag.ContinueOnError)
asJSON := fs.Bool("json", false,
"output full DAG (Up/Down/Usernames) as raw JSON")
if err := fs.Parse(args); err != nil {
return err
}
positional := fs.Args()
if len(positional) != 1 {
return fmt.Errorf("usage: okg group members GROUP")
}
groupName := positional[0]
c, err := newWhoClient(cfg)
if err != nil {
return err
}
groupOwid, err := resolveGroupOwid(c, groupName)
if err != nil {
return err
}
ms, err := c.GroupMembers(groupOwid)
if err != nil {
return err
}
if *asJSON {
buf, err := json.MarshalIndent(ms, "", " ")
if err != nil {
return err
}
fmt.Println(string(buf))
return nil
}
// Direct members are the first level of Down. Print one
// username per line; deeper nesting (group-of-groups) is
// available via --json.
if len(ms.Down) == 0 {
return nil
}
for _, owid := range ms.Down[0] {
fmt.Println(ms.Usernames[owid])
}
return nil
}
func runGroupDelete(cfg *Config, args []string) error {
fs := flag.NewFlagSet(
"group delete", flag.ContinueOnError)
if err := fs.Parse(args); err != nil {
return err
}
positional := fs.Args()
if len(positional) != 1 {
return fmt.Errorf("usage: okg group delete NAME")
}
groupName := positional[0]
c, err := newWhoClient(cfg)
if err != nil {
return err
}
// /groups/delete takes an owid; resolve via /groups/list.
groupOwid, err := resolveGroupOwid(c, groupName)
if err != nil {
return err
}
if err := c.DeleteGroup(who.DeleteGroupRequest{
Owid: groupOwid,
}); err != nil {
return err
}
fmt.Printf("Deleted group %s\n", groupName)
return nil
}
// resolveGroupOwid translates a group username to its owid via
// /groups/list. Returns an error if the group isn't visible to
// the caller.
func resolveGroupOwid(
c *who.HTTPClient, name string,
) (string, error) {
groups, err := c.ListGroups()
if err != nil {
return "", err
}
for _, g := range groups {
if g.Username == name {
return g.Owid, nil
}
}
return "", fmt.Errorf(
"group %q not found (or not visible to caller)", name)
}
// newWhoClient builds a //who client. Shared by every
// `okg group` and `okg authz` subcommand.
func newWhoClient(cfg *Config) (*who.HTTPClient, error) {
if cfg.ApiKey == "" {
return nil, fmt.Errorf(
"no API key — run `okg auth login --key sk-...`")
}
return who.NewHTTPClient(cfg.Host, cfg.ApiKey), nil
}