]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-6226 Create IdentityProvider API
authorJulien Lancelot <julien.lancelot@sonarsource.com>
Tue, 19 Jan 2016 10:44:16 +0000 (11:44 +0100)
committerJulien Lancelot <julien.lancelot@sonarsource.com>
Mon, 25 Jan 2016 14:26:49 +0000 (15:26 +0100)
45 files changed:
it/it-tests/src/test/java/it/authorisation/AuthenticationTest.java
pom.xml
server/sonar-server/pom.xml
server/sonar-server/src/main/java/org/sonar/server/authentication/AuthenticationError.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/authentication/AuthenticationModule.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/authentication/BaseContextFactory.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/authentication/CsrfVerifier.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/authentication/IdentityProviderRepository.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/authentication/InitFilter.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/authentication/NotAllowUserToSignUpException.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/authentication/OAuth2CallbackFilter.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/authentication/OAuth2ContextFactory.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/authentication/UserIdentityAuthenticator.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/authentication/package-info.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/platform/ServerImpl.java
server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
server/sonar-server/src/main/java/org/sonar/server/ui/JRubyFacade.java
server/sonar-server/src/main/java/org/sonar/server/user/UserUpdater.java
server/sonar-server/src/test/java/org/sonar/server/authentication/BaseContextFactoryTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/authentication/CsrfVerifierTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/authentication/FakeBasicIdentityProvider.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/authentication/FakeOAuth2IdentityProvider.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/authentication/IdentityProviderRepositoryRule.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/authentication/IdentityProviderRepositoryTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/authentication/InitFilterTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/authentication/OAuth2CallbackFilterTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/authentication/OAuth2ContextFactoryTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/authentication/TestIdentityProvider.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/authentication/UserIdentityAuthenticatorTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/platform/ServerImplTest.java
server/sonar-server/src/test/java/org/sonar/server/platform/ServerLifecycleNotifierTest.java
server/sonar-server/src/test/java/org/sonar/server/platform/ws/StatusActionTest.java
server/sonar-web/src/main/webapp/WEB-INF/app/controllers/sessions_controller.rb
server/sonar-web/src/main/webapp/WEB-INF/app/views/sessions/_form.html.erb
server/sonar-web/src/main/webapp/WEB-INF/app/views/sessions/not_allowed_to_sign_up.html.erb [new file with mode: 0644]
server/sonar-web/src/main/webapp/WEB-INF/app/views/sessions/unauthorized.html.erb [new file with mode: 0644]
server/sonar-web/src/main/webapp/WEB-INF/lib/authenticated_system.rb
sonar-batch/src/main/java/org/sonar/batch/platform/DefaultServer.java
sonar-plugin-api/src/main/java/org/sonar/api/platform/Server.java
sonar-plugin-api/src/main/java/org/sonar/api/server/authentication/BaseIdentityProvider.java [new file with mode: 0644]
sonar-plugin-api/src/main/java/org/sonar/api/server/authentication/IdentityProvider.java [new file with mode: 0644]
sonar-plugin-api/src/main/java/org/sonar/api/server/authentication/OAuth2IdentityProvider.java [new file with mode: 0644]
sonar-plugin-api/src/main/java/org/sonar/api/server/authentication/UserIdentity.java [new file with mode: 0644]
sonar-plugin-api/src/main/java/org/sonar/api/server/authentication/package-info.java [new file with mode: 0644]
sonar-plugin-api/src/test/java/org/sonar/api/server/authentication/UserIdentityTest.java [new file with mode: 0644]

index 818f5cf37fdefbf033e6620699cb1fd6b59c8548..1ed047c08bbae832742e920bedc7bffe31e03dea 100644 (file)
@@ -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 8567ca2c751ff58822e4b09d2628047e77f766e9..b438f4804070e219626ad20b0d7ce2af9de52053 100644 (file)
--- a/pom.xml
+++ b/pom.xml
         <scope>test</scope>
       </dependency>
 
