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
This commit is contained in:
@@ -111,32 +111,40 @@ func runConflictingUsersCommand() func(context *cli.Context) error {
|
|||||||
cUser.Print()
|
cUser.Print()
|
||||||
|
|
||||||
// waiting for user to choose which user to merge to
|
// waiting for user to choose which user to merge to
|
||||||
logger.Infof("Choose which user to merge into:")
|
chosenUser, err := promptToMerge(cUser)
|
||||||
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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("could not parse id from string")
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
otherUsers := cUser.Ids
|
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() {
|
if confirm() {
|
||||||
err = mergeUser(v, cUser, sqlStore)
|
err = mergeUser(context.Context, chosenUser, cUser, sqlStore)
|
||||||
if err != nil {
|
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:
|
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:
|
default:
|
||||||
logger.Infof("could not identify the conflict resolution for found users %s", cUser.Ids)
|
logger.Infof("could not identify the conflict resolution for found users %s", cUser.Ids)
|
||||||
continue
|
continue
|
||||||
@@ -237,7 +245,7 @@ func (cUser ConflictingUsers) Conflict() conflictType {
|
|||||||
return cType
|
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, ",")
|
stringIds := strings.Split(cUser.Ids, ",")
|
||||||
fromUserIds := make([]int64, 0, len(stringIds))
|
fromUserIds := make([]int64, 0, len(stringIds))
|
||||||
for _, raw := range stringIds {
|
for _, raw := range stringIds {
|
||||||
@@ -247,7 +255,7 @@ func mergeUser(mergeIntoUser int64, cUser ConflictingUsers, sqlStore *sqlstore.S
|
|||||||
}
|
}
|
||||||
fromUserIds = append(fromUserIds, v)
|
fromUserIds = append(fromUserIds, v)
|
||||||
}
|
}
|
||||||
return sqlStore.MergeUser(mergeIntoUser, fromUserIds)
|
return sqlStore.MergeUser(ctx, mergeIntoUser, fromUserIds)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ConflictingUsers struct {
|
type ConflictingUsers struct {
|
||||||
|
|||||||
@@ -46,11 +46,74 @@ func TestUserManagerListConflictingUsers(t *testing.T) {
|
|||||||
}
|
}
|
||||||
_, err = sqlStore.CreateUser(context.Background(), dupUserLogincmd)
|
_, err = sqlStore.CreateUser(context.Background(), dupUserLogincmd)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
// require metrics and statistics to be 2
|
|
||||||
m, err := GetUsersWithConflictingEmailsOrLogins(context.Background(), sqlStore)
|
m, err := GetUsersWithConflictingEmailsOrLogins(context.Background(), sqlStore)
|
||||||
t.Logf("%v+", m)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, 1, len(m))
|
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))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 {
|
func deleteUserInTransaction(ss *SQLStore, sess *DBSession, cmd *models.DeleteUserCommand) error {
|
||||||
// Check if user exists
|
// Check if user exists
|
||||||
user := user.User{ID: cmd.UserId}
|
user := user.User{ID: cmd.UserId}
|
||||||
@@ -818,26 +822,66 @@ func UserDeletions() []string {
|
|||||||
return deletes
|
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:
|
// 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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateUserPermissions sets the user Server Admin flag
|
// MergeUser sets the user Server Admin flag
|
||||||
func (ss *SQLStore) MergeUser(intoUserId int64, fromUserIds []int64) error {
|
func (ss *SQLStore) MergeUser(ctx context.Context, intoUserId int64, fromUserIds []int64) error {
|
||||||
return ss.WithTransactionalDbSession(context.Background(), func(sess *DBSession) error {
|
return ss.WithTransactionalDbSession(ctx, func(sess *DBSession) error {
|
||||||
|
|
||||||
var intoUser user.User
|
var intoUser user.User
|
||||||
if _, err := sess.ID(intoUserId).Where(notServiceAccountFilter(ss)).Get(&intoUser); err != nil {
|
if _, err := sess.ID(intoUserId).Where(notServiceAccountFilter(ss)).Get(&intoUser); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, fromUserId := range fromUserIds {
|
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
|
// 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
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user