]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-10338 Allow authentication of user using an exising email
authorJulien Lancelot <julien.lancelot@sonarsource.com>
Tue, 30 Jan 2018 12:31:10 +0000 (13:31 +0100)
committerJulien Lancelot <julien.lancelot@sonarsource.com>
Wed, 7 Feb 2018 15:43:01 +0000 (16:43 +0100)
31 files changed:
server/sonar-db-dao/src/main/java/org/sonar/db/user/UserDao.java
server/sonar-db-dao/src/main/java/org/sonar/db/user/UserMapper.java
server/sonar-db-dao/src/main/resources/org/sonar/db/user/UserMapper.xml
server/sonar-db-dao/src/test/java/org/sonar/db/user/UserDaoTest.java
server/sonar-server/src/main/java/org/sonar/server/authentication/AuthenticationError.java
server/sonar-server/src/main/java/org/sonar/server/authentication/AuthenticationModule.java
server/sonar-server/src/main/java/org/sonar/server/authentication/AuthenticationRedirection.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/authentication/BaseContextFactory.java
server/sonar-server/src/main/java/org/sonar/server/authentication/Cookies.java
server/sonar-server/src/main/java/org/sonar/server/authentication/EmailAlreadyExistsException.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/authentication/InitFilter.java
server/sonar-server/src/main/java/org/sonar/server/authentication/OAuth2AuthenticationParameters.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/authentication/OAuth2AuthenticationParametersImpl.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/authentication/OAuth2CallbackFilter.java
server/sonar-server/src/main/java/org/sonar/server/authentication/OAuth2ContextFactory.java
server/sonar-server/src/main/java/org/sonar/server/authentication/OAuth2Redirection.java [deleted file]
server/sonar-server/src/main/java/org/sonar/server/authentication/RealmAuthenticator.java
server/sonar-server/src/main/java/org/sonar/server/authentication/SsoAuthenticator.java
server/sonar-server/src/main/java/org/sonar/server/authentication/UserIdentityAuthenticator.java
server/sonar-server/src/main/java/org/sonar/server/user/UserUpdater.java
server/sonar-server/src/test/java/org/sonar/server/authentication/BaseContextFactoryTest.java
server/sonar-server/src/test/java/org/sonar/server/authentication/InitFilterTest.java
server/sonar-server/src/test/java/org/sonar/server/authentication/OAuth2AuthenticationParametersImplTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/authentication/OAuth2CallbackFilterTest.java
server/sonar-server/src/test/java/org/sonar/server/authentication/OAuth2ContextFactoryTest.java
server/sonar-server/src/test/java/org/sonar/server/authentication/OAuth2RedirectionTest.java [deleted file]
server/sonar-server/src/test/java/org/sonar/server/authentication/RealmAuthenticatorTest.java
server/sonar-server/src/test/java/org/sonar/server/authentication/UserIdentityAuthenticatorTest.java
server/sonar-server/src/test/java/org/sonar/server/user/UserUpdaterCreateTest.java
server/sonar-server/src/test/java/org/sonar/server/user/UserUpdaterUpdateTest.java
tests/src/test/java/org/sonarqube/tests/Category4Suite.java

index 351a9404d6f3df3f5684b7dd345ee12187319c65..c063482fc98bb38e4c0f344fa2218365463ca428 100644 (file)
@@ -152,12 +152,13 @@ public class UserDao implements Dao {
   }
 
   /**
-   * Check if an active user with the given email exits in database
+   * Search for an active user with the given email exits in database
    *
    * Please note that email is case insensitive, result for searching 'mail@email.com' or 'Mail@Email.com' will be the same
    */
