From 9aa0451ada472e51764f9320aadcfd67751dda17 Mon Sep 17 00:00:00 2001 From: Sergiu Ghitea Date: Fri, 25 Nov 2022 16:56:15 +0100 Subject: [PATCH] Add api command --- docs/auth0_api.md | 55 +++++++++ docs/index.md | 1 + internal/cli/api.go | 186 +++++++++++++++++++++++++++++++ internal/cli/root.go | 1 + test/integration/test-cases.yaml | 14 +++ 5 files changed, 257 insertions(+) create mode 100644 docs/auth0_api.md create mode 100644 internal/cli/api.go diff --git a/docs/auth0_api.md b/docs/auth0_api.md new file mode 100644 index 000000000..9a626bc59 --- /dev/null +++ b/docs/auth0_api.md @@ -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 + +``` +auth0 api [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. + --force Skip confirmation. + --format string Command output format. Options: json. + --no-color Disable colors. + --no-input Disable interactivity. + --tenant string Specific tenant to use. +``` + +### SEE ALSO + +* [auth0](/auth0-cli/) - Supercharge your development workflow. + diff --git a/docs/index.md b/docs/index.md index cd97fc1de..d764117f7 100644 --- a/docs/index.md +++ b/docs/index.md @@ -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 diff --git a/internal/cli/api.go b/internal/cli/api.go new file mode 100644 index 000000000..d5d9556ab --- /dev/null +++ b/internal/cli/api.go @@ -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" + +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{ + "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 ", + 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 +and POST for requests with data. + +%s %s + +%s %s`, + "Auth0 Management API Docs:\n", apiDocsURL, + "Available Methods:\n", "GET, POST, PUT, PATCH, DELETE", + ), + Example: `auth0 api "/organizations?include_totals=true" +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) + } + + 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") + + 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) + } + + i.Method = i.RawMethod + + 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) + } + + 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, "/")) + 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 + } + } else { + i.RawMethod = strings.ToUpper(args[0]) + } + + i.RawURI = args[lenArgs-1] +} diff --git a/internal/cli/root.go b/internal/cli/root.go index 546ad68ab..57155c498 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -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)) diff --git a/test/integration/test-cases.yaml b/test/integration/test-cases.yaml index e61e61f56..dcf7a0e75 100644 --- a/test/integration/test-cases.yaml +++ b/test/integration/test-cases.yaml @@ -639,3 +639,17 @@ tests: - STAGE_PRE_USER_REGISTRATION_MAX_ATTEMPTS - STAGE_PRE_USER_REGISTRATION_RATE exit-code: 0 + + api get tenant settings: + 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