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
121 changes: 121 additions & 0 deletions cmd/set_password.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
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 SetPasswordCmd(api *uaa.API, username, origin, attributes, password, 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.")
}
Comment on lines +19 to +21

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

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

// setPasswordByID makes a PUT request to /Users/{id}/password with {"password": "newpassword"}
func setPasswordByID(api *uaa.API, userID, password, zoneID string) error {
path := fmt.Sprintf("/Users/%s/password", userID)
data := fmt.Sprintf(`{"password": "%s"}`, password)

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

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

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

return nil
}

func SetPasswordValidations(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 setPasswordCmd = &cobra.Command{
Use: "set-password USERNAME",
Short: "Set password for a user (admin)",
PreRun: func(cmd *cobra.Command, args []string) {
cli.NotifyValidationErrors(SetPasswordValidations(GetSavedConfig(), args), cmd, log)
},
Run: func(cmd *cobra.Command, args []string) {
cfg := GetSavedConfig()

// Get password from flag or prompt if not provided
if userPassword == "" {
secret := cli.InteractiveSecret{Prompt: "New password"}
var err error
userPassword, err = secret.Get()
if err != nil {
cli.NotifyErrorsWithRetry(err, log, GetSavedConfig())
return
}
}
Comment on lines +75 to +84

if userPassword == "" {
cli.NotifyErrorsWithRetry(errors.New("Password must be specified with --password flag or entered when prompted."), log, GetSavedConfig())
return
}

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 = SetPasswordCmd(api, args[0], origin, attributes, userPassword, zoneSubdomain)
cli.NotifyErrorsWithRetry(err, log, GetSavedConfig())
},
}

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

setPasswordCmd.Flags().StringVarP(&userPassword, "password", "p", "", "new password for the user")
setPasswordCmd.Flags().StringVarP(&zoneSubdomain, "zone", "z", "", "the identity zone subdomain in which to set the password")
setPasswordCmd.Flags().StringVarP(&origin, "origin", "o", "", "the identity provider in which to search. Examples: uaa, ldap, etc.")
}
172 changes: 172 additions & 0 deletions cmd/set_password_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
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("SetPassword", func() {
BeforeEach(func() {
c := config.NewConfigWithServerURL(server.URL())
ctx := config.NewContextWithToken("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ")
c.AddContext(ctx)
Expect(config.WriteConfig(c)).Should(Succeed())
})

It("sets a user password", func() {
server.RouteToHandler("GET", "/Users", CombineHandlers(
VerifyRequest("GET", "/Users", "count=100&filter=userName+eq+%22testuser%22&startIndex=1"),
RespondWith(http.StatusOK, fixtures.PaginatedResponse(uaa.User{Username: "testuser", ID: "abcdef", Meta: &uaa.Meta{Version: 10}})),
))
server.RouteToHandler("PUT", "/Users/abcdef/password", CombineHandlers(
VerifyRequest("PUT", "/Users/abcdef/password", ""),
VerifyJSON(`{"password": "newpass"}`),
RespondWith(http.StatusOK, `{"message": "password updated"}`),
))

session := runCommand("set-password", "testuser", "--password", "newpass")

Expect(server.ReceivedRequests()).To(HaveLen(2))
Eventually(session).Should(Exit(0))
Expect(session.Out).To(Say("Password for user testuser successfully set."))
})

It("sets a user password with --verbose", func() {
server.RouteToHandler("GET", "/Users", CombineHandlers(
VerifyRequest("GET", "/Users", "count=100&filter=userName+eq+%22testuser%22&startIndex=1"),
RespondWith(http.StatusOK, fixtures.PaginatedResponse(uaa.User{Username: "testuser", ID: "abcdef", Meta: &uaa.Meta{Version: 10}})),
))
server.RouteToHandler("PUT", "/Users/abcdef/password", CombineHandlers(
VerifyRequest("PUT", "/Users/abcdef/password", ""),
VerifyJSON(`{"password": "newpass"}`),
RespondWith(http.StatusOK, `{"message": "password updated"}`),
))

session := runCommand("set-password", "testuser", "--password", "newpass", "--verbose")

Expect(server.ReceivedRequests()).To(HaveLen(2))
Eventually(session).Should(Exit(0))
Expect(session.Out).To(Say("Password for user testuser successfully set."))
})

It("sets a user password with --origin", func() {
server.RouteToHandler("GET", "/Users", CombineHandlers(
VerifyRequest("GET", "/Users", "count=100&filter=userName+eq+%22testuser%22+and+origin+eq+%22ldap%22&startIndex=1"),
RespondWith(http.StatusOK, fixtures.PaginatedResponse(uaa.User{Username: "testuser", ID: "abcdef", Meta: &uaa.Meta{Version: 10}})),
))
server.RouteToHandler("PUT", "/Users/abcdef/password", CombineHandlers(
VerifyRequest("PUT", "/Users/abcdef/password", ""),
VerifyJSON(`{"password": "newpass"}`),
RespondWith(http.StatusOK, `{"message": "password updated"}`),
))

session := runCommand("set-password", "testuser", "--password", "newpass", "--origin", "ldap")

Expect(server.ReceivedRequests()).To(HaveLen(2))
Eventually(session).Should(Exit(0))
Expect(session.Out).To(Say("Password for user testuser successfully set."))
})

It("sets a user password with --zone", func() {
server.RouteToHandler("GET", "/Users", CombineHandlers(
VerifyRequest("GET", "/Users", "count=100&filter=userName+eq+%22testuser%22&startIndex=1"),
VerifyHeaderKV("X-Identity-Zone-Id", "test-zone"),
RespondWith(http.StatusOK, fixtures.PaginatedResponse(uaa.User{Username: "testuser", ID: "abcdef", Meta: &uaa.Meta{Version: 10}})),
))
server.RouteToHandler("PUT", "/Users/abcdef/password", CombineHandlers(
VerifyRequest("PUT", "/Users/abcdef/password", ""),
VerifyHeaderKV("X-Identity-Zone-Id", "test-zone"),
VerifyJSON(`{"password": "newpass"}`),
RespondWith(http.StatusOK, `{"message": "password updated"}`),
))

session := runCommand("set-password", "testuser", "--password", "newpass", "--zone", "test-zone")

Expect(server.ReceivedRequests()).To(HaveLen(2))
Eventually(session).Should(Exit(0))
Expect(session.Out).To(Say("Password for user testuser successfully set."))
})

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("set-password", "nobody", "--password", "newpass")

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

It("displays error when password change request fails", func() {
server.RouteToHandler("GET", "/Users", CombineHandlers(
VerifyRequest("GET", "/Users", "count=100&filter=userName+eq+%22testuser%22&startIndex=1"),
RespondWith(http.StatusOK, fixtures.PaginatedResponse(uaa.User{Username: "testuser", ID: "abcdef", Meta: &uaa.Meta{Version: 10}})),
))
server.RouteToHandler("PUT", "/Users/abcdef/password", CombineHandlers(
VerifyRequest("PUT", "/Users/abcdef/password", ""),
RespondWith(http.StatusBadRequest, `{"error": "invalid_password", "error_description": "Password does not meet policy requirements"}`),
))

session := runCommand("set-password", "testuser", "--password", "weak")

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

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

session := runCommand("set-password", "testuser", "--password", "newpass")

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("set-password", "testuser", "--password", "newpass")

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("set-password")

Expect(session.Err).To(Say("The positional argument USERNAME must be specified."))
Expect(session).Should(Exit(1))
})

It("displays help when no password provided (interactive mode skipped in tests)", func() {
c := config.NewConfigWithServerURL(server.URL())
ctx := config.NewContextWithToken("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ")
c.AddContext(ctx)
config.WriteConfig(c)

// In test environment, when no password is provided, the interactive prompt
// will fail due to inappropriate ioctl. This tests that the validation
// is working even though the specific error may vary in test vs. real usage.
session := runCommand("set-password", "testuser")

Eventually(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 |
| [`set-password`](commands/set-password.md) | Set password for a user (admin) |

## Managing Groups

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

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

Set password for a user account by username (admin operation). This command allows administrators to change a user's password without knowing the current password.

## Usage

```
uaa set-password USERNAME [flags]
```

## Flags

| Flag | Short | Default | Description |
|------|-------|---------|-------------|
| `--password` | `-p` | | New password for the user (will prompt if not provided) |
| `--origin` | `-o` | | Identity provider in which to search. Examples: uaa, ldap, etc. |
| `--zone` | `-z` | | Identity zone subdomain in which to set the password |

## Global Flags

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

## Examples

```bash
# Set password interactively (will prompt)
uaa set-password testuser

# Set password via command line flag
uaa set-password testuser --password newpassword123

# Set password for user in specific origin
uaa set-password testuser --password newpass --origin ldap

# Set password for user in specific zone
uaa set-password testuser --password newpass --zone my-zone

# Use verbose mode to see request details
uaa set-password testuser --password newpass --verbose
```

## Authentication Requirements

This command requires administrative privileges. You need a token with `password.write` or `scim.write` scopes.

## See Also

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

---

[← Command Reference](../commands.md)
4 changes: 2 additions & 2 deletions docs/migrating-from-uaac.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,8 @@ The `uaa` CLI outputs a combination of human-readable status messages and JSON d
| `uaac user deactivate [name]` | [`uaa deactivate-user USERNAME`](commands/deactivate-user.md) | |
| `uaac user update [name]` | *(no equivalent)* | Use `uaa curl /Users/USER_ID -X PUT -d '{...}'` |
| `uaac user ids [username\|id...]` | *(no equivalent)* | Use `uaa get-user USERNAME` for individual lookups |
| `uaac user unlock [name]` | *(no equivalent)* | Use `uaa curl /Users/USER_ID/status -X PATCH -d '{"locked":false}'` |
| `uaac password set [name]` | *(no equivalent)* | Use `uaa curl /Users/USER_ID/password -X PUT -d '{"password":"NEW"}'` |
| `uaac user unlock [name]` | [`uaa unlock-user USERNAME`](commands/unlock-user.md) | |
| `uaac password set [name]` | [`uaa set-password USERNAME --password NEWPASS`](commands/set-password.md) | |
| `uaac password change` | *(no equivalent)* | Use `uaa curl /Users/USER_ID/password -X PUT -d '{"oldPassword":"OLD","password":"NEW"}'` |

### Clients
Expand Down