From 9aa0451ada472e51764f9320aadcfd67751dda17 Mon Sep 17 00:00:00 2001 From: Sergiu Ghitea Date: Fri, 25 Nov 2022 16:56:15 +0100 Subject: [PATCH 01/16] 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 From 2f7766265a6d993261716ef593b6cd5792d88afe Mon Sep 17 00:00:00 2001 From: Sergiu Ghitea Date: Mon, 28 Nov 2022 17:19:00 +0100 Subject: [PATCH 02/16] Change type of apiValidMethods from map to slice of strings --- internal/cli/api.go | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/internal/cli/api.go b/internal/cli/api.go index d5d9556ab..36479b53a 100644 --- a/internal/cli/api.go +++ b/internal/cli/api.go @@ -27,12 +27,12 @@ var apiFlags = apiCmdFlags{ }, } -var apiValidMethods = map[string]bool{ - "GET": true, - "POST": true, - "PUT": true, - "PATCH": true, - "DELETE": true, +var apiValidMethods = []string{ + http.MethodGet, + http.MethodPost, + http.MethodPut, + http.MethodPatch, + http.MethodDelete, } type ( @@ -60,14 +60,13 @@ func apiCmd(cli *cli) *cobra.Command { 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. +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", + "Available Methods:\n", strings.Join(apiValidMethods, ", "), ), Example: `auth0 api "/organizations?include_totals=true" auth0 api get "/organizations?include_totals=true" @@ -141,13 +140,18 @@ func (i *apiCmdInputs) fromArgs(args []string, domain string) error { } 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) + for _, validMethod := range apiValidMethods { + if i.RawMethod == validMethod { + i.Method = i.RawMethod + return nil + } } - i.Method = i.RawMethod - - return nil + return fmt.Errorf( + "invalid method given: %s, accepting only %s", + i.RawMethod, + strings.Join(apiValidMethods, ", "), + ) } func (i *apiCmdInputs) validateAndSetData() error { From 26eec53df462c003a71edb4af6c0de9c49091202 Mon Sep 17 00:00:00 2001 From: Sergiu Ghitea Date: Mon, 28 Nov 2022 17:36:01 +0100 Subject: [PATCH 03/16] Update api cmd examples --- internal/cli/api.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/cli/api.go b/internal/cli/api.go index 36479b53a..e4eacd766 100644 --- a/internal/cli/api.go +++ b/internal/cli/api.go @@ -68,9 +68,9 @@ The method argument is optional, and when you don’t specify it, the command de "Auth0 Management API Docs:\n", apiDocsURL, "Available Methods:\n", strings.Join(apiValidMethods, ", "), ), - Example: `auth0 api "/organizations?include_totals=true" -auth0 api get "/organizations?include_totals=true" -auth0 api clients --data "{\"name\":\"apiTest\"}" + Example: `auth0 api "/stats/daily?from=20221101&to=20221118" +auth0 api get "/tenants/settings" +auth0 api clients --data "{\"name\":\"ssoTest\",\"app_type\":\"sso_integration\"}" `, RunE: apiCmdRun(cli, &inputs), } From 96dd7b20b003510f452fc4d432619710432fac6e Mon Sep 17 00:00:00 2001 From: Sergiu Ghitea Date: Mon, 28 Nov 2022 17:38:50 +0100 Subject: [PATCH 04/16] Add user agent to api cmd requests --- internal/cli/api.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/cli/api.go b/internal/cli/api.go index e4eacd766..822f08ab2 100644 --- a/internal/cli/api.go +++ b/internal/cli/api.go @@ -12,6 +12,7 @@ import ( "github.com/spf13/cobra" "github.com/auth0/auth0-cli/internal/ansi" + "github.com/auth0/auth0-cli/internal/buildinfo" ) const apiDocsURL = "https://auth0.com/docs/api/management/v2" @@ -101,6 +102,7 @@ func apiCmdRun(cli *cli, inputs *apiCmdInputs) func(cmd *cobra.Command, args []s bearerToken := cli.config.Tenants[cli.tenant].AccessToken request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", bearerToken)) request.Header.Set("Content-Type", "application/json") + request.Header.Set("User-Agent", fmt.Sprintf("%s/%s", userAgent, strings.TrimPrefix(buildinfo.Version, "v"))) response, err = http.DefaultClient.Do(request) return err From 042face9e3dfded87f50a61c4b3aed468b879b0f Mon Sep 17 00:00:00 2001 From: Sergiu Ghitea Date: Mon, 28 Nov 2022 18:25:04 +0100 Subject: [PATCH 05/16] Add unit tests for parsing api cmd inputs --- internal/cli/api_test.go | 87 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 internal/cli/api_test.go diff --git a/internal/cli/api_test.go b/internal/cli/api_test.go new file mode 100644 index 000000000..b6c622cea --- /dev/null +++ b/internal/cli/api_test.go @@ -0,0 +1,87 @@ +package cli + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestAPICmdInputs_FromArgs(t *testing.T) { + const testDomain = "example.auth0.com" + var testCases = []struct { + name string + givenArgs []string + givenDataFlag string + expectedMethod string + expectedURL string + expectedError string + }{ + { + name: "it can correctly parse input arguments", + givenArgs: []string{"get", "/tenants/settings"}, + expectedMethod: http.MethodGet, + expectedURL: "https://" + testDomain + "/api/v2/tenants/settings", + }, + { + name: "it can correctly parse input arguments and data flag", + givenArgs: []string{"post", "/clients"}, + givenDataFlag: `{"name":"genericTest"}`, + expectedMethod: http.MethodPost, + expectedURL: "https://" + testDomain + "/api/v2/clients", + }, + { + name: "it can correctly parse input arguments when get method is missing", + givenArgs: []string{"/tenants/settings"}, + expectedMethod: http.MethodGet, + expectedURL: "https://" + testDomain + "/api/v2/tenants/settings", + }, + { + name: "it can correctly parse input arguments and data flag when post method is missing", + givenArgs: []string{"/clients"}, + givenDataFlag: `{"name":"genericTest"}`, + expectedMethod: http.MethodPost, + expectedURL: "https://" + testDomain + "/api/v2/clients", + }, + { + name: "it fails to parse input arguments when method is invalid", + givenArgs: []string{"abracadabra", "/clients"}, + expectedError: "invalid method given: ABRACADABRA, accepting only GET, POST, PUT, PATCH, DELETE", + }, + { + name: "it fails to parse input arguments when data is not a valid JSON", + givenArgs: []string{"patch", "/clients"}, + givenDataFlag: "{", + expectedError: "invalid json data given: {", + }, + { + name: "it fails to parse input arguments when uri is invalid", + givenArgs: []string{"get", "#$%^&*(#$%%^("}, + expectedError: "invalid uri given: parse \"https://example.auth0.com/api/v2/#$%^&*(#$%%^(\": invalid URL escape \"%^&\"", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + if len(testCase.givenArgs) < 1 { + t.Fatalf("the test cases need to pass at least 1 argument") + } + + actualInputs := &apiCmdInputs{ + RawData: testCase.givenDataFlag, + } + + err := actualInputs.fromArgs(testCase.givenArgs, testDomain) + + if testCase.expectedError != "" { + assert.EqualError(t, err, testCase.expectedError) + return + } else { + assert.NoError(t, err) + } + + assert.Equal(t, testCase.expectedMethod, actualInputs.Method) + assert.Equal(t, testCase.expectedURL, actualInputs.URL.String()) + }) + } +} From 15bb5a9a83606e0fe4dcf15ec13a7d49e215583e Mon Sep 17 00:00:00 2001 From: Sergiu Ghitea Date: Mon, 28 Nov 2022 18:26:34 +0100 Subject: [PATCH 06/16] Update api cmd docs --- docs/auth0_api.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/auth0_api.md b/docs/auth0_api.md index 9a626bc59..ab6ccbf0b 100644 --- a/docs/auth0_api.md +++ b/docs/auth0_api.md @@ -9,8 +9,7 @@ Makes an authenticated HTTP request to the Auth0 Management API 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. +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 @@ -25,9 +24,9 @@ auth0 api [flags] ### Examples ``` -auth0 api "/organizations?include_totals=true" -auth0 api get "/organizations?include_totals=true" -auth0 api clients --data "{\"name\":\"apiTest\"}" +auth0 api "/stats/daily?from=20221101&to=20221118" +auth0 api get "/tenants/settings" +auth0 api clients --data "{\"name\":\"ssoTest\",\"app_type\":\"sso_integration\"}" ``` From dd53121ecec8e702658d45f4dfcb94e19c54f8e2 Mon Sep 17 00:00:00 2001 From: Sergiu Ghitea Date: Tue, 29 Nov 2022 12:20:25 +0100 Subject: [PATCH 07/16] Pass query params through flags --- internal/cli/api.go | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/internal/cli/api.go b/internal/cli/api.go index 822f08ab2..400499d7f 100644 --- a/internal/cli/api.go +++ b/internal/cli/api.go @@ -26,6 +26,14 @@ var apiFlags = apiCmdFlags{ IsRequired: false, AlwaysPrompt: false, }, + QueryParams: Flag{ + Name: "QueryParams", + LongForm: "query", + ShortForm: "q", + Help: "Query params to send with the request.", + IsRequired: false, + AlwaysPrompt: false, + }, } var apiValidMethods = []string{ @@ -38,16 +46,18 @@ var apiValidMethods = []string{ type ( apiCmdFlags struct { - Data Flag + Data Flag + QueryParams Flag } apiCmdInputs struct { - RawMethod string - RawURI string - RawData string - Method string - URL *url.URL - Data io.Reader + RawMethod string + RawURI string + RawData string + RawQueryParams map[string]string + Method string + URL *url.URL + Data io.Reader } ) @@ -69,7 +79,7 @@ The method argument is optional, and when you don’t specify it, the command de "Auth0 Management API Docs:\n", apiDocsURL, "Available Methods:\n", strings.Join(apiValidMethods, ", "), ), - Example: `auth0 api "/stats/daily?from=20221101&to=20221118" + Example: `auth0 api "/stats/daily" -q "from=20221101" -q "to=20221118" auth0 api get "/tenants/settings" auth0 api clients --data "{\"name\":\"ssoTest\",\"app_type\":\"sso_integration\"}" `, @@ -77,6 +87,7 @@ auth0 api clients --data "{\"name\":\"ssoTest\",\"app_type\":\"sso_integration\" } apiFlags.Data.RegisterString(cmd, &inputs.RawData, "") + apiFlags.QueryParams.RegisterStringMap(cmd, &inputs.RawQueryParams, nil) return cmd } @@ -172,6 +183,12 @@ func (i *apiCmdInputs) validateAndSetEndpoint(domain string) error { return fmt.Errorf("invalid uri given: %w", err) } + params := endpoint.Query() + for key, value := range i.RawQueryParams { + params.Set(key, value) + } + endpoint.RawQuery = params.Encode() + i.URL = endpoint return nil From e41cde69f94f09a3d85b038229c2673cf03eb7b0 Mon Sep 17 00:00:00 2001 From: Sergiu Ghitea Date: Tue, 29 Nov 2022 12:41:32 +0100 Subject: [PATCH 08/16] Allow json data to be piped to the api cmd --- internal/cli/api.go | 18 +++++++++++++++--- .../fixtures/update-tenant-settings.json | 3 +++ test/integration/test-cases.yaml | 7 +++++++ 3 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 test/integration/fixtures/update-tenant-settings.json diff --git a/internal/cli/api.go b/internal/cli/api.go index 400499d7f..5f0d34e52 100644 --- a/internal/cli/api.go +++ b/internal/cli/api.go @@ -13,6 +13,7 @@ import ( "github.com/auth0/auth0-cli/internal/ansi" "github.com/auth0/auth0-cli/internal/buildinfo" + "github.com/auth0/auth0-cli/internal/iostream" ) const apiDocsURL = "https://auth0.com/docs/api/management/v2" @@ -168,11 +169,22 @@ func (i *apiCmdInputs) validateAndSetMethod() error { } func (i *apiCmdInputs) validateAndSetData() error { - if i.RawData != "" && !json.Valid([]byte(i.RawData)) { - return fmt.Errorf("invalid json data given: %+v", i.RawData) + var data []byte + + if i.RawData != "" { + data = []byte(i.RawData) + } + + pipedRawData := iostream.PipedInput() + if pipedRawData != nil && data == nil { + data = pipedRawData + } + + if data != nil && !json.Valid(data) { + return fmt.Errorf("invalid json data given: %s", data) } - i.Data = bytes.NewReader([]byte(i.RawData)) + i.Data = bytes.NewReader(data) return nil } diff --git a/test/integration/fixtures/update-tenant-settings.json b/test/integration/fixtures/update-tenant-settings.json new file mode 100644 index 000000000..7ca7ffde2 --- /dev/null +++ b/test/integration/fixtures/update-tenant-settings.json @@ -0,0 +1,3 @@ +{ + "idle_session_lifetime": 73 +} diff --git a/test/integration/test-cases.yaml b/test/integration/test-cases.yaml index dcf7a0e75..87a9db7c2 100644 --- a/test/integration/test-cases.yaml +++ b/test/integration/test-cases.yaml @@ -647,6 +647,13 @@ tests: enabled_locales.#: "1" exit-code: 0 + api patch tenant settings with piped data: + command: cat ./test/integration/fixtures/update-tenant-settings.json | auth0 api patch "tenants/settings" + stdout: + json: + idle_session_lifetime: "73" + exit-code: 0 + api patch tenant settings: command: auth0 api patch "tenants/settings" --data "{\"idle_session_lifetime\":72}" stdout: From eae2bbe21f6842b0c50a8837eb937ecb3671ab77 Mon Sep 17 00:00:00 2001 From: Sergiu Ghitea Date: Tue, 29 Nov 2022 13:18:43 +0100 Subject: [PATCH 09/16] Remove format flag for api cmd --- docs/auth0_api.md | 7 ++++--- internal/cli/api.go | 5 +++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/docs/auth0_api.md b/docs/auth0_api.md index ab6ccbf0b..5f97a0b3c 100644 --- a/docs/auth0_api.md +++ b/docs/auth0_api.md @@ -24,7 +24,7 @@ auth0 api [flags] ### Examples ``` -auth0 api "/stats/daily?from=20221101&to=20221118" +auth0 api "/stats/daily" -q "from=20221101" -q "to=20221118" auth0 api get "/tenants/settings" auth0 api clients --data "{\"name\":\"ssoTest\",\"app_type\":\"sso_integration\"}" @@ -33,8 +33,9 @@ auth0 api clients --data "{\"name\":\"ssoTest\",\"app_type\":\"sso_integration\" ### Options ``` - -d, --data string JSON data payload to send with the request. - -h, --help help for api + -d, --data string JSON data payload to send with the request. + -h, --help help for api + -q, --query stringToString Query params to send with the request. (default []) ``` ### Options inherited from parent commands diff --git a/internal/cli/api.go b/internal/cli/api.go index 5f0d34e52..0194c7574 100644 --- a/internal/cli/api.go +++ b/internal/cli/api.go @@ -87,6 +87,11 @@ auth0 api clients --data "{\"name\":\"ssoTest\",\"app_type\":\"sso_integration\" RunE: apiCmdRun(cli, &inputs), } + cmd.SetHelpFunc(func(command *cobra.Command, strings []string) { + command.Flags().MarkHidden("format") + command.Parent().HelpFunc()(command, strings) + }) + apiFlags.Data.RegisterString(cmd, &inputs.RawData, "") apiFlags.QueryParams.RegisterStringMap(cmd, &inputs.RawQueryParams, nil) From b16e634b8466735eb652a2ba5bc1300d2b0d6bc4 Mon Sep 17 00:00:00 2001 From: Sergiu Ghitea Date: Tue, 29 Nov 2022 13:35:13 +0100 Subject: [PATCH 10/16] Ask for confirmation when method is delete in api cmd --- internal/cli/api.go | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/internal/cli/api.go b/internal/cli/api.go index 0194c7574..261060128 100644 --- a/internal/cli/api.go +++ b/internal/cli/api.go @@ -14,6 +14,7 @@ import ( "github.com/auth0/auth0-cli/internal/ansi" "github.com/auth0/auth0-cli/internal/buildinfo" "github.com/auth0/auth0-cli/internal/iostream" + "github.com/auth0/auth0-cli/internal/prompt" ) const apiDocsURL = "https://auth0.com/docs/api/management/v2" @@ -104,6 +105,13 @@ func apiCmdRun(cli *cli, inputs *apiCmdInputs) func(cmd *cobra.Command, args []s return fmt.Errorf("failed to parse command inputs: %w", err) } + if inputs.Method == http.MethodDelete && !cli.force && canPrompt(cmd) { + message := "Are you sure you want to proceed? Deleting is a destructive action." + if confirmed := prompt.Confirm(message); !confirmed { + return nil + } + } + var response *http.Response if err := ansi.Waiting(func() error { request, err := http.NewRequestWithContext( @@ -133,6 +141,10 @@ func apiCmdRun(cli *cli, inputs *apiCmdInputs) func(cmd *cobra.Command, args []s return err } + if len(rawBodyJSON) == 0 { + return nil + } + var prettyJSON bytes.Buffer if err := json.Indent(&prettyJSON, rawBodyJSON, "", " "); err != nil { return fmt.Errorf("failed to prepare json output: %w", err) @@ -185,7 +197,7 @@ func (i *apiCmdInputs) validateAndSetData() error { data = pipedRawData } - if data != nil && !json.Valid(data) { + if len(data) > 0 && !json.Valid(data) { return fmt.Errorf("invalid json data given: %s", data) } From 2e5ac1e6cead6a25afe89f7c69b0dfe596584054 Mon Sep 17 00:00:00 2001 From: Sergiu Ghitea Date: Tue, 29 Nov 2022 13:42:27 +0100 Subject: [PATCH 11/16] Add debug info --- internal/cli/api.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/internal/cli/api.go b/internal/cli/api.go index 261060128..a6652506d 100644 --- a/internal/cli/api.go +++ b/internal/cli/api.go @@ -129,6 +129,10 @@ func apiCmdRun(cli *cli, inputs *apiCmdInputs) func(cmd *cobra.Command, args []s request.Header.Set("Content-Type", "application/json") request.Header.Set("User-Agent", fmt.Sprintf("%s/%s", userAgent, strings.TrimPrefix(buildinfo.Version, "v"))) + if cli.debug { + cli.renderer.Infof("[%s]: %s", request.Method, request.URL.String()) + } + response, err = http.DefaultClient.Do(request) return err }); err != nil { @@ -142,6 +146,9 @@ func apiCmdRun(cli *cli, inputs *apiCmdInputs) func(cmd *cobra.Command, args []s } if len(rawBodyJSON) == 0 { + if cli.debug { + cli.renderer.Infof("Response body is empty.") + } return nil } From b56f66112567730d35fb9f8055872f15c704d987 Mon Sep 17 00:00:00 2001 From: Sergiu Ghitea Date: Tue, 29 Nov 2022 14:29:47 +0100 Subject: [PATCH 12/16] Lowercase methods in docs --- docs/auth0_api.md | 2 +- internal/cli/api.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/auth0_api.md b/docs/auth0_api.md index 5f97a0b3c..79e67e750 100644 --- a/docs/auth0_api.md +++ b/docs/auth0_api.md @@ -15,7 +15,7 @@ Auth0 Management API Docs: https://auth0.com/docs/api/management/v2 Available Methods: - GET, POST, PUT, PATCH, DELETE + get, post, put, patch, delete ``` auth0 api [flags] diff --git a/internal/cli/api.go b/internal/cli/api.go index a6652506d..312ba8596 100644 --- a/internal/cli/api.go +++ b/internal/cli/api.go @@ -79,7 +79,7 @@ The method argument is optional, and when you don’t specify it, the command de %s %s`, "Auth0 Management API Docs:\n", apiDocsURL, - "Available Methods:\n", strings.Join(apiValidMethods, ", "), + "Available Methods:\n", strings.ToLower(strings.Join(apiValidMethods, ", ")), ), Example: `auth0 api "/stats/daily" -q "from=20221101" -q "to=20221118" auth0 api get "/tenants/settings" From 304faa15f1e5d4c6b2ba2a4729f878ed4b1cccf7 Mon Sep 17 00:00:00 2001 From: Sergiu Ghitea Date: Tue, 29 Nov 2022 14:31:24 +0100 Subject: [PATCH 13/16] Remove forward slash in api cmd examples --- docs/auth0_api.md | 4 ++-- internal/cli/api.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/auth0_api.md b/docs/auth0_api.md index 79e67e750..29686f903 100644 --- a/docs/auth0_api.md +++ b/docs/auth0_api.md @@ -24,8 +24,8 @@ auth0 api [flags] ### Examples ``` -auth0 api "/stats/daily" -q "from=20221101" -q "to=20221118" -auth0 api get "/tenants/settings" +auth0 api "stats/daily" -q "from=20221101" -q "to=20221118" +auth0 api get "tenants/settings" auth0 api clients --data "{\"name\":\"ssoTest\",\"app_type\":\"sso_integration\"}" ``` diff --git a/internal/cli/api.go b/internal/cli/api.go index 312ba8596..f928152c4 100644 --- a/internal/cli/api.go +++ b/internal/cli/api.go @@ -81,8 +81,8 @@ The method argument is optional, and when you don’t specify it, the command de "Auth0 Management API Docs:\n", apiDocsURL, "Available Methods:\n", strings.ToLower(strings.Join(apiValidMethods, ", ")), ), - Example: `auth0 api "/stats/daily" -q "from=20221101" -q "to=20221118" -auth0 api get "/tenants/settings" + Example: `auth0 api "stats/daily" -q "from=20221101" -q "to=20221118" +auth0 api get "tenants/settings" auth0 api clients --data "{\"name\":\"ssoTest\",\"app_type\":\"sso_integration\"}" `, RunE: apiCmdRun(cli, &inputs), From b1b346e3445c89bb44fb5886e8ac90d4938c83cb Mon Sep 17 00:00:00 2001 From: Sergiu Ghitea Date: Tue, 29 Nov 2022 14:43:55 +0100 Subject: [PATCH 14/16] Add warning when json data is passed both as a flag and as piped input --- internal/cli/api.go | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/internal/cli/api.go b/internal/cli/api.go index f928152c4..97b7771f2 100644 --- a/internal/cli/api.go +++ b/internal/cli/api.go @@ -13,6 +13,7 @@ import ( "github.com/auth0/auth0-cli/internal/ansi" "github.com/auth0/auth0-cli/internal/buildinfo" + "github.com/auth0/auth0-cli/internal/display" "github.com/auth0/auth0-cli/internal/iostream" "github.com/auth0/auth0-cli/internal/prompt" ) @@ -53,6 +54,8 @@ type ( } apiCmdInputs struct { + renderer *display.Renderer + RawMethod string RawURI string RawData string @@ -64,7 +67,9 @@ type ( ) func apiCmd(cli *cli) *cobra.Command { - var inputs apiCmdInputs + inputs := apiCmdInputs{ + renderer: cli.renderer, + } cmd := &cobra.Command{ Use: "api ", @@ -200,10 +205,17 @@ func (i *apiCmdInputs) validateAndSetData() error { } pipedRawData := iostream.PipedInput() - if pipedRawData != nil && data == nil { + if len(pipedRawData) > 0 && data == nil { data = pipedRawData } + if len(pipedRawData) > 0 && len(i.RawData) > 0 { + i.renderer.Warnf( + "JSON data was passed using both the flag and as piped input. " + + "The command will use only the data from the flag.", + ) + } + if len(data) > 0 && !json.Valid(data) { return fmt.Errorf("invalid json data given: %s", data) } From f401dfd09705e3d6960f19f7f9a6333cbcdadba4 Mon Sep 17 00:00:00 2001 From: Sergiu Ghitea Date: Tue, 29 Nov 2022 15:54:16 +0100 Subject: [PATCH 15/16] Update docs --- docs/auth0_api.md | 6 +++--- internal/cli/api.go | 10 +++++----- internal/cli/api_test.go | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/auth0_api.md b/docs/auth0_api.md index 29686f903..719abafec 100644 --- a/docs/auth0_api.md +++ b/docs/auth0_api.md @@ -18,7 +18,7 @@ Available Methods: get, post, put, patch, delete ``` -auth0 api [flags] +auth0 api [flags] ``` ### Examples @@ -27,13 +27,13 @@ auth0 api [flags] auth0 api "stats/daily" -q "from=20221101" -q "to=20221118" auth0 api get "tenants/settings" auth0 api clients --data "{\"name\":\"ssoTest\",\"app_type\":\"sso_integration\"}" - +cat data.json | auth0 api post clients ``` ### Options ``` - -d, --data string JSON data payload to send with the request. + -d, --data string JSON data payload to send with the request. Data can be piped in as well instead of using this flag. -h, --help help for api -q, --query stringToString Query params to send with the request. (default []) ``` diff --git a/internal/cli/api.go b/internal/cli/api.go index 97b7771f2..08f2da98e 100644 --- a/internal/cli/api.go +++ b/internal/cli/api.go @@ -25,7 +25,7 @@ var apiFlags = apiCmdFlags{ Name: "RawData", LongForm: "data", ShortForm: "d", - Help: "JSON data payload to send with the request.", + Help: "JSON data payload to send with the request. Data can be piped in as well instead of using this flag.", IsRequired: false, AlwaysPrompt: false, }, @@ -72,7 +72,7 @@ func apiCmd(cli *cli) *cobra.Command { } cmd := &cobra.Command{ - Use: "api ", + Use: "api ", Args: cobra.RangeArgs(1, 2), Short: "Makes an authenticated HTTP request to the Auth0 Management API", Long: fmt.Sprintf( @@ -89,7 +89,7 @@ The method argument is optional, and when you don’t specify it, the command de Example: `auth0 api "stats/daily" -q "from=20221101" -q "to=20221118" auth0 api get "tenants/settings" auth0 api clients --data "{\"name\":\"ssoTest\",\"app_type\":\"sso_integration\"}" -`, +cat data.json | auth0 api post clients`, RunE: apiCmdRun(cli, &inputs), } @@ -212,7 +212,7 @@ func (i *apiCmdInputs) validateAndSetData() error { if len(pipedRawData) > 0 && len(i.RawData) > 0 { i.renderer.Warnf( "JSON data was passed using both the flag and as piped input. " + - "The command will use only the data from the flag.", + "The Auth0 CLI will use only the data from the flag.", ) } @@ -226,7 +226,7 @@ func (i *apiCmdInputs) validateAndSetData() error { } func (i *apiCmdInputs) validateAndSetEndpoint(domain string) error { - endpoint, err := url.Parse("https://" + domain + "/api/v2/" + strings.Trim(i.RawURI, "/")) + endpoint, err := url.Parse(fmt.Sprintf("https://%s/api/v2/%s", domain, strings.Trim(i.RawURI, "/"))) if err != nil { return fmt.Errorf("invalid uri given: %w", err) } diff --git a/internal/cli/api_test.go b/internal/cli/api_test.go index b6c622cea..581ff60ca 100644 --- a/internal/cli/api_test.go +++ b/internal/cli/api_test.go @@ -25,14 +25,14 @@ func TestAPICmdInputs_FromArgs(t *testing.T) { }, { name: "it can correctly parse input arguments and data flag", - givenArgs: []string{"post", "/clients"}, + givenArgs: []string{"post", "clients"}, givenDataFlag: `{"name":"genericTest"}`, expectedMethod: http.MethodPost, expectedURL: "https://" + testDomain + "/api/v2/clients", }, { name: "it can correctly parse input arguments when get method is missing", - givenArgs: []string{"/tenants/settings"}, + givenArgs: []string{"tenants/settings"}, expectedMethod: http.MethodGet, expectedURL: "https://" + testDomain + "/api/v2/tenants/settings", }, @@ -50,7 +50,7 @@ func TestAPICmdInputs_FromArgs(t *testing.T) { }, { name: "it fails to parse input arguments when data is not a valid JSON", - givenArgs: []string{"patch", "/clients"}, + givenArgs: []string{"patch", "clients"}, givenDataFlag: "{", expectedError: "invalid json data given: {", }, From 951021997888cee13018a92bafda3c9ba4e719a8 Mon Sep 17 00:00:00 2001 From: Sergiu Ghitea Date: Tue, 29 Nov 2022 16:23:15 +0100 Subject: [PATCH 16/16] Add exit case 1 test case for api cmd in commander tests --- test/integration/test-cases.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/integration/test-cases.yaml b/test/integration/test-cases.yaml index 87a9db7c2..caf39c1df 100644 --- a/test/integration/test-cases.yaml +++ b/test/integration/test-cases.yaml @@ -660,3 +660,7 @@ tests: json: idle_session_lifetime: "72" exit-code: 0 + + api patch tenant settings with wrong json: + command: auth0 api patch "tenants/settings" --data "{\"idle_session_lifetime:72}" + exit-code: 1