Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions cmd/unlock_user.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package cmd

import (
"fmt"

"code.cloudfoundry.org/uaa-cli/cli"
"code.cloudfoundry.org/uaa-cli/config"
"code.cloudfoundry.org/uaa-cli/utils"
"errors"
"github.com/cloudfoundry-community/go-uaa"
"github.com/spf13/cobra"
)

func UnlockUserCmd(api *uaa.API, username, origin, attributes, zoneID string) error {
user, err := api.GetUserByUsername(username, origin, attributes)
if err != nil {
return err
}
if user.Meta == nil {
return errors.New("The user did not have expected metadata version.")
}

err = unlockUserByID(api, user.ID, zoneID)
if err != nil {
return err
}

log.Infof("Account for user %v successfully unlocked.", utils.Emphasize(user.Username))
return nil
}

// unlockUserByID makes a PATCH request to /Users/{id}/status with {"locked": false}
func unlockUserByID(api *uaa.API, userID, zoneID string) error {
path := fmt.Sprintf("/Users/%s/status", userID)
data := `{"locked": false}`

headers := []string{"Content-Type: application/json"}
if zoneID != "" {
headers = append(headers, fmt.Sprintf("X-Identity-Zone-Id: %s", zoneID))
}
Comment on lines +23 to +40

_, _, status, err := api.Curl(path, "PATCH", data, headers)
if err != nil {
return err
}

if status >= 400 {
return fmt.Errorf("unlock user failed with status %d", status)
}
Comment on lines +42 to +49

return nil
}

func UnlockUserValidations(cfg config.Config, args []string) error {
if err := cli.EnsureContextInConfig(cfg); err != nil {
return err
}

if len(args) == 0 {
return errors.New("The positional argument USERNAME must be specified.")
}
return nil
}

var unlockUserCmd = &cobra.Command{
Use: "unlock-user USERNAME",
Short: "Unlock a user account by username",
PreRun: func(cmd *cobra.Command, args []string) {
cli.NotifyValidationErrors(UnlockUserValidations(GetSavedConfig(), args), cmd, log)
},
Run: func(cmd *cobra.Command, args []string) {
cfg := GetSavedConfig()

if zoneSubdomain == "" {
zoneSubdomain = cfg.ZoneSubdomain
}

token := cfg.GetActiveContext().Token
api, err := uaa.New(
cfg.GetActiveTarget().BaseUrl,
uaa.WithToken(&token),
uaa.WithZoneID(zoneSubdomain),
uaa.WithSkipSSLValidation(cfg.GetActiveTarget().SkipSSLValidation),
uaa.WithVerbosity(verbose),
)
if err != nil {
cli.NotifyErrorsWithRetry(err, log, GetSavedConfig())
return
}

err = UnlockUserCmd(api, args[0], origin, attributes, zoneSubdomain)
cli.NotifyErrorsWithRetry(err, log, GetSavedConfig())
},
}

func init() {
RootCmd.AddCommand(unlockUserCmd)
unlockUserCmd.Annotations = make(map[string]string)
unlockUserCmd.Annotations[USER_CRUD_CATEGORY] = "true"

unlockUserCmd.Flags().StringVarP(&zoneSubdomain, "zone", "z", "", "the identity zone subdomain from which to unlock the user")
unlockUserCmd.Flags().StringVarP(&origin, "origin", "o", "", "the identity provider in which to search. Examples: uaa, ldap, etc.")
}
158 changes: 158 additions & 0 deletions cmd/unlock_user_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package cmd_test

import (
"net/http"

"code.cloudfoundry.org/uaa-cli/config"
"code.cloudfoundry.org/uaa-cli/fixtures"
"github.com/cloudfoundry-community/go-uaa"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
. "github.com/onsi/gomega/gbytes"
. "github.com/onsi/gomega/gexec"
. "github.com/onsi/gomega/ghttp"
)

