From 633df64cc63d3b824f20a5f8edcedcb2852fc6c9 Mon Sep 17 00:00:00 2001 From: "Benjamin S. Allen" Date: Mon, 8 Mar 2021 13:22:24 -0600 Subject: [PATCH] Rename add command to create, remove to delete to match API naming. Add --create flag for modify to create the user if it doesn't already exist. Detect if a user is already alread member of a group before trying to associate the group. Cleanup log text. --- cmd/duocli/duocli.go | 17 ++++---- pkg/cli/user/user.go | 99 +++++++++++++++++++++++++++++++------------- 2 files changed, 79 insertions(+), 37 deletions(-) diff --git a/cmd/duocli/duocli.go b/cmd/duocli/duocli.go index b849d87..97c2c39 100644 --- a/cmd/duocli/duocli.go +++ b/cmd/duocli/duocli.go @@ -35,9 +35,9 @@ func main() { HideHelpCommand: true, Subcommands: []*cli.Command{ { - Name: "add", - Usage: "add a user", - Action: user.Add, + Name: "create", + Usage: "create a user", + Action: user.Create, Flags: []cli.Flag{ &cli.StringFlag{Name: "username", Aliases: []string{"u"}, Required: true, Usage: "username"}, &cli.StringSliceFlag{Name: "group", Aliases: []string{"g"}, Usage: "add user to group, can be specified multiple times to add user to multiple groups"}, @@ -67,15 +67,16 @@ func main() { &cli.StringFlag{Name: "firstName", Aliases: []string{"f"}, Usage: "first name of user"}, &cli.StringFlag{Name: "lastName", Aliases: []string{"l"}, Usage: "last name of user"}, &cli.StringFlag{Name: "status", Aliases: []string{"s"}, Usage: "status of user: active, disabled, or bypass"}, + &cli.BoolFlag{Name: "create", Aliases: []string{"c"}, Usage: "create user if not found"}, }, }, { - Name: "remove", - Usage: "remove user and any attached phones", - Action: user.Remove, + Name: "delete", + Usage: "delete user and any attached phones", + Action: user.Delete, Flags: []cli.Flag{ &cli.StringSliceFlag{Name: "username", Aliases: []string{"u"}, Required: true, Usage: "username, can be specified multiple times"}, - &cli.BoolFlag{Name: "phone", Aliases: []string{"P"}, Usage: "remove any phones found attached to the user before removing the user", Value: true}, + &cli.BoolFlag{Name: "phone", Aliases: []string{"P"}, Usage: "delete any phones found attached to the user before deleting the user", Value: true}, }, }, }, @@ -101,6 +102,6 @@ func main() { err := app.Run(os.Args) if err != nil { - log.Fatalf("Error, %v", err) + log.Fatalf("error, %v", err) } } diff --git a/pkg/cli/user/user.go b/pkg/cli/user/user.go index e3fa42e..97bc28d 100644 --- a/pkg/cli/user/user.go +++ b/pkg/cli/user/user.go @@ -10,7 +10,7 @@ import ( "github.com/urfave/cli/v2" ) -func Add(c *cli.Context) error { +func Create(c *cli.Context) error { username := c.String("username") if username == "" { return fmt.Errorf("username argument required") @@ -28,7 +28,7 @@ func Add(c *cli.Context) error { case "bypass": case "disabled": default: - return fmt.Errorf("status not set to active, bypass or disabled, %s", status) + return fmt.Errorf("status not set to active, bypass or disabled: %s", status) } } @@ -38,7 +38,7 @@ func Add(c *cli.Context) error { return err } - log.Printf("adding user: %s", username) + log.Printf("adding user %s", username) user := admin.User{ Username: username, @@ -54,10 +54,10 @@ func Add(c *cli.Context) error { } if result.Stat != "OK" { - return fmt.Errorf("Duo API returned non-ok status response on creating user: %s with message: %s", username, *result.Message) + return fmt.Errorf("Duo API returned non-ok status response when creating user %s, message: %s", username, *result.Message) } - return associateGroupsWithUser(result.Response.UserID, groups, adm) + return associateGroupsWithUser(result.Response, groups, adm) } func Modify(c *cli.Context) error { @@ -68,6 +68,7 @@ func Modify(c *cli.Context) error { firstName := c.String("firstName") lastName := c.String("lastName") status := c.String("status") + create := c.Bool("create") if status != "" { switch status { @@ -89,8 +90,8 @@ func Modify(c *cli.Context) error { if err != nil { return err } - if len(getUser.Response) == 0 { - log.Printf("warning, user not found %s", username) + if len(getUser.Response) == 0 && !create { + return fmt.Errorf("user not found %s", username) } if len(getUser.Response) > 1 { return fmt.Errorf("more than one user found with this username or alias") @@ -104,87 +105,118 @@ func Modify(c *cli.Context) error { Status: status, } - log.Printf("updating user: %s", username) - - result, err := adm.ModifyUser(getUser.Response[0].UserID, user.URLValues()) + var result *admin.GetUserResult + if create && len(getUser.Response) == 0 { + log.Printf("adding user %s", username) + result, err = adm.CreateUser(user.URLValues()) + } else { + log.Printf("updating user %s", username) + result, err = adm.ModifyUser(getUser.Response[0].UserID, user.URLValues()) + } if err != nil { return err } if result.Stat != "OK" { - return fmt.Errorf("Duo API returned non-ok status response on modifying user: %s with message: %s", username, *result.Message) + return fmt.Errorf("Duo API returned non-ok status response when modifying user %s, message: %s", username, *result.Message) } - if err := associateGroupsWithUser(result.Response.UserID, addgroups, adm); err != nil { + if err := associateGroupsWithUser(result.Response, addgroups, adm); err != nil { return err } - return disassociateGroupsWithUser(result.Response.UserID, delgroups, adm) + // Don't try to delete groups if the user was just created + if create && len(getUser.Response) == 0 { + return nil + } + + return disassociateGroupsWithUser(result.Response, delgroups, adm) } -func associateGroupsWithUser(userID string, groups []string, adm *admin.Client) error { +func associateGroupsWithUser(user admin.User, groups []string, adm *admin.Client) error { if len(groups) > 0 { duoGroups, err := adm.GetGroups() if err != nil { return err } - duoGroupsResp := duoGroups.Response + GROUP: for _, group := range groups { + for _, userDuoGroup := range user.Groups { + if group == userDuoGroup.Name { + log.Printf("group %s is already associated with user %s, skipping", group, user.Username) + continue GROUP + } + } var grpFound string - for _, duoGroup := range duoGroupsResp { + for _, duoGroup := range duoGroups.Response { if group == duoGroup.Name { grpFound = duoGroup.GroupID } } if grpFound != "" { - result, err := adm.AssociateGroupWithUser(userID, grpFound) + log.Printf("associating group %s with user %s", group, user.Username) + result, err := adm.AssociateGroupWithUser(user.UserID, grpFound) if err != nil { return err } if result.Stat != "OK" { - return fmt.Errorf("Duo API returned non-ok status response on associating user with group: %s with message: %s", group, *result.Message) + return fmt.Errorf("Duo API returned non-ok status response when associating user with group %s, message: %s", group, *result.Message) } } else { - log.Printf("warning, specified group not found in Duo: %s", group) + log.Printf("warning, group %s not found in Duo, skipping", group) + continue } } } return nil } -func disassociateGroupsWithUser(userID string, groups []string, adm *admin.Client) error { +func disassociateGroupsWithUser(user admin.User, groups []string, adm *admin.Client) error { if len(groups) > 0 { duoGroups, err := adm.GetGroups() if err != nil { return err } - duoGroupsResp := duoGroups.Response for _, group := range groups { + var existGroupFound bool + for _, userDuoGroup := range user.Groups { + if group == userDuoGroup.Name { + existGroupFound = true + } + } + + if !existGroupFound { + log.Printf("group %s is not associated with user %s, skipping", group, user.Username) + continue + } + var grpFound string - for _, duoGroup := range duoGroupsResp { + for _, duoGroup := range duoGroups.Response { if group == duoGroup.Name { grpFound = duoGroup.GroupID } } if grpFound != "" { - result, err := adm.DisassociateGroupFromUser(userID, grpFound) + log.Printf("disassociating group %s from user %s", group, user.Username) + result, err := adm.DisassociateGroupFromUser(user.UserID, grpFound) if err != nil { return err } if result.Stat != "OK" { - return fmt.Errorf("Duo API returned non-ok status response on disassociating user with group: %s, with message: %s", group, *result.Message) + return fmt.Errorf("Duo API returned non-ok status response when disassociating user with group %s, message: %s", group, *result.Message) } } else { - log.Printf("warning, specified group not found in Duo: %s", group) + log.Printf("warning, group %s not found in Duo, skipping", group) + continue } } } return nil } -func Remove(c *cli.Context) error { +func Delete(c *cli.Context) error { usernames := c.StringSlice("username") devices := c.Bool("devices") @@ -203,7 +235,7 @@ func Remove(c *cli.Context) error { return err } if result.Stat != "OK" { - return fmt.Errorf("Duo API returned non-ok status response on searching for user: %s, with message: %s", user, *result.Message) + return fmt.Errorf("Duo API returned non-ok status response on searching for user: %s, message: %s", user, *result.Message) } if len(result.Response) == 0 { log.Printf("warning, user %s not found, skipping", user) @@ -216,16 +248,25 @@ func Remove(c *cli.Context) error { if devices { for _, phone := range result.Response[0].Phones { - log.Printf("removing phone %s from user %s", phone.Name, user) + log.Printf("deleting phone %s from user %s", phone.Name, user) phoneResult, err := adm.DeletePhone(phone.PhoneID) if err != nil { return err } if phoneResult.Stat != "OK" { - log.Printf("warning, Duo API returned non-ok status response on remove phone: %s for user: %s, with message: %s", phone.Name, user, *result.Message) + log.Printf("warning, Duo API returned non-ok status response when deleting phone: %s for user: %s, message: %s", phone.Name, user, *result.Message) } } } + + log.Printf("deleting user %s", user) + deleteResult, err := adm.DeleteUser(result.Response[0].UserID) + if err != nil { + return err + } + if deleteResult.Stat != "OK" { + log.Printf("warning, Duo API returned non-ok status response when deleting user: %s, message: %s", user, *result.Message) + } } return nil }