From 29adcd4e48577bd2160a3c0b2eef4c8a09280eaa Mon Sep 17 00:00:00 2001 From: eleijonmarck Date: Wed, 20 Jul 2022 14:02:15 +0100 Subject: [PATCH] Command: add conflict merge user command - MergeUser will - replace all user_ids from conflicting users to the chosen userId - delete users whose user_ids are not the chosen user - SameIdentification will - update chosen user with chosen email,login details - delete users whose user_ids are not the chosen user --- .../commands/conflict_user_command.go | 48 +++++++------ .../commands/conflict_user_command_test.go | 67 ++++++++++++++++++- pkg/services/sqlstore/user.go | 60 ++++++++++++++--- 3 files changed, 145 insertions(+), 30 deletions(-) diff --git a/pkg/cmd/grafana-cli/commands/conflict_user_command.go b/pkg/cmd/grafana-cli/commands/conflict_user_command.go index 286117771b9..4fc95b2b6f8 100644 --- a/pkg/cmd/grafana-cli/commands/conflict_user_command.go +++ b/pkg/cmd/grafana-cli/commands/conflict_user_command.go @@ -111,32 +111,40 @@ func runConflictingUsersCommand() func(context *cli.Context) error { cUser.Print() // waiting for user to choose which user to merge to - logger.Infof("Choose which user to merge into:") - scanner := bufio.NewScanner(os.Stdin) - if ok := scanner.Scan(); !ok { - if err := scanner.Err(); err != nil { - return fmt.Errorf("can't read conflict option from stdin: %w", err) - } - return fmt.Errorf("can't read conflict option from stdin") - } - choosenUserToMergeInto := scanner.Text() - if !strings.Contains(cUser.Ids, choosenUserToMergeInto) { - return fmt.Errorf("not a conflicting user id") - } - v, err := strconv.ParseInt(choosenUserToMergeInto, 10, 64) + chosenUser, err := promptToMerge(cUser) if err != nil { - return fmt.Errorf("could not parse id from string") + return err } + otherUsers := cUser.Ids - logger.Infof("this will merge users %d into the choosen user %s\n\n", otherUsers, choosenUserToMergeInto) + logger.Infof("this will merge users %s into the chosen user %d\n\n", otherUsers, chosenUser) if confirm() { - err = mergeUser(v, cUser, sqlStore) + err = mergeUser(context.Context, chosenUser, cUser, sqlStore) if err != nil { - return fmt.Errorf("couldnt merge user with error %w", err) + return fmt.Errorf("could not merge user with error %w", err) } } case SameIdentification: - return fmt.Errorf("have not implemented ways to deal with non mergeable users") + // waiting for user to choose which user to merge to + chosenUser, err := promptToMerge(cUser) + if err != nil { + return err + } + err = sqlStore.UpdateUser(context.Context, &models.UpdateUserCommand{UserId: chosenUser}) + if err != nil { + return fmt.Errorf("could not update user with details %w", err) + } + otherUsers := strings.Split(cUser.Ids, ",") + for _, oUser := range otherUsers { + oUser, err := strconv.ParseInt(oUser, 10, 64) + if err != nil { + return err + } + err = sqlStore.DeleteUser(context.Context, &models.DeleteUserCommand{UserId: oUser}) + if err != nil { + return fmt.Errorf("could not update user with details %w", err) + } + } default: logger.Infof("could not identify the conflict resolution for found users %s", cUser.Ids) continue @@ -237,7 +245,7 @@ func (cUser ConflictingUsers) Conflict() conflictType { return cType } -func mergeUser(mergeIntoUser int64, cUser ConflictingUsers, sqlStore *sqlstore.SQLStore) error { +func mergeUser(ctx context.Context, mergeIntoUser int64, cUser ConflictingUsers, sqlStore *sqlstore.SQLStore) error { stringIds := strings.Split(cUser.Ids, ",") fromUserIds := make([]int64, 0, len(stringIds)) for _, raw := range stringIds { @@ -247,7 +255,7 @@ func mergeUser(mergeIntoUser int64, cUser ConflictingUsers, sqlStore *sqlstore.S } fromUserIds = append(fromUserIds, v) } - return sqlStore.MergeUser(mergeIntoUser, fromUserIds) + return sqlStore.MergeUser(ctx, mergeIntoUser, fromUserIds) } type ConflictingUsers struct { diff --git a/pkg/cmd/grafana-cli/commands/conflict_user_command_test.go b/pkg/cmd/grafana-cli/commands/conflict_user_command_test.go index 1b8efac87d4..c0a33969a88 100644 --- a/pkg/cmd/grafana-cli/commands/conflict_user_command_test.go +++ b/pkg/cmd/grafana-cli/commands/conflict_user_command_test.go @@ -46,11 +46,74 @@ func TestUserManagerListConflictingUsers(t *testing.T) { } _, err = sqlStore.CreateUser(context.Background(), dupUserLogincmd) require.NoError(t, err) - // require metrics and statistics to be 2 m, err := GetUsersWithConflictingEmailsOrLogins(context.Background(), sqlStore) - t.Logf("%v+", m) require.NoError(t, err) require.Equal(t, 1, len(m)) } }) } + +func TestMergeUser(t *testing.T) { + t.Run("should be able to merge user", func(t *testing.T) { + // Restore after destructive operation + sqlStore := sqlstore.InitTestDB(t) + + const testOrgID int64 = 1 + // "Skipping conflicting users test for mysql as it does make unique constraint case insensitive by default + if sqlStore.GetDialect().DriverName() != "mysql" { + + // setup + dupUserEmailcmd := user.CreateUserCommand{ + Email: "USERDUPLICATETEST1@TEST.COM", + Name: "user name 1", + Login: "USER_DUPLICATE_TEST_1_LOGIN", + OrgID: testOrgID, + } + userWithUpperCase, err := sqlStore.CreateUser(context.Background(), dupUserEmailcmd) + require.NoError(t, err) + + // add additional user with conflicting login where DOMAIN is upper case + dupUserLogincmd := user.CreateUserCommand{ + Email: "userduplicatetest1@test.com", + Name: "user name 1", + Login: "user_duplicate_test_1_login", + OrgID: testOrgID, + } + userWithLowerCase, err := sqlStore.CreateUser(context.Background(), dupUserLogincmd) + require.NoError(t, err) + // fromUser should be replaced by userWithLowerCase + team1, err := sqlStore.CreateTeam("group1 name", "test1@test.com", testOrgID) + require.NoError(t, err) + err = sqlStore.AddTeamMember(userWithUpperCase.ID, testOrgID, team1.Id, false, 0) + require.NoError(t, err) + // setup finished + + m, err := GetUsersWithConflictingEmailsOrLogins(context.Background(), sqlStore) + require.NoError(t, err) + mergeErr := mergeUser(context.Background(), userWithLowerCase.ID, m[0], sqlStore) + require.NoError(t, mergeErr) + + // start test + // fromUser should be deleted after merger + t.Logf("testing getting user") + query := &models.GetUserByIdQuery{Id: userWithUpperCase.ID} + err = sqlStore.GetUserById(context.Background(), query) + require.Error(t, models.ErrUserNotFound, err) + + testUser := &models.SignedInUser{ + OrgId: testOrgID, + Permissions: map[int64]map[string][]string{ + 1: { + ac.ActionTeamsRead: []string{ac.ScopeTeamsAll}, + ac.ActionOrgUsersRead: []string{ac.ScopeUsersAll}, + }, + }, + } + t.Logf("testing getting team member") + q1 := &models.GetTeamMembersQuery{OrgId: testOrgID, TeamId: team1.Id, SignedInUser: testUser} + err = sqlStore.GetTeamMembers(context.Background(), q1) + require.NoError(t, err) + require.Equal(t, 0, len(q1.Result)) + } + }) +} diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index c5d76619b02..57dfeece19d 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -743,6 +743,10 @@ func (ss *SQLStore) DeleteUser(ctx context.Context, cmd *models.DeleteUserComman }) } +func (ss *SQLStore) DeleteUserInSession(ctx context.Context, sess *DBSession, cmd *models.DeleteUserCommand) error { + return deleteUserInTransaction(ss, sess, cmd) +} + func deleteUserInTransaction(ss *SQLStore, sess *DBSession, cmd *models.DeleteUserCommand) error { // Check if user exists user := user.User{ID: cmd.UserId} @@ -818,26 +822,66 @@ func UserDeletions() []string { return deletes } -func updateAllTablesForFromUser(intoUser user.User, fromUserId int64) error { +func UserUpdates() []string { + deletes := []string{ + "UPDATE star set email = ?, user_id = ?, login = ? WHERE user_id = ?", + "UPDATE team_member set user_id = ? WHERE user_id = ?", + "DELETE FROM team_member WHERE user_id = ?", + "UPDATE FROM " + dialect.Quote("user") + " WHERE id = ?", + "UPDATE FROM org_user WHERE user_id = ?", + "DELETE FROM dashboard_acl WHERE user_id = ?", + "DELETE FROM preferences WHERE user_id = ?", + "DELETE FROM team_member WHERE user_id = ?", + "DELETE FROM user_auth WHERE user_id = ?", + "DELETE FROM user_auth_token WHERE user_id = ?", + "DELETE FROM quota WHERE user_id = ?", + } + return deletes +} + +func (ss *SQLStore) updateUserIds(intoUser user.User, fromUser user.User, sess *DBSession) error { // TODO: - fmt.Printf("merging user %d into user %d", fromUserId, intoUser.ID) + sql := ` + UPDATE team_member + SET user_id = ? + WHERE user_id = ?; + + UPDATE user_role + SET user_id = ? + WHERE user_id = ?; + ` + _, err := sess.Exec(sql, intoUser.ID, fromUser.ID) + if err != nil { + return err + } return nil } -// UpdateUserPermissions sets the user Server Admin flag -func (ss *SQLStore) MergeUser(intoUserId int64, fromUserIds []int64) error { - return ss.WithTransactionalDbSession(context.Background(), func(sess *DBSession) error { - +// MergeUser sets the user Server Admin flag +func (ss *SQLStore) MergeUser(ctx context.Context, intoUserId int64, fromUserIds []int64) error { + return ss.WithTransactionalDbSession(ctx, func(sess *DBSession) error { var intoUser user.User if _, err := sess.ID(intoUserId).Where(notServiceAccountFilter(ss)).Get(&intoUser); err != nil { return err } for _, fromUserId := range fromUserIds { + var fromUser user.User + if _, err := sess.ID(fromUserId).Where(notServiceAccountFilter(ss)).Get(&fromUser); err != nil { + return err + } // update all tables fromUserIds to intoUserIds - updateAllTablesForFromUser(intoUser, fromUserId) - } + err := ss.updateUserIds(intoUser, fromUser, sess) + if err != nil { + return err + } + // deletes the from user + delErr := ss.DeleteUserInSession(ctx, sess, &models.DeleteUserCommand{UserId: fromUserId}) + if delErr != nil { + return delErr + } + } return nil }) }