diff options
author | Julien Lancelot <julien.lancelot@sonarsource.com> | 2018-01-30 13:31:10 +0100 |
---|---|---|
committer | Julien Lancelot <julien.lancelot@sonarsource.com> | 2018-02-07 16:43:01 +0100 |
commit | 465047d8b497de79ff636e98dab0e34a6edf6257 (patch) | |
tree | d9197bdccf395d1ecc57fb89a90b173d2ec35a30 /server/sonar-server | |
parent | 4991e63dbe3edb7327588c080e88c3de5a6ec3f5 (diff) | |
download | sonarqube-465047d8b497de79ff636e98dab0e34a6edf6257.tar.gz sonarqube-465047d8b497de79ff636e98dab0e34a6edf6257.zip |
SONAR-10338 Allow authentication of user using an exising email
Diffstat (limited to 'server/sonar-server')
26 files changed, 963 insertions, 426 deletions
diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/AuthenticationError.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/AuthenticationError.java index 1d087426193..81ccb80a258 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/authentication/AuthenticationError.java +++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/AuthenticationError.java @@ -19,16 +19,14 @@ */ 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); - } - } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/AuthenticationModule.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/AuthenticationModule.java index 76dcba9e106..3ed62249fc4 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/authentication/AuthenticationModule.java +++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/AuthenticationModule.java @@ -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 index 00000000000..27ebcd9ac2a --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/AuthenticationRedirection.java @@ -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); + } + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/BaseContextFactory.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/BaseContextFactory.java index 2f728fe239b..8ebb25b8deb 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/authentication/BaseContextFactory.java +++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/BaseContextFactory.java @@ -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)); } diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/Cookies.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/Cookies.java index 3af60437264..175240a7f89 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/authentication/Cookies.java +++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/Cookies.java @@ -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 index 00000000000..8e127a28e37 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/EmailAlreadyExistsException.java @@ -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())); + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/InitFilter.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/InitFilter.java index 8a699c523c1..40244e38724 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/authentication/InitFilter.java +++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/InitFilter.java @@ -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 index 00000000000..622b06c6e0f --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/OAuth2AuthenticationParameters.java @@ -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 index 00000000000..d7d1eb35bb2 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/OAuth2AuthenticationParametersImpl.java @@ -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); + } + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/OAuth2CallbackFilter.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/OAuth2CallbackFilter.java index 20f84b3df65..114050a7cee 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/authentication/OAuth2CallbackFilter.java +++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/OAuth2CallbackFilter.java @@ -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())); } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/OAuth2ContextFactory.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/OAuth2ContextFactory.java index a66a1777c64..0696ab81f38 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/authentication/OAuth2ContextFactory.java +++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/OAuth2ContextFactory.java @@ -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 index 7b7c07c48cf..00000000000 --- a/server/sonar-server/src/main/java/org/sonar/server/authentication/OAuth2Redirection.java +++ /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()); - } - -} diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/RealmAuthenticator.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/RealmAuthenticator.java index 336d8099e7a..8f081b41291 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/authentication/RealmAuthenticator.java +++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/RealmAuthenticator.java @@ -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) { diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/SsoAuthenticator.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/SsoAuthenticator.java index 5aa37b32d17..691c806b0e0 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/authentication/SsoAuthenticator.java +++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/SsoAuthenticator.java @@ -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 diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/UserIdentityAuthenticator.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/UserIdentityAuthenticator.java index 2a2add51ff8..95db2536fb5 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/authentication/UserIdentityAuthenticator.java +++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/UserIdentityAuthenticator.java @@ -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[]{}); + } + } diff --git a/server/sonar-server/src/main/java/org/sonar/server/user/UserUpdater.java b/server/sonar-server/src/main/java/org/sonar/server/user/UserUpdater.java index 9448a2c9fe1..a012215840f 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/user/UserUpdater.java +++ b/server/sonar-server/src/main/java/org/sonar/server/user/UserUpdater.java @@ -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 diff --git a/server/sonar-server/src/test/java/org/sonar/server/authentication/BaseContextFactoryTest.java b/server/sonar-server/src/test/java/org/sonar/server/authentication/BaseContextFactoryTest.java index 3ec7e10a7f6..cf17bfbce75 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/authentication/BaseContextFactoryTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/authentication/BaseContextFactoryTest.java @@ -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)); } + } diff --git a/server/sonar-server/src/test/java/org/sonar/server/authentication/InitFilterTest.java b/server/sonar-server/src/test/java/org/sonar/server/authentication/InitFilterTest.java index 8af7054d08b..4b37c140ad6 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/authentication/InitFilterTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/authentication/InitFilterTest.java @@ -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 index 00000000000..49adf58319a --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/authentication/OAuth2AuthenticationParametersImplTest.java @@ -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); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/authentication/OAuth2CallbackFilterTest.java b/server/sonar-server/src/test/java/org/sonar/server/authentication/OAuth2CallbackFilterTest.java index 9c7152bef72..792455da6bf 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/authentication/OAuth2CallbackFilterTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/authentication/OAuth2CallbackFilterTest.java @@ -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); @@ -203,6 +193,19 @@ public class OAuth2CallbackFilterTest { } @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 } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/authentication/OAuth2ContextFactoryTest.java b/server/sonar-server/src/test/java/org/sonar/server/authentication/OAuth2ContextFactoryTest.java index 263ef365e9d..02b33b9b68d 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/authentication/OAuth2ContextFactoryTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/authentication/OAuth2ContextFactoryTest.java @@ -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 index a6536910cf5..00000000000 --- a/server/sonar-server/src/test/java/org/sonar/server/authentication/OAuth2RedirectionTest.java +++ /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); - } - -} diff --git a/server/sonar-server/src/test/java/org/sonar/server/authentication/RealmAuthenticatorTest.java b/server/sonar-server/src/test/java/org/sonar/server/authentication/RealmAuthenticatorTest.java index 2ffea6ad157..ead8ab63cf3 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/authentication/RealmAuthenticatorTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/authentication/RealmAuthenticatorTest.java @@ -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"); diff --git a/server/sonar-server/src/test/java/org/sonar/server/authentication/UserIdentityAuthenticatorTest.java b/server/sonar-server/src/test/java/org/sonar/server/authentication/UserIdentityAuthenticatorTest.java index f3075bb8280..c4e774e85aa 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/authentication/UserIdentityAuthenticatorTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/authentication/UserIdentityAuthenticatorTest.java @@ -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(); @@ -217,6 +284,81 @@ public class UserIdentityAuthenticatorTest { } @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); UserDto user = db.users().insertUser(newUserDto() @@ -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) { diff --git a/server/sonar-server/src/test/java/org/sonar/server/user/UserUpdaterCreateTest.java b/server/sonar-server/src/test/java/org/sonar/server/user/UserUpdaterCreateTest.java index 6883fc6e2b5..6f08edb7914 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/user/UserUpdaterCreateTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/user/UserUpdaterCreateTest.java @@ -274,6 +274,22 @@ public class UserUpdaterCreateTest { } @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); expectedException.expectMessage("Login can't be empty"); @@ -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(); } diff --git a/server/sonar-server/src/test/java/org/sonar/server/user/UserUpdaterUpdateTest.java b/server/sonar-server/src/test/java/org/sonar/server/user/UserUpdaterUpdateTest.java index 111dfc42713..f6fbc223f9f 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/user/UserUpdaterUpdateTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/user/UserUpdaterUpdateTest.java @@ -390,6 +390,23 @@ public class UserUpdaterUpdateTest { } @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")); createDefaultGroup(); |