From 27afbb7fc56810c29724c73428b8ec666a30b4f9 Mon Sep 17 00:00:00 2001 From: Cyril David Date: Mon, 12 Jul 2021 14:30:10 -0700 Subject: [PATCH] feat: add role permissions (#329) * feat: add roles permissions list/associate/remove This feature adds a set of commands: ``` auth0 roles permissions list auth0 roles permissions associate auth0 roles permissions remove ``` * add/associate * Better UX * Lint * Update internal/display/role_permissions.go Co-authored-by: Rita Zerrizuela * s/assoc/add Co-authored-by: Rita Zerrizuela --- internal/auth0/role.go | 15 ++ internal/cli/apis.go | 21 ++- internal/cli/flags.go | 23 +++ internal/cli/roles.go | 1 + internal/cli/roles_permissions.go | 268 +++++++++++++++++++++++++++ internal/display/role_permissions.go | 79 ++++++++ 6 files changed, 401 insertions(+), 6 deletions(-) create mode 100644 internal/cli/roles_permissions.go create mode 100644 internal/display/role_permissions.go diff --git a/internal/auth0/role.go b/internal/auth0/role.go index 9ad436352..f255d5973 100644 --- a/internal/auth0/role.go +++ b/internal/auth0/role.go @@ -17,4 +17,19 @@ type RoleAPI interface { // Delete a role. Delete(id string, opts ...management.RequestOption) (err error) + + // AssociatePermissions associates permissions to a role. + // + // See: https://auth0.com/docs/api/management/v2#!/Roles/post_role_permission_assignment + AssociatePermissions(id string, permissions []*management.Permission, opts ...management.RequestOption) error + + // Permissions retrieves all permissions granted by a role. + // + // See: https://auth0.com/docs/api/management/v2#!/Roles/get_role_permission + Permissions(id string, opts ...management.RequestOption) (p *management.PermissionList, err error) + + // RemovePermissions removes permissions associated to a role. + // + // See: https://auth0.com/docs/api/management/v2#!/Roles/delete_role_permission_assignment + RemovePermissions(id string, permissions []*management.Permission, opts ...management.RequestOption) error } diff --git a/internal/cli/apis.go b/internal/cli/apis.go index e8d5fc8c2..3c5650091 100644 --- a/internal/cli/apis.go +++ b/internal/cli/apis.go @@ -196,7 +196,7 @@ auth0 apis create -n myapi -e 6100 --offline-access=true`, return err } - if err :=apiOfflineAccess.AskBool(cmd, &inputs.AllowOfflineAccess, nil); err != nil { + if err := apiOfflineAccess.AskBool(cmd, &inputs.AllowOfflineAccess, nil); err != nil { return err } @@ -240,11 +240,11 @@ auth0 apis create -n myapi -e 6100 --offline-access=true`, func updateApiCmd(cli *cli) *cobra.Command { var inputs struct { - ID string - Name string - Scopes []string - TokenLifetime int - AllowOfflineAccess bool + ID string + Name string + Scopes []string + TokenLifetime int + AllowOfflineAccess bool } cmd := &cobra.Command{ @@ -497,6 +497,12 @@ func apiDefaultTokenLifetime() int { } func (c *cli) apiPickerOptions() (pickerOptions, error) { + return c.filteredAPIPickerOptions(func(r *management.ResourceServer) bool { + return true + }) +} + +func (c *cli) filteredAPIPickerOptions(include func(r *management.ResourceServer) bool) (pickerOptions, error) { list, err := c.api.ResourceServer.List() if err != nil { return nil, err @@ -506,6 +512,9 @@ func (c *cli) apiPickerOptions() (pickerOptions, error) { // labels. var opts pickerOptions for _, r := range list.ResourceServers { + if !include(r) { + continue + } label := fmt.Sprintf("%s %s", r.GetName(), ansi.Faint("("+r.GetIdentifier()+")")) opts = append(opts, pickerOption{value: r.GetID(), label: label}) diff --git a/internal/cli/flags.go b/internal/cli/flags.go index 35ff6a7e7..92a24e9c4 100644 --- a/internal/cli/flags.go +++ b/internal/cli/flags.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/AlecAivazis/survey/v2" + "github.com/auth0/auth0-cli/internal/ansi" "github.com/auth0/auth0-cli/internal/auth0" "github.com/auth0/auth0-cli/internal/prompt" "github.com/spf13/cobra" @@ -70,6 +71,28 @@ func (f *Flag) SelectU(cmd *cobra.Command, value interface{}, options []string, return selectFlag(cmd, f, value, options, defaultValue, true) } +func (f *Flag) Pick(cmd *cobra.Command, result *string, fn pickerOptionsFunc) error { + var opts pickerOptions + err := ansi.Waiting(func() error { + var err error + opts, err = fn() + return err + }) + + if err != nil { + return err + } + + defaultLabel := opts.defaultLabel() + var val string + if err := selectFlag(cmd, f, &val, opts.labels(), &defaultLabel, false); err != nil { + return err + } + + *result = opts.getValue(val) + return nil +} + func (f *Flag) EditorPrompt(cmd *cobra.Command, value *string, initialValue, filename string, infoFn func()) error { out, err := prompt.CaptureInputViaEditor( initialValue, diff --git a/internal/cli/roles.go b/internal/cli/roles.go index 18caa1b50..80f80a6cb 100644 --- a/internal/cli/roles.go +++ b/internal/cli/roles.go @@ -48,6 +48,7 @@ func rolesCmd(cli *cli) *cobra.Command { cmd.AddCommand(createRoleCmd(cli)) cmd.AddCommand(updateRoleCmd(cli)) cmd.AddCommand(deleteRoleCmd(cli)) + cmd.AddCommand(rolePermissionsCmd(cli)) return cmd } diff --git a/internal/cli/roles_permissions.go b/internal/cli/roles_permissions.go new file mode 100644 index 000000000..1c8f3fb5e --- /dev/null +++ b/internal/cli/roles_permissions.go @@ -0,0 +1,268 @@ +package cli + +import ( + "fmt" + "net/url" + + "github.com/AlecAivazis/survey/v2" + "github.com/auth0/auth0-cli/internal/ansi" + "github.com/spf13/cobra" + "gopkg.in/auth0.v5" + "gopkg.in/auth0.v5/management" +) + +var ( + roleAPIIdentifier = Flag{ + Name: "API", + LongForm: "api-id", + ShortForm: "a", + Help: "API Identifier.", + IsRequired: true, + } + + roleAPIPermissions = Flag{ + Name: "Permissions", + LongForm: "permissions", + ShortForm: "p", + Help: "Permissions.", + IsRequired: true, + } +) + +func rolePermissionsCmd(cli *cli) *cobra.Command { + cmd := &cobra.Command{ + Use: "permissions", + Short: "Manage permissions within the role resource", + Long: "Manage permissions within the role resource.", + } + + cmd.SetUsageTemplate(resourceUsageTemplate()) + cmd.AddCommand(listRolePermissionsCmd(cli)) + cmd.AddCommand(addRolePermissionsCmd(cli)) + cmd.AddCommand(removeRolePermissionsCmd(cli)) + + return cmd +} + +func listRolePermissionsCmd(cli *cli) *cobra.Command { + var inputs struct { + ID string + } + + cmd := &cobra.Command{ + Use: "list", + Aliases: []string{"ls"}, + Args: cobra.MaximumNArgs(1), + Short: "List permissions defined within a role", + Long: `List existing permissions defined in a role. To add a permission try: +auth0 roles permissions add `, + Example: `auth0 roles permissions list +auth0 roles permissions ls`, + 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] + } + + var list *management.PermissionList + + if err := ansi.Waiting(func() error { + var err error + list, err = cli.api.Role.Permissions(inputs.ID) + return err + }); err != nil { + return fmt.Errorf("An unexpected error occurred: %w", err) + } + + cli.renderer.RolePermissionList(list.Permissions) + return nil + }, + } + + return cmd +} + +func addRolePermissionsCmd(cli *cli) *cobra.Command { + var inputs struct { + ID string + APIIdentifier string + Permissions []string + } + + cmd := &cobra.Command{ + Use: "add", + Args: cobra.MaximumNArgs(1), + Short: "Add a permission to a role", + Long: `Add an existing permission defined in one of your APIs. +To add a permission try: + + auth0 roles permissions add -p `, + Example: `auth0 roles permissions add -p +auth0 roles permissions add`, + 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 err := roleAPIIdentifier.Pick(cmd, &inputs.APIIdentifier, cli.apiPickerOptionsWithoutAuth0); err != nil { + return err + } + + var rs *management.ResourceServer + + if len(inputs.Permissions) == 0 { + var err error + rs, err = cli.pickRolePermissions(inputs.APIIdentifier, &inputs.Permissions) + if err != nil { + return err + } + } + + ps := makePermissions(rs.GetIdentifier(), inputs.Permissions) + if err := cli.api.Role.AssociatePermissions(inputs.ID, ps); err != nil { + return err + } + + role, err := cli.api.Role.Read(inputs.ID) + if err != nil { + return err + } + + cli.renderer.RolePermissionAdd(role, rs, inputs.Permissions) + return nil + }, + } + + roleAPIIdentifier.RegisterString(cmd, &inputs.APIIdentifier, "") + roleAPIPermissions.RegisterStringSlice(cmd, &inputs.Permissions, nil) + return cmd +} + +func removeRolePermissionsCmd(cli *cli) *cobra.Command { + var inputs struct { + ID string + APIIdentifier string + Permissions []string + } + + cmd := &cobra.Command{ + Use: "remove", + Aliases: []string{"rm"}, + Args: cobra.MaximumNArgs(1), + Short: "Remove a permission from a role", + Long: `Remove an existing permission defined in one of your APIs. +To remove a permission try: + + auth0 roles permissions remove -p `, + Example: `auth0 roles permissions remove -p +auth0 roles permissions rm`, + 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 err := roleAPIIdentifier.Pick(cmd, &inputs.APIIdentifier, cli.apiPickerOptionsWithoutAuth0); err != nil { + return err + } + + var rs *management.ResourceServer + + if len(inputs.Permissions) == 0 { + var err error + rs, err = cli.pickRolePermissions(inputs.APIIdentifier, &inputs.Permissions) + if err != nil { + return err + } + } + + ps := makePermissions(rs.GetIdentifier(), inputs.Permissions) + if err := cli.api.Role.RemovePermissions(inputs.ID, ps); err != nil { + return err + } + + role, err := cli.api.Role.Read(inputs.ID) + if err != nil { + return err + } + + cli.renderer.RolePermissionRemove(role, rs, inputs.Permissions) + return nil + }, + } + + roleAPIIdentifier.RegisterString(cmd, &inputs.APIIdentifier, "") + roleAPIPermissions.RegisterStringSlice(cmd, &inputs.Permissions, nil) + return cmd +} + +func (c *cli) apiPickerOptionsWithoutAuth0() (pickerOptions, error) { + ten, err := c.getTenant() + if err != nil { + return nil, err + } + + return c.filteredAPIPickerOptions(func(r *management.ResourceServer) bool { + u, err := url.Parse(r.GetIdentifier()) + if err != nil { + // We really should't get an error here, but for + // correctness it's indeterminate, therefore we return + // false. + return false + } + + // We only allow API Identifiers not matching the tenant + // domain, similar to the dashboard UX. + return u.Host != ten.Domain + }) +} + +func (c *cli) pickRolePermissions(id string, permissions *[]string) (*management.ResourceServer, error) { + // NOTE(cyx): We're inlining this for now since we have no generic + // usecase for this particular picker type yet. + var err error + rs, err := c.api.ResourceServer.Read(id) + if err != nil { + return nil, err + } + + var options []string + for _, s := range rs.Scopes { + options = append(options, s.GetValue()) + } + + p := &survey.MultiSelect{ + Message: "Permissions", + Options: options, + } + + if err := survey.AskOne(p, permissions); err != nil { + return nil, err + } + + return rs, nil +} + +func makePermissions(id string, permissions []string) []*management.Permission { + var result []*management.Permission + for _, p := range permissions { + result = append(result, &management.Permission{ + ResourceServerIdentifier: auth0.String(id), + Name: auth0.String(p), + }) + } + return result +} diff --git a/internal/display/role_permissions.go b/internal/display/role_permissions.go new file mode 100644 index 000000000..dfc60acd1 --- /dev/null +++ b/internal/display/role_permissions.go @@ -0,0 +1,79 @@ +package display + +import ( + "strings" + + "github.com/auth0/auth0-cli/internal/ansi" + "gopkg.in/auth0.v5/management" +) + +type rolePermissionView struct { + APIID string + APIName string + Name string + Description string + raw interface{} +} + +func (v *rolePermissionView) Object() interface{} { + return v.raw +} + +func (v *rolePermissionView) AsTableHeader() []string { + return []string{"API Identifier", "API Name", "Permission Name", "Description"} +} + +func (v *rolePermissionView) AsTableRow() []string { + return []string{ + ansi.Faint(v.APIID), + v.APIName, + v.Name, + v.Description, + } +} + +func (v *rolePermissionView) KeyValues() [][]string { + return [][]string{ + {"ID", ansi.Faint(v.APIID)}, + {"NAME", v.APIName}, + {"PERMISSION NAME", v.Name}, + {"DESCRIPTION", v.Description}, + } +} + +func (r *Renderer) RolePermissionList(perms []*management.Permission) { + resource := "role permissions" + + r.Heading(resource) + + if len(perms) == 0 { + r.EmptyState(resource) + r.Infof("Use 'auth0 roles permissions add' to add one") + return + } + + var res []View + for _, perm := range perms { + res = append(res, &rolePermissionView{ + APIName: perm.GetResourceServerName(), + APIID: perm.GetResourceServerIdentifier(), + Name: perm.GetName(), + Description: perm.GetDescription(), + raw: perm, + }) + } + + r.Results(res) +} + +func (r *Renderer) RolePermissionAdd(role *management.Role, rs *management.ResourceServer, perms []string) { + r.Heading("role permissions added") + + r.Infof("Added permissions %s (%s) to role %s.", ansi.Green(strings.Join(perms, ", ")), ansi.Faint(rs.GetIdentifier()), ansi.Green(role.GetName())) +} + +func (r *Renderer) RolePermissionRemove(role *management.Role, rs *management.ResourceServer, perms []string) { + r.Heading("role permissions removed") + + r.Infof("Removed permissions %s (%s) from role %s.", ansi.Green(strings.Join(perms, ", ")), ansi.Faint(rs.GetIdentifier()), ansi.Green(role.GetName())) +}