From 53cb647b92523f247ad99e1b946e7997c94d2acd Mon Sep 17 00:00:00 2001 From: "Wilson E. Husin" Date: Tue, 30 Apr 2024 21:56:28 -0700 Subject: [PATCH] import: create or merge user, wip --- cmd/import.go | 84 ++++++++++++++++++++++++++++++-------------- console/print.go | 10 ++++++ store/queries.sql | 4 +++ store/queries.sql.go | 10 ++++++ 4 files changed, 82 insertions(+), 26 deletions(-) diff --git a/cmd/import.go b/cmd/import.go index 99bd828..6319e9c 100644 --- a/cmd/import.go +++ b/cmd/import.go @@ -264,39 +264,71 @@ func importUsers(ctx context.Context, provider pager.Pager, fh *firehydrant.Clie return fmt.Errorf("unable to match users to FireHydrant: %w", err) } - // Manually link users which do not have matching email addresses + // Find out which users should be pre-created in FireHydrant via SCIM unmatched, err := store.UseQueries(ctx).ListUnmatchedExtUsers(ctx) if err != nil { return fmt.Errorf("unable to list unmatched users: %w", err) } - if len(unmatched) > 0 { - console.Warnf("Please match the following users to their FireHydrant account.\n") - fhUsers, err := fh.ListUsers(ctx) - if err != nil { - return fmt.Errorf("unable to list FireHydrant users: %w", err) + if len(unmatched) == 0 { + console.Successf("All users are already matched to FireHydrant.\n") + return nil + } + + // Get padding number to pretty print the information in table-like view. + idPad := console.PadStrings(unmatched, func(u store.ExtUser) int { return len(u.ID) }) + emailPad := console.PadStrings(unmatched, func(u store.ExtUser) int { return len(u.Email) }) + + importOpts := []store.ExtUser{{ID: "[+] IMPORT ALL"}, {ID: "[<] SKIP ALL"}} + importOpts = append(importOpts, unmatched...) + selected, toImport, err := console.MultiSelectf(importOpts, func(u store.ExtUser) string { + return fmt.Sprintf("%*s %-*s %s", idPad, u.ID, emailPad, u.Email, u.Name) + }, "These users do not have a FireHydrant account. Which users should be created / merged in FireHydrant?") + if err != nil { + return fmt.Errorf("selecting users to import: %w", err) + } + switch selected[0] { + case 0: + console.Successf("[+] All users will be created in FireHydrant.\n") + case 1: + console.Warnf("[<] No users will be created in FireHydrant.\n") + if err := store.UseQueries(ctx).DeleteUnmatchedExtUsers(ctx); err != nil { + return fmt.Errorf("unable to delete unmatched users: %w", err) } - options := []store.FhUser{{Name: "[<] SKIP"}} - options = append(options, fhUsers...) + return nil + default: + console.Warnf("Selected %d users to be imported to FireHydrant.\n", len(toImport)) + } - for _, u := range unmatched { - selected, fhUser, err := console.Selectf(options, func(u store.FhUser) string { - return fmt.Sprintf("%s %s", u.Name, u.Email) - }, fmt.Sprintf("Which FireHydrant user should '%s' be imported to?", u.Name)) - if err != nil { - return fmt.Errorf("selecting FireHydrant user for '%s': %w", u.Name, err) - } - if selected == 0 { - console.Infof("[<] User '%s' will not be imported to FireHydrant.\n", u.Name) - continue - } - if err := store.UseQueries(ctx).LinkExtUser(ctx, store.LinkExtUserParams{ - ID: u.ID, - FhUserID: sql.NullString{String: fhUser.ID, Valid: true}, - }); err != nil { - return fmt.Errorf("linking user '%s': %w", u.Name, err) - } - console.Successf("[=] User '%s' linked to FireHydrant user '%s'.\n", u.Email, fhUser.Email) + // We now ask if all of them are to be created as new users, or if they should be matched to existing FireHydrant users. + console.Warnf("Please match the following users to a FireHydrant account.\n") + fhUsers, err := fh.ListUsers(ctx) + if err != nil { + return fmt.Errorf("unable to list FireHydrant users: %w", err) + } + + namePad := console.PadStrings(fhUsers, func(u store.FhUser) int { return len(u.Name) }) + + matchOpts := []store.FhUser{{Name: "[+] CREATE NEW"}} + matchOpts = append(matchOpts, fhUsers...) + for _, u := range unmatched { + selected, fhUser, err := console.Selectf(matchOpts, func(u store.FhUser) string { + return fmt.Sprintf("%*s %s", namePad, u.Name, u.Email) + }, fmt.Sprintf("Which FireHydrant user should '%s' be imported to?", u.Name)) + if err != nil { + return fmt.Errorf("selecting FireHydrant user for '%s': %w", u.Name, err) + } + if selected == 0 { + console.Infof("[+] User '%s (%s)' will be created in FireHydrant.\n", u.Name, u.Email) + // TODO: Create user in FireHydrant, then link below + continue + } + if err := store.UseQueries(ctx).LinkExtUser(ctx, store.LinkExtUserParams{ + ID: u.ID, + FhUserID: sql.NullString{String: fhUser.ID, Valid: true}, + }); err != nil { + return fmt.Errorf("linking user '%s': %w", u.Name, err) } + console.Successf("[=] User '%s' linked to FireHydrant user '%s'.\n", u.Email, fhUser.Email) } return nil } diff --git a/console/print.go b/console/print.go index a533ee8..c1def2d 100644 --- a/console/print.go +++ b/console/print.go @@ -8,3 +8,13 @@ var ( Errorf = color.New(color.FgHiRed).Add(color.Underline).PrintfFunc() Warnf = color.New(color.FgHiYellow).Add(color.Bold).PrintfFunc() ) + +func PadStrings[T any](elems []T, intFn func(T) int) int { + max := 0 + for _, elem := range elems { + if l := intFn(elem); l > max { + max = l + } + } + return max +} diff --git a/store/queries.sql b/store/queries.sql index 90e8a07..98180eb 100644 --- a/store/queries.sql +++ b/store/queries.sql @@ -27,6 +27,10 @@ SELECT * FROM ext_users; SELECT * FROM ext_users WHERE fh_user_id IS NULL; +-- name: DeleteUnmatchedExtUsers :exec +DELETE FROM ext_users +WHERE fh_user_id IS NULL; + -- name: InsertExtUser :exec INSERT INTO ext_users (id, name, email, fh_user_id) VALUES (?, ?, ?, ?); diff --git a/store/queries.sql.go b/store/queries.sql.go index 5247f53..80e2468 100644 --- a/store/queries.sql.go +++ b/store/queries.sql.go @@ -19,6 +19,16 @@ func (q *Queries) DeleteExtEscalationPolicyUnimported(ctx context.Context) error return err } +const deleteUnmatchedExtUsers = `-- name: DeleteUnmatchedExtUsers :exec +DELETE FROM ext_users +WHERE fh_user_id IS NULL +` + +func (q *Queries) DeleteUnmatchedExtUsers(ctx context.Context) error { + _, err := q.db.ExecContext(ctx, deleteUnmatchedExtUsers) + return err +} + const getExtSchedule = `-- name: GetExtSchedule :one SELECT id, name, description, timezone, strategy, shift_duration, start_time, handoff_time, handoff_day FROM ext_schedules WHERE id = ? `