diff --git a/cmd/mattermost/commands/user.go b/cmd/mattermost/commands/user.go index 8e430735bc..f08194d49d 100644 --- a/cmd/mattermost/commands/user.go +++ b/cmd/mattermost/commands/user.go @@ -46,12 +46,13 @@ var UserCreateCmd = &cobra.Command{ } var UserConvertCmd = &cobra.Command{ - Use: "convert [emails, usernames, userIds] --bot", - Short: "Convert users to bots", - Long: "Convert users to bots", - Example: ` user convert user@example.com anotherUser --bot`, - Args: cobra.MinimumNArgs(1), - RunE: userConvertCmdF, + Use: "convert [emails, usernames, userIds] --bot", + Short: "Convert users to bots, or a bot to a user", + Long: "Convert users to bots, or a bot to a user", + Example: ` user convert user@example.com anotherUser --bot + user convert botusername --email new.email@email.com --password password --user`, + Args: cobra.MinimumNArgs(1), + RunE: userConvertCmdF, } var UserInviteCmd = &cobra.Command{ @@ -172,6 +173,15 @@ func init() { UserCreateCmd.Flags().Bool("system_admin", false, "Optional. If supplied, the new user will be a system administrator. Defaults to false.") UserConvertCmd.Flags().Bool("bot", false, "If supplied, convert users to bots.") + UserConvertCmd.Flags().Bool("user", false, "If supplied, convert a bot to a user.") + UserConvertCmd.Flags().String("password", "", "The password for converted new user account. Required when \"user\" flag is set.") + UserConvertCmd.Flags().String("username", "", "Username for the converted user account. Ignored when \"user\" flag is missing.") + UserConvertCmd.Flags().String("email", "", "The email address for the converted user account. Ignored when \"user\" flag is missing.") + UserConvertCmd.Flags().String("nickname", "", "The nickname for the converted user account. Ignored when \"user\" flag is missing.") + UserConvertCmd.Flags().String("firstname", "", "The first name for the converted user account. Ignored when \"user\" flag is missing.") + UserConvertCmd.Flags().String("lastname", "", "The last name for the converted user account. Ignored when \"user\" flag is missing.") + UserConvertCmd.Flags().String("locale", "", "The locale (ex: en, fr) for converted new user account. Ignored when \"user\" flag is missing.") + UserConvertCmd.Flags().Bool("system_admin", false, "If supplied, the converted user will be a system administrator. Defaults to false. Ignored when \"user\" flag is missing.") DeleteUserCmd.Flags().Bool("confirm", false, "Confirm you really want to delete the user and a DB backup has been performed.") @@ -398,6 +408,126 @@ func usersToBots(args []string, a *app.App) { } } +func getUpdatedPassword(command *cobra.Command, a *app.App, user *model.User) (string, error) { + password, err := command.Flags().GetString("password") + if err != nil { + return "", fmt.Errorf("Unable to read password. Error: %s", err.Error()) + } + + if password == "" { + return "", errors.New("Password is required.") + } + + return password, nil +} + +func getUpdatedUserModel(command *cobra.Command, a *app.App, user *model.User) (*model.User, error) { + username, _ := command.Flags().GetString("username") + if username == "" { + if user.Username == "" { + return nil, errors.New("Invalid username. Username is empty.") + } + } else { + user.Username = username + } + + email, _ := command.Flags().GetString("email") + if email == "" { + if user.Email == "" { + return nil, errors.New("Invalid email. Email is empty.") + } + } else { + user.Email = email + } + + nickname, _ := command.Flags().GetString("nickname") + if nickname != "" { + user.Nickname = nickname + } + + firstname, _ := command.Flags().GetString("firstname") + if firstname != "" { + user.FirstName = firstname + } + + lastname, _ := command.Flags().GetString("lastname") + if lastname != "" { + user.LastName = lastname + } + + locale, _ := command.Flags().GetString("locale") + if locale != "" { + user.Locale = locale + } + + if !user.IsLDAPUser() && !user.IsSAMLUser() && !app.CheckUserDomain(user, *a.Config().TeamSettings.RestrictCreationToDomains) { + return nil, errors.New("The email does not belong to an accepted domain.") + } + + return user, nil +} + +func botToUser(command *cobra.Command, args []string, a *app.App) error { + if len(args) != 1 { + return errors.New("Expect 1 argument. See help text for more details.") + } + + user := getUserFromUserArg(a, args[0]) + if user == nil { + return errors.New("Unable to find bot.") + } + + _, appErr := a.GetBot(user.Id, false) + if appErr != nil { + return fmt.Errorf("Unable to find bot. Error: %s", appErr.Error()) + } + + password, err := getUpdatedPassword(command, a, user) + if err != nil { + return err + } + + user, err = getUpdatedUserModel(command, a, user) + if err != nil { + return err + } + + user, appErr = a.UpdateUser(user, false) + if appErr != nil { + return fmt.Errorf("Unable to update user. Error: %s" + appErr.Error()) + } + + appErr = a.UpdatePassword(user, password) + if appErr != nil { + return fmt.Errorf("Unable to update password. Error: %s", appErr.Error()) + } + + systemAdmin, _ := command.Flags().GetBool("system_admin") + if systemAdmin && !user.IsInRole(model.SYSTEM_ADMIN_ROLE_ID) { + if _, appErr = a.UpdateUserRoles( + user.Id, + fmt.Sprintf("%s %s", user.Roles, model.SYSTEM_ADMIN_ROLE_ID), + false); appErr != nil { + return fmt.Errorf("Unable to make user system admin. Error: %s" + appErr.Error()) + } + } + + result := <-a.Srv.Store.Bot().PermanentDelete(user.Id) + if result.Err != nil { + return fmt.Errorf("Unable to delete bot. Error: %s", result.Err.Error()) + } + + CommandPrettyPrintln("id: " + user.Id) + CommandPrettyPrintln("username: " + user.Username) + CommandPrettyPrintln("email: " + user.Email) + CommandPrettyPrintln("nickname: " + user.Nickname) + CommandPrettyPrintln("first_name: " + user.FirstName) + CommandPrettyPrintln("last_name: " + user.LastName) + CommandPrettyPrintln("roles: " + user.Roles) + CommandPrettyPrintln("locale: " + user.Locale) + return nil +} + func userConvertCmdF(command *cobra.Command, args []string) error { a, err := InitDBCommandContextCobra(command) if err != nil { @@ -410,8 +540,21 @@ func userConvertCmdF(command *cobra.Command, args []string) error { return errors.New("Invalid command. See help text for details.") } - if !toBot { - return errors.New("Expect \"bot\" flag to be set. See help text for details.") + toUser, err := command.Flags().GetBool("user") + if err != nil { + return errors.New("Invalid command. See help text for details.") + } + + if !(toUser || toBot) { + return errors.New("Expect either \"user\" flag or \"bot\" flag. See help text for details.") + } + + if toUser && toBot { + return errors.New("Expect either \"user\" flag or \"bot\" flag but not both. See help text for details.") + } + + if toUser { + return botToUser(command, args, a) } usersToBots(args, a) diff --git a/cmd/mattermost/commands/user_test.go b/cmd/mattermost/commands/user_test.go index 33920c87ce..7be243843e 100644 --- a/cmd/mattermost/commands/user_test.go +++ b/cmd/mattermost/commands/user_test.go @@ -117,11 +117,77 @@ func TestChangeUserEmail(t *testing.T) { } -func TestConvertUserToBot(t *testing.T) { +func TestConvertUser(t *testing.T) { th := Setup().InitBasic() defer th.TearDown() - th.CheckCommand(t, "user", "convert", th.BasicUser.Username, "anotherinvaliduser", "--bot") - result := <-th.App.Srv.Store.Bot().Get(th.BasicUser.Id, false) - require.Nil(t, result.Err) + t.Run("Invalid command line input", func(t *testing.T) { + err := th.RunCommand(t, "user", "convert", th.BasicUser.Username) + require.NotNil(t, err) + + err = th.RunCommand(t, "user", "convert", th.BasicUser.Username, "--user", "--bot") + require.NotNil(t, err) + + err = th.RunCommand(t, "user", "convert", "--bot") + require.NotNil(t, err) + }) + + t.Run("Convert to bot from username", func(t *testing.T) { + th.CheckCommand(t, "user", "convert", th.BasicUser.Username, "anotherinvaliduser", "--bot") + result := <-th.App.Srv.Store.Bot().Get(th.BasicUser.Id, false) + require.Nil(t, result.Err) + }) + + t.Run("Unable to convert to user with missing password", func(t *testing.T) { + err := th.RunCommand(t, "user", "convert", th.BasicUser.Username, "--user") + require.NotNil(t, err) + }) + + t.Run("Unable to convert to user with invalid email", func(t *testing.T) { + err := th.RunCommand(t, "user", "convert", th.BasicUser.Username, "--user", + "--password", "password", + "--email", "invalidEmail") + require.NotNil(t, err) + }) + + t.Run("Convert to user with minimum flags", func(t *testing.T) { + err := th.RunCommand(t, "user", "convert", th.BasicUser.Username, "--user", + "--password", "password") + require.Nil(t, err) + result := <-th.App.Srv.Store.Bot().Get(th.BasicUser.Id, false) + require.NotNil(t, result.Err) + }) + + t.Run("Convert to bot from email", func(t *testing.T) { + th.CheckCommand(t, "user", "convert", th.BasicUser2.Email, "--bot") + result := <-th.App.Srv.Store.Bot().Get(th.BasicUser2.Id, false) + require.Nil(t, result.Err) + }) + + t.Run("Convert to user with all flags", func(t *testing.T) { + err := th.RunCommand(t, "user", "convert", th.BasicUser2.Username, "--user", + "--password", "password", + "--username", "newusername", + "--email", "valid@email.com", + "--nickname", "newNickname", + "--firstname", "newFirstName", + "--lastname", "newLastName", + "--locale", "en_CA", + "--system_admin") + require.Nil(t, err) + + result := <-th.App.Srv.Store.Bot().Get(th.BasicUser2.Id, false) + require.NotNil(t, result.Err) + + user, appErr := th.App.Srv.Store.User().Get(th.BasicUser2.Id) + require.Nil(t, appErr) + require.Equal(t, "newusername", user.Username) + require.Equal(t, "valid@email.com", user.Email) + require.Equal(t, "newNickname", user.Nickname) + require.Equal(t, "newFirstName", user.FirstName) + require.Equal(t, "newLastName", user.LastName) + require.Equal(t, "en_CA", user.Locale) + require.True(t, user.IsInRole("system_admin")) + }) + } diff --git a/store/sqlstore/bot_store.go b/store/sqlstore/bot_store.go index 9557be6865..7be1a8fe68 100644 --- a/store/sqlstore/bot_store.go +++ b/store/sqlstore/bot_store.go @@ -233,12 +233,6 @@ func (us SqlBotStore) Update(bot *model.Bot) store.StoreChannel { // If the corresponding user is to be deleted, it must be done via the user store. func (us SqlBotStore) PermanentDelete(botUserId string) store.StoreChannel { return store.Do(func(result *store.StoreResult) { - userResult := <-us.User().PermanentDelete(botUserId) - if userResult.Err != nil { - result.Err = userResult.Err - return - } - if _, err := us.GetMaster().Exec(` DELETE FROM Bots diff --git a/store/storetest/bot_store.go b/store/storetest/bot_store.go index debb8b881e..e090593c87 100644 --- a/store/storetest/bot_store.go +++ b/store/storetest/bot_store.go @@ -48,6 +48,7 @@ func testBotStoreGet(t *testing.T, ss store.Store) { DeleteAt: 0, }) store.Must(ss.Bot().PermanentDelete(permanentlyDeletedBot.UserId)) + defer func() { store.Must(ss.User().PermanentDelete(permanentlyDeletedBot.UserId)) }() b1, _ := makeBotWithUser(ss, &model.Bot{ Username: "b1", @@ -123,6 +124,7 @@ func testBotStoreGetAll(t *testing.T, ss store.Store) { DeleteAt: 0, }) store.Must(ss.Bot().PermanentDelete(permanentlyDeletedBot.UserId)) + defer func() { store.Must(ss.User().PermanentDelete(permanentlyDeletedBot.UserId)) }() b1, _ := makeBotWithUser(ss, &model.Bot{ Username: "b1",