]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-14175 SONAR-14176 Detect usage of admin account with default credential
authorJulien Lancelot <julien.lancelot@sonarsource.com>
Fri, 27 Nov 2020 08:16:06 +0000 (09:16 +0100)
committersonartech <sonartech@sonarsource.com>
Wed, 2 Dec 2020 20:06:57 +0000 (20:06 +0000)
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

server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v00/PopulateInitialSchema.java
server/sonar-webserver-core/src/main/java/org/sonar/server/startup/DetectActiveAdminAccountWithDefaultCredential.java [new file with mode: 0644]
server/sonar-webserver-core/src/test/java/org/sonar/server/startup/DetectActiveAdminAccountWithDefaultCredentialTest.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/test/java/org/sonar/server/user/ws/ChangePasswordActionTest.java
server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevelStartup.java

index 0e971237b62d5b5ce34fddc85b13f49f3fd4ba9f..b98a48b763d414e286e2d34465e1f9587e8bb0d5 100644 (file)
@@ -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<String> 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 (file)
index 0000000..09249a0
--- /dev/null
@@ -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 (file)
index 0000000..5fee148
--- /dev/null
@@ -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();
+  }
+}
index 62d6b550aa19b5c18fc07df4442458d0ffc120fa..9b8dbc14ba56eaddb4860f992bcf5d2d4500073b 100644 (file)
@@ -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;
   }
 
index 0d17d7971d3c0c4a18a7050c6fdd70ce63e9ded8..121acd0431108ad29a1e1211dd026748bbc74955 100644 (file)
@@ -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