-
       <!-- tomcat -->
       <dependency>
         <groupId>org.apache.tomcat.embed</groupId>
index e03b5d898055a7d6f38d628925a3a9e2edf8dadd..aaaae163989bcdae9ef36fc658b39afa6b8e1cab 100644 (file)
       <groupId>${project.groupId}</groupId>
       <artifactId>sonar-dev-cockpit-bridge</artifactId>
     </dependency>
+
     <!-- unit tests -->
     <dependency>
       <groupId>${project.groupId}</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 (file)
index 0000000..35e4bcc
--- /dev/null
@@ -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 (file)
index 0000000..2ecebed
--- /dev/null
@@ -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 (file)
index 0000000..465f093
--- /dev/null
@@ -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 (file)
index 0000000..9a03715
--- /dev/null
@@ -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 (file)
index 0000000..b20acc1
--- /dev/null
@@ -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<String, IdentityProvider> providersByKey = new HashMap<>();
+
+  public IdentityProviderRepository(List<IdentityProvider> 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<IdentityProvider> getAllEnabledAndSorted() {
+    return from(providersByKey.values())
+      .filter(IsEnabledFilter.INSTANCE)
+      .toSortedList(
+        Ordering.natural().onResultOf(ToName.INSTANCE)
+      );
+  }
+
+  private enum IsEnabledFilter implements Predicate<IdentityProvider> {
+    INSTANCE;
+
+    @Override
+    public boolean apply(@Nonnull IdentityProvider input) {
+      return input.isEnabled();
+    }
+  }
+
+  private enum ToKey implements Function<IdentityProvider, String> {
+    INSTANCE;
+
+    @Override
+    public String apply(@Nonnull IdentityProvider input) {
+      return input.getKey();
+    }
+  }
+
+  private enum ToName implements Function<IdentityProvider, String> {
+    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 (file)
index 0000000..65b0eee
--- /dev/null
@@ -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 (file)
index 0000000..0226996
--- /dev/null
@@ -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 (file)
index 0000000..7240a93
--- /dev/null
@@ -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 (file)
index 0000000..ce1d920
--- /dev/null
@@ -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 (file)
index 0000000..803b3fc
--- /dev/null
@@ -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> 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 (file)
index 0000000..e62e32d
--- /dev/null
@@ -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;
index 04ff6969808c7e87937d7599b793d06f92ffd1f6..7ab16e8d70d91a97f591aa027c12a24cae99d5fd 100644 (file)
@@ -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);
+  }
+
 }
index 1e1d569e77ff74b1567404400efbd60fdcc56e8c..89b2cfd82e00f2ac5cd1fef845435e61e36014c7 100644 (file)
@@ -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,
index 402900b408393a89420467f79c1f451abd583a02..1ac5edd05ecd787b39c676427cec59bc4494cb7e 100644 (file)
@@ -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<IdentityProvider> getIdentityProviders(){
+    return get(IdentityProviderRepository.class).getAllEnabledAndSorted();
+  }
+
 }
index 2003643daee2afe1fb034e29f9b403d0f2a565a7..9fd0e44b2383f48c3f4644590850a3b81c0ce90d 100644 (file)
@@ -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<String> scmAccounts, @Nullable String login, @Nullable String email, @Nullable UserDto existingUser,
-                                   List<Message> messages) {
+    List<Message> 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 (file)
index 0000000..08ca98f
--- /dev/null
@@ -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 (file)
index 0000000..918e0e9
--- /dev/null
@@ -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<Cookie> 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 (file)
index 0000000..a0ab806
--- /dev/null
@@ -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 (file)
index 0000000..b174a29
--- /dev/null
@@ -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 (file)
index 0000000..40d1897
--- /dev/null
@@ -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 (file)
index 0000000..a82d030
--- /dev/null
@@ -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<IdentityProvider> 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<IdentityProvider> 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 (file)
index 0000000..04e3c74
--- /dev/null
@@ -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 (file)
index 0000000..e8e6c76
--- /dev/null
@@ -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 (file)
index 0000000..bb291b0
--- /dev/null
@@ -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 (file)
index 0000000..60d4622
--- /dev/null
@@ -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 (file)
index 0000000..649ba0a
--- /dev/null
@@ -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.<UserDto>absent());
+    when(userDao.selectOrFailByExternalIdentity(dbSession, USER_IDENTITY.getId(), IDENTITY_PROVIDER.getKey())).thenReturn(ACTIVE_USER);
+
+    underTest.authenticate(USER_IDENTITY, IDENTITY_PROVIDER, httpSession);
+
+    ArgumentCaptor<NewUser> 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<UpdateUser> 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<NewUser> 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.<UserDto>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);
+  }
+}
index 0999df58358f313ddde670bb1f10351eb29a73aa..6dd519dfe403df9813df8a7b8ca3a93725fd4ff6 100644 (file)
@@ -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();
+  }
 }
