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
// 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
<scope>test</scope>
</dependency>
-
<!-- tomcat -->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<groupId>${project.groupId}</groupId>
<artifactId>sonar-dev-cockpit-bridge</artifactId>
</dependency>
+
<!-- unit tests -->
<dependency>
<groupId>${project.groupId}</groupId>
--- /dev/null
+/*
+ * 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);
+ }
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+/*
+ * 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());
+ }
+ }
+}
--- /dev/null
+/*
+ * 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();
+ }
+ }
+}
--- /dev/null
+/*
+ * 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();
+ }
+ }
+}
--- /dev/null
+/*
+ * 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
+ }
+}
--- /dev/null
+/*
+ * 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;
+ }
+}
--- /dev/null
+/*
+ * 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
+ }
+}
--- /dev/null
+/*
+ * 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());
+ }
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+ }
+}
--- /dev/null
+/*
+ * 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;
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;
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);
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) {
public String getURL() {
return null;
}
+
+ private String get(String key, String defaultValue) {
+ return Objects.firstNonNull(settings.getString(key), defaultValue);
+ }
+
}
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;
L10nWs.class,
// authentication
- AuthenticationWs.class,
+ AuthenticationModule.class,
// users
SecurityRealmFactory.class,
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;
get(ResourceIndexDao.class).indexResource(resourceId);
}
+ public List<IdentityProvider> getIdentityProviders(){
+ return get(IdentityProviderRepository.class).getAllEnabledAndSorted();
+ }
+
}
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 {
}
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"));
--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+/*
+ * 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;
+ }
+
+}
--- /dev/null
+/*
+ * 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;
+ }
+}
--- /dev/null
+/*
+ * 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();
+ }
+ }
+ };
+ }
+
+}
--- /dev/null
+/*
+ * 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();
+ }
+}
--- /dev/null
+/*
+ * 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();
+ }
+}
--- /dev/null
+/*
+ * 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();
+ }
+
+}
--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+/*
+ * 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();
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+}
*/
package org.sonar.server.platform;
+import java.io.File;
import org.hamcrest.core.Is;
import org.junit.Before;
import org.junit.Rule;
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;
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();
+ }
}
*/
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;
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;
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();
end
end
+ def unauthorized
+ flash[:error] = session['error']
+ session['error'] = nil
+ params[:layout]='false'
+ render :action => 'unauthorized'
+ end
+
end
<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>
--- /dev/null
+<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>
--- /dev/null
+<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>
# 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
# 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.
@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
*/
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 {
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"), "/");
*/
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
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
--- /dev/null
+/*
+ * 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);
+
+ }
+}
--- /dev/null
+/*
+ * 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();
+}
--- /dev/null
+/*
+ * 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);
+ }
+
+}
--- /dev/null
+/*
+ * 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)");
+ }
+ }
+}
--- /dev/null
+/*
+ * 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;
--- /dev/null
+/*
+ * 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();
+ }
+}