Skip to content

Commit

Permalink
Rename add command to create, remove to delete to match API naming. A…
Browse files Browse the repository at this point in the history
…dd --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.
  • Loading branch information
bensallen committed Mar 8, 2021
1 parent 689b527 commit 633df64
Show file tree
Hide file tree
Showing 2 changed files with 79 additions and 37 deletions.
17 changes: 9 additions & 8 deletions cmd/duocli/duocli.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Expand Down Expand Up @@ -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},
},
},
},
Expand All @@ -101,6 +102,6 @@ func main() {

err := app.Run(os.Args)
if err != nil {
log.Fatalf("Error, %v", err)
log.Fatalf("error, %v", err)
}
}
99 changes: 70 additions & 29 deletions pkg/cli/user/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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)

}
}
Expand All @@ -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,
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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")
Expand All @@ -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")

Expand All @@ -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)
Expand All @@ -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
}
Expand Down

0 comments on commit 633df64

Please sign in to comment.