Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DXCDT-264: Add api command #531

Merged
merged 16 commits into from
Nov 29, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
55 changes: 55 additions & 0 deletions docs/auth0_api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
---
layout: default
---
## auth0 api

Makes an authenticated HTTP request to the Auth0 Management API

### Synopsis

Makes an authenticated HTTP request to the Auth0 Management API and prints the response as JSON.

The method argument is optional, and when you don’t specify it, the command defaults to GET for requests without data
and POST for requests with data.

Auth0 Management API Docs:
https://auth0.com/docs/api/management/v2

Available Methods:
GET, POST, PUT, PATCH, DELETE
sergiught marked this conversation as resolved.
Show resolved Hide resolved

```
auth0 api <method> <uri> [flags]
```

### Examples

```
auth0 api "/organizations?include_totals=true"
auth0 api get "/organizations?include_totals=true"
auth0 api clients --data "{\"name\":\"apiTest\"}"
```

### Options

```
-d, --data string JSON data payload to send with the request.
-h, --help help for api
```

### Options inherited from parent commands

```
--debug Enable debug mode.
sergiught marked this conversation as resolved.
Show resolved Hide resolved
--force Skip confirmation.
sergiught marked this conversation as resolved.
Show resolved Hide resolved
--format string Command output format. Options: json.
sergiught marked this conversation as resolved.
Show resolved Hide resolved
--no-color Disable colors.
Widcket marked this conversation as resolved.
Show resolved Hide resolved
--no-input Disable interactivity.
--tenant string Specific tenant to use.
```

### SEE ALSO

* [auth0](/auth0-cli/) - Supercharge your development workflow.

1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Supercharge your development workflow.
### SEE ALSO

* [auth0 actions](auth0_actions.md) - Manage resources for actions
* [auth0 api](auth0_api.md) - Makes an authenticated HTTP request to the Auth0 Management API
* [auth0 apis](auth0_apis.md) - Manage resources for APIs
* [auth0 apps](auth0_apps.md) - Manage resources for applications
* [auth0 branding](auth0_branding.md) - Manage branding options
Expand Down
186 changes: 186 additions & 0 deletions internal/cli/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
package cli

import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"

"github.com/spf13/cobra"

"github.com/auth0/auth0-cli/internal/ansi"
)

const apiDocsURL = "https://auth0.com/docs/api/management/v2"
willvedd marked this conversation as resolved.
Show resolved Hide resolved

var apiFlags = apiCmdFlags{
Data: Flag{
Name: "RawData",
LongForm: "data",
ShortForm: "d",
Help: "JSON data payload to send with the request.",
IsRequired: false,
AlwaysPrompt: false,
},
}

var apiValidMethods = map[string]bool{
sergiught marked this conversation as resolved.
Show resolved Hide resolved
"GET": true,
"POST": true,
"PUT": true,
"PATCH": true,
"DELETE": true,
}

type (
apiCmdFlags struct {
Data Flag
}

apiCmdInputs struct {
RawMethod string
RawURI string
RawData string
Method string
URL *url.URL
Data io.Reader
}
)

func apiCmd(cli *cli) *cobra.Command {
var inputs apiCmdInputs

cmd := &cobra.Command{
Use: "api <method> <uri>",
sergiught marked this conversation as resolved.
Show resolved Hide resolved
Args: cobra.RangeArgs(1, 2),
Short: "Makes an authenticated HTTP request to the Auth0 Management API",
Long: fmt.Sprintf(
`Makes an authenticated HTTP request to the Auth0 Management API and prints the response as JSON.

The method argument is optional, and when you don’t specify it, the command defaults to GET for requests without data
willvedd marked this conversation as resolved.
Show resolved Hide resolved
and POST for requests with data.
sergiught marked this conversation as resolved.
Show resolved Hide resolved

%s %s

%s %s`,
"Auth0 Management API Docs:\n", apiDocsURL,
"Available Methods:\n", "GET, POST, PUT, PATCH, DELETE",
sergiught marked this conversation as resolved.
Show resolved Hide resolved
),
Example: `auth0 api "/organizations?include_totals=true"
sergiught marked this conversation as resolved.
Show resolved Hide resolved
auth0 api get "/organizations?include_totals=true"
auth0 api clients --data "{\"name\":\"apiTest\"}"
`,
RunE: apiCmdRun(cli, &inputs),
}

apiFlags.Data.RegisterString(cmd, &inputs.RawData, "")

return cmd
}

