From 2c21bdec38460b793305079d65247cb548dd440a Mon Sep 17 00:00:00 2001 From: Julien Lancelot Date: Fri, 27 Nov 2020 09:16:06 +0100 Subject: [PATCH] SONAR-14175 SONAR-14176 Detect usage of admin account with default credential SONAR-14175 Add a startup task to detect admin default credential usage and set reset_password flag to true SONAR-14176 Warn administrators when default admin credential is detected --- .../version/v00/PopulateInitialSchema.java | 10 +- ...tiveAdminAccountWithDefaultCredential.java | 76 +++++++++++++ ...AdminAccountWithDefaultCredentialTest.java | 102 ++++++++++++++++++ .../user/ws/ChangePasswordActionTest.java | 6 +- .../platformlevel/PlatformLevelStartup.java | 6 +- 5 files changed, 191 insertions(+), 9 deletions(-) create mode 100644 server/sonar-webserver-core/src/main/java/org/sonar/server/startup/DetectActiveAdminAccountWithDefaultCredential.java create mode 100644 server/sonar-webserver-core/src/test/java/org/sonar/server/startup/DetectActiveAdminAccountWithDefaultCredentialTest.java diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v00/PopulateInitialSchema.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v00/PopulateInitialSchema.java index 0e971237b62..b98a48b763d 100644 --- a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v00/PopulateInitialSchema.java +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v00/PopulateInitialSchema.java @@ -41,6 +41,7 @@ public class PopulateInitialSchema extends DataChange { private static final String ADMINS_GROUP = "sonar-administrators"; private static final String USERS_GROUP = "sonar-users"; private static final String ADMIN_USER = "admin"; + private static final String ADMIN_CRYPTED_PASSWORD = "$2a$12$uCkkXmhW5ThVK8mpBvnXOOJRLd64LJeHTeCkSuB3lfaR2N0AYBaSi"; private static final List ADMIN_ROLES = Arrays.asList("admin", "profileadmin", "gateadmin", "provisioning", "applicationcreator", "portfoliocreator"); private final System2 system2; @@ -78,14 +79,15 @@ public class PopulateInitialSchema extends DataChange { "(uuid, login, name, email, external_id, external_login, external_identity_provider, user_local, crypted_password, salt, hash_method, is_root, onboarded, " + "created_at, updated_at)" + " values " + - "(?, ?, 'Administrator', null, 'admin', 'admin', 'sonarqube', ?, '$2a$12$uCkkXmhW5ThVK8mpBvnXOOJRLd64LJeHTeCkSuB3lfaR2N0AYBaSi', null, 'BCRYPT', ?, ?, ?, ?)") + "(?, ?, 'Administrator', null, 'admin', 'admin', 'sonarqube', ?, ?, null, 'BCRYPT', ?, ?, ?, ?)") .setString(1, uuidFactory.create()) .setString(2, ADMIN_USER) .setBoolean(3, true) - .setBoolean(4, false) - .setBoolean(5, true) - .setLong(6, now) + .setString(4, ADMIN_CRYPTED_PASSWORD) + .setBoolean(5, false) + .setBoolean(6, true) .setLong(7, now) + .setLong(8, now) .execute() .commit(); diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/startup/DetectActiveAdminAccountWithDefaultCredential.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/startup/DetectActiveAdminAccountWithDefaultCredential.java new file mode 100644 index 00000000000..09249a02a36 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/startup/DetectActiveAdminAccountWithDefaultCredential.java @@ -0,0 +1,76 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +package org.sonar.server.startup; + +import org.picocontainer.Startable; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.user.UserDto; +import org.sonar.server.authentication.CredentialsLocalAuthentication; +import org.sonar.server.authentication.event.AuthenticationEvent; +import org.sonar.server.authentication.event.AuthenticationException; + +/** + * Detect usage of an active admin account with default credential in order to ask this account to reset its password during authentication. + */ +public class DetectActiveAdminAccountWithDefaultCredential implements Startable { + + private static final Logger LOGGER = Loggers.get(DetectActiveAdminAccountWithDefaultCredential.class); + + private final DbClient dbClient; + private final CredentialsLocalAuthentication localAuthentication; + + public DetectActiveAdminAccountWithDefaultCredential(DbClient dbClient, CredentialsLocalAuthentication localAuthentication) { + this.dbClient = dbClient; + this.localAuthentication = localAuthentication; + } + + @Override + public void start() { + try (DbSession dbSession = dbClient.openSession(false)) { + UserDto admin = dbClient.userDao().selectActiveUserByLogin(dbSession, "admin"); + if (admin == null || !isDefaultCredentialUser(dbSession, admin)) { + return; + } + LOGGER.warn("*******************************************************************************************************************"); + LOGGER.warn("Default Administrator credentials are still being used. Make sure to change the password or deactivate the account."); + LOGGER.warn("*******************************************************************************************************************"); + dbClient.userDao().update(dbSession, admin.setResetPassword(true)); + dbSession.commit(); + } + } + + private boolean isDefaultCredentialUser(DbSession dbSession, UserDto user) { + try { + localAuthentication.authenticate(dbSession, user, "admin", AuthenticationEvent.Method.BASIC); + return true; + } catch (AuthenticationException ex) { + return false; + } + } + + @Override + public void stop() { + // Nothing to do + } +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/startup/DetectActiveAdminAccountWithDefaultCredentialTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/startup/DetectActiveAdminAccountWithDefaultCredentialTest.java new file mode 100644 index 00000000000..5fee1484965 --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/startup/DetectActiveAdminAccountWithDefaultCredentialTest.java @@ -0,0 +1,102 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +package org.sonar.server.startup; + +import org.junit.After; +import org.junit.Rule; +import org.junit.Test; +import org.sonar.api.utils.System2; +import org.sonar.api.utils.log.LogTester; +import org.sonar.api.utils.log.LoggerLevel; +import org.sonar.db.DbTester; +import org.sonar.db.user.UserDto; +import org.sonar.server.authentication.CredentialsLocalAuthentication; + +import static org.assertj.core.api.Assertions.assertThat; + +public class DetectActiveAdminAccountWithDefaultCredentialTest { + + private static final String ADMIN_LOGIN = "admin"; + + @Rule + public DbTester db = DbTester.create(System2.INSTANCE); + @Rule + public LogTester logTester = new LogTester(); + + private final CredentialsLocalAuthentication localAuthentication = new CredentialsLocalAuthentication(db.getDbClient()); + + private final DetectActiveAdminAccountWithDefaultCredential underTest = new DetectActiveAdminAccountWithDefaultCredential(db.getDbClient(), localAuthentication); + + @After + public void after() { + underTest.stop(); + } + + @Test + public void set_reset_flag_to_true_and_add_log_when_admin_account_with_default_credential_is_detected() { + UserDto admin = db.users().insertUser(u -> u.setLogin(ADMIN_LOGIN)); + changePassword(admin, "admin"); + + underTest.start(); + + assertThat(db.users().selectUserByLogin(admin.getLogin()).get().isResetPassword()).isTrue(); + assertThat(logTester.logs(LoggerLevel.WARN)).contains("Default Administrator credentials are still being used. Make sure to change the password or deactivate the account."); + } + + @Test + public void do_nothing_when_admin_is_not_using_default_credential() { + UserDto admin = db.users().insertUser(u -> u.setLogin(ADMIN_LOGIN)); + changePassword(admin, "something_else"); + + underTest.start(); + + assertThat(db.users().selectUserByLogin(admin.getLogin()).get().isResetPassword()).isFalse(); + assertThat(logTester.logs()).isEmpty(); + } + + @Test + public void do_nothing_when_no_admin_account_with_default_credential_detected() { + UserDto otherUser = db.users().insertUser(); + changePassword(otherUser, "admin"); + + underTest.start(); + + assertThat(db.users().selectUserByLogin(otherUser.getLogin()).get().isResetPassword()).isFalse(); + assertThat(logTester.logs()).isEmpty(); + } + + @Test + public void do_nothing_when_admin_account_with_default_credential_is_disabled() { + UserDto admin = db.users().insertUser(u -> u.setLogin(ADMIN_LOGIN).setActive(false)); + changePassword(admin, "admin"); + + underTest.start(); + + assertThat(db.users().selectUserByLogin(admin.getLogin()).get().isResetPassword()).isFalse(); + assertThat(logTester.logs()).isEmpty(); + } + + private void changePassword(UserDto user, String password) { + localAuthentication.storeHashPassword(user, password); + db.getDbClient().userDao().update(db.getSession(), user); + db.commit(); + } +} diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/user/ws/ChangePasswordActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/user/ws/ChangePasswordActionTest.java index 62d6b550aa1..9b8dbc14ba5 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/user/ws/ChangePasswordActionTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/user/ws/ChangePasswordActionTest.java @@ -34,7 +34,6 @@ import org.sonar.server.exceptions.NotFoundException; import org.sonar.server.organization.TestDefaultOrganizationProvider; import org.sonar.server.tester.UserSessionRule; import org.sonar.server.user.NewUserNotifier; -import org.sonar.server.user.UpdateUser; import org.sonar.server.user.UserUpdater; import org.sonar.server.user.index.UserIndexDefinition; import org.sonar.server.user.index.UserIndexer; @@ -226,8 +225,9 @@ public class ChangePasswordActionTest { private UserDto createLocalUser(String password) { UserDto user = createLocalUser(); - userUpdater.updateAndCommit(db.getSession(), user, new UpdateUser().setLogin(user.getLogin()).setPassword(password), u -> { - }); + localAuthentication.storeHashPassword(user, password); + db.getDbClient().userDao().update(db.getSession(), user); + db.commit(); return user; } diff --git a/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevelStartup.java b/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevelStartup.java index 0d17d7971d3..121acd04311 100644 --- a/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevelStartup.java +++ b/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevelStartup.java @@ -37,14 +37,15 @@ import org.sonar.server.qualityprofile.BuiltInQualityProfilesUpdateListener; import org.sonar.server.qualityprofile.RegisterQualityProfiles; import org.sonar.server.rule.RegisterRules; import org.sonar.server.rule.WebServerRuleFinder; +import org.sonar.server.startup.DetectActiveAdminAccountWithDefaultCredential; import org.sonar.server.startup.GeneratePluginIndex; import org.sonar.server.startup.RegisterMetrics; import org.sonar.server.startup.RegisterPermissionTemplates; import org.sonar.server.startup.RegisterPlugins; import org.sonar.server.startup.RenameDeprecatedPropertyKeys; +import org.sonar.server.startup.UpgradeSuggestionsCleaner; import org.sonar.server.user.DoPrivileged; import org.sonar.server.user.ThreadLocalUserSession; -import org.sonar.server.startup.UpgradeSuggestionsCleaner; public class PlatformLevelStartup extends PlatformLevel { public PlatformLevelStartup(PlatformLevel parent) { @@ -72,7 +73,8 @@ public class PlatformLevelStartup extends PlatformLevel { RegisterPermissionTemplates.class, RenameDeprecatedPropertyKeys.class, CeQueueCleaner.class, - UpgradeSuggestionsCleaner.class); + UpgradeSuggestionsCleaner.class, + DetectActiveAdminAccountWithDefaultCredential.class); // RegisterServletFilters makes the WebService engine of Level4 served by the MasterServletFilter, therefor it // must be started after all the other startup tasks -- 2.39.5