]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-14175 Redirect when 'reset_password' is set
authorJacek <jacek.poreda@sonarsource.com>
Thu, 26 Nov 2020 11:41:10 +0000 (12:41 +0100)
committersonartech <sonartech@sonarsource.com>
Wed, 2 Dec 2020 20:06:57 +0000 (20:06 +0000)
20 files changed:
server/sonar-web/public/WEB-INF/web.xml
server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/AuthenticationModule.java
server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/ResetPasswordFilter.java [new file with mode: 0644]
server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/SafeModeUserSession.java
server/sonar-webserver-auth/src/main/java/org/sonar/server/user/DoPrivileged.java
server/sonar-webserver-auth/src/main/java/org/sonar/server/user/ServerUserSession.java
server/sonar-webserver-auth/src/main/java/org/sonar/server/user/ThreadLocalUserSession.java
server/sonar-webserver-auth/src/main/java/org/sonar/server/user/UserSession.java
server/sonar-webserver-auth/src/main/java/org/sonar/server/user/UserUpdater.java
server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/ResetPasswordFilterTest.java [new file with mode: 0644]
server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/SafeModeUserSessionTest.java
server/sonar-webserver-auth/src/test/java/org/sonar/server/user/DoPrivilegedTest.java
server/sonar-webserver-auth/src/test/java/org/sonar/server/user/ServerUserSessionTest.java
server/sonar-webserver-auth/src/test/java/org/sonar/server/user/ThreadLocalUserSessionTest.java
server/sonar-webserver-auth/src/test/java/org/sonar/server/user/UserUpdaterUpdateTest.java
server/sonar-webserver-auth/src/testFixtures/java/org/sonar/server/tester/AbstractMockUserSession.java
server/sonar-webserver-auth/src/testFixtures/java/org/sonar/server/tester/AnonymousMockUserSession.java
server/sonar-webserver-auth/src/testFixtures/java/org/sonar/server/tester/UserSessionRule.java
server/sonar-webserver-auth/src/testFixtures/java/org/sonar/server/user/TestUserSessionFactory.java
server/sonar-webserver/src/main/java/org/sonar/server/platform/web/UserSessionFilter.java

index dd785a71287ea702fc6ec869f1142e7f62e51cc3..d2ba0126c87481a502ab0116649fea8c2077e9d9 100644 (file)
@@ -2,10 +2,10 @@
 
 <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xmlns="http://java.sun.com/xml/ns/javaee"
-         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
-         id="SonarQube"
-         version="3.0"
-         metadata-complete="true">
+  xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
+  id="SonarQube"
+  version="3.0"
+  metadata-complete="true">
   <display-name>SonarQube</display-name>
 
   <filter>
