diff --git a/src/main/java/fr/xephi/authme/api/v3/AuthMeApi.java b/src/main/java/fr/xephi/authme/api/v3/AuthMeApi.java index a23883c6..0e1e099e 100644 --- a/src/main/java/fr/xephi/authme/api/v3/AuthMeApi.java +++ b/src/main/java/fr/xephi/authme/api/v3/AuthMeApi.java @@ -285,6 +285,16 @@ public class AuthMeApi { management.performUnregisterByAdmin(null, name, Bukkit.getPlayer(name)); } + /** + * Change a user's password + * + * @param name the user name + * @param newPassword the new password + */ + public void changePassword(String name, String newPassword) { + management.performPasswordChangeAsAdmin(null, name, newPassword); + } + /** * Get all the registered names (lowercase) * diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/ChangePasswordAdminCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/ChangePasswordAdminCommand.java index e12f4d38..3c6264cc 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/ChangePasswordAdminCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/ChangePasswordAdminCommand.java @@ -1,13 +1,7 @@ package fr.xephi.authme.command.executable.authme; -import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.command.ExecutableCommand; -import fr.xephi.authme.data.auth.PlayerCache; -import fr.xephi.authme.datasource.DataSource; -import fr.xephi.authme.message.MessageKey; -import fr.xephi.authme.security.PasswordSecurity; -import fr.xephi.authme.security.crypts.HashedPassword; -import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.process.Management; import fr.xephi.authme.service.CommonService; import fr.xephi.authme.service.ValidationService; import fr.xephi.authme.service.ValidationService.ValidationResult; @@ -21,26 +15,17 @@ import java.util.List; */ public class ChangePasswordAdminCommand implements ExecutableCommand { - @Inject - private PasswordSecurity passwordSecurity; - - @Inject - private PlayerCache playerCache; - - @Inject - private DataSource dataSource; - - @Inject - private BukkitService bukkitService; - @Inject private ValidationService validationService; @Inject private CommonService commonService; + @Inject + private Management management; + @Override - public void executeCommand(final CommandSender sender, List arguments) { + public void executeCommand(CommandSender sender, List arguments) { // Get the player and password final String playerName = arguments.get(0); final String playerPass = arguments.get(1); @@ -49,36 +34,8 @@ public class ChangePasswordAdminCommand implements ExecutableCommand { ValidationResult validationResult = validationService.validatePassword(playerPass, playerName); if (validationResult.hasError()) { commonService.send(sender, validationResult.getMessageKey(), validationResult.getArgs()); - return; - } - - // Set the password - bukkitService.runTaskOptionallyAsync(() -> changePassword(playerName.toLowerCase(), playerPass, sender)); - } - - /** - * Changes the password of the given player to the given password. - * - * @param nameLowercase the name of the player - * @param password the password to set - * @param sender the sender initiating the password change - */ - private void changePassword(String nameLowercase, String password, CommandSender sender) { - if (!isNameRegistered(nameLowercase)) { - commonService.send(sender, MessageKey.UNKNOWN_USER); - return; - } - - HashedPassword hashedPassword = passwordSecurity.computeHash(password, nameLowercase); - if (dataSource.updatePassword(nameLowercase, hashedPassword)) { - commonService.send(sender, MessageKey.PASSWORD_CHANGED_SUCCESS); - ConsoleLogger.info(sender.getName() + " changed password of " + nameLowercase); } else { - commonService.send(sender, MessageKey.ERROR); + management.performPasswordChangeAsAdmin(sender, playerName, playerPass); } } - - private boolean isNameRegistered(String nameLowercase) { - return playerCache.isAuthenticated(nameLowercase) || dataSource.isAuthAvailable(nameLowercase); - } } diff --git a/src/main/java/fr/xephi/authme/process/Management.java b/src/main/java/fr/xephi/authme/process/Management.java index d2707c52..4f0743ed 100644 --- a/src/main/java/fr/xephi/authme/process/Management.java +++ b/src/main/java/fr/xephi/authme/process/Management.java @@ -94,6 +94,10 @@ public class Management { runTask(() -> asyncChangePassword.changePassword(player, oldPassword, newPassword)); } + public void performPasswordChangeAsAdmin(CommandSender sender, String playerName, String newPassword) { + runTask(() -> asyncChangePassword.changePasswordAsAdmin(sender, playerName, newPassword)); + } + private void runTask(Runnable runnable) { bukkitService.runTaskOptionallyAsync(runnable); } diff --git a/src/main/java/fr/xephi/authme/process/changepassword/AsyncChangePassword.java b/src/main/java/fr/xephi/authme/process/changepassword/AsyncChangePassword.java index 2a3fa175..27e84d24 100644 --- a/src/main/java/fr/xephi/authme/process/changepassword/AsyncChangePassword.java +++ b/src/main/java/fr/xephi/authme/process/changepassword/AsyncChangePassword.java @@ -9,6 +9,7 @@ import fr.xephi.authme.process.AsynchronousProcess; import fr.xephi.authme.service.CommonService; import fr.xephi.authme.security.PasswordSecurity; import fr.xephi.authme.security.crypts.HashedPassword; +import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import javax.inject.Inject; @@ -30,7 +31,13 @@ public class AsyncChangePassword implements AsynchronousProcess { AsyncChangePassword() { } - + /** + * Change password for an online player + * + * @param player the player + * @param oldPassword the old password used by the player + * @param newPassword the new password chosen by the player + */ public void changePassword(final Player player, String oldPassword, String newPassword) { final String name = player.getName().toLowerCase(); PlayerAuth auth = playerCache.getAuth(name); @@ -50,5 +57,38 @@ public class AsyncChangePassword implements AsynchronousProcess { commonService.send(player, MessageKey.WRONG_PASSWORD); } } -} + /** + * Change a user's password as an administrator, without asking for the previous one + * + * @param sender who is performing the operation, null if called by other plugins + * @param playerName the player name + * @param newPassword the new password chosen for the player + */ + public void changePasswordAsAdmin(CommandSender sender, final String playerName, String newPassword) { + final String lowerCaseName = playerName.toLowerCase(); + if (!(playerCache.isAuthenticated(lowerCaseName) || dataSource.isAuthAvailable(lowerCaseName))) { + if (sender == null) { + ConsoleLogger.warning("Tried to change password for user " + lowerCaseName + " but it doesn't exist!"); + } else { + commonService.send(sender, MessageKey.UNKNOWN_USER); + } + return; + } + + HashedPassword hashedPassword = passwordSecurity.computeHash(newPassword, lowerCaseName); + if (dataSource.updatePassword(lowerCaseName, hashedPassword)) { + if (sender != null) { + commonService.send(sender, MessageKey.PASSWORD_CHANGED_SUCCESS); + ConsoleLogger.info(sender.getName() + " changed password of " + lowerCaseName); + } else { + ConsoleLogger.info("Changed password of " + lowerCaseName); + } + } else { + if (sender != null) { + commonService.send(sender, MessageKey.ERROR); + } + ConsoleLogger.warning("An error occurred while changing password for user " + lowerCaseName + "!"); + } + } +} diff --git a/src/test/java/fr/xephi/authme/api/v3/AuthMeApiTest.java b/src/test/java/fr/xephi/authme/api/v3/AuthMeApiTest.java index 0fadd3f1..d1ddaf73 100644 --- a/src/test/java/fr/xephi/authme/api/v3/AuthMeApiTest.java +++ b/src/test/java/fr/xephi/authme/api/v3/AuthMeApiTest.java @@ -280,6 +280,19 @@ public class AuthMeApiTest { verify(management).performUnregisterByAdmin(null, name, player); } + @Test + public void shouldChangePassword() { + // given + String name = "Bobby12"; + String password = "resetPw!"; + + // when + api.changePassword(name, password); + + // then + verify(management).performPasswordChangeAsAdmin(null, name, password); + } + private static Player mockPlayerWithName(String name) { Player player = mock(Player.class); given(player.getName()).willReturn(name); diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/ChangePasswordAdminCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/ChangePasswordAdminCommandTest.java index 9e957a62..b10b9c45 100644 --- a/src/test/java/fr/xephi/authme/command/executable/authme/ChangePasswordAdminCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/authme/ChangePasswordAdminCommandTest.java @@ -1,18 +1,11 @@ package fr.xephi.authme.command.executable.authme; -import fr.xephi.authme.TestHelper; -import fr.xephi.authme.data.auth.PlayerAuth; -import fr.xephi.authme.data.auth.PlayerCache; -import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.message.MessageKey; -import fr.xephi.authme.security.PasswordSecurity; -import fr.xephi.authme.security.crypts.HashedPassword; -import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.process.Management; import fr.xephi.authme.service.CommonService; import fr.xephi.authme.service.ValidationService; import fr.xephi.authme.service.ValidationService.ValidationResult; import org.bukkit.command.CommandSender; -import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; @@ -21,11 +14,8 @@ import org.mockito.junit.MockitoJUnitRunner; import java.util.Arrays; -import static fr.xephi.authme.TestHelper.runOptionallyAsyncTask; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; @@ -39,133 +29,45 @@ public class ChangePasswordAdminCommandTest { private ChangePasswordAdminCommand command; @Mock - private CommonService service; - - @Mock - private PasswordSecurity passwordSecurity; - - @Mock - private DataSource dataSource; - - @Mock - private PlayerCache playerCache; - - @Mock - private BukkitService bukkitService; + private CommonService commonService; @Mock private ValidationService validationService; - @BeforeClass - public static void setUpLogger() { - TestHelper.setupLogger(); + @Mock + private Management management; + + @Test + public void shouldForwardRequestToManagement() { + // given + String name = "theUser"; + String pass = "newPassword"; + given(validationService.validatePassword(pass, name)).willReturn(new ValidationResult()); + CommandSender sender = mock(CommandSender.class); + + // when + command.executeCommand(sender, Arrays.asList(name, pass)); + + // then + verify(validationService).validatePassword(pass, name); + verify(management).performPasswordChangeAsAdmin(sender, name, pass); } @Test - public void shouldRejectInvalidPassword() { + public void shouldSendErrorToCommandSender() { // given + String name = "theUser"; + String pass = "newPassword"; + given(validationService.validatePassword(pass, name)).willReturn( + new ValidationResult(MessageKey.INVALID_PASSWORD_LENGTH, "7")); CommandSender sender = mock(CommandSender.class); - given(validationService.validatePassword("Bobby", "bobby")).willReturn( - new ValidationResult(MessageKey.PASSWORD_IS_USERNAME_ERROR)); // when - command.executeCommand(sender, Arrays.asList("bobby", "Bobby")); + command.executeCommand(sender, Arrays.asList(name, pass)); // then - verify(validationService).validatePassword("Bobby", "bobby"); - verify(service).send(sender, MessageKey.PASSWORD_IS_USERNAME_ERROR, new String[0]); - verifyZeroInteractions(dataSource); + verify(validationService).validatePassword(pass, name); + verify(commonService).send(sender, MessageKey.INVALID_PASSWORD_LENGTH, "7"); + verifyZeroInteractions(management); } - - @Test - public void shouldRejectCommandForUnknownUser() { - // given - CommandSender sender = mock(CommandSender.class); - String player = "player"; - String password = "password"; - given(playerCache.isAuthenticated(player)).willReturn(false); - given(validationService.validatePassword(password, player)).willReturn(new ValidationResult()); - - // when - command.executeCommand(sender, Arrays.asList(player, password)); - runOptionallyAsyncTask(bukkitService); - - // then - verify(service).send(sender, MessageKey.UNKNOWN_USER); - verify(dataSource, never()).updatePassword(any(PlayerAuth.class)); - } - - @Test - public void shouldUpdatePasswordOfLoggedInUser() { - // given - CommandSender sender = mock(CommandSender.class); - String player = "my_user12"; - String password = "passPass"; - given(playerCache.isAuthenticated(player)).willReturn(true); - - HashedPassword hashedPassword = mock(HashedPassword.class); - given(passwordSecurity.computeHash(password, player)).willReturn(hashedPassword); - given(dataSource.updatePassword(player, hashedPassword)).willReturn(true); - given(validationService.validatePassword(password, player)).willReturn(new ValidationResult()); - - // when - command.executeCommand(sender, Arrays.asList(player, password)); - runOptionallyAsyncTask(bukkitService); - - // then - verify(validationService).validatePassword(password, player); - verify(service).send(sender, MessageKey.PASSWORD_CHANGED_SUCCESS); - verify(passwordSecurity).computeHash(password, player); - verify(dataSource).updatePassword(player, hashedPassword); - } - - @Test - public void shouldUpdatePasswordOfOfflineUser() { - // given - CommandSender sender = mock(CommandSender.class); - String player = "my_user12"; - String password = "passPass"; - given(playerCache.isAuthenticated(player)).willReturn(false); - given(dataSource.isAuthAvailable(player)).willReturn(true); - given(validationService.validatePassword(password, player)).willReturn(new ValidationResult()); - - HashedPassword hashedPassword = mock(HashedPassword.class); - given(passwordSecurity.computeHash(password, player)).willReturn(hashedPassword); - given(dataSource.updatePassword(player, hashedPassword)).willReturn(true); - - // when - command.executeCommand(sender, Arrays.asList(player, password)); - runOptionallyAsyncTask(bukkitService); - - // then - verify(validationService).validatePassword(password, player); - verify(service).send(sender, MessageKey.PASSWORD_CHANGED_SUCCESS); - verify(passwordSecurity).computeHash(password, player); - verify(dataSource).updatePassword(player, hashedPassword); - } - - @Test - public void shouldReportWhenSaveFailed() { - // given - CommandSender sender = mock(CommandSender.class); - String player = "my_user12"; - String password = "passPass"; - given(playerCache.isAuthenticated(player)).willReturn(true); - given(validationService.validatePassword(password, player)).willReturn(new ValidationResult()); - - HashedPassword hashedPassword = mock(HashedPassword.class); - given(passwordSecurity.computeHash(password, player)).willReturn(hashedPassword); - given(dataSource.updatePassword(player, hashedPassword)).willReturn(false); - - // when - command.executeCommand(sender, Arrays.asList(player, password)); - runOptionallyAsyncTask(bukkitService); - - // then - verify(validationService).validatePassword(password, player); - verify(service).send(sender, MessageKey.ERROR); - verify(passwordSecurity).computeHash(password, player); - verify(dataSource).updatePassword(player, hashedPassword); - } - } diff --git a/src/test/java/fr/xephi/authme/process/changepassword/AsyncChangePasswordTest.java b/src/test/java/fr/xephi/authme/process/changepassword/AsyncChangePasswordTest.java new file mode 100644 index 00000000..76633544 --- /dev/null +++ b/src/test/java/fr/xephi/authme/process/changepassword/AsyncChangePasswordTest.java @@ -0,0 +1,127 @@ +package fr.xephi.authme.process.changepassword; + +import fr.xephi.authme.TestHelper; +import fr.xephi.authme.data.auth.PlayerCache; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.security.PasswordSecurity; +import fr.xephi.authme.security.crypts.HashedPassword; +import fr.xephi.authme.service.CommonService; +import org.bukkit.command.CommandSender; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.only; +import static org.mockito.Mockito.verify; + +/** + * Test for {@link AsyncChangePassword}. + */ +@RunWith(MockitoJUnitRunner.class) +public class AsyncChangePasswordTest { + + @InjectMocks + private AsyncChangePassword asyncChangePassword; + + @Mock + private CommonService commonService; + @Mock + private DataSource dataSource; + @Mock + private PlayerCache playerCache; + @Mock + private PasswordSecurity passwordSecurity; + + @Before + public void setUpLogger() { + TestHelper.setupLogger(); + } + + @Test + public void shouldRejectCommandForUnknownUser() { + // given + CommandSender sender = mock(CommandSender.class); + String player = "player"; + String password = "password"; + given(playerCache.isAuthenticated(player)).willReturn(false); + given(dataSource.isAuthAvailable(player)).willReturn(false); + + // when + asyncChangePassword.changePasswordAsAdmin(sender, player, password); + + // then + verify(commonService).send(sender, MessageKey.UNKNOWN_USER); + verify(dataSource, only()).isAuthAvailable(player); + } + + @Test + public void shouldUpdatePasswordOfLoggedInUser() { + // given + CommandSender sender = mock(CommandSender.class); + String player = "my_user12"; + String password = "passPass"; + given(playerCache.isAuthenticated(player)).willReturn(true); + + HashedPassword hashedPassword = mock(HashedPassword.class); + given(passwordSecurity.computeHash(password, player)).willReturn(hashedPassword); + given(dataSource.updatePassword(player, hashedPassword)).willReturn(true); + + // when + asyncChangePassword.changePasswordAsAdmin(sender, player, password); + + // then + verify(commonService).send(sender, MessageKey.PASSWORD_CHANGED_SUCCESS); + verify(passwordSecurity).computeHash(password, player); + verify(dataSource).updatePassword(player, hashedPassword); + } + + @Test + public void shouldUpdatePasswordOfOfflineUser() { + // given + CommandSender sender = mock(CommandSender.class); + String player = "my_user12"; + String password = "passPass"; + given(playerCache.isAuthenticated(player)).willReturn(false); + given(dataSource.isAuthAvailable(player)).willReturn(true); + + HashedPassword hashedPassword = mock(HashedPassword.class); + given(passwordSecurity.computeHash(password, player)).willReturn(hashedPassword); + given(dataSource.updatePassword(player, hashedPassword)).willReturn(true); + + // when + asyncChangePassword.changePasswordAsAdmin(sender, player, password); + + // then + verify(commonService).send(sender, MessageKey.PASSWORD_CHANGED_SUCCESS); + verify(passwordSecurity).computeHash(password, player); + verify(dataSource).updatePassword(player, hashedPassword); + } + + @Test + public void shouldReportWhenSaveFailed() { + // given + CommandSender sender = mock(CommandSender.class); + String player = "my_user12"; + String password = "passPass"; + given(playerCache.isAuthenticated(player)).willReturn(true); + + HashedPassword hashedPassword = mock(HashedPassword.class); + given(passwordSecurity.computeHash(password, player)).willReturn(hashedPassword); + given(dataSource.updatePassword(player, hashedPassword)).willReturn(false); + + // when + asyncChangePassword.changePasswordAsAdmin(sender, player, password); + + // then + verify(commonService).send(sender, MessageKey.ERROR); + verify(passwordSecurity).computeHash(password, player); + verify(dataSource).updatePassword(player, hashedPassword); + } + +}