From 2bcd1b062b5f26b9e325b3bd2b744d5c56704352 Mon Sep 17 00:00:00 2001
From: Julien Lancelot
Date: Tue, 19 Jan 2016 11:44:16 +0100
Subject: [PATCH] SONAR-6226 Create IdentityProvider API
---
.../it/authorisation/AuthenticationTest.java | 7 +-
pom.xml | 1 -
server/sonar-server/pom.xml | 1 +
.../authentication/AuthenticationError.java | 65 +++++++
.../authentication/AuthenticationModule.java | 38 +++++
.../authentication/BaseContextFactory.java | 73 ++++++++
.../server/authentication/CsrfVerifier.java | 80 +++++++++
.../IdentityProviderRepository.java | 91 ++++++++++
.../server/authentication/InitFilter.java | 95 +++++++++++
.../NotAllowUserToSignUpException.java | 35 ++++
.../authentication/OAuth2CallbackFilter.java | 84 +++++++++
.../authentication/OAuth2ContextFactory.java | 115 +++++++++++++
.../UserIdentityAuthenticator.java | 81 +++++++++
.../server/authentication/package-info.java | 23 +++
.../org/sonar/server/platform/ServerImpl.java | 42 +++--
.../platformlevel/PlatformLevel4.java | 4 +-
.../java/org/sonar/server/ui/JRubyFacade.java | 6 +
.../org/sonar/server/user/UserUpdater.java | 24 +--
.../BaseContextFactoryTest.java | 78 +++++++++
.../authentication/CsrfVerifierTest.java | 127 ++++++++++++++
.../FakeBasicIdentityProvider.java | 42 +++++
.../FakeOAuth2IdentityProvider.java | 51 ++++++
.../IdentityProviderRepositoryRule.java | 48 ++++++
.../IdentityProviderRepositoryTest.java | 90 ++++++++++
.../server/authentication/InitFilterTest.java | 160 ++++++++++++++++++
.../OAuth2CallbackFilterTest.java | 114 +++++++++++++
.../OAuth2ContextFactoryTest.java | 148 ++++++++++++++++
.../authentication/TestIdentityProvider.java | 99 +++++++++++
.../UserIdentityAuthenticatorTest.java | 149 ++++++++++++++++
.../sonar/server/platform/ServerImplTest.java | 36 +++-
.../platform/ServerLifecycleNotifierTest.java | 23 ++-
.../server/platform/ws/StatusActionTest.java | 15 ++
.../app/controllers/sessions_controller.rb | 7 +
.../WEB-INF/app/views/sessions/_form.html.erb | 26 ++-
.../sessions/not_allowed_to_sign_up.html.erb | 10 ++
.../app/views/sessions/unauthorized.html.erb | 14 ++
.../WEB-INF/lib/authenticated_system.rb | 8 +-
.../sonar/batch/platform/DefaultServer.java | 32 ++--
.../java/org/sonar/api/platform/Server.java | 30 +++-
.../authentication/BaseIdentityProvider.java | 67 ++++++++
.../authentication/IdentityProvider.java | 70 ++++++++
.../OAuth2IdentityProvider.java | 95 +++++++++++
.../server/authentication/UserIdentity.java | 129 ++++++++++++++
.../server/authentication/package-info.java | 23 +++
.../authentication/UserIdentityTest.java | 142 ++++++++++++++++
45 files changed, 2640 insertions(+), 58 deletions(-)
create mode 100644 server/sonar-server/src/main/java/org/sonar/server/authentication/AuthenticationError.java
create mode 100644 server/sonar-server/src/main/java/org/sonar/server/authentication/AuthenticationModule.java
create mode 100644 server/sonar-server/src/main/java/org/sonar/server/authentication/BaseContextFactory.java
create mode 100644 server/sonar-server/src/main/java/org/sonar/server/authentication/CsrfVerifier.java
create mode 100644 server/sonar-server/src/main/java/org/sonar/server/authentication/IdentityProviderRepository.java
create mode 100644 server/sonar-server/src/main/java/org/sonar/server/authentication/InitFilter.java
create mode 100644 server/sonar-server/src/main/java/org/sonar/server/authentication/NotAllowUserToSignUpException.java
create mode 100644 server/sonar-server/src/main/java/org/sonar/server/authentication/OAuth2CallbackFilter.java
create mode 100644 server/sonar-server/src/main/java/org/sonar/server/authentication/OAuth2ContextFactory.java
create mode 100644 server/sonar-server/src/main/java/org/sonar/server/authentication/UserIdentityAuthenticator.java
create mode 100644 server/sonar-server/src/main/java/org/sonar/server/authentication/package-info.java
create mode 100644 server/sonar-server/src/test/java/org/sonar/server/authentication/BaseContextFactoryTest.java
create mode 100644 server/sonar-server/src/test/java/org/sonar/server/authentication/CsrfVerifierTest.java
create mode 100644 server/sonar-server/src/test/java/org/sonar/server/authentication/FakeBasicIdentityProvider.java
create mode 100644 server/sonar-server/src/test/java/org/sonar/server/authentication/FakeOAuth2IdentityProvider.java
create mode 100644 server/sonar-server/src/test/java/org/sonar/server/authentication/IdentityProviderRepositoryRule.java
create mode 100644 server/sonar-server/src/test/java/org/sonar/server/authentication/IdentityProviderRepositoryTest.java
create mode 100644 server/sonar-server/src/test/java/org/sonar/server/authentication/InitFilterTest.java
create mode 100644 server/sonar-server/src/test/java/org/sonar/server/authentication/OAuth2CallbackFilterTest.java
create mode 100644 server/sonar-server/src/test/java/org/sonar/server/authentication/OAuth2ContextFactoryTest.java
create mode 100644 server/sonar-server/src/test/java/org/sonar/server/authentication/TestIdentityProvider.java
create mode 100644 server/sonar-server/src/test/java/org/sonar/server/authentication/UserIdentityAuthenticatorTest.java
create mode 100644 server/sonar-web/src/main/webapp/WEB-INF/app/views/sessions/not_allowed_to_sign_up.html.erb
create mode 100644 server/sonar-web/src/main/webapp/WEB-INF/app/views/sessions/unauthorized.html.erb
create mode 100644 sonar-plugin-api/src/main/java/org/sonar/api/server/authentication/BaseIdentityProvider.java
create mode 100644 sonar-plugin-api/src/main/java/org/sonar/api/server/authentication/IdentityProvider.java
create mode 100644 sonar-plugin-api/src/main/java/org/sonar/api/server/authentication/OAuth2IdentityProvider.java
create mode 100644 sonar-plugin-api/src/main/java/org/sonar/api/server/authentication/UserIdentity.java
create mode 100644 sonar-plugin-api/src/main/java/org/sonar/api/server/authentication/package-info.java
create mode 100644 sonar-plugin-api/src/test/java/org/sonar/api/server/authentication/UserIdentityTest.java
diff --git a/it/it-tests/src/test/java/it/authorisation/AuthenticationTest.java b/it/it-tests/src/test/java/it/authorisation/AuthenticationTest.java
index 818f5cf37fd..1ed047c08bb 100644
--- a/it/it-tests/src/test/java/it/authorisation/AuthenticationTest.java
+++ b/it/it-tests/src/test/java/it/authorisation/AuthenticationTest.java
@@ -118,11 +118,8 @@ public class AuthenticationTest {
assertThat(searchResponse.getUserTokensCount()).isEqualTo(0);
}
- /**
- * This is currently a limitation of Ruby on Rails stack.
- */
@Test
- public void basic_authentication_does_not_support_utf8_passwords() {
+ public void basic_authentication_with_utf8_passwords() {
String userId = UUID.randomUUID().toString();
String login = format("login-%s", userId);
// see http://www.cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-test.txt
@@ -134,7 +131,7 @@ public class AuthenticationTest {
// authenticate
WsClient wsClient = new HttpWsClient(new HttpConnector.Builder().url(ORCHESTRATOR.getServer().getUrl()).credentials(login, password).build());
WsResponse response = wsClient.wsConnector().call(new GetRequest("api/authentication/validate"));
- assertThat(response.content()).isEqualTo("{\"valid\":false}");
+ assertThat(response.content()).isEqualTo("{\"valid\":true}");
}
@Test
diff --git a/pom.xml b/pom.xml
index 8567ca2c751..b438f480407 100644
--- a/pom.xml
+++ b/pom.xml
@@ -919,7 +919,6 @@
test
-
org.apache.tomcat.embed
diff --git a/server/sonar-server/pom.xml b/server/sonar-server/pom.xml
index e03b5d89805..aaaae163989 100644
--- a/server/sonar-server/pom.xml
+++ b/server/sonar-server/pom.xml
@@ -185,6 +185,7 @@
${project.groupId}sonar-dev-cockpit-bridge
+
${project.groupId}
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
new file mode 100644
index 00000000000..35e4bcc3d74
--- /dev/null
+++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/AuthenticationError.java
@@ -0,0 +1,65 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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 javax.servlet.http.HttpServletResponse;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+
+import static java.lang.String.format;
+
+public class AuthenticationError {
+
+ private static final Logger LOGGER = Loggers.get(AuthenticationError.class);
+
+ private static final String UNAUTHORIZED_PATH = "/sessions/unauthorized";
+ private static final String NOT_ALLOWED_TO_SIGHNUP_PATH = "/sessions/not_allowed_to_sign_up?providerName=%s";
+
+ private AuthenticationError() {
+ // Utility class
+ }
+
+ public static void handleError(Exception e, HttpServletResponse response, String message) {
+ LOGGER.error(message, e);
+ redirectToUnauthorized(response);
+ }
+
+ public static void handleError(HttpServletResponse response, String message) {
+ LOGGER.error(message);
+ redirectToUnauthorized(response);
+ }
+
+ public static void handleNotAllowedToSignUpError(NotAllowUserToSignUpException e, HttpServletResponse response) {
+ redirectTo(response, format(NOT_ALLOWED_TO_SIGHNUP_PATH, e.getProvider().getName()));
+ }
+
+ private 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
new file mode 100644
index 00000000000..2ecebed37bd
--- /dev/null
+++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/AuthenticationModule.java
@@ -0,0 +1,38 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.core.platform.Module;
+import org.sonar.server.authentication.ws.AuthenticationWs;
+
+public class AuthenticationModule extends Module {
+ @Override
+ protected void configureModule() {
+ add(
+ AuthenticationWs.class,
+ InitFilter.class,
+ OAuth2CallbackFilter.class,
+ IdentityProviderRepository.class,
+ BaseContextFactory.class,
+ OAuth2ContextFactory.class,
+ UserIdentityAuthenticator.class,
+ CsrfVerifier.class);
+ }
+}
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
new file mode 100644
index 00000000000..465f093f903
--- /dev/null
+++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/BaseContextFactory.java
@@ -0,0 +1,73 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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 javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.sonar.api.platform.Server;
+import org.sonar.api.server.authentication.BaseIdentityProvider;
+import org.sonar.api.server.authentication.UserIdentity;
+
+public class BaseContextFactory {
+
+ private final UserIdentityAuthenticator userIdentityAuthenticator;
+ private final Server server;
+
+ public BaseContextFactory(UserIdentityAuthenticator userIdentityAuthenticator, Server server) {
+ this.userIdentityAuthenticator = userIdentityAuthenticator;
+ this.server = server;
+ }
+
+ public BaseIdentityProvider.Context newContext(HttpServletRequest request, HttpServletResponse response, BaseIdentityProvider identityProvider) {
+ return new ContextImpl(request, response, identityProvider);
+ }
+
+ private class ContextImpl implements BaseIdentityProvider.Context {
+ private final HttpServletRequest request;
+ private final HttpServletResponse response;
+ private final BaseIdentityProvider identityProvider;
+
+ public ContextImpl(HttpServletRequest request, HttpServletResponse response, BaseIdentityProvider identityProvider) {
+ this.request = request;
+ this.response = response;
+ this.identityProvider = identityProvider;
+ }
+
+ @Override
+ public HttpServletRequest getRequest() {
+ return request;
+ }
+
+ @Override
+ public HttpServletResponse getResponse() {
+ return response;
+ }
+
+ @Override
+ public String getServerBaseURL() {
+ return server.getPublicRootUrl();
+ }
+
+ @Override
+ public void authenticate(UserIdentity userIdentity) {
+ userIdentityAuthenticator.authenticate(userIdentity, identityProvider, request.getSession());
+ }
+ }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/CsrfVerifier.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/CsrfVerifier.java
new file mode 100644
index 00000000000..9a0371506fb
--- /dev/null
+++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/CsrfVerifier.java
@@ -0,0 +1,80 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.math.BigInteger;
+import java.security.SecureRandom;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.sonar.api.platform.Server;
+import org.sonar.server.exceptions.UnauthorizedException;
+
+import static org.apache.commons.codec.digest.DigestUtils.sha256Hex;
+import static org.apache.commons.lang.StringUtils.isBlank;
+
+public class CsrfVerifier {
+
+ private static final String CSRF_STATE_COOKIE = "OAUTHSTATE";
+
+ private final Server server;
+
+ public CsrfVerifier(Server server) {
+ this.server = server;
+ }
+
+ public String generateState(HttpServletResponse response) {
+ // Create a state token to prevent request forgery.
+ // Store it in the session for later validation.
+ String state = new BigInteger(130, new SecureRandom()).toString(32);
+ Cookie cookie = new Cookie(CSRF_STATE_COOKIE, sha256Hex(state));
+ cookie.setPath("/");
+ cookie.setHttpOnly(true);
+ cookie.setMaxAge(-1);
+ cookie.setSecure(server.isSecured());
+ response.addCookie(cookie);
+ return state;
+ }
+
+ public void verifyState(HttpServletRequest request, HttpServletResponse response) {
+ Cookie stateCookie = null;
+ Cookie[] cookies = request.getCookies();
+ for (Cookie cookie : cookies) {
+ if (CSRF_STATE_COOKIE.equals(cookie.getName())) {
+ stateCookie = cookie;
+ }
+ }
+ if (stateCookie == null) {
+ throw new UnauthorizedException();
+ }
+ String hashInCookie = stateCookie.getValue();
+
+ // remove cookie
+ stateCookie.setValue(null);
+ stateCookie.setMaxAge(0);
+ stateCookie.setPath("/");
+ response.addCookie(stateCookie);
+
+ String stateInRequest = request.getParameter("state");
+ if (isBlank(stateInRequest) || !sha256Hex(stateInRequest).equals(hashInCookie)) {
+ throw new UnauthorizedException();
+ }
+ }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/IdentityProviderRepository.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/IdentityProviderRepository.java
new file mode 100644
index 00000000000..b20acc191d9
--- /dev/null
+++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/IdentityProviderRepository.java
@@ -0,0 +1,91 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.base.Function;
+import com.google.common.base.Predicate;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.Ordering;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.Nonnull;
+import org.sonar.api.server.authentication.IdentityProvider;
+
+import static com.google.common.collect.FluentIterable.from;
+
+public class IdentityProviderRepository {
+
+ protected final Map providersByKey = new HashMap<>();
+
+ public IdentityProviderRepository(List identityProviders) {
+ this.providersByKey.putAll(FluentIterable.from(identityProviders).uniqueIndex(ToKey.INSTANCE));
+ }
+
+ /**
+ * Used by pico when no identity provider available
+ */
+ public IdentityProviderRepository() {
+ this.providersByKey.clear();
+ }
+
+ public IdentityProvider getEnabledByKey(String key) {
+ IdentityProvider identityProvider = providersByKey.get(key);
+ if (identityProvider != null && IsEnabledFilter.INSTANCE.apply(identityProvider)) {
+ return identityProvider;
+ }
+ throw new IllegalArgumentException(String.format("Identity provider %s does not exist or is not enabled", key));
+ }
+
+ public List getAllEnabledAndSorted() {
+ return from(providersByKey.values())
+ .filter(IsEnabledFilter.INSTANCE)
+ .toSortedList(
+ Ordering.natural().onResultOf(ToName.INSTANCE)
+ );
+ }
+
+ private enum IsEnabledFilter implements Predicate {
+ INSTANCE;
+
+ @Override
+ public boolean apply(@Nonnull IdentityProvider input) {
+ return input.isEnabled();
+ }
+ }
+
+ private enum ToKey implements Function {
+ INSTANCE;
+
+ @Override
+ public String apply(@Nonnull IdentityProvider input) {
+ return input.getKey();
+ }
+ }
+
+ private enum ToName implements Function {
+ INSTANCE;
+
+ @Override
+ public String apply(@Nonnull IdentityProvider input) {
+ return input.getName();
+ }
+ }
+}
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
new file mode 100644
index 00000000000..65b0eee5cb6
--- /dev/null
+++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/InitFilter.java
@@ -0,0 +1,95 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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 javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.sonar.api.server.authentication.BaseIdentityProvider;
+import org.sonar.api.server.authentication.IdentityProvider;
+import org.sonar.api.server.authentication.OAuth2IdentityProvider;
+import org.sonar.api.web.ServletFilter;
+
+import static com.google.common.base.Strings.isNullOrEmpty;
+import static java.lang.String.format;
+import static org.sonar.server.authentication.AuthenticationError.handleError;
+import static org.sonar.server.authentication.AuthenticationError.handleNotAllowedToSignUpError;
+
+public class InitFilter extends ServletFilter {
+
+ private static final String INIT_CONTEXT = "/sessions/init";
+
+ private final IdentityProviderRepository identityProviderRepository;
+ private final BaseContextFactory baseContextFactory;
+ private final OAuth2ContextFactory oAuth2ContextFactory;
+
+ public InitFilter(IdentityProviderRepository identityProviderRepository, BaseContextFactory baseContextFactory, OAuth2ContextFactory oAuth2ContextFactory) {
+ this.identityProviderRepository = identityProviderRepository;
+ this.baseContextFactory = baseContextFactory;
+ this.oAuth2ContextFactory = oAuth2ContextFactory;
+ }
+
+ @Override
+ public UrlPattern doGetPattern() {
+ return UrlPattern.create(INIT_CONTEXT + "/*");
+ }
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+ HttpServletRequest httpRequest = (HttpServletRequest) request;
+ String requestUri = httpRequest.getRequestURI();
+ final String keyProvider = requestUri.replace(INIT_CONTEXT + "/", "");
+ try {
+ if (isNullOrEmpty(keyProvider)) {
+ throw new IllegalArgumentException("A valid identity provider key is required");
+ }
+
+ IdentityProvider provider = identityProviderRepository.getEnabledByKey(keyProvider);
+ if (provider instanceof BaseIdentityProvider) {
+ BaseIdentityProvider baseIdentityProvider = (BaseIdentityProvider) provider;
+ baseIdentityProvider.init(baseContextFactory.newContext(httpRequest, (HttpServletResponse) response, baseIdentityProvider));
+ } else if (provider instanceof OAuth2IdentityProvider) {
+ OAuth2IdentityProvider oAuth2IdentityProvider = (OAuth2IdentityProvider) provider;
+ oAuth2IdentityProvider.init(oAuth2ContextFactory.newContext(httpRequest, (HttpServletResponse) response, oAuth2IdentityProvider));
+ } else {
+ throw new UnsupportedOperationException(format("Unsupported IdentityProvider class: %s ", provider.getClass()));
+ }
+ } catch (NotAllowUserToSignUpException e) {
+ handleNotAllowedToSignUpError(e, (HttpServletResponse) response);
+ } catch (Exception e) {
+ handleError(e, (HttpServletResponse) response, String.format("Fail to initialize authentication with provider '%s'", keyProvider));
+ }
+ }
+
+ @Override
+ public void init(FilterConfig filterConfig) throws ServletException {
+ // Nothing to do
+ }
+
+ @Override
+ public void destroy() {
+ // Nothing to do
+ }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/NotAllowUserToSignUpException.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/NotAllowUserToSignUpException.java
new file mode 100644
index 00000000000..02269964a45
--- /dev/null
+++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/NotAllowUserToSignUpException.java
@@ -0,0 +1,35 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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;
+
+public class NotAllowUserToSignUpException extends RuntimeException {
+
+ private final IdentityProvider provider;
+
+ public NotAllowUserToSignUpException(IdentityProvider provider) {
+ this.provider = provider;
+ }
+
+ public IdentityProvider getProvider() {
+ return provider;
+ }
+}
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
new file mode 100644
index 00000000000..7240a936f7f
--- /dev/null
+++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/OAuth2CallbackFilter.java
@@ -0,0 +1,84 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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 javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.sonar.api.server.authentication.IdentityProvider;
+import org.sonar.api.server.authentication.OAuth2IdentityProvider;
+import org.sonar.api.web.ServletFilter;
+
+import static org.sonar.server.authentication.AuthenticationError.handleError;
+import static org.sonar.server.authentication.AuthenticationError.handleNotAllowedToSignUpError;
+
+public class OAuth2CallbackFilter extends ServletFilter {
+
+ public static final String CALLBACK_PATH = "/oauth2/callback";
+
+ private final IdentityProviderRepository identityProviderRepository;
+ private final OAuth2ContextFactory oAuth2ContextFactory;
+
+ public OAuth2CallbackFilter(IdentityProviderRepository identityProviderRepository, OAuth2ContextFactory oAuth2ContextFactory) {
+ this.identityProviderRepository = identityProviderRepository;
+ this.oAuth2ContextFactory = oAuth2ContextFactory;
+ }
+
+ @Override
+ public UrlPattern doGetPattern() {
+ return UrlPattern.create(CALLBACK_PATH + "/*");
+ }
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+ HttpServletRequest httpRequest = (HttpServletRequest) request;
+ String requestUri = httpRequest.getRequestURI();
+ String keyProvider = requestUri.replace(CALLBACK_PATH + "/", "");
+
+ try {
+ IdentityProvider provider = identityProviderRepository.getEnabledByKey(keyProvider);
+ if (provider instanceof OAuth2IdentityProvider) {
+ OAuth2IdentityProvider oauthProvider = (OAuth2IdentityProvider) provider;
+ oauthProvider.callback(oAuth2ContextFactory.newCallback(httpRequest, (HttpServletResponse) response, oauthProvider));
+ } else {
+ handleError((HttpServletResponse) response, String.format("Not an OAuth2IdentityProvider: %s", provider.getClass()));
+ }
+ } catch (NotAllowUserToSignUpException e) {
+ handleNotAllowedToSignUpError(e, (HttpServletResponse) response);
+ } catch (Exception e) {
+ handleError(e, (HttpServletResponse) response, String.format("Fail to callback authentication with %s", keyProvider));
+ }
+ }
+
+ @Override
+ public void init(FilterConfig filterConfig) throws ServletException {
+ // Nothing to do
+ }
+
+ @Override
+ public void destroy() {
+ // Nothing to do
+ }
+}
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
new file mode 100644
index 00000000000..ce1d920becd
--- /dev/null
+++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/OAuth2ContextFactory.java
@@ -0,0 +1,115 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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 javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.sonar.api.platform.Server;
+import org.sonar.api.server.authentication.OAuth2IdentityProvider;
+import org.sonar.api.server.authentication.UserIdentity;
+
+import static org.sonar.api.CoreProperties.SERVER_BASE_URL;
+
+public class OAuth2ContextFactory {
+
+ private final UserIdentityAuthenticator userIdentityAuthenticator;
+ private final Server server;
+ private final CsrfVerifier csrfVerifier;
+
+ public OAuth2ContextFactory(UserIdentityAuthenticator userIdentityAuthenticator, Server server, CsrfVerifier csrfVerifier) {
+ this.userIdentityAuthenticator = userIdentityAuthenticator;
+ this.server = server;
+ this.csrfVerifier = csrfVerifier;
+ }
+
+ public OAuth2IdentityProvider.InitContext newContext(HttpServletRequest request, HttpServletResponse response, OAuth2IdentityProvider identityProvider) {
+ return new OAuthContextImpl(request, response, identityProvider);
+ }
+
+ public OAuth2IdentityProvider.CallbackContext newCallback(HttpServletRequest request, HttpServletResponse response, OAuth2IdentityProvider identityProvider) {
+ return new OAuthContextImpl(request, response, identityProvider);
+ }
+
+ private class OAuthContextImpl implements OAuth2IdentityProvider.InitContext, OAuth2IdentityProvider.CallbackContext {
+
+ private final HttpServletRequest request;
+ private final HttpServletResponse response;
+ private final OAuth2IdentityProvider identityProvider;
+
+ public OAuthContextImpl(HttpServletRequest request, HttpServletResponse response, OAuth2IdentityProvider identityProvider) {
+ this.request = request;
+ this.response = response;
+ this.identityProvider = identityProvider;
+ }
+
+ @Override
+ public String getCallbackUrl() {
+ String publicRootUrl = server.getPublicRootUrl();
+ if (publicRootUrl.startsWith("http:") && !server.isDev()) {
+ throw new IllegalStateException(String.format("The server url should be configured in https, please update the property '%s'", SERVER_BASE_URL));
+ }
+ return publicRootUrl + OAuth2CallbackFilter.CALLBACK_PATH + "/" + identityProvider.getKey();
+ }
+
+ @Override
+ public String generateCsrfState() {
+ return csrfVerifier.generateState(response);
+ }
+
+ @Override
+ public HttpServletRequest getRequest() {
+ return request;
+ }
+
+ @Override
+ public HttpServletResponse getResponse() {
+ return response;
+ }
+
+ @Override
+ public void redirectTo(String url) {
+ try {
+ response.sendRedirect(url);
+ } catch (IOException e) {
+ throw new IllegalStateException(String.format("Fail to redirect to %s", url), e);
+ }
+ }
+
+ @Override
+ public void verifyCsrfState() {
+ csrfVerifier.verifyState(request, response);
+ }
+
+ @Override
+ public void redirectToRequestedPage() {
+ try {
+ getResponse().sendRedirect("/");
+ } catch (IOException e) {
+ throw new IllegalStateException("Fail to redirect to home", e);
+ }
+ }
+
+ @Override
+ public void authenticate(UserIdentity userIdentity) {
+ userIdentityAuthenticator.authenticate(userIdentity, identityProvider, request.getSession());
+ }
+ }
+}
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
new file mode 100644
index 00000000000..803b3fc39a8
--- /dev/null
+++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/UserIdentityAuthenticator.java
@@ -0,0 +1,81 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.base.Optional;
+import javax.servlet.http.HttpSession;
+import org.sonar.api.server.authentication.IdentityProvider;
+import org.sonar.api.server.authentication.UserIdentity;
+import org.sonar.core.util.UuidFactory;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.user.UserDto;
+import org.sonar.server.user.NewUser;
+import org.sonar.server.user.UpdateUser;
+import org.sonar.server.user.UserUpdater;
+
+public class UserIdentityAuthenticator {
+
+ private final DbClient dbClient;
+ private final UserUpdater userUpdater;
+ private final UuidFactory uuidFactory;
+
+ public UserIdentityAuthenticator(DbClient dbClient, UserUpdater userUpdater, UuidFactory uuidFactory) {
+ this.dbClient = dbClient;
+ this.userUpdater = userUpdater;
+ this.uuidFactory = uuidFactory;
+ }
+
+ public void authenticate(UserIdentity user, IdentityProvider provider, HttpSession session) {
+ long userDbId = register(user, provider);
+
+ // hack to disable Ruby on Rails authentication
+ session.setAttribute("user_id", userDbId);
+ }
+
+ private long register(UserIdentity user, IdentityProvider provider) {
+ DbSession dbSession = dbClient.openSession(false);
+ try {
+ String userId = user.getId();
+ Optional userDto = dbClient.userDao().selectByExternalIdentity(dbSession, userId, provider.getKey());
+ if (userDto.isPresent() && userDto.get().isActive()) {
+ userUpdater.update(dbSession, UpdateUser.create(userDto.get().getLogin())
+ .setEmail(user.getEmail())
+ .setName(user.getName())
+ );
+ return userDto.get().getId();
+ }
+
+ if (!provider.allowsUsersToSignUp()) {
+ throw new NotAllowUserToSignUpException(provider);
+ }
+ userUpdater.create(dbSession, NewUser.create()
+ .setLogin(uuidFactory.create())
+ .setEmail(user.getEmail())
+ .setName(user.getName())
+ .setExternalIdentity(new NewUser.ExternalIdentity(provider.getKey(), userId))
+ );
+ return dbClient.userDao().selectOrFailByExternalIdentity(dbSession, userId, provider.getKey()).getId();
+
+ } finally {
+ dbClient.closeSession(dbSession);
+ }
+ }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/package-info.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/package-info.java
new file mode 100644
index 00000000000..e62e32d0b94
--- /dev/null
+++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.server.authentication;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/ServerImpl.java b/server/sonar-server/src/main/java/org/sonar/server/platform/ServerImpl.java
index 04ff6969808..7ab16e8d70d 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/platform/ServerImpl.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/platform/ServerImpl.java
@@ -21,7 +21,17 @@ package org.sonar.server.platform;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
+import com.google.common.base.Objects;
import com.google.common.io.Resources;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Properties;
+import javax.annotation.CheckForNull;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.picocontainer.Startable;
@@ -32,16 +42,8 @@ import org.sonar.api.utils.log.Logger;
import org.sonar.api.utils.log.Loggers;
import org.sonar.process.ProcessProperties;
-import javax.annotation.CheckForNull;
-
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.URL;
-import java.nio.charset.StandardCharsets;
-import java.text.SimpleDateFormat;
-import java.util.Date;
-import java.util.Properties;
+import static org.sonar.api.CoreProperties.SERVER_BASE_URL;
+import static org.sonar.api.CoreProperties.SERVER_BASE_URL_DEFAULT_VALUE;
public final class ServerImpl extends Server implements Startable {
private static final Logger LOG = Loggers.get(ServerImpl.class);
@@ -139,6 +141,21 @@ public final class ServerImpl extends Server implements Startable {
return contextPath;
}
+ @Override
+ public String getPublicRootUrl() {
+ return get(SERVER_BASE_URL, SERVER_BASE_URL_DEFAULT_VALUE);
+ }
+
+ @Override
+ public boolean isDev() {
+ return settings.getBoolean("sonar.web.dev");
+ }
+
+ @Override
+ public boolean isSecured() {
+ return get(SERVER_BASE_URL, SERVER_BASE_URL_DEFAULT_VALUE).startsWith("https://");
+ }
+
private static String readVersion(String filename) throws IOException {
URL url = ServerImpl.class.getResource(filename);
if (url != null) {
@@ -170,4 +187,9 @@ public final class ServerImpl extends Server implements Startable {
public String getURL() {
return null;
}
+
+ private String get(String key, String defaultValue) {
+ return Objects.firstNonNull(settings.getString(key), defaultValue);
+ }
+
}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
index 1e1d569e77f..89b2cfd82e0 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
@@ -48,7 +48,7 @@ import org.sonar.server.activity.index.ActivityIndexDefinition;
import org.sonar.server.activity.index.ActivityIndexer;
import org.sonar.server.activity.ws.ActivitiesWs;
import org.sonar.server.activity.ws.ActivityMapping;
-import org.sonar.server.authentication.ws.AuthenticationWs;
+import org.sonar.server.authentication.AuthenticationModule;
import org.sonar.server.batch.BatchWsModule;
import org.sonar.server.charts.ChartFactory;
import org.sonar.server.charts.DistributionAreaChart;
@@ -521,7 +521,7 @@ public class PlatformLevel4 extends PlatformLevel {
L10nWs.class,
// authentication
- AuthenticationWs.class,
+ AuthenticationModule.class,
// users
SecurityRealmFactory.class,
diff --git a/server/sonar-server/src/main/java/org/sonar/server/ui/JRubyFacade.java b/server/sonar-server/src/main/java/org/sonar/server/ui/JRubyFacade.java
index 402900b4083..1ac5edd05ec 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/ui/JRubyFacade.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/ui/JRubyFacade.java
@@ -49,6 +49,8 @@ import org.sonar.db.component.ResourceIndexDao;
import org.sonar.db.version.DatabaseMigration;
import org.sonar.db.version.DatabaseVersion;
import org.sonar.process.ProcessProperties;
+import org.sonar.server.authentication.IdentityProviderRepository;
+import org.sonar.api.server.authentication.IdentityProvider;
import org.sonar.server.component.ComponentCleanerService;
import org.sonar.server.db.migrations.DatabaseMigrator;
import org.sonar.server.measure.MeasureFilterEngine;
@@ -411,4 +413,8 @@ public final class JRubyFacade {
get(ResourceIndexDao.class).indexResource(resourceId);
}
+ public List getIdentityProviders(){
+ return get(IdentityProviderRepository.class).getAllEnabledAndSorted();
+ }
+
}
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 2003643daee..9fd0e44b238 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
@@ -133,20 +133,24 @@ public class UserUpdater {
public void update(UpdateUser updateUser) {
DbSession dbSession = dbClient.openSession(false);
try {
- UserDto user = dbClient.userDao().selectByLogin(dbSession, updateUser.login());
- if (user == null) {
- throw new NotFoundException(String.format("User with login '%s' has not been found", updateUser.login()));
- }
- updateUserDto(dbSession, updateUser, user);
- updateUser(dbSession, user);
- dbSession.commit();
- notifyNewUser(user.getLogin(), user.getName(), user.getEmail());
- userIndexer.index();
+ update(dbSession, updateUser);
} finally {
dbClient.closeSession(dbSession);
}
}
+ public void update(DbSession dbSession, UpdateUser updateUser) {
+ UserDto user = dbClient.userDao().selectByLogin(dbSession, updateUser.login());
+ if (user == null) {
+ throw new NotFoundException(String.format("User with login '%s' has not been found", updateUser.login()));
+ }
+ updateUserDto(dbSession, updateUser, user);
+ updateUser(dbSession, user);
+ dbSession.commit();
+ notifyNewUser(user.getLogin(), user.getName(), user.getEmail());
+ userIndexer.index();
+ }
+
public void deactivateUserByLogin(String login) {
DbSession dbSession = dbClient.openSession(false);
try {
@@ -297,7 +301,7 @@ public class UserUpdater {
}
private void validateScmAccounts(DbSession dbSession, List scmAccounts, @Nullable String login, @Nullable String email, @Nullable UserDto existingUser,
- List messages) {
+ List messages) {
for (String scmAccount : scmAccounts) {
if (scmAccount.equals(login) || scmAccount.equals(email)) {
messages.add(Message.of("user.login_or_email_used_as_scm_account"));
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
new file mode 100644
index 00000000000..08ca98fa224
--- /dev/null
+++ b/server/sonar-server/src/test/java/org/sonar/server/authentication/BaseContextFactoryTest.java
@@ -0,0 +1,78 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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 javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+import org.junit.Before;
+import org.junit.Test;
+import org.sonar.api.platform.Server;
+import org.sonar.api.server.authentication.BaseIdentityProvider;
+import org.sonar.api.server.authentication.UserIdentity;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class BaseContextFactoryTest {
+
+ static String PUBLIC_ROOT_URL = "https://mydomain.com";
+
+ static UserIdentity USER_IDENTITY = UserIdentity.builder()
+ .setId("johndoo")
+ .setName("John")
+ .setEmail("john@email.com")
+ .build();
+
+ UserIdentityAuthenticator userIdentityAuthenticator = mock(UserIdentityAuthenticator.class);
+ Server server = mock(Server.class);
+
+ HttpServletRequest request = mock(HttpServletRequest.class);
+ HttpServletResponse response = mock(HttpServletResponse.class);
+ BaseIdentityProvider identityProvider = mock(BaseIdentityProvider.class);
+
+ BaseContextFactory underTest = new BaseContextFactory(userIdentityAuthenticator, server);
+
+ @Before
+ public void setUp() throws Exception {
+ when(server.getPublicRootUrl()).thenReturn(PUBLIC_ROOT_URL);
+ }
+
+ @Test
+ public void create_context() throws Exception {
+ BaseIdentityProvider.Context context = underTest.newContext(request, response, identityProvider);
+
+ assertThat(context.getRequest()).isEqualTo(request);
+ assertThat(context.getResponse()).isEqualTo(response);
+ assertThat(context.getServerBaseURL()).isEqualTo(PUBLIC_ROOT_URL);
+ }
+
+ @Test
+ public void authenticate() throws Exception {
+ 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, session);
+ }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/authentication/CsrfVerifierTest.java b/server/sonar-server/src/test/java/org/sonar/server/authentication/CsrfVerifierTest.java
new file mode 100644
index 00000000000..918e0e9f27b
--- /dev/null
+++ b/server/sonar-server/src/test/java/org/sonar/server/authentication/CsrfVerifierTest.java
@@ -0,0 +1,127 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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 javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.mockito.ArgumentCaptor;
+import org.sonar.api.platform.Server;
+import org.sonar.server.exceptions.UnauthorizedException;
+
+import static org.apache.commons.codec.digest.DigestUtils.sha1Hex;
+import static org.apache.commons.codec.digest.DigestUtils.sha256Hex;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class CsrfVerifierTest {
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ ArgumentCaptor cookieArgumentCaptor = ArgumentCaptor.forClass(Cookie.class);
+
+ Server server = mock(Server.class);
+ HttpServletResponse response = mock(HttpServletResponse.class);
+ HttpServletRequest request = mock(HttpServletRequest.class);
+
+ CsrfVerifier underTest = new CsrfVerifier(server);
+
+ @Test
+ public void generate_state_on_secured_server() throws Exception {
+ when(server.isSecured()).thenReturn(true);
+
+ String state = underTest.generateState(response);
+ assertThat(state).isNotEmpty();
+
+ verify(response).addCookie(cookieArgumentCaptor.capture());
+
+ verifyCookie(cookieArgumentCaptor.getValue(), true);
+ }
+
+ @Test
+ public void generate_state_on_not_secured_server() throws Exception {
+ when(server.isSecured()).thenReturn(false);
+
+ String state = underTest.generateState(response);
+ assertThat(state).isNotEmpty();
+
+ verify(response).addCookie(cookieArgumentCaptor.capture());
+
+ verifyCookie(cookieArgumentCaptor.getValue(), false);
+ }
+
+ @Test
+ public void verify_state() throws Exception {
+ String state = "state";
+ when(request.getCookies()).thenReturn(new Cookie[] {new Cookie("OAUTHSTATE", sha256Hex(state))});
+ when(request.getParameter("state")).thenReturn(state);
+
+ underTest.verifyState(request, response);
+
+ verify(response).addCookie(cookieArgumentCaptor.capture());
+ Cookie updatedCookie = cookieArgumentCaptor.getValue();
+ assertThat(updatedCookie.getName()).isEqualTo("OAUTHSTATE");
+ assertThat(updatedCookie.getValue()).isNull();
+ assertThat(updatedCookie.getPath()).isEqualTo("/");
+ assertThat(updatedCookie.getMaxAge()).isEqualTo(0);
+ }
+
+ @Test
+ public void fail_with_unauthorized_when_state_cookie_is_not_the_same_as_state_parameter() throws Exception {
+ when(request.getCookies()).thenReturn(new Cookie[] {new Cookie("OAUTHSTATE", sha1Hex("state"))});
+ when(request.getParameter("state")).thenReturn("other value");
+
+ thrown.expect(UnauthorizedException.class);
+ underTest.verifyState(request, response);
+ }
+
+ @Test
+ public void fail_to_verify_state_when_state_cookie_is_null() throws Exception {
+ when(request.getCookies()).thenReturn(new Cookie[] {new Cookie("OAUTHSTATE", null)});
+ when(request.getParameter("state")).thenReturn("state");
+
+ thrown.expect(UnauthorizedException.class);
+ underTest.verifyState(request, response);
+ }
+
+ @Test
+ public void fail_with_unauthorized_when_state_parameter_is_empty() throws Exception {
+ when(request.getCookies()).thenReturn(new Cookie[] {new Cookie("OAUTHSTATE", sha1Hex("state"))});
+ when(request.getParameter("state")).thenReturn("");
+
+ thrown.expect(UnauthorizedException.class);
+ underTest.verifyState(request, response);
+ }
+
+ private void verifyCookie(Cookie cookie, boolean isSecured) {
+ assertThat(cookie.getName()).isEqualTo("OAUTHSTATE");
+ assertThat(cookie.getValue()).isNotEmpty();
+ assertThat(cookie.getPath()).isEqualTo("/");
+ assertThat(cookie.isHttpOnly()).isTrue();
+ assertThat(cookie.getMaxAge()).isEqualTo(-1);
+ assertThat(cookie.getSecure()).isEqualTo(isSecured);
+ }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/authentication/FakeBasicIdentityProvider.java b/server/sonar-server/src/test/java/org/sonar/server/authentication/FakeBasicIdentityProvider.java
new file mode 100644
index 00000000000..a0ab806fb10
--- /dev/null
+++ b/server/sonar-server/src/test/java/org/sonar/server/authentication/FakeBasicIdentityProvider.java
@@ -0,0 +1,42 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.BaseIdentityProvider;
+
+class FakeBasicIdentityProvider extends TestIdentityProvider implements BaseIdentityProvider {
+
+ private boolean initCalled = false;
+
+ public FakeBasicIdentityProvider(String key, boolean enabled) {
+ setKey(key);
+ setEnabled(enabled);
+ }
+
+ @Override
+ public void init(Context context) {
+ initCalled = true;
+ }
+
+ public boolean isInitCalled() {
+ return initCalled;
+ }
+
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/authentication/FakeOAuth2IdentityProvider.java b/server/sonar-server/src/test/java/org/sonar/server/authentication/FakeOAuth2IdentityProvider.java
new file mode 100644
index 00000000000..b174a2988f9
--- /dev/null
+++ b/server/sonar-server/src/test/java/org/sonar/server/authentication/FakeOAuth2IdentityProvider.java
@@ -0,0 +1,51 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.OAuth2IdentityProvider;
+
+class FakeOAuth2IdentityProvider extends TestIdentityProvider implements OAuth2IdentityProvider {
+
+ private boolean initCalled = false;
+ private boolean callbackCalled = false;
+
+ public FakeOAuth2IdentityProvider(String key, boolean enabled) {
+ setKey(key);
+ setEnabled(enabled);
+ }
+
+ @Override
+ public void init(InitContext context) {
+ initCalled = true;
+ }
+
+ @Override
+ public void callback(CallbackContext context) {
+ callbackCalled = true;
+ }
+
+ public boolean isInitCalled() {
+ return initCalled;
+ }
+
+ public boolean isCallbackCalled() {
+ return callbackCalled;
+ }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/authentication/IdentityProviderRepositoryRule.java b/server/sonar-server/src/test/java/org/sonar/server/authentication/IdentityProviderRepositoryRule.java
new file mode 100644
index 00000000000..40d1897f3f1
--- /dev/null
+++ b/server/sonar-server/src/test/java/org/sonar/server/authentication/IdentityProviderRepositoryRule.java
@@ -0,0 +1,48 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+import org.sonar.api.server.authentication.IdentityProvider;
+
+public class IdentityProviderRepositoryRule extends IdentityProviderRepository implements TestRule {
+
+ public IdentityProviderRepositoryRule addIdentityProvider(IdentityProvider identityProvider) {
+ providersByKey.put(identityProvider.getKey(), identityProvider);
+ return this;
+ }
+
+ @Override
+ public Statement apply(final Statement statement, Description description) {
+ return new Statement() {
+ @Override
+ public void evaluate() throws Throwable {
+ try {
+ statement.evaluate();
+ } finally {
+ providersByKey.clear();
+ }
+ }
+ };
+ }
+
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/authentication/IdentityProviderRepositoryTest.java b/server/sonar-server/src/test/java/org/sonar/server/authentication/IdentityProviderRepositoryTest.java
new file mode 100644
index 00000000000..a82d0306a8b
--- /dev/null
+++ b/server/sonar-server/src/test/java/org/sonar/server/authentication/IdentityProviderRepositoryTest.java
@@ -0,0 +1,90 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.List;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.server.authentication.IdentityProvider;
+
+import static java.util.Arrays.asList;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class IdentityProviderRepositoryTest {
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ static IdentityProvider GITHUB = new TestIdentityProvider()
+ .setKey("github")
+ .setName("Github")
+ .setEnabled(true);
+
+ static IdentityProvider BITBUCKET = new TestIdentityProvider()
+ .setKey("bitbucket")
+ .setName("Bitbucket")
+ .setEnabled(true);
+
+ static IdentityProvider DISABLED = new TestIdentityProvider()
+ .setKey("disabled")
+ .setName("Disabled")
+ .setEnabled(false);
+
+ @Test
+ public void return_enabled_provider() throws Exception {
+ IdentityProviderRepository underTest = new IdentityProviderRepository(asList(GITHUB, BITBUCKET, DISABLED));
+
+ assertThat(underTest.getEnabledByKey(GITHUB.getKey())).isEqualTo(GITHUB);
+ assertThat(underTest.getEnabledByKey(BITBUCKET.getKey())).isEqualTo(BITBUCKET);
+ }
+
+ @Test
+ public void fail_on_disabled_provider() throws Exception {
+ IdentityProviderRepository underTest = new IdentityProviderRepository(asList(GITHUB, BITBUCKET, DISABLED));
+
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("Identity provider disabled does not exist or is not enabled");
+ underTest.getEnabledByKey(DISABLED.getKey());
+ }
+
+ @Test
+ public void return_all_enabled_providers() throws Exception {
+ IdentityProviderRepository underTest = new IdentityProviderRepository(asList(GITHUB, BITBUCKET, DISABLED));
+
+ List providers = underTest.getAllEnabledAndSorted();
+ assertThat(providers).containsOnly(GITHUB, BITBUCKET);
+ }
+
+ @Test
+ public void return_sorted_enabled_providers() throws Exception {
+ IdentityProviderRepository underTest = new IdentityProviderRepository(asList(GITHUB, BITBUCKET));
+
+ List providers = underTest.getAllEnabledAndSorted();
+ assertThat(providers).containsExactly(BITBUCKET, GITHUB);
+ }
+
+ @Test
+ public void return_nothing_when_no_identity_provider() throws Exception {
+ IdentityProviderRepository underTest = new IdentityProviderRepository();
+
+ assertThat(underTest.getAllEnabledAndSorted()).isEmpty();
+ }
+}
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
new file mode 100644
index 00000000000..04e3c74edce
--- /dev/null
+++ b/server/sonar-server/src/test/java/org/sonar/server/authentication/InitFilterTest.java
@@ -0,0 +1,160 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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 javax.servlet.FilterChain;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.server.authentication.BaseIdentityProvider;
+import org.sonar.api.server.authentication.IdentityProvider;
+import org.sonar.api.server.authentication.OAuth2IdentityProvider;
+import org.sonar.api.utils.log.LogTester;
+import org.sonar.api.utils.log.LoggerLevel;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class InitFilterTest {
+
+ static String OAUTH2_PROVIDER_KEY = "github";
+ static String BASIC_PROVIDER_KEY = "openid";
+
+ @Rule
+ public LogTester logTester = new LogTester();
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ @Rule
+ public IdentityProviderRepositoryRule identityProviderRepository = new IdentityProviderRepositoryRule();
+
+ BaseContextFactory baseContextFactory = mock(BaseContextFactory.class);
+ OAuth2ContextFactory oAuth2ContextFactory = mock(OAuth2ContextFactory.class);
+
+ HttpServletRequest request = mock(HttpServletRequest.class);
+ HttpServletResponse response = mock(HttpServletResponse.class);
+ FilterChain chain = mock(FilterChain.class);
+
+ FakeOAuth2IdentityProvider oAuth2IdentityProvider = new FakeOAuth2IdentityProvider(OAUTH2_PROVIDER_KEY, true);
+ OAuth2IdentityProvider.InitContext oauth2Context = mock(OAuth2IdentityProvider.InitContext.class);
+
+ FakeBasicIdentityProvider baseIdentityProvider = new FakeBasicIdentityProvider(BASIC_PROVIDER_KEY, true);
+ BaseIdentityProvider.Context baseContext = mock(BaseIdentityProvider.Context.class);
+
+ InitFilter underTest = new InitFilter(identityProviderRepository, baseContextFactory, oAuth2ContextFactory);
+
+ @Before
+ public void setUp() throws Exception {
+ when(oAuth2ContextFactory.newContext(request, response, oAuth2IdentityProvider)).thenReturn(oauth2Context);
+ when(baseContextFactory.newContext(request, response, baseIdentityProvider)).thenReturn(baseContext);
+ }
+
+ @Test
+ public void do_get_pattern() throws Exception {
+ assertThat(underTest.doGetPattern()).isNotNull();
+ }
+
+ @Test
+ public void do_filter_on_auth2_identity_provider() throws Exception {
+ when(request.getRequestURI()).thenReturn("/sessions/init/" + OAUTH2_PROVIDER_KEY);
+ identityProviderRepository.addIdentityProvider(oAuth2IdentityProvider);
+
+ underTest.doFilter(request, response, chain);
+
+ assertOAuth2InitCalled();
+ }
+
+ @Test
+ public void do_filter_on_basic_identity_provider() throws Exception {
+ when(request.getRequestURI()).thenReturn("/sessions/init/" + BASIC_PROVIDER_KEY);
+ identityProviderRepository.addIdentityProvider(baseIdentityProvider);
+
+ underTest.doFilter(request, response, chain);
+
+ assertBasicInitCalled();
+ }
+
+ @Test
+ public void fail_if_identity_provider_key_is_empty() throws Exception {
+ when(request.getRequestURI()).thenReturn("/sessions/init/");
+
+ underTest.doFilter(request, response, chain);
+
+ assertError("Fail to initialize authentication with provider ''");
+ }
+
+ @Test
+ public void fail_if_identity_provider_class_is_unsuported() throws Exception {
+ final String unsupportedKey = "unsupported";
+ when(request.getRequestURI()).thenReturn("/sessions/init/" + unsupportedKey);
+ identityProviderRepository.addIdentityProvider(new IdentityProvider() {
+ @Override
+ public String getKey() {
+ return unsupportedKey;
+ }
+
+ @Override
+ public String getName() {
+ return null;
+ }
+
+ @Override
+ public String getIconPath() {
+ return null;
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return true;
+ }
+
+ @Override
+ public boolean allowsUsersToSignUp() {
+ return false;
+ }
+ });
+
+ underTest.doFilter(request, response, chain);
+
+ assertError("Fail to initialize authentication with provider 'unsupported'");
+ }
+
+ private void assertOAuth2InitCalled(){
+ assertThat(logTester.logs(LoggerLevel.ERROR)).isEmpty();
+ assertThat(oAuth2IdentityProvider.isInitCalled()).isTrue();
+ }
+
+ private void assertBasicInitCalled(){
+ assertThat(logTester.logs(LoggerLevel.ERROR)).isEmpty();
+ assertThat(baseIdentityProvider.isInitCalled()).isTrue();
+ }
+
+ private void assertError(String expectedError) throws Exception {
+ assertThat(logTester.logs(LoggerLevel.ERROR)).contains(expectedError);
+ verify(response).sendRedirect("/sessions/unauthorized");
+ assertThat(oAuth2IdentityProvider.isInitCalled()).isFalse();
+ }
+}
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
new file mode 100644
index 00000000000..e8e6c765916
--- /dev/null
+++ b/server/sonar-server/src/test/java/org/sonar/server/authentication/OAuth2CallbackFilterTest.java
@@ -0,0 +1,114 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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 javax.servlet.FilterChain;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.server.authentication.OAuth2IdentityProvider;
+import org.sonar.api.utils.log.LogTester;
+import org.sonar.api.utils.log.LoggerLevel;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class OAuth2CallbackFilterTest {
+
+ static String OAUTH2_PROVIDER_KEY = "github";
+
+ @Rule
+ public LogTester logTester = new LogTester();
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ @Rule
+ public IdentityProviderRepositoryRule identityProviderRepository = new IdentityProviderRepositoryRule();
+
+ OAuth2ContextFactory oAuth2ContextFactory = mock(OAuth2ContextFactory.class);
+
+ HttpServletRequest request = mock(HttpServletRequest.class);
+ HttpServletResponse response = mock(HttpServletResponse.class);
+ FilterChain chain = mock(FilterChain.class);
+
+ FakeOAuth2IdentityProvider oAuth2IdentityProvider = new FakeOAuth2IdentityProvider(OAUTH2_PROVIDER_KEY, true);
+ OAuth2IdentityProvider.InitContext oauth2Context = mock(OAuth2IdentityProvider.InitContext.class);
+
+ OAuth2CallbackFilter underTest = new OAuth2CallbackFilter(identityProviderRepository, oAuth2ContextFactory);
+
+ @Before
+ public void setUp() throws Exception {
+ when(oAuth2ContextFactory.newContext(request, response, oAuth2IdentityProvider)).thenReturn(oauth2Context);
+ }
+
+ @Test
+ public void do_get_pattern() throws Exception {
+ assertThat(underTest.doGetPattern()).isNotNull();
+ }
+
+ @Test
+ public void do_filter_on_auth2_identity_provider() throws Exception {
+ when(request.getRequestURI()).thenReturn("/oauth2/callback/" + OAUTH2_PROVIDER_KEY);
+ identityProviderRepository.addIdentityProvider(oAuth2IdentityProvider);
+
+ underTest.doFilter(request, response, chain);
+
+ assertCallbackCalled();
+ }
+
+ @Test
+ public void fail_on_not_oauth2_provider() throws Exception {
+ String providerKey = "openid";
+ when(request.getRequestURI()).thenReturn("/oauth2/callback/" + providerKey);
+ identityProviderRepository.addIdentityProvider(new FakeBasicIdentityProvider(providerKey, true));
+
+ underTest.doFilter(request, response, chain);
+
+ assertError("Not an OAuth2IdentityProvider: class org.sonar.server.authentication.FakeBasicIdentityProvider");
+ }
+
+ @Test
+ public void fail_on_disabled_provider() throws Exception {
+ when(request.getRequestURI()).thenReturn("/oauth2/callback/" + OAUTH2_PROVIDER_KEY);
+ identityProviderRepository.addIdentityProvider(new FakeOAuth2IdentityProvider(OAUTH2_PROVIDER_KEY, false));
+
+ underTest.doFilter(request, response, chain);
+
+ assertError("Fail to callback authentication with github");
+ }
+
+ private void assertCallbackCalled(){
+ assertThat(logTester.logs(LoggerLevel.ERROR)).isEmpty();
+ assertThat(oAuth2IdentityProvider.isCallbackCalled()).isTrue();
+ }
+
+ private void assertError(String expectedError) throws Exception {
+ assertThat(logTester.logs(LoggerLevel.ERROR)).contains(expectedError);
+ verify(response).sendRedirect("/sessions/unauthorized");
+ assertThat(oAuth2IdentityProvider.isInitCalled()).isFalse();
+ }
+
+}
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
new file mode 100644
index 00000000000..bb291b0667a
--- /dev/null
+++ b/server/sonar-server/src/test/java/org/sonar/server/authentication/OAuth2ContextFactoryTest.java
@@ -0,0 +1,148 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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 javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.platform.Server;
+import org.sonar.api.server.authentication.OAuth2IdentityProvider;
+import org.sonar.api.server.authentication.UserIdentity;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class OAuth2ContextFactoryTest {
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ static String PROVIDER_KEY = "github";
+
+ static String SECURED_PUBLIC_ROOT_URL = "https://mydomain.com";
+ static String NOT_SECURED_PUBLIC_URL = "http://mydomain.com";
+
+ static UserIdentity USER_IDENTITY = UserIdentity.builder()
+ .setId("johndoo")
+ .setName("John")
+ .setEmail("john@email.com")
+ .build();
+
+ UserIdentityAuthenticator userIdentityAuthenticator = mock(UserIdentityAuthenticator.class);
+ Server server = mock(Server.class);
+ CsrfVerifier csrfVerifier = mock(CsrfVerifier.class);
+
+ HttpServletRequest request = mock(HttpServletRequest.class);
+ HttpServletResponse response = mock(HttpServletResponse.class);
+ HttpSession session = mock(HttpSession.class);
+ OAuth2IdentityProvider identityProvider = mock(OAuth2IdentityProvider.class);
+
+ OAuth2ContextFactory underTest = new OAuth2ContextFactory(userIdentityAuthenticator, server, csrfVerifier);
+
+ @Before
+ public void setUp() throws Exception {
+ when(request.getSession()).thenReturn(session);
+ when(identityProvider.getKey()).thenReturn(PROVIDER_KEY);
+ }
+
+ @Test
+ public void create_context() throws Exception {
+ when(server.getPublicRootUrl()).thenReturn(SECURED_PUBLIC_ROOT_URL);
+
+ OAuth2IdentityProvider.InitContext context = underTest.newContext(request, response, identityProvider);
+
+ assertThat(context.getRequest()).isEqualTo(request);
+ assertThat(context.getResponse()).isEqualTo(response);
+ assertThat(context.getCallbackUrl()).isEqualTo("https://mydomain.com/oauth2/callback/github");
+ }
+
+ @Test
+ public void generate_csrf_state() throws Exception {
+ OAuth2IdentityProvider.InitContext context = underTest.newContext(request, response, identityProvider);
+
+ context.generateCsrfState();
+
+ verify(csrfVerifier).generateState(response);
+ }
+
+ @Test
+ public void redirect_to() throws Exception {
+ OAuth2IdentityProvider.InitContext context = underTest.newContext(request, response, identityProvider);
+
+ context.redirectTo("/test");
+
+ verify(response).sendRedirect("/test");
+ }
+
+ @Test
+ public void fail_to_get_callback_url_on_not_secured_server() throws Exception {
+ when(server.getPublicRootUrl()).thenReturn(NOT_SECURED_PUBLIC_URL);
+
+ OAuth2IdentityProvider.InitContext context = underTest.newContext(request, response, identityProvider);
+
+ thrown.expect(IllegalStateException.class);
+ thrown.expectMessage("The server url should be configured in https, please update the property 'sonar.core.serverBaseURL'");
+ context.getCallbackUrl();
+ }
+
+ @Test
+ public void create_callback() throws Exception {
+ when(server.getPublicRootUrl()).thenReturn(SECURED_PUBLIC_ROOT_URL);
+
+ OAuth2IdentityProvider.CallbackContext callback = underTest.newCallback(request, response, identityProvider);
+
+ assertThat(callback.getRequest()).isEqualTo(request);
+ assertThat(callback.getResponse()).isEqualTo(response);
+ assertThat(callback.getCallbackUrl()).isEqualTo("https://mydomain.com/oauth2/callback/github");
+ }
+
+ @Test
+ public void authenticate() throws Exception {
+ OAuth2IdentityProvider.CallbackContext callback = underTest.newCallback(request, response, identityProvider);
+
+ callback.authenticate(USER_IDENTITY);
+
+ verify(userIdentityAuthenticator).authenticate(USER_IDENTITY, identityProvider, session);
+ }
+
+ @Test
+ public void redirect_to_requested_page() throws Exception {
+ OAuth2IdentityProvider.CallbackContext callback = underTest.newCallback(request, response, identityProvider);
+
+ callback.redirectToRequestedPage();
+
+ verify(response).sendRedirect("/");
+ }
+
+ @Test
+ public void verify_csrf_state() throws Exception {
+ OAuth2IdentityProvider.CallbackContext callback = underTest.newCallback(request, response, identityProvider);
+
+ callback.verifyCsrfState();
+
+ verify(csrfVerifier).verifyState(request, response);
+ }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/authentication/TestIdentityProvider.java b/server/sonar-server/src/test/java/org/sonar/server/authentication/TestIdentityProvider.java
new file mode 100644
index 00000000000..60d4622f1fa
--- /dev/null
+++ b/server/sonar-server/src/test/java/org/sonar/server/authentication/TestIdentityProvider.java
@@ -0,0 +1,99 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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;
+
+public class TestIdentityProvider implements IdentityProvider {
+
+ private String key;
+ private String name;
+ private String iconPatch;
+ private boolean enabled;
+ private boolean allowsUsersToSignUp;
+
+ @Override
+ public String getKey() {
+ return key;
+ }
+
+ public TestIdentityProvider setKey(String key) {
+ this.key = key;
+ return this;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ public TestIdentityProvider setName(String name) {
+ this.name = name;
+ return this;
+ }
+
+ @Override
+ public String getIconPath() {
+ return iconPatch;
+ }
+
+ public TestIdentityProvider setIconPatch(String iconPatch) {
+ this.iconPatch = iconPatch;
+ return this;
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public TestIdentityProvider setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ return this;
+ }
+
+ @Override
+ public boolean allowsUsersToSignUp() {
+ return allowsUsersToSignUp;
+ }
+
+ public TestIdentityProvider setAllowsUsersToSignUp(boolean allowsUsersToSignUp) {
+ this.allowsUsersToSignUp = allowsUsersToSignUp;
+ return this;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (getClass() != o.getClass()) {
+ return false;
+ }
+
+ TestIdentityProvider that = (TestIdentityProvider) o;
+ return key.equals(that.key);
+ }
+
+ @Override
+ public int hashCode() {
+ return key.hashCode();
+ }
+}
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
new file mode 100644
index 00000000000..649ba0a35c5
--- /dev/null
+++ b/server/sonar-server/src/test/java/org/sonar/server/authentication/UserIdentityAuthenticatorTest.java
@@ -0,0 +1,149 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.base.Optional;
+import javax.servlet.http.HttpSession;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.mockito.ArgumentCaptor;
+import org.sonar.api.server.authentication.UserIdentity;
+import org.sonar.core.util.UuidFactory;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.user.UserDao;
+import org.sonar.db.user.UserDto;
+import org.sonar.server.user.NewUser;
+import org.sonar.server.user.UpdateUser;
+import org.sonar.server.user.UserUpdater;
+
+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.verify;
+import static org.mockito.Mockito.when;
+
+public class UserIdentityAuthenticatorTest {
+
+ static String USER_LOGIN = "ABCD";
+ static UserDto ACTIVE_USER = new UserDto().setId(10L).setLogin(USER_LOGIN).setActive(true);
+ static UserDto UNACTIVE_USER = new UserDto().setId(11L).setLogin("UNACTIVE").setActive(false);
+
+ static UserIdentity USER_IDENTITY = UserIdentity.builder()
+ .setId("johndoo")
+ .setName("John")
+ .setEmail("john@email.com")
+ .build();
+
+ static TestIdentityProvider IDENTITY_PROVIDER = new TestIdentityProvider()
+ .setKey("github")
+ .setEnabled(true)
+ .setAllowsUsersToSignUp(true);
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ DbClient dbClient = mock(DbClient.class);
+ DbSession dbSession = mock(DbSession.class);
+ UserDao userDao = mock(UserDao.class);
+
+ HttpSession httpSession = mock(HttpSession.class);
+ UserUpdater userUpdater = mock(UserUpdater.class);
+ UuidFactory uuidFactory = mock(UuidFactory.class);
+
+ UserIdentityAuthenticator underTest = new UserIdentityAuthenticator(dbClient, userUpdater, uuidFactory);
+
+ @Before
+ public void setUp() throws Exception {
+ when(dbClient.openSession(false)).thenReturn(dbSession);
+ when(dbClient.userDao()).thenReturn(userDao);
+ when(uuidFactory.create()).thenReturn(USER_LOGIN);
+ }
+
+ @Test
+ public void authenticate_new_user() throws Exception {
+ when(userDao.selectByExternalIdentity(dbSession, USER_IDENTITY.getId(), IDENTITY_PROVIDER.getKey())).thenReturn(Optional.absent());
+ when(userDao.selectOrFailByExternalIdentity(dbSession, USER_IDENTITY.getId(), IDENTITY_PROVIDER.getKey())).thenReturn(ACTIVE_USER);
+
+ underTest.authenticate(USER_IDENTITY, IDENTITY_PROVIDER, httpSession);
+
+ ArgumentCaptor newUserArgumentCaptor = ArgumentCaptor.forClass(NewUser.class);
+ verify(userUpdater).create(eq(dbSession), newUserArgumentCaptor.capture());
+ NewUser newUser = newUserArgumentCaptor.getValue();
+
+ assertThat(newUser.login()).isEqualTo(USER_LOGIN);
+ assertThat(newUser.name()).isEqualTo("John");
+ assertThat(newUser.email()).isEqualTo("john@email.com");
+ assertThat(newUser.externalIdentity().getProvider()).isEqualTo("github");
+ assertThat(newUser.externalIdentity().getId()).isEqualTo("johndoo");
+ }
+
+ @Test
+ public void authenticate_existing_user() throws Exception {
+ when(userDao.selectByExternalIdentity(dbSession, USER_IDENTITY.getId(), IDENTITY_PROVIDER.getKey())).thenReturn(Optional.of(ACTIVE_USER));
+
+ underTest.authenticate(USER_IDENTITY, IDENTITY_PROVIDER, httpSession);
+
+ ArgumentCaptor updateUserArgumentCaptor = ArgumentCaptor.forClass(UpdateUser.class);
+ verify(userUpdater).update(eq(dbSession), updateUserArgumentCaptor.capture());
+ UpdateUser newUser = updateUserArgumentCaptor.getValue();
+
+ assertThat(newUser.login()).isEqualTo(USER_LOGIN);
+ assertThat(newUser.name()).isEqualTo("John");
+ assertThat(newUser.email()).isEqualTo("john@email.com");
+ }
+
+ @Test
+ public void authenticate_existing_disabled_user() throws Exception {
+ when(userDao.selectByExternalIdentity(dbSession, USER_IDENTITY.getId(), IDENTITY_PROVIDER.getKey())).thenReturn(Optional.of(UNACTIVE_USER));
+ when(userDao.selectOrFailByExternalIdentity(dbSession, USER_IDENTITY.getId(), IDENTITY_PROVIDER.getKey())).thenReturn(UNACTIVE_USER);
+
+ underTest.authenticate(USER_IDENTITY, IDENTITY_PROVIDER, httpSession);
+
+ ArgumentCaptor newUserArgumentCaptor = ArgumentCaptor.forClass(NewUser.class);
+ verify(userUpdater).create(eq(dbSession), newUserArgumentCaptor.capture());
+ }
+
+ @Test
+ public void update_session_for_rails() throws Exception {
+ when(userDao.selectByExternalIdentity(dbSession, USER_IDENTITY.getId(), IDENTITY_PROVIDER.getKey())).thenReturn(Optional.of(ACTIVE_USER));
+
+ underTest.authenticate(USER_IDENTITY, IDENTITY_PROVIDER, httpSession);
+
+ verify(httpSession).setAttribute("user_id", ACTIVE_USER.getId());
+ }
+
+ @Test
+ public void fail_to_authenticate_new_user_when_allow_users_to_signup_is_false() throws Exception {
+ when(userDao.selectByExternalIdentity(dbSession, USER_IDENTITY.getId(), IDENTITY_PROVIDER.getKey())).thenReturn(Optional.absent());
+ when(userDao.selectOrFailByExternalIdentity(dbSession, USER_IDENTITY.getId(), IDENTITY_PROVIDER.getKey())).thenReturn(ACTIVE_USER);
+
+ TestIdentityProvider identityProvider = new TestIdentityProvider()
+ .setKey("github")
+ .setName("Github")
+ .setEnabled(true)
+ .setAllowsUsersToSignUp(false);
+
+ thrown.expect(NotAllowUserToSignUpException.class);
+ underTest.authenticate(USER_IDENTITY, identityProvider, httpSession);
+ }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/platform/ServerImplTest.java b/server/sonar-server/src/test/java/org/sonar/server/platform/ServerImplTest.java
index 0999df58358..6dd519dfe40 100644
--- a/server/sonar-server/src/test/java/org/sonar/server/platform/ServerImplTest.java
+++ b/server/sonar-server/src/test/java/org/sonar/server/platform/ServerImplTest.java
@@ -19,6 +19,7 @@
*/
package org.sonar.server.platform;
+import java.io.File;
import org.hamcrest.core.Is;
import org.junit.Before;
import org.junit.Rule;
@@ -29,8 +30,6 @@ import org.sonar.api.CoreProperties;
import org.sonar.api.config.Settings;
import org.sonar.process.ProcessProperties;
-import java.io.File;
-
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertThat;
@@ -139,4 +138,37 @@ public class ServerImplTest {
assertThat(server.getContextPath()).isEqualTo("/my_path");
}
+ @Test
+ public void is_dev() throws Exception {
+ settings.setProperty("sonar.web.dev", true);
+ server.start();
+ assertThat(server.isDev()).isTrue();
+ }
+
+ @Test
+ public void get_default_public_root_url() throws Exception {
+ server.start();
+ assertThat(server.getPublicRootUrl()).isEqualTo("http://localhost:9000");
+ }
+
+ @Test
+ public void get_public_root_url() throws Exception {
+ settings.setProperty("sonar.core.serverBaseURL", "http://mydomain.com");
+ server.start();
+ assertThat(server.getPublicRootUrl()).isEqualTo("http://mydomain.com");
+ }
+
+ @Test
+ public void is_secured_on_secured_server() throws Exception {
+ settings.setProperty("sonar.core.serverBaseURL", "https://mydomain.com");
+ server.start();
+ assertThat(server.isSecured()).isTrue();
+ }
+
+ @Test
+ public void is_secured_on_not_secured_server() throws Exception {
+ settings.setProperty("sonar.core.serverBaseURL", "http://mydomain.com");
+ server.start();
+ assertThat(server.isSecured()).isFalse();
+ }
}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/platform/ServerLifecycleNotifierTest.java b/server/sonar-server/src/test/java/org/sonar/server/platform/ServerLifecycleNotifierTest.java
index bb83fa8d1bd..d91368da8b4 100644
--- a/server/sonar-server/src/test/java/org/sonar/server/platform/ServerLifecycleNotifierTest.java
+++ b/server/sonar-server/src/test/java/org/sonar/server/platform/ServerLifecycleNotifierTest.java
@@ -19,17 +19,15 @@
*/
package org.sonar.server.platform;
+import java.io.File;
+import java.util.Date;
+import javax.annotation.CheckForNull;
import org.junit.Before;
import org.junit.Test;
import org.sonar.api.platform.Server;
import org.sonar.api.platform.ServerStartHandler;
import org.sonar.api.platform.ServerStopHandler;
-import javax.annotation.CheckForNull;
-
-import java.io.File;
-import java.util.Date;
-
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
@@ -120,6 +118,21 @@ class FakeServer extends Server {
return null;
}
+ @Override
+ public String getPublicRootUrl() {
+ return null;
+ }
+
+ @Override
+ public boolean isDev() {
+ return false;
+ }
+
+ @Override
+ public boolean isSecured() {
+ return false;
+ }
+
@Override
public String getURL() {
return null;
diff --git a/server/sonar-server/src/test/java/org/sonar/server/platform/ws/StatusActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/platform/ws/StatusActionTest.java
index 27edee62edd..74ef58d2ad4 100644
--- a/server/sonar-server/src/test/java/org/sonar/server/platform/ws/StatusActionTest.java
+++ b/server/sonar-server/src/test/java/org/sonar/server/platform/ws/StatusActionTest.java
@@ -230,6 +230,21 @@ public class StatusActionTest {
throw new UnsupportedOperationException();
}
+ @Override
+ public String getPublicRootUrl() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean isDev() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean isSecured() {
+ throw new UnsupportedOperationException();
+ }
+
@Override
public String getURL() {
throw new UnsupportedOperationException();
diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/sessions_controller.rb b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/sessions_controller.rb
index 5385d577fb1..535c53b7a4d 100644
--- a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/sessions_controller.rb
+++ b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/sessions_controller.rb
@@ -70,4 +70,11 @@ class SessionsController < ApplicationController
end
end
+ def unauthorized
+ flash[:error] = session['error']
+ session['error'] = nil
+ params[:layout]='false'
+ render :action => 'unauthorized'
+ end
+
end
diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/sessions/_form.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/sessions/_form.html.erb
index 2d40f8d5a1e..21231c69b9b 100644
--- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/sessions/_form.html.erb
+++ b/server/sonar-web/src/main/webapp/WEB-INF/app/views/sessions/_form.html.erb
@@ -33,10 +33,28 @@