diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 88dd710d8..2d21afbb0 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -30,7 +30,7 @@ var requiredScopes = []string{ "create:clients", "delete:clients", "read:clients", "update:clients", "create:resource_servers", "delete:resource_servers", "read:resource_servers", "update:resource_servers", "create:rules", "delete:rules", "read:rules", "update:rules", - "read:client_keys", "read:logs", + "read:client_keys", "read:logs", "read:users", "update:users", } // RequiredScopes returns the scopes used for login. diff --git a/internal/auth0/auth0.go b/internal/auth0/auth0.go index 5ba7c7201..6afe2f232 100644 --- a/internal/auth0/auth0.go +++ b/internal/auth0/auth0.go @@ -16,6 +16,7 @@ type API struct { Log LogAPI ResourceServer ResourceServerAPI Rule RuleAPI + User UserAPI } func NewAPI(m *management.Management) *API { @@ -28,6 +29,7 @@ func NewAPI(m *management.Management) *API { Log: m.Log, ResourceServer: m.ResourceServer, Rule: m.Rule, + User: m.User, } } diff --git a/internal/auth0/user.go b/internal/auth0/user.go new file mode 100644 index 000000000..1a8017210 --- /dev/null +++ b/internal/auth0/user.go @@ -0,0 +1,14 @@ +//go:generate mockgen -source=user.go -destination=user_mock.go -package=auth0 + +package auth0 + +import "gopkg.in/auth0.v5/management" + +type UserAPI interface { + // Retrieves a list of blocked IP addresses of a particular user. + Blocks(id string, opts ...management.RequestOption) ([]*management.UserBlock, error) + + // Unblock a user that was blocked due to an excessive amount of incorrectly + // provided credentials. + Unblock(id string, opts ...management.RequestOption) error +} diff --git a/internal/cli/root.go b/internal/cli/root.go index 5cef67cdf..273081fa8 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -86,6 +86,7 @@ func Execute() { // so add new commands in a place that reflect its relevance or relation with other commands: rootCmd.AddCommand(loginCmd(cli)) rootCmd.AddCommand(tenantsCmd(cli)) + rootCmd.AddCommand(usersCmd(cli)) rootCmd.AddCommand(appsCmd(cli)) rootCmd.AddCommand(rulesCmd(cli)) rootCmd.AddCommand(apisCmd(cli)) diff --git a/internal/cli/users.go b/internal/cli/users.go new file mode 100644 index 000000000..2d66a6156 --- /dev/null +++ b/internal/cli/users.go @@ -0,0 +1,124 @@ +package cli + +import ( + "fmt" + + "github.com/auth0/auth0-cli/internal/ansi" + "github.com/spf13/cobra" + "gopkg.in/auth0.v5/management" +) + +var ( + userID = Argument{ + Name: "User ID", + Help: "Id of the user.", + } +) + +func usersCmd(cli *cli) *cobra.Command { + cmd := &cobra.Command{ + Use: "users", + Short: "Manage resources for users", + } + + cmd.SetUsageTemplate(resourceUsageTemplate()) + cmd.AddCommand(userBlocksCmd(cli)) + cmd.AddCommand(deleteUserBlocksCmd(cli)) + return cmd +} + +func userBlocksCmd(cli *cli) *cobra.Command { + cmd := &cobra.Command{ + Use: "blocks", + Short: "Manage brute-force protection user blocks.", + } + + cmd.SetUsageTemplate(resourceUsageTemplate()) + cmd.AddCommand(listUserBlocksCmd(cli)) + return cmd +} + +func listUserBlocksCmd(cli *cli) *cobra.Command { + var inputs struct { + userID string + } + + cmd := &cobra.Command{ + Use: "list", + Args: cobra.MaximumNArgs(1), + Short: "List brute-force protection blocks for a given user", + Long: `List brute-force protection blocks for a given user: + +auth0 users blocks list +`, + PreRun: func(cmd *cobra.Command, args []string) { + prepareInteractivity(cmd) + }, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + if err := userID.Ask(cmd, &inputs.userID); err != nil { + return err + } + } else { + inputs.userID = args[0] + } + + var userBlocks []*management.UserBlock + + err := ansi.Waiting(func() error { + var err error + userBlocks, err = cli.api.User.Blocks(inputs.userID) + return err + }) + + if err != nil { + return fmt.Errorf("Unable to load user blocks %v, error: %w", inputs.userID, err) + } + + cli.renderer.UserBlocksList(userBlocks) + return nil + }, + } + + return cmd +} + +func deleteUserBlocksCmd(cli *cli) *cobra.Command { + var inputs struct { + userID string + } + + cmd := &cobra.Command{ + Use: "unblock", + Args: cobra.MaximumNArgs(1), + Short: "Delete brute-force protection blocks for a given user", + Long: `Delete brute-force protection blocks for a given user: + +auth0 users unblock +`, + PreRun: func(cmd *cobra.Command, args []string) { + prepareInteractivity(cmd) + }, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + if err := userID.Ask(cmd, &inputs.userID); err != nil { + return err + } + } else { + inputs.userID = args[0] + } + + err := ansi.Spinner("Deleting blocks for user...", func() error { + return cli.api.User.Unblock(inputs.userID) + }) + + if err != nil { + return err + } + + return nil + }, + } + + return cmd +} diff --git a/internal/display/user_blocks.go b/internal/display/user_blocks.go new file mode 100644 index 000000000..a51748d8f --- /dev/null +++ b/internal/display/user_blocks.go @@ -0,0 +1,48 @@ +package display + +import ( + "gopkg.in/auth0.v5/management" +) + +type userBlockView struct { + Identifier string + IP string +} + +func (v *userBlockView) AsTableHeader() []string { + return []string{"Identifier", "IP"} +} + +func (v *userBlockView) AsTableRow() []string { + return []string{v.Identifier, v.IP} +} + +func (v *userBlockView) KeyValues() [][]string { + return [][]string{ + []string{"Identifier", v.Identifier}, + []string{"IP", v.IP}, + } +} + +func (r *Renderer) UserBlocksList(userBlocks []*management.UserBlock) { + resource := "user blocks" + + r.Heading(resource) + + if len(userBlocks) == 0 { + r.EmptyState(resource) + return + } + + var res []View + + for _, userBlock := range userBlocks { + res = append(res, &userBlockView{ + Identifier: *userBlock.Identifier, + IP: *userBlock.IP, + }) + } + + r.Results(res) + +}