index bb83fa8d1bd66cfa96ac13f3701a3596d1e2c63b..d91368da8b4ab975f1d5478bc2abb67782307d61 100644 (file)
  */
 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;
index 27edee62edd98ae0b22b7a024914094266b902af..74ef58d2ad4d1f3c9f3e05d6b5273cfb1b7e4fb8 100644 (file)
@@ -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();
index 5385d577fb1e20924ba7d4ce116bcb370d6f0536..535c53b7a4d6ea525af038f840fd66017d742f5c 100644 (file)
@@ -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
index 2d40f8d5a1e18e315e8a4c95d0072b8a5fc706c5..21231c69b9b21287a802d3c67be0705b70a52cd0 100644 (file)
     <label for="remember_me"><%= message('sessions.remember_me') -%></label>
   </p>
 
-  <p class="text-right">
-    <button name="commit"><%= message('sessions.log_in') -%></button>
-    <a class="spacer-left" href="<%= home_path -%>"><%= message('cancel') -%></a>
-  </p>
+  <div>
+    <div class="pull-left">
+      <% auth_providers = Api::Utils.java_facade.getIdentityProviders().to_a %>
+      <ul class="list-inline">
+        <% auth_providers.each do |provider| %>
+          <li>
+            <a class="oauth-link"
+               href="<%= ApplicationController.root_context -%>/sessions/init/<%= provider.getKey().to_s %>"
+               title="Login with <%= provider.getName().to_s -%>">
+              <img alt="<%= provider.getName().to_s -%>" width="24" height="24"
+                   src="<%= ApplicationController.root_context + provider.getIconPath().to_s -%>">
+            </a>
+          </li>
+        <% end %>
+      </ul>
+    </div>
+    <div class="text-right overflow-hidden">
+      <button name="commit"><%= message('sessions.log_in') -%></button>
+      <a class="spacer-left" href="<%= home_path -%>"><%= message('cancel') -%></a>
+    </div>
+  </div>
+
 </form>
 
 <script>
diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/sessions/not_allowed_to_sign_up.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/sessions/not_allowed_to_sign_up.html.erb
new file mode 100644 (file)
index 0000000..dd8ddb1
--- /dev/null
@@ -0,0 +1,10 @@
+<table class="spaced">
+  <tr>
+    <td align="center">
+
+      <div id="login_form">
+        <p id="unauthorized"><%= params[:providerName] %> users are not allowed to signup</p>
+      </div>
+    </td>
+  </tr>
+</table>
diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/sessions/unauthorized.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/sessions/unauthorized.html.erb
new file mode 100644 (file)
index 0000000..d552618
--- /dev/null
@@ -0,0 +1,14 @@
+<table class="spaced">
+  <tr>
+    <td align="center">
+
+      <% if flash[:error] %>
+        <div class="error"><%= flash[:error] %></div>
+      <% end %>
+
+      <div id="login_form">
+        <p id="unauthorized">You're not authorized to access this page. Please contact the administrator.</p>
+      </div>
+    </td>
+  </tr>
+</table>
index 8f28272d4a0b2caea0263cf6d566ef468942c535..b97eff8c155fbbd8da22c5fd40b33d70ff8eb3e6 100644 (file)
@@ -14,10 +14,10 @@ module AuthenticatedSystem
   # Store the given user id in the session.
   def current_user=(new_user)
     if new_user
