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(), + }) +} +*/