func apiCmdRun(cli *cli, inputs *apiCmdInputs) func(cmd *cobra.Command, args []string) error {
return func(cmd *cobra.Command, args []string) error {
if err := inputs.fromArgs(args, cli.tenant); err != nil {
return fmt.Errorf("failed to parse command inputs: %w", err)
willvedd marked this conversation as resolved.
Show resolved Hide resolved
}

var response *http.Response
if err := ansi.Waiting(func() error {
request, err := http.NewRequestWithContext(
cmd.Context(),
inputs.Method,
inputs.URL.String(),
inputs.Data,
)
if err != nil {
return err
}

bearerToken := cli.config.Tenants[cli.tenant].AccessToken
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", bearerToken))
request.Header.Set("Content-Type", "application/json")
willvedd marked this conversation as resolved.
Show resolved Hide resolved

response, err = http.DefaultClient.Do(request)
return err
}); err != nil {
return fmt.Errorf("failed to send request: %w", err)
}
defer response.Body.Close()

rawBodyJSON, err := io.ReadAll(response.Body)
if err != nil {
return err
}

var prettyJSON bytes.Buffer
if err := json.Indent(&prettyJSON, rawBodyJSON, "", " "); err != nil {
return fmt.Errorf("failed to prepare json output: %w", err)
}

cli.renderer.Output(ansi.ColorizeJSON(prettyJSON.String(), false))

return nil
}
}

func (i *apiCmdInputs) fromArgs(args []string, domain string) error {
i.parseRaw(args)

if err := i.validateAndSetMethod(); err != nil {
return err
}

if err := i.validateAndSetData(); err != nil {
return err
}

return i.validateAndSetEndpoint(domain)
}

func (i *apiCmdInputs) validateAndSetMethod() error {
if _, ok := apiValidMethods[i.RawMethod]; !ok {
return fmt.Errorf("invalid method given: %s, accepting only GET, POST, PUT, PATCH and DELETE", i.RawMethod)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

String interpolation with apiValidMethods?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in: 2f776626 (#531)

}

i.Method = i.RawMethod
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a functional distinction between Method and RawMethod or than symmetry with the other arguments? If not, maybe consider removing it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RawMethod hasn't been validated yet and Method is validated.


return nil
}

func (i *apiCmdInputs) validateAndSetData() error {
if i.RawData != "" && !json.Valid([]byte(i.RawData)) {
return fmt.Errorf("invalid json data given: %+v", i.RawData)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly to other feedback, this error could be more human friendly. I think we'd have a good idea of when and how this error triggers, so we could provide clearer guidance on how to correct.

See: https://clig.dev/#errors

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The returned error wraps all inner errors so the final one ends up to be for example:

$auth0 api put z --data {"zz:2} 
"

=== tenat-example.auth0.com error

 ▸    failed to parse command inputs: invalid json data given: {zz:2}

exit status 1

What do you suggest so we make it more human friendly? As in this case it's particularly hard as the json.Valid just returns true or false and isn't able to tell that the input was missing a ".

}

i.Data = bytes.NewReader([]byte(i.RawData))

return nil
}

func (i *apiCmdInputs) validateAndSetEndpoint(domain string) error {
endpoint, err := url.Parse("https://" + domain + "/api/v2/" + strings.Trim(i.RawURI, "/"))
sergiught marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return fmt.Errorf("invalid uri given: %w", err)
}

i.URL = endpoint

return nil
}

func (i *apiCmdInputs) parseRaw(args []string) {
lenArgs := len(args)
if lenArgs == 1 {
i.RawMethod = http.MethodGet
if i.RawData != "" {
i.RawMethod = http.MethodPost
willvedd marked this conversation as resolved.
Show resolved Hide resolved
}
} else {
i.RawMethod = strings.ToUpper(args[0])
}

i.RawURI = args[lenArgs-1]
}
1 change: 1 addition & 0 deletions internal/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ func addSubcommands(rootCmd *cobra.Command, cli *cli) {
rootCmd.AddCommand(attackProtectionCmd(cli))
rootCmd.AddCommand(testCmd(cli))
rootCmd.AddCommand(logsCmd(cli))
rootCmd.AddCommand(apiCmd(cli))

// keep completion at the bottom:
rootCmd.AddCommand(completionCmd(cli))
Expand Down
14 changes: 14 additions & 0 deletions test/integration/test-cases.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -639,3 +639,17 @@ tests:
- STAGE_PRE_USER_REGISTRATION_MAX_ATTEMPTS
- STAGE_PRE_USER_REGISTRATION_RATE
exit-code: 0

api get tenant settings:
sergiught marked this conversation as resolved.
Show resolved Hide resolved
command: auth0 api get "tenants/settings"
stdout:
json:
enabled_locales.#: "1"
exit-code: 0

api patch tenant settings:
command: auth0 api patch "tenants/settings" --data "{\"idle_session_lifetime\":72}"
stdout:
json:
idle_session_lifetime: "72"
exit-code: 0