diff options
author | Julien Lancelot <julien.lancelot@sonarsource.com> | 2020-11-30 10:44:46 +0100 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2020-12-02 20:06:57 +0000 |
commit | 3c492694e75c91329f63f4557d7f2425afc4ea64 (patch) | |
tree | 2195e86eb84414ee46f43c36e34d5293fd963473 /server/sonar-webserver-core | |
parent | abda9cf968f84f4f5f41ecb1054963dcc6c34218 (diff) | |
download | sonarqube-3c492694e75c91329f63f4557d7f2425afc4ea64.tar.gz sonarqube-3c492694e75c91329f63f4557d7f2425afc4ea64.zip |
SONAR-14176 Send email to admins when default admin credential is detected
Diffstat (limited to 'server/sonar-webserver-core')
7 files changed, 365 insertions, 16 deletions
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/authentication/DefaultAdminCredentialsVerifier.java index 09249a02a36..c79cce61aae 100644 --- 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/authentication/DefaultAdminCredentialsVerifier.java @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -package org.sonar.server.startup; +package org.sonar.server.authentication; import org.picocontainer.Startable; import org.sonar.api.utils.log.Logger; @@ -26,37 +26,42 @@ 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; +import org.sonar.server.notification.NotificationManager; + +import static org.sonar.server.property.InternalProperties.DEFAULT_ADMIN_CREDENTIAL_USAGE_EMAIL; /** * 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 { +public class DefaultAdminCredentialsVerifier implements Startable { - private static final Logger LOGGER = Loggers.get(DetectActiveAdminAccountWithDefaultCredential.class); + private static final Logger LOGGER = Loggers.get(DefaultAdminCredentialsVerifier.class); private final DbClient dbClient; private final CredentialsLocalAuthentication localAuthentication; + private final NotificationManager notificationManager; - public DetectActiveAdminAccountWithDefaultCredential(DbClient dbClient, CredentialsLocalAuthentication localAuthentication) { + public DefaultAdminCredentialsVerifier(DbClient dbClient, CredentialsLocalAuthentication localAuthentication, NotificationManager notificationManager) { this.dbClient = dbClient; this.localAuthentication = localAuthentication; + this.notificationManager = notificationManager; } @Override public void start() { - try (DbSession dbSession = dbClient.openSession(false)) { - UserDto admin = dbClient.userDao().selectActiveUserByLogin(dbSession, "admin"); - if (admin == null || !isDefaultCredentialUser(dbSession, admin)) { + try (DbSession session = dbClient.openSession(false)) { + UserDto admin = dbClient.userDao().selectActiveUserByLogin(session, "admin"); + if (admin == null || !isDefaultCredentialUser(session, 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(); + dbClient.userDao().update(session, admin.setResetPassword(true)); + sendEmailToAdmins(session); + session.commit(); } } @@ -69,6 +74,16 @@ public class DetectActiveAdminAccountWithDefaultCredential implements Startable } } + private void sendEmailToAdmins(DbSession session) { + if (dbClient.internalPropertiesDao().selectByKey(session, DEFAULT_ADMIN_CREDENTIAL_USAGE_EMAIL) + .map(Boolean::parseBoolean) + .orElse(false)) { + return; + } + notificationManager.scheduleForSending(new DefaultAdminCredentialsVerifierNotification()); + dbClient.internalPropertiesDao().save(session, DEFAULT_ADMIN_CREDENTIAL_USAGE_EMAIL, Boolean.TRUE.toString()); + } + @Override public void stop() { // Nothing to do diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/authentication/DefaultAdminCredentialsVerifierNotification.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/authentication/DefaultAdminCredentialsVerifierNotification.java new file mode 100644 index 00000000000..46336b4356c --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/authentication/DefaultAdminCredentialsVerifierNotification.java @@ -0,0 +1,32 @@ +/* + * 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.authentication; + +import org.sonar.api.notifications.Notification; + +public class DefaultAdminCredentialsVerifierNotification extends Notification { + + static final String TYPE = "default-admin-credential-verifier"; + + public DefaultAdminCredentialsVerifierNotification() { + super(TYPE); + } +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/authentication/DefaultAdminCredentialsVerifierNotificationHandler.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/authentication/DefaultAdminCredentialsVerifierNotificationHandler.java new file mode 100644 index 00000000000..ac8efa6fbaa --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/authentication/DefaultAdminCredentialsVerifierNotificationHandler.java @@ -0,0 +1,62 @@ +/* + * 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.authentication; + +import java.util.Collection; +import java.util.Optional; +import java.util.Set; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.server.notification.EmailNotificationHandler; +import org.sonar.server.notification.NotificationDispatcherMetadata; +import org.sonar.server.notification.email.EmailNotificationChannel; +import org.sonar.server.notification.email.EmailNotificationChannel.EmailDeliveryRequest; + +import static java.util.stream.Collectors.toSet; + +public class DefaultAdminCredentialsVerifierNotificationHandler extends EmailNotificationHandler<DefaultAdminCredentialsVerifierNotification> { + + private final DbClient dbClient; + + public DefaultAdminCredentialsVerifierNotificationHandler(DbClient dbClient, EmailNotificationChannel emailNotificationChannel) { + super(emailNotificationChannel); + this.dbClient = dbClient; + } + + @Override + public Optional<NotificationDispatcherMetadata> getMetadata() { + return Optional.empty(); + } + + @Override + public Class<DefaultAdminCredentialsVerifierNotification> getNotificationClass() { + return DefaultAdminCredentialsVerifierNotification.class; + } + + @Override + public Set<EmailDeliveryRequest> toEmailDeliveryRequests(Collection<DefaultAdminCredentialsVerifierNotification> notifications) { + try (DbSession session = dbClient.openSession(false)) { + return dbClient.authorizationDao().selectGlobalAdministerEmailSubscribers(session) + .stream() + .flatMap(t -> notifications.stream().map(notification -> new EmailDeliveryRequest(t.getEmail(), notification))) + .collect(toSet()); + } + } +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/authentication/DefaultAdminCredentialsVerifierNotificationTemplate.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/authentication/DefaultAdminCredentialsVerifierNotificationTemplate.java new file mode 100644 index 00000000000..243b00536aa --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/authentication/DefaultAdminCredentialsVerifierNotificationTemplate.java @@ -0,0 +1,47 @@ +/* + * 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.authentication; + +import javax.annotation.CheckForNull; +import org.sonar.api.notifications.Notification; +import org.sonar.server.issue.notification.EmailMessage; +import org.sonar.server.issue.notification.EmailTemplate; + +public class DefaultAdminCredentialsVerifierNotificationTemplate implements EmailTemplate { + + static final String SUBJECT = "Default Administrator credentials are still used"; + static final String BODY_FORMAT = "Hello,\n\n" + + "Your SonarQube instance is still using default administrator credentials.\n" + + "Make sure to change the password for the 'admin' account or deactivate this account."; + + @Override + @CheckForNull + public EmailMessage format(Notification notification) { + if (!DefaultAdminCredentialsVerifierNotification.TYPE.equals(notification.getType())) { + return null; + } + + return new EmailMessage() + .setMessageId(DefaultAdminCredentialsVerifierNotification.TYPE) + .setSubject(SUBJECT) + .setPlainTextMessage(BODY_FORMAT); + } + +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/authentication/DefaultAdminCredentialsVerifierNotificationHandlerTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/authentication/DefaultAdminCredentialsVerifierNotificationHandlerTest.java new file mode 100644 index 00000000000..97bb756356a --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/authentication/DefaultAdminCredentialsVerifierNotificationHandlerTest.java @@ -0,0 +1,120 @@ +/* + * 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.authentication; + +import java.util.Set; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.stubbing.Answer; +import org.sonar.db.DbTester; +import org.sonar.db.user.GroupDto; +import org.sonar.db.user.UserDto; +import org.sonar.server.notification.email.EmailNotificationChannel; + +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anySet; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; +import static org.sonar.db.permission.GlobalPermission.ADMINISTER; + +public class DefaultAdminCredentialsVerifierNotificationHandlerTest { + + @Rule + public DbTester db = DbTester.create(); + + private EmailNotificationChannel emailNotificationChannel = mock(EmailNotificationChannel.class); + + private DefaultAdminCredentialsVerifierNotificationHandler underTest = new DefaultAdminCredentialsVerifierNotificationHandler(db.getDbClient(), + emailNotificationChannel); + + @Before + public void setUp() { + when(emailNotificationChannel.deliverAll(anySet())) + .then((Answer<Integer>) invocationOnMock -> ((Set<EmailNotificationChannel.EmailDeliveryRequest>) invocationOnMock.getArguments()[0]).size()); + } + + @Test + public void deliver_to_all_admins_having_emails() { + when(emailNotificationChannel.isActivated()).thenReturn(true); + DefaultAdminCredentialsVerifierNotification detectActiveAdminAccountWithDefaultCredentialNotification = mock(DefaultAdminCredentialsVerifierNotification.class); + // Users granted admin permission directly + UserDto admin1 = db.users().insertUser(u -> u.setEmail("admin1")); + UserDto adminWithNoEmail = db.users().insertUser(u -> u.setEmail(null)); + db.users().insertPermissionOnUser(admin1, ADMINISTER); + db.users().insertPermissionOnUser(adminWithNoEmail, ADMINISTER); + // User granted admin permission by group membership + UserDto admin2 = db.users().insertUser(u -> u.setEmail("admin2")); + GroupDto adminGroup = db.users().insertGroup(); + db.users().insertPermissionOnGroup(adminGroup, ADMINISTER); + db.users().insertMember(adminGroup, admin2); + db.users().insertUser(u -> u.setEmail("otherUser")); + + int deliver = underTest.deliver(singletonList(detectActiveAdminAccountWithDefaultCredentialNotification)); + + // Only 2 admins have there email defined + assertThat(deliver).isEqualTo(2); + verify(emailNotificationChannel).isActivated(); + verify(emailNotificationChannel).deliverAll(anySet()); + verifyNoMoreInteractions(detectActiveAdminAccountWithDefaultCredentialNotification); + } + + @Test + public void deliver_to_no_one_when_no_admins() { + when(emailNotificationChannel.isActivated()).thenReturn(true); + DefaultAdminCredentialsVerifierNotification detectActiveAdminAccountWithDefaultCredentialNotification = mock(DefaultAdminCredentialsVerifierNotification.class); + db.users().insertUser(u -> u.setEmail("otherUser")); + + int deliver = underTest.deliver(singletonList(detectActiveAdminAccountWithDefaultCredentialNotification)); + + assertThat(deliver).isZero(); + verify(emailNotificationChannel).isActivated(); + verifyNoMoreInteractions(emailNotificationChannel); + verifyNoMoreInteractions(detectActiveAdminAccountWithDefaultCredentialNotification); + } + + @Test + public void do_nothing_if_emailNotificationChannel_is_disabled() { + when(emailNotificationChannel.isActivated()).thenReturn(false); + DefaultAdminCredentialsVerifierNotification detectActiveAdminAccountWithDefaultCredentialNotification = mock( + DefaultAdminCredentialsVerifierNotification.class); + + int deliver = underTest.deliver(singletonList(detectActiveAdminAccountWithDefaultCredentialNotification)); + + assertThat(deliver).isZero(); + verify(emailNotificationChannel).isActivated(); + verifyNoMoreInteractions(emailNotificationChannel); + verifyNoMoreInteractions(detectActiveAdminAccountWithDefaultCredentialNotification); + } + + @Test + public void getMetadata_returns_empty() { + assertThat(underTest.getMetadata()).isEmpty(); + } + + @Test + public void getNotificationClass() { + assertThat(underTest.getNotificationClass()).isEqualTo(DefaultAdminCredentialsVerifierNotification.class); + } + +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/authentication/DefaultAdminCredentialsVerifierNotificationTemplateTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/authentication/DefaultAdminCredentialsVerifierNotificationTemplateTest.java new file mode 100644 index 00000000000..a1eb94a8548 --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/authentication/DefaultAdminCredentialsVerifierNotificationTemplateTest.java @@ -0,0 +1,50 @@ +/* + * 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.authentication; + +import org.junit.Test; +import org.sonar.api.notifications.Notification; +import org.sonar.server.issue.notification.EmailMessage; + +import static org.assertj.core.api.Assertions.assertThat; + +public class DefaultAdminCredentialsVerifierNotificationTemplateTest { + + private DefaultAdminCredentialsVerifierNotificationTemplate underTest = new DefaultAdminCredentialsVerifierNotificationTemplate(); + + @Test + public void do_not_format_other_notifications() { + assertThat(underTest.format(new Notification("foo"))).isNull(); + } + + @Test + public void format_notification() { + Notification notification = new Notification(DefaultAdminCredentialsVerifierNotification.TYPE); + + EmailMessage emailMessage = underTest.format(notification); + + assertThat(emailMessage.getSubject()).isEqualTo("Default Administrator credentials are still used"); + assertThat(emailMessage.getMessage()).isEqualTo("Hello,\n\n" + + "Your SonarQube instance is still using default administrator credentials.\n" + + "Make sure to change the password for the 'admin' account or deactivate this account."); + } + +} 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/authentication/DefaultAdminCredentialsVerifierTest.java index 5fee1484965..db88777c60c 100644 --- 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/authentication/DefaultAdminCredentialsVerifierTest.java @@ -18,32 +18,38 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -package org.sonar.server.startup; +package org.sonar.server.authentication; import org.junit.After; import org.junit.Rule; import org.junit.Test; -import org.sonar.api.utils.System2; +import org.sonar.api.notifications.Notification; 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 org.sonar.server.notification.NotificationManager; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.sonar.server.property.InternalProperties.DEFAULT_ADMIN_CREDENTIAL_USAGE_EMAIL; -public class DetectActiveAdminAccountWithDefaultCredentialTest { +public class DefaultAdminCredentialsVerifierTest { private static final String ADMIN_LOGIN = "admin"; @Rule - public DbTester db = DbTester.create(System2.INSTANCE); + public DbTester db = DbTester.create(); @Rule public LogTester logTester = new LogTester(); private final CredentialsLocalAuthentication localAuthentication = new CredentialsLocalAuthentication(db.getDbClient()); + private final NotificationManager notificationManager = mock(NotificationManager.class); - private final DetectActiveAdminAccountWithDefaultCredential underTest = new DetectActiveAdminAccountWithDefaultCredential(db.getDbClient(), localAuthentication); + private final DefaultAdminCredentialsVerifier underTest = new DefaultAdminCredentialsVerifier(db.getDbClient(), localAuthentication, notificationManager); @After public void after() { @@ -59,6 +65,20 @@ public class DetectActiveAdminAccountWithDefaultCredentialTest { 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."); + assertThat(db.getDbClient().internalPropertiesDao().selectByKey(db.getSession(), DEFAULT_ADMIN_CREDENTIAL_USAGE_EMAIL).get()).isEqualTo("true"); + verify(notificationManager).scheduleForSending(any(Notification.class)); + } + + @Test + public void do_not_send_email_to_admins_when_already_sent() { + UserDto admin = db.users().insertUser(u -> u.setLogin(ADMIN_LOGIN)); + changePassword(admin, "admin"); + db.getDbClient().internalPropertiesDao().save(db.getSession(), DEFAULT_ADMIN_CREDENTIAL_USAGE_EMAIL, "true"); + db.commit(); + + underTest.start(); + + verifyNoMoreInteractions(notificationManager); } @Test @@ -70,6 +90,7 @@ public class DetectActiveAdminAccountWithDefaultCredentialTest { assertThat(db.users().selectUserByLogin(admin.getLogin()).get().isResetPassword()).isFalse(); assertThat(logTester.logs()).isEmpty(); + verifyNoMoreInteractions(notificationManager); } @Test @@ -81,6 +102,7 @@ public class DetectActiveAdminAccountWithDefaultCredentialTest { assertThat(db.users().selectUserByLogin(otherUser.getLogin()).get().isResetPassword()).isFalse(); assertThat(logTester.logs()).isEmpty(); + verifyNoMoreInteractions(notificationManager); } @Test @@ -92,6 +114,7 @@ public class DetectActiveAdminAccountWithDefaultCredentialTest { assertThat(db.users().selectUserByLogin(admin.getLogin()).get().isResetPassword()).isFalse(); assertThat(logTester.logs()).isEmpty(); + verifyNoMoreInteractions(notificationManager); } private void changePassword(UserDto user, String password) { |