aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-server
diff options
context:
space:
mode:
authorJulien Lancelot <julien.lancelot@sonarsource.com>2018-01-30 13:31:10 +0100
committerJulien Lancelot <julien.lancelot@sonarsource.com>2018-02-07 16:43:01 +0100
commit465047d8b497de79ff636e98dab0e34a6edf6257 (patch)
treed9197bdccf395d1ecc57fb89a90b173d2ec35a30 /server/sonar-server
parent4991e63dbe3edb7327588c080e88c3de5a6ec3f5 (diff)
downloadsonarqube-465047d8b497de79ff636e98dab0e34a6edf6257.tar.gz
sonarqube-465047d8b497de79ff636e98dab0e34a6edf6257.zip
SONAR-10338 Allow authentication of user using an exising email
Diffstat (limited to 'server/sonar-server')
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/authentication/AuthenticationError.java21
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/authentication/AuthenticationModule.java2
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/authentication/AuthenticationRedirection.java52
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/authentication/BaseContextFactory.java5
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/authentication/Cookies.java17
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/authentication/EmailAlreadyExistsException.java58
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/authentication/InitFilter.java16
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/authentication/OAuth2AuthenticationParameters.java44
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/authentication/OAuth2AuthenticationParametersImpl.java133
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/authentication/OAuth2CallbackFilter.java14
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/authentication/OAuth2ContextFactory.java16
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/authentication/OAuth2Redirection.java78
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/authentication/RealmAuthenticator.java3
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/authentication/SsoAuthenticator.java3
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/authentication/UserIdentityAuthenticator.java101
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/user/UserUpdater.java11
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/authentication/BaseContextFactoryTest.java23
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/authentication/InitFilterTest.java87
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/authentication/OAuth2AuthenticationParametersImplTest.java167
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/authentication/OAuth2CallbackFilterTest.java73
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/authentication/OAuth2ContextFactoryTest.java52
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/authentication/OAuth2RedirectionTest.java134
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/authentication/RealmAuthenticatorTest.java30
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/authentication/UserIdentityAuthenticatorTest.java198
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/user/UserUpdaterCreateTest.java34
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/user/UserUpdaterUpdateTest.java17
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();