index 18681bee6a05064e6ca70a819d24e154b3f2d8dd..abe9238b597440767376aef1e00cd28c723012f6 100644 (file)
@@ -45,6 +45,7 @@ public class AuthenticationModule extends Module {
       OAuth2ContextFactory.class,
       OAuthCsrfVerifier.class,
       RequestAuthenticatorImpl.class,
+      ResetPasswordFilter.class,
       ExpiredSessionsCleaner.class,
       ExpiredSessionsCleanerExecutorServiceImpl.class,
       UserLastConnectionDatesUpdaterImpl.class,
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/ResetPasswordFilter.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/ResetPasswordFilter.java
new file mode 100644 (file)
index 0000000..60f49d6
--- /dev/null
@@ -0,0 +1,81 @@
+/*
+ * 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 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.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 ResetPasswordFilter extends ServletFilter {
+  private static final String RESET_PASSWORD_PATH = "/account/reset_password";
+
+  private static final Set<String> SKIPPED_URLS = ImmutableSet.of(
+    RESET_PASSWORD_PATH,
+    "/batch/*", "/api/*");
+
+  private final ThreadLocalUserSession userSession;
+
+  public ResetPasswordFilter(ThreadLocalUserSession userSession) {
+    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;
+
+    if (userSession.hasSession() && userSession.isLoggedIn() && userSession.shouldResetPassword()) {
+      redirectTo(response, request.getContextPath() + RESET_PASSWORD_PATH);
+    }
+
+    chain.doFilter(request, response);
+  }
+
+  @Override
+  public void destroy() {
+    // nothing to do
+  }
+}
index d1e01c20352c2906a39e03df899ee35622c05cef..18a20f7868b1edf6e009d116f960f0abf26087b6 100644 (file)
@@ -69,6 +69,11 @@ public class SafeModeUserSession extends AbstractUserSession {
     return Collections.emptyList();
   }
 
+  @Override
+  public boolean shouldResetPassword() {
+    return false;
+  }
+
   @Override
   public Optional<IdentityProvider> getIdentityProvider() {
     return Optional.empty();
index a12128028671266129c195342c4a7960b7bff9c0..e1dd749f8f9d14683603d2de552240c9718cf0fe 100644 (file)
@@ -86,6 +86,11 @@ public final class DoPrivileged {
         return Collections.emptyList();
       }
 
+      @Override
+      public boolean shouldResetPassword() {
+        return false;
+      }
+
       @Override
       public boolean isLoggedIn() {
         return false;
index af7a154a72e3f007dd83edd9bb7f41bc61bd57b0..811abe6607355474fbef0c14f07ac92543c952b3 100644 (file)
@@ -95,6 +95,11 @@ public class ServerUserSession extends AbstractUserSession {
     return groups;
   }
 
+  @Override
+  public boolean shouldResetPassword() {
+    return userDto != null && userDto.isResetPassword();
+  }
+
   @Override
   public boolean isLoggedIn() {
     return userDto != null;
index cf9c34744287ea37312e717e1fee4e21f50c2392..e97ccac10b95397749a38f2aad923d512cfdd6b3 100644 (file)
@@ -110,6 +110,11 @@ public class ThreadLocalUserSession implements UserSession {
     return this;
   }
 
+  @Override
+  public boolean shouldResetPassword() {
+    return get().shouldResetPassword();
+  }
+
   @Override
   public boolean hasPermission(GlobalPermission permission) {
     return get().hasPermission(permission);
index fbe4a16ccbec28fd16b7579b7ea2535b6b4dc371..5ba9d59af7634ffa2e335a75c4e9a5fa9dafbd4c 100644 (file)
@@ -62,6 +62,8 @@ public interface UserSession {
    */
   Collection<GroupDto> getGroups();
 
+  boolean shouldResetPassword();
+
   /**
    * This enum supports by name only the few providers for which specific code exists.
    */
index 96807e15c039d51d4088259b0c0fb11a4152721a..ba996626e594707db84315fc81fc3b361a53395f 100644 (file)
@@ -253,6 +253,7 @@ public class UserUpdater {
     String password = updateUser.password();
     if (updateUser.isPasswordChanged() && validatePasswords(password, messages) && checkPasswordChangeAllowed(userDto, messages)) {
       localAuthentication.storeHashPassword(userDto, password);
+      userDto.setResetPassword(false);
       return true;
     }
     return false;
diff --git a/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/ResetPasswordFilterTest.java b/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/ResetPasswordFilterTest.java
new file mode 100644 (file)
index 0000000..c0ce26d
--- /dev/null
@@ -0,0 +1,147 @@
+/*
+ * 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 com.tngtech.java.junit.dataprovider.DataProvider;
+import com.tngtech.java.junit.dataprovider.DataProviderRunner;
+import com.tngtech.java.junit.dataprovider.UseDataProvider;
+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.server.user.ThreadLocalUserSession;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+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 ResetPasswordFilterTest {
+
+  private final HttpServletRequest request = mock(HttpServletRequest.class);
+  private final HttpServletResponse response = mock(HttpServletResponse.class);
+  private final FilterChain chain = mock(FilterChain.class);
+  private final ThreadLocalUserSession session = mock(ThreadLocalUserSession.class);
+
+  private final ResetPasswordFilter underTest = new ResetPasswordFilter(session);
+
+  @Before
+  public void before() {
+    // set URI to valid for redirect
+    when(request.getRequestURI()).thenReturn("/");
+    when(request.getContextPath()).thenReturn("");
+
+    // set reset password conditions
+    when(session.hasSession()).thenReturn(true);
+    when(session.isLoggedIn()).thenReturn(true);
+    when(session.shouldResetPassword()).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_reset_password_set() throws Exception {
+    underTest.doFilter(request, response, chain);
+
+    verify(response).sendRedirect(eq("/account/reset_password"));
+  }
+
+  @Test
+  public void redirect_if_reset_password_set_and_web_context_configured() throws Exception {
+    when(request.getContextPath()).thenReturn("/sonarqube");
+
+    underTest.doFilter(request, response, chain);
+
+    verify(response).sendRedirect(eq("/sonarqube/account/reset_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(eq("/sonarqube/account/reset_password"));
+  }
+
+  @Test
+  public void do_not_redirect_if_no_session() throws Exception {
+    when(session.hasSession()).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_reset_password_not_set() throws Exception {
+    when(session.shouldResetPassword()).thenReturn(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/*"},
+      {"/account/reset_password"},
+    };
+  }
+
+}
index ffd301b19617f46b68d32aec9601d0ec4dc490b1..b75577598bb49cb6ae27b74c3b7c9f5ef0ef3d0c 100644 (file)
@@ -34,12 +34,14 @@ public class SafeModeUserSessionTest {
     assertThat(underTest.getLogin()).isNull();
     assertThat(underTest.getUuid()).isNull();
     assertThat(underTest.isLoggedIn()).isFalse();
+    assertThat(underTest.shouldResetPassword()).isFalse();
     assertThat(underTest.getName()).isNull();
     assertThat(underTest.getGroups()).isEmpty();
   }
 
   @Test
   public void session_has_no_permissions() {
+    assertThat(underTest.shouldResetPassword()).isFalse();
     assertThat(underTest.isRoot()).isFalse();
     assertThat(underTest.isSystemAdministrator()).isFalse();
     assertThat(underTest.hasPermissionImpl(GlobalPermission.ADMINISTER)).isFalse();
index 197e2fb91c2fa0fbb74178468586f9a5cb01ecc2..10f1e7f6eee0f4cd76491fdb17cd4697b543890b 100644 (file)
@@ -49,6 +49,7 @@ public class DoPrivilegedTest {
     assertThat(catcher.userSession.isLoggedIn()).isFalse();
     assertThat(catcher.userSession.hasComponentPermission("any permission", new ComponentDto())).isTrue();
     assertThat(catcher.userSession.isSystemAdministrator()).isTrue();
+    assertThat(catcher.userSession.shouldResetPassword()).isFalse();
 
     // verify session in place after task is done
     assertThat(threadLocalUserSession.get()).isSameAs(session);
index 6e2708f4d9e55578098ee6548b717c3944476a7e..c0c3ac23b98fafd919a9cb0e30bc7210de928e51 100644 (file)
@@ -21,9 +21,9 @@ package org.sonar.server.user;
 
 import java.util.Arrays;
 import javax.annotation.Nullable;
+import org.assertj.core.api.ThrowableAssert.ThrowingCallable;
 import org.junit.Rule;
 import org.junit.Test;
-import org.junit.rules.ExpectedException;
 import org.sonar.api.utils.System2;
 import org.sonar.api.web.UserRole;
 import org.sonar.db.DbClient;
@@ -36,6 +36,7 @@ import org.sonar.server.exceptions.ForbiddenException;
 import static com.google.common.base.Preconditions.checkState;
 import static java.util.Arrays.asList;
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.sonar.core.permission.GlobalPermissions.PROVISIONING;
 import static org.sonar.core.permission.GlobalPermissions.SYSTEM_ADMIN;
 import static org.sonar.db.component.ComponentTesting.newChildComponent;
@@ -48,8 +49,6 @@ public class ServerUserSessionTest {
 
   @Rule
   public DbTester db = DbTester.create(System2.INSTANCE);
-  @Rule
-  public ExpectedException expectedException = ExpectedException.none();
   private DbClient dbClient = db.getDbClient();
 
   @Test
@@ -61,6 +60,23 @@ public class ServerUserSessionTest {
     assertThat(session.isLoggedIn()).isFalse();
   }
 
+  @Test
+  public void shouldResetPassword_is_false_on_anonymous() {
+    assertThat(newAnonymousSession().shouldResetPassword()).isFalse();
+  }
+
+  @Test
+  public void shouldResetPassword_is_false_if_set_on_UserDto() {
+    UserDto user = db.users().insertUser(userDto -> userDto.setResetPassword(false));
+    assertThat(newUserSession(user).shouldResetPassword()).isFalse();
+  }
+
+  @Test
+  public void shouldResetPassword_is_true_if_set_on_UserDto() {
+    UserDto user = db.users().insertUser(userDto -> userDto.setResetPassword(true));
+    assertThat(newUserSession(user).shouldResetPassword()).isTrue();
+  }
+
   @Test
   public void getGroups_is_empty_on_anonymous() {
     assertThat(newAnonymousSession().getGroups()).isEmpty();
@@ -113,9 +129,7 @@ public class ServerUserSessionTest {
     UserDto user = db.users().insertUser();
     UserSession underTest = newUserSession(user);
 
-    expectInsufficientPrivilegesForbiddenException();
-
-    underTest.checkIsRoot();
+    assertThatForbiddenExceptionIsThrown(underTest::checkIsRoot);
   }
 
   @Test
@@ -163,18 +177,14 @@ public class ServerUserSessionTest {
     db.users().insertProjectPermissionOnUser(user, UserRole.USER, project);
     UserSession session = newUserSession(user);
 
-    expectInsufficientPrivilegesForbiddenException();
-
-    session.checkComponentUuidPermission(UserRole.USER, "another-uuid");
+    assertThatForbiddenExceptionIsThrown(() -> session.checkComponentUuidPermission(UserRole.USER, "another-uuid"));
   }
 
   @Test
   public void checkPermission_throws_ForbiddenException_when_user_doesnt_have_the_specified_permission() {
     UserDto user = db.users().insertUser();
 
-    expectInsufficientPrivilegesForbiddenException();
-
-    newUserSession(user).checkPermission(PROVISION_PROJECTS);
+    assertThatForbiddenExceptionIsThrown(() -> newUserSession(user).checkPermission(PROVISION_PROJECTS));
   }
 
   @Test
@@ -565,10 +575,9 @@ public class ServerUserSessionTest {
 
     UserSession session = newUserSession(user);
 
-    expectedException.expect(ForbiddenException.class);
-    expectedException.expectMessage("Insufficient privileges");
-
-    session.checkIsSystemAdministrator();
+    assertThatThrownBy(session::checkIsSystemAdministrator)
+      .isInstanceOf(ForbiddenException.class)
+      .hasMessage("Insufficient privileges");
   }
 
   @Test
@@ -596,9 +605,10 @@ public class ServerUserSessionTest {
     return newUserSession(null);
   }
 
-  private void expectInsufficientPrivilegesForbiddenException() {
-    expectedException.expect(ForbiddenException.class);
-    expectedException.expectMessage("Insufficient privileges");
+  private void assertThatForbiddenExceptionIsThrown(ThrowingCallable shouldRaiseThrowable) {
+    assertThatThrownBy(shouldRaiseThrowable)
+      .isInstanceOf(ForbiddenException.class)
+      .hasMessage("Insufficient privileges");
   }
 
 }
index 9896a6f2d8443d4802e86ff7c628ddad6ab08c4b..0375b6e7a4f06da9198adc3ab34337405a87c720 100644 (file)
@@ -21,9 +21,7 @@ package org.sonar.server.user;
 
 import org.junit.After;
 import org.junit.Before;
-import org.junit.Rule;
 import org.junit.Test;
-import org.junit.rules.ExpectedException;
 import org.sonar.db.user.GroupDto;
 import org.sonar.db.user.GroupTesting;
 import org.sonar.server.exceptions.UnauthorizedException;
@@ -31,14 +29,12 @@ import org.sonar.server.tester.AnonymousMockUserSession;
 import org.sonar.server.tester.MockUserSession;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 public class ThreadLocalUserSessionTest {
 
   private ThreadLocalUserSession threadLocalUserSession = new ThreadLocalUserSession();
 
-  @Rule
-  public ExpectedException thrown = ExpectedException.none();
-
   @Before
   public void setUp() {
     // for test isolation
@@ -56,6 +52,7 @@ public class ThreadLocalUserSessionTest {
     GroupDto group = GroupTesting.newGroupDto();
     MockUserSession expected = new MockUserSession("karadoc")
       .setUuid("karadoc-uuid")
+      .setResetPassword(true)
       .setGroups(group);
     threadLocalUserSession.set(expected);
 
@@ -64,6 +61,7 @@ public class ThreadLocalUserSessionTest {
     assertThat(threadLocalUserSession.getLogin()).isEqualTo("karadoc");
     assertThat(threadLocalUserSession.getUuid()).isEqualTo("karadoc-uuid");
     assertThat(threadLocalUserSession.isLoggedIn()).isTrue();
+    assertThat(threadLocalUserSession.shouldResetPassword()).isTrue();
     assertThat(threadLocalUserSession.getGroups()).extracting(GroupDto::getUuid).containsOnly(group.getUuid());
   }
 
@@ -76,13 +74,14 @@ public class ThreadLocalUserSessionTest {
     assertThat(session).isSameAs(expected);
     assertThat(threadLocalUserSession.getLogin()).isNull();
     assertThat(threadLocalUserSession.isLoggedIn()).isFalse();
+    assertThat(threadLocalUserSession.shouldResetPassword()).isFalse();
     assertThat(threadLocalUserSession.getGroups()).isEmpty();
   }
 
   @Test
   public void throw_UnauthorizedException_when_no_session() {
-    thrown.expect(UnauthorizedException.class);
-    threadLocalUserSession.get();
+    assertThatThrownBy(() -> threadLocalUserSession.get())
+      .isInstanceOf(UnauthorizedException.class);
   }
 
 }
index b50f53ef7c6851ac44cd0d33b470a40ac93337b0..63995f29ce7c67c0539a782ba5a3f4b974958b91 100644 (file)
@@ -21,10 +21,10 @@ package org.sonar.server.user;
 
 import com.google.common.collect.Multimap;
 import java.util.List;
+import java.util.function.Consumer;
 import org.elasticsearch.search.SearchHit;
 import org.junit.Rule;
 import org.junit.Test;
-import org.junit.rules.ExpectedException;
 import org.sonar.api.config.internal.MapSettings;
 import org.sonar.api.impl.utils.AlwaysIncreasingSystem2;
 import org.sonar.api.utils.System2;
@@ -48,6 +48,7 @@ import org.sonar.server.usergroups.DefaultGroupFinder;
 import static java.util.Arrays.asList;
 import static java.util.Collections.singletonList;
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.assertj.core.api.Assertions.tuple;
 import static org.assertj.core.data.MapEntry.entry;
 import static org.mockito.Mockito.mock;
@@ -59,11 +60,11 @@ import static org.sonar.db.user.UserTesting.newUserDto;
 public class UserUpdaterUpdateTest {
 
   private static final String DEFAULT_LOGIN = "marius";
+  private static final Consumer<UserDto> EMPTY_USER_CONSUMER = userDto -> {
+  };
 
   private System2 system2 = new AlwaysIncreasingSystem2();
 
-  @Rule
-  public ExpectedException expectedException = ExpectedException.none();
   @Rule
   public EsTester es = EsTester.create();
   @Rule
@@ -371,6 +372,30 @@ public class UserUpdaterUpdateTest {
     assertThat(dto.getEmail()).isEqualTo("marius@lesbronzes.fr");
   }
 
+  @Test
+  public void update_user_password_set_reset_password_flag_to_false() {
+    UserDto user = db.users().insertUser(newLocalUser(DEFAULT_LOGIN, "Marius", "marius@lesbronzes.fr")
+      .setScmAccounts(asList("ma", "marius33"))
+      .setSalt("salt")
+      .setResetPassword(true)
+      .setCryptedPassword("crypted password"));
+    createDefaultGroup();
+
+    underTest.updateAndCommit(session, user, new UpdateUser()
+      .setPassword("password2"), u -> {
+      });
+
+    UserDto dto = dbClient.userDao().selectByLogin(session, DEFAULT_LOGIN);
+    assertThat(dto.getSalt()).isNotEqualTo("salt");
+    assertThat(dto.getCryptedPassword()).isNotEqualTo("crypted password");
+    assertThat(dto.isResetPassword()).isFalse();
+
+    // Following fields has not changed
+    assertThat(dto.getName()).isEqualTo("Marius");
+    assertThat(dto.getScmAccountsAsList()).containsOnly("ma", "marius33");
+    assertThat(dto.getEmail()).isEqualTo("marius@lesbronzes.fr");
+  }
+
   @Test
   public void update_only_external_id() {
     UserDto user = db.users().insertUser(newExternalUser(DEFAULT_LOGIN, "Marius", "marius@email.com")
@@ -474,11 +499,11 @@ public class UserUpdaterUpdateTest {
   public void fail_to_set_null_password_when_local_user() {
     UserDto user = db.users().insertUser(newLocalUser(DEFAULT_LOGIN, "Marius", "marius@email.com"));
     createDefaultGroup();
-    expectedException.expect(BadRequestException.class);
-    expectedException.expectMessage("Password can't be empty");
 
-    underTest.updateAndCommit(session, user, new UpdateUser().setPassword(null), u -> {
-    });
+    UpdateUser updateUser = new UpdateUser().setPassword(null);
+    assertThatThrownBy(() -> underTest.updateAndCommit(session, user, updateUser, EMPTY_USER_CONSUMER))
+      .isInstanceOf(BadRequestException.class)
+      .hasMessage("Password can't be empty");
   }
 
   @Test
@@ -487,11 +512,11 @@ public class UserUpdaterUpdateTest {
       .setLogin(DEFAULT_LOGIN)
       .setLocal(false));
     createDefaultGroup();
-    expectedException.expect(BadRequestException.class);
-    expectedException.expectMessage("Password cannot be changed when external authentication is used");
 
-    underTest.updateAndCommit(session, user, new UpdateUser().setPassword("password2"), u -> {
-    });
+    UpdateUser updateUser = new UpdateUser().setPassword("password2");
+    assertThatThrownBy(() -> underTest.updateAndCommit(session, user, updateUser, EMPTY_USER_CONSUMER))
+      .isInstanceOf(BadRequestException.class)
+      .hasMessage("Password cannot be changed when external authentication is used");
   }
 
   @Test
@@ -539,50 +564,52 @@ public class UserUpdaterUpdateTest {
     db.users().insertUser(newLocalUser("john", "John", "john@email.com").setScmAccounts(singletonList("jo")));
     createDefaultGroup();
 
-    expectedException.expect(BadRequestException.class);
-    expectedException.expectMessage("The scm account 'jo' is already used by user(s) : 'John (john)'");
-
-    underTest.updateAndCommit(session, user, new UpdateUser()
+    UpdateUser updateUser = new UpdateUser()
       .setName("Marius2")
       .setEmail("marius2@mail.com")
       .setPassword("password2")
-      .setScmAccounts(asList("jo")), u -> {
-      });
+      .setScmAccounts(asList("jo"));
+
+    assertThatThrownBy(() -> underTest.updateAndCommit(session, user, updateUser, EMPTY_USER_CONSUMER))
+      .isInstanceOf(BadRequestException.class)
+      .hasMessage("The scm account 'jo' is already used by user(s) : 'John (john)'");
   }
 
   @Test
   public void fail_to_update_user_when_scm_account_is_user_login() {
     UserDto user = db.users().insertUser(newLocalUser(DEFAULT_LOGIN, "Marius", "marius@lesbronzes.fr"));
     createDefaultGroup();
-    expectedException.expect(BadRequestException.class);
-    expectedException.expectMessage("Login and email are automatically considered as SCM accounts");
 
-    underTest.updateAndCommit(session, user, new UpdateUser().setScmAccounts(asList(DEFAULT_LOGIN)), u -> {
-    });
+    UpdateUser updateUser = new UpdateUser().setScmAccounts(asList(DEFAULT_LOGIN));
+
+    assertThatThrownBy(() -> underTest.updateAndCommit(session, user, updateUser, EMPTY_USER_CONSUMER))
+      .isInstanceOf(BadRequestException.class)
+      .hasMessage("Login and email are automatically considered as SCM accounts");
   }
 
   @Test
   public void fail_to_update_user_when_scm_account_is_existing_user_email() {
     UserDto user = db.users().insertUser(newLocalUser(DEFAULT_LOGIN, "Marius", "marius@lesbronzes.fr"));
     createDefaultGroup();
-    expectedException.expect(BadRequestException.class);
-    expectedException.expectMessage("Login and email are automatically considered as SCM accounts");
 
-    underTest.updateAndCommit(session, user, new UpdateUser().setScmAccounts(asList("marius@lesbronzes.fr")), u -> {
-    });
+    UpdateUser updateUser = new UpdateUser().setScmAccounts(asList("marius@lesbronzes.fr"));
+    assertThatThrownBy(() -> underTest.updateAndCommit(session, user, updateUser, EMPTY_USER_CONSUMER))
+      .isInstanceOf(BadRequestException.class)
+      .hasMessage("Login and email are automatically considered as SCM accounts");
   }
 
   @Test
   public void fail_to_update_user_when_scm_account_is_new_user_email() {
     UserDto user = db.users().insertUser(newLocalUser(DEFAULT_LOGIN, "Marius", "marius@lesbronzes.fr"));
     createDefaultGroup();
-    expectedException.expect(BadRequestException.class);
-    expectedException.expectMessage("Login and email are automatically considered as SCM accounts");
 
-    underTest.updateAndCommit(session, user, new UpdateUser()
+    UpdateUser updateUser = new UpdateUser()
       .setEmail("marius@newmail.com")
-      .setScmAccounts(asList("marius@newmail.com")), u -> {
-      });
+      .setScmAccounts(singletonList("marius@newmail.com"));
+
+    assertThatThrownBy(() -> underTest.updateAndCommit(session, user, updateUser, EMPTY_USER_CONSUMER))
+      .isInstanceOf(BadRequestException.class)
+      .hasMessage("Login and email are automatically considered as SCM accounts");
   }
 
   @Test
@@ -590,11 +617,11 @@ public class UserUpdaterUpdateTest {
     UserDto user = db.users().insertUser();
     createDefaultGroup();
 
-    expectedException.expect(BadRequestException.class);
-    expectedException.expectMessage("Use only letters, numbers, and .-_@ please.");
+    UpdateUser updateUser = new UpdateUser().setLogin("With space");
 
-    underTest.updateAndCommit(session, user, new UpdateUser().setLogin("With space"), u -> {
-    });
+    assertThatThrownBy(() -> underTest.updateAndCommit(session, user, updateUser, EMPTY_USER_CONSUMER))
+      .isInstanceOf(BadRequestException.class)
+      .hasMessage("Use only letters, numbers, and .-_@ please.");
   }
 
   @Test
@@ -603,11 +630,10 @@ public class UserUpdaterUpdateTest {
     UserDto user = db.users().insertUser(u -> u.setActive(false));
     UserDto existingUser = db.users().insertUser(u -> u.setLogin("existing_login"));
 
-    expectedException.expect(IllegalArgumentException.class);
-    expectedException.expectMessage("A user with login 'existing_login' already exists");
-
-    underTest.updateAndCommit(session, user, new UpdateUser().setLogin(existingUser.getLogin()), u -> {
-    });
+    UpdateUser updateUser = new UpdateUser().setLogin(existingUser.getLogin());
+    assertThatThrownBy(() -> underTest.updateAndCommit(session, user, updateUser, EMPTY_USER_CONSUMER))
+      .isInstanceOf(IllegalArgumentException.class)
+      .hasMessage("A user with login 'existing_login' already exists");
   }
 
   @Test
@@ -616,12 +642,16 @@ public class UserUpdaterUpdateTest {
     UserDto user = db.users().insertUser(u -> u.setActive(false));
     UserDto existingUser = db.users().insertUser(u -> u.setExternalId("existing_external_id").setExternalIdentityProvider("existing_external_provider"));
 
-    expectedException.expect(IllegalArgumentException.class);
-    expectedException.expectMessage("A user with provider id 'existing_external_id' and identity provider 'existing_external_provider' already exists");
+    UpdateUser updateUser = new UpdateUser()
+      .setExternalIdentity(
+        new ExternalIdentity(
+          existingUser.getExternalIdentityProvider(),
+          existingUser.getExternalLogin(),
+          existingUser.getExternalId()));
 
-    underTest.updateAndCommit(session, user, new UpdateUser()
-      .setExternalIdentity(new ExternalIdentity(existingUser.getExternalIdentityProvider(), existingUser.getExternalLogin(), existingUser.getExternalId())), u -> {
-      });
+    assertThatThrownBy(() -> underTest.updateAndCommit(session, user, updateUser, EMPTY_USER_CONSUMER))
+      .isInstanceOf(IllegalArgumentException.class)
+      .hasMessage("A user with provider id 'existing_external_id' and identity provider 'existing_external_provider' already exists");
   }
 
   private GroupDto createDefaultGroup() {
index dd2f1c208ecb5f18c2a39f48ed6ca8ac131da029..2458d7079e4d691465f4732f9d9ec2e2cd08a058 100644 (file)
@@ -44,6 +44,7 @@ public abstract class AbstractMockUserSession<T extends AbstractMockUserSession>
   private Map<String, String> projectUuidByComponentUuid = new HashMap<>();
   private Set<String> projectPermissions = new HashSet<>();
   private boolean systemAdministrator = false;
+  private boolean resetPassword = false;
 
   protected AbstractMockUserSession(Class<T> clazz) {
     this.clazz = clazz;
@@ -137,4 +138,13 @@ public abstract class AbstractMockUserSession<T extends AbstractMockUserSession>
     return isRoot() || systemAdministrator;
   }
 
+  public T setResetPassword(boolean b) {
+    this.resetPassword = b;
+    return clazz.cast(this);
+  }
+
+  @Override
+  public boolean shouldResetPassword() {
+    return resetPassword;
+  }
 }
index 37fb591cde1447a56cbece3106295a9768585bde..3a01b2fa171a9faa442ecb561a2e0c45f16554df 100644 (file)
@@ -40,7 +40,8 @@ public class AnonymousMockUserSession extends AbstractMockUserSession<AnonymousM
     return null;
   }
 
-  @Override public String getUuid() {
+  @Override
+  public String getUuid() {
     return null;
   }
 
index 05c58ae67e0f1f2932a83d5cab81843b3636b677..864cbd13cb38abda7182c7151dceec518627eafb 100644 (file)
@@ -283,6 +283,11 @@ public class UserSessionRule implements TestRule, UserSession {
     return currentUserSession.getGroups();
   }
 
+  @Override
+  public boolean shouldResetPassword() {
+    return currentUserSession.shouldResetPassword();
+  }
+
   @Override
   public Optional<IdentityProvider> getIdentityProvider() {
     return currentUserSession.getIdentityProvider();
index 89bd75a2462fe256c75d6de0f35190812fca178a..14bd327a6d264b14c59119ed4b33eb0cfffc6f15 100644 (file)
@@ -81,6 +81,11 @@ public class TestUserSessionFactory implements UserSessionFactory {
       throw notImplemented();
     }
 
+    @Override
+    public boolean shouldResetPassword() {
+      return user != null && user.isResetPassword();
+    }
+
     @Override
     public Optional<IdentityProvider> getIdentityProvider() {
       throw notImplemented();
index aa87a67963ac2565fc12ba20ede4b55d15c70d7c..9a9bc56cdf49c0e6e615dd81db109a541c85f8a7 100644 (file)
@@ -46,7 +46,8 @@ public class UserSessionFilter implements Filter {
     this.platform = PlatformImpl.getInstance();
   }
 
-  @VisibleForTesting UserSessionFilter(Platform platform) {
+  @VisibleForTesting
+  UserSessionFilter(Platform platform) {
     this.platform = platform;
   }