var _ = Describe("UnlockUser", func() {
BeforeEach(func() {
c := config.NewConfigWithServerURL(server.URL())
ctx := config.NewContextWithToken("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ")
c.AddContext(ctx)
Expect(config.WriteConfig(c)).Should(Succeed())
})

It("unlocks a user", func() {
server.RouteToHandler("GET", "/Users", CombineHandlers(
VerifyRequest("GET", "/Users", "count=100&filter=userName+eq+%22woodstock%40peanuts.com%22&startIndex=1"),
RespondWith(http.StatusOK, fixtures.PaginatedResponse(uaa.User{Username: "woodstock@peanuts.com", ID: "abcdef", Meta: &uaa.Meta{Version: 10}})),
))
server.RouteToHandler("PATCH", "/Users/abcdef/status", CombineHandlers(
VerifyRequest("PATCH", "/Users/abcdef/status", ""),
VerifyJSON(`{"locked": false}`),
RespondWith(http.StatusOK, `{"locked": false}`),
))

session := runCommand("unlock-user", "woodstock@peanuts.com")

Expect(server.ReceivedRequests()).To(HaveLen(2))
Eventually(session).Should(Exit(0))
Expect(session.Out).To(Say("Account for user woodstock@peanuts.com successfully unlocked."))
})

It("unlocks a user with --verbose", func() {
server.RouteToHandler("GET", "/Users", CombineHandlers(
VerifyRequest("GET", "/Users", "count=100&filter=userName+eq+%22woodstock%40peanuts.com%22&startIndex=1"),
RespondWith(http.StatusOK, fixtures.PaginatedResponse(uaa.User{Username: "woodstock@peanuts.com", ID: "abcdef", Meta: &uaa.Meta{Version: 10}})),
))
server.RouteToHandler("PATCH", "/Users/abcdef/status", CombineHandlers(
VerifyRequest("PATCH", "/Users/abcdef/status", ""),
VerifyJSON(`{"locked": false}`),
RespondWith(http.StatusOK, `{"locked": false}`),
))

session := runCommand("unlock-user", "woodstock@peanuts.com", "--verbose")

Expect(server.ReceivedRequests()).To(HaveLen(2))
Eventually(session).Should(Exit(0))
Expect(session.Out).To(Say("Account for user woodstock@peanuts.com successfully unlocked."))
})

It("unlocks a user with --origin", func() {
server.RouteToHandler("GET", "/Users", CombineHandlers(
VerifyRequest("GET", "/Users", "count=100&filter=userName+eq+%22woodstock%40peanuts.com%22+and+origin+eq+%22ldap%22&startIndex=1"),
RespondWith(http.StatusOK, fixtures.PaginatedResponse(uaa.User{Username: "woodstock@peanuts.com", ID: "abcdef", Meta: &uaa.Meta{Version: 10}})),
))
server.RouteToHandler("PATCH", "/Users/abcdef/status", CombineHandlers(
VerifyRequest("PATCH", "/Users/abcdef/status", ""),
VerifyJSON(`{"locked": false}`),
RespondWith(http.StatusOK, `{"locked": false}`),
))

session := runCommand("unlock-user", "woodstock@peanuts.com", "--origin", "ldap")

Expect(server.ReceivedRequests()).To(HaveLen(2))
Eventually(session).Should(Exit(0))
Expect(session.Out).To(Say("Account for user woodstock@peanuts.com successfully unlocked."))
})

It("unlocks a user with --zone", func() {
server.RouteToHandler("GET", "/Users", CombineHandlers(
VerifyRequest("GET", "/Users", "count=100&filter=userName+eq+%22woodstock%40peanuts.com%22&startIndex=1"),
VerifyHeaderKV("X-Identity-Zone-Id", "test-zone"),
RespondWith(http.StatusOK, fixtures.PaginatedResponse(uaa.User{Username: "woodstock@peanuts.com", ID: "abcdef", Meta: &uaa.Meta{Version: 10}})),
))
server.RouteToHandler("PATCH", "/Users/abcdef/status", CombineHandlers(
VerifyRequest("PATCH", "/Users/abcdef/status", ""),
VerifyHeaderKV("X-Identity-Zone-Id", "test-zone"),
VerifyJSON(`{"locked": false}`),
RespondWith(http.StatusOK, `{"locked": false}`),
))

session := runCommand("unlock-user", "woodstock@peanuts.com", "--zone", "test-zone")

Expect(server.ReceivedRequests()).To(HaveLen(2))
Eventually(session).Should(Exit(0))
Expect(session.Out).To(Say("Account for user woodstock@peanuts.com successfully unlocked."))
})

Describe("error conditions", func() {
It("displays error when user not found", func() {
server.RouteToHandler("GET", "/Users", CombineHandlers(
VerifyRequest("GET", "/Users", "count=100&filter=userName+eq+%22nobody%22&startIndex=1"),
RespondWith(http.StatusNotFound, `{"error": "scim_resource_not_found", "error_description": "User nobody does not exist"}`),
))

session := runCommand("unlock-user", "nobody")

Eventually(session).Should(Exit(1))
})

It("displays error when unlock request fails", func() {
server.RouteToHandler("GET", "/Users", CombineHandlers(
VerifyRequest("GET", "/Users", "count=100&filter=userName+eq+%22woodstock%40peanuts.com%22&startIndex=1"),
RespondWith(http.StatusOK, fixtures.PaginatedResponse(uaa.User{Username: "woodstock@peanuts.com", ID: "abcdef", Meta: &uaa.Meta{Version: 10}})),
))
server.RouteToHandler("PATCH", "/Users/abcdef/status", CombineHandlers(
VerifyRequest("PATCH", "/Users/abcdef/status", ""),
RespondWith(http.StatusBadRequest, `{"error": "invalid_user", "error_description": "User cannot be unlocked"}`),
))

session := runCommand("unlock-user", "woodstock@peanuts.com")

Eventually(session).Should(Exit(1))
})
})

Describe("validations", func() {
It("requires a target", func() {
config.WriteConfig(config.NewConfig())

session := runCommand("unlock-user", "woodstock@peanuts.com")

Expect(session.Err).To(Say("You must set a target in order to use this command."))
Expect(session).Should(Exit(1))
})

It("requires a context", func() {
cfg := config.NewConfigWithServerURL(server.URL())
config.WriteConfig(cfg)

session := runCommand("unlock-user", "woodstock@peanuts.com")

Expect(session.Err).To(Say("You must have a token in your context to perform this command."))
Expect(session).Should(Exit(1))
})

It("requires a username", func() {
c := config.NewConfigWithServerURL(server.URL())
ctx := config.NewContextWithToken("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ")
c.AddContext(ctx)
config.WriteConfig(c)

session := runCommand("unlock-user")

Expect(session.Err).To(Say("The positional argument USERNAME must be specified."))
Expect(session).Should(Exit(1))
})
})
})
1 change: 1 addition & 0 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ Each command name below links to a page with a full description, including all a
| [`delete-user`](commands/delete-user.md) | Delete a user by username |
| [`activate-user`](commands/activate-user.md) | Activate a user by username |
| [`deactivate-user`](commands/deactivate-user.md) | Deactivate a user by username |
| [`unlock-user`](commands/unlock-user.md) | Unlock a user account by username |

## Managing Groups

Expand Down
42 changes: 42 additions & 0 deletions docs/commands/unlock-user.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# unlock-user

[← Command Reference](../commands.md)

Unlock a user account by username. This removes lockouts caused by failed login attempts.

## Usage

```
uaa unlock-user USERNAME [flags]
```

## Flags

| Flag | Short | Default | Description |
|------|-------|---------|-------------|
| `--origin` | `-o` | | Identity provider to search for the user (e.g., uaa, ldap) |
| `--zone` | `-z` | | Identity zone subdomain from which to unlock the user |

## Global Flags

| Flag | Short | Description |
|------|-------|-------------|
| `--verbose` | `-v` | Print additional info on HTTP requests |

## Examples

```bash
uaa unlock-user bob
uaa unlock-user bob --origin ldap
uaa unlock-user bob --zone my-zone
```

## See Also

- [activate-user](activate-user.md)
- [deactivate-user](deactivate-user.md)
- [get-user](get-user.md)

---

[← Command Reference](../commands.md)