-  public boolean doesEmailExist(DbSession dbSession, String email) {
-    return mapper(dbSession).countByEmail(email.toLowerCase(Locale.ENGLISH)) > 0;
+  @CheckForNull
+  public UserDto selectByEmail(DbSession dbSession, String email) {
+    return mapper(dbSession).selectByEmail(email.toLowerCase(Locale.ENGLISH));
   }
 
   public void scrollByLogins(DbSession dbSession, Collection<String> logins, Consumer<UserDto> consumer) {
index cd2fc8572dc4a0bda77b42b14c21226826f33fb4..5d816dab951d9957e6452fb8f6f08f2d5ff89082 100644 (file)
@@ -52,9 +52,10 @@ public interface UserMapper {
 
   List<UserDto> selectByIds(@Param("ids") List<Integer> ids);
 
-  void scrollAll(ResultHandler<UserDto> handler);
+  @CheckForNull
+  UserDto selectByEmail(String email);
 
-  long countByEmail(String email);
+  void scrollAll(ResultHandler<UserDto> handler);
 
   /**
    * Count actives users which are root and which login is not the specified one.
index da99028da2c905d2605314c2780e41208851c128..d7c568696e5eb4d4bdc252df0b60db0004ffd6ba 100644 (file)
     ORDER BY u.name
   </select>
 
-  <select id="countByEmail" parameterType="String" resultType="long">
-    SELECT count(1)
+  <select id="selectByEmail" parameterType="String" resultType="User">
+    SELECT
+    <include refid="userColumns"/>
     FROM users u
-    where lower(u.email)=#{email} AND u.active=${_true}
+    WHERE lower(u.email)=#{email, jdbcType=VARCHAR}
+    AND u.active=${_true}
   </select>
 
   <select id="countRootUsersButLogin" parameterType="String" resultType="long">
index e896d039dade6cf6e59e6574a4c604cd95371cf5..e4e219ec6654a74cc21a763e6e5248bd3159ebe6 100644 (file)
@@ -547,13 +547,13 @@ public class UserDaoTest {
   }
 
   @Test
-  public void exists_by_email() {
-    UserDto activeUser = insertActiveUser();
-    UserDto disableUser = insertUser(false);
+  public void select_by_email() {
+    UserDto activeUser = db.users().insertUser();
+    UserDto disableUser = db.users().insertUser(u -> u.setActive(false));
 
-    assertThat(underTest.doesEmailExist(session, activeUser.getEmail())).isTrue();
-    assertThat(underTest.doesEmailExist(session, disableUser.getEmail())).isFalse();
-    assertThat(underTest.doesEmailExist(session, "unknown")).isFalse();
+    assertThat(underTest.selectByEmail(session, activeUser.getEmail())).isNotNull();
+    assertThat(underTest.selectByEmail(session, disableUser.getEmail())).isNull();
+    assertThat(underTest.selectByEmail(session, "unknown")).isNull();
   }
 
   @Test
index 1d087426193b6ec0f2c24bfe7173163fd3c3b558..81ccb80a258ec6f90a5d50112a025628802a5497 100644 (file)
  */
 package org.sonar.server.authentication;
 
-import java.io.IOException;
-import java.io.UnsupportedEncodingException;
 import javax.servlet.http.HttpServletResponse;
 import org.sonar.api.utils.log.Logger;
 import org.sonar.api.utils.log.Loggers;
 import org.sonar.server.authentication.event.AuthenticationException;
 
 import static java.lang.String.format;
-import static java.net.URLEncoder.encode;
-import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.sonar.server.authentication.AuthenticationRedirection.encodeMessage;
+import static org.sonar.server.authentication.AuthenticationRedirection.redirectTo;
 
 final class AuthenticationError {
 
@@ -62,23 +60,8 @@ final class AuthenticationError {
     return contextPath + format(UNAUTHORIZED_PATH_WITH_MESSAGE, encodeMessage(publicMessage));
   }
 
-  private static String encodeMessage(String message) {
-    try {
-      return encode(message, UTF_8.name());
-    } catch (UnsupportedEncodingException e) {
-      throw new IllegalStateException(format("Fail to encode %s", message), e);
-    }
-  }
-
   public static void redirectToUnauthorized(HttpServletResponse response) {
     redirectTo(response, UNAUTHORIZED_PATH);
   }
 
-  private static void redirectTo(HttpServletResponse response, String url) {
-    try {
-      response.sendRedirect(url);
-    } catch (IOException e) {
-      throw new IllegalStateException(format("Fail to redirect to %s", url), e);
-    }
-  }
 }
index 76dcba9e10667afbe7923f5430cf63c6a0452ac7..3ed62249fc403fb8cd5793360f84680b74ccc995 100644 (file)
@@ -43,7 +43,7 @@ public class AuthenticationModule extends Module {
       JwtSerializer.class,
       JwtHttpHandler.class,
       JwtCsrfVerifier.class,
-      OAuth2Redirection.class,
+      OAuth2AuthenticationParametersImpl.class,
       LoginAction.class,
       LogoutAction.class,
       CredentialsAuthenticator.class,
diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/AuthenticationRedirection.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/AuthenticationRedirection.java
new file mode 100644 (file)
index 0000000..27ebcd9
--- /dev/null
@@ -0,0 +1,52 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.io.IOException;
+import java.io.UnsupportedEncodingException;
+import javax.servlet.http.HttpServletResponse;
+
+import static java.lang.String.format;
+import static java.net.URLEncoder.encode;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+class AuthenticationRedirection {
+
+  private AuthenticationRedirection() {
+    // Only static methods
+  }
+
+  static String encodeMessage(String message) {
+    try {
+      return encode(message, UTF_8.name());
+    } catch (UnsupportedEncodingException e) {
+      throw new IllegalStateException(format("Fail to encode %s", message), e);
+    }
+  }
+
+  static void redirectTo(HttpServletResponse response, String url) {
+    try {
+      response.sendRedirect(url);
+    } catch (IOException e) {
+      throw new IllegalStateException(format("Fail to redirect to %s", url), e);
+    }
+  }
+}
index 2f728fe239bdd8b8c23defafeb74948b2d7a4758..8ebb25b8debb2bd18b60637c2fe473781ecdfc54 100644 (file)
@@ -25,10 +25,11 @@ import org.sonar.api.platform.Server;
 import org.sonar.api.server.authentication.BaseIdentityProvider;
 import org.sonar.api.server.authentication.UserIdentity;
 import org.sonar.db.user.UserDto;
+import org.sonar.server.authentication.event.AuthenticationEvent.Source;
 import org.sonar.server.user.ThreadLocalUserSession;
 import org.sonar.server.user.UserSessionFactory;
 
-import static org.sonar.server.authentication.event.AuthenticationEvent.Source;
+import static org.sonar.server.authentication.UserIdentityAuthenticator.ExistingEmailStrategy.FORBID;
 
 public class BaseContextFactory {
 
@@ -79,7 +80,7 @@ public class BaseContextFactory {
 
     @Override
     public void authenticate(UserIdentity userIdentity) {
-      UserDto userDto = userIdentityAuthenticator.authenticate(userIdentity, identityProvider, Source.external(identityProvider));
+      UserDto userDto = userIdentityAuthenticator.authenticate(userIdentity, identityProvider, Source.external(identityProvider), FORBID);
       jwtHttpHandler.generateToken(userDto, request, response);
       threadLocalUserSession.set(userSessionFactory.create(userDto));
     }
index 3af60437264ea4f623674ddaaa26a5eba83fcc49..175240a7f890df75896b5bdb1b706516cdb3943c 100644 (file)
@@ -28,6 +28,11 @@ import javax.servlet.http.HttpServletRequest;
 import static com.google.common.base.Strings.isNullOrEmpty;
 import static java.util.Objects.requireNonNull;
 
+/**
+ * Helper class to create a {@link javax.servlet.http.Cookie}.
+ *
+ * The {@link javax.servlet.http.Cookie#secure} will automatically be set.
+ */
 public class Cookies {
 
   private static final String HTTPS_HEADER = "X-Forwarded-Proto";
@@ -64,21 +69,33 @@ public class Cookies {
       this.request = request;
     }
 
+    /**
+     * Name of the cookie
+     */
     public CookieBuilder setName(String name) {
       this.name = requireNonNull(name);
       return this;
     }
 
+    /**
+     * Name of the cookie
+     */
     public CookieBuilder setValue(@Nullable String value) {
       this.value = value;
       return this;
     }
 
+    /**
+     * Sets the flag that controls if this cookie will be hidden from scripts on the client side.
+     */
     public CookieBuilder setHttpOnly(boolean httpOnly) {
       this.httpOnly = httpOnly;
       return this;
     }
 
+    /**
+     * Sets the maximum age of the cookie in seconds.
+     */
     public CookieBuilder setExpiry(int expiry) {
       this.expiry = expiry;
       return this;
diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/EmailAlreadyExistsException.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/EmailAlreadyExistsException.java
new file mode 100644 (file)
index 0000000..8e127a2
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.server.authentication.IdentityProvider;
+import org.sonar.api.server.authentication.UserIdentity;
+import org.sonar.db.user.UserDto;
+
+import static java.lang.String.format;
+import static org.sonar.server.authentication.AuthenticationRedirection.encodeMessage;
+
+/**
+ * This exception is used to redirect the user to a page explaining him that his email is already used by another account,
+ * and where he has the ability to authenticate by "steeling" this email.
+ */
+public class EmailAlreadyExistsException extends RuntimeException {
+
+  private static final String PATH = "/sessions/email_already_exists?email=%s&login=%s&provider=%s&existingLogin=%s&existingProvider=%s";
+
+  private final String email;
+  private final UserDto existingUser;
+  private final UserIdentity userIdentity;
+  private final IdentityProvider provider;
+
+  EmailAlreadyExistsException(String email, UserDto existingUser, UserIdentity userIdentity, IdentityProvider provider) {
+    this.email = email;
+    this.existingUser = existingUser;
+    this.userIdentity = userIdentity;
+    this.provider = provider;
+  }
+
+  public String getPath(String contextPath) {
+    return contextPath + format(PATH,
+      encodeMessage(email),
+      encodeMessage(userIdentity.getProviderLogin()),
+      encodeMessage(provider.getKey()),
+      encodeMessage(existingUser.getExternalIdentity()),
+      encodeMessage(existingUser.getExternalIdentityProvider()));
+  }
+}
index 8a699c523c179664d788e793e07b8b4eb8a15d11..40244e387240beb3eb43a4aaf97d3a2b108f3f7f 100644 (file)
@@ -36,6 +36,7 @@ import org.sonar.server.authentication.event.AuthenticationException;
 import static java.lang.String.format;
 import static org.sonar.server.authentication.AuthenticationError.handleAuthenticationError;
 import static org.sonar.server.authentication.AuthenticationError.handleError;
+import static org.sonar.server.authentication.AuthenticationRedirection.redirectTo;
 import static org.sonar.server.authentication.event.AuthenticationEvent.Source;
 
 public class InitFilter extends AuthenticationFilter {
@@ -45,15 +46,15 @@ public class InitFilter extends AuthenticationFilter {
   private final BaseContextFactory baseContextFactory;
   private final OAuth2ContextFactory oAuth2ContextFactory;
   private final AuthenticationEvent authenticationEvent;
-  private final OAuth2Redirection oAuthRedirection;
+  private final OAuth2AuthenticationParameters oAuthOAuth2AuthenticationParameters;
 
   public InitFilter(IdentityProviderRepository identityProviderRepository, BaseContextFactory baseContextFactory,
-    OAuth2ContextFactory oAuth2ContextFactory, Server server, AuthenticationEvent authenticationEvent, OAuth2Redirection oAuthRedirection) {
+    OAuth2ContextFactory oAuth2ContextFactory, Server server, AuthenticationEvent authenticationEvent, OAuth2AuthenticationParameters oAuthOAuth2AuthenticationParameters) {
     super(server, identityProviderRepository);
     this.baseContextFactory = baseContextFactory;
     this.oAuth2ContextFactory = oAuth2ContextFactory;
     this.authenticationEvent = authenticationEvent;
-    this.oAuthRedirection = oAuthRedirection;
+    this.oAuthOAuth2AuthenticationParameters = oAuthOAuth2AuthenticationParameters;
   }
 
   @Override
@@ -77,16 +78,20 @@ public class InitFilter extends AuthenticationFilter {
       if (provider instanceof BaseIdentityProvider) {
         handleBaseIdentityProvider(request, response, (BaseIdentityProvider) provider);
       } else if (provider instanceof OAuth2IdentityProvider) {
+        oAuthOAuth2AuthenticationParameters.init(request, response);
         handleOAuth2IdentityProvider(request, response, (OAuth2IdentityProvider) provider);
       } else {
         handleError(response, format("Unsupported IdentityProvider class: %s", provider.getClass()));
       }
     } catch (AuthenticationException e) {
-      oAuthRedirection.delete(request, response);
+      oAuthOAuth2AuthenticationParameters.delete(request, response);
       authenticationEvent.loginFailure(request, e);
       handleAuthenticationError(e, response, getContextPath());
+    } catch (EmailAlreadyExistsException e) {
+      oAuthOAuth2AuthenticationParameters.delete(request, response);
+      redirectTo(response, e.getPath(getContextPath()));
     } catch (Exception e) {
-      oAuthRedirection.delete(request, response);
+      oAuthOAuth2AuthenticationParameters.delete(request, response);
       handleError(e, response, format("Fail to initialize authentication with provider '%s'", provider.getKey()));
     }
   }
@@ -105,7 +110,6 @@ public class InitFilter extends AuthenticationFilter {
 
   private void handleOAuth2IdentityProvider(HttpServletRequest request, HttpServletResponse response, OAuth2IdentityProvider provider) {
     try {
-      oAuthRedirection.create(request, response);
       provider.init(oAuth2ContextFactory.newContext(request, response, provider));
     } catch (UnauthorizedException e) {
       throw AuthenticationException.newBuilder()
diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/OAuth2AuthenticationParameters.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/OAuth2AuthenticationParameters.java
new file mode 100644 (file)
index 0000000..622b06c
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.Optional;
+import javax.servlet.FilterConfig;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.sonar.api.server.authentication.OAuth2IdentityProvider;
+
+/**
+ * This class is used to store some parameters during the OAuth2 authentication process, by using a cookie.
+ *
+ * Parameters are read from the request during {@link InitFilter#init(FilterConfig)}
+ * and reset when {@link OAuth2IdentityProvider.CallbackContext#redirectToRequestedPage()} is called.
+ */
+public interface OAuth2AuthenticationParameters {
+
+  void init(HttpServletRequest request, HttpServletResponse response);
+
+  Optional<String> getReturnTo(HttpServletRequest request);
+
+  Optional<Boolean> getAllowEmailShift(HttpServletRequest request);
+
+  void delete(HttpServletRequest request, HttpServletResponse response);
+
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/OAuth2AuthenticationParametersImpl.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/OAuth2AuthenticationParametersImpl.java
new file mode 100644 (file)
index 0000000..d7d1eb3
--- /dev/null
@@ -0,0 +1,133 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.reflect.TypeToken;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import java.io.UnsupportedEncodingException;
+import java.lang.reflect.Type;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import static java.net.URLDecoder.decode;
+import static java.net.URLEncoder.encode;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.apache.commons.lang.StringUtils.isNotBlank;
+import static org.sonar.server.authentication.Cookies.findCookie;
+import static org.sonar.server.authentication.Cookies.newCookieBuilder;
+
+public class OAuth2AuthenticationParametersImpl implements OAuth2AuthenticationParameters {
+
+  private static final String AUTHENTICATION_COOKIE_NAME = "AUTH-PARAMS";
+  private static final int FIVE_MINUTES_IN_SECONDS = 5 * 60;
+
+  /**
+   * The HTTP parameter that contains the path where the user should be redirect to.
+   * Please note that the web context is included.
+   */
+  private static final String RETURN_TO_PARAMETER = "return_to";
+
+  /**
+   * This parameter is used to allow the shift of email from an existing user to the authenticating user
+   */
+  private static final String ALLOW_EMAIL_SHIFT_PARAMETER = "allowEmailShift";
+
+  private static final Type JSON_MAP_TYPE = new TypeToken<HashMap<String, String>>() {
+  }.getType();
+
+  @Override
+  public void init(HttpServletRequest request, HttpServletResponse response) {
+    String returnTo = request.getParameter(RETURN_TO_PARAMETER);
+    String allowEmailShift = request.getParameter(ALLOW_EMAIL_SHIFT_PARAMETER);
+    Map<String, String> parameters = new HashMap<>();
+    if (isNotBlank(returnTo)) {
+      parameters.put(RETURN_TO_PARAMETER, returnTo);
+    }
+    if (isNotBlank(allowEmailShift)) {
+      parameters.put(ALLOW_EMAIL_SHIFT_PARAMETER, allowEmailShift);
+    }
+    if (parameters.isEmpty()) {
+      return;
+    }
+    response.addCookie(newCookieBuilder(request)
+      .setName(AUTHENTICATION_COOKIE_NAME)
+      .setValue(toJson(parameters))
+      .setHttpOnly(true)
+      .setExpiry(FIVE_MINUTES_IN_SECONDS)
+      .build());
+  }
+
+  @Override
+  public Optional<String> getReturnTo(HttpServletRequest request) {
+    return getParameter(request, RETURN_TO_PARAMETER);
+  }
+
+  @Override
+  public Optional<Boolean> getAllowEmailShift(HttpServletRequest request) {
+    Optional<String> parameter = getParameter(request, ALLOW_EMAIL_SHIFT_PARAMETER);
+    return parameter.map(Boolean::parseBoolean);
+  }
+
+  private static Optional<String> getParameter(HttpServletRequest request, String parameterKey) {
+    Optional<javax.servlet.http.Cookie> cookie = findCookie(AUTHENTICATION_COOKIE_NAME, request);
+    if (!cookie.isPresent()) {
+      return Optional.empty();
+    }
+
+    Map<String, String> parameters = fromJson(cookie.get().getValue());
+    if (parameters.isEmpty()) {
+      return Optional.empty();
+    }
+    return Optional.ofNullable(parameters.get(parameterKey));
+  }
+
+  @Override
+  public void delete(HttpServletRequest request, HttpServletResponse response) {
+    response.addCookie(newCookieBuilder(request)
+      .setName(AUTHENTICATION_COOKIE_NAME)
+      .setValue(null)
+      .setHttpOnly(true)
+      .setExpiry(0)
+      .build());
+  }
+
+  private static String toJson(Map<String, String> map) {
+    Gson gson = new GsonBuilder().create();
+    try {
+      return encode(gson.toJson(map), UTF_8.name());
+    } catch (UnsupportedEncodingException e) {
+      throw new IllegalStateException(e);
+    }
+  }
+
+  private static Map<String, String> fromJson(String json) {
+    Gson gson = new GsonBuilder().create();
+    try {
+      return gson.fromJson(decode(json, UTF_8.name()), JSON_MAP_TYPE);
+    } catch (UnsupportedEncodingException e) {
+      throw new IllegalStateException(e);
+    }
+  }
+}
index 20f84b3df652defb400bb63538548ffcf370c7f2..114050a7cee75fb122f73380c34de8c408e9f5ce 100644 (file)
@@ -37,20 +37,21 @@ import org.sonar.server.authentication.event.AuthenticationException;
 import static java.lang.String.format;
 import static org.sonar.server.authentication.AuthenticationError.handleAuthenticationError;
 import static org.sonar.server.authentication.AuthenticationError.handleError;
+import static org.sonar.server.authentication.AuthenticationRedirection.redirectTo;
 import static org.sonar.server.authentication.event.AuthenticationEvent.Source;
 
 public class OAuth2CallbackFilter extends AuthenticationFilter {
 
   private final OAuth2ContextFactory oAuth2ContextFactory;
   private final AuthenticationEvent authenticationEvent;
-  private final OAuth2Redirection oAuthRedirection;
+  private final OAuth2AuthenticationParameters oauth2Parameters;
 
   public OAuth2CallbackFilter(IdentityProviderRepository identityProviderRepository, OAuth2ContextFactory oAuth2ContextFactory,
-    Server server, AuthenticationEvent authenticationEvent, OAuth2Redirection oAuthRedirection) {
+    Server server, AuthenticationEvent authenticationEvent, OAuth2AuthenticationParameters oauth2Parameters) {
     super(server, identityProviderRepository);
     this.oAuth2ContextFactory = oAuth2ContextFactory;
     this.authenticationEvent = authenticationEvent;
-    this.oAuthRedirection = oAuthRedirection;
+    this.oauth2Parameters = oauth2Parameters;
   }
 
   @Override
@@ -77,11 +78,14 @@ public class OAuth2CallbackFilter extends AuthenticationFilter {
         handleError(response, format("Not an OAuth2IdentityProvider: %s", provider.getClass()));
       }
     } catch (AuthenticationException e) {
-      oAuthRedirection.delete(request, response);
+      oauth2Parameters.delete(request, response);
       authenticationEvent.loginFailure(request, e);
       handleAuthenticationError(e, response, getContextPath());
+    } catch (EmailAlreadyExistsException e) {
+      oauth2Parameters.delete(request, response);
+      redirectTo(response, e.getPath(getContextPath()));
     } catch (Exception e) {
-      oAuthRedirection.delete(request, response);
+      oauth2Parameters.delete(request, response);
       handleError(e, response, format("Fail to callback authentication with '%s'", provider.getKey()));
     }
   }
index a66a1777c6464acc989b08452972eaad048597c4..0696ab81f3868ab3e741ed6571562e7e20b207b9 100644 (file)
@@ -34,6 +34,8 @@ import org.sonar.server.user.UserSessionFactory;
 
 import static java.lang.String.format;
 import static org.sonar.server.authentication.OAuth2CallbackFilter.CALLBACK_PATH;
+import static org.sonar.server.authentication.UserIdentityAuthenticator.ExistingEmailStrategy.ALLOW;
+import static org.sonar.server.authentication.UserIdentityAuthenticator.ExistingEmailStrategy.WARN;
 
 @ServerSide
 public class OAuth2ContextFactory {
@@ -44,17 +46,17 @@ public class OAuth2ContextFactory {
   private final OAuthCsrfVerifier csrfVerifier;
   private final JwtHttpHandler jwtHttpHandler;
   private final UserSessionFactory userSessionFactory;
-  private final OAuth2Redirection oAuthRedirection;
+  private final OAuth2AuthenticationParameters oAuthParameters;
 
   public OAuth2ContextFactory(ThreadLocalUserSession threadLocalUserSession, UserIdentityAuthenticator userIdentityAuthenticator, Server server,
-    OAuthCsrfVerifier csrfVerifier, JwtHttpHandler jwtHttpHandler, UserSessionFactory userSessionFactory, OAuth2Redirection oAuthRedirection) {
+    OAuthCsrfVerifier csrfVerifier, JwtHttpHandler jwtHttpHandler, UserSessionFactory userSessionFactory, OAuth2AuthenticationParameters oAuthParameters) {
     this.threadLocalUserSession = threadLocalUserSession;
     this.userIdentityAuthenticator = userIdentityAuthenticator;
     this.server = server;
     this.csrfVerifier = csrfVerifier;
     this.jwtHttpHandler = jwtHttpHandler;
     this.userSessionFactory = userSessionFactory;
-    this.oAuthRedirection = oAuthRedirection;
+    this.oAuthParameters = oAuthParameters;
   }
 
   public OAuth2IdentityProvider.InitContext newContext(HttpServletRequest request, HttpServletResponse response, OAuth2IdentityProvider identityProvider) {
@@ -114,16 +116,18 @@ public class OAuth2ContextFactory {
     @Override
     public void redirectToRequestedPage() {
       try {
-        Optional<String> redirectTo = oAuthRedirection.getAndDelete(request, response);
+        Optional<String> redirectTo = oAuthParameters.getReturnTo(request);
+        oAuthParameters.delete(request, response);
         getResponse().sendRedirect(redirectTo.orElse(server.getContextPath() + "/"));
       } catch (IOException e) {
-        throw new IllegalStateException("Fail to redirect to home", e);
+        throw new IllegalStateException("Fail to redirect to requested page", e);
       }
     }
 
     @Override
     public void authenticate(UserIdentity userIdentity) {
-      UserDto userDto = userIdentityAuthenticator.authenticate(userIdentity, identityProvider, AuthenticationEvent.Source.oauth2(identityProvider));
+      Boolean allowEmailShift = oAuthParameters.getAllowEmailShift(request).orElse(false);
+      UserDto userDto = userIdentityAuthenticator.authenticate(userIdentity, identityProvider, AuthenticationEvent.Source.oauth2(identityProvider), allowEmailShift ? ALLOW : WARN);
       jwtHttpHandler.generateToken(userDto, request, response);
       threadLocalUserSession.set(userSessionFactory.create(userDto));
     }
diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/OAuth2Redirection.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/OAuth2Redirection.java
deleted file mode 100644 (file)
index 7b7c07c..0000000
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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.Optional;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-import static org.apache.commons.lang.StringUtils.isBlank;
-import static org.sonar.server.authentication.Cookies.findCookie;
-import static org.sonar.server.authentication.Cookies.newCookieBuilder;
-
-public class OAuth2Redirection {
-
-  private static final String REDIRECT_TO_COOKIE = "REDIRECT_TO";
-
-  /**
-   * The HTTP parameter that contains the path where the user should be redirect to.
-   * Please note that the web context is included.
-   */
-  private static final String RETURN_TO_PARAMETER = "return_to";
-
-  public void create(HttpServletRequest request, HttpServletResponse response) {
-    String redirectTo = request.getParameter(RETURN_TO_PARAMETER);
-    if (isBlank(redirectTo)) {
-      return;
-    }
-    response.addCookie(newCookieBuilder(request)
-      .setName(REDIRECT_TO_COOKIE)
-      .setValue(redirectTo)
-      .setHttpOnly(true)
-      .setExpiry(-1)
-      .build());
-  }
-
-  public Optional<String> getAndDelete(HttpServletRequest request, HttpServletResponse response) {
-    Optional<Cookie> cookie = findCookie(REDIRECT_TO_COOKIE, request);
-    if (!cookie.isPresent()) {
-      return Optional.empty();
-    }
-
-    delete(request, response);
-
-    String redirectTo = cookie.get().getValue();
-    if (isBlank(redirectTo)) {
-      return Optional.empty();
-    }
-    return Optional.of(redirectTo);
-  }
-
-  public void delete(HttpServletRequest request, HttpServletResponse response) {
-    response.addCookie(newCookieBuilder(request)
-      .setName(REDIRECT_TO_COOKIE)
-      .setValue(null)
-      .setHttpOnly(true)
-      .setExpiry(0)
-      .build());
-  }
-
-}
index 336d8099e7afeb3f0fc905e7f38074b81aa4766e..8f081b412910535039161157f8891a472191c7f8 100644 (file)
@@ -45,6 +45,7 @@ import org.sonar.server.user.SecurityRealmFactory;
 import static java.util.Objects.requireNonNull;
 import static org.apache.commons.lang.StringUtils.isEmpty;
 import static org.apache.commons.lang.StringUtils.trimToNull;
+import static org.sonar.server.authentication.UserIdentityAuthenticator.ExistingEmailStrategy.FORBID;
 import static org.sonar.server.user.ExternalIdentity.SQ_AUTHORITY;
 
 public class RealmAuthenticator implements Startable {
@@ -138,7 +139,7 @@ public class RealmAuthenticator implements Startable {
       Collection<String> groups = externalGroupsProvider.doGetGroups(context);
       userIdentityBuilder.setGroups(new HashSet<>(groups));
     }
-    return userIdentityAuthenticator.authenticate(userIdentityBuilder.build(), new ExternalIdentityProvider(), realmEventSource(method));
+    return userIdentityAuthenticator.authenticate(userIdentityBuilder.build(), new ExternalIdentityProvider(), realmEventSource(method), FORBID);
   }
 
   private String getLogin(String userLogin) {
index 5aa37b32d17a4f867acca25765f2c2f19156c0e7..691c806b0e0f7594ea634c5011a11646e360318b 100644 (file)
@@ -54,6 +54,7 @@ import static org.sonar.process.ProcessProperties.Property.SONAR_WEB_SSO_GROUPS_
 import static org.sonar.process.ProcessProperties.Property.SONAR_WEB_SSO_LOGIN_HEADER;
 import static org.sonar.process.ProcessProperties.Property.SONAR_WEB_SSO_NAME_HEADER;
 import static org.sonar.process.ProcessProperties.Property.SONAR_WEB_SSO_REFRESH_INTERVAL_IN_MINUTES;
+import static org.sonar.server.authentication.UserIdentityAuthenticator.ExistingEmailStrategy.FORBID;
 import static org.sonar.server.user.ExternalIdentity.SQ_AUTHORITY;
 
 public class SsoAuthenticator implements Startable {
@@ -160,7 +161,7 @@ public class SsoAuthenticator implements Startable {
       String groupsValue = getHeaderValue(headerValuesByNames, SONAR_WEB_SSO_GROUPS_HEADER.getKey());
       userIdentityBuilder.setGroups(groupsValue == null ? Collections.emptySet() : new HashSet<>(COMA_SPLITTER.splitToList(groupsValue)));
     }
-    return userIdentityAuthenticator.authenticate(userIdentityBuilder.build(), new SsoIdentityProvider(), Source.sso());
+    return userIdentityAuthenticator.authenticate(userIdentityBuilder.build(), new SsoIdentityProvider(), Source.sso(), FORBID);
   }
 
   @CheckForNull
index 2a2add51ff8752c035f541c9e38bceca074db445..95db2536fb5ec30589361bb1c35cf33b85796b40 100644 (file)
@@ -53,6 +53,24 @@ import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex;
 
 public class UserIdentityAuthenticator {
 
+  /**
+   * Strategy to be executed when the email of the user is already used by another user
+   */
+  enum ExistingEmailStrategy {
+    /**
+     * Authentication is allowed, the email is moved from other user to current user
+     */
+    ALLOW,
+    /**
+     * Authentication process is stopped, the user is redirected to a page explaining that the email is already used
+     */
+    WARN,
+    /**
+     * Forbid authentication of the user
+     */
+    FORBID
+  }
+
   private static final Logger LOGGER = Loggers.get(UserIdentityAuthenticator.class);
 
   private final DbClient dbClient;
@@ -70,23 +88,30 @@ public class UserIdentityAuthenticator {
     this.defaultGroupFinder = defaultGroupFinder;
   }
 
-  public UserDto authenticate(UserIdentity user, IdentityProvider provider, AuthenticationEvent.Source source) {
-    return register(user, provider, source);
-  }
-
-  private UserDto register(UserIdentity user, IdentityProvider provider, AuthenticationEvent.Source source) {
+  public UserDto authenticate(UserIdentity user, IdentityProvider provider, AuthenticationEvent.Source source, ExistingEmailStrategy existingEmailStrategy) {
     try (DbSession dbSession = dbClient.openSession(false)) {
       String userLogin = user.getLogin();
       UserDto userDto = dbClient.userDao().selectByLogin(dbSession, userLogin);
       if (userDto != null && userDto.isActive()) {
-        registerExistingUser(dbSession, userDto, user, provider);
+        registerExistingUser(dbSession, userDto, user, provider, source, existingEmailStrategy);
         return userDto;
       }
-      return registerNewUser(dbSession, user, provider, source);
+      return registerNewUser(dbSession, user, provider, source, existingEmailStrategy);
     }
   }
 
-  private UserDto registerNewUser(DbSession dbSession, UserIdentity identity, IdentityProvider provider, AuthenticationEvent.Source source) {
+  private void registerExistingUser(DbSession dbSession, UserDto userDto, UserIdentity identity, IdentityProvider provider, AuthenticationEvent.Source source,
+    ExistingEmailStrategy existingEmailStrategy) {
+    UpdateUser update = UpdateUser.create(userDto.getLogin())
+      .setEmail(identity.getEmail())
+      .setName(identity.getName())
+      .setExternalIdentity(new ExternalIdentity(provider.getKey(), identity.getProviderLogin()));
+    Optional<UserDto> otherUserToIndex = validateEmail(dbSession, identity, provider, source, existingEmailStrategy);
+    userUpdater.updateAndCommit(dbSession, update, u -> syncGroups(dbSession, identity, u), toArray(otherUserToIndex));
+  }
+
+  private UserDto registerNewUser(DbSession dbSession, UserIdentity identity, IdentityProvider provider, AuthenticationEvent.Source source,
+    ExistingEmailStrategy existingEmailStrategy) {
     if (!provider.allowsUsersToSignUp()) {
       throw AuthenticationException.newBuilder()
         .setSource(source)
@@ -95,34 +120,46 @@ public class UserIdentityAuthenticator {
         .setPublicMessage(format("'%s' users are not allowed to sign up", provider.getKey()))
         .build();
     }
-
-    String email = identity.getEmail();
-    if (email != null && dbClient.userDao().doesEmailExist(dbSession, email)) {
-      throw AuthenticationException.newBuilder()
-        .setSource(source)
-        .setLogin(identity.getLogin())
-        .setMessage(format("Email '%s' is already used", email))
-        .setPublicMessage(format(
-          "You can't sign up because email '%s' is already used by an existing user. This means that you probably already registered with another account.",
-          email))
-        .build();
-    }
-
-    String userLogin = identity.getLogin();
+    Optional<UserDto> otherUserToIndex = validateEmail(dbSession, identity, provider, source, existingEmailStrategy);
     return userUpdater.createAndCommit(dbSession, NewUser.builder()
-      .setLogin(userLogin)
+      .setLogin(identity.getLogin())
       .setEmail(identity.getEmail())
       .setName(identity.getName())
       .setExternalIdentity(new ExternalIdentity(provider.getKey(), identity.getProviderLogin()))
-      .build(), u -> syncGroups(dbSession, identity, u));
+      .build(),
+      u -> syncGroups(dbSession, identity, u),
+      toArray(otherUserToIndex));
   }
 
-  private void registerExistingUser(DbSession dbSession, UserDto userDto, UserIdentity identity, IdentityProvider provider) {
-    UpdateUser update = UpdateUser.create(userDto.getLogin())
-      .setEmail(identity.getEmail())
-      .setName(identity.getName())
-      .setExternalIdentity(new ExternalIdentity(provider.getKey(), identity.getProviderLogin()));
-    userUpdater.updateAndCommit(dbSession, update, u -> syncGroups(dbSession, identity, u));
+  private Optional<UserDto> validateEmail(DbSession dbSession, UserIdentity identity, IdentityProvider provider, AuthenticationEvent.Source source,
+    ExistingEmailStrategy existingEmailStrategy) {
+    String email = identity.getEmail();
+    if (email == null) {
+      return Optional.empty();
+    }
+    UserDto existingUser = dbClient.userDao().selectByEmail(dbSession, email);
+    if (existingUser == null || existingUser.getLogin().equals(identity.getLogin())) {
+      return Optional.empty();
+    }
+    switch (existingEmailStrategy) {
+      case ALLOW:
+        existingUser.setEmail(null);
+        dbClient.userDao().update(dbSession, existingUser);
+        return Optional.of(existingUser);
+      case WARN:
+        throw new EmailAlreadyExistsException(email, existingUser, identity, provider);
+      case FORBID:
+        throw AuthenticationException.newBuilder()
+          .setSource(source)
+          .setLogin(identity.getLogin())
+          .setMessage(format("Email '%s' is already used", email))
+          .setPublicMessage(format(
+            "You can't sign up because email '%s' is already used by an existing user. This means that you probably already registered with another account.",
+            email))
+          .build();
+      default:
+        throw new IllegalStateException(format("Unknown strategy %s", existingEmailStrategy));
+    }
   }
 
   private void syncGroups(DbSession dbSession, UserIdentity userIdentity, UserDto userDto) {
@@ -172,4 +209,8 @@ public class UserIdentityAuthenticator {
     return organizationFlags.isEnabled(dbSession) ? Optional.empty() : Optional.of(defaultGroupFinder.findDefaultGroup(dbSession, defaultOrganizationProvider.get().getUuid()));
   }
 
+  private static UserDto[] toArray(Optional<UserDto> userDto) {
+    return userDto.map(u -> new UserDto[]{u}).orElse(new UserDto[]{});
+  }
+
 }
index 9448a2c9fe17edcb5313f21bda9c1cd81219f31b..a012215840f4374d5596fcb4b696349ce324cc51 100644 (file)
@@ -29,6 +29,7 @@ import java.util.List;
 import java.util.Objects;
 import java.util.Random;
 import java.util.function.Consumer;
+import java.util.stream.Stream;
 import javax.annotation.Nullable;
 import org.apache.commons.codec.digest.DigestUtils;
 import org.sonar.api.config.Configuration;
@@ -51,6 +52,8 @@ import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Strings.isNullOrEmpty;
 import static com.google.common.collect.Lists.newArrayList;
 import static java.lang.String.format;
+import static java.util.Arrays.stream;
+import static java.util.stream.Stream.concat;
 import static org.sonar.core.config.CorePropertyDefinitions.ONBOARDING_TUTORIAL_SHOW_TO_NEW_USERS;
 import static org.sonar.core.util.stream.MoreCollectors.toList;
 import static org.sonar.db.user.UserDto.encryptPassword;
@@ -93,7 +96,7 @@ public class UserUpdater {
     this.config = config;
   }
 
-  public UserDto createAndCommit(DbSession dbSession, NewUser newUser, Consumer<UserDto> beforeCommit) {
+  public UserDto createAndCommit(DbSession dbSession, NewUser newUser, Consumer<UserDto> beforeCommit, UserDto... otherUsersToIndex) {
     String login = newUser.login();
     UserDto userDto = dbClient.userDao().selectByLogin(dbSession, newUser.login());
     if (userDto == null) {
@@ -102,7 +105,7 @@ public class UserUpdater {
       reactivateUser(dbSession, userDto, login, newUser);
     }
     beforeCommit.accept(userDto);
-    userIndexer.commitAndIndex(dbSession, userDto);
+    userIndexer.commitAndIndex(dbSession, concat(Stream.of(userDto), stream(otherUsersToIndex)).collect(toList()));
 
     notifyNewUser(userDto.getLogin(), userDto.getName(), newUser.email());
     return userDto;
@@ -124,7 +127,7 @@ public class UserUpdater {
     addUserToDefaultOrganizationAndDefaultGroup(dbSession, existingUser);
   }
 
-  public void updateAndCommit(DbSession dbSession, UpdateUser updateUser, Consumer<UserDto> beforeCommit) {
+  public void updateAndCommit(DbSession dbSession, UpdateUser updateUser, Consumer<UserDto> beforeCommit, UserDto... otherUsersToIndex) {
     UserDto dto = dbClient.userDao().selectByLogin(dbSession, updateUser.login());
     checkFound(dto, "User with login '%s' has not been found", updateUser.login());
     boolean isUserUpdated = updateDto(dbSession, updateUser, dto);
@@ -132,7 +135,7 @@ public class UserUpdater {
       // at least one change. Database must be updated and Elasticsearch re-indexed
       updateUser(dbSession, dto);
       beforeCommit.accept(dto);
-      userIndexer.commitAndIndex(dbSession, dto);
+      userIndexer.commitAndIndex(dbSession, concat(Stream.of(dto), stream(otherUsersToIndex)).collect(toList()));
       notifyNewUser(dto.getLogin(), dto.getName(), dto.getEmail());
     } else {
       // no changes but still execute the consumer
index 3ec7e10a7f647808334ec6584fa909c585ea2bad..cf17bfbce753d07d9aa89d483e4577bf3ce3a50b 100644 (file)
@@ -29,11 +29,9 @@ import org.sonar.api.platform.Server;
 import org.sonar.api.server.authentication.BaseIdentityProvider;
 import org.sonar.api.server.authentication.UserIdentity;
 import org.sonar.api.utils.System2;
-import org.sonar.db.DbClient;
-import org.sonar.db.DbSession;
 import org.sonar.db.DbTester;
 import org.sonar.db.user.UserDto;
-import org.sonar.server.authentication.event.AuthenticationEvent;
+import org.sonar.server.authentication.event.AuthenticationEvent.Source;
 import org.sonar.server.user.TestUserSessionFactory;
 import org.sonar.server.user.ThreadLocalUserSession;
 import org.sonar.server.user.UserSession;
@@ -44,7 +42,7 @@ import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
-import static org.sonar.db.user.UserTesting.newUserDto;
+import static org.sonar.server.authentication.UserIdentityAuthenticator.ExistingEmailStrategy.FORBID;
 
 public class BaseContextFactoryTest {
 
@@ -60,10 +58,6 @@ public class BaseContextFactoryTest {
   @Rule
   public DbTester dbTester = DbTester.create(System2.INSTANCE);
 
-  private DbClient dbClient = dbTester.getDbClient();
-
-  private DbSession dbSession = dbTester.getSession();
-
   private ThreadLocalUserSession threadLocalUserSession = mock(ThreadLocalUserSession.class);
 
   private UserIdentityAuthenticator userIdentityAuthenticator = mock(UserIdentityAuthenticator.class);
@@ -80,11 +74,8 @@ public class BaseContextFactoryTest {
   @Before
   public void setUp() throws Exception {
     when(server.getPublicRootUrl()).thenReturn(PUBLIC_ROOT_URL);
-
-    UserDto userDto = dbClient.userDao().insert(dbSession, newUserDto());
-    dbSession.commit();
     when(identityProvider.getName()).thenReturn("provIdeur Nameuh");
-    when(userIdentityAuthenticator.authenticate(USER_IDENTITY, identityProvider, AuthenticationEvent.Source.external(identityProvider))).thenReturn(userDto);
+    when(request.getSession()).thenReturn(mock(HttpSession.class));
   }
 
   @Test
@@ -98,13 +89,15 @@ public class BaseContextFactoryTest {
 
   @Test
   public void authenticate() {
+    UserDto userDto = dbTester.users().insertUser();
+    when(userIdentityAuthenticator.authenticate(USER_IDENTITY, identityProvider, Source.external(identityProvider), FORBID)).thenReturn(userDto);
     BaseIdentityProvider.Context context = underTest.newContext(request, response, identityProvider);
-    HttpSession session = mock(HttpSession.class);
-    when(request.getSession()).thenReturn(session);
 
     context.authenticate(USER_IDENTITY);
-    verify(userIdentityAuthenticator).authenticate(USER_IDENTITY, identityProvider, AuthenticationEvent.Source.external(identityProvider));
+
+    verify(userIdentityAuthenticator).authenticate(USER_IDENTITY, identityProvider, Source.external(identityProvider), FORBID);
     verify(jwtHttpHandler).generateToken(any(UserDto.class), eq(request), eq(response));
     verify(threadLocalUserSession).set(any(UserSession.class));
   }
+
 }
index 8af7054d08ba757dec0b79e3bb23963e2a61cc7d..4b37c140ad6969e8473b72b9bdbc9892eee9e76e 100644 (file)
@@ -33,17 +33,21 @@ import org.sonar.api.server.authentication.Display;
 import org.sonar.api.server.authentication.IdentityProvider;
 import org.sonar.api.server.authentication.OAuth2IdentityProvider;
 import org.sonar.api.server.authentication.UnauthorizedException;
+import org.sonar.api.server.authentication.UserIdentity;
 import org.sonar.api.utils.log.LogTester;
 import org.sonar.api.utils.log.LoggerLevel;
+import org.sonar.db.user.UserDto;
 import org.sonar.server.authentication.event.AuthenticationEvent;
 import org.sonar.server.authentication.event.AuthenticationException;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyZeroInteractions;
 import static org.mockito.Mockito.when;
+import static org.sonar.db.user.UserTesting.newUserDto;
 
 public class InitFilterTest {
 
@@ -72,11 +76,11 @@ public class InitFilterTest {
   private FakeBasicIdentityProvider baseIdentityProvider = new FakeBasicIdentityProvider(BASIC_PROVIDER_KEY, true);
   private BaseIdentityProvider.Context baseContext = mock(BaseIdentityProvider.Context.class);
   private AuthenticationEvent authenticationEvent = mock(AuthenticationEvent.class);
-  private OAuth2Redirection oAuthRedirection = mock(OAuth2Redirection.class);
+  private OAuth2AuthenticationParameters auth2AuthenticationParameters = mock(OAuth2AuthenticationParameters.class);
 
   private ArgumentCaptor<AuthenticationException> authenticationExceptionCaptor = ArgumentCaptor.forClass(AuthenticationException.class);
 
-  private InitFilter underTest = new InitFilter(identityProviderRepository, baseContextFactory, oAuth2ContextFactory, server, authenticationEvent, oAuthRedirection);
+  private InitFilter underTest = new InitFilter(identityProviderRepository, baseContextFactory, oAuth2ContextFactory, server, authenticationEvent, auth2AuthenticationParameters);
 
   @Before
   public void setUp() throws Exception {
@@ -91,7 +95,7 @@ public class InitFilterTest {
   }
 
   @Test
-  public void do_filter_with_context() throws Exception {
+  public void do_filter_with_context() {
     when(server.getContextPath()).thenReturn("/sonarqube");
     when(request.getRequestURI()).thenReturn("/sonarqube/sessions/init/" + OAUTH2_PROVIDER_KEY);
     identityProviderRepository.addIdentityProvider(oAuth2IdentityProvider);
@@ -100,11 +104,10 @@ public class InitFilterTest {
 
     assertOAuth2InitCalled();
     verifyZeroInteractions(authenticationEvent);
-    verify(oAuthRedirection).create(eq(request), eq(response));
   }
 
   @Test
-  public void do_filter_on_auth2_identity_provider() throws Exception {
+  public void do_filter_on_auth2_identity_provider() {
     when(request.getRequestURI()).thenReturn("/sessions/init/" + OAUTH2_PROVIDER_KEY);
     identityProviderRepository.addIdentityProvider(oAuth2IdentityProvider);
 
@@ -112,11 +115,10 @@ public class InitFilterTest {
 
     assertOAuth2InitCalled();
     verifyZeroInteractions(authenticationEvent);
-    verify(oAuthRedirection).create(eq(request), eq(response));
   }
 
   @Test
-  public void do_filter_on_basic_identity_provider() throws Exception {
+  public void do_filter_on_basic_identity_provider() {
     when(request.getRequestURI()).thenReturn("/sessions/init/" + BASIC_PROVIDER_KEY);
     identityProviderRepository.addIdentityProvider(baseIdentityProvider);
 
@@ -124,7 +126,27 @@ public class InitFilterTest {
 
     assertBasicInitCalled();
     verifyZeroInteractions(authenticationEvent);
-    verifyZeroInteractions(oAuthRedirection);
+  }
+
+  @Test
+  public void init_authentication_parameter_on_auth2_identity_provider() {
+    when(server.getContextPath()).thenReturn("/sonarqube");
+    when(request.getRequestURI()).thenReturn("/sonarqube/sessions/init/" + OAUTH2_PROVIDER_KEY);
+    identityProviderRepository.addIdentityProvider(oAuth2IdentityProvider);
+
+    underTest.doFilter(request, response, chain);
+
+    verify(auth2AuthenticationParameters).init(eq(request), eq(response));
+  }
+
+  @Test
+  public void does_not_init_authentication_parameter_on_basic_authentication() {
+    when(request.getRequestURI()).thenReturn("/sessions/init/" + BASIC_PROVIDER_KEY);
+    identityProviderRepository.addIdentityProvider(baseIdentityProvider);
+
+    underTest.doFilter(request, response, chain);
+
+    verify(auth2AuthenticationParameters, never()).init(eq(request), eq(response));
   }
 
   @Test
@@ -135,7 +157,7 @@ public class InitFilterTest {
 
     assertError("No provider key found in URI");
     verifyZeroInteractions(authenticationEvent);
-    verifyZeroInteractions(oAuthRedirection);
+    verifyZeroInteractions(auth2AuthenticationParameters);
   }
 
   @Test
@@ -146,7 +168,7 @@ public class InitFilterTest {
 
     assertError("No provider key found in URI");
     verifyZeroInteractions(authenticationEvent);
-    verifyZeroInteractions(oAuthRedirection);
+    verifyZeroInteractions(auth2AuthenticationParameters);
   }
 
   @Test
@@ -160,7 +182,7 @@ public class InitFilterTest {
 
     assertError("Unsupported IdentityProvider class: class org.sonar.server.authentication.InitFilterTest$UnsupportedIdentityProvider");
     verifyZeroInteractions(authenticationEvent);
-    verifyZeroInteractions(oAuthRedirection);
+    verifyZeroInteractions(auth2AuthenticationParameters);
   }
 
   @Test
@@ -178,7 +200,7 @@ public class InitFilterTest {
     assertThat(authenticationException.getSource()).isEqualTo(AuthenticationEvent.Source.external(identityProvider));
     assertThat(authenticationException.getLogin()).isNull();
     assertThat(authenticationException.getPublicMessage()).isEqualTo("Email john@email.com is already used");
-    verifyDeleteRedirection();
+    verifyDeleteAuthCookie();
   }
 
   @Test
@@ -191,7 +213,20 @@ public class InitFilterTest {
     underTest.doFilter(request, response, chain);
 
     verify(response).sendRedirect("/sonarqube/sessions/unauthorized?message=Email+john%40email.com+is+already+used");
-    verifyDeleteRedirection();
+    verifyDeleteAuthCookie();
+  }
+
+  @Test
+  public void redirect_when_failing_because_of_EmailAlreadyExistException() throws Exception {
+    UserDto existingUser = newUserDto().setEmail("john@email.com").setExternalIdentity("john.bitbucket").setExternalIdentityProvider("bitbucket");
+    FailWithEmailAlreadyExistException identityProvider = new FailWithEmailAlreadyExistException("failing", existingUser);
+    when(request.getRequestURI()).thenReturn("/sessions/init/" + identityProvider.getKey());
+    identityProviderRepository.addIdentityProvider(identityProvider);
+
+    underTest.doFilter(request, response, chain);
+
+    verify(response).sendRedirect("/sessions/email_already_exists?email=john%40email.com&login=john.github&provider=failing&existingLogin=john.bitbucket&existingProvider=bitbucket");
+    verify(auth2AuthenticationParameters).delete(eq(request), eq(response));
   }
 
   @Test
@@ -204,7 +239,7 @@ public class InitFilterTest {
 
     verify(response).sendRedirect("/sessions/unauthorized");
     assertThat(logTester.logs(LoggerLevel.ERROR)).containsExactlyInAnyOrder("Fail to initialize authentication with provider 'failing'");
-    verifyDeleteRedirection();
+    verifyDeleteAuthCookie();
   }
 
   private void assertOAuth2InitCalled() {
@@ -223,8 +258,8 @@ public class InitFilterTest {
     assertThat(oAuth2IdentityProvider.isInitCalled()).isFalse();
   }
 
-  private void verifyDeleteRedirection() {
-    verify(oAuthRedirection).delete(eq(request), eq(response));
+  private void verifyDeleteAuthCookie() {
+    verify(auth2AuthenticationParameters).delete(eq(request), eq(response));
   }
 
   private static class FailWithUnauthorizedExceptionIdProvider extends FakeBasicIdentityProvider {
@@ -251,6 +286,26 @@ public class InitFilterTest {
     }
   }
 
+  private static class FailWithEmailAlreadyExistException extends FakeBasicIdentityProvider {
+
+    private final UserDto existingUser;
+
+    public FailWithEmailAlreadyExistException(String key, UserDto existingUser) {
+      super(key, true);
+      this.existingUser = existingUser;
+    }
+
+    @Override
+    public void init(Context context) {
+      throw new EmailAlreadyExistsException(existingUser.getEmail(), existingUser, UserIdentity.builder()
+        .setProviderLogin("john.github")
+        .setLogin("john.github")
+        .setName(existingUser.getName())
+        .setEmail(existingUser.getEmail())
+        .build(), this);
+    }
+  }
+
   private static class UnsupportedIdentityProvider implements IdentityProvider {
     private final String unsupportedKey;
 
diff --git a/server/sonar-server/src/test/java/org/sonar/server/authentication/OAuth2AuthenticationParametersImplTest.java b/server/sonar-server/src/test/java/org/sonar/server/authentication/OAuth2AuthenticationParametersImplTest.java
new file mode 100644 (file)
index 0000000..49adf58
--- /dev/null
@@ -0,0 +1,167 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.Optional;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.sonar.api.platform.Server;
+
+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.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class OAuth2AuthenticationParametersImplTest {
+
+  private static final String AUTHENTICATION_COOKIE_NAME = "AUTH-PARAMS";
+  private ArgumentCaptor<Cookie> cookieArgumentCaptor = ArgumentCaptor.forClass(Cookie.class);
+
+  private Server server = mock(Server.class);
+  private HttpServletResponse response = mock(HttpServletResponse.class);
+  private HttpServletRequest request = mock(HttpServletRequest.class);
+
+  private OAuth2AuthenticationParametersImpl underTest = new OAuth2AuthenticationParametersImpl();
+
+  @Before
+  public void setUp() throws Exception {
+    when(server.getContextPath()).thenReturn("");
+  }
+
+  @Test
+  public void init_create_cookie_containing_parameters_from_request() {
+    when(request.getParameter("return_to")).thenReturn("/settings");
+    when(request.getParameter("allowEmailShift")).thenReturn("true");
+
+    underTest.init(request, response);
+
+    verify(response).addCookie(cookieArgumentCaptor.capture());
+    Cookie cookie = cookieArgumentCaptor.getValue();
+    assertThat(cookie.getName()).isEqualTo(AUTHENTICATION_COOKIE_NAME);
+    assertThat(cookie.getValue()).isNotEmpty();
+    assertThat(cookie.getPath()).isEqualTo("/");
+    assertThat(cookie.isHttpOnly()).isTrue();
+    assertThat(cookie.getMaxAge()).isEqualTo(300);
+    assertThat(cookie.getSecure()).isFalse();
+  }
+
+  @Test
+  public void init_does_not_create_cookie_when_no_parameter() {
+    underTest.init(request, response);
+
+    verify(response, never()).addCookie(any(Cookie.class));
+  }
+
+  @Test
+  public void init_does_not_create_cookie_when_parameters_are_empty() {
+    when(request.getParameter("return_to")).thenReturn("");
+    when(request.getParameter("allowEmailShift")).thenReturn("");
+
+    underTest.init(request, response);
+
+    verify(response, never()).addCookie(any(Cookie.class));
+  }
+
+  @Test
+  public void init_does_not_create_cookie_when_parameters_are_null() {
+    when(request.getParameter("return_to")).thenReturn(null);
+    when(request.getParameter("allowEmailShift")).thenReturn(null);
+
+    underTest.init(request, response);
+
+    verify(response, never()).addCookie(any(Cookie.class));
+  }
+
+  @Test
+  public void get_return_to_parameter() {
+    when(request.getCookies()).thenReturn(new Cookie[] {new Cookie(AUTHENTICATION_COOKIE_NAME, "{\"return_to\":\"/settings\"}")});
+
+    Optional<String> redirection = underTest.getReturnTo(request);
+
+    assertThat(redirection).isNotEmpty();
+    assertThat(redirection.get()).isEqualTo("/settings");
+  }
+
+  @Test
+  public void get_return_to_is_empty_when_no_cookie() {
+    when(request.getCookies()).thenReturn(new Cookie[] {});
+
+    Optional<String> redirection = underTest.getReturnTo(request);
+
+    assertThat(redirection).isEmpty();
+  }
+
+  @Test
+  public void get_return_to_is_empty_when_no_value() {
+    when(request.getCookies()).thenReturn(new Cookie[] {new Cookie(AUTHENTICATION_COOKIE_NAME, "{}")});
+
+    Optional<String> redirection = underTest.getReturnTo(request);
+
+    assertThat(redirection).isEmpty();
+  }
+
+  @Test
+  public void get_allowEmailShift_parameter() {
+    when(request.getCookies()).thenReturn(new Cookie[] {new Cookie(AUTHENTICATION_COOKIE_NAME, "{\"allowEmailShift\":\"true\"}")});
+
+    Optional<Boolean> allowEmailShift = underTest.getAllowEmailShift(request);
+
+    assertThat(allowEmailShift).isNotEmpty();
+    assertThat(allowEmailShift.get()).isTrue();
+  }
+
+  @Test
+  public void get_allowEmailShift_is_empty_when_no_cookie() {
+    when(request.getCookies()).thenReturn(new Cookie[] {});
+
+    Optional<Boolean> allowEmailShift = underTest.getAllowEmailShift(request);
+
+    assertThat(allowEmailShift).isEmpty();
+  }
+
+  @Test
+  public void get_allowEmailShift_is_empty_when_no_value() {
+    when(request.getCookies()).thenReturn(new Cookie[] {new Cookie(AUTHENTICATION_COOKIE_NAME, "{}")});
+
+    Optional<Boolean> allowEmailShift = underTest.getAllowEmailShift(request);
+
+    assertThat(allowEmailShift).isEmpty();
+  }
+
+  @Test
+  public void delete() {
+    when(request.getCookies()).thenReturn(new Cookie[] {new Cookie(AUTHENTICATION_COOKIE_NAME, "{\"return_to\":\"/settings\"}")});
+
+    underTest.delete(request, response);
+
+    verify(response).addCookie(cookieArgumentCaptor.capture());
+    Cookie updatedCookie = cookieArgumentCaptor.getValue();
+    assertThat(updatedCookie.getName()).isEqualTo(AUTHENTICATION_COOKIE_NAME);
+    assertThat(updatedCookie.getValue()).isNull();
+    assertThat(updatedCookie.getPath()).isEqualTo("/");
+    assertThat(updatedCookie.getMaxAge()).isEqualTo(0);
+  }
+}
index 9c7152bef721b2928a54710967c58ea934cbf587..792455da6bf2601c252fc5c893a2e6954b7ff10d 100644 (file)
@@ -33,6 +33,7 @@ import org.sonar.api.server.authentication.UnauthorizedException;
 import org.sonar.api.server.authentication.UserIdentity;
 import org.sonar.api.utils.log.LogTester;
 import org.sonar.api.utils.log.LoggerLevel;
+import org.sonar.db.user.UserDto;
 import org.sonar.server.authentication.event.AuthenticationEvent;
 import org.sonar.server.authentication.event.AuthenticationException;
 
@@ -42,6 +43,7 @@ import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyZeroInteractions;
 import static org.mockito.Mockito.when;
+import static org.sonar.db.user.UserTesting.newUserDto;
 import static org.sonar.server.authentication.event.AuthenticationEvent.Source;
 
 public class OAuth2CallbackFilterTest {
@@ -65,7 +67,7 @@ public class OAuth2CallbackFilterTest {
 
   private FakeOAuth2IdentityProvider oAuth2IdentityProvider = new WellbehaveFakeOAuth2IdentityProvider(OAUTH2_PROVIDER_KEY, true, LOGIN);
   private AuthenticationEvent authenticationEvent = mock(AuthenticationEvent.class);
-  private OAuth2Redirection oAuthRedirection = mock(OAuth2Redirection.class);
+  private OAuth2AuthenticationParameters oAuthRedirection = mock(OAuth2AuthenticationParameters.class);
 
   private ArgumentCaptor<AuthenticationException> authenticationExceptionCaptor = ArgumentCaptor.forClass(AuthenticationException.class);
 
@@ -83,7 +85,7 @@ public class OAuth2CallbackFilterTest {
   }
 
   @Test
-  public void do_filter_with_context() throws Exception {
+  public void do_filter_with_context() {
     when(server.getContextPath()).thenReturn("/sonarqube");
     when(request.getRequestURI()).thenReturn("/sonarqube/oauth2/callback/" + OAUTH2_PROVIDER_KEY);
     identityProviderRepository.addIdentityProvider(oAuth2IdentityProvider);
@@ -95,7 +97,7 @@ public class OAuth2CallbackFilterTest {
   }
 
   @Test
-  public void do_filter_with_context_no_log_if_provider_did_not_call_authenticate_on_context() throws Exception {
+  public void do_filter_with_context_no_log_if_provider_did_not_call_authenticate_on_context() {
     when(server.getContextPath()).thenReturn("/sonarqube");
     when(request.getRequestURI()).thenReturn("/sonarqube/oauth2/callback/" + OAUTH2_PROVIDER_KEY);
     FakeOAuth2IdentityProvider identityProvider = new FakeOAuth2IdentityProvider(OAUTH2_PROVIDER_KEY, true);
@@ -113,7 +115,7 @@ public class OAuth2CallbackFilterTest {
   }
 
   @Test
-  public void do_filter_on_auth2_identity_provider() throws Exception {
+  public void do_filter_on_auth2_identity_provider() {
     when(request.getRequestURI()).thenReturn("/oauth2/callback/" + OAUTH2_PROVIDER_KEY);
     identityProviderRepository.addIdentityProvider(oAuth2IdentityProvider);
 
@@ -149,10 +151,6 @@ public class OAuth2CallbackFilterTest {
   @Test
   public void redirect_when_failing_because_of_UnauthorizedExceptionException() throws Exception {
     FailWithUnauthorizedExceptionIdProvider identityProvider = new FailWithUnauthorizedExceptionIdProvider();
-    identityProvider
-      .setKey("failing")
-      .setName("name of failing")
-      .setEnabled(true);
     when(request.getRequestURI()).thenReturn("/oauth2/callback/" + identityProvider.getKey());
     identityProviderRepository.addIdentityProvider(identityProvider);
 
@@ -172,10 +170,6 @@ public class OAuth2CallbackFilterTest {
   public void redirect_with_context_path_when_failing_because_of_UnauthorizedExceptionException() throws Exception {
     when(server.getContextPath()).thenReturn("/sonarqube");
     FailWithUnauthorizedExceptionIdProvider identityProvider = new FailWithUnauthorizedExceptionIdProvider();
-    identityProvider
-      .setKey("failing")
-      .setName("name of failing")
-      .setEnabled(true);
     when(request.getRequestURI()).thenReturn("/sonarqube/oauth2/callback/" + identityProvider.getKey());
     identityProviderRepository.addIdentityProvider(identityProvider);
 
@@ -188,10 +182,6 @@ public class OAuth2CallbackFilterTest {
   @Test
   public void redirect_when_failing_because_of_Exception() throws Exception {
     FailWithIllegalStateException identityProvider = new FailWithIllegalStateException();
-    identityProvider
-      .setKey("failing")
-      .setName("name of failing")
-      .setEnabled(true);
     when(request.getRequestURI()).thenReturn("/oauth2/callback/" + identityProvider.getKey());
     identityProviderRepository.addIdentityProvider(identityProvider);
 
@@ -202,6 +192,19 @@ public class OAuth2CallbackFilterTest {
     verify(oAuthRedirection).delete(eq(request), eq(response));
   }
 
+  @Test
+  public void redirect_when_failing_because_of_EmailAlreadyExistException() throws Exception {
+    UserDto existingUser = newUserDto().setEmail("john@email.com").setExternalIdentity("john.bitbucket").setExternalIdentityProvider("bitbucket");
+    FailWithEmailAlreadyExistException identityProvider = new FailWithEmailAlreadyExistException(existingUser);
+    when(request.getRequestURI()).thenReturn("/oauth2/callback/" + identityProvider.getKey());
+    identityProviderRepository.addIdentityProvider(identityProvider);
+
+    underTest.doFilter(request, response, chain);
+
+    verify(response).sendRedirect("/sessions/email_already_exists?email=john%40email.com&login=john.github&provider=failing&existingLogin=john.bitbucket&existingProvider=bitbucket");
+    verify(oAuthRedirection).delete(eq(request), eq(response));
+  }
+
   @Test
   public void fail_when_no_oauth2_provider_provided() throws Exception {
     when(request.getRequestURI()).thenReturn("/oauth2/callback");
@@ -223,29 +226,49 @@ public class OAuth2CallbackFilterTest {
     assertThat(oAuth2IdentityProvider.isInitCalled()).isFalse();
   }
 
-  private static class FailWithUnauthorizedExceptionIdProvider extends TestIdentityProvider implements OAuth2IdentityProvider {
-
+  private static class FailWithUnauthorizedExceptionIdProvider extends FailingIdentityProvider  {
     @Override
-    public void init(InitContext context) {
-
+    public void callback(CallbackContext context) {
+      throw new UnauthorizedException("Email john@email.com is already used");
     }
+  }
 
+  private static class FailWithIllegalStateException extends FailingIdentityProvider  {
     @Override
     public void callback(CallbackContext context) {
-      throw new UnauthorizedException("Email john@email.com is already used");
+      throw new IllegalStateException("Failure !");
     }
   }
 
-  private static class FailWithIllegalStateException extends TestIdentityProvider implements OAuth2IdentityProvider {
+  private static class FailWithEmailAlreadyExistException extends FailingIdentityProvider {
 
-    @Override
-    public void init(InitContext context) {
+    private final UserDto existingUser;
 
+    public FailWithEmailAlreadyExistException(UserDto existingUser) {
+      this.existingUser = existingUser;
     }
 
     @Override
     public void callback(CallbackContext context) {
-      throw new IllegalStateException("Failure !");
+      throw new EmailAlreadyExistsException(existingUser.getEmail(), existingUser, UserIdentity.builder()
+        .setProviderLogin("john.github")
+        .setLogin("john.github")
+        .setName(existingUser.getName())
+        .setEmail(existingUser.getEmail())
+        .build(), this);
+    }
+  }
+
+  private static abstract class FailingIdentityProvider extends TestIdentityProvider implements OAuth2IdentityProvider {
+    FailingIdentityProvider() {
+      this.setKey("failing");
+      this.setName("Failing");
+      this.setEnabled(true);
+    }
+
+    @Override
+    public void init(InitContext context) {
+      // Nothing to do
     }
   }
 
index 263ef365e9d79bb20310c585ec9e4faaa06eb509..02b33b9b68d6245fde87cacb49955aa4bc61c5d3 100644 (file)
@@ -31,8 +31,6 @@ import org.sonar.api.platform.Server;
 import org.sonar.api.server.authentication.OAuth2IdentityProvider;
 import org.sonar.api.server.authentication.UserIdentity;
 import org.sonar.api.utils.System2;
-import org.sonar.db.DbClient;
-import org.sonar.db.DbSession;
 import org.sonar.db.DbTester;
 import org.sonar.db.user.UserDto;
 import org.sonar.server.user.TestUserSessionFactory;
@@ -45,7 +43,8 @@ import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
-import static org.sonar.db.user.UserTesting.newUserDto;
+import static org.sonar.server.authentication.UserIdentityAuthenticator.ExistingEmailStrategy.ALLOW;
+import static org.sonar.server.authentication.UserIdentityAuthenticator.ExistingEmailStrategy.WARN;
 import static org.sonar.server.authentication.event.AuthenticationEvent.Source;
 
 public class OAuth2ContextFactoryTest {
@@ -66,32 +65,26 @@ public class OAuth2ContextFactoryTest {
   @Rule
   public DbTester dbTester = DbTester.create(System2.INSTANCE);
 
-  private DbClient dbClient = dbTester.getDbClient();
-  private DbSession dbSession = dbTester.getSession();
   private ThreadLocalUserSession threadLocalUserSession = mock(ThreadLocalUserSession.class);
   private UserIdentityAuthenticator userIdentityAuthenticator = mock(UserIdentityAuthenticator.class);
   private Server server = mock(Server.class);
   private OAuthCsrfVerifier csrfVerifier = mock(OAuthCsrfVerifier.class);
   private JwtHttpHandler jwtHttpHandler = mock(JwtHttpHandler.class);
   private TestUserSessionFactory userSessionFactory = TestUserSessionFactory.standalone();
-  private OAuth2Redirection oAuthRedirection = mock(OAuth2Redirection.class);
+  private OAuth2AuthenticationParameters oAuthParameters = mock(OAuth2AuthenticationParameters.class);
   private HttpServletRequest request = mock(HttpServletRequest.class);
   private HttpServletResponse response = mock(HttpServletResponse.class);
   private HttpSession session = mock(HttpSession.class);
   private OAuth2IdentityProvider identityProvider = mock(OAuth2IdentityProvider.class);
 
   private OAuth2ContextFactory underTest = new OAuth2ContextFactory(threadLocalUserSession, userIdentityAuthenticator, server, csrfVerifier, jwtHttpHandler, userSessionFactory,
-    oAuthRedirection);
+    oAuthParameters);
 
   @Before
   public void setUp() throws Exception {
-    UserDto userDto = dbClient.userDao().insert(dbSession, newUserDto());
-    dbSession.commit();
-
     when(request.getSession()).thenReturn(session);
     when(identityProvider.getKey()).thenReturn(PROVIDER_KEY);
     when(identityProvider.getName()).thenReturn(PROVIDER_NAME);
-    when(userIdentityAuthenticator.authenticate(USER_IDENTITY, identityProvider, Source.oauth2(identityProvider))).thenReturn(userDto);
   }
 
   @Test
@@ -136,19 +129,33 @@ public class OAuth2ContextFactoryTest {
 
   @Test
   public void authenticate() {
+    UserDto userDto = dbTester.users().insertUser();
+    when(userIdentityAuthenticator.authenticate(USER_IDENTITY, identityProvider, Source.oauth2(identityProvider), WARN)).thenReturn(userDto);
     OAuth2IdentityProvider.CallbackContext callback = newCallbackContext();
 
     callback.authenticate(USER_IDENTITY);
 
-    verify(userIdentityAuthenticator).authenticate(USER_IDENTITY, identityProvider, Source.oauth2(identityProvider));
+    verify(userIdentityAuthenticator).authenticate(USER_IDENTITY, identityProvider, Source.oauth2(identityProvider), WARN);
     verify(jwtHttpHandler).generateToken(any(UserDto.class), eq(request), eq(response));
     verify(threadLocalUserSession).set(any(UserSession.class));
   }
 
+  @Test
+  public void authenticate_with_allow_email_shift() {
+    when(oAuthParameters.getAllowEmailShift(request)).thenReturn(Optional.of(true));
+    UserDto userDto = dbTester.users().insertUser();
+    when(userIdentityAuthenticator.authenticate(USER_IDENTITY, identityProvider, Source.oauth2(identityProvider), ALLOW)).thenReturn(userDto);
+    OAuth2IdentityProvider.CallbackContext callback = newCallbackContext();
+
+    callback.authenticate(USER_IDENTITY);
+
+    verify(userIdentityAuthenticator).authenticate(USER_IDENTITY, identityProvider, Source.oauth2(identityProvider), ALLOW);
+  }
+
   @Test
   public void redirect_to_home() throws Exception {
     when(server.getContextPath()).thenReturn("");
-    when(oAuthRedirection.getAndDelete(request, response)).thenReturn(Optional.empty());
+    when(oAuthParameters.getReturnTo(request)).thenReturn(Optional.empty());
     OAuth2IdentityProvider.CallbackContext callback = newCallbackContext();
 
     callback.redirectToRequestedPage();
@@ -159,7 +166,7 @@ public class OAuth2ContextFactoryTest {
   @Test
   public void redirect_to_home_with_context() throws Exception {
     when(server.getContextPath()).thenReturn("/sonarqube");
-    when(oAuthRedirection.getAndDelete(request, response)).thenReturn(Optional.empty());
+    when(oAuthParameters.getReturnTo(request)).thenReturn(Optional.empty());
     OAuth2IdentityProvider.CallbackContext callback = newCallbackContext();
 
     callback.redirectToRequestedPage();
@@ -169,7 +176,7 @@ public class OAuth2ContextFactoryTest {
 
   @Test
   public void redirect_to_requested_page() throws Exception {
-    when(oAuthRedirection.getAndDelete(request, response)).thenReturn(Optional.of("/settings"));
+    when(oAuthParameters.getReturnTo(request)).thenReturn(Optional.of("/settings"));
     when(server.getContextPath()).thenReturn("");
     OAuth2IdentityProvider.CallbackContext callback = newCallbackContext();
 
@@ -179,8 +186,8 @@ public class OAuth2ContextFactoryTest {
   }
 
   @Test
-  public void redirect_to_requested_page_doesnt_need_context() throws Exception {
-    when(oAuthRedirection.getAndDelete(request, response)).thenReturn(Optional.of("/sonarqube/settings"));
+  public void redirect_to_requested_page_does_not_need_context() throws Exception {
+    when(oAuthParameters.getReturnTo(request)).thenReturn(Optional.of("/sonarqube/settings"));
     when(server.getContextPath()).thenReturn("/other");
     OAuth2IdentityProvider.CallbackContext callback = newCallbackContext();
 
@@ -198,6 +205,17 @@ public class OAuth2ContextFactoryTest {
     verify(csrfVerifier).verifyState(request, response, identityProvider);
   }
 
+  @Test
+  public void delete_oauth2_parameters_during_redirection() {
+    when(oAuthParameters.getReturnTo(request)).thenReturn(Optional.of("/settings"));
+    when(server.getContextPath()).thenReturn("");
+    OAuth2IdentityProvider.CallbackContext callback = newCallbackContext();
+
+    callback.redirectToRequestedPage();
+
+    verify(oAuthParameters).delete(eq(request), eq(response));
+  }
+
   private OAuth2IdentityProvider.InitContext newInitContext() {
     return underTest.newContext(request, response, identityProvider);
   }
diff --git a/server/sonar-server/src/test/java/org/sonar/server/authentication/OAuth2RedirectionTest.java b/server/sonar-server/src/test/java/org/sonar/server/authentication/OAuth2RedirectionTest.java
deleted file mode 100644 (file)
index a653691..0000000
+++ /dev/null
@@ -1,134 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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.Optional;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import org.junit.Before;
-import org.junit.Test;
-import org.mockito.ArgumentCaptor;
-import org.sonar.api.platform.Server;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.Matchers.any;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-public class OAuth2RedirectionTest {
-
-  private ArgumentCaptor<Cookie> cookieArgumentCaptor = ArgumentCaptor.forClass(Cookie.class);
-
-  private Server server = mock(Server.class);
-  private HttpServletResponse response = mock(HttpServletResponse.class);
-  private HttpServletRequest request = mock(HttpServletRequest.class);
-
-  private OAuth2Redirection underTest = new OAuth2Redirection();
-
-  @Before
-  public void setUp() throws Exception {
-    when(server.getContextPath()).thenReturn("");
-  }
-
-  @Test
-  public void create_cookie() {
-    when(request.getParameter("return_to")).thenReturn("/settings");
-
-    underTest.create(request, response);
-
-    verify(response).addCookie(cookieArgumentCaptor.capture());
-    Cookie cookie = cookieArgumentCaptor.getValue();
-    assertThat(cookie.getName()).isEqualTo("REDIRECT_TO");
-    assertThat(cookie.getValue()).isEqualTo("/settings");
-    assertThat(cookie.getPath()).isEqualTo("/");
-    assertThat(cookie.isHttpOnly()).isTrue();
-    assertThat(cookie.getMaxAge()).isEqualTo(-1);
-    assertThat(cookie.getSecure()).isFalse();
-  }
-
-  @Test
-  public void does_not_create_cookie_when_return_to_parameter_is_empty() {
-    when(request.getParameter("return_to")).thenReturn("");
-
-    underTest.create(request, response);
-
-    verify(response, never()).addCookie(any());
-  }
-
-  @Test
-  public void does_not_create_cookie_when_return_to_parameter_is_null() {
-    when(request.getParameter("return_to")).thenReturn(null);
-
-    underTest.create(request, response);
-
-    verify(response, never()).addCookie(any());
-  }
-
-  @Test
-  public void get_and_delete() {
-    when(request.getCookies()).thenReturn(new Cookie[]{new Cookie("REDIRECT_TO", "/settings")});
-
-    Optional<String> redirection = underTest.getAndDelete(request, response);
-
-    assertThat(redirection).isEqualTo(Optional.of("/settings"));
-    verify(response).addCookie(cookieArgumentCaptor.capture());
-    Cookie updatedCookie = cookieArgumentCaptor.getValue();
-    assertThat(updatedCookie.getName()).isEqualTo("REDIRECT_TO");
-    assertThat(updatedCookie.getValue()).isNull();
-    assertThat(updatedCookie.getPath()).isEqualTo("/");
-    assertThat(updatedCookie.getMaxAge()).isEqualTo(0);
-  }
-
-  @Test
-  public void get_and_delete_returns_nothing_when_no_cookie() {
-    when(request.getCookies()).thenReturn(new Cookie[]{});
-
-    Optional<String> redirection = underTest.getAndDelete(request, response);
-
-    assertThat(redirection).isEmpty();
-  }
-
-  @Test
-  public void get_and_delete_returns_nothing_redirect_value_is_null() {
-    when(request.getCookies()).thenReturn(new Cookie[]{new Cookie("REDIRECT_TO", null)});
-
-    Optional<String> redirection = underTest.getAndDelete(request, response);
-
-    assertThat(redirection).isEmpty();
-  }
-
-  @Test
-  public void delete() {
-    when(request.getCookies()).thenReturn(new Cookie[]{new Cookie("REDIRECT_TO", "/settings")});
-
-    underTest.delete(request, response);
-
-    verify(response).addCookie(cookieArgumentCaptor.capture());
-    Cookie updatedCookie = cookieArgumentCaptor.getValue();
-    assertThat(updatedCookie.getName()).isEqualTo("REDIRECT_TO");
-    assertThat(updatedCookie.getValue()).isNull();
-    assertThat(updatedCookie.getPath()).isEqualTo("/");
-    assertThat(updatedCookie.getMaxAge()).isEqualTo(0);
-  }
-
-}
index 2ffea6ad1574e5c48c983988069a8dcd157ee124..ead8ab63cf36535f9eb41d18f6d67fc5f193f6f7 100644 (file)
@@ -41,6 +41,7 @@ import org.sonar.server.user.SecurityRealmFactory;
 import static java.util.Arrays.asList;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.junit.rules.ExpectedException.none;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Matchers.any;
 import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
@@ -49,6 +50,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.verifyZeroInteractions;
 import static org.mockito.Mockito.when;
 import static org.sonar.db.user.UserTesting.newUserDto;
+import static org.sonar.server.authentication.UserIdentityAuthenticator.ExistingEmailStrategy.FORBID;
 import static org.sonar.server.authentication.event.AuthenticationEvent.Method.BASIC;
 import static org.sonar.server.authentication.event.AuthenticationEvent.Method.BASIC_TOKEN;
 import static org.sonar.server.authentication.event.AuthenticationExceptionMatcher.authenticationException;
@@ -96,11 +98,11 @@ public class RealmAuthenticatorTest {
     userDetails.setName("name");
     userDetails.setEmail("email");
     when(externalUsersProvider.doGetUserDetails(any(ExternalUsersProvider.Context.class))).thenReturn(userDetails);
-    when(userIdentityAuthenticator.authenticate(any(UserIdentity.class), any(IdentityProvider.class), any(Source.class))).thenReturn(USER);
+    when(userIdentityAuthenticator.authenticate(any(UserIdentity.class), any(IdentityProvider.class), any(Source.class), eq(FORBID))).thenReturn(USER);
 
     underTest.authenticate(LOGIN, PASSWORD, request, BASIC);
 
-    verify(userIdentityAuthenticator).authenticate(userIdentityArgumentCaptor.capture(), identityProviderArgumentCaptor.capture(), sourceCaptor.capture());
+    verify(userIdentityAuthenticator).authenticate(userIdentityArgumentCaptor.capture(), identityProviderArgumentCaptor.capture(), sourceCaptor.capture(), eq(FORBID));
     UserIdentity userIdentity = userIdentityArgumentCaptor.getValue();
     assertThat(userIdentity.getLogin()).isEqualTo(LOGIN);
     assertThat(userIdentity.getProviderLogin()).isEqualTo(LOGIN);
@@ -118,11 +120,11 @@ public class RealmAuthenticatorTest {
     userDetails.setName("name");
     userDetails.setEmail("email");
     when(externalUsersProvider.doGetUserDetails(any(ExternalUsersProvider.Context.class))).thenReturn(userDetails);
-    when(userIdentityAuthenticator.authenticate(any(UserIdentity.class), any(IdentityProvider.class), any(Source.class))).thenReturn(USER);
+    when(userIdentityAuthenticator.authenticate(any(UserIdentity.class), any(IdentityProvider.class), any(Source.class), eq(FORBID))).thenReturn(USER);
 
     underTest.authenticate(LOGIN, PASSWORD, request, BASIC);
 
-    verify(userIdentityAuthenticator).authenticate(userIdentityArgumentCaptor.capture(), identityProviderArgumentCaptor.capture(), sourceCaptor.capture());
+    verify(userIdentityAuthenticator).authenticate(userIdentityArgumentCaptor.capture(), identityProviderArgumentCaptor.capture(), sourceCaptor.capture(), eq(FORBID));
 
     assertThat(identityProviderArgumentCaptor.getValue().getKey()).isEqualTo("sonarqube");
     assertThat(identityProviderArgumentCaptor.getValue().getName()).isEqualTo("sonarqube");
@@ -138,11 +140,11 @@ public class RealmAuthenticatorTest {
     UserDetails userDetails = new UserDetails();
     userDetails.setEmail("email");
     when(externalUsersProvider.doGetUserDetails(any(ExternalUsersProvider.Context.class))).thenReturn(userDetails);
-    when(userIdentityAuthenticator.authenticate(any(UserIdentity.class), any(IdentityProvider.class), any(Source.class))).thenReturn(USER);
+    when(userIdentityAuthenticator.authenticate(any(UserIdentity.class), any(IdentityProvider.class), any(Source.class), eq(FORBID))).thenReturn(USER);
 
     underTest.authenticate(LOGIN, PASSWORD, request, BASIC);
 
-    verify(userIdentityAuthenticator).authenticate(userIdentityArgumentCaptor.capture(), identityProviderArgumentCaptor.capture(), sourceCaptor.capture());
+    verify(userIdentityAuthenticator).authenticate(userIdentityArgumentCaptor.capture(), identityProviderArgumentCaptor.capture(), sourceCaptor.capture(), eq(FORBID));
     assertThat(identityProviderArgumentCaptor.getValue().getName()).isEqualTo("sonarqube");
     verify(authenticationEvent).loginSuccess(request, LOGIN, Source.realm(BASIC, REALM_NAME));
   }
@@ -150,11 +152,11 @@ public class RealmAuthenticatorTest {
   @Test
   public void authenticate_with_group_sync() {
     when(externalGroupsProvider.doGetGroups(any(ExternalGroupsProvider.Context.class))).thenReturn(asList("group1", "group2"));
-    when(userIdentityAuthenticator.authenticate(any(UserIdentity.class), any(IdentityProvider.class), any(Source.class))).thenReturn(USER);
+    when(userIdentityAuthenticator.authenticate(any(UserIdentity.class), any(IdentityProvider.class), any(Source.class), eq(FORBID))).thenReturn(USER);
     executeStartWithGroupSync();
     executeAuthenticate();
 
-    verify(userIdentityAuthenticator).authenticate(userIdentityArgumentCaptor.capture(), identityProviderArgumentCaptor.capture(), sourceCaptor.capture());
+    verify(userIdentityAuthenticator).authenticate(userIdentityArgumentCaptor.capture(), identityProviderArgumentCaptor.capture(), sourceCaptor.capture(), eq(FORBID));
 
     UserIdentity userIdentity = userIdentityArgumentCaptor.getValue();
     assertThat(userIdentity.shouldSyncGroups()).isTrue();
@@ -169,11 +171,11 @@ public class RealmAuthenticatorTest {
     UserDetails userDetails = new UserDetails();
     userDetails.setName(null);
     when(externalUsersProvider.doGetUserDetails(any(ExternalUsersProvider.Context.class))).thenReturn(userDetails);
-    when(userIdentityAuthenticator.authenticate(any(UserIdentity.class), any(IdentityProvider.class), any(Source.class))).thenReturn(USER);
+    when(userIdentityAuthenticator.authenticate(any(UserIdentity.class), any(IdentityProvider.class), any(Source.class), eq(FORBID))).thenReturn(USER);
 
     underTest.authenticate(LOGIN, PASSWORD, request, BASIC);
 
-    verify(userIdentityAuthenticator).authenticate(userIdentityArgumentCaptor.capture(), identityProviderArgumentCaptor.capture(), sourceCaptor.capture());
+    verify(userIdentityAuthenticator).authenticate(userIdentityArgumentCaptor.capture(), identityProviderArgumentCaptor.capture(), sourceCaptor.capture(), eq(FORBID));
     assertThat(userIdentityArgumentCaptor.getValue().getName()).isEqualTo(LOGIN);
     verify(authenticationEvent).loginSuccess(request, LOGIN, Source.realm(BASIC, REALM_NAME));
   }
@@ -181,11 +183,11 @@ public class RealmAuthenticatorTest {
   @Test
   public void use_downcase_login() {
     settings.setProperty("sonar.authenticator.downcase", true);
-    when(userIdentityAuthenticator.authenticate(any(UserIdentity.class), any(IdentityProvider.class), any(Source.class))).thenReturn(USER);
+    when(userIdentityAuthenticator.authenticate(any(UserIdentity.class), any(IdentityProvider.class), any(Source.class), eq(FORBID))).thenReturn(USER);
     executeStartWithoutGroupSync();
     executeAuthenticate("LOGIN");
 
-    verify(userIdentityAuthenticator).authenticate(userIdentityArgumentCaptor.capture(), identityProviderArgumentCaptor.capture(), sourceCaptor.capture());
+    verify(userIdentityAuthenticator).authenticate(userIdentityArgumentCaptor.capture(), identityProviderArgumentCaptor.capture(), sourceCaptor.capture(), eq(FORBID));
     UserIdentity userIdentity = userIdentityArgumentCaptor.getValue();
     assertThat(userIdentity.getLogin()).isEqualTo("login");
     assertThat(userIdentity.getProviderLogin()).isEqualTo("login");
@@ -195,11 +197,11 @@ public class RealmAuthenticatorTest {
   @Test
   public void does_not_user_downcase_login() {
     settings.setProperty("sonar.authenticator.downcase", false);
-    when(userIdentityAuthenticator.authenticate(any(UserIdentity.class), any(IdentityProvider.class), any(Source.class))).thenReturn(USER);
+    when(userIdentityAuthenticator.authenticate(any(UserIdentity.class), any(IdentityProvider.class), any(Source.class), eq(FORBID))).thenReturn(USER);
     executeStartWithoutGroupSync();
     executeAuthenticate("LoGiN");
 
-    verify(userIdentityAuthenticator).authenticate(userIdentityArgumentCaptor.capture(), identityProviderArgumentCaptor.capture(), sourceCaptor.capture());
+    verify(userIdentityAuthenticator).authenticate(userIdentityArgumentCaptor.capture(), identityProviderArgumentCaptor.capture(), sourceCaptor.capture(), eq(FORBID));
     UserIdentity userIdentity = userIdentityArgumentCaptor.getValue();
     assertThat(userIdentity.getLogin()).isEqualTo("LoGiN");
     assertThat(userIdentity.getProviderLogin()).isEqualTo("LoGiN");
index f3075bb828007481a43f1456c62d6b17f5323ef4..c4e774e85aa71695b17e65065d3c5477b4ca3e34 100644 (file)
@@ -26,7 +26,6 @@ import org.junit.Test;
 import org.junit.rules.ExpectedException;
 import org.sonar.api.config.internal.MapSettings;
 import org.sonar.api.server.authentication.UserIdentity;
-import org.sonar.api.utils.System2;
 import org.sonar.api.utils.internal.AlwaysIncreasingSystem2;
 import org.sonar.core.util.stream.MoreCollectors;
 import org.sonar.db.DbTester;
@@ -52,6 +51,9 @@ import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.Mockito.mock;
 import static org.sonar.core.config.CorePropertyDefinitions.ONBOARDING_TUTORIAL_SHOW_TO_NEW_USERS;
 import static org.sonar.db.user.UserTesting.newUserDto;
+import static org.sonar.server.authentication.UserIdentityAuthenticator.ExistingEmailStrategy.ALLOW;
+import static org.sonar.server.authentication.UserIdentityAuthenticator.ExistingEmailStrategy.FORBID;
+import static org.sonar.server.authentication.UserIdentityAuthenticator.ExistingEmailStrategy.WARN;
 import static org.sonar.server.authentication.event.AuthenticationExceptionMatcher.authenticationException;
 
 public class UserIdentityAuthenticatorTest {
@@ -74,7 +76,7 @@ public class UserIdentityAuthenticatorTest {
   private MapSettings settings = new MapSettings();
 
   @Rule
-  public ExpectedException thrown = ExpectedException.none();
+  public ExpectedException expectedException = ExpectedException.none();
   @Rule
   public DbTester db = DbTester.create(new AlwaysIncreasingSystem2());
   @Rule
@@ -99,7 +101,8 @@ public class UserIdentityAuthenticatorTest {
   @Test
   public void authenticate_new_user() {
     organizationFlags.setEnabled(true);
-    underTest.authenticate(USER_IDENTITY, IDENTITY_PROVIDER, Source.realm(Method.BASIC, IDENTITY_PROVIDER.getName()));
+
+    underTest.authenticate(USER_IDENTITY, IDENTITY_PROVIDER, Source.realm(Method.BASIC, IDENTITY_PROVIDER.getName()), FORBID);
 
     UserDto user = db.users().selectUserByLogin(USER_LOGIN).get();
     assertThat(user).isNotNull();
@@ -109,7 +112,6 @@ public class UserIdentityAuthenticatorTest {
     assertThat(user.getExternalIdentity()).isEqualTo("johndoo");
     assertThat(user.getExternalIdentityProvider()).isEqualTo("github");
     assertThat(user.isRoot()).isFalse();
-
     checkGroupMembership(user);
   }
 
@@ -158,7 +160,7 @@ public class UserIdentityAuthenticatorTest {
     organizationFlags.setEnabled(true);
     settings.setProperty(ONBOARDING_TUTORIAL_SHOW_TO_NEW_USERS, true);
 
-    underTest.authenticate(USER_IDENTITY, IDENTITY_PROVIDER, Source.realm(Method.BASIC, IDENTITY_PROVIDER.getName()));
+    underTest.authenticate(USER_IDENTITY, IDENTITY_PROVIDER, Source.realm(Method.BASIC, IDENTITY_PROVIDER.getName()), FORBID);
 
     assertThat(db.users().selectUserByLogin(USER_LOGIN).get().isOnboarded()).isFalse();
   }
@@ -168,22 +170,87 @@ public class UserIdentityAuthenticatorTest {
     organizationFlags.setEnabled(true);
     settings.setProperty(ONBOARDING_TUTORIAL_SHOW_TO_NEW_USERS, false);
 
-    underTest.authenticate(USER_IDENTITY, IDENTITY_PROVIDER, Source.realm(Method.BASIC, IDENTITY_PROVIDER.getName()));
+    underTest.authenticate(USER_IDENTITY, IDENTITY_PROVIDER, Source.realm(Method.BASIC, IDENTITY_PROVIDER.getName()), FORBID);
 
     assertThat(db.users().selectUserByLogin(USER_LOGIN).get().isOnboarded()).isTrue();
   }
 
   @Test
-  public void authenticate_existing_user() {
+  public void authenticate_new_user_update_existing_user_email_when_strategy_is_ALLOW() {
+    organizationFlags.setEnabled(true);
+    UserDto existingUser = db.users().insertUser(u -> u.setEmail("john@email.com"));
+    UserIdentity newUser = UserIdentity.builder()
+      .setProviderLogin("johndoo")
+      .setLogin("new_login")
+      .setName(existingUser.getName())
+      .setEmail(existingUser.getEmail())
+      .build();
+
+    underTest.authenticate(newUser, IDENTITY_PROVIDER, Source.local(Method.BASIC), ALLOW);
+
+    UserDto newUserReloaded = db.users().selectUserByLogin(newUser.getLogin()).get();
+    assertThat(newUserReloaded.getEmail()).isEqualTo(existingUser.getEmail());
+    UserDto existingUserReloaded = db.users().selectUserByLogin(existingUser.getLogin()).get();
+    assertThat(existingUserReloaded.getEmail()).isNull();
+  }
+
+  @Test
+  public void throw_EmailAlreadyExistException_when_authenticating_new_user_when_email_already_exists_and_strategy_is_WARN() {
+    organizationFlags.setEnabled(true);
+    UserDto existingUser = db.users().insertUser(u -> u.setEmail("john@email.com"));
+    UserIdentity newUser = UserIdentity.builder()
+      .setProviderLogin("johndoo")
+      .setLogin("new_login")
+      .setName(existingUser.getName())
+      .setEmail(existingUser.getEmail())
+      .build();
+
+    expectedException.expect(EmailAlreadyExistsException.class);
+
+    underTest.authenticate(newUser, IDENTITY_PROVIDER, Source.local(Method.BASIC), WARN);
+  }
+
+  @Test
+  public void throw_AuthenticationException_when_authenticating_new_user_when_email_already_exists_and_strategy_is_FORBID() {
     db.users().insertUser(newUserDto()
-      .setLogin(USER_LOGIN)
+      .setLogin("Existing user with same email")
       .setActive(true)
+      .setEmail("john@email.com"));
+    Source source = Source.realm(Method.FORM, IDENTITY_PROVIDER.getName());
+
+    expectedException.expect(authenticationException().from(source)
+      .withLogin(USER_IDENTITY.getLogin())
+      .andPublicMessage("You can't sign up because email 'john@email.com' is already used by an existing user. " +
+        "This means that you probably already registered with another account."));
+    expectedException.expectMessage("Email 'john@email.com' is already used");
+
+    underTest.authenticate(USER_IDENTITY, IDENTITY_PROVIDER, source, FORBID);
+  }
+
+  @Test
+  public void fail_to_authenticate_new_user_when_allow_users_to_signup_is_false() {
+    TestIdentityProvider identityProvider = new TestIdentityProvider()
+      .setKey("github")
+      .setName("Github")
+      .setEnabled(true)
+      .setAllowsUsersToSignUp(false);
+    Source source = Source.realm(Method.FORM, identityProvider.getName());
+
+    expectedException.expect(authenticationException().from(source).withLogin(USER_IDENTITY.getLogin()).andPublicMessage("'github' users are not allowed to sign up"));
+    expectedException.expectMessage("User signup disabled for provider 'github'");
+    underTest.authenticate(USER_IDENTITY, identityProvider, source, FORBID);
+  }
+
+  @Test
+  public void authenticate_existing_user() {
+    db.users().insertUser(u -> u
+      .setLogin(USER_LOGIN)
       .setName("Old name")
       .setEmail("Old email")
       .setExternalIdentity("old identity")
       .setExternalIdentityProvider("old provide"));
 
-    underTest.authenticate(USER_IDENTITY, IDENTITY_PROVIDER, Source.local(Method.BASIC));
+    underTest.authenticate(USER_IDENTITY, IDENTITY_PROVIDER, Source.local(Method.BASIC), FORBID);
 
     UserDto userDto = db.users().selectUserByLogin(USER_LOGIN).get();
     assertThat(userDto.isActive()).isTrue();
@@ -197,7 +264,7 @@ public class UserIdentityAuthenticatorTest {
   @Test
   public void authenticate_existing_disabled_user() {
     organizationFlags.setEnabled(true);
-    db.users().insertUser(newUserDto()
+    db.users().insertUser(u -> u
       .setLogin(USER_LOGIN)
       .setActive(false)
       .setName("Old name")
@@ -205,7 +272,7 @@ public class UserIdentityAuthenticatorTest {
       .setExternalIdentity("old identity")
       .setExternalIdentityProvider("old provide"));
 
-    underTest.authenticate(USER_IDENTITY, IDENTITY_PROVIDER, Source.local(Method.BASIC_TOKEN));
+    underTest.authenticate(USER_IDENTITY, IDENTITY_PROVIDER, Source.local(Method.BASIC_TOKEN), FORBID);
 
     UserDto userDto = db.users().selectUserByLogin(USER_LOGIN).get();
     assertThat(userDto.isActive()).isTrue();
@@ -216,6 +283,81 @@ public class UserIdentityAuthenticatorTest {
     assertThat(userDto.isRoot()).isFalse();
   }
 
+  @Test
+  public void authenticate_existing_user_when_email_already_exists_and_strategy_is_ALLOW() {
+    organizationFlags.setEnabled(true);
+    UserDto existingUser = db.users().insertUser(u -> u.setEmail("john@email.com"));
+    UserDto currentUser = db.users().insertUser(u -> u.setEmail(null));
+    UserIdentity userIdentity = UserIdentity.builder()
+      .setLogin(currentUser.getLogin())
+      .setProviderLogin("johndoo")
+      .setName("John")
+      .setEmail("john@email.com")
+      .build();
+
+    underTest.authenticate(userIdentity, IDENTITY_PROVIDER, Source.local(Method.BASIC), ALLOW);
+
+    UserDto currentUserReloaded = db.users().selectUserByLogin(currentUser.getLogin()).get();
+    assertThat(currentUserReloaded.getEmail()).isEqualTo("john@email.com");
+    UserDto existingUserReloaded = db.users().selectUserByLogin(existingUser.getLogin()).get();
+    assertThat(existingUserReloaded.getEmail()).isNull();
+  }
+
+  @Test
+  public void throw_EmailAlreadyExistException_when_authenticating_existing_user_when_email_already_exists_and_strategy_is_WARN() {
+    organizationFlags.setEnabled(true);
+    UserDto existingUser = db.users().insertUser(u -> u.setEmail("john@email.com"));
+    UserDto currentUser = db.users().insertUser(u -> u.setEmail(null));
+    UserIdentity userIdentity = UserIdentity.builder()
+      .setLogin(currentUser.getLogin())
+      .setProviderLogin("johndoo")
+      .setName("John")
+      .setEmail("john@email.com")
+      .build();
+
+    expectedException.expect(EmailAlreadyExistsException.class);
+
+    underTest.authenticate(userIdentity, IDENTITY_PROVIDER, Source.local(Method.BASIC), WARN);
+  }
+
+  @Test
+  public void throw_AuthenticationException_when_authenticating_existing_user_when_email_already_exists_and_strategy_is_FORBID() {
+    organizationFlags.setEnabled(true);
+    UserDto existingUser = db.users().insertUser(u -> u.setEmail("john@email.com"));
+    UserDto currentUser = db.users().insertUser(u -> u.setEmail(null));
+    UserIdentity userIdentity = UserIdentity.builder()
+      .setLogin(currentUser.getLogin())
+      .setProviderLogin("johndoo")
+      .setName("John")
+      .setEmail("john@email.com")
+      .build();
+
+    expectedException.expect(authenticationException().from(Source.realm(Method.FORM, IDENTITY_PROVIDER.getName()))
+      .withLogin(userIdentity.getLogin())
+      .andPublicMessage("You can't sign up because email 'john@email.com' is already used by an existing user. " +
+        "This means that you probably already registered with another account."));
+    expectedException.expectMessage("Email 'john@email.com' is already used");
+
+    underTest.authenticate(userIdentity, IDENTITY_PROVIDER, Source.realm(Method.FORM, IDENTITY_PROVIDER.getName()), FORBID);
+  }
+
+  @Test
+  public void does_not_fail_to_authenticate_user_when_email_has_not_changed_and_strategy_is_FORBID() {
+    organizationFlags.setEnabled(true);
+    UserDto currentUser = db.users().insertUser(u -> u.setEmail("john@email.com"));
+    UserIdentity userIdentity = UserIdentity.builder()
+      .setLogin(currentUser.getLogin())
+      .setProviderLogin("johndoo")
+      .setName("John")
+      .setEmail("john@email.com")
+      .build();
+
+    underTest.authenticate(userIdentity, IDENTITY_PROVIDER, Source.local(Method.BASIC), FORBID);
+
+    UserDto currentUserReloaded = db.users().selectUserByLogin(currentUser.getLogin()).get();
+    assertThat(currentUserReloaded.getEmail()).isEqualTo("john@email.com");
+  }
+
   @Test
   public void authenticate_existing_user_and_add_new_groups() {
     organizationFlags.setEnabled(true);
@@ -296,41 +438,11 @@ public class UserIdentityAuthenticatorTest {
       .setLogin(user.getLogin())
       .setName(user.getName())
       .setGroups(newHashSet(groupName))
-      .build(), IDENTITY_PROVIDER, Source.sso());
+      .build(), IDENTITY_PROVIDER, Source.sso(), FORBID);
 
     checkGroupMembership(user, groupInDefaultOrg);
   }
 
-  @Test
-  public void fail_to_authenticate_new_user_when_allow_users_to_signup_is_false() {
-    TestIdentityProvider identityProvider = new TestIdentityProvider()
-      .setKey("github")
-      .setName("Github")
-      .setEnabled(true)
-      .setAllowsUsersToSignUp(false);
-    Source source = Source.realm(Method.FORM, identityProvider.getName());
-
-    thrown.expect(authenticationException().from(source).withLogin(USER_IDENTITY.getLogin()).andPublicMessage("'github' users are not allowed to sign up"));
-    thrown.expectMessage("User signup disabled for provider 'github'");
-    underTest.authenticate(USER_IDENTITY, identityProvider, source);
-  }
-
-  @Test
-  public void fail_to_authenticate_new_user_when_email_already_exists() {
-    db.users().insertUser(newUserDto()
-      .setLogin("Existing user with same email")
-      .setActive(true)
-      .setEmail("john@email.com"));
-    Source source = Source.realm(Method.FORM, IDENTITY_PROVIDER.getName());
-
-    thrown.expect(authenticationException().from(source)
-      .withLogin(USER_IDENTITY.getLogin())
-      .andPublicMessage("You can't sign up because email 'john@email.com' is already used by an existing user. " +
-        "This means that you probably already registered with another account."));
-    thrown.expectMessage("Email 'john@email.com' is already used");
-    underTest.authenticate(USER_IDENTITY, IDENTITY_PROVIDER, source);
-  }
-
   private void authenticate(String login, String... groups) {
     underTest.authenticate(UserIdentity.builder()
       .setProviderLogin("johndoo")
@@ -338,7 +450,7 @@ public class UserIdentityAuthenticatorTest {
       .setName("John")
       // No group
       .setGroups(stream(groups).collect(MoreCollectors.toSet()))
-      .build(), IDENTITY_PROVIDER, Source.sso());
+      .build(), IDENTITY_PROVIDER, Source.sso(), FORBID);
   }
 
   private void checkGroupMembership(UserDto user, GroupDto... expectedGroups) {
index 6883fc6e2b57368c84860bad91fff3c5fe6b5e76..6f08edb7914d5e06802f83d35bb8f8128bd113b9 100644 (file)
@@ -273,6 +273,22 @@ public class UserUpdaterCreateTest {
     assertThat(dbClient.userDao().selectByLogin(session, "user").isOnboarded()).isFalse();
   }
 
+  @Test
+  public void create_user_and_index_other_user() {
+    createDefaultGroup();
+    UserDto otherUser = db.users().insertUser();
+
+    UserDto created = underTest.createAndCommit(db.getSession(), NewUser.builder()
+      .setLogin("user")
+      .setName("User")
+      .setEmail("user@mail.com")
+      .setPassword("PASSWORD")
+      .build(), u -> {
+      }, otherUser);
+
+    assertThat(es.getIds(UserIndexDefinition.INDEX_TYPE_USER)).containsExactlyInAnyOrder(created.getLogin(), otherUser.getLogin());
+  }
+
   @Test
   public void fail_to_create_user_with_missing_login() {
     expectedException.expect(BadRequestException.class);
@@ -593,7 +609,7 @@ public class UserUpdaterCreateTest {
       .setEmail("marius2@mail.com")
       .setPassword("password2")
       .build(), u -> {
-    });
+      });
     session.commit();
 
     assertThat(dto.isActive()).isTrue();
@@ -622,7 +638,7 @@ public class UserUpdaterCreateTest {
       .setName("Marius2")
       .setEmail("marius2@mail.com")
       .build(), u -> {
-    });
+      });
     session.commit();
 
     assertThat(dto.isActive()).isTrue();
@@ -647,7 +663,7 @@ public class UserUpdaterCreateTest {
       .setName("Marius2")
       .setExternalIdentity(new ExternalIdentity("github", "john"))
       .build(), u -> {
-    });
+      });
     session.commit();
 
     UserDto dto = dbClient.userDao().selectByLogin(session, DEFAULT_LOGIN);
@@ -667,7 +683,7 @@ public class UserUpdaterCreateTest {
       .setName("Marius2")
       .setPassword("password")
       .build(), u -> {
-    });
+      });
     session.commit();
 
     UserDto dto = dbClient.userDao().selectByLogin(session, DEFAULT_LOGIN);
@@ -690,7 +706,7 @@ public class UserUpdaterCreateTest {
       .setEmail("marius2@mail.com")
       .setPassword("password2")
       .build(), u -> {
-    });
+      });
   }
 
   @Test
@@ -709,7 +725,7 @@ public class UserUpdaterCreateTest {
       .setEmail("marius2@mail.com")
       .setPassword("password2")
       .build(), u -> {
-    });
+      });
     session.commit();
 
     Multimap<String, String> groups = dbClient.groupMembershipDao().selectGroupsByLogins(session, asList(DEFAULT_LOGIN));
@@ -732,7 +748,7 @@ public class UserUpdaterCreateTest {
       .setEmail("marius2@mail.com")
       .setPassword("password2")
       .build(), u -> {
-    });
+      });
     session.commit();
 
     Multimap<String, String> groups = dbClient.groupMembershipDao().selectGroupsByLogins(session, asList(DEFAULT_LOGIN));
@@ -777,7 +793,7 @@ public class UserUpdaterCreateTest {
       .setLogin(user.getLogin())
       .setName("name")
       .build(), u -> {
-    });
+      });
 
     assertThat(dbClient.userDao().selectByLogin(session, user.getLogin()).isOnboarded()).isTrue();
   }
@@ -794,7 +810,7 @@ public class UserUpdaterCreateTest {
       .setLogin(user.getLogin())
       .setName("name")
       .build(), u -> {
-    });
+      });
 
     assertThat(dbClient.userDao().selectByLogin(session, user.getLogin()).isOnboarded()).isFalse();
   }
index 111dfc4271353aea34c2ed86c1d00669b9da0a65..f6fbc223f9fb6a103b8c969f6dbc6858cadff05b 100644 (file)
@@ -389,6 +389,23 @@ public class UserUpdaterUpdateTest {
     assertThat(dbClient.userDao().selectByLogin(session, DEFAULT_LOGIN).getUpdatedAt()).isEqualTo(user.getUpdatedAt());
   }
 
+  @Test
+  public void update_user_and_index_other_user() {
+    createDefaultGroup();
+    UserDto user = db.users().insertUser(newLocalUser(DEFAULT_LOGIN, "Marius", "marius@email.com")
+      .setScmAccounts(asList("ma", "marius33")));
+    UserDto otherUser = db.users().insertUser();
+
+    underTest.updateAndCommit(session, UpdateUser.create(DEFAULT_LOGIN)
+      .setName("Marius2")
+      .setEmail("marius2@mail.com")
+      .setPassword("password2")
+      .setScmAccounts(asList("ma2")), u -> {
+    }, otherUser);
+
+    assertThat(es.getIds(UserIndexDefinition.INDEX_TYPE_USER)).containsExactlyInAnyOrder(user.getLogin(), otherUser.getLogin());
+  }
+
   @Test
   public void fail_to_set_null_password_when_local_user() {
     db.users().insertUser(newLocalUser(DEFAULT_LOGIN, "Marius", "marius@email.com"));
index e05708f2e2e71c8ad860040dffb24d92bdc95d8c..3745186d27a2bf5c3129c6319beea7851acc1f7b 100644 (file)
@@ -113,5 +113,7 @@ public class Category4Suite {
     // reduce memory for Elasticsearch to 128M
     .setServerProperty("sonar.search.javaOpts", "-Xms128m -Xmx128m")
 
+    .setServerProperty("sonar.web.javaAdditionalOpts", "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8001")
+
     .build();
 }