-
Notifications
You must be signed in to change notification settings - Fork 55
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
Changes from all commits
d161b02
25b30dc
d2afaaa
b7c3c13
81886fe
990c398
7f4744c
19b6a0c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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" | ||
) | ||
|
@@ -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, | ||
} | ||
|
@@ -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 | ||
} | ||
|
@@ -537,7 +536,7 @@ func importUsersCmd(cli *cli) *cobra.Command { | |
Connection string | ||
ConnectionID string | ||
Template string | ||
TemplateBody string | ||
UsersBody string | ||
Upsert bool | ||
SendCompletionEmail bool | ||
} | ||
|
@@ -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 == "" { | ||
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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.") | ||
|
@@ -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 | ||
} | ||
|
@@ -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)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 { | ||
|
@@ -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 | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -52,25 +52,6 @@ func TestConnectionsPickerOptions(t *testing.T) { | |
assert.ErrorContains(t, err, "There are currently no database connections.") | ||
}, | ||
}, | ||
{ | ||
name: "no database connections", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"), | ||
|
@@ -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) | ||
|
There was a problem hiding this comment.
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.