From daf7e30867a6a5d7135750b71f0c4886d5343b03 Mon Sep 17 00:00:00 2001 From: Rita Zerrizuela Date: Tue, 6 Jul 2021 15:18:54 -0300 Subject: [PATCH] Add organizations CRUD [CLI-189] (#325) * Initial implementation of orgs CRUD * Update color example * Fix cerate and update commands * Add integration test * Fix tests * Add docs * Use makeOrganization * Remove unused mockgen comments --- commander.yaml | 5 +- docs/auth0_orgs.md | 38 +++ docs/auth0_orgs_create.md | 52 ++++ docs/auth0_orgs_delete.md | 43 +++ docs/auth0_orgs_list.md | 44 +++ docs/auth0_orgs_open.md | 42 +++ docs/auth0_orgs_show.md | 43 +++ docs/auth0_orgs_update.md | 50 ++++ docs/index.md | 1 + internal/auth/auth.go | 1 + internal/auth0/action.go | 2 - internal/auth0/anomaly.go | 2 - internal/auth0/auth0.go | 4 +- internal/auth0/branding.go | 1 - internal/auth0/connection.go | 2 - internal/auth0/custom_domain.go | 1 - internal/auth0/email_template.go | 2 - internal/auth0/log_stream.go | 2 - internal/auth0/organization.go | 30 ++ internal/auth0/role.go | 2 - internal/auth0/tenant.go | 1 - internal/auth0/user.go | 2 - internal/cli/actions.go | 6 +- internal/cli/organizations.go | 460 ++++++++++++++++++++++++++++++ internal/cli/root.go | 1 + internal/display/organizations.go | 110 +++++++ 26 files changed, 925 insertions(+), 22 deletions(-) create mode 100644 docs/auth0_orgs.md create mode 100644 docs/auth0_orgs_create.md create mode 100644 docs/auth0_orgs_delete.md create mode 100644 docs/auth0_orgs_list.md create mode 100644 docs/auth0_orgs_open.md create mode 100644 docs/auth0_orgs_show.md create mode 100644 docs/auth0_orgs_update.md create mode 100644 internal/auth0/organization.go create mode 100644 internal/cli/organizations.go create mode 100644 internal/display/organizations.go diff --git a/commander.yaml b/commander.yaml index 23f9fc0ac..a7d44f3df 100644 --- a/commander.yaml +++ b/commander.yaml @@ -26,6 +26,9 @@ tests: auth0 actions list: exit-code: 0 + auth0 orgs list: + exit-code: 0 + auth0 branding domains list: exit-code: 0 @@ -498,4 +501,4 @@ tests: stdout: json: Description: betterDescription - exit-code: 0 \ No newline at end of file + exit-code: 0 diff --git a/docs/auth0_orgs.md b/docs/auth0_orgs.md new file mode 100644 index 000000000..274a7b0f6 --- /dev/null +++ b/docs/auth0_orgs.md @@ -0,0 +1,38 @@ +--- +layout: default +--- +## auth0 orgs + +Manage resources for organizations + +### Synopsis + +Manage resources for organizations. + +### Options + +``` + -h, --help help for orgs +``` + +### 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. +* [auth0 orgs create](auth0_orgs_create.md) - Create a new organization +* [auth0 orgs delete](auth0_orgs_delete.md) - Delete an organization +* [auth0 orgs list](auth0_orgs_list.md) - List your organizations +* [auth0 orgs open](auth0_orgs_open.md) - Open organization settings page in the Auth0 Dashboard +* [auth0 orgs show](auth0_orgs_show.md) - Show an organization +* [auth0 orgs update](auth0_orgs_update.md) - Update an organization + diff --git a/docs/auth0_orgs_create.md b/docs/auth0_orgs_create.md new file mode 100644 index 000000000..e07c2a34b --- /dev/null +++ b/docs/auth0_orgs_create.md @@ -0,0 +1,52 @@ +--- +layout: default +--- +## auth0 orgs create + +Create a new organization + +### Synopsis + +Create a new organization. + +``` +auth0 orgs create [flags] +``` + +### Examples + +``` +auth0 orgs create +auth0 orgs create --name myorganization +auth0 orgs create --n myorganization --display "My Organization" +auth0 orgs create --n myorganization -d "My Organization" -l "https://example.com/logo.png" -a "#635DFF" -b "#2A2E35" +auth0 orgs create --n myorganization -d "My Organization" -m "KEY=value" -m "OTHER_KEY=other_value" +``` + +### Options + +``` + -a, --accent string Accent color used to customize the login pages. + -b, --background string Background color used to customize the login pages. + -d, --display string Friendly name of the organization. + -h, --help help for create + -l, --logo string URL of the logo to be displayed on the login page. + -m, --metadata stringToString Metadata associated with the organization (max 255 chars). Maximum of 10 metadata properties allowed. (default []) + -n, --name string Name of the organization. +``` + +### 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 orgs](auth0_orgs.md) - Manage resources for organizations + diff --git a/docs/auth0_orgs_delete.md b/docs/auth0_orgs_delete.md new file mode 100644 index 000000000..067330af8 --- /dev/null +++ b/docs/auth0_orgs_delete.md @@ -0,0 +1,43 @@ +--- +layout: default +--- +## auth0 orgs delete + +Delete an organization + +### Synopsis + +Delete an organization. + +``` +auth0 orgs delete [flags] +``` + +### Examples + +``` +auth0 orgs delete +auth0 orgs delete +``` + +### Options + +``` + -h, --help help for delete +``` + +### 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 orgs](auth0_orgs.md) - Manage resources for organizations + diff --git a/docs/auth0_orgs_list.md b/docs/auth0_orgs_list.md new file mode 100644 index 000000000..a00945375 --- /dev/null +++ b/docs/auth0_orgs_list.md @@ -0,0 +1,44 @@ +--- +layout: default +--- +## auth0 orgs list + +List your organizations + +### Synopsis + +List your existing organizations. To create one try: +auth0 orgs create + +``` +auth0 orgs list [flags] +``` + +### Examples + +``` +auth0 orgs list +auth0 orgs ls +``` + +### Options + +``` + -h, --help help for list +``` + +### 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 orgs](auth0_orgs.md) - Manage resources for organizations + diff --git a/docs/auth0_orgs_open.md b/docs/auth0_orgs_open.md new file mode 100644 index 000000000..c7f500505 --- /dev/null +++ b/docs/auth0_orgs_open.md @@ -0,0 +1,42 @@ +--- +layout: default +--- +## auth0 orgs open + +Open organization settings page in the Auth0 Dashboard + +### Synopsis + +Open organization settings page in the Auth0 Dashboard. + +``` +auth0 orgs open [flags] +``` + +### Examples + +``` +auth0 orgs open +``` + +### Options + +``` + -h, --help help for open +``` + +### 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 orgs](auth0_orgs.md) - Manage resources for organizations + diff --git a/docs/auth0_orgs_show.md b/docs/auth0_orgs_show.md new file mode 100644 index 000000000..352e403c9 --- /dev/null +++ b/docs/auth0_orgs_show.md @@ -0,0 +1,43 @@ +--- +layout: default +--- +## auth0 orgs show + +Show an organization + +### Synopsis + +Show an organization. + +``` +auth0 orgs show [flags] +``` + +### Examples + +``` +auth0 orgs show +auth0 orgs show +``` + +### Options + +``` + -h, --help help for show +``` + +### 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 orgs](auth0_orgs.md) - Manage resources for organizations + diff --git a/docs/auth0_orgs_update.md b/docs/auth0_orgs_update.md new file mode 100644 index 000000000..d1d6127d7 --- /dev/null +++ b/docs/auth0_orgs_update.md @@ -0,0 +1,50 @@ +--- +layout: default +--- +## auth0 orgs update + +Update an organization + +### Synopsis + +Update an organization. + +``` +auth0 orgs update [flags] +``` + +### Examples + +``` +auth0 orgs update +auth0 orgs update --display "My Organization" +auth0 orgs update -d "My Organization" -l "https://example.com/logo.png" -a "#635DFF" -b "#2A2E35" +auth0 orgs update -d "My Organization" -m "KEY=value" -m "OTHER_KEY=other_value" +``` + +### Options + +``` + -a, --accent string Accent color used to customize the login pages. + -b, --background string Background color used to customize the login pages. + -d, --display string Friendly name of the organization. + -h, --help help for update + -l, --logo string URL of the logo to be displayed on the login page. + -m, --metadata stringToString Metadata associated with the organization (max 255 chars). Maximum of 10 metadata properties allowed. (default []) +``` + +### 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 orgs](auth0_orgs.md) - Manage resources for organizations + diff --git a/docs/index.md b/docs/index.md index 7c8a2619b..924c505aa 100644 --- a/docs/index.md +++ b/docs/index.md @@ -28,6 +28,7 @@ Supercharge your development workflow. * [auth0 login](auth0_login.md) - Authenticate the Auth0 CLI * [auth0 logout](auth0_logout.md) - Log out of a tenant's session * [auth0 logs](auth0_logs.md) - View tenant logs +* [auth0 orgs](auth0_orgs.md) - Manage resources for organizations * [auth0 quickstarts](auth0_quickstarts.md) - Quickstart support for getting bootstrapped * [auth0 roles](auth0_roles.md) - Manage resources for roles * [auth0 rules](auth0_rules.md) - Manage resources for rules diff --git a/internal/auth/auth.go b/internal/auth/auth.go index add4744e4..56808c5c6 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -36,6 +36,7 @@ var requiredScopes = []string{ "read:anomaly_blocks", "delete:anomaly_blocks", "create:log_streams", "delete:log_streams", "read:log_streams", "update:log_streams", "create:actions", "delete:actions", "read:actions", "update:actions", + "create:organizations", "delete:organizations", "read:organizations", "update:organizations", } type Authenticator struct { diff --git a/internal/auth0/action.go b/internal/auth0/action.go index 90a27797a..b816fc062 100644 --- a/internal/auth0/action.go +++ b/internal/auth0/action.go @@ -1,5 +1,3 @@ -//go:generate mockgen -source=action.go -destination=action_mock.go -package=auth0 - package auth0 import "gopkg.in/auth0.v5/management" diff --git a/internal/auth0/anomaly.go b/internal/auth0/anomaly.go index b95d1b867..82140ad0f 100644 --- a/internal/auth0/anomaly.go +++ b/internal/auth0/anomaly.go @@ -1,5 +1,3 @@ -//go:generate mockgen -source=anomaly.go -destination=anomaly_mock.go -package=auth0 - package auth0 import "gopkg.in/auth0.v5/management" diff --git a/internal/auth0/auth0.go b/internal/auth0/auth0.go index 3b56c5571..b02c8eef5 100644 --- a/internal/auth0/auth0.go +++ b/internal/auth0/auth0.go @@ -17,6 +17,7 @@ type API struct { EmailTemplate EmailTemplateAPI Log LogAPI LogStream LogStreamAPI + Organization OrganizationAPI ResourceServer ResourceServerAPI Role RoleAPI Rule RuleAPI @@ -30,16 +31,17 @@ func NewAPI(m *management.Management) *API { Anomaly: m.Anomaly, Branding: m.Branding, Client: m.Client, + Connection: m.Connection, CustomDomain: m.CustomDomain, EmailTemplate: m.EmailTemplate, Log: m.Log, LogStream: m.LogStream, + Organization: m.Organization, ResourceServer: m.ResourceServer, Role: m.Role, Rule: m.Rule, Tenant: m.Tenant, User: m.User, - Connection: m.Connection, } } diff --git a/internal/auth0/branding.go b/internal/auth0/branding.go index 4fd35b1a2..f57125c1d 100644 --- a/internal/auth0/branding.go +++ b/internal/auth0/branding.go @@ -1,4 +1,3 @@ -//go:generate mockgen -source=branding.go -destination=branding_mock.go -package=auth0 package auth0 import "gopkg.in/auth0.v5/management" diff --git a/internal/auth0/connection.go b/internal/auth0/connection.go index 8abfb91c0..9a48fc3e3 100644 --- a/internal/auth0/connection.go +++ b/internal/auth0/connection.go @@ -1,5 +1,3 @@ -//go:generate mockgen -source=connection.go -destination=connection_mock.go -package=auth0 - package auth0 import "gopkg.in/auth0.v5/management" diff --git a/internal/auth0/custom_domain.go b/internal/auth0/custom_domain.go index b7143d7cb..f65179016 100644 --- a/internal/auth0/custom_domain.go +++ b/internal/auth0/custom_domain.go @@ -1,4 +1,3 @@ -//go:generate mockgen -source=custom_domain.go -destination=custom_domain_mock.go -package=auth0 package auth0 import "gopkg.in/auth0.v5/management" diff --git a/internal/auth0/email_template.go b/internal/auth0/email_template.go index c406921ef..85ca830a6 100644 --- a/internal/auth0/email_template.go +++ b/internal/auth0/email_template.go @@ -1,5 +1,3 @@ -//go:generate mockgen -source=email_template.go -destination=email_template_mock.go -package=auth0 - package auth0 import "gopkg.in/auth0.v5/management" diff --git a/internal/auth0/log_stream.go b/internal/auth0/log_stream.go index bca2b246e..79363d090 100644 --- a/internal/auth0/log_stream.go +++ b/internal/auth0/log_stream.go @@ -1,5 +1,3 @@ -//go:generate mockgen -source=log_stream.go -destination=log_stream_mock.go -package=auth0 - package auth0 import "gopkg.in/auth0.v5/management" diff --git a/internal/auth0/organization.go b/internal/auth0/organization.go new file mode 100644 index 000000000..ef8a67bca --- /dev/null +++ b/internal/auth0/organization.go @@ -0,0 +1,30 @@ +package auth0 + +import "gopkg.in/auth0.v5/management" + +type OrganizationAPI interface { + // Create an Organization + // + // See: https://auth0.com/docs/api/management/v2/#!/Organizations/post_organizations + Create(o *management.Organization, opts ...management.RequestOption) error + + // Get a specific organization + // + // See: https://auth0.com/docs/api/management/v2/#!/Organizations/get_organizations_by_id + Read(id string, opts ...management.RequestOption) (*management.Organization, error) + + // Modify an organization + // + // See: https://auth0.com/docs/api/management/v2/#!/Organizations/patch_organizations_by_id + Update(o *management.Organization, opts ...management.RequestOption) error + + // Delete a specific organization + // + // See: https://auth0.com/docs/api/management/v2/#!/Organizations/delete_organizations_by_id + Delete(id string, opts ...management.RequestOption) error + + // List available organizations + // + // See: https://auth0.com/docs/api/management/v2/#!/Organizations/get_organizations + List(opts ...management.RequestOption) (c *management.OrganizationList, err error) +} diff --git a/internal/auth0/role.go b/internal/auth0/role.go index 5f6f4b1a8..9ad436352 100644 --- a/internal/auth0/role.go +++ b/internal/auth0/role.go @@ -1,5 +1,3 @@ -//go:generate mockgen -source=role.go -destination=role_mock.go -package=auth0 - package auth0 import "gopkg.in/auth0.v5/management" diff --git a/internal/auth0/tenant.go b/internal/auth0/tenant.go index 4da949e3f..2464e829c 100644 --- a/internal/auth0/tenant.go +++ b/internal/auth0/tenant.go @@ -1,4 +1,3 @@ -//go:generate mockgen -source=tenant.go -destination=tenant_mock.go -package=auth0 package auth0 import "gopkg.in/auth0.v5/management" diff --git a/internal/auth0/user.go b/internal/auth0/user.go index 5e7123a3a..9ea4fc237 100644 --- a/internal/auth0/user.go +++ b/internal/auth0/user.go @@ -1,5 +1,3 @@ -//go:generate mockgen -source=user.go -destination=user_mock.go -package=auth0 - package auth0 import "gopkg.in/auth0.v5/management" diff --git a/internal/cli/actions.go b/internal/cli/actions.go index a53437d5d..6f923e962 100644 --- a/internal/cli/actions.go +++ b/internal/cli/actions.go @@ -74,11 +74,11 @@ func actionsCmd(cli *cli) *cobra.Command { cmd.SetUsageTemplate(resourceUsageTemplate()) cmd.AddCommand(listActionsCmd(cli)) - cmd.AddCommand(showActionCmd(cli)) cmd.AddCommand(createActionCmd(cli)) + cmd.AddCommand(showActionCmd(cli)) + cmd.AddCommand(updateActionCmd(cli)) cmd.AddCommand(deleteActionCmd(cli)) cmd.AddCommand(openActionCmd(cli)) - cmd.AddCommand(updateActionCmd(cli)) return cmd } @@ -172,7 +172,7 @@ auth0 actions create --n myaction --trigger post-login auth0 actions create --n myaction -t post-login -d "lodash=4.0.0" -d "uuid=8.0.0" auth0 actions create --n myaction -t post-login -d "lodash=4.0.0" -s "API_KEY=value" -s "SECRET=value`, RunE: func(cmd *cobra.Command, args []string) error { - if err := apiName.Ask(cmd, &inputs.Name, nil); err != nil { + if err := actionName.Ask(cmd, &inputs.Name, nil); err != nil { return err } diff --git a/internal/cli/organizations.go b/internal/cli/organizations.go new file mode 100644 index 000000000..5387ff4d4 --- /dev/null +++ b/internal/cli/organizations.go @@ -0,0 +1,460 @@ +package cli + +import ( + "errors" + "fmt" + "net/url" + + "github.com/auth0/auth0-cli/internal/ansi" + "github.com/auth0/auth0-cli/internal/prompt" + "github.com/spf13/cobra" + "gopkg.in/auth0.v5/management" +) + +const ( + apiOrganizationColorPrimary = "primary" + apiOrganizationColorPageBackground = "page_background" +) + +var ( + organizationID = Argument{ + Name: "Id", + Help: "Id of the organization.", + } + + organizationName = Flag{ + Name: "Name", + LongForm: "name", + ShortForm: "n", + Help: "Name of the organization.", + IsRequired: true, + } + + organizationDisplay = Flag{ + Name: "Display Name", + LongForm: "display", + ShortForm: "d", + Help: "Friendly name of the organization.", + AlwaysPrompt: true, + } + + organizationLogo = Flag{ + Name: "Logo URL", + LongForm: "logo", + ShortForm: "l", + Help: "URL of the logo to be displayed on the login page.", + } + + organizationAccent = Flag{ + Name: "Accent Color", + LongForm: "accent", + ShortForm: "a", + Help: "Accent color used to customize the login pages.", + } + + organizationBackground = Flag{ + Name: "Background Color", + LongForm: "background", + ShortForm: "b", + Help: "Background color used to customize the login pages.", + } + + organizationMetadata = Flag{ + Name: "Metadata", + LongForm: "metadata", + ShortForm: "m", + Help: "Metadata associated with the organization (max 255 chars). Maximum of 10 metadata properties allowed.", + } +) + +func organizationsCmd(cli *cli) *cobra.Command { + cmd := &cobra.Command{ + Use: "orgs", + Aliases: []string{"organizations"}, + Short: "Manage resources for organizations", + Long: "Manage resources for organizations.", + } + + cmd.SetUsageTemplate(resourceUsageTemplate()) + cmd.AddCommand(listOrganizationsCmd(cli)) + cmd.AddCommand(createOrganizationCmd(cli)) + cmd.AddCommand(showOrganizationCmd(cli)) + cmd.AddCommand(updateOrganizationCmd(cli)) + cmd.AddCommand(deleteOrganizationCmd(cli)) + cmd.AddCommand(openOrganizationCmd(cli)) + + return cmd +} + +func listOrganizationsCmd(cli *cli) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Aliases: []string{"ls"}, + Args: cobra.NoArgs, + Short: "List your organizations", + Long: `List your existing organizations. To create one try: +auth0 orgs create`, + Example: `auth0 orgs list +auth0 orgs ls`, + RunE: func(cmd *cobra.Command, args []string) error { + var list *management.OrganizationList + + if err := ansi.Waiting(func() error { + var err error + list, err = cli.api.Organization.List() + return err + }); err != nil { + return fmt.Errorf("An unexpected error occurred: %w", err) + } + + cli.renderer.OrganizationList(list.Organizations) + return nil + }, + } + + return cmd +} + +func showOrganizationCmd(cli *cli) *cobra.Command { + var inputs struct { + ID string + } + + cmd := &cobra.Command{ + Use: "show", + Args: cobra.MaximumNArgs(1), + Short: "Show an organization", + Long: "Show an organization.", + Example: `auth0 orgs show +auth0 orgs show `, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + err := organizationID.Pick(cmd, &inputs.ID, cli.organizationPickerOptions) + if err != nil { + return err + } + } else { + inputs.ID = args[0] + } + + var organization *management.Organization + + if err := ansi.Waiting(func() error { + var err error + organization, err = cli.api.Organization.Read(url.PathEscape(inputs.ID)) + return err + }); err != nil { + return fmt.Errorf("Unable to get an organization with Id '%s': %w", inputs.ID, err) + } + + cli.renderer.OrganizationShow(organization) + return nil + }, + } + + return cmd +} + +func createOrganizationCmd(cli *cli) *cobra.Command { + var inputs struct { + Name string + DisplayName string + LogoURL string + AccentColor string + BackgroundColor string + Metadata map[string]string + } + + cmd := &cobra.Command{ + Use: "create", + Args: cobra.NoArgs, + Short: "Create a new organization", + Long: "Create a new organization.", + Example: `auth0 orgs create +auth0 orgs create --name myorganization +auth0 orgs create --n myorganization --display "My Organization" +auth0 orgs create --n myorganization -d "My Organization" -l "https://example.com/logo.png" -a "#635DFF" -b "#2A2E35" +auth0 orgs create --n myorganization -d "My Organization" -m "KEY=value" -m "OTHER_KEY=other_value"`, + RunE: func(cmd *cobra.Command, args []string) error { + if err := organizationName.Ask(cmd, &inputs.Name, nil); err != nil { + return err + } + + if err := organizationDisplay.Ask(cmd, &inputs.DisplayName, nil); err != nil { + return err + } + + o := &management.Organization{ + Name: &inputs.Name, + DisplayName: &inputs.DisplayName, + Metadata: apiOrganizationMetadataFor(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 { + o.Branding.Colors = map[string]string{} + + if isAccentColorSet { + o.Branding.Colors[apiOrganizationColorPrimary] = inputs.AccentColor + } + + if isBackgroundColorSet { + o.Branding.Colors[apiOrganizationColorPageBackground] = inputs.BackgroundColor + } + } + } + + if err := ansi.Waiting(func() error { + return cli.api.Organization.Create(o) + }); err != nil { + return fmt.Errorf("An unexpected error occurred while attempting to create an organization with name '%s': %w", inputs.Name, err) + } + + cli.renderer.OrganizationCreate(o) + return nil + }, + } + + organizationName.RegisterString(cmd, &inputs.Name, "") + organizationDisplay.RegisterString(cmd, &inputs.DisplayName, "") + organizationLogo.RegisterString(cmd, &inputs.LogoURL, "") + organizationAccent.RegisterString(cmd, &inputs.AccentColor, "") + organizationBackground.RegisterString(cmd, &inputs.BackgroundColor, "") + organizationMetadata.RegisterStringMap(cmd, &inputs.Metadata, nil) + + return cmd +} + +func updateOrganizationCmd(cli *cli) *cobra.Command { + var inputs struct { + ID string + DisplayName string + LogoURL string + AccentColor string + BackgroundColor string + Metadata map[string]string + } + + cmd := &cobra.Command{ + Use: "update", + Args: cobra.MaximumNArgs(1), + Short: "Update an organization", + Long: "Update an organization.", + Example: `auth0 orgs update +auth0 orgs update --display "My Organization" +auth0 orgs update -d "My Organization" -l "https://example.com/logo.png" -a "#635DFF" -b "#2A2E35" +auth0 orgs update -d "My Organization" -m "KEY=value" -m "OTHER_KEY=other_value"`, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) > 0 { + inputs.ID = args[0] + } else { + err := organizationID.Pick(cmd, &inputs.ID, cli.organizationPickerOptions) + if err != nil { + return err + } + } + + var current *management.Organization + err := ansi.Waiting(func() error { + var err error + current, 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) + } + + if err := organizationDisplay.AskU(cmd, &inputs.DisplayName, current.DisplayName); err != nil { + return err + } + + if inputs.DisplayName == "" { + inputs.DisplayName = current.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, + 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 + needToAddColors := isAccentColorSet || isBackgroundColorSet || currentHasColors + + if isLogoURLSet || needToAddColors { + o.Branding = &management.OrganizationBranding{} + + if isLogoURLSet { + o.Branding.LogoUrl = &inputs.LogoURL + } else if currentHasBranding { + o.Branding.LogoUrl = current.Branding.LogoUrl + } + + if needToAddColors { + o.Branding.Colors = map[string]string{} + + if isAccentColorSet { + o.Branding.Colors[apiOrganizationColorPrimary] = inputs.AccentColor + } else if currentHasColors && len(current.Branding.Colors[apiOrganizationColorPrimary]) > 0 { + o.Branding.Colors[apiOrganizationColorPrimary] = current.Branding.Colors[apiOrganizationColorPrimary] + } + + if isBackgroundColorSet { + o.Branding.Colors[apiOrganizationColorPageBackground] = inputs.BackgroundColor + } else if currentHasColors && len(current.Branding.Colors[apiOrganizationColorPageBackground]) > 0 { + o.Branding.Colors[apiOrganizationColorPageBackground] = current.Branding.Colors[apiOrganizationColorPageBackground] + } + } + } + + if len(inputs.Metadata) == 0 { + o.Metadata = current.Metadata + } else { + o.Metadata = apiOrganizationMetadataFor(inputs.Metadata) + } + + if err = ansi.Waiting(func() error { + return cli.api.Organization.Update(o) + }); err != nil { + return err + } + + cli.renderer.OrganizationUpdate(o) + return nil + }, + } + + organizationDisplay.RegisterStringU(cmd, &inputs.DisplayName, "") + organizationLogo.RegisterStringU(cmd, &inputs.LogoURL, "") + organizationAccent.RegisterStringU(cmd, &inputs.AccentColor, "") + organizationBackground.RegisterStringU(cmd, &inputs.BackgroundColor, "") + organizationMetadata.RegisterStringMapU(cmd, &inputs.Metadata, nil) + + return cmd +} + +func deleteOrganizationCmd(cli *cli) *cobra.Command { + var inputs struct { + ID string + } + + cmd := &cobra.Command{ + Use: "delete", + Args: cobra.MaximumNArgs(1), + Short: "Delete an organization", + Long: "Delete an organization.", + Example: `auth0 orgs delete +auth0 orgs delete `, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + err := organizationID.Pick(cmd, &inputs.ID, cli.organizationPickerOptions) + if err != nil { + return err + } + } else { + inputs.ID = args[0] + } + + if !cli.force && canPrompt(cmd) { + if confirmed := prompt.Confirm("Are you sure you want to proceed?"); !confirmed { + return nil + } + } + + return ansi.Spinner("Deleting organization", func() error { + _, err := cli.api.Organization.Read(url.PathEscape(inputs.ID)) + + if err != nil { + return fmt.Errorf("Unable to delete organization: %w", err) + } + + return cli.api.Organization.Delete(url.PathEscape(inputs.ID)) + }) + }, + } + + return cmd +} + +func openOrganizationCmd(cli *cli) *cobra.Command { + var inputs struct { + ID string + } + + cmd := &cobra.Command{ + Use: "open", + Args: cobra.MaximumNArgs(1), + Short: "Open organization settings page in the Auth0 Dashboard", + Long: "Open organization settings page in the Auth0 Dashboard.", + Example: "auth0 orgs open ", + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + err := organizationID.Pick(cmd, &inputs.ID, cli.organizationPickerOptions) + if err != nil { + return err + } + } else { + inputs.ID = args[0] + } + + openManageURL(cli, cli.config.DefaultTenant, formatOrganizationDetailsPath(url.PathEscape(inputs.ID))) + return nil + }, + } + + return cmd +} + +func (c *cli) organizationPickerOptions() (pickerOptions, error) { + list, err := c.api.Organization.List() + if err != nil { + return nil, err + } + + var opts pickerOptions + for _, r := range list.Organizations { + label := fmt.Sprintf("%s %s", r.GetName(), ansi.Faint("("+r.GetID()+")")) + + opts = append(opts, pickerOption{value: r.GetID(), label: label}) + } + + if len(opts) == 0 { + return nil, errors.New("There are currently no organizations.") + } + + return opts, nil +} + +func formatOrganizationDetailsPath(id string) string { + if len(id) == 0 { + return "" + } + return fmt.Sprintf("organizations/%s/overview", id) +} + +func apiOrganizationMetadataFor(metadata map[string]string) map[string]interface{} { + res := make(map[string]interface{}) + for k, v := range metadata { + key := k + value := v + res[key] = value + } + return res +} diff --git a/internal/cli/root.go b/internal/cli/root.go index b13bf386e..bb0ed5065 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -190,6 +190,7 @@ func addSubcommands(rootCmd *cobra.Command, cli *cli) { rootCmd.AddCommand(actionsCmd(cli)) rootCmd.AddCommand(apisCmd(cli)) rootCmd.AddCommand(rolesCmd(cli)) + rootCmd.AddCommand(organizationsCmd(cli)) rootCmd.AddCommand(brandingCmd(cli)) rootCmd.AddCommand(ipsCmd(cli)) rootCmd.AddCommand(quickstartsCmd(cli)) diff --git a/internal/display/organizations.go b/internal/display/organizations.go new file mode 100644 index 000000000..9af37648a --- /dev/null +++ b/internal/display/organizations.go @@ -0,0 +1,110 @@ +package display + +import ( + "encoding/json" + "io" + + "github.com/auth0/auth0-cli/internal/ansi" + "gopkg.in/auth0.v5/management" +) + +type organizationView struct { + ID string + Name string + DisplayName string + LogoURL string + AccentColor string + BackgroundColor string + Metadata string + raw interface{} +} + +func (v *organizationView) AsTableHeader() []string { + return []string{"ID", "Name", "Display Name"} +} + +func (v *organizationView) AsTableRow() []string { + return []string{ansi.Faint(v.ID), v.Name, v.DisplayName} +} + +func (v *organizationView) KeyValues() [][]string { + return [][]string{ + {"ID", ansi.Faint(v.ID)}, + {"NAME", v.Name}, + {"DISPLAY NAME", v.DisplayName}, + {"LOGO URL", v.LogoURL}, + {"ACCENT COLOR", v.AccentColor}, + {"BACKGROUND COLOR", v.BackgroundColor}, + {"METADATA", v.Metadata}, + } +} + +func (v *organizationView) Object() interface{} { + return v.raw +} + +func (r *Renderer) OrganizationList(organizations []*management.Organization) { + resource := "organizations" + + r.Heading(resource) + + if len(organizations) == 0 { + r.EmptyState(resource) + r.Infof("Use 'auth0 orgs create' to add one") + return + } + + var res []View + for _, o := range organizations { + res = append(res, makeOrganizationView(o, r.MessageWriter)) + } + + r.Results(res) +} + +func (r *Renderer) OrganizationShow(organization *management.Organization) { + r.Heading("organization") + r.Result(makeOrganizationView(organization, r.MessageWriter)) +} + +func (r *Renderer) OrganizationCreate(organization *management.Organization) { + r.Heading("organization created") + r.Result(makeOrganizationView(organization, r.MessageWriter)) +} + +func (r *Renderer) OrganizationUpdate(organization *management.Organization) { + r.Heading("organization updated") + r.Result(makeOrganizationView(organization, r.MessageWriter)) +} + +func makeOrganizationView(organization *management.Organization, w io.Writer) *organizationView { + accentColor := "" + backgroundColor := "" + + if organization.Branding != nil && organization.Branding.Colors != nil { + if len(organization.Branding.Colors["primary"]) > 0 { + accentColor = organization.Branding.Colors["primary"] + } + + if len(organization.Branding.Colors["page_background"]) > 0 { + backgroundColor = organization.Branding.Colors["page_background"] + } + } + + metadata := "" + buf, err := json.MarshalIndent(organization.Metadata, "", " ") + if err == nil { + metadata = string(buf) + } + + return &organizationView{ + ID: organization.GetID(), + Name: organization.GetName(), + DisplayName: organization.GetDisplayName(), + LogoURL: organization.GetBranding().GetLogoUrl(), + AccentColor: accentColor, + BackgroundColor: backgroundColor, + Metadata: ansi.ColorizeJSON(metadata, false, w), + raw: organization, + } +}