diff --git a/cmd/change_client_secret.go b/cmd/change_client_secret.go index 12465fc..40afa5e 100644 --- a/cmd/change_client_secret.go +++ b/cmd/change_client_secret.go @@ -33,14 +33,18 @@ func ChangeClientSecretValidation(cfg config.Config, oldSecret, newSecret string return nil } +type changeClientSecretRequest struct { + OldSecret string `json:"oldSecret"` + Secret string `json:"secret"` +} + func ChangeClientSecretCmd(api *uaa.API, log cli.Logger, cfg config.Config, oldSecret, newSecret string) error { context := cfg.GetActiveContext() clientId := context.ClientId - // Prepare the request body for the secret change - requestBody := map[string]interface{}{ - "oldSecret": oldSecret, - "secret": newSecret, + requestBody := changeClientSecretRequest{ + OldSecret: oldSecret, + Secret: newSecret, } requestBodyJSON, err := json.Marshal(requestBody) @@ -48,11 +52,11 @@ func ChangeClientSecretCmd(api *uaa.API, log cli.Logger, cfg config.Config, oldS return err } - // Make the API call to change the client secret + // api.Curl bypasses the go-uaa SDK's structured request path, so the zone + // header that GetAPIFromSavedTokenInContext's WithZoneID would otherwise + // add automatically must be set explicitly here. path := fmt.Sprintf("/oauth/clients/%s/secret", clientId) headers := []string{"Content-Type: application/json"} - - // Add zone header if specified if cfg.ZoneSubdomain != "" { headers = append(headers, fmt.Sprintf("X-Identity-Zone-Id: %s", cfg.ZoneSubdomain)) } diff --git a/cmd/update_user.go b/cmd/update_user.go index 2655984..b0fc791 100644 --- a/cmd/update_user.go +++ b/cmd/update_user.go @@ -78,13 +78,16 @@ func UpdateUserCmd(api *uaa.API, printer cli.Printer, username, familyName, give return printer.Print(updatedUser) } -func UpdateUserValidation(cfg config.Config, args []string) error { +func UpdateUserValidation(cfg config.Config, args []string, familyName, givenName string, emails, phones, delAttrs []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.") } + if familyName == "" && givenName == "" && len(emails) == 0 && len(phones) == 0 && len(delAttrs) == 0 { + return errors.New("At least one of --familyName, --givenName, --email, --phone, or --delAttrs must be specified.") + } return nil } @@ -92,7 +95,7 @@ var updateUserCmd = &cobra.Command{ Use: "update-user USERNAME", Short: "Update a user account", PreRun: func(cmd *cobra.Command, args []string) { - cli.NotifyValidationErrors(UpdateUserValidation(GetSavedConfig(), args), cmd, log) + cli.NotifyValidationErrors(UpdateUserValidation(GetSavedConfig(), args, familyName, givenName, emails, phoneNumbers, delAttrs), cmd, log) }, Run: func(cmd *cobra.Command, args []string) { cfg := GetSavedConfig() @@ -111,11 +114,11 @@ func init() { updateUserCmd.Annotations = make(map[string]string) updateUserCmd.Annotations[USER_CRUD_CATEGORY] = "true" - updateUserCmd.Flags().StringVarP(&familyName, "family_name", "", "", "family name") - updateUserCmd.Flags().StringVarP(&givenName, "given_name", "", "", "given name") + updateUserCmd.Flags().StringVarP(&familyName, "familyName", "", "", "family name") + updateUserCmd.Flags().StringVarP(&givenName, "givenName", "", "", "given name") updateUserCmd.Flags().StringVarP(&origin, "origin", "o", "", "user origin") - updateUserCmd.Flags().StringSliceVarP(&emails, "emails", "", []string{}, "email addresses (multiple may be specified)") - updateUserCmd.Flags().StringSliceVarP(&phoneNumbers, "phones", "", []string{}, "phone numbers (multiple may be specified)") - updateUserCmd.Flags().StringSliceVarP(&delAttrs, "del_attrs", "", []string{}, "attributes to remove (phoneNumbers, name, etc.)") + updateUserCmd.Flags().StringSliceVarP(&emails, "email", "", []string{}, "email address (multiple may be specified)") + updateUserCmd.Flags().StringSliceVarP(&phoneNumbers, "phone", "", []string{}, "phone number (multiple may be specified)") + updateUserCmd.Flags().StringSliceVarP(&delAttrs, "delAttrs", "", []string{}, "attributes to remove (phoneNumbers, name, etc.)") updateUserCmd.Flags().StringVarP(&zoneSubdomain, "zone", "z", "", "the identity zone subdomain in which to update the user") } \ No newline at end of file diff --git a/cmd/update_user_test.go b/cmd/update_user_test.go index 163090c..2ea3574 100644 --- a/cmd/update_user_test.go +++ b/cmd/update_user_test.go @@ -1,6 +1,11 @@ package cmd_test import ( + "bytes" + "encoding/json" + "io" + "net/http" + "code.cloudfoundry.org/uaa-cli/cli" "code.cloudfoundry.org/uaa-cli/config" "code.cloudfoundry.org/uaa-cli/fixtures" @@ -10,9 +15,19 @@ import ( . "github.com/onsi/gomega/gbytes" . "github.com/onsi/gomega/gexec" . "github.com/onsi/gomega/ghttp" - "net/http" ) +// captureRequestBody records the raw request body into dest and restores it +// so downstream ghttp handlers (e.g. VerifyRequest) can still read it. +func captureRequestBody(dest *[]byte) http.HandlerFunc { + return func(w http.ResponseWriter, req *http.Request) { + body, err := io.ReadAll(req.Body) + Expect(err).NotTo(HaveOccurred()) + *dest = body + req.Body = io.NopCloser(bytes.NewReader(body)) + } +} + var _ = Describe("UpdateUser", func() { BeforeEach(func() { cfg := config.NewConfigWithServerURL(server.URL()) @@ -45,11 +60,21 @@ var _ = Describe("UpdateUser", func() { Eventually(session).Should(Exit(1)) Expect(session.Err).To(Say("The positional argument USERNAME must be specified.")) }) + + It("requires at least one update flag", func() { + session := runCommand("update-user", "marcus@stoicism.com") + + Eventually(session).Should(Exit(1)) + Expect(session.Err).To(Say("At least one of --familyName, --givenName, --email, --phone, or --delAttrs must be specified.")) + Expect(server.ReceivedRequests()).To(HaveLen(0)) + }) }) Describe("UpdateUserCmd", func() { Describe("Success cases", func() { It("updates user with given_name only", func() { + var putBody []byte + // First GET to retrieve user server.RouteToHandler("GET", "/Users", CombineHandlers( RespondWith(http.StatusOK, fixtures.PaginatedResponse(uaa.User{ID: "fb5f32e1-5cb3-49e6-93df-6df9c8c8bd70", Username: "marcus@stoicism.com"})), @@ -60,6 +85,7 @@ var _ = Describe("UpdateUser", func() { // Then PUT to update user server.RouteToHandler("PUT", "/Users/fb5f32e1-5cb3-49e6-93df-6df9c8c8bd70", CombineHandlers( + captureRequestBody(&putBody), RespondWith(http.StatusOK, fixtures.MarcusUserResponse), VerifyRequest("PUT", "/Users/fb5f32e1-5cb3-49e6-93df-6df9c8c8bd70"), VerifyHeaderKV("Authorization", "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"), @@ -67,14 +93,22 @@ var _ = Describe("UpdateUser", func() { VerifyHeaderKV("Content-Type", "application/json"), )) - session := runCommand("update-user", "marcus@stoicism.com", "--given_name", "Bob") + session := runCommand("update-user", "marcus@stoicism.com", "--givenName", "Bob") Expect(server.ReceivedRequests()).To(HaveLen(2)) Expect(session).To(Exit(0)) Expect(session.Out).To(Say("Account for user marcus@stoicism.com successfully updated")) + + var payload map[string]interface{} + Expect(json.Unmarshal(putBody, &payload)).To(Succeed()) + name, ok := payload["name"].(map[string]interface{}) + Expect(ok).To(BeTrue()) + Expect(name["givenName"]).To(Equal("Bob")) }) It("updates user with multiple attributes", func() { + var putBody []byte + // First GET to retrieve user server.RouteToHandler("GET", "/Users", CombineHandlers( RespondWith(http.StatusOK, fixtures.PaginatedResponse(uaa.User{ID: "fb5f32e1-5cb3-49e6-93df-6df9c8c8bd70", Username: "marcus@stoicism.com"})), @@ -83,17 +117,25 @@ var _ = Describe("UpdateUser", func() { // Then PUT to update user server.RouteToHandler("PUT", "/Users/fb5f32e1-5cb3-49e6-93df-6df9c8c8bd70", CombineHandlers( + captureRequestBody(&putBody), RespondWith(http.StatusOK, fixtures.MarcusUserResponse), VerifyRequest("PUT", "/Users/fb5f32e1-5cb3-49e6-93df-6df9c8c8bd70"), )) - session := runCommand("update-user", "marcus@stoicism.com", - "--given_name", "Bob", - "--family_name", "Smith") + session := runCommand("update-user", "marcus@stoicism.com", + "--givenName", "Bob", + "--familyName", "Smith") Expect(server.ReceivedRequests()).To(HaveLen(2)) Expect(session).To(Exit(0)) Expect(session.Out).To(Say("Account for user marcus@stoicism.com successfully updated")) + + var payload map[string]interface{} + Expect(json.Unmarshal(putBody, &payload)).To(Succeed()) + name, ok := payload["name"].(map[string]interface{}) + Expect(ok).To(BeTrue()) + Expect(name["givenName"]).To(Equal("Bob")) + Expect(name["familyName"]).To(Equal("Smith")) }) It("updates user with origin specified", func() { @@ -110,15 +152,17 @@ var _ = Describe("UpdateUser", func() { VerifyRequest("PUT", "/Users/fb5f32e1-5cb3-49e6-93df-6df9c8c8bd70"), )) - session := runCommand("update-user", "marcus@stoicism.com", - "--origin", "ldap", - "--given_name", "Bob") + session := runCommand("update-user", "marcus@stoicism.com", + "--origin", "ldap", + "--givenName", "Bob") Expect(server.ReceivedRequests()).To(HaveLen(2)) Expect(session).To(Exit(0)) }) It("updates user with emails", func() { + var putBody []byte + // First GET to retrieve user server.RouteToHandler("GET", "/Users", CombineHandlers( RespondWith(http.StatusOK, fixtures.PaginatedResponse(uaa.User{ID: "fb5f32e1-5cb3-49e6-93df-6df9c8c8bd70", Username: "marcus@stoicism.com"})), @@ -127,18 +171,29 @@ var _ = Describe("UpdateUser", func() { // Then PUT to update user server.RouteToHandler("PUT", "/Users/fb5f32e1-5cb3-49e6-93df-6df9c8c8bd70", CombineHandlers( + captureRequestBody(&putBody), RespondWith(http.StatusOK, fixtures.MarcusUserResponse), VerifyRequest("PUT", "/Users/fb5f32e1-5cb3-49e6-93df-6df9c8c8bd70"), )) - session := runCommand("update-user", "marcus@stoicism.com", - "--emails", "new@email.com") + session := runCommand("update-user", "marcus@stoicism.com", + "--email", "new@email.com") Expect(server.ReceivedRequests()).To(HaveLen(2)) Expect(session).To(Exit(0)) + + var payload map[string]interface{} + Expect(json.Unmarshal(putBody, &payload)).To(Succeed()) + emails, ok := payload["emails"].([]interface{}) + Expect(ok).To(BeTrue()) + Expect(emails).To(HaveLen(1)) + email := emails[0].(map[string]interface{}) + Expect(email["value"]).To(Equal("new@email.com")) }) It("updates user with phones", func() { + var putBody []byte + // First GET to retrieve user server.RouteToHandler("GET", "/Users", CombineHandlers( RespondWith(http.StatusOK, fixtures.PaginatedResponse(uaa.User{ID: "fb5f32e1-5cb3-49e6-93df-6df9c8c8bd70", Username: "marcus@stoicism.com"})), @@ -147,35 +202,55 @@ var _ = Describe("UpdateUser", func() { // Then PUT to update user server.RouteToHandler("PUT", "/Users/fb5f32e1-5cb3-49e6-93df-6df9c8c8bd70", CombineHandlers( + captureRequestBody(&putBody), RespondWith(http.StatusOK, fixtures.MarcusUserResponse), VerifyRequest("PUT", "/Users/fb5f32e1-5cb3-49e6-93df-6df9c8c8bd70"), )) - session := runCommand("update-user", "marcus@stoicism.com", - "--phones", "555-1234") + session := runCommand("update-user", "marcus@stoicism.com", + "--phone", "555-1234") Expect(server.ReceivedRequests()).To(HaveLen(2)) Expect(session).To(Exit(0)) + + var payload map[string]interface{} + Expect(json.Unmarshal(putBody, &payload)).To(Succeed()) + phones, ok := payload["phoneNumbers"].([]interface{}) + Expect(ok).To(BeTrue()) + Expect(phones).To(HaveLen(1)) + phone := phones[0].(map[string]interface{}) + Expect(phone["value"]).To(Equal("555-1234")) }) - It("updates user with del_attrs removing phone numbers", func() { - // First GET to retrieve user + It("updates user with delAttrs removing phone numbers", func() { + var putBody []byte + + // First GET to retrieve user, which already has a phone number server.RouteToHandler("GET", "/Users", CombineHandlers( - RespondWith(http.StatusOK, fixtures.PaginatedResponse(uaa.User{ID: "fb5f32e1-5cb3-49e6-93df-6df9c8c8bd70", Username: "marcus@stoicism.com"})), + RespondWith(http.StatusOK, fixtures.PaginatedResponse(uaa.User{ + ID: "fb5f32e1-5cb3-49e6-93df-6df9c8c8bd70", + Username: "marcus@stoicism.com", + PhoneNumbers: []uaa.PhoneNumber{{Value: "555-0000"}}, + })), VerifyRequest("GET", "/Users"), )) // Then PUT to update user server.RouteToHandler("PUT", "/Users/fb5f32e1-5cb3-49e6-93df-6df9c8c8bd70", CombineHandlers( + captureRequestBody(&putBody), RespondWith(http.StatusOK, fixtures.MarcusUserResponse), VerifyRequest("PUT", "/Users/fb5f32e1-5cb3-49e6-93df-6df9c8c8bd70"), )) - session := runCommand("update-user", "marcus@stoicism.com", - "--del_attrs", "phoneNumbers") + session := runCommand("update-user", "marcus@stoicism.com", + "--delAttrs", "phoneNumbers") Expect(server.ReceivedRequests()).To(HaveLen(2)) Expect(session).To(Exit(0)) + + var payload map[string]interface{} + Expect(json.Unmarshal(putBody, &payload)).To(Succeed()) + Expect(payload["phoneNumbers"]).To(BeNil()) }) It("works with zone parameter", func() { @@ -193,8 +268,8 @@ var _ = Describe("UpdateUser", func() { VerifyHeaderKV("X-Identity-Zone-Id", "twilight-zone"), )) - session := runCommand("update-user", "marcus@stoicism.com", - "--given_name", "Bob", + session := runCommand("update-user", "marcus@stoicism.com", + "--givenName", "Bob", "--zone", "twilight-zone") Expect(server.ReceivedRequests()).To(HaveLen(2)) @@ -214,7 +289,7 @@ var _ = Describe("UpdateUser", func() { VerifyRequest("PUT", "/Users/fb5f32e1-5cb3-49e6-93df-6df9c8c8bd70"), )) - session := runCommand("update-user", "marcus@stoicism.com", "--given_name", "Bob") + session := runCommand("update-user", "marcus@stoicism.com", "--givenName", "Bob") Expect(server.ReceivedRequests()).To(HaveLen(2)) Expect(session).To(Exit(0)) @@ -230,7 +305,7 @@ var _ = Describe("UpdateUser", func() { VerifyRequest("GET", "/Users"), )) - session := runCommand("update-user", "nobody", "--given_name", "Bob") + session := runCommand("update-user", "nobody", "--givenName", "Bob") Expect(server.ReceivedRequests()).To(HaveLen(1)) Expect(session).To(Exit(1)) @@ -249,7 +324,7 @@ var _ = Describe("UpdateUser", func() { VerifyRequest("PUT", "/Users/fb5f32e1-5cb3-49e6-93df-6df9c8c8bd70"), )) - session := runCommand("update-user", "marcus@stoicism.com", "--given_name", "Bob") + session := runCommand("update-user", "marcus@stoicism.com", "--givenName", "Bob") Expect(server.ReceivedRequests()).To(HaveLen(2)) Expect(session).To(Exit(1)) @@ -270,8 +345,8 @@ var _ = Describe("UpdateUser", func() { VerifyRequest("PUT", "/Users/fb5f32e1-5cb3-49e6-93df-6df9c8c8bd70"), )) - session := runCommand("update-user", "marcus@stoicism.com", - "--given_name", "Bob", + session := runCommand("update-user", "marcus@stoicism.com", + "--givenName", "Bob", "--verbose") Expect(server.ReceivedRequests()).To(HaveLen(2)) @@ -281,4 +356,4 @@ var _ = Describe("UpdateUser", func() { }) }) }) -}) \ No newline at end of file +}) diff --git a/docs/commands/change-client-secret.md b/docs/commands/change-client-secret.md index c22c60e..9c82a41 100644 --- a/docs/commands/change-client-secret.md +++ b/docs/commands/change-client-secret.md @@ -1,6 +1,6 @@ -# uaa change-client-secret +# change-client-secret -## Overview +[← Command Reference](../commands.md) Change the secret for the currently authenticated client. This command allows a client to change its own secret by providing both the old secret and the new secret. @@ -14,37 +14,30 @@ uaa change-client-secret --old_secret OLD_SECRET --secret NEW_SECRET [flags] This command requires an active client context obtained via the `client_credentials` grant type. -## Arguments +## Flags -| Argument | Description | -|----------|-------------| -| `--old_secret` | The current secret for the client | -| `--secret`, `-s` | The new secret for the client | +| Flag | Short | Default | Description | +|------|-------|---------|-------------| +| `--old_secret` | | | The current secret for the client | +| `--secret` | `-s` | | The new secret for the client | +| `--zone` | `-z` | | Identity zone subdomain where the client resides | -## Options +## Global Flags -| Option | Description | -|--------|-------------| -| `--zone`, `-z` | Identity zone subdomain where the client resides | -| `--verbose`, `-v` | Display verbose output including HTTP request/response details | +| Flag | Short | Description | +|------|-------|-------------| +| `--verbose` | `-v` | Print additional info on HTTP requests | ## Examples -### Change client secret with explicit values - ```bash +# Change client secret with explicit values uaa change-client-secret --old_secret currentsecret --secret newsecret -``` - -### Change client secret in a specific zone -```bash +# Change client secret in a specific zone uaa change-client-secret --old_secret currentsecret --secret newsecret --zone myzone -``` - -### Change client secret with verbose output -```bash +# Change client secret with verbose output uaa change-client-secret --old_secret currentsecret --secret newsecret --verbose ``` @@ -59,4 +52,14 @@ uaa change-client-secret --old_secret currentsecret --secret newsecret --verbose - This command is for self-service secret changes where a client changes its own secret - Both the old and new secrets must be provided for security reasons - After changing the secret, you will need to re-authenticate with the new secret -- Use `--verbose` to see the actual HTTP request being made to the UAA \ No newline at end of file +- Use `--verbose` to see the actual HTTP request being made to the UAA + +## See Also + +- [set-client-secret](set-client-secret.md) +- [update-client](update-client.md) +- [get-client-credentials-token](get-client-credentials-token.md) + +--- + +[← Command Reference](../commands.md) diff --git a/docs/commands/update-user.md b/docs/commands/update-user.md index d264e8b..6aea6a9 100644 --- a/docs/commands/update-user.md +++ b/docs/commands/update-user.md @@ -14,12 +14,12 @@ uaa update-user USERNAME [flags] | Flag | Short | Default | Description | |------|-------|---------|-------------| -| `--given_name` | | | Given (first) name | -| `--family_name` | | | Family (last) name | -| `--emails` | | | Email addresses (flag may be specified multiple times) | -| `--phones` | | | Phone numbers (flag may be specified multiple times) | +| `--givenName` | | | Given (first) name | +| `--familyName` | | | Family (last) name | +| `--email` | | | Email address (flag may be specified multiple times) | +| `--phone` | | | Phone number (flag may be specified multiple times) | | `--origin` | `-o` | | Identity provider origin to search for user (e.g. `uaa`, `ldap`) | -| `--del_attrs` | | | Attributes to remove (e.g. `phoneNumbers`, `name`) | +| `--delAttrs` | | | Attributes to remove (e.g. `phoneNumbers`, `name`) | | `--zone` | `-z` | | Identity zone subdomain in which to update the user | ## Global Flags @@ -32,27 +32,27 @@ uaa update-user USERNAME [flags] ```bash # Update a user's name -uaa update-user bob --given_name Robert --family_name Smith +uaa update-user bob --givenName Robert --familyName Smith # Update a user's email addresses -uaa update-user alice --emails alice@newdomain.com --emails alice.jones@work.com +uaa update-user alice --email alice@newdomain.com --email alice.jones@work.com # Update a user from a specific origin -uaa update-user carol --origin ldap --given_name Caroline +uaa update-user carol --origin ldap --givenName Caroline # Remove phone numbers from a user -uaa update-user bob --del_attrs phoneNumbers +uaa update-user bob --delAttrs phoneNumbers # Update multiple attributes at once uaa update-user alice \ - --given_name Alice \ - --family_name Johnson \ - --emails alice.johnson@example.com \ - --phones 555-1234 + --givenName Alice \ + --familyName Johnson \ + --email alice.johnson@example.com \ + --phone 555-1234 # Update user in a specific zone with verbose output uaa update-user bob \ - --given_name Robert \ + --givenName Robert \ --zone my-zone \ --verbose ``` @@ -60,9 +60,9 @@ uaa update-user bob \ ## Notes - The command first retrieves the existing user, then merges the specified updates -- At least one update flag must be specified -- When using `--del_attrs`, be careful not to remove required attributes -- The `--emails` attribute cannot be deleted as it may make the user unusable +- At least one update flag (`--givenName`, `--familyName`, `--email`, `--phone`, or `--delAttrs`) must be specified +- When using `--delAttrs`, be careful not to remove required attributes +- The `--email` attribute cannot be deleted as it may make the user unusable - Use `--verbose` to see the HTTP PUT request details ## See Also