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", } 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) +} diff --git a/internal/cli/roles.go b/internal/cli/roles.go new file mode 100644 index 000000000..f120c2473 --- /dev/null +++ b/internal/cli/roles.go @@ -0,0 +1,303 @@ +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", + Short: "Manage resources for roles", + Long: "Manage resources for roles.", + Aliases: []string{"role"}, + } + + 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 +} + +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 +} + +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/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..83c5e4704 --- /dev/null +++ b/internal/display/roles.go @@ -0,0 +1,78 @@ +package display + +import ( + "github.com/auth0/auth0-cli/internal/ansi" + "gopkg.in/auth0.v5/management" +) + +type roleView struct { + ID string + Name string + Description string +} + +func (v *roleView) AsTableHeader() []string { + return []string{"Role ID", "Name", "Description"} +} + +func (v *roleView) AsTableRow() []string { + return []string{ + ansi.Faint(v.ID), + v.Name, + v.Description, + } +} + +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" + + 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) 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("role updated") + r.roleResult(role) +} + +func (r *Renderer) roleResult(role *management.Role) { + r.Result(&roleView{ + Name: role.GetName(), + ID: ansi.Faint(role.GetID()), + Description: role.GetDescription(), + }) +}