From cc71faf64b2e4ec96d9aed839cb33e37c3fed9d5 Mon Sep 17 00:00:00 2001 From: Cyril David Date: Fri, 14 May 2021 15:21:04 -0700 Subject: [PATCH 1/5] Add roles to auth0 layer --- internal/auth0/auth0.go | 2 ++ internal/auth0/role.go | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 internal/auth0/role.go diff --git a/internal/auth0/auth0.go b/internal/auth0/auth0.go index 33f8d6dff..cae1c53a9 100644 --- a/internal/auth0/auth0.go +++ b/internal/auth0/auth0.go @@ -13,6 +13,7 @@ type API struct { CustomDomain CustomDomainAPI Log LogAPI ResourceServer ResourceServerAPI + Role RoleAPI Rule RuleAPI Tenant TenantAPI User UserAPI @@ -26,6 +27,7 @@ func NewAPI(m *management.Management) *API { CustomDomain: m.CustomDomain, Log: m.Log, ResourceServer: m.ResourceServer, + Role: m.Role, Rule: m.Rule, Tenant: m.Tenant, User: m.User, diff --git a/internal/auth0/role.go b/internal/auth0/role.go new file mode 100644 index 000000000..5f6f4b1a8 --- /dev/null +++ b/internal/auth0/role.go @@ -0,0 +1,22 @@ +//go:generate mockgen -source=role.go -destination=role_mock.go -package=auth0 + +package auth0 + +import "gopkg.in/auth0.v5/management" + +type RoleAPI interface { + // Create a new role. + Create(r *management.Role, opts ...management.RequestOption) (err error) + + // Retrieve a role. + Read(id string, opts ...management.RequestOption) (r *management.Role, err error) + + // List all roles that can be assigned to users or groups. + List(opts ...management.RequestOption) (r *management.RoleList, err error) + + // Update a role. + Update(id string, r *management.Role, opts ...management.RequestOption) (err error) + + // Delete a role. + Delete(id string, opts ...management.RequestOption) (err error) +} From 5b0fe535d7630ff236d9ccdeba078399ff78e01e Mon Sep 17 00:00:00 2001 From: Cyril David Date: Fri, 14 May 2021 15:24:31 -0700 Subject: [PATCH 2/5] Update scopes to add roles; update tests --- internal/auth/auth.go | 3 ++- internal/auth/auth_test.go | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 3052bc386..b99364dd3 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -29,10 +29,11 @@ var requiredScopes = []string{ "offline_access", // <-- to get a refresh token. "create:clients", "delete:clients", "read:clients", "update:clients", "create:resource_servers", "delete:resource_servers", "read:resource_servers", "update:resource_servers", + "create:roles", "delete:roles", "read:roles", "update:roles", "create:rules", "delete:rules", "read:rules", "update:rules", - "read:client_keys", "read:logs", "read:connections", "update:connections", "create:users", "delete:users", "read:users", "update:users", "read:branding", "update:branding", + "read:connections", "update:connections", "read:client_keys", "read:logs", "read:tenant_settings", "read:custom_domains", } diff --git a/internal/auth/auth_test.go b/internal/auth/auth_test.go index 3000d8b9d..daef2ccfc 100644 --- a/internal/auth/auth_test.go +++ b/internal/auth/auth_test.go @@ -7,7 +7,9 @@ func TestRequiredScopes(t *testing.T) { crudResources := []string{ "clients", "resource_servers", + "roles", "rules", + "users", } crudPrefixes := []string{"create:", "delete:", "read:", "update:"} @@ -24,9 +26,9 @@ func TestRequiredScopes(t *testing.T) { t.Run("verify special scopes", func(t *testing.T) { list := []string{ - "read:client_keys", "read:logs", - "read:users", "update:users", "read:branding", "update:branding", + "read:connections", "update:connections", + "read:custom_domains", "read:client_keys", "read:logs", "read:tenant_settings", } From 10fde61e39dd22e199de662cd69b43f8efca6ab9 Mon Sep 17 00:00:00 2001 From: Cyril David Date: Fri, 14 May 2021 15:24:39 -0700 Subject: [PATCH 3/5] Add list rolesCmd --- internal/cli/roles.go | 52 +++++++++++++++++++++++++++ internal/cli/root.go | 1 + internal/display/roles.go | 75 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 128 insertions(+) create mode 100644 internal/cli/roles.go create mode 100644 internal/display/roles.go diff --git a/internal/cli/roles.go b/internal/cli/roles.go new file mode 100644 index 000000000..ea3472d83 --- /dev/null +++ b/internal/cli/roles.go @@ -0,0 +1,52 @@ +package cli + +import ( + "fmt" + + "github.com/auth0/auth0-cli/internal/ansi" + "github.com/spf13/cobra" + "gopkg.in/auth0.v5/management" +) + +func rolesCmd(cli *cli) *cobra.Command { + cmd := &cobra.Command{ + Use: "roles", + Short: "Manage resources for roles", + Long: "Manage resources for roles.", + Aliases: []string{"role"}, + } + + cmd.SetUsageTemplate(resourceUsageTemplate()) + cmd.AddCommand(listRolesCmd(cli)) + + return cmd +} + +func listRolesCmd(cli *cli) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Aliases: []string{"ls"}, + Args: cobra.NoArgs, + Short: "List your roles", + Long: `List your existing roles. To create one try: +auth0 roles create`, + Example: `auth0 roles list +auth0 roles ls`, + RunE: func(cmd *cobra.Command, args []string) error { + var list *management.RoleList + + if err := ansi.Waiting(func() error { + var err error + list, err = cli.api.Role.List() + return err + }); err != nil { + return fmt.Errorf("An unexpected error occurred: %w", err) + } + + cli.renderer.RoleList(list.Roles) + return nil + }, + } + + return cmd +} diff --git a/internal/cli/root.go b/internal/cli/root.go index d7217ec3e..26a166ed0 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -100,6 +100,7 @@ func Execute() { rootCmd.AddCommand(logsCmd(cli)) rootCmd.AddCommand(logoutCmd(cli)) rootCmd.AddCommand(brandingCmd(cli)) + rootCmd.AddCommand(rolesCmd(cli)) // keep completion at the bottom: rootCmd.AddCommand(completionCmd(cli)) diff --git a/internal/display/roles.go b/internal/display/roles.go new file mode 100644 index 000000000..0944be3cc --- /dev/null +++ b/internal/display/roles.go @@ -0,0 +1,75 @@ +package display + +import ( + "github.com/auth0/auth0-cli/internal/ansi" + "github.com/auth0/auth0-cli/internal/auth0" + "gopkg.in/auth0.v5/management" +) + +type roleView struct { + Name string + ID string + Description string +} + +func (v *roleView) AsTableHeader() []string { + return []string{"Name", "Role ID", "Description"} +} + +func (v *roleView) AsTableRow() []string { + return []string{ + v.Name, + ansi.Faint(v.ID), + v.Description, + } +} + +func (r *Renderer) RoleList(roles []*management.Role) { + resource := "roles" + + r.Heading(resource) + + if len(roles) == 0 { + r.EmptyState(resource) + r.Infof("Use 'auth0 roles create' to add one") + return + } + + var res []View + for _, role := range roles { + res = append(res, &roleView{ + Name: role.GetName(), + ID: role.GetID(), + Description: role.GetDescription(), + }) + } + + r.Results(res) +} + +func (r *Renderer) RoleGet(role *management.Role) { + r.Heading(ansi.Bold(r.Tenant), "role\n") + r.Results([]View{&roleView{ + Name: auth0.StringValue(role.Name), + ID: auth0.StringValue(role.ID), + Description: auth0.StringValue(role.Description), + }}) +} + +func (r *Renderer) RoleUpdate(role *management.Role) { + r.Heading(ansi.Bold(r.Tenant), "role\n") + r.Results([]View{&roleView{ + Name: auth0.StringValue(role.Name), + ID: auth0.StringValue(role.ID), + Description: auth0.StringValue(role.Description), + }}) +} + +func (r *Renderer) RoleCreate(role *management.Role) { + r.Heading(ansi.Bold(r.Tenant), "role\n") + r.Results([]View{&roleView{ + Name: auth0.StringValue(role.Name), + ID: auth0.StringValue(role.ID), + Description: auth0.StringValue(role.Description), + }}) +} From 20dad88d8175651ff73341b7fc3c25816c814837 Mon Sep 17 00:00:00 2001 From: Cyril David Date: Fri, 14 May 2021 16:00:36 -0700 Subject: [PATCH 4/5] Complete rest of CRUD commands --- internal/cli/roles.go | 251 ++++++++++++++++++++++++++++++++++++++ internal/display/roles.go | 45 +++---- 2 files changed, 275 insertions(+), 21 deletions(-) diff --git a/internal/cli/roles.go b/internal/cli/roles.go index ea3472d83..f120c2473 100644 --- a/internal/cli/roles.go +++ b/internal/cli/roles.go @@ -1,13 +1,39 @@ package cli import ( + "errors" "fmt" "github.com/auth0/auth0-cli/internal/ansi" + "github.com/auth0/auth0-cli/internal/prompt" "github.com/spf13/cobra" "gopkg.in/auth0.v5/management" ) +// errNoRoles signifies no roles exist in a tenant +var errNoRoles = errors.New("there are currently no roles") + +var ( + roleID = Argument{ + Name: "Role ID", + Help: "Id of the role.", + } + roleName = Flag{ + Name: "Name", + LongForm: "name", + ShortForm: "n", + Help: "Name of the role.", + IsRequired: true, + } + roleDescription = Flag{ + Name: "Description", + LongForm: "description", + ShortForm: "d", + Help: "Description of the role.", + // IsRequired: true, + } +) + func rolesCmd(cli *cli) *cobra.Command { cmd := &cobra.Command{ Use: "roles", @@ -18,6 +44,10 @@ func rolesCmd(cli *cli) *cobra.Command { cmd.SetUsageTemplate(resourceUsageTemplate()) cmd.AddCommand(listRolesCmd(cli)) + cmd.AddCommand(showRoleCmd(cli)) + cmd.AddCommand(createRoleCmd(cli)) + cmd.AddCommand(updateRoleCmd(cli)) + cmd.AddCommand(deleteRoleCmd(cli)) return cmd } @@ -50,3 +80,224 @@ auth0 roles ls`, return cmd } + +func showRoleCmd(cli *cli) *cobra.Command { + var inputs struct { + ID string + } + + cmd := &cobra.Command{ + Use: "show", + Args: cobra.MaximumNArgs(1), + Short: "Show a role", + Long: "Show a role.", + Example: `auth0 roles show +auth0 roles show `, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + err := roleID.Pick(cmd, &inputs.ID, cli.rolePickerOptions) + if err != nil { + return err + } + } else { + inputs.ID = args[0] + } + + r := &management.Role{ID: &inputs.ID} + + if err := ansi.Waiting(func() error { + var err error + r, err = cli.api.Role.Read(inputs.ID) + return err + }); err != nil { + return fmt.Errorf("Unable to load role. The Id %v specified doesn't exist", inputs.ID) + } + + cli.renderer.RoleShow(r) + return nil + }, + } + + return cmd +} + +func createRoleCmd(cli *cli) *cobra.Command { + var inputs struct { + Name string + Description string + } + + cmd := &cobra.Command{ + Use: "create", + Args: cobra.NoArgs, + Short: "Create a new role", + Long: "Create a new role.", + Example: `auth0 roles create +auth0 roles create --name myrole +auth0 roles create -n myrole --description "awesome role"`, + RunE: func(cmd *cobra.Command, args []string) error { + // Prompt for role name + if err := roleName.Ask(cmd, &inputs.Name, nil); err != nil { + return err + } + + // Prompt for role description + if err := roleDescription.Ask(cmd, &inputs.Description, nil); err != nil { + return err + } + + // Load values into a fresh role instance + r := &management.Role{ + Name: &inputs.Name, + Description: &inputs.Description, + } + + // Create role + if err := ansi.Waiting(func() error { + return cli.api.Role.Create(r) + }); err != nil { + return fmt.Errorf("Unable to create role: %v", err) + } + + // Render role creation specific view + cli.renderer.RoleCreate(r) + return nil + }, + } + + roleName.RegisterString(cmd, &inputs.Name, "") + roleDescription.RegisterString(cmd, &inputs.Description, "") + + return cmd +} + +func updateRoleCmd(cli *cli) *cobra.Command { + var inputs struct { + ID string + Name string + Description string + } + + cmd := &cobra.Command{ + Use: "update", + Args: cobra.MaximumNArgs(1), + Short: "Update a role", + Long: "Update a role.", + Example: `auth0 roles update +auth0 roles update --name myrole +auth0 roles update -n myrole --description "awesome role"`, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + err := roleID.Pick(cmd, &inputs.ID, cli.rolePickerOptions) + if err != nil { + return err + } + } else { + inputs.ID = args[0] + } + + // Prompt for role name + if err := roleName.AskU(cmd, &inputs.Name, nil); err != nil { + return err + } + + // Prompt for role description + if err := roleDescription.AskU(cmd, &inputs.Description, nil); err != nil { + return err + } + + // Start with an empty role object. We'll conditionally + // hydrate it based on the provided parameters since + // we'll do PATCH semantics. + r := &management.Role{} + + if inputs.Name != "" { + r.Name = &inputs.Name + } + + if inputs.Description != "" { + r.Description = &inputs.Description + } + + // Update role + if err := ansi.Waiting(func() error { + return cli.api.Role.Update(inputs.ID, r) + }); err != nil { + return fmt.Errorf("Unable to update role: %v", err) + } + + // Render role creation specific view + cli.renderer.RoleUpdate(r) + return nil + }, + } + + roleName.RegisterStringU(cmd, &inputs.Name, "") + roleDescription.RegisterStringU(cmd, &inputs.Description, "") + + return cmd +} + +func deleteRoleCmd(cli *cli) *cobra.Command { + var inputs struct { + ID string + } + + cmd := &cobra.Command{ + Use: "delete", + Args: cobra.MaximumNArgs(1), + Short: "Delete an role", + Long: "Delete an role.", + Example: `auth0 roles delete +auth0 roles delete `, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + err := roleID.Pick(cmd, &inputs.ID, cli.rolePickerOptions) + 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 Role", func() error { + _, err := cli.api.Role.Read(inputs.ID) + + if err != nil { + return fmt.Errorf("Unable to delete role. The specified Id: %v doesn't exist", inputs.ID) + } + + return cli.api.Role.Delete(inputs.ID) + }) + }, + } + + return cmd +} + +func (c *cli) rolePickerOptions() (pickerOptions, error) { + list, err := c.api.Role.List() + if err != nil { + return nil, err + } + + var opts pickerOptions + + for _, c := range list.Roles { + value := c.GetID() + label := fmt.Sprintf("%s %s", c.GetName(), ansi.Faint("("+value+")")) + opts = append(opts, pickerOption{value: value, label: label}) + } + + if len(opts) == 0 { + return nil, errNoRoles + } + + return opts, nil +} diff --git a/internal/display/roles.go b/internal/display/roles.go index 0944be3cc..3dc71d4f1 100644 --- a/internal/display/roles.go +++ b/internal/display/roles.go @@ -2,7 +2,6 @@ package display import ( "github.com/auth0/auth0-cli/internal/ansi" - "github.com/auth0/auth0-cli/internal/auth0" "gopkg.in/auth0.v5/management" ) @@ -24,6 +23,14 @@ 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}, + } +} + func (r *Renderer) RoleList(roles []*management.Role) { resource := "roles" @@ -47,29 +54,25 @@ func (r *Renderer) RoleList(roles []*management.Role) { r.Results(res) } -func (r *Renderer) RoleGet(role *management.Role) { - r.Heading(ansi.Bold(r.Tenant), "role\n") - r.Results([]View{&roleView{ - Name: auth0.StringValue(role.Name), - ID: auth0.StringValue(role.ID), - Description: auth0.StringValue(role.Description), - }}) +func (r *Renderer) RoleShow(role *management.Role) { + r.Heading("role") + r.roleResult(role) +} + +func (r *Renderer) RoleCreate(role *management.Role) { + r.Heading("role created") + r.roleResult(role) } func (r *Renderer) RoleUpdate(role *management.Role) { - r.Heading(ansi.Bold(r.Tenant), "role\n") - r.Results([]View{&roleView{ - Name: auth0.StringValue(role.Name), - ID: auth0.StringValue(role.ID), - Description: auth0.StringValue(role.Description), - }}) + r.Heading("role updated") + r.roleResult(role) } -func (r *Renderer) RoleCreate(role *management.Role) { - r.Heading(ansi.Bold(r.Tenant), "role\n") - r.Results([]View{&roleView{ - Name: auth0.StringValue(role.Name), - ID: auth0.StringValue(role.ID), - Description: auth0.StringValue(role.Description), - }}) +func (r *Renderer) roleResult(role *management.Role) { + r.Result(&roleView{ + Name: role.GetName(), + ID: ansi.Faint(role.GetID()), + Description: role.GetDescription(), + }) } From 11bf1b963038cf4848e89cafdd127c8c72db1dd6 Mon Sep 17 00:00:00 2001 From: Cyril David Date: Fri, 14 May 2021 17:09:02 -0700 Subject: [PATCH 5/5] Reorder ID/name for role list --- internal/display/roles.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/display/roles.go b/internal/display/roles.go index 3dc71d4f1..83c5e4704 100644 --- a/internal/display/roles.go +++ b/internal/display/roles.go @@ -6,19 +6,19 @@ import ( ) type roleView struct { - Name string ID string + Name string Description string } func (v *roleView) AsTableHeader() []string { - return []string{"Name", "Role ID", "Description"} + return []string{"Role ID", "Name", "Description"} } func (v *roleView) AsTableRow() []string { return []string{ - v.Name, ansi.Faint(v.ID), + v.Name, v.Description, } }