-      session[:user_id] = new_user.id
+      session['user_id'] = new_user.id
       @current_user = new_user
     else
-      session[:user_id] = nil
+      session['user_id'] = nil
       @current_user = false
     end
   end
@@ -121,7 +121,7 @@ module AuthenticatedSystem
 
   # Called from #current_user.  First attempt to login by the user id stored in the session.
   def login_from_session
-    self.current_user = User.find_by_id(session[:user_id]) if session[:user_id]
+    self.current_user = User.find_by_id(session['user_id']) if session['user_id']
   end
 
   # Called from #current_user.  Now, attempt to login by basic authentication information.
@@ -171,7 +171,7 @@ module AuthenticatedSystem
     @current_user.forget_me if @current_user.is_a? User
     @current_user = false     # not logged in, and don't do it for me
     kill_remember_cookie!     # Kill client-side auth cookie
-    session[:user_id] = nil   # keeps the session but kill our variable
+    session['user_id'] = nil   # keeps the session but kill our variable
     # explicitly kill any other session variables you set
   end
 
index 510f281ac0e8cc5f872349b133220850c250353e..b747438a9f578e278aafa952cdd1bc8652e7151c 100644 (file)
  */
 package org.sonar.batch.platform;
 
+import java.io.File;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import javax.annotation.CheckForNull;
 import org.apache.commons.lang.StringUtils;
-
-import org.sonar.batch.bootstrap.GlobalProperties;
 import org.slf4j.LoggerFactory;
-import org.sonar.api.batch.BatchSide;
 import org.sonar.api.CoreProperties;
+import org.sonar.api.batch.BatchSide;
 import org.sonar.api.config.Settings;
 import org.sonar.api.platform.Server;
