From 2680e2e408ae01ef08709fcc033cd1ac433d22dc Mon Sep 17 00:00:00 2001 From: Will Vedder Date: Thu, 5 Jan 2023 11:46:53 -0500 Subject: [PATCH 1/3] DXCDT-301: Refactor secret storage (1/2) (#577) * Refactoring storing of secrets * Fixing tests * Cleaning up tests * Fixing tests * Using internal function instead * Rewriting error messages * Reorganizing into own secret module * Rewriting error * Fixing test * Rename secret to keyring * Refactor keyring pkg Co-authored-by: Will Vedder Co-authored-by: Sergiu Ghitea --- internal/auth/auth.go | 10 ----- internal/auth/secrets.go | 22 ----------- internal/auth/secrets_test.go | 63 -------------------------------- internal/auth/token.go | 15 +++----- internal/auth/token_test.go | 17 ++++++--- internal/cli/cli.go | 12 +++--- internal/cli/login.go | 11 ++---- internal/cli/logout.go | 2 +- internal/keyring/keyring.go | 32 ++++++++++++++++ internal/keyring/keyring_test.go | 43 ++++++++++++++++++++++ 10 files changed, 102 insertions(+), 125 deletions(-) delete mode 100644 internal/auth/secrets.go delete mode 100644 internal/auth/secrets_test.go create mode 100644 internal/keyring/keyring.go create mode 100644 internal/keyring/keyring_test.go diff --git a/internal/auth/auth.go b/internal/auth/auth.go index bcbbcdd62..e5e94f299 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -19,8 +19,6 @@ import ( const ( audiencePath = "/api/v2/" waitThresholdInSeconds = 3 - // SecretsNamespace is the namespace used to set/get values from the keychain. - SecretsNamespace = "auth0-cli" ) var requiredScopes = []string{ @@ -52,14 +50,6 @@ type Authenticator struct { OauthTokenEndpoint string } -// SecretStore provides access to stored sensitive data. -type SecretStore interface { - // Get gets the secret - Get(namespace, key string) (string, error) - // Delete removes the secret - Delete(namespace, key string) error -} - type Result struct { Tenant string Domain string diff --git a/internal/auth/secrets.go b/internal/auth/secrets.go deleted file mode 100644 index 86203ad1b..000000000 --- a/internal/auth/secrets.go +++ /dev/null @@ -1,22 +0,0 @@ -package auth - -import ( - "github.com/zalando/go-keyring" -) - -type Keyring struct{} - -// Set sets the given key/value pair with the given namespace. -func (k *Keyring) Set(namespace, key, value string) error { - return keyring.Set(namespace, key, value) -} - -// Get gets a value for the given namespace and key. -func (k *Keyring) Get(namespace, key string) (string, error) { - return keyring.Get(namespace, key) -} - -// Delete deletes a value for the given namespace and key. -func (k *Keyring) Delete(namespace, key string) error { - return keyring.Delete(namespace, key) -} diff --git a/internal/auth/secrets_test.go b/internal/auth/secrets_test.go deleted file mode 100644 index e98a3fdc1..000000000 --- a/internal/auth/secrets_test.go +++ /dev/null @@ -1,63 +0,0 @@ -package auth - -import ( - "testing" - - "github.com/zalando/go-keyring" -) - -func TestSecrets(t *testing.T) { - t.Run("fail: not found", func(t *testing.T) { - // init underlying keychain manager - keyring.MockInit() - - kr := &Keyring{} - _, err := kr.Get("mynamespace", "foo") - - if got, want := err, keyring.ErrNotFound; got != want { - t.Fatalf("wanted error: %v, got: %v", want, got) - } - }) - - t.Run("succeed: get secret", func(t *testing.T) { - // init underlying keychain manager - keyring.MockInit() - - // set with the underlying manager: - err := keyring.Set("mynamespace", "foo", "bar") - if err != nil { - t.Fatal(err) - } - - kr := &Keyring{} - v, err := kr.Get("mynamespace", "foo") - if err != nil { - t.Fatal(err) - } - - if got, want := v, "bar"; got != want { - t.Fatalf("wanted error: %v, got: %v", want, got) - } - }) - - t.Run("succeed: set secret", func(t *testing.T) { - // init underlying keychain manager - keyring.MockInit() - - kr := &Keyring{} - err := kr.Set("mynamespace", "foo", "bar") - if err != nil { - t.Fatal(err) - } - - // get with the underlying manager: - v, err := keyring.Get("mynamespace", "foo") - if err != nil { - t.Fatal(err) - } - - if got, want := v, "bar"; got != want { - t.Fatalf("wanted secret: %v, got: %v", want, got) - } - }) -} diff --git a/internal/auth/token.go b/internal/auth/token.go index 122b5b137..9969a9a3d 100644 --- a/internal/auth/token.go +++ b/internal/auth/token.go @@ -8,6 +8,8 @@ import ( "io" "net/http" "net/url" + + "github.com/auth0/auth0-cli/internal/keyring" ) type TokenResponse struct { @@ -19,25 +21,18 @@ type TokenResponse struct { type TokenRetriever struct { Authenticator *Authenticator - Secrets SecretStore Client *http.Client } -// Delete deletes the given tenant from the secrets storage. -func (t *TokenRetriever) Delete(tenant string) error { - return t.Secrets.Delete(SecretsNamespace, tenant) -} - // Refresh gets a new access token from the provided refresh token, // The request is used the default client_id and endpoint for device authentication. func (t *TokenRetriever) Refresh(ctx context.Context, tenant string) (TokenResponse, error) { - // get stored refresh token: - refreshToken, err := t.Secrets.Get(SecretsNamespace, tenant) + refreshToken, err := keyring.GetRefreshToken(tenant) if err != nil { - return TokenResponse{}, fmt.Errorf("cannot get the stored refresh token: %w", err) + return TokenResponse{}, fmt.Errorf("failed to retrieve refresh token from keyring: %w", err) } if refreshToken == "" { - return TokenResponse{}, errors.New("cannot use the stored refresh token: the token is empty") + return TokenResponse{}, errors.New("failed to use stored refresh token: the token is empty") } // get access token: r, err := t.Client.PostForm(t.Authenticator.OauthTokenEndpoint, url.Values{ diff --git a/internal/auth/token_test.go b/internal/auth/token_test.go index 8f404254e..b9e637af9 100644 --- a/internal/auth/token_test.go +++ b/internal/auth/token_test.go @@ -8,11 +8,12 @@ import ( "testing" "github.com/golang/mock/gomock" + goKeyring "github.com/zalando/go-keyring" - "github.com/auth0/auth0-cli/internal/auth/mock" + "github.com/auth0/auth0-cli/internal/keyring" ) -// HTTPTransport implements an http.RoundTripper for testing purposes only. +// HTTPTransport implements a http.RoundTripper for testing purposes only. type testTransport struct { withResponse *http.Response withError error @@ -29,8 +30,13 @@ func TestTokenRetriever_Refresh(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - secretsMock := mock.NewMockSecretStore(ctrl) - secretsMock.EXPECT().Get("auth0-cli", "mytenant").Return("refresh-token-here", nil).Times(1) + testTenantName := "auth0-cli-test.us.auth0.com" + + goKeyring.MockInit() + err := keyring.StoreRefreshToken(testTenantName, "refresh-token-here") + if err != nil { + t.Fatal(err) + } transport := &testTransport{ withResponse: &http.Response{ @@ -48,11 +54,10 @@ func TestTokenRetriever_Refresh(t *testing.T) { tr := &TokenRetriever{ Authenticator: &Authenticator{"https://test.com/api/v2/", "client-id", "https://test.com/oauth/device/code", "https://test.com/token"}, - Secrets: secretsMock, Client: client, } - got, err := tr.Refresh(context.Background(), "mytenant") + got, err := tr.Refresh(context.Background(), testTenantName) if err != nil { t.Fatal(err) } diff --git a/internal/cli/cli.go b/internal/cli/cli.go index 00870a0fc..062aa9b99 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -26,6 +26,7 @@ import ( "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/keyring" ) const ( @@ -149,7 +150,6 @@ func (t *Tenant) regenerateAccessToken(ctx context.Context, c *cli) error { if t.authenticatedWithDeviceCodeFlow() { tokenRetriever := &auth.TokenRetriever{ Authenticator: c.authenticator, - Secrets: &auth.Keyring{}, Client: http.DefaultClient, } @@ -250,7 +250,8 @@ func (c *cli) prepareTenant(ctx context.Context) (Tenant, error) { ) } - c.renderer.Warnf("Failed to renew access token. Please log in to re-authorize the CLI.\n") + c.renderer.Warnf("Failed to renew access token: %s", err) + c.renderer.Warnf("Please log in to re-authorize the CLI.\n") return RunLoginAsUser(ctx, c, t.additionalRequestedScopes()) } @@ -365,12 +366,11 @@ func (c *cli) removeTenant(ten string) error { } if err := c.persistConfig(); err != nil { - return fmt.Errorf("Unexpected error persisting config: %w", err) + return fmt.Errorf("failed to persist config: %w", err) } - tr := &auth.TokenRetriever{Secrets: &auth.Keyring{}} - if err := tr.Delete(ten); err != nil { - return fmt.Errorf("Unexpected error clearing tenant information: %w", err) + if err := keyring.DeleteSecretsForTenant(ten); err != nil { + return fmt.Errorf("failed to delete tenant secrets: %w", err) } return nil diff --git a/internal/cli/login.go b/internal/cli/login.go index b54780413..362092c5f 100644 --- a/internal/cli/login.go +++ b/internal/cli/login.go @@ -9,6 +9,7 @@ import ( "github.com/auth0/auth0-cli/internal/ansi" "github.com/auth0/auth0-cli/internal/auth" + "github.com/auth0/auth0-cli/internal/keyring" "github.com/auth0/auth0-cli/internal/prompt" ) @@ -195,13 +196,9 @@ func RunLoginAsUser(ctx context.Context, cli *cli, additionalScopes []string) (T cli.renderer.Infof("Tenant: %s", result.Domain) cli.renderer.Newline() - // Store the refresh token. - secretsStore := &auth.Keyring{} - err = secretsStore.Set(auth.SecretsNamespace, result.Domain, result.RefreshToken) - if err != nil { - message = "Could not store the refresh token locally, " + - "please expect to login again once your access token expired. See %s." - cli.renderer.Warnf(message, "https://github.com/auth0/auth0-cli/blob/main/KNOWN-ISSUES.md") + if err := keyring.StoreRefreshToken(result.Domain, result.RefreshToken); err != nil { + cli.renderer.Warnf("Could not store the refresh token to the keyring: %s", err) + cli.renderer.Warnf("Expect to login again when your access token expires.") } tenant := Tenant{ diff --git a/internal/cli/logout.go b/internal/cli/logout.go index 8fc8d5fd5..d0b3f37f7 100644 --- a/internal/cli/logout.go +++ b/internal/cli/logout.go @@ -50,7 +50,7 @@ func logoutCmd(cli *cli) *cobra.Command { } if err := cli.removeTenant(selectedTenant); err != nil { - return err // This error is already formatted for display + return err } cli.renderer.Infof("Successfully logged out tenant: %s", selectedTenant) diff --git a/internal/keyring/keyring.go b/internal/keyring/keyring.go new file mode 100644 index 000000000..0398fb780 --- /dev/null +++ b/internal/keyring/keyring.go @@ -0,0 +1,32 @@ +package keyring + +import ( + "errors" + + "github.com/zalando/go-keyring" +) + +const secretRefreshToken = "Auth0 CLI Refresh Token" + +// StoreRefreshToken stores a tenant's refresh token in the system keyring. +func StoreRefreshToken(tenant, value string) error { + return keyring.Set(secretRefreshToken, tenant, value) +} + +// GetRefreshToken retrieves a tenant's refresh token from the system keyring. +func GetRefreshToken(tenant string) (string, error) { + return keyring.Get(secretRefreshToken, tenant) +} + +// DeleteSecretsForTenant deletes all secrets for a given tenant. +func DeleteSecretsForTenant(tenant string) error { + if err := keyring.Delete(secretRefreshToken, tenant); err != nil { + if errors.Is(err, keyring.ErrNotFound) { + return nil + } + + return err + } + + return nil +} diff --git a/internal/keyring/keyring_test.go b/internal/keyring/keyring_test.go new file mode 100644 index 000000000..a9110baba --- /dev/null +++ b/internal/keyring/keyring_test.go @@ -0,0 +1,43 @@ +package keyring + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/zalando/go-keyring" +) + +const testTenantName = "auth0-cli-test.us.auth0.com" + +func TestSecrets(t *testing.T) { + t.Run("it fails to retrieve an nonexistent refresh token", func(t *testing.T) { + keyring.MockInit() + + _, actualError := GetRefreshToken(testTenantName) + assert.EqualError(t, actualError, keyring.ErrNotFound.Error()) + }) + + t.Run("it successfully retrieves an existent refresh token", func(t *testing.T) { + keyring.MockInit() + + expectedRefreshToken := "fake-refresh-token" + err := keyring.Set(secretRefreshToken, testTenantName, expectedRefreshToken) + assert.NoError(t, err) + + actualRefreshToken, err := GetRefreshToken(testTenantName) + assert.NoError(t, err) + assert.Equal(t, expectedRefreshToken, actualRefreshToken) + }) + + t.Run("it successfully stores a refresh token", func(t *testing.T) { + keyring.MockInit() + + expectedRefreshToken := "fake-refresh-token" + err := StoreRefreshToken(testTenantName, expectedRefreshToken) + assert.NoError(t, err) + + actualRefreshToken, err := GetRefreshToken(testTenantName) + assert.NoError(t, err) + assert.Equal(t, expectedRefreshToken, actualRefreshToken) + }) +} From 8c4301c437b6d7813bb29384ca806aae44cbc980 Mon Sep 17 00:00:00 2001 From: Will Vedder Date: Thu, 5 Jan 2023 12:03:01 -0500 Subject: [PATCH 2/3] DXCDT-301: Storing client secret in keyring (2/2) (#578) Store client secret in keyring Co-authored-by: Sergiu Ghitea --- internal/cli/cli.go | 22 +++++++++++++------- internal/cli/login.go | 16 +++++++++------ internal/keyring/keyring.go | 35 +++++++++++++++++++++++++++----- internal/keyring/keyring_test.go | 31 ++++++++++++++++++++++++++++ 4 files changed, 86 insertions(+), 18 deletions(-) diff --git a/internal/cli/cli.go b/internal/cli/cli.go index 062aa9b99..eb0cd381c 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -54,7 +54,6 @@ type Tenant struct { Apps map[string]app `json:"apps,omitempty"` DefaultAppID string `json:"default_app_id,omitempty"` ClientID string `json:"client_id"` - ClientSecret string `json:"client_secret"` } type app struct { @@ -97,11 +96,11 @@ type cli struct { } func (t *Tenant) authenticatedWithClientCredentials() bool { - return t.ClientID != "" && t.ClientSecret != "" + return t.ClientID != "" } func (t *Tenant) authenticatedWithDeviceCodeFlow() bool { - return t.ClientID == "" && t.ClientSecret == "" + return t.ClientID == "" } func (t *Tenant) hasExpiredToken() bool { @@ -131,11 +130,16 @@ func (t *Tenant) additionalRequestedScopes() []string { func (t *Tenant) regenerateAccessToken(ctx context.Context, c *cli) error { if t.authenticatedWithClientCredentials() { + clientSecret, err := keyring.GetClientSecret(t.Domain) + if err != nil { + return fmt.Errorf("failed to retrieve client secret from keyring: %w", err) + } + token, err := auth.GetAccessTokenFromClientCreds( ctx, auth.ClientCredentials{ ClientID: t.ClientID, - ClientSecret: t.ClientSecret, + ClientSecret: clientSecret, Domain: t.Domain, }, ) @@ -242,12 +246,16 @@ func (c *cli) prepareTenant(ctx context.Context) (Tenant, error) { if err := t.regenerateAccessToken(ctx, c); err != nil { if t.authenticatedWithClientCredentials() { - return t, fmt.Errorf( - "failed to fetch access token using client credentials.\n\n"+ - "This may occur if the designated application has been deleted or the client secret has been rotated.\n\n"+ + errorMessage := fmt.Errorf( + "failed to fetch access token using client credentials: %w\n\n"+ + "This may occur if the designated Auth0 application has been deleted, "+ + "the client secret has been rotated or previous failure to store client secret in the keyring.\n\n"+ "Please re-authenticate by running: %s", + err, ansi.Bold("auth0 login --domain --client-secret "), ) + + return t, errorMessage } c.renderer.Warnf("Failed to renew access token: %s", err) diff --git a/internal/cli/login.go b/internal/cli/login.go index 362092c5f..e79468980 100644 --- a/internal/cli/login.go +++ b/internal/cli/login.go @@ -266,15 +266,19 @@ func RunLoginAsMachine(ctx context.Context, inputs LoginInputs, cli *cli, cmd *c "Ensure that the provided client-id, client-secret and domain are correct. \n\nerror: %w\n", err) } + if err = keyring.StoreClientSecret(inputs.Domain, inputs.ClientSecret); err != nil { + cli.renderer.Warnf("Could not store the client secret to the keyring: %s", err) + cli.renderer.Warnf("Expect to login again when your access token expires.") + } + t := Tenant{ - Domain: inputs.Domain, - AccessToken: token.AccessToken, - ExpiresAt: token.ExpiresAt, - ClientID: inputs.ClientID, - ClientSecret: inputs.ClientSecret, + Domain: inputs.Domain, + AccessToken: token.AccessToken, + ExpiresAt: token.ExpiresAt, + ClientID: inputs.ClientID, } - if err := cli.addTenant(t); err != nil { + if err = cli.addTenant(t); err != nil { return fmt.Errorf("unexpected error when attempting to save tenant data: %w", err) } diff --git a/internal/keyring/keyring.go b/internal/keyring/keyring.go index 0398fb780..e59e80502 100644 --- a/internal/keyring/keyring.go +++ b/internal/keyring/keyring.go @@ -2,11 +2,16 @@ package keyring import ( "errors" + "fmt" + "strings" "github.com/zalando/go-keyring" ) -const secretRefreshToken = "Auth0 CLI Refresh Token" +const ( + secretRefreshToken = "Auth0 CLI Refresh Token" + secretClientSecret = "Auth0 CLI Client Secret" +) // StoreRefreshToken stores a tenant's refresh token in the system keyring. func StoreRefreshToken(tenant, value string) error { @@ -18,15 +23,35 @@ func GetRefreshToken(tenant string) (string, error) { return keyring.Get(secretRefreshToken, tenant) } +// StoreClientSecret stores a tenant's client secret in the system keyring. +func StoreClientSecret(tenant, value string) error { + return keyring.Set(secretClientSecret, tenant, value) +} + +// GetClientSecret retrieves a tenant's client secret from the system keyring. +func GetClientSecret(tenant string) (string, error) { + return keyring.Get(secretClientSecret, tenant) +} + // DeleteSecretsForTenant deletes all secrets for a given tenant. func DeleteSecretsForTenant(tenant string) error { + var multiErrors []string + if err := keyring.Delete(secretRefreshToken, tenant); err != nil { - if errors.Is(err, keyring.ErrNotFound) { - return nil + if !errors.Is(err, keyring.ErrNotFound) { + multiErrors = append(multiErrors, fmt.Sprintf("failed to delete refresh token from keyring: %s", err)) } + } + + if err := keyring.Delete(secretClientSecret, tenant); err != nil { + if !errors.Is(err, keyring.ErrNotFound) { + multiErrors = append(multiErrors, fmt.Sprintf("failed to delete client secret from keyring: %s", err)) + } + } - return err + if len(multiErrors) == 0 { + return nil } - return nil + return errors.New(strings.Join(multiErrors, ", ")) } diff --git a/internal/keyring/keyring_test.go b/internal/keyring/keyring_test.go index a9110baba..9c8d84cfb 100644 --- a/internal/keyring/keyring_test.go +++ b/internal/keyring/keyring_test.go @@ -40,4 +40,35 @@ func TestSecrets(t *testing.T) { assert.NoError(t, err) assert.Equal(t, expectedRefreshToken, actualRefreshToken) }) + + t.Run("it fails to retrieve an nonexistent client secret", func(t *testing.T) { + keyring.MockInit() + + _, actualError := GetClientSecret(testTenantName) + assert.EqualError(t, actualError, keyring.ErrNotFound.Error()) + }) + + t.Run("it successfully retrieves an existent client secret", func(t *testing.T) { + keyring.MockInit() + + expectedRefreshToken := "fake-refresh-token" + err := keyring.Set(secretClientSecret, testTenantName, expectedRefreshToken) + assert.NoError(t, err) + + actualRefreshToken, err := GetClientSecret(testTenantName) + assert.NoError(t, err) + assert.Equal(t, expectedRefreshToken, actualRefreshToken) + }) + + t.Run("it successfully stores a client secret", func(t *testing.T) { + keyring.MockInit() + + expectedRefreshToken := "fake-refresh-token" + err := StoreClientSecret(testTenantName, expectedRefreshToken) + assert.NoError(t, err) + + actualRefreshToken, err := GetClientSecret(testTenantName) + assert.NoError(t, err) + assert.Equal(t, expectedRefreshToken, actualRefreshToken) + }) } From abb6b2eaa6519ddee7a44b9e49461bfea327a4ed Mon Sep 17 00:00:00 2001 From: Sergiu Ghitea <28300158+sergiught@users.noreply.github.com> Date: Thu, 5 Jan 2023 18:23:43 +0100 Subject: [PATCH 3/3] DXCDT-303: Fix orgs update cmd (#583) --- internal/cli/organizations.go | 97 +++++++++++------------- test/integration/scripts/get-org-id.sh | 6 ++ test/integration/scripts/test-cleanup.sh | 16 ++++ test/integration/test-cases.yaml | 48 +++++++++++- 4 files changed, 113 insertions(+), 54 deletions(-) create mode 100755 test/integration/scripts/get-org-id.sh diff --git a/internal/cli/organizations.go b/internal/cli/organizations.go index 89eb06151..c8938292b 100644 --- a/internal/cli/organizations.go +++ b/internal/cli/organizations.go @@ -229,46 +229,42 @@ func createOrganizationCmd(cli *cli) *cobra.Command { return err } - o := &management.Organization{ + newOrg := &management.Organization{ Name: &inputs.Name, DisplayName: &inputs.DisplayName, - Metadata: &inputs.Metadata, } - isLogoURLSet := len(inputs.LogoURL) > 0 - isAccentColorSet := len(inputs.AccentColor) > 0 - isBackgroundColorSet := len(inputs.BackgroundColor) > 0 - isAnyColorSet := isAccentColorSet || isBackgroundColorSet - - if isLogoURLSet || isAnyColorSet { - o.Branding = &management.OrganizationBranding{} - - if isLogoURLSet { - o.Branding.LogoURL = &inputs.LogoURL - } - - if isAnyColorSet { - colors := make(map[string]string) + if inputs.Metadata != nil { + newOrg.Metadata = &inputs.Metadata + } - if isAccentColorSet { - colors[apiOrganizationColorPrimary] = inputs.AccentColor - } + branding := management.OrganizationBranding{} + if inputs.LogoURL != "" { + branding.LogoURL = &inputs.LogoURL + } - if isBackgroundColorSet { - colors[apiOrganizationColorPageBackground] = inputs.BackgroundColor - } + colors := make(map[string]string) + if inputs.AccentColor != "" { + colors[apiOrganizationColorPrimary] = inputs.AccentColor + } + if inputs.BackgroundColor != "" { + colors[apiOrganizationColorPageBackground] = inputs.BackgroundColor + } + if len(colors) > 0 { + branding.Colors = &colors + } - o.Branding.Colors = &colors - } + if branding.String() != "{}" { + newOrg.Branding = &branding } if err := ansi.Waiting(func() error { - return cli.api.Organization.Create(o) + return cli.api.Organization.Create(newOrg) }); err != nil { - return fmt.Errorf("An unexpected error occurred while attempting to create an organization with name '%s': %w", inputs.Name, err) + return fmt.Errorf("failed to create an organization with name '%s': %w", inputs.Name, err) } - cli.renderer.OrganizationCreate(o) + cli.renderer.OrganizationCreate(newOrg) return nil }, } @@ -317,46 +313,42 @@ func updateOrganizationCmd(cli *cli) *cobra.Command { } } - var current *management.Organization + var oldOrg *management.Organization err := ansi.Waiting(func() error { var err error - current, err = cli.api.Organization.Read(inputs.ID) + oldOrg, err = cli.api.Organization.Read(inputs.ID) return err }) if err != nil { - return fmt.Errorf("Failed to fetch organization with ID: %s %v", inputs.ID, err) + return fmt.Errorf("failed to fetch organization with ID: %s %w", inputs.ID, err) } - if err := organizationDisplay.AskU(cmd, &inputs.DisplayName, current.DisplayName); err != nil { + if err := organizationDisplay.AskU(cmd, &inputs.DisplayName, oldOrg.DisplayName); err != nil { return err } if inputs.DisplayName == "" { - inputs.DisplayName = current.GetDisplayName() + inputs.DisplayName = oldOrg.GetDisplayName() } - // Prepare organization payload for update. This will also be - // re-hydrated by the SDK, which we'll use below during - // display. - o := &management.Organization{ - ID: current.ID, + newOrg := &management.Organization{ DisplayName: &inputs.DisplayName, } isLogoURLSet := len(inputs.LogoURL) > 0 isAccentColorSet := len(inputs.AccentColor) > 0 isBackgroundColorSet := len(inputs.BackgroundColor) > 0 - currentHasBranding := current.Branding != nil - currentHasColors := currentHasBranding && current.Branding.Colors != nil + currentHasBranding := oldOrg.Branding != nil + currentHasColors := currentHasBranding && oldOrg.Branding.Colors != nil needToAddColors := isAccentColorSet || isBackgroundColorSet || currentHasColors if isLogoURLSet || needToAddColors { - o.Branding = &management.OrganizationBranding{} + newOrg.Branding = &management.OrganizationBranding{} if isLogoURLSet { - o.Branding.LogoURL = &inputs.LogoURL + newOrg.Branding.LogoURL = &inputs.LogoURL } else if currentHasBranding { - o.Branding.LogoURL = current.Branding.LogoURL + newOrg.Branding.LogoURL = oldOrg.Branding.LogoURL } if needToAddColors { @@ -364,33 +356,32 @@ func updateOrganizationCmd(cli *cli) *cobra.Command { if isAccentColorSet { colors[apiOrganizationColorPrimary] = inputs.AccentColor - } else if currentHasColors && len(current.Branding.GetColors()[apiOrganizationColorPrimary]) > 0 { - colors[apiOrganizationColorPrimary] = current.Branding.GetColors()[apiOrganizationColorPrimary] + } else if currentHasColors && len(oldOrg.Branding.GetColors()[apiOrganizationColorPrimary]) > 0 { + colors[apiOrganizationColorPrimary] = oldOrg.Branding.GetColors()[apiOrganizationColorPrimary] } if isBackgroundColorSet { colors[apiOrganizationColorPageBackground] = inputs.BackgroundColor - } else if currentHasColors && len(current.Branding.GetColors()[apiOrganizationColorPageBackground]) > 0 { - colors[apiOrganizationColorPageBackground] = current.Branding.GetColors()[apiOrganizationColorPageBackground] + } else if currentHasColors && len(oldOrg.Branding.GetColors()[apiOrganizationColorPageBackground]) > 0 { + colors[apiOrganizationColorPageBackground] = oldOrg.Branding.GetColors()[apiOrganizationColorPageBackground] } - o.Branding.Colors = &colors + newOrg.Branding.Colors = &colors } } - if len(inputs.Metadata) == 0 { - o.Metadata = current.Metadata - } else { - o.Metadata = &inputs.Metadata + newOrg.Metadata = oldOrg.Metadata + if len(inputs.Metadata) != 0 { + newOrg.Metadata = &inputs.Metadata } if err = ansi.Waiting(func() error { - return cli.api.Organization.Update(inputs.ID, o) + return cli.api.Organization.Update(inputs.ID, newOrg) }); err != nil { return err } - cli.renderer.OrganizationUpdate(o) + cli.renderer.OrganizationUpdate(newOrg) return nil }, } diff --git a/test/integration/scripts/get-org-id.sh b/test/integration/scripts/get-org-id.sh new file mode 100755 index 000000000..b5ec98ef1 --- /dev/null +++ b/test/integration/scripts/get-org-id.sh @@ -0,0 +1,6 @@ +#! /bin/bash + +org=$( auth0 orgs create -n integration-test-org-better -d "Integration Test Better Organization" --json --no-input ) + +mkdir -p ./test/integration/identifiers +echo "$org" | jq -r '.["id"]' > ./test/integration/identifiers/org-id diff --git a/test/integration/scripts/test-cleanup.sh b/test/integration/scripts/test-cleanup.sh index f20930530..a8da99c0f 100755 --- a/test/integration/scripts/test-cleanup.sh +++ b/test/integration/scripts/test-cleanup.sh @@ -88,3 +88,19 @@ for rule in $( echo "${rules}" | jq -r '.[] | @base64' ); do $( auth0 rules delete "$id") fi done + +orgs=$( auth0 orgs list --json --no-input ) + +for org in $( echo "${orgs}" | jq -r '.[] | @base64' ); do + _jq() { + echo "${org}" | base64 --decode | jq -r "${1}" + } + + id=$(_jq '.id') + name=$(_jq '.name') + if [[ $name = integration-test-org-* ]] + then + echo deleting "$name" + $( auth0 orgs delete "$id") + fi +done diff --git a/test/integration/test-cases.yaml b/test/integration/test-cases.yaml index 8457936a6..255573f57 100644 --- a/test/integration/test-cases.yaml +++ b/test/integration/test-cases.yaml @@ -450,7 +450,7 @@ tests: email: betteruser@example.com # Name is not being displayed, hence using email exit-code: 0 - # Test 'roles create' + # Test 'roles create' roles create and check data: command: auth0 roles create --name integration-test-role-new1 --description testRole --json --no-input exit-code: 0 @@ -664,3 +664,49 @@ tests: api patch tenant settings with wrong json: command: auth0 api patch "tenants/settings" --data "{\"idle_session_lifetime:72}" exit-code: 1 + + create organization and check json output: + command: auth0 orgs create --name integration-test-org-new --display "Integration Test Organization" --json --no-input + exit-code: 0 + stdout: + json: + name: "integration-test-org-new" + display_name: "Integration Test Organization" + + create organization and check table output: + command: auth0 orgs create --name integration-test-org-new2 --display "Integration Test Organization2" --no-input + exit-code: 0 + stdout: + contains: + - NAME integration-test-org-new2 + - DISPLAY NAME Integration Test Organization2 + + create organization to use in other tests: + command: ./test/integration/scripts/get-org-id.sh + exit-code: 0 + + show organization and check json output: + command: auth0 orgs show $(cat ./test/integration/identifiers/org-id) --json + exit-code: 0 + stdout: + json: + name: "integration-test-org-better" + display_name: "Integration Test Better Organization" + + show organization and check table output: + command: auth0 orgs show $(cat ./test/integration/identifiers/org-id) + exit-code: 0 + stdout: + contains: + - NAME integration-test-org-better + - DISPLAY NAME Integration Test Better Organization + + update organization: + command: auth0 orgs update $(cat ./test/integration/identifiers/org-id) -d "Integration Test Updated Organization" -a "#00FFAA" -b "#AA1166" --json --no-input + exit-code: 0 + stdout: + json: + name: "integration-test-org-better" + display_name: "Integration Test Updated Organization" + branding.colors.page_background: "#AA1166" + branding.colors.primary: "#00FFAA"