From 67a94e684204b241093a40cfe1aaf83cbe0eabe2 Mon Sep 17 00:00:00 2001 From: "Jorge L. Fatta" Date: Fri, 21 May 2021 19:02:20 -0300 Subject: [PATCH] feat: apps `--reveal` flag to expose secrets (#301) * feat: apps --reveal flag to expose secrets * hide client secret from ls * fix: signing keys only available if --reveal * remove redundant type from array (simplifycompositelit) * Update internal/cli/apps.go Co-authored-by: Cyril David * Update internal/cli/apps.go Co-authored-by: Rita Zerrizuela Co-authored-by: Cyril David Co-authored-by: Rita Zerrizuela --- internal/cli/apps.go | 34 +++++++---- internal/cli/apps_test.go | 103 +++++++++++++++++++++----------- internal/display/apps.go | 78 ++++++++++++++++-------- internal/display/roles.go | 6 +- internal/display/rules.go | 10 ++-- internal/display/user_blocks.go | 4 +- internal/display/users.go | 14 ++--- 7 files changed, 160 insertions(+), 89 deletions(-) diff --git a/internal/cli/apps.go b/internal/cli/apps.go index a962bbcb2..91168ea94 100644 --- a/internal/cli/apps.go +++ b/internal/cli/apps.go @@ -111,6 +111,12 @@ var ( Help: "List of grant types supported for this application. Can include code, implicit, refresh-token, credentials, password, password-realm, mfa-oob, mfa-otp, mfa-recovery-code, and device-code.", IsRequired: false, } + reveal = Flag{ + Name: "Reveal", + LongForm: "reveal", + ShortForm: "r", + Help: "Display the Client Secret as part of the command output.", + } ) func appsCmd(cli *cli) *cobra.Command { @@ -179,6 +185,9 @@ func useAppCmd(cli *cli) *cobra.Command { } func listAppsCmd(cli *cli) *cobra.Command { + var inputs struct{ + Reveal bool + } cmd := &cobra.Command{ Use: "list", Aliases: []string{"ls"}, @@ -189,6 +198,7 @@ auth0 apps create`, Example: `auth0 apps list auth0 apps ls`, RunE: func(cmd *cobra.Command, args []string) error { + var list *management.ClientList if err := ansi.Waiting(func() error { @@ -199,17 +209,20 @@ auth0 apps ls`, return fmt.Errorf("An unexpected error occurred: %w", err) } - cli.renderer.ApplicationList(list.Clients) + cli.renderer.ApplicationList(list.Clients, inputs.Reveal) return nil }, } + reveal.RegisterBool(cmd, &inputs.Reveal, false) + return cmd } func showAppCmd(cli *cli) *cobra.Command { var inputs struct { - ID string + ID string + Reveal bool } cmd := &cobra.Command{ @@ -239,12 +252,13 @@ auth0 apps show `, return fmt.Errorf("Unable to load application. The Id %v specified doesn't exist", inputs.ID) } - revealClientSecret := auth0.StringValue(a.AppType) != "native" && auth0.StringValue(a.AppType) != "spa" - cli.renderer.ApplicationShow(a, revealClientSecret) + cli.renderer.ApplicationShow(a, inputs.Reveal) return nil }, } + reveal.RegisterBool(cmd, &inputs.Reveal, false) + return cmd } @@ -302,6 +316,7 @@ func createAppCmd(cli *cli) *cobra.Command { AllowedLogoutURLs []string AuthMethod string Grants []string + Reveal bool } var oidcConformant = true var algorithm = "RS256" @@ -418,9 +433,7 @@ auth0 apps create -n myapp -t [native|spa|regular|m2m] --description -n myapp --type [native|spa|regular|m2m]`, } // Render result - revealClientSecret := auth0.StringValue(a.AppType) != appTypeNative && auth0.StringValue(a.AppType) != appTypeSPA - cli.renderer.ApplicationUpdate(a, revealClientSecret) + cli.renderer.ApplicationUpdate(a, inputs.Reveal) return nil }, @@ -637,7 +651,7 @@ auth0 apps update -n myapp --type [native|spa|regular|m2m]`, appLogoutURLs.RegisterStringSliceU(cmd, &inputs.AllowedLogoutURLs, nil) appAuthMethod.RegisterStringU(cmd, &inputs.AuthMethod, "") appGrants.RegisterStringSliceU(cmd, &inputs.Grants, nil) - + reveal.RegisterBool(cmd, &inputs.Reveal, false) return cmd } diff --git a/internal/cli/apps_test.go b/internal/cli/apps_test.go index c161478a5..e92d175b3 100644 --- a/internal/cli/apps_test.go +++ b/internal/cli/apps_test.go @@ -12,46 +12,77 @@ import ( ) func TestAppsListCmd(t *testing.T) { - // Step 1: Setup our client mock for this test. We only care about - // Clients so no need to bootstrap other bits. - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - clientAPI := auth0.NewMockClientAPI(ctrl) - clientAPI.EXPECT(). - List(gomock.Any()). - Return(&management.ClientList{ - Clients: []*management.Client{ - { - Name: auth0.String("some-name"), - ClientID: auth0.String("some-id"), - Callbacks: stringToInterfaceSlice([]string{"http://localhost"}), - }, + tests := []struct { + name string + assertOutput func(t testing.TB, out string) + args []string + }{ + { + name: "happy path", + assertOutput: func(t testing.TB, out string) { + expectTable(t, out, + []string{"CLIENT ID", "NAME", "TYPE"}, + [][]string{ + {"some-id", "some-name", "Generic"}, + }, + ) + }, + }, + { + name: "reveal secrets", + args: []string{"--reveal"}, + assertOutput: func(t testing.TB, out string) { + expectTable(t, out, + []string{"CLIENT ID", "NAME", "TYPE", "CLIENT SECRET"}, + [][]string{ + {"some-id", "some-name", "Generic", "secret-here"}, + }, + ) }, - }, nil) - - stdout := &bytes.Buffer{} - - // Step 2: Setup our cli context. The important bits are - // renderer and api. - cli := &cli{ - renderer: &display.Renderer{ - MessageWriter: ioutil.Discard, - ResultWriter: stdout, }, - api: &auth0.API{Client: clientAPI}, } - cmd := listAppsCmd(cli) + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // Step 1: Setup our client mock for this test. We only care about + // Clients so no need to bootstrap other bits. + ctrl := gomock.NewController(t) + defer ctrl.Finish() - if err := cmd.Execute(); err != nil { - t.Fatal(err) - } + clientAPI := auth0.NewMockClientAPI(ctrl) + clientAPI.EXPECT(). + List(gomock.Any()). + Return(&management.ClientList{ + Clients: []*management.Client{ + { + Name: auth0.String("some-name"), + ClientID: auth0.String("some-id"), + Callbacks: stringToInterfaceSlice([]string{"http://localhost"}), + ClientSecret: auth0.String("secret-here"), + }, + }, + }, nil) - expectTable(t, stdout.String(), - []string{"CLIENT ID", "NAME", "TYPE"}, - [][]string{ - {"some-id", "some-name", "Generic"}, - }, - ) + stdout := &bytes.Buffer{} + + // Step 2: Setup our cli context. The important bits are + // renderer and api. + cli := &cli{ + renderer: &display.Renderer{ + MessageWriter: ioutil.Discard, + ResultWriter: stdout, + }, + api: &auth0.API{Client: clientAPI}, + } + + cmd := listAppsCmd(cli) + cmd.SetArgs(test.args) + + if err := cmd.Execute(); err != nil { + t.Fatal(err) + } + + test.assertOutput(t, stdout.String()) + }) + } } diff --git a/internal/display/apps.go b/internal/display/apps.go index 982ce3edb..bb9d5122f 100644 --- a/internal/display/apps.go +++ b/internal/display/apps.go @@ -95,36 +95,36 @@ func (v *applicationView) KeyValues() [][]string { if v.revealSecret { return [][]string{ - []string{"CLIENT ID", ansi.Faint(v.ClientID)}, - []string{"NAME", v.Name}, - []string{"DESCRIPTION", v.Description}, - []string{"TYPE", applyColor(v.Type)}, - []string{"CLIENT SECRET", ansi.Italic(v.ClientSecret)}, - []string{"CALLBACKS", callbacks}, - []string{"ALLOWED LOGOUT URLS", allowedLogoutURLs}, - []string{"ALLOWED ORIGINS", allowedOrigins}, - []string{"ALLOWED WEB ORIGINS", allowedWebOrigins}, - []string{"TOKEN ENDPOINT AUTH", v.AuthMethod}, - []string{"GRANTS", grants}, + {"CLIENT ID", ansi.Faint(v.ClientID)}, + {"NAME", v.Name}, + {"DESCRIPTION", v.Description}, + {"TYPE", applyColor(v.Type)}, + {"CLIENT SECRET", ansi.Italic(v.ClientSecret)}, + {"CALLBACKS", callbacks}, + {"ALLOWED LOGOUT URLS", allowedLogoutURLs}, + {"ALLOWED ORIGINS", allowedOrigins}, + {"ALLOWED WEB ORIGINS", allowedWebOrigins}, + {"TOKEN ENDPOINT AUTH", v.AuthMethod}, + {"GRANTS", grants}, } } return [][]string{ - []string{"CLIENT ID", ansi.Faint(v.ClientID)}, - []string{"NAME", v.Name}, - []string{"DESCRIPTION", v.Description}, - []string{"TYPE", applyColor(v.Type)}, - []string{"CALLBACKS", callbacks}, - []string{"ALLOWED LOGOUT URLS", allowedLogoutURLs}, - []string{"ALLOWED ORIGINS", allowedOrigins}, - []string{"ALLOWED WEB ORIGINS", allowedWebOrigins}, - []string{"TOKEN ENDPOINT AUTH", v.AuthMethod}, - []string{"GRANTS", grants}, + {"CLIENT ID", ansi.Faint(v.ClientID)}, + {"NAME", v.Name}, + {"DESCRIPTION", v.Description}, + {"TYPE", applyColor(v.Type)}, + {"CALLBACKS", callbacks}, + {"ALLOWED LOGOUT URLS", allowedLogoutURLs}, + {"ALLOWED ORIGINS", allowedOrigins}, + {"ALLOWED WEB ORIGINS", allowedWebOrigins}, + {"TOKEN ENDPOINT AUTH", v.AuthMethod}, + {"GRANTS", grants}, } } func (v *applicationView) Object() interface{} { - return v.raw + return safeRaw(v.raw.(*management.Client), v.revealSecret) } // applicationListView is a slimmed down view of a client for displaying @@ -133,13 +133,13 @@ type applicationListView struct { Name string Type string ClientID string - ClientSecret string + ClientSecret string `json:"ClientSecret,omitempty"` revealSecret bool } func (v *applicationListView) AsTableHeader() []string { if v.revealSecret { - return []string{"ClientID", "Name", "Type", "Client Secret"} + return []string{"Client ID", "Name", "Type", "Client Secret"} } return []string{"Client ID", "Name", "Type"} } @@ -160,7 +160,7 @@ func (v *applicationListView) AsTableRow() []string { } } -func (r *Renderer) ApplicationList(clients []*management.Client) { +func (r *Renderer) ApplicationList(clients []*management.Client, revealSecrets bool) { resource := "applications" r.Heading(resource) @@ -176,11 +176,19 @@ func (r *Renderer) ApplicationList(clients []*management.Client) { if auth0.StringValue(c.Name) == deprecatedAppName { continue } + + // in case of format=JSON: + clientSecret := "" + if revealSecrets { + clientSecret = auth0.StringValue(c.ClientSecret) + } + res = append(res, &applicationListView{ + revealSecret: revealSecrets, Name: auth0.StringValue(c.Name), Type: appTypeFor(c.AppType), ClientID: auth0.StringValue(c.ClientID), - ClientSecret: auth0.StringValue(c.ClientSecret), + ClientSecret: clientSecret, }) } @@ -212,6 +220,10 @@ func (r *Renderer) ApplicationShow(client *management.Client, revealSecrets bool func (r *Renderer) ApplicationCreate(client *management.Client, revealSecrets bool) { r.Heading("application created") + if !revealSecrets { + client.ClientSecret = auth0.String("") + } + v := &applicationView{ revealSecret: revealSecrets, Name: auth0.StringValue(client.Name), @@ -247,6 +259,10 @@ func (r *Renderer) ApplicationCreate(client *management.Client, revealSecrets bo func (r *Renderer) ApplicationUpdate(client *management.Client, revealSecrets bool) { r.Heading("application updated") + if !revealSecrets { + client.ClientSecret = auth0.String("") + } + v := &applicationView{ revealSecret: revealSecrets, Name: auth0.StringValue(client.Name), @@ -328,3 +344,13 @@ func applyColor(a string) string { return a } } + +func safeRaw(c *management.Client, revealSecrets bool) *management.Client { + if revealSecrets { + return c + } + + c.ClientSecret = nil + c.SigningKeys = nil + return c +} diff --git a/internal/display/roles.go b/internal/display/roles.go index 83c5e4704..8f50d1c02 100644 --- a/internal/display/roles.go +++ b/internal/display/roles.go @@ -25,9 +25,9 @@ func (v *roleView) AsTableRow() []string { func (v *roleView) KeyValues() [][]string { return [][]string{ - []string{"ID", ansi.Faint(v.ID)}, - []string{"NAME", v.Name}, - []string{"DESCRIPTION", v.Description}, + {"ID", ansi.Faint(v.ID)}, + {"NAME", v.Name}, + {"DESCRIPTION", v.Description}, } } diff --git a/internal/display/rules.go b/internal/display/rules.go index 013fa37bb..6b7f32a7a 100644 --- a/internal/display/rules.go +++ b/internal/display/rules.go @@ -29,11 +29,11 @@ func (v *ruleView) AsTableRow() []string { func (v *ruleView) KeyValues() [][]string { return [][]string{ - []string{"NAME", v.Name}, - []string{"ID", ansi.Faint(v.ID)}, - []string{"ENABLED", strconv.FormatBool(v.Enabled)}, - []string{"ORDER", strconv.Itoa(v.Order)}, - []string{"SCRIPT", v.Script}, + {"NAME", v.Name}, + {"ID", ansi.Faint(v.ID)}, + {"ENABLED", strconv.FormatBool(v.Enabled)}, + {"ORDER", strconv.Itoa(v.Order)}, + {"SCRIPT", v.Script}, } } diff --git a/internal/display/user_blocks.go b/internal/display/user_blocks.go index a51748d8f..a3a7c9c1c 100644 --- a/internal/display/user_blocks.go +++ b/internal/display/user_blocks.go @@ -19,8 +19,8 @@ func (v *userBlockView) AsTableRow() []string { func (v *userBlockView) KeyValues() [][]string { return [][]string{ - []string{"Identifier", v.Identifier}, - []string{"IP", v.IP}, + {"Identifier", v.Identifier}, + {"IP", v.IP}, } } diff --git a/internal/display/users.go b/internal/display/users.go index 327bce940..916a9529c 100644 --- a/internal/display/users.go +++ b/internal/display/users.go @@ -36,16 +36,16 @@ func (v *userView) AsTableRow() []string { func (v *userView) KeyValues() [][]string { if v.RequireUsername { return [][]string{ - []string{"ID", ansi.Faint(v.UserID)}, - []string{"EMAIL", v.Email}, - []string{"CONNECTION", v.Connection}, - []string{"USERNAME", v.Username}, + {"ID", ansi.Faint(v.UserID)}, + {"EMAIL", v.Email}, + {"CONNECTION", v.Connection}, + {"USERNAME", v.Username}, } } return [][]string{ - []string{"ID", ansi.Faint(v.UserID)}, - []string{"EMAIL", v.Email}, - []string{"CONNECTION", v.Connection}, + {"ID", ansi.Faint(v.UserID)}, + {"EMAIL", v.Email}, + {"CONNECTION", v.Connection}, } }