From df91ee1df61308197d608827b1d722975ecc323c Mon Sep 17 00:00:00 2001 From: Cyril David Date: Fri, 21 May 2021 21:55:10 -0700 Subject: [PATCH 1/6] 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 ``` --- 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 | 254 +++++++++++++++++++++++++++ internal/display/role_permissions.go | 90 ++++++++++ 6 files changed, 398 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..538c0c6aa --- /dev/null +++ b/internal/cli/roles_permissions.go @@ -0,0 +1,254 @@ +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/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.", + Aliases: []string{"permission"}, + } + + cmd.SetUsageTemplate(resourceUsageTemplate()) + cmd.AddCommand(listRolePermissionsCmd(cli)) + cmd.AddCommand(associateRolePermissionsCmd(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 associate `, + 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 associateRolePermissionsCmd(cli *cli) *cobra.Command { + var inputs struct { + ID string + APIIdentifier string + Permissions []string + } + + cmd := &cobra.Command{ + Use: "associate", + Aliases: []string{"assoc"}, + Args: cobra.MaximumNArgs(1), + Short: "Associate a permission to a role", + Long: `Associate an existing permission defined in one of your APIs. +To add a permission try: + + auth0 roles permissions associate -p `, + Example: `auth0 roles permissions associate -p +auth0 roles permissions assoc`, + 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 + } + + 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 + } + + 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()) + } + + prompt := &survey.MultiSelect{ + Message: "Permissions", + Options: options, + } + survey.AskOne(prompt, permissions) + + 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: &id, + Name: &p, + }) + } + return result +} diff --git a/internal/display/role_permissions.go b/internal/display/role_permissions.go new file mode 100644 index 000000000..9876d4c48 --- /dev/null +++ b/internal/display/role_permissions.go @@ -0,0 +1,90 @@ +package display + +import ( + "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 associate' 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) 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(), + }) +} +*/ From 7c53bcaa80b10f740ae102857852696632f75ad7 Mon Sep 17 00:00:00 2001 From: Cyril David Date: Fri, 9 Jul 2021 22:40:10 -0700 Subject: [PATCH 2/6] add/associate --- internal/cli/roles_permissions.go | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/internal/cli/roles_permissions.go b/internal/cli/roles_permissions.go index 538c0c6aa..304807f07 100644 --- a/internal/cli/roles_permissions.go +++ b/internal/cli/roles_permissions.go @@ -30,15 +30,14 @@ var ( 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.", - Aliases: []string{"permission"}, + 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(associateRolePermissionsCmd(cli)) + cmd.AddCommand(addRolePermissionsCmd(cli)) cmd.AddCommand(removeRolePermissionsCmd(cli)) return cmd @@ -55,7 +54,7 @@ func listRolePermissionsCmd(cli *cli) *cobra.Command { 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 associate `, +auth0 roles permissions add `, Example: `auth0 roles permissions list auth0 roles permissions ls`, RunE: func(cmd *cobra.Command, args []string) error { @@ -86,7 +85,7 @@ auth0 roles permissions ls`, return cmd } -func associateRolePermissionsCmd(cli *cli) *cobra.Command { +func addRolePermissionsCmd(cli *cli) *cobra.Command { var inputs struct { ID string APIIdentifier string @@ -94,15 +93,14 @@ func associateRolePermissionsCmd(cli *cli) *cobra.Command { } cmd := &cobra.Command{ - Use: "associate", - Aliases: []string{"assoc"}, - Args: cobra.MaximumNArgs(1), - Short: "Associate a permission to a role", - Long: `Associate an existing permission defined in one of your APIs. + 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 associate -p `, - Example: `auth0 roles permissions associate -p + auth0 roles permissions add -p `, + Example: `auth0 roles permissions add -p auth0 roles permissions assoc`, RunE: func(cmd *cobra.Command, args []string) error { if len(args) == 0 { From 8b0ee9a380f2fcf0c9a24a1e2f0de321e37c7638 Mon Sep 17 00:00:00 2001 From: Cyril David Date: Fri, 9 Jul 2021 23:24:25 -0700 Subject: [PATCH 3/6] Better UX --- internal/cli/roles_permissions.go | 21 +++++++++++++++++---- internal/display/role_permissions.go | 27 ++++++++------------------- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/internal/cli/roles_permissions.go b/internal/cli/roles_permissions.go index 304807f07..5ccaa0c28 100644 --- a/internal/cli/roles_permissions.go +++ b/internal/cli/roles_permissions.go @@ -7,6 +7,7 @@ import ( "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" ) @@ -131,6 +132,12 @@ auth0 roles permissions assoc`, return err } + role, err := cli.api.Role.Read(inputs.ID) + if err != nil { + return err + } + + cli.renderer.RolePermissionAdd(role, rs, inputs.Permissions) return nil }, } @@ -187,6 +194,12 @@ auth0 roles permissions rm`, return err } + role, err := cli.api.Role.Read(inputs.ID) + if err != nil { + return err + } + + cli.renderer.RolePermissionRemove(role, rs, inputs.Permissions) return nil }, } @@ -231,11 +244,11 @@ func (c *cli) pickRolePermissions(id string, permissions *[]string) (*management options = append(options, s.GetValue()) } - prompt := &survey.MultiSelect{ + p := &survey.MultiSelect{ Message: "Permissions", Options: options, } - survey.AskOne(prompt, permissions) + survey.AskOne(p, permissions) return rs, nil } @@ -244,8 +257,8 @@ func makePermissions(id string, permissions []string) []*management.Permission { var result []*management.Permission for _, p := range permissions { result = append(result, &management.Permission{ - ResourceServerIdentifier: &id, - Name: &p, + ResourceServerIdentifier: auth0.String(id), + Name: auth0.String(p), }) } return result diff --git a/internal/display/role_permissions.go b/internal/display/role_permissions.go index 9876d4c48..a2a023ee1 100644 --- a/internal/display/role_permissions.go +++ b/internal/display/role_permissions.go @@ -1,6 +1,8 @@ package display import ( + "strings" + "github.com/auth0/auth0-cli/internal/ansi" "gopkg.in/auth0.v5/management" ) @@ -64,27 +66,14 @@ func (r *Renderer) RolePermissionList(perms []*management.Permission) { r.Results(res) } -/* -func (r *Renderer) RoleShow(role *management.Role) { - r.Heading("role") - r.roleResult(role) -} +func (r *Renderer) RolePermissionAdd(role *management.Role, rs *management.ResourceServer, perms []string) { + r.Heading("role permissions added") -func (r *Renderer) RoleCreate(role *management.Role) { - r.Heading("role created") - r.roleResult(role) + 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) RoleUpdate(role *management.Role) { - r.Heading("role updated") - r.roleResult(role) -} +func (r *Renderer) RolePermissionRemove(role *management.Role, rs *management.ResourceServer, perms []string) { + r.Heading("role permissions removed") -func (r *Renderer) roleResult(role *management.Role) { - r.Result(&roleView{ - Name: role.GetName(), - ID: ansi.Faint(role.GetID()), - Description: role.GetDescription(), - }) + r.Infof("Removed permissions %s (%s) from role %s.", ansi.Green(strings.Join(perms, ", ")), ansi.Faint(rs.GetIdentifier()), ansi.Green(role.GetName())) } -*/ From dd0b2624dc06fda6121153477800666a83958756 Mon Sep 17 00:00:00 2001 From: Cyril David Date: Fri, 9 Jul 2021 23:26:24 -0700 Subject: [PATCH 4/6] Lint --- internal/cli/roles_permissions.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/internal/cli/roles_permissions.go b/internal/cli/roles_permissions.go index 5ccaa0c28..16531bb5d 100644 --- a/internal/cli/roles_permissions.go +++ b/internal/cli/roles_permissions.go @@ -248,7 +248,10 @@ func (c *cli) pickRolePermissions(id string, permissions *[]string) (*management Message: "Permissions", Options: options, } - survey.AskOne(p, permissions) + + if err := survey.AskOne(p, permissions); err != nil { + return nil, err + } return rs, nil } From eb893d98b9ea0f85660732b290d63405dcacbe93 Mon Sep 17 00:00:00 2001 From: Cyril David Date: Mon, 12 Jul 2021 09:40:37 -0700 Subject: [PATCH 5/6] Update internal/display/role_permissions.go Co-authored-by: Rita Zerrizuela --- internal/display/role_permissions.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/display/role_permissions.go b/internal/display/role_permissions.go index a2a023ee1..dfc60acd1 100644 --- a/internal/display/role_permissions.go +++ b/internal/display/role_permissions.go @@ -48,7 +48,7 @@ func (r *Renderer) RolePermissionList(perms []*management.Permission) { if len(perms) == 0 { r.EmptyState(resource) - r.Infof("Use 'auth0 roles permissions associate' to add one") + r.Infof("Use 'auth0 roles permissions add' to add one") return } From a59bd4c0a62aa1c6ec35c0b74c4f6e3fd646f7cf Mon Sep 17 00:00:00 2001 From: Cyril David Date: Mon, 12 Jul 2021 14:24:17 -0700 Subject: [PATCH 6/6] s/assoc/add --- internal/cli/roles_permissions.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/cli/roles_permissions.go b/internal/cli/roles_permissions.go index 16531bb5d..1c8f3fb5e 100644 --- a/internal/cli/roles_permissions.go +++ b/internal/cli/roles_permissions.go @@ -102,7 +102,7 @@ To add a permission try: auth0 roles permissions add -p `, Example: `auth0 roles permissions add -p -auth0 roles permissions assoc`, +auth0 roles permissions add`, RunE: func(cmd *cobra.Command, args []string) error { if len(args) == 0 { err := roleID.Pick(cmd, &inputs.ID, cli.rolePickerOptions)