-
-import javax.annotation.CheckForNull;
-
-import java.io.File;
-import java.text.ParseException;
-import java.text.SimpleDateFormat;
-import java.util.Date;
+import org.sonar.batch.bootstrap.GlobalProperties;
 
 @BatchSide
 public class DefaultServer extends Server {
@@ -86,6 +83,21 @@ public class DefaultServer 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 StringUtils.removeEnd(StringUtils.defaultIfBlank(props.property("sonar.host.url"), "http://localhost:9000"), "/");
index 286dab7d644a63542308c2470f8cdd9958fbf392..4fa6a3a5d1adc21565319b0ed86942b2dfe6ebee 100644 (file)
  */
 package org.sonar.api.platform;
 
-import org.sonar.api.batch.BatchSide;
-import org.sonar.api.server.ServerSide;
-
-import javax.annotation.CheckForNull;
-
 import java.io.File;
 import java.util.Date;
+import javax.annotation.CheckForNull;
+import org.sonar.api.batch.BatchSide;
+import org.sonar.api.server.ServerSide;
 
 /**
  * @since 2.2
@@ -47,6 +45,28 @@ public abstract class Server {
 
   public abstract String getContextPath();
 
+  /**
+   * Return the public root url, for instance : https://nemo.sonarqube.org.
+   * Default value is {@link org.sonar.api.CoreProperties#SERVER_BASE_URL_DEFAULT_VALUE}
+   *
+   * @since 5.4
+   */
+  public abstract String getPublicRootUrl();
+
+  /**
+   * The dev mode is enabled when the property sonar.web.dev is true.
+   *
+   * @since 5.4
+   */
+  public abstract boolean isDev();
+
+  /**
+   * Return whether or not the {#getPublicRootUrl} is started with https.
+   *
+   * @since 5.4
+   */
+  public abstract boolean isSecured();
+
   /**
    * @return the server URL when executed from batch, else null.
    * @since 2.4
diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/server/authentication/BaseIdentityProvider.java b/sonar-plugin-api/src/main/java/org/sonar/api/server/authentication/BaseIdentityProvider.java
new file mode 100644 (file)
index 0000000..6f17ee5
--- /dev/null
@@ -0,0 +1,67 @@
+/*
+ * 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.api.server.authentication;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * @since 5.4
+ */
+public interface BaseIdentityProvider extends IdentityProvider {
+
+  /**
+   * Entry-point of authentication workflow. Executed by core when user
+   * clicks on the related button in login form (GET /sessions/init/{provider key}).
+   */
+  void init(Context context);
+
+  interface Context {
+
+    /**
+     * Get the received HTTP request.
+     * Note - {@code getRequest().getSession()} must not be used in order to support
+     * future clustering of web servers without stateful server sessions.
+     */
+    HttpServletRequest getRequest();
+
+    /**
+     * Get the HTTP response to send
+     */
+    HttpServletResponse getResponse();
+
+    /**
+     * Return the server base URL
+     * @see org.sonar.api.platform.Server#getPublicRootUrl()
+     */
+    String getServerBaseURL();
+
+    /**
+     * Authenticate and register the user into the platform.
+     *
+     * The first time a user is authenticated (and if {@link #allowsUsersToSignUp()} is true), a new user will be registered.
+     * Then, only user's name and email are updated.
+     *
+     * @throws NotAllowUserToSignUpException when {@link #allowsUsersToSignUp()} is false and a new user try to authenticate
+     */
+    void authenticate(UserIdentity userIdentity);
+
+  }
+}
diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/server/authentication/IdentityProvider.java b/sonar-plugin-api/src/main/java/org/sonar/api/server/authentication/IdentityProvider.java
new file mode 100644 (file)
index 0000000..9944d2c
--- /dev/null
@@ -0,0 +1,70 @@
+/*
+ * 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.api.server.authentication;
+
+import org.sonar.api.server.ServerSide;
+
+/**
+ * Entry-point to define a new Identity provider.
+ * Only one of this two interfaces can be used :
+ * <ul>
+ *   <li>{@link OAuth2IdentityProvider}</li> for OAuth2 authentication
+ *   <li>{@link BaseIdentityProvider}</li> for other kind of authentication
+ * </ul>
+ *
+ * @since 5.4
+ */
+@ServerSide
+public interface IdentityProvider {
+
+  /**
+   * Unique key of provider, for example "github".
+   * Must not be blank.
+   */
+  String getKey();
+
+  /**
+   * Name displayed in login form.
+   * Must not be blank.
+   */
+  String getName();
+
+  /**
+   * URL path to the provider icon, as deployed at runtime, for example "/static/authgithub/github.svg" (in this
+   * case "authgithub" is the plugin key. Source file is "src/main/resources/static/github.svg"). Must not be blank.
+   * <p/>
+   * The recommended format is SVG with a size of 24x24 pixels.
+   * Other supported format is PNG, with a size of 48x48 pixels.
+   */
+  String getIconPath();
+
+  /**
+   * Is the provider fully configured and enabled ? If {@code true}, then
+   * the provider is available in login form.
+   */
+  boolean isEnabled();
+
+  /**
+   * Can users sign-up (connecting with their account for the first time) ? If {@code true},
+   * then users can register and create their account into SonarQube, else only already
+   * registered users can login.
+   */
+  boolean allowsUsersToSignUp();
+}
diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/server/authentication/OAuth2IdentityProvider.java b/sonar-plugin-api/src/main/java/org/sonar/api/server/authentication/OAuth2IdentityProvider.java
new file mode 100644 (file)
index 0000000..4409803
--- /dev/null
@@ -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.api.server.authentication;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * @since 5.4
+ */
+public interface OAuth2IdentityProvider extends IdentityProvider {
+
+  /**
+   * Entry-point of authentication workflow. Executed by core when user
+   * clicks on the related button in login form (GET /sessions/init/{provider key}).
+   */
+  void init(InitContext context);
+
+  /**
+   * This method is called when the identity provider has authenticated a user.
+   */
+  void callback(CallbackContext context);
+
+  interface OAuth2Context {
+
+    /**
+     * The callback URL that must be used by the identity provider
+     */
+    String getCallbackUrl();
+
+    /**
+     * Get the received HTTP request.
+     * Note - {@code getRequest().getSession()} must not be used in order to support
+     * future clustering of web servers without stateful server sessions.
+     */
+    HttpServletRequest getRequest();
+
+    /**
+     * Get the HTTP response to send
+     */
+    HttpServletResponse getResponse();
+  }
+
+  interface InitContext extends OAuth2Context {
+
+    /**
+     * Generate a non-guessable state to prevent Cross Site Request Forgery.
+     */
+    String generateCsrfState();
+
+    /**
+     * Redirect the request to the url.
+     * Can be used to redirect to the identity provider authentication url.
+     */
+    void redirectTo(String url);
+  }
+
+  interface CallbackContext extends OAuth2Context {
+
+    /**
+     * Check that the state is valid.
+     * It should only be called If {@link InitContext#generateCsrfState()} was used in the init
+     */
+    void verifyCsrfState();
+
+    /**
+     * Redirect the request to the requested page.
+     * Must be called at the end of {@link OAuth2IdentityProvider#callback(CallbackContext)}
+     */
+    void redirectToRequestedPage();
+
+    /**
+     * Authenticate and register the user into the platform
+     */
+    void authenticate(UserIdentity userIdentity);
+  }
+
+}
diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/server/authentication/UserIdentity.java b/sonar-plugin-api/src/main/java/org/sonar/api/server/authentication/UserIdentity.java
new file mode 100644 (file)
index 0000000..ae1564a
--- /dev/null
@@ -0,0 +1,129 @@
+/*
+ * 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.api.server.authentication;
+
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static org.apache.commons.lang.StringUtils.isNotBlank;
+
+/**
+ * User information provided by the Identity Provider to be register into the platform.
+ *
+ * @since 5.4
+ */
+@Immutable
+public final class UserIdentity {
+
+  private final String id;
+  private final String name;
+  private final String email;
+
+  private UserIdentity(Builder builder) {
+    this.id = builder.id;
+    this.name = builder.name;
+    this.email = builder.email;
+  }
+
+  /**
+   * Non-blank user ID, unique for the related {@link IdentityProvider}. If two {@link IdentityProvider}
+   * define two users with the same ID, then users are considered as different.
+   */
+  public String getId() {
+    return id;
+  }
+
+  /**
+   * Non-blank display name. Uniqueness is not mandatory, even it's recommended for easier search of users
+   * in webapp.
+   */
+  public String getName() {
+    return name;
+  }
+
+  /**
+   * Optional non-blank email. If defined, then it must be unique among all the users defined by all
+   * {@link IdentityProvider}. If not unique, then authentication will fail.
+   */
+  @CheckForNull
+  public String getEmail() {
+    return email;
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static class Builder {
+    private String id;
+    private String name;
+    private String email;
+
+    private Builder() {
+    }
+
+    /**
+     * @see UserIdentity#getId()
+     */
+    public Builder setId(String id) {
+      this.id = id;
+      return this;
+    }
+
+    /**
+     * @see UserIdentity#getName()
+     */
+    public Builder setName(String name) {
+      this.name = name;
+      return this;
+    }
+
+    /**
+     * @see UserIdentity#getEmail()
+     */
+    public Builder setEmail(@Nullable String email) {
+      this.email = email;
+      return this;
+    }
+
+    public UserIdentity build() {
+      validateId(id);
+      validateName(name);
+      validateEmail(email);
+      return new UserIdentity(this);
+    }
+
+    private static void validateId(String id){
+      checkArgument(isNotBlank(id), "User id must not be blank");
+      checkArgument(id.length() <= 255 && id.length() >= 3, "User id size is incorrect (Between 3 and 255 characters)");
+    }
+
+    private static void validateName(String name){
+      checkArgument(isNotBlank(name), "User name must not be blank");
+      checkArgument(name.length() <= 200, "User name size is too big (200 characters max)");
+    }
+
+    private static void validateEmail(@Nullable String email){
+      checkArgument(email == null || email.length() <= 100, "User email size is too big (100 characters max)");
+    }
+  }
+}
diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/server/authentication/package-info.java b/sonar-plugin-api/src/main/java/org/sonar/api/server/authentication/package-info.java
new file mode 100644 (file)
index 0000000..d5dd183
--- /dev/null
@@ -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.api.server.authentication;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/sonar-plugin-api/src/test/java/org/sonar/api/server/authentication/UserIdentityTest.java b/sonar-plugin-api/src/test/java/org/sonar/api/server/authentication/UserIdentityTest.java
new file mode 100644 (file)
index 0000000..c74e1da
--- /dev/null
@@ -0,0 +1,142 @@
+/*
+ * 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.api.server.authentication;
+
+import com.google.common.base.Strings;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class UserIdentityTest {
+
+  @Rule
+  public ExpectedException thrown= ExpectedException.none();
+
+  @Test
+  public void create_user() throws Exception {
+    UserIdentity underTest = UserIdentity.builder()
+      .setId("john")
+      .setName("John")
+      .setEmail("john@email.com")
+      .build();
+
+    assertThat(underTest.getId()).isEqualTo("john");
+    assertThat(underTest.getName()).isEqualTo("John");
+    assertThat(underTest.getEmail()).isEqualTo("john@email.com");
+  }
+
+  @Test
+  public void create_user_without_email() throws Exception {
+    UserIdentity underTest = UserIdentity.builder()
+      .setId("john")
+      .setName("John")
+      .build();
+
+    assertThat(underTest.getEmail()).isNull();
+  }
+
+  @Test
+  public void fail_when_id_is_null() throws Exception {
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("User id must not be blank");
+    UserIdentity.builder()
+      .setName("John")
+      .setEmail("john@email.com")
+      .build();
+  }
+
+  @Test
+  public void fail_when_id_is_empty() throws Exception {
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("User id must not be blank");
+    UserIdentity.builder()
+      .setId("")
+      .setName("John")
+      .setEmail("john@email.com")
+      .build();
+  }
+
+  @Test
+  public void fail_when_id_is_loo_long() throws Exception {
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("User id size is incorrect (Between 3 and 255 characters)");
+    UserIdentity.builder()
+      .setId(Strings.repeat("1", 256))
+      .setName("John")
+      .setEmail("john@email.com")
+      .build();
+  }
+
+  @Test
+  public void fail_when_id_is_loo_small() throws Exception {
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("User id size is incorrect (Between 3 and 255 characters)");
+    UserIdentity.builder()
+      .setId("ab")
+      .setName("John")
+      .setEmail("john@email.com")
+      .build();
+  }
+
+  @Test
+  public void fail_when_name_is_null() throws Exception {
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("User name must not be blank");
+    UserIdentity.builder()
+      .setId("john")
+      .setEmail("john@email.com")
+      .build();
+  }
+
+  @Test
+  public void fail_when_name_is_empty() throws Exception {
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("User name must not be blank");
+    UserIdentity.builder()
+      .setId("john")
+      .setName("")
+      .setEmail("john@email.com")
+      .build();
+  }
+
+  @Test
+  public void fail_when_name_is_loo_long() throws Exception {
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("User name size is too big (200 characters max)");
+    UserIdentity.builder()
+      .setId("john")
+      .setName(Strings.repeat("1", 201))
+      .setEmail("john@email.com")
+      .build();
+  }
+
+  @Test
+  public void fail_when_email_is_loo_long() throws Exception {
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("User email size is too big (100 characters max)");
+    UserIdentity.builder()
+      .setId("john")
+      .setName("John")
+      .setEmail(Strings.repeat("1", 101))
+      .build();
+  }
+}