Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor and improve users import cmd #735

Merged
merged 8 commits into from
Apr 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions docs/auth0_users_import.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,29 @@ auth0 users import [flags]
```
auth0 users import
auth0 users import --connection "Username-Password-Authentication"
auth0 users import --connection "Username-Password-Authentication" --users "[]"
auth0 users import --connection "Username-Password-Authentication" --users "$(cat path/to/users.json)"
cat path/to/users.json | auth0 users import --connection "Username-Password-Authentication"
auth0 users import -c "Username-Password-Authentication" --template "Basic Example"
auth0 users import -c "Username-Password-Authentication" -t "Basic Example" --upsert
auth0 users import -c "Username-Password-Authentication" -t "Basic Example" --upsert --email-results=false
auth0 users import -c "Username-Password-Authentication" --users "$(cat path/to/users.json)" --upsert --email-results
auth0 users import -c "Username-Password-Authentication" --users "$(cat path/to/users.json)" --upsert --email-results --no-input
cat path/to/users.json | auth0 users import -c "Username-Password-Authentication" --upsert --email-results --no-input
auth0 users import -c "Username-Password-Authentication" -u "$(cat path/to/users.json)" --upsert --email-results
cat path/to/users.json | auth0 users import -c "Username-Password-Authentication" --upsert --email-results
auth0 users import -c "Username-Password-Authentication" -t "Basic Example" --upsert --email-results
auth0 users import -c "Username-Password-Authentication" -t "Basic Example" --upsert=false --email-results=false
auth0 users import -c "Username-Password-Authentication" -t "Basic Example" --upsert=false --email-results=false
```


## Flags

```
-c, --connection string Name of the database connection this user should be created in.
-r, --email-results When true, sends a completion email to all tenant owners when the job is finished. The default is true, so you must explicitly set this parameter to false if you do not want emails sent. (default true)
-t, --template string Name of JSON example to be used.
-u, --upsert When set to false, pre-existing users that match on email address, user ID, or username will fail. When set to true, pre-existing users that match on any of these fields will be updated, but only with upsertable attributes.
--email-results When true, sends a completion email to all tenant owners when the job is finished. The default is true, so you must explicitly set this parameter to false if you do not want emails sent. (default true)
-t, --template string Name of JSON example to be used. Cannot be used if the '--users' flag is passed. Options include: 'Empty', 'Basic Example', 'Custom Password Hash Example' and 'MFA Factors Example'.
--upsert When set to false, pre-existing users that match on email address, user ID, or username will fail. When set to true, pre-existing users that match on any of these fields will be updated, but only with upsertable attributes.
-u, --users string JSON payload that contains an array of user(s) to be imported. Cannot be used if the '--template' flag is passed.
```


Expand Down
153 changes: 82 additions & 71 deletions internal/cli/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (

"github.com/auth0/auth0-cli/internal/ansi"
"github.com/auth0/auth0-cli/internal/auth0"
"github.com/auth0/auth0-cli/internal/iostream"
"github.com/auth0/auth0-cli/internal/prompt"
"github.com/auth0/auth0-cli/internal/users"
)
Expand Down Expand Up @@ -76,30 +77,29 @@ var (
Help: "Number of users, that match the search criteria, to retrieve. Minimum 1, maximum 1000. If limit is hit, refine the search query.",
}
userImportTemplate = Flag{
Name: "Template",
LongForm: "template",
ShortForm: "t",
Help: "Name of JSON example to be used.",
Name: "Template",
LongForm: "template",
ShortForm: "t",
Help: "Name of JSON example to be used. Cannot be used if the '--users' flag is passed. " +
"Options include: 'Empty', 'Basic Example', 'Custom Password Hash Example' and 'MFA Factors Example'.",
IsRequired: false,
}
userImportTemplateBody = Flag{
Name: "Template Body",
LongForm: "template-body",
ShortForm: "b",
Help: "JSON template body that contains an array of user(s) to be imported.",
userImportBody = Flag{
Name: "Users Payload",
LongForm: "users",
ShortForm: "u",
Help: "JSON payload that contains an array of user(s) to be imported. Cannot be used if the '--template' flag is passed.",
IsRequired: false,
}
userEmailResults = Flag{
Name: "Email Completion Results",
LongForm: "email-results",
ShortForm: "r",
Help: "When true, sends a completion email to all tenant owners when the job is finished. The default is true, so you must explicitly set this parameter to false if you do not want emails sent.",
IsRequired: false,
}
userImportUpsert = Flag{
Name: "Upsert",
LongForm: "upsert",
ShortForm: "u",
Help: "When set to false, pre-existing users that match on email address, user ID, or username will fail. When set to true, pre-existing users that match on any of these fields will be updated, but only with upsertable attributes.",
IsRequired: false,
}
Expand Down Expand Up @@ -229,9 +229,8 @@ func createUserCmd(cli *cli) *cobra.Command {
auth0 users create --name "John Doe" --email [email protected] --connection "Username-Password-Authentication" --username "example"
auth0 users create -n "John Doe" -e [email protected] -c "Username-Password-Authentication" -u "example" --json`,
RunE: func(cmd *cobra.Command, args []string) error {
// Select from the available connection types
// Users API currently support database connections
options, err := cli.connectionPickerOptions()
// Users API currently only supports database connections.
options, err := cli.dbConnectionPickerOptions()
if err != nil {
return err
}
Expand Down Expand Up @@ -537,7 +536,7 @@ func importUsersCmd(cli *cli) *cobra.Command {
Connection string
ConnectionID string
Template string
TemplateBody string
UsersBody string
Upsert bool
SendCompletionEmail bool
}
Expand All @@ -549,76 +548,89 @@ func importUsersCmd(cli *cli) *cobra.Command {
The file size limit for a bulk import is 500KB. You will need to start multiple imports if your data exceeds this size.`,
Example: ` auth0 users import
auth0 users import --connection "Username-Password-Authentication"
auth0 users import --connection "Username-Password-Authentication" --users "[]"
auth0 users import --connection "Username-Password-Authentication" --users "$(cat path/to/users.json)"
cat path/to/users.json | auth0 users import --connection "Username-Password-Authentication"
auth0 users import -c "Username-Password-Authentication" --template "Basic Example"
auth0 users import -c "Username-Password-Authentication" -t "Basic Example" --upsert
auth0 users import -c "Username-Password-Authentication" -t "Basic Example" --upsert --email-results=false`,
auth0 users import -c "Username-Password-Authentication" --users "$(cat path/to/users.json)" --upsert --email-results
auth0 users import -c "Username-Password-Authentication" --users "$(cat path/to/users.json)" --upsert --email-results --no-input
cat path/to/users.json | auth0 users import -c "Username-Password-Authentication" --upsert --email-results --no-input
auth0 users import -c "Username-Password-Authentication" -u "$(cat path/to/users.json)" --upsert --email-results
cat path/to/users.json | auth0 users import -c "Username-Password-Authentication" --upsert --email-results
auth0 users import -c "Username-Password-Authentication" -t "Basic Example" --upsert --email-results
auth0 users import -c "Username-Password-Authentication" -t "Basic Example" --upsert=false --email-results=false
auth0 users import -c "Username-Password-Authentication" -t "Basic Example" --upsert=false --email-results=false`,
RunE: func(cmd *cobra.Command, args []string) error {
// Select from the available connection types
// Users API currently support database connections
options, optionsErr := cli.connectionPickerOptions()
if optionsErr != nil {
return optionsErr
// Users API currently only supports database connections.
dbConnectionOptions, err := cli.dbConnectionPickerOptions()
if err != nil {
return err
}

if err := userConnection.Select(cmd, &inputs.Connection, options, nil); err != nil {
if err := userConnection.Select(cmd, &inputs.Connection, dbConnectionOptions, nil); err != nil {
return err
}

// Get Connection ID
conn, connErr := cli.api.Connection.ReadByName(inputs.Connection)
if connErr != nil {
return fmt.Errorf("Connection does not exist: %w", connErr)
connection, err := cli.api.Connection.ReadByName(inputs.Connection)
if err != nil {
return fmt.Errorf("failed to find connection with name %q: %w", inputs.Connection, err)
}

inputs.ConnectionID = *conn.ID
inputs.ConnectionID = connection.GetID()

// Present user with template options
if templateErr := userImportTemplate.Select(cmd, &inputs.Template, userImportOptions.labels(), nil); templateErr != nil {
return templateErr
pipedUsersBody := iostream.PipedInput()
if len(pipedUsersBody) > 0 && inputs.UsersBody == "" {
inputs.UsersBody = string(pipedUsersBody)
}

editorErr := userImportTemplateBody.OpenEditor(
cmd,
&inputs.TemplateBody,
userImportOptions.getValue(inputs.Template),
inputs.Template+".*.json",
cli.userImportEditorHint,
)
if editorErr != nil {
return fmt.Errorf("Failed to capture input from the editor: %w", editorErr)
if inputs.UsersBody == "" {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we don't pass a body directly, then we're gonna select a template and use the editor.

err := userImportTemplate.Select(cmd, &inputs.Template, userImportOptions.labels(), nil)
if err != nil {
return err
}

if err := userImportBody.OpenEditor(
cmd,
&inputs.UsersBody,
userImportOptions.getValue(inputs.Template),
inputs.Template+".*.json",
cli.userImportEditorHint,
); err != nil {
return fmt.Errorf("failed to capture input from the editor: %w", err)
}
}

var confirmed bool
if confirmedErr := prompt.AskBool("Do you want to import these user(s)?", &confirmed, true); confirmedErr != nil {
return fmt.Errorf("Failed to capture prompt input: %w", confirmedErr)
if canPrompt(cmd) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This gets skipped if we use --no-input.

var confirmed bool
if err := prompt.AskBool("Do you want to import these user(s)?", &confirmed, true); err != nil {
return fmt.Errorf("failed to capture prompt input: %w", err)
}
if !confirmed {
return nil
}
}

if !confirmed {
return nil
var usersBody []map[string]interface{}
if err := json.Unmarshal([]byte(inputs.UsersBody), &usersBody); err != nil {
return fmt.Errorf("invalid JSON input: %w", err)
}

// Convert json array to map
jsonstr := inputs.TemplateBody
var jsonmap []map[string]interface{}
jsonErr := json.Unmarshal([]byte(jsonstr), &jsonmap)
if jsonErr != nil {
return fmt.Errorf("Invalid JSON input: %w", jsonErr)
job := &management.Job{
ConnectionID: &inputs.ConnectionID,
Users: usersBody,
Upsert: &inputs.Upsert,
SendCompletionEmail: &inputs.SendCompletionEmail,
}

err := ansi.Waiting(func() error {
return cli.api.Jobs.ImportUsers(&management.Job{
ConnectionID: &inputs.ConnectionID,
Users: jsonmap,
Upsert: &inputs.Upsert,
SendCompletionEmail: &inputs.SendCompletionEmail,
})
})
if err != nil {
if err := ansi.Waiting(func() error {
return cli.api.Jobs.ImportUsers(job)
}); err != nil {
return err
}

cli.renderer.Heading("starting user import job...")
fmt.Println(jsonstr)
cli.renderer.Heading("started user import job")
cli.renderer.Infof("Job with ID '%s' successfully started.", ansi.Bold(job.GetID()))
cli.renderer.Infof("Run '%s' to get the status of the job.", ansi.Cyan("auth0 api jobs/"+job.GetID()))

if inputs.SendCompletionEmail {
cli.renderer.Infof("Results of your user import job will be sent to your email.")
Expand All @@ -630,8 +642,10 @@ The file size limit for a bulk import is 500KB. You will need to start multiple

userConnection.RegisterString(cmd, &inputs.Connection, "")
userImportTemplate.RegisterString(cmd, &inputs.Template, "")
userImportBody.RegisterString(cmd, &inputs.UsersBody, "")
userEmailResults.RegisterBool(cmd, &inputs.SendCompletionEmail, true)
userImportUpsert.RegisterBool(cmd, &inputs.Upsert, false)
cmd.MarkFlagsMutuallyExclusive("template", "users")

return cmd
}
Expand All @@ -643,17 +657,15 @@ func formatUserDetailsPath(id string) string {
return fmt.Sprintf("users/%s", id)
}

func (c *cli) connectionPickerOptions() ([]string, error) {
var res []string

list, err := c.api.Connection.List()
func (c *cli) dbConnectionPickerOptions() ([]string, error) {
list, err := c.api.Connection.List(management.Parameter("strategy", management.ConnectionStrategyAuth0))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're improving the logic here by directly only retrieving database connections from the API instead of retrieving everything and then filtering them out.

if err != nil {
return nil, err
}

var res []string
for _, conn := range list.Connections {
if conn.GetStrategy() == "auth0" {
res = append(res, conn.GetName())
}
res = append(res, conn.GetName())
}

if len(res) == 0 {
Expand All @@ -666,9 +678,8 @@ func (c *cli) connectionPickerOptions() ([]string, error) {
func (c *cli) getUserConnection(users *management.User) []string {
var res []string
for _, i := range users.Identities {
res = append(res, fmt.Sprintf("%v", auth0.StringValue(i.Connection)))
res = append(res, i.GetConnection())
}

return res
}

Expand Down
21 changes: 1 addition & 20 deletions internal/cli/users_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,25 +52,6 @@ func TestConnectionsPickerOptions(t *testing.T) {
assert.ErrorContains(t, err, "There are currently no database connections.")
},
},
{
name: "no database connections",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No longer needed as we retrieve only database conns directly from the API.

connections: []*management.Connection{
{
Name: auth0.String("some-name-1"),
Strategy: auth0.String("foo"),
},
{
Name: auth0.String("some-name-2"),
Strategy: auth0.String("foo"),
},
},
assertOutput: func(t testing.TB, options []string) {
t.Fail()
},
assertError: func(t testing.TB, err error) {
assert.ErrorContains(t, err, "There are currently no database connections.")
},
},
{
name: "API error",
apiError: errors.New("error"),
Expand Down Expand Up @@ -98,7 +79,7 @@ func TestConnectionsPickerOptions(t *testing.T) {
api: &auth0.API{Connection: connectionAPI},
}

options, err := cli.connectionPickerOptions()
options, err := cli.dbConnectionPickerOptions()

if err != nil {
test.assertError(t, err)
Expand Down
20 changes: 20 additions & 0 deletions test/integration/users-test-cases.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,23 @@ tests:
stderr:
contains:
- "Open the following URL in a browser: https://manage.auth0.com/dashboard/"

016 - users import:
command: auth0 users import -c "Username-Password-Authentication" --users "[]" --email-results=false --no-input
exit-code: 0
stderr:
contains:
- "started user import job"
- "Job with ID"
- "successfully started"
- "to get the status of the job"

017 - users import with piped data:
command: echo "[]" | auth0 users import -c "Username-Password-Authentication" --email-results=false --no-input
exit-code: 0
stderr:
contains:
- "started user import job"
- "Job with ID"
- "successfully started"
- "to get the status of the job"