diff options
author | Wouter Admiraal <wouter.admiraal@sonarsource.com> | 2021-03-11 14:49:53 +0100 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2021-03-18 20:08:12 +0000 |
commit | 66249573d70075f5238ecc23ae80baf09af949d4 (patch) | |
tree | d3d948a149806c70533b34356b7851b1f2be261a /server/sonar-webserver-auth | |
parent | 434951a0bf4f7165fecda893ee0ea9771784791d (diff) | |
download | sonarqube-66249573d70075f5238ecc23ae80baf09af949d4.tar.gz sonarqube-66249573d70075f5238ecc23ae80baf09af949d4.zip |
SONAR-14586 Add a new filter to redirect system administrators to reset admin password form
Diffstat (limited to 'server/sonar-webserver-auth')
-rw-r--r-- | server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/AuthenticationModule.java | 1 | ||||
-rw-r--r-- | server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/DefaultAdminCredentialsVerifier.java | 76 | ||||
-rw-r--r-- | server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/DefaultAdminCredentialsVerifierFilter.java | 96 | ||||
-rw-r--r-- | server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/DefaultAdminCredentialsVerifierImpl.java | 113 | ||||
-rw-r--r-- | server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/DefaultAdminCredentialsVerifierFilterTest.java | 169 | ||||
-rw-r--r-- | server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/DefaultAdminCredentialsVerifierImplTest.java (renamed from server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/DefaultAdminCredentialsVerifierTest.java) | 21 |
6 files changed, 399 insertions, 77 deletions
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/AuthenticationModule.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/AuthenticationModule.java index b70aad1de24..fffbaeb3064 100644 --- a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/AuthenticationModule.java +++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/AuthenticationModule.java @@ -34,6 +34,7 @@ public class AuthenticationModule extends Module { CredentialsAuthentication.class, CredentialsExternalAuthentication.class, CredentialsLocalAuthentication.class, + DefaultAdminCredentialsVerifierFilter.class, HttpHeadersAuthentication.class, IdentityProviderRepository.class, InitFilter.class, diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/DefaultAdminCredentialsVerifier.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/DefaultAdminCredentialsVerifier.java index dd8aeb372c9..85927a4ab8d 100644 --- a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/DefaultAdminCredentialsVerifier.java +++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/DefaultAdminCredentialsVerifier.java @@ -19,80 +19,8 @@ */ package org.sonar.server.authentication; -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.event.AuthenticationEvent; -import org.sonar.server.authentication.event.AuthenticationException; -import org.sonar.server.notification.NotificationManager; +public interface DefaultAdminCredentialsVerifier { -import static org.sonar.server.log.ServerProcessLogging.STARTUP_LOGGER_NAME; -import static org.sonar.server.property.InternalProperties.DEFAULT_ADMIN_CREDENTIAL_USAGE_EMAIL; + boolean hasDefaultCredentialUser(); -/** - * Detect usage of an active admin account with default credential in order to ask this account to reset its password during authentication. - */ -public class DefaultAdminCredentialsVerifier implements Startable { - - private static final Logger LOGGER = Loggers.get(STARTUP_LOGGER_NAME); - - private final DbClient dbClient; - private final CredentialsLocalAuthentication localAuthentication; - private final NotificationManager notificationManager; - - public DefaultAdminCredentialsVerifier(DbClient dbClient, CredentialsLocalAuthentication localAuthentication, NotificationManager notificationManager) { - this.dbClient = dbClient; - this.localAuthentication = localAuthentication; - this.notificationManager = notificationManager; - } - - @Override - public void start() { - try (DbSession session = dbClient.openSession(false)) { - UserDto admin = dbClient.userDao().selectActiveUserByLogin(session, "admin"); - if (admin == null || !isDefaultCredentialUser(session, admin)) { - return; - } - addWarningInSonarDotLog(); - dbClient.userDao().update(session, admin.setResetPassword(true)); - sendEmailToAdmins(session); - session.commit(); - } - } - - private static void addWarningInSonarDotLog() { - String highlighter = "####################################################################################################################"; - String msg = "Default Administrator credentials are still being used. Make sure to change the password or deactivate the account."; - - LOGGER.warn(highlighter); - LOGGER.warn(msg); - LOGGER.warn(highlighter); - } - - private boolean isDefaultCredentialUser(DbSession dbSession, UserDto user) { - try { - localAuthentication.authenticate(dbSession, user, "admin", AuthenticationEvent.Method.BASIC); - return true; - } catch (AuthenticationException ex) { - return false; - } - } - - 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-auth/src/main/java/org/sonar/server/authentication/DefaultAdminCredentialsVerifierFilter.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/DefaultAdminCredentialsVerifierFilter.java new file mode 100644 index 00000000000..ed262193da7 --- /dev/null +++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/DefaultAdminCredentialsVerifierFilter.java @@ -0,0 +1,96 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 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 com.google.common.collect.ImmutableSet; +import java.io.IOException; +import java.util.Set; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.sonar.api.config.Configuration; +import org.sonar.api.web.ServletFilter; +import org.sonar.server.user.ThreadLocalUserSession; + +import static org.sonar.api.web.ServletFilter.UrlPattern.Builder.staticResourcePatterns; +import static org.sonar.server.authentication.AuthenticationRedirection.redirectTo; + +public class DefaultAdminCredentialsVerifierFilter extends ServletFilter { + private static final String RESET_PASSWORD_PATH = "/account/reset_password"; + private static final String CHANGE_ADMIN_PASSWORD_PATH = "/admin/change_admin_password"; + // This property is used by Orchestrator to disable this force redirect. It should never be used in production, which + // is why this is not defined in org.sonar.process.ProcessProperties. + private static final String SONAR_FORCE_REDIRECT_DEFAULT_ADMIN_CREDENTIALS = "sonar.forceRedirectOnDefaultAdminCredentials"; + + private static final Set<String> SKIPPED_URLS = ImmutableSet.of( + RESET_PASSWORD_PATH, + CHANGE_ADMIN_PASSWORD_PATH, + "/batch/*", "/api/*"); + + private final Configuration config; + private final DefaultAdminCredentialsVerifier defaultAdminCredentialsVerifier; + private final ThreadLocalUserSession userSession; + + public DefaultAdminCredentialsVerifierFilter(Configuration config, DefaultAdminCredentialsVerifier defaultAdminCredentialsVerifier, ThreadLocalUserSession userSession) { + this.config = config; + this.defaultAdminCredentialsVerifier = defaultAdminCredentialsVerifier; + this.userSession = userSession; + } + + @Override + public UrlPattern doGetPattern() { + return UrlPattern.builder() + .includes("/*") + .excludes(staticResourcePatterns()) + .excludes(SKIPPED_URLS) + .build(); + } + + @Override + public void init(FilterConfig filterConfig) { + // nothing to do + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException { + HttpServletRequest request = (HttpServletRequest) servletRequest; + HttpServletResponse response = (HttpServletResponse) servletResponse; + boolean forceRedirect = config + .getBoolean(SONAR_FORCE_REDIRECT_DEFAULT_ADMIN_CREDENTIALS) + .orElse(true); + + if (forceRedirect && userSession.hasSession() && userSession.isLoggedIn() + && userSession.isSystemAdministrator() && !"admin".equals(userSession.getLogin()) + && defaultAdminCredentialsVerifier.hasDefaultCredentialUser()) { + redirectTo(response, request.getContextPath() + CHANGE_ADMIN_PASSWORD_PATH); + } + + chain.doFilter(request, response); + } + + @Override + public void destroy() { + // nothing to do + } +} diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/DefaultAdminCredentialsVerifierImpl.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/DefaultAdminCredentialsVerifierImpl.java new file mode 100644 index 00000000000..b7a9905904a --- /dev/null +++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/DefaultAdminCredentialsVerifierImpl.java @@ -0,0 +1,113 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 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.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.event.AuthenticationEvent; +import org.sonar.server.authentication.event.AuthenticationException; +import org.sonar.server.notification.NotificationManager; + +import static org.sonar.server.log.ServerProcessLogging.STARTUP_LOGGER_NAME; +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 DefaultAdminCredentialsVerifierImpl implements Startable, DefaultAdminCredentialsVerifier { + + private static final Logger LOGGER = Loggers.get(STARTUP_LOGGER_NAME); + + private final DbClient dbClient; + private final CredentialsLocalAuthentication localAuthentication; + private final NotificationManager notificationManager; + + public DefaultAdminCredentialsVerifierImpl(DbClient dbClient, CredentialsLocalAuthentication localAuthentication, NotificationManager notificationManager) { + this.dbClient = dbClient; + this.localAuthentication = localAuthentication; + this.notificationManager = notificationManager; + } + + @Override + public void start() { + try (DbSession session = dbClient.openSession(false)) { + UserDto admin = getAdminUser(session); + if (admin == null || !isDefaultCredentialUser(session, admin)) { + return; + } + addWarningInSonarDotLog(); + dbClient.userDao().update(session, admin.setResetPassword(true)); + sendEmailToAdmins(session); + session.commit(); + } + } + + public boolean hasDefaultCredentialUser() { + try (DbSession session = dbClient.openSession(false)) { + UserDto admin = getAdminUser(session); + if (admin == null) { + return false; + } else { + return isDefaultCredentialUser(session, admin); + } + } + } + + private UserDto getAdminUser(DbSession session) { + return dbClient.userDao().selectActiveUserByLogin(session, "admin"); + } + + private static void addWarningInSonarDotLog() { + String highlighter = "####################################################################################################################"; + String msg = "Default Administrator credentials are still being used. Make sure to change the password or deactivate the account."; + + LOGGER.warn(highlighter); + LOGGER.warn(msg); + LOGGER.warn(highlighter); + } + + private boolean isDefaultCredentialUser(DbSession dbSession, UserDto user) { + try { + localAuthentication.authenticate(dbSession, user, "admin", AuthenticationEvent.Method.BASIC); + return true; + } catch (AuthenticationException ex) { + return false; + } + } + + 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-auth/src/test/java/org/sonar/server/authentication/DefaultAdminCredentialsVerifierFilterTest.java b/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/DefaultAdminCredentialsVerifierFilterTest.java new file mode 100644 index 00000000000..f154ed91b14 --- /dev/null +++ b/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/DefaultAdminCredentialsVerifierFilterTest.java @@ -0,0 +1,169 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 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 com.tngtech.java.junit.dataprovider.DataProvider; +import com.tngtech.java.junit.dataprovider.DataProviderRunner; +import com.tngtech.java.junit.dataprovider.UseDataProvider; +import java.util.Optional; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.sonar.api.config.Configuration; +import org.sonar.server.user.ThreadLocalUserSession; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +@RunWith(DataProviderRunner.class) +public class DefaultAdminCredentialsVerifierFilterTest { + + private final HttpServletRequest request = mock(HttpServletRequest.class); + private final HttpServletResponse response = mock(HttpServletResponse.class); + private final FilterChain chain = mock(FilterChain.class); + private final Configuration config = mock(Configuration.class); + private final DefaultAdminCredentialsVerifier defaultAdminCredentialsVerifier = mock(DefaultAdminCredentialsVerifier.class); + private final ThreadLocalUserSession session = mock(ThreadLocalUserSession.class); + + private final DefaultAdminCredentialsVerifierFilter underTest = new DefaultAdminCredentialsVerifierFilter(config, defaultAdminCredentialsVerifier, session); + + @Before + public void before() { + when(request.getRequestURI()).thenReturn("/"); + when(request.getContextPath()).thenReturn(""); + + when(config.getBoolean("sonar.forceRedirectOnDefaultAdminCredentials")).thenReturn(Optional.of(true)); + when(defaultAdminCredentialsVerifier.hasDefaultCredentialUser()).thenReturn(true); + when(session.hasSession()).thenReturn(true); + when(session.isLoggedIn()).thenReturn(true); + when(session.isSystemAdministrator()).thenReturn(true); + } + + @Test + public void verify_other_methods() { + underTest.init(mock(FilterConfig.class)); + underTest.destroy(); + + verifyNoInteractions(request, response, chain, session); + } + + @Test + public void redirect_if_instance_uses_default_admin_credentials() throws Exception { + underTest.doFilter(request, response, chain); + + verify(response).sendRedirect("/admin/change_admin_password"); + } + + @Test + public void redirect_if_instance_uses_default_admin_credentials_and_web_context_configured() throws Exception { + when(request.getContextPath()).thenReturn("/sonarqube"); + + underTest.doFilter(request, response, chain); + + verify(response).sendRedirect("/sonarqube/admin/change_admin_password"); + } + + @Test + public void redirect_if_request_uri_ends_with_slash() throws Exception { + when(request.getRequestURI()).thenReturn("/projects/"); + when(request.getContextPath()).thenReturn("/sonarqube"); + + underTest.doFilter(request, response, chain); + + verify(response).sendRedirect("/sonarqube/admin/change_admin_password"); + } + + @Test + public void do_not_redirect_if_not_a_system_administrator() throws Exception { + when(session.isSystemAdministrator()).thenReturn(false); + + underTest.doFilter(request, response, chain); + + verify(response, never()).sendRedirect(any()); + } + + @Test + public void do_not_redirect_if_not_logged_in() throws Exception { + when(session.isLoggedIn()).thenReturn(false); + + underTest.doFilter(request, response, chain); + + verify(response, never()).sendRedirect(any()); + } + + @Test + public void do_not_redirect_if_user_is_admin() throws Exception { + when(session.getLogin()).thenReturn("admin"); + + underTest.doFilter(request, response, chain); + + verify(response, never()).sendRedirect(any()); + } + + @Test + public void do_not_redirect_if_instance_does_not_use_default_admin_credentials() throws Exception { + when(defaultAdminCredentialsVerifier.hasDefaultCredentialUser()).thenReturn(false); + + underTest.doFilter(request, response, chain); + + verify(response, never()).sendRedirect(any()); + } + + @Test + public void do_not_redirect_if_config_says_so() throws Exception { + when(config.getBoolean("sonar.forceRedirectOnDefaultAdminCredentials")).thenReturn(Optional.of(false)); + + underTest.doFilter(request, response, chain); + + verify(response, never()).sendRedirect(any()); + } + + @Test + @UseDataProvider("skipped_urls") + public void doGetPattern_verify(String urltoSkip) throws Exception { + when(request.getRequestURI()).thenReturn(urltoSkip); + when(request.getContextPath()).thenReturn(""); + underTest.doGetPattern().matches(urltoSkip); + + verify(response, never()).sendRedirect(any()); + } + + @DataProvider + public static Object[][] skipped_urls() { + return new Object[][] { + {"/batch/index"}, + {"/batch/file"}, + {"/api/issues"}, + {"/api/issues/"}, + {"/api/*"}, + {"/admin/change_admin_password"}, + {"/account/reset_password"}, + }; + } + +} diff --git a/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/DefaultAdminCredentialsVerifierTest.java b/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/DefaultAdminCredentialsVerifierImplTest.java index a307fa0475f..df125fdeeb3 100644 --- a/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/DefaultAdminCredentialsVerifierTest.java +++ b/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/DefaultAdminCredentialsVerifierImplTest.java @@ -36,7 +36,7 @@ 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 DefaultAdminCredentialsVerifierTest { +public class DefaultAdminCredentialsVerifierImplTest { private static final String ADMIN_LOGIN = "admin"; @@ -48,7 +48,7 @@ public class DefaultAdminCredentialsVerifierTest { private final CredentialsLocalAuthentication localAuthentication = new CredentialsLocalAuthentication(db.getDbClient()); private final NotificationManager notificationManager = mock(NotificationManager.class); - private final DefaultAdminCredentialsVerifier underTest = new DefaultAdminCredentialsVerifier(db.getDbClient(), localAuthentication, notificationManager); + private final DefaultAdminCredentialsVerifierImpl underTest = new DefaultAdminCredentialsVerifierImpl(db.getDbClient(), localAuthentication, notificationManager); @After public void after() { @@ -56,6 +56,21 @@ public class DefaultAdminCredentialsVerifierTest { } @Test + public void correctly_detect_if_admin_account_is_used_with_default_credential() { + UserDto admin = db.users().insertUser(u -> u.setLogin(ADMIN_LOGIN)); + changePassword(admin, "admin"); + assertThat(underTest.hasDefaultCredentialUser()).isTrue(); + + changePassword(admin, "1234"); + assertThat(underTest.hasDefaultCredentialUser()).isFalse(); + } + + @Test + public void does_not_break_if_admin_account_does_not_exist() { + assertThat(underTest.hasDefaultCredentialUser()).isFalse(); + } + + @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"); @@ -64,7 +79,7 @@ public class DefaultAdminCredentialsVerifierTest { 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"); + assertThat(db.getDbClient().internalPropertiesDao().selectByKey(db.getSession(), DEFAULT_ADMIN_CREDENTIAL_USAGE_EMAIL)).contains("true"); verify(notificationManager).scheduleForSending(any(Notification.class)); } |