From: Julien Lancelot Date: Tue, 21 Jun 2016 12:03:15 +0000 (+0200) Subject: SONAR-7763 Allow authentication using basic HTTP authentication in Java X-Git-Tag: 6.0-RC1~237 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=d82358c63d0fb979fb3cc27429a42ec833dc161a;p=sonarqube.git SONAR-7763 Allow authentication using basic HTTP authentication in Java --- diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/AuthLoginAction.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/AuthLoginAction.java deleted file mode 100644 index 770cd6329ec..00000000000 --- a/server/sonar-server/src/main/java/org/sonar/server/authentication/AuthLoginAction.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * 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 static java.net.HttpURLConnection.HTTP_BAD_REQUEST; -import static org.elasticsearch.common.Strings.isNullOrEmpty; - -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.web.ServletFilter; -import org.sonar.db.user.UserDto; -import org.sonar.server.exceptions.UnauthorizedException; - -public class AuthLoginAction extends ServletFilter { - - static final String AUTH_LOGIN_URL = "/api/authentication/login"; - - private static final String POST = "POST"; - - private final CredentialsAuthenticator credentialsAuthenticator; - private final JwtHttpHandler jwtHttpHandler; - - public AuthLoginAction(CredentialsAuthenticator credentialsAuthenticator, JwtHttpHandler jwtHttpHandler) { - this.credentialsAuthenticator = credentialsAuthenticator; - this.jwtHttpHandler = jwtHttpHandler; - } - - @Override - public UrlPattern doGetPattern() { - return UrlPattern.create(AUTH_LOGIN_URL); - } - - @Override - public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException { - HttpServletRequest request = (HttpServletRequest) servletRequest; - HttpServletResponse response = (HttpServletResponse) servletResponse; - - if (!request.getMethod().equals(POST)) { - response.setStatus(HTTP_BAD_REQUEST); - return; - } - try { - UserDto userDto = authenticate(request); - jwtHttpHandler.generateToken(userDto, response); - // TODO add chain.doFilter when Rack filter will not be executed after this filter (or use a Servlet) - } catch (UnauthorizedException e) { - response.setStatus(e.httpCode()); - } - } - - private UserDto authenticate(HttpServletRequest request) { - String login = request.getParameter("login"); - String password = request.getParameter("password"); - if (isNullOrEmpty(login) || isNullOrEmpty(password)) { - throw new UnauthorizedException(); - } - return credentialsAuthenticator.authenticate(login, password, request); - } - - @Override - public void init(FilterConfig filterConfig) throws ServletException { - // Nothing to do - } - - @Override - public void destroy() { - // Nothing to do - } -} diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/AuthenticationModule.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/AuthenticationModule.java index a55033f0e7e..db451df6810 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/authentication/AuthenticationModule.java +++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/AuthenticationModule.java @@ -21,6 +21,7 @@ package org.sonar.server.authentication; import org.sonar.core.platform.Module; import org.sonar.server.authentication.ws.AuthenticationWs; +import org.sonar.server.authentication.ws.LoginAction; public class AuthenticationModule extends Module { @Override @@ -34,12 +35,13 @@ public class AuthenticationModule extends Module { OAuth2ContextFactory.class, UserIdentityAuthenticator.class, OAuthCsrfVerifier.class, - ValidateJwtTokenFilter.class, + UserSessionInitializer.class, JwtSerializer.class, JwtHttpHandler.class, JwtCsrfVerifier.class, - AuthLoginAction.class, + LoginAction.class, CredentialsAuthenticator.class, - RealmAuthenticator.class); + RealmAuthenticator.class, + BasicAuthenticator.class); } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/BaseContextFactory.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/BaseContextFactory.java index ea7df897a90..e62b3cb28ae 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/authentication/BaseContextFactory.java +++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/BaseContextFactory.java @@ -24,18 +24,26 @@ 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; +import org.sonar.db.DbClient; import org.sonar.db.user.UserDto; +import org.sonar.server.user.ServerUserSession; +import org.sonar.server.user.ThreadLocalUserSession; public class BaseContextFactory { + private final DbClient dbClient; + private final ThreadLocalUserSession threadLocalUserSession; private final UserIdentityAuthenticator userIdentityAuthenticator; private final Server server; private final JwtHttpHandler jwtHttpHandler; - public BaseContextFactory(UserIdentityAuthenticator userIdentityAuthenticator, Server server, JwtHttpHandler jwtHttpHandler) { + public BaseContextFactory(DbClient dbClient, UserIdentityAuthenticator userIdentityAuthenticator, Server server, JwtHttpHandler jwtHttpHandler, + ThreadLocalUserSession threadLocalUserSession) { + this.dbClient = dbClient; this.userIdentityAuthenticator = userIdentityAuthenticator; this.server = server; this.jwtHttpHandler = jwtHttpHandler; + this.threadLocalUserSession = threadLocalUserSession; } public BaseIdentityProvider.Context newContext(HttpServletRequest request, HttpServletResponse response, BaseIdentityProvider identityProvider) { @@ -72,6 +80,7 @@ public class BaseContextFactory { public void authenticate(UserIdentity userIdentity) { UserDto userDto = userIdentityAuthenticator.authenticate(userIdentity, identityProvider); jwtHttpHandler.generateToken(userDto, response); + threadLocalUserSession.set(ServerUserSession.createForUser(dbClient, userDto)); } } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/BasicAuthenticator.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/BasicAuthenticator.java new file mode 100644 index 00000000000..3c000d9b467 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/BasicAuthenticator.java @@ -0,0 +1,104 @@ +/* + * 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 static java.util.Locale.ENGLISH; +import static org.elasticsearch.common.Strings.isEmpty; + +import com.google.common.base.Charsets; +import java.util.Base64; +import java.util.Optional; +import javax.servlet.http.HttpServletRequest; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.user.UserDto; +import org.sonar.server.exceptions.UnauthorizedException; +import org.sonar.server.usertoken.UserTokenAuthenticator; + +public class BasicAuthenticator { + + private static final Base64.Decoder BASE64_DECODER = Base64.getDecoder(); + + private static final String AUTHORIZATION_HEADER = "Authorization"; + private static final String BASIC_AUTHORIZATION = "BASIC"; + + private final DbClient dbClient; + private final CredentialsAuthenticator credentialsAuthenticator; + private final UserTokenAuthenticator userTokenAuthenticator; + + public BasicAuthenticator(DbClient dbClient, CredentialsAuthenticator credentialsAuthenticator, + UserTokenAuthenticator userTokenAuthenticator) { + this.dbClient = dbClient; + this.credentialsAuthenticator = credentialsAuthenticator; + this.userTokenAuthenticator = userTokenAuthenticator; + } + + public Optional authenticate(HttpServletRequest request) { + String authorizationHeader = request.getHeader(AUTHORIZATION_HEADER); + if (authorizationHeader == null || !authorizationHeader.toUpperCase(ENGLISH).startsWith(BASIC_AUTHORIZATION)) { + return Optional.empty(); + } + + String[] credentials = getCredentials(authorizationHeader); + String login = credentials[0]; + String password = credentials[1]; + return Optional.of(authenticate(login, password, request)); + } + + private static String[] getCredentials(String authorizationHeader) { + String basicAuthEncoded = authorizationHeader.substring(6); + String basicAuthDecoded = new String(BASE64_DECODER.decode(basicAuthEncoded.getBytes(Charsets.UTF_8)), Charsets.UTF_8); + + int semiColonPos = basicAuthDecoded.indexOf(':'); + if (semiColonPos <= 0) { + throw new UnauthorizedException("Invalid credentials : " + basicAuthDecoded); + } + String login = basicAuthDecoded.substring(0, semiColonPos); + String password = basicAuthDecoded.substring(semiColonPos + 1); + return new String[] {login, password}; + } + + private UserDto authenticate(String login, String password, HttpServletRequest request) { + if (isEmpty(password)) { + return authenticateFromUserToken(login); + } else { + return credentialsAuthenticator.authenticate(login, password, request); + } + } + + private UserDto authenticateFromUserToken(String token) { + Optional authenticatedLogin = userTokenAuthenticator.authenticate(token); + if (!authenticatedLogin.isPresent()) { + throw new UnauthorizedException("Token doesn't exist"); + } + DbSession dbSession = dbClient.openSession(false); + try { + UserDto userDto = dbClient.userDao().selectActiveUserByLogin(dbSession, authenticatedLogin.get()); + if (userDto == null) { + throw new UnauthorizedException("User doesn't exist"); + } + return userDto; + } finally { + dbClient.closeSession(dbSession); + } + } + +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/JwtHttpHandler.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/JwtHttpHandler.java index d42349b5f6e..2e8e88e258f 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/authentication/JwtHttpHandler.java +++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/JwtHttpHandler.java @@ -23,7 +23,6 @@ package org.sonar.server.authentication; import static java.util.Objects.requireNonNull; import static org.elasticsearch.common.Strings.isNullOrEmpty; import static org.sonar.server.authentication.CookieUtils.findCookie; -import static org.sonar.server.user.ServerUserSession.createForUser; import com.google.common.collect.ImmutableMap; import io.jsonwebtoken.Claims; @@ -41,9 +40,6 @@ import org.sonar.api.utils.System2; import org.sonar.db.DbClient; import org.sonar.db.DbSession; import org.sonar.db.user.UserDto; -import org.sonar.server.exceptions.UnauthorizedException; -import org.sonar.server.user.ServerUserSession; -import org.sonar.server.user.ThreadLocalUserSession; @ServerSide public class JwtHttpHandler { @@ -71,20 +67,17 @@ public class JwtHttpHandler { // This timeout is used to disconnect the user we he has not browse any page for a while private final int sessionTimeoutInSeconds; private final JwtCsrfVerifier jwtCsrfVerifier; - private final ThreadLocalUserSession threadLocalUserSession; - public JwtHttpHandler(System2 system2, DbClient dbClient, Server server, Settings settings, JwtSerializer jwtSerializer, JwtCsrfVerifier jwtCsrfVerifier, - ThreadLocalUserSession threadLocalUserSession) { + public JwtHttpHandler(System2 system2, DbClient dbClient, Server server, Settings settings, JwtSerializer jwtSerializer, JwtCsrfVerifier jwtCsrfVerifier) { this.jwtSerializer = jwtSerializer; this.server = server; this.dbClient = dbClient; this.system2 = system2; this.sessionTimeoutInSeconds = getSessionTimeoutInSeconds(settings); this.jwtCsrfVerifier = jwtCsrfVerifier; - this.threadLocalUserSession = threadLocalUserSession; } - void generateToken(UserDto user, HttpServletResponse response) { + public void generateToken(UserDto user, HttpServletResponse response) { String csrfState = jwtCsrfVerifier.generateState(response, sessionTimeoutInSeconds); String token = jwtSerializer.encode(new JwtSerializer.JwtSession( @@ -94,22 +87,23 @@ public class JwtHttpHandler { LAST_REFRESH_TIME_PARAM, system2.now(), CSRF_JWT_PARAM, csrfState))); response.addCookie(createCookie(JWT_COOKIE, token, sessionTimeoutInSeconds)); - threadLocalUserSession.set(createForUser(dbClient, user)); } - void validateToken(HttpServletRequest request, HttpServletResponse response) { - validate(request, response); - if (!threadLocalUserSession.isLoggedIn()) { - threadLocalUserSession.set(ServerUserSession.createForAnonymous(dbClient)); + public Optional validateToken(HttpServletRequest request, HttpServletResponse response) { + Optional userDto = validate(request, response); + if (userDto.isPresent()) { + return userDto; } + removeToken(response); + return Optional.empty(); } - private void validate(HttpServletRequest request, HttpServletResponse response) { + private Optional validate(HttpServletRequest request, HttpServletResponse response) { Optional token = getTokenFromCookie(request); if (!token.isPresent()) { - return; + return Optional.empty(); } - validateToken(token.get(), request, response); + return validateToken(token.get(), request, response); } private static Optional getTokenFromCookie(HttpServletRequest request) { @@ -125,18 +119,16 @@ public class JwtHttpHandler { return Optional.of(token); } - private void validateToken(String tokenEncoded, HttpServletRequest request, HttpServletResponse response) { + private Optional validateToken(String tokenEncoded, HttpServletRequest request, HttpServletResponse response) { Optional claims = jwtSerializer.decode(tokenEncoded); if (!claims.isPresent()) { - removeToken(response); - return; + return Optional.empty(); } Date now = new Date(system2.now()); Claims token = claims.get(); if (now.after(DateUtils.addSeconds(token.getIssuedAt(), SESSION_DISCONNECT_IN_SECONDS))) { - removeToken(response); - return; + return Optional.empty(); } jwtCsrfVerifier.verifyState(request, (String) token.get(CSRF_JWT_PARAM)); @@ -146,10 +138,9 @@ public class JwtHttpHandler { Optional user = selectUserFromDb(token.getSubject()); if (!user.isPresent()) { - removeToken(response); - throw new UnauthorizedException("User does not exist"); + return Optional.empty(); } - threadLocalUserSession.set(createForUser(dbClient, user.get())); + return Optional.of(user.get()); } private static Date getLastRefreshDate(Claims token) { @@ -167,7 +158,6 @@ public class JwtHttpHandler { void removeToken(HttpServletResponse response) { response.addCookie(createCookie(JWT_COOKIE, null, 0)); jwtCsrfVerifier.removeState(response); - threadLocalUserSession.remove(); } private Cookie createCookie(String name, @Nullable String value, int expirationInSeconds) { diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/OAuth2ContextFactory.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/OAuth2ContextFactory.java index 6f5948cc616..18cbc0d8808 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/authentication/OAuth2ContextFactory.java +++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/OAuth2ContextFactory.java @@ -30,16 +30,24 @@ import org.sonar.api.platform.Server; import org.sonar.api.server.authentication.OAuth2IdentityProvider; import org.sonar.api.server.authentication.UserIdentity; import org.sonar.api.utils.MessageException; +import org.sonar.db.DbClient; import org.sonar.db.user.UserDto; +import org.sonar.server.user.ServerUserSession; +import org.sonar.server.user.ThreadLocalUserSession; public class OAuth2ContextFactory { + private final DbClient dbClient; + private final ThreadLocalUserSession threadLocalUserSession; private final UserIdentityAuthenticator userIdentityAuthenticator; private final Server server; private final OAuthCsrfVerifier csrfVerifier; private final JwtHttpHandler jwtHttpHandler; - public OAuth2ContextFactory(UserIdentityAuthenticator userIdentityAuthenticator, Server server, OAuthCsrfVerifier csrfVerifier, JwtHttpHandler jwtHttpHandler) { + public OAuth2ContextFactory(DbClient dbClient, ThreadLocalUserSession threadLocalUserSession, UserIdentityAuthenticator userIdentityAuthenticator, Server server, + OAuthCsrfVerifier csrfVerifier, JwtHttpHandler jwtHttpHandler) { + this.dbClient = dbClient; + this.threadLocalUserSession = threadLocalUserSession; this.userIdentityAuthenticator = userIdentityAuthenticator; this.server = server; this.csrfVerifier = csrfVerifier; @@ -117,6 +125,7 @@ public class OAuth2ContextFactory { public void authenticate(UserIdentity userIdentity) { UserDto userDto = userIdentityAuthenticator.authenticate(userIdentity, identityProvider); jwtHttpHandler.generateToken(userDto, response); + threadLocalUserSession.set(ServerUserSession.createForUser(dbClient, userDto)); } } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/UserSessionInitializer.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/UserSessionInitializer.java new file mode 100644 index 00000000000..5aa8b8e2d3b --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/UserSessionInitializer.java @@ -0,0 +1,126 @@ +/* + * 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 static java.net.HttpURLConnection.HTTP_UNAUTHORIZED; +import static org.sonar.api.CoreProperties.CORE_FORCE_AUTHENTICATION_PROPERTY; +import static org.sonar.api.web.ServletFilter.UrlPattern; +import static org.sonar.api.web.ServletFilter.UrlPattern.Builder.staticResourcePatterns; +import static org.sonar.server.authentication.ws.LoginAction.AUTH_LOGIN_URL; +import static org.sonar.server.user.ServerUserSession.createForAnonymous; +import static org.sonar.server.user.ServerUserSession.createForUser; + +import com.google.common.collect.ImmutableSet; +import java.util.Optional; +import java.util.Set; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.sonar.api.config.Settings; +import org.sonar.api.server.ServerSide; +import org.sonar.db.DbClient; +import org.sonar.db.user.UserDto; +import org.sonar.server.exceptions.UnauthorizedException; +import org.sonar.server.user.ThreadLocalUserSession; + +@ServerSide +public class UserSessionInitializer { + + // SONAR-6546 these urls should be get from WebService + private static final Set SKIPPED_URLS = ImmutableSet.of( + "/batch/index", "/batch/file", + "/maintenance/*", + "/setup/*", + "/sessions/*", + "/api/system/db_migration_status", "/api/system/status", "/api/system/migrate_db", + "/api/server/*", + AUTH_LOGIN_URL); + + private static final UrlPattern URL_PATTERN = UrlPattern.builder() + .includes("/*") + .excludes(staticResourcePatterns()) + .excludes(SKIPPED_URLS) + .build(); + + private final DbClient dbClient; + private final Settings settings; + private final JwtHttpHandler jwtHttpHandler; + private final BasicAuthenticator basicAuthenticator; + private final ThreadLocalUserSession userSession; + + public UserSessionInitializer(DbClient dbClient, Settings settings, JwtHttpHandler jwtHttpHandler, BasicAuthenticator basicAuthenticator, + ThreadLocalUserSession userSession) { + this.dbClient = dbClient; + this.settings = settings; + this.jwtHttpHandler = jwtHttpHandler; + this.basicAuthenticator = basicAuthenticator; + this.userSession = userSession; + } + + public boolean initUserSession(HttpServletRequest request, HttpServletResponse response) { + String path = request.getRequestURI().replaceFirst(request.getContextPath(), ""); + try { + // Do not set user session when url is excluded + if (!URL_PATTERN.matches(path)) { + return true; + } + setUserSession(request, response); + return true; + } catch (UnauthorizedException e) { + jwtHttpHandler.removeToken(response); + response.setStatus(HTTP_UNAUTHORIZED); + if (isWsUrl(path)) { + return false; + } + // WS should stop here. Rails page should continue in order to deal with redirection + return true; + } + } + + private void setUserSession(HttpServletRequest request, HttpServletResponse response) { + Optional user = authenticate(request, response); + if (user.isPresent()) { + userSession.set(createForUser(dbClient, user.get())); + } else { + if (settings.getBoolean(CORE_FORCE_AUTHENTICATION_PROPERTY)) { + throw new UnauthorizedException("User must be authenticated"); + } + userSession.set(createForAnonymous(dbClient)); + } + } + + public void removeUserSession() { + userSession.remove(); + } + + // Try first to authenticate from JWT token, then try from basic http header + private Optional authenticate(HttpServletRequest request, HttpServletResponse response) { + Optional user = jwtHttpHandler.validateToken(request, response); + if (user.isPresent()) { + return user; + } + return basicAuthenticator.authenticate(request); + } + + private static boolean isWsUrl(String path) { + return path.startsWith("/batch/") || path.startsWith("/api/"); + } + +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/ValidateJwtTokenFilter.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/ValidateJwtTokenFilter.java deleted file mode 100644 index fd3878b665e..00000000000 --- a/server/sonar-server/src/main/java/org/sonar/server/authentication/ValidateJwtTokenFilter.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * 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 static java.net.HttpURLConnection.HTTP_UNAUTHORIZED; -import static org.sonar.api.CoreProperties.CORE_FORCE_AUTHENTICATION_PROPERTY; -import static org.sonar.api.web.ServletFilter.UrlPattern.Builder.staticResourcePatterns; -import static org.sonar.server.authentication.AuthLoginAction.AUTH_LOGIN_URL; - -import com.google.common.collect.ImmutableSet; -import java.io.IOException; -import java.util.Set; -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.config.Settings; -import org.sonar.api.server.ServerSide; -import org.sonar.api.web.ServletFilter; -import org.sonar.server.exceptions.UnauthorizedException; -import org.sonar.server.user.UserSession; - -@ServerSide -public class ValidateJwtTokenFilter extends ServletFilter { - - // SONAR-6546 these urls should be get from WebService - private static final Set SKIPPED_URLS = ImmutableSet.of( - "/batch/index", "/batch/file", "/batch_bootstrap/index", - "/maintenance/*", - "/setup/*", - "/sessions/*", - "/api/system/db_migration_status", "/api/system/status", "/api/system/migrate_db", - "/api/server/*", - AUTH_LOGIN_URL - ); - - private final Settings settings; - private final JwtHttpHandler jwtHttpHandler; - private final UserSession userSession; - - public ValidateJwtTokenFilter(Settings settings, JwtHttpHandler jwtHttpHandler, UserSession userSession) { - this.settings = settings; - this.jwtHttpHandler = jwtHttpHandler; - this.userSession = userSession; - } - - @Override - public UrlPattern doGetPattern() { - return UrlPattern.builder() - .includes("/*") - .excludes(staticResourcePatterns()) - .excludes(SKIPPED_URLS) - .build(); - } - - @Override - public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException { - HttpServletRequest request = (HttpServletRequest) servletRequest; - HttpServletResponse response = (HttpServletResponse) servletResponse; - String path = request.getRequestURI().replaceFirst(request.getContextPath(), ""); - - try { - if (isDeprecatedBatchWs(path)) { - chain.doFilter(request, response); - return; - } - - jwtHttpHandler.validateToken(request, response); - // TODO handle basic authentication - if (!userSession.isLoggedIn() && settings.getBoolean(CORE_FORCE_AUTHENTICATION_PROPERTY)) { - throw new UnauthorizedException("User must be authenticated"); - } - chain.doFilter(request, response); - } catch (UnauthorizedException e) { - jwtHttpHandler.removeToken(response); - response.setStatus(HTTP_UNAUTHORIZED); - - if (isWsUrl(path)) { - return; - } - // WS should stop here. Rails page should continue in order to deal with redirection - chain.doFilter(request, response); - } - } - - // Scanner is still using deprecated /batch/.jar WS - private static boolean isDeprecatedBatchWs(String path){ - return path.startsWith("/batch/") && path.endsWith(".jar"); - } - - private static boolean isWsUrl(String path){ - return path.startsWith("/batch/") || path.startsWith("/api/"); - } - - @Override - public void init(FilterConfig filterConfig) throws ServletException { - // Nothing to do - } - - @Override - public void destroy() { - // Nothing to do - } -} diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/ws/LoginAction.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/ws/LoginAction.java new file mode 100644 index 00000000000..4407c90e50c --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/ws/LoginAction.java @@ -0,0 +1,103 @@ +/* + * 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.ws; + +import static java.net.HttpURLConnection.HTTP_BAD_REQUEST; +import static org.elasticsearch.common.Strings.isNullOrEmpty; + +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.web.ServletFilter; +import org.sonar.db.DbClient; +import org.sonar.db.user.UserDto; +import org.sonar.server.authentication.CredentialsAuthenticator; +import org.sonar.server.authentication.JwtHttpHandler; +import org.sonar.server.exceptions.UnauthorizedException; +import org.sonar.server.user.ServerUserSession; +import org.sonar.server.user.ThreadLocalUserSession; + +public class LoginAction extends ServletFilter { + + public static final String AUTH_LOGIN_URL = "/api/authentication/login"; + + private static final String POST = "POST"; + + private final DbClient dbClient; + private final CredentialsAuthenticator credentialsAuthenticator; + private final JwtHttpHandler jwtHttpHandler; + private final ThreadLocalUserSession threadLocalUserSession; + + public LoginAction(DbClient dbClient, CredentialsAuthenticator credentialsAuthenticator, JwtHttpHandler jwtHttpHandler, ThreadLocalUserSession threadLocalUserSession) { + this.dbClient = dbClient; + this.credentialsAuthenticator = credentialsAuthenticator; + this.jwtHttpHandler = jwtHttpHandler; + this.threadLocalUserSession = threadLocalUserSession; + } + + @Override + public UrlPattern doGetPattern() { + return UrlPattern.create(AUTH_LOGIN_URL); + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException { + HttpServletRequest request = (HttpServletRequest) servletRequest; + HttpServletResponse response = (HttpServletResponse) servletResponse; + + if (!request.getMethod().equals(POST)) { + response.setStatus(HTTP_BAD_REQUEST); + return; + } + try { + UserDto userDto = authenticate(request); + jwtHttpHandler.generateToken(userDto, response); + threadLocalUserSession.set(ServerUserSession.createForUser(dbClient, userDto)); + // TODO add chain.doFilter when Rack filter will not be executed after this filter (or use a Servlet) + } catch (UnauthorizedException e) { + response.setStatus(e.httpCode()); + } + } + + private UserDto authenticate(HttpServletRequest request) { + String login = request.getParameter("login"); + String password = request.getParameter("password"); + if (isNullOrEmpty(login) || isNullOrEmpty(password)) { + throw new UnauthorizedException(); + } + return credentialsAuthenticator.authenticate(login, password, request); + } + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + // Nothing to do + } + + @Override + public void destroy() { + // Nothing to do + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/user/UserSessionFilter.java b/server/sonar-server/src/main/java/org/sonar/server/user/UserSessionFilter.java index f5b7ccc4afc..58c351e6ea2 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/user/UserSessionFilter.java +++ b/server/sonar-server/src/main/java/org/sonar/server/user/UserSessionFilter.java @@ -19,6 +19,7 @@ */ package org.sonar.server.user; +import com.google.common.annotations.VisibleForTesting; import java.io.IOException; import javax.servlet.Filter; import javax.servlet.FilterChain; @@ -26,44 +27,58 @@ import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; -import org.sonar.api.utils.log.Loggers; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.sonar.server.authentication.UserSessionInitializer; import org.sonar.server.platform.Platform; -/** - * @since 3.6 - */ public class UserSessionFilter implements Filter { + private final Platform platform; + private UserSessionInitializer userSessionInitializer; public UserSessionFilter() { this.platform = Platform.getInstance(); } - public UserSessionFilter(Platform platform) { + @VisibleForTesting + UserSessionFilter(Platform platform) { this.platform = platform; } @Override - public void init(FilterConfig filterConfig) throws ServletException { - // nothing to do + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException { + try { + HttpServletRequest request = (HttpServletRequest) servletRequest; + HttpServletResponse response = (HttpServletResponse) servletResponse; + init(); + if (!isInitialized() || userSessionInitializer.initUserSession(request, response)) { + chain.doFilter(servletRequest, servletResponse); + } + } finally { + if (isInitialized()) { + userSessionInitializer.removeUserSession(); + } + } + } + + private boolean isInitialized() { + return userSessionInitializer != null; + } + + private void init() { + if (userSessionInitializer == null) { + userSessionInitializer = platform.getContainer().getComponentByType(UserSessionInitializer.class); + } } @Override - public void destroy() { + public void init(FilterConfig filterConfig) throws ServletException { // nothing to do } @Override - public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException { - try { - chain.doFilter(servletRequest, servletResponse); - } finally { - ThreadLocalUserSession userSession = platform.getContainer().getComponentByType(ThreadLocalUserSession.class); - if (userSession == null) { - Loggers.get(UserSessionFilter.class).error("Can not retrieve ThreadLocalUserSession from Platform"); - } else { - userSession.remove(); - } - } + public void destroy() { + // nothing to do } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/usertoken/UserTokenAuthenticator.java b/server/sonar-server/src/main/java/org/sonar/server/usertoken/UserTokenAuthenticator.java index fb26c6f5c78..bb751b20ae9 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/usertoken/UserTokenAuthenticator.java +++ b/server/sonar-server/src/main/java/org/sonar/server/usertoken/UserTokenAuthenticator.java @@ -38,15 +38,15 @@ public class UserTokenAuthenticator { * The returned login is not validated. If database is corrupted (table USER_TOKENS badly purged * for instance), then the login may not relate to a valid user. */ - public Optional authenticate(String token) { + public java.util.Optional authenticate(String token) { String tokenHash = tokenGenerator.hash(token); DbSession dbSession = dbClient.openSession(false); try { Optional userToken = dbClient.userTokenDao().selectByTokenHash(dbSession, tokenHash); if (userToken.isPresent()) { - return Optional.of(userToken.get().getLogin()); + return java.util.Optional.of(userToken.get().getLogin()); } - return Optional.absent(); + return java.util.Optional.empty(); } finally { dbClient.closeSession(dbSession); } diff --git a/server/sonar-server/src/test/java/org/sonar/server/authentication/AuthLoginActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/authentication/AuthLoginActionTest.java deleted file mode 100644 index cbd7f126d5f..00000000000 --- a/server/sonar-server/src/test/java/org/sonar/server/authentication/AuthLoginActionTest.java +++ /dev/null @@ -1,122 +0,0 @@ -/* - * 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 static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyZeroInteractions; -import static org.mockito.Mockito.when; - -import java.io.IOException; -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import org.junit.Test; -import org.sonar.db.user.UserDto; -import org.sonar.db.user.UserTesting; -import org.sonar.server.exceptions.UnauthorizedException; - -public class AuthLoginActionTest { - - static final String LOGIN = "LOGIN"; - static final String PASSWORD = "PASSWORD"; - - static final UserDto USER = UserTesting.newUserDto().setLogin(LOGIN); - - HttpServletRequest request = mock(HttpServletRequest.class); - HttpServletResponse response = mock(HttpServletResponse.class); - FilterChain chain = mock(FilterChain.class); - - CredentialsAuthenticator credentialsAuthenticator = mock(CredentialsAuthenticator.class); - JwtHttpHandler jwtHttpHandler = mock(JwtHttpHandler.class); - - AuthLoginAction underTest = new AuthLoginAction(credentialsAuthenticator, jwtHttpHandler); - - @Test - public void do_get_pattern() throws Exception { - assertThat(underTest.doGetPattern().matches("/api/authentication/login")).isTrue(); - assertThat(underTest.doGetPattern().matches("/api/authentication/logout")).isFalse(); - assertThat(underTest.doGetPattern().matches("/foo")).isFalse(); - } - - @Test - public void do_authenticate() throws Exception { - when(credentialsAuthenticator.authenticate(LOGIN, PASSWORD, request)).thenReturn(USER); - - executeRequest(LOGIN, PASSWORD); - - verify(credentialsAuthenticator).authenticate(LOGIN, PASSWORD, request); - verify(jwtHttpHandler).generateToken(USER, response); - verifyZeroInteractions(chain); - } - - @Test - public void ignore_get_request() throws Exception { - when(request.getMethod()).thenReturn("GET"); - - underTest.doFilter(request, response, chain); - - verifyZeroInteractions(credentialsAuthenticator, jwtHttpHandler, chain); - } - - @Test - public void return_authorized_code_when_unauthorized_exception_is_thrown() throws Exception { - doThrow(new UnauthorizedException()).when(credentialsAuthenticator).authenticate(LOGIN, PASSWORD, request); - - executeRequest(LOGIN, PASSWORD); - - verify(response).setStatus(401); - } - - @Test - public void return_unauthorized_code_when_no_login() throws Exception { - executeRequest(null, PASSWORD); - verify(response).setStatus(401); - } - - @Test - public void return_unauthorized_code_when_empty_login() throws Exception { - executeRequest("", PASSWORD); - verify(response).setStatus(401); - } - - @Test - public void return_unauthorized_code_when_no_password() throws Exception { - executeRequest(LOGIN, null); - verify(response).setStatus(401); - } - - @Test - public void return_unauthorized_code_when_empty_password() throws Exception { - executeRequest(LOGIN, ""); - verify(response).setStatus(401); - } - - private void executeRequest(String login, String password) throws IOException, ServletException { - when(request.getMethod()).thenReturn("POST"); - when(request.getParameter("login")).thenReturn(login); - when(request.getParameter("password")).thenReturn(password); - underTest.doFilter(request, response, chain); - } -} diff --git a/server/sonar-server/src/test/java/org/sonar/server/authentication/BaseContextFactoryTest.java b/server/sonar-server/src/test/java/org/sonar/server/authentication/BaseContextFactoryTest.java index 8128b3ac55e..cbe62a31472 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/authentication/BaseContextFactoryTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/authentication/BaseContextFactoryTest.java @@ -25,16 +25,24 @@ import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.sonar.db.user.UserTesting.newUserDto; import 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.sonar.api.platform.Server; import org.sonar.api.server.authentication.BaseIdentityProvider; import org.sonar.api.server.authentication.UserIdentity; +import org.sonar.api.utils.System2; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.DbTester; import org.sonar.db.user.UserDto; +import org.sonar.server.user.ThreadLocalUserSession; +import org.sonar.server.user.UserSession; public class BaseContextFactoryTest { @@ -47,6 +55,15 @@ public class BaseContextFactoryTest { .setEmail("john@email.com") .build(); + @Rule + public DbTester dbTester = DbTester.create(System2.INSTANCE); + + DbClient dbClient = dbTester.getDbClient(); + + DbSession dbSession = dbTester.getSession(); + + ThreadLocalUserSession threadLocalUserSession = mock(ThreadLocalUserSession.class); + UserIdentityAuthenticator userIdentityAuthenticator = mock(UserIdentityAuthenticator.class); Server server = mock(Server.class); @@ -55,11 +72,15 @@ public class BaseContextFactoryTest { BaseIdentityProvider identityProvider = mock(BaseIdentityProvider.class); JwtHttpHandler jwtHttpHandler = mock(JwtHttpHandler.class); - BaseContextFactory underTest = new BaseContextFactory(userIdentityAuthenticator, server, jwtHttpHandler); + BaseContextFactory underTest = new BaseContextFactory(dbClient, userIdentityAuthenticator, server, jwtHttpHandler, threadLocalUserSession); @Before public void setUp() throws Exception { when(server.getPublicRootUrl()).thenReturn(PUBLIC_ROOT_URL); + + UserDto userDto = dbClient.userDao().insert(dbSession, newUserDto()); + dbSession.commit(); + when(userIdentityAuthenticator.authenticate(USER_IDENTITY, identityProvider)).thenReturn(userDto); } @Test @@ -80,5 +101,6 @@ public class BaseContextFactoryTest { context.authenticate(USER_IDENTITY); verify(userIdentityAuthenticator).authenticate(USER_IDENTITY, identityProvider); verify(jwtHttpHandler).generateToken(any(UserDto.class), eq(response)); + verify(threadLocalUserSession).set(any(UserSession.class)); } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/authentication/BasicAuthenticatorTest.java b/server/sonar-server/src/test/java/org/sonar/server/authentication/BasicAuthenticatorTest.java new file mode 100644 index 00000000000..692db7ea601 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/authentication/BasicAuthenticatorTest.java @@ -0,0 +1,162 @@ +/* + * 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 static com.google.common.base.Charsets.UTF_8; +import static org.assertj.core.api.Java6Assertions.assertThat; +import static org.junit.rules.ExpectedException.none; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +import java.util.Base64; +import java.util.Optional; +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.sonar.api.utils.System2; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.DbTester; +import org.sonar.db.user.UserDto; +import org.sonar.db.user.UserTesting; +import org.sonar.server.exceptions.UnauthorizedException; +import org.sonar.server.usertoken.UserTokenAuthenticator; + +public class BasicAuthenticatorTest { + + private static final Base64.Encoder BASE64_ENCODER = Base64.getEncoder(); + + static final String LOGIN = "login"; + static final String PASSWORD = "password"; + static final String CREDENTIALS_IN_BASE64 = toBase64(LOGIN + ":" + PASSWORD); + + static final UserDto USER = UserTesting.newUserDto().setLogin(LOGIN); + + @Rule + public ExpectedException expectedException = none(); + + @Rule + public DbTester dbTester = DbTester.create(System2.INSTANCE); + + DbClient dbClient = dbTester.getDbClient(); + + DbSession dbSession = dbTester.getSession(); + + CredentialsAuthenticator credentialsAuthenticator = mock(CredentialsAuthenticator.class); + UserTokenAuthenticator userTokenAuthenticator = mock(UserTokenAuthenticator.class); + + HttpServletRequest request = mock(HttpServletRequest.class); + HttpServletResponse response = mock(HttpServletResponse.class); + + BasicAuthenticator underTest = new BasicAuthenticator(dbClient, credentialsAuthenticator, userTokenAuthenticator); + + @Test + public void authenticate_from_basic_http_header() throws Exception { + when(request.getHeader("Authorization")).thenReturn("Basic " + CREDENTIALS_IN_BASE64); + when(credentialsAuthenticator.authenticate(LOGIN, PASSWORD, request)).thenReturn(USER); + + underTest.authenticate(request); + + verify(credentialsAuthenticator).authenticate(LOGIN, PASSWORD, request); + } + + @Test + public void authenticate_from_basic_http_header_with_password_containing_semi_colon() throws Exception { + String password = "!ascii-only:-)@"; + when(request.getHeader("Authorization")).thenReturn("Basic " + toBase64(LOGIN + ":" + password)); + when(credentialsAuthenticator.authenticate(LOGIN, password, request)).thenReturn(USER); + + underTest.authenticate(request); + + verify(credentialsAuthenticator).authenticate(LOGIN, password, request); + } + + @Test + public void does_not_authenticate_when_no_authorization_header() throws Exception { + underTest.authenticate(request); + + verifyZeroInteractions(credentialsAuthenticator); + } + + @Test + public void does_not_authenticate_when_authorization_header_is_not_BASIC() throws Exception { + when(request.getHeader("Authorization")).thenReturn("OTHER " + CREDENTIALS_IN_BASE64); + + underTest.authenticate(request); + + verifyZeroInteractions(credentialsAuthenticator); + } + + @Test + public void fail_to_authenticate_when_no_login() throws Exception { + when(request.getHeader("Authorization")).thenReturn("Basic " + toBase64(":" + PASSWORD)); + + expectedException.expect(UnauthorizedException.class); + underTest.authenticate(request); + } + + @Test + public void authenticate_from_user_token() throws Exception { + insertUser(UserTesting.newUserDto().setLogin(LOGIN)); + when(userTokenAuthenticator.authenticate("token")).thenReturn(Optional.of(LOGIN)); + when(request.getHeader("Authorization")).thenReturn("Basic " + toBase64("token:")); + + Optional userDto = underTest.authenticate(request); + + assertThat(userDto.isPresent()).isTrue(); + assertThat(userDto.get().getLogin()).isEqualTo(LOGIN); + } + + @Test + public void does_not_authenticate_from_user_token_when_token_is_invalid() throws Exception { + insertUser(UserTesting.newUserDto().setLogin(LOGIN)); + when(userTokenAuthenticator.authenticate("token")).thenReturn(Optional.empty()); + when(request.getHeader("Authorization")).thenReturn("Basic " + toBase64("token:")); + + expectedException.expect(UnauthorizedException.class); + underTest.authenticate(request); + } + + @Test + public void does_not_authenticate_from_user_token_when_token_does_not_match_active_user() throws Exception { + insertUser(UserTesting.newUserDto().setLogin(LOGIN)); + when(userTokenAuthenticator.authenticate("token")).thenReturn(Optional.of("Unknown user")); + when(request.getHeader("Authorization")).thenReturn("Basic " + toBase64("token:")); + + expectedException.expect(UnauthorizedException.class); + underTest.authenticate(request); + } + + private UserDto insertUser(UserDto userDto){ + dbClient.userDao().insert(dbSession, userDto); + dbSession.commit(); + return userDto; + } + + private static String toBase64(String text){ + return new String(BASE64_ENCODER.encode(text.getBytes(UTF_8))); + } + +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/authentication/JwtHttpHandlerTest.java b/server/sonar-server/src/test/java/org/sonar/server/authentication/JwtHttpHandlerTest.java index 2f67f52021a..e276fde6007 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/authentication/JwtHttpHandlerTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/authentication/JwtHttpHandlerTest.java @@ -54,9 +54,6 @@ import org.sonar.db.DbClient; import org.sonar.db.DbSession; import org.sonar.db.DbTester; import org.sonar.db.user.UserDto; -import org.sonar.server.exceptions.UnauthorizedException; -import org.sonar.server.user.ServerUserSession; -import org.sonar.server.user.ThreadLocalUserSession; public class JwtHttpHandlerTest { @@ -75,8 +72,6 @@ public class JwtHttpHandlerTest { @Rule public DbTester dbTester = DbTester.create(INSTANCE); - ThreadLocalUserSession threadLocalUserSession = new ThreadLocalUserSession(); - DbClient dbClient = dbTester.getDbClient(); DbSession dbSession = dbTester.getSession(); @@ -96,11 +91,10 @@ public class JwtHttpHandlerTest { UserDto userDto = newUserDto().setLogin(USER_LOGIN); - JwtHttpHandler underTest = new JwtHttpHandler(system2, dbClient, server, settings, jwtSerializer, jwtCsrfVerifier, threadLocalUserSession); + JwtHttpHandler underTest = new JwtHttpHandler(system2, dbClient, server, settings, jwtSerializer, jwtCsrfVerifier); @Before public void setUp() throws Exception { - threadLocalUserSession.remove(); when(system2.now()).thenReturn(NOW); when(server.isSecured()).thenReturn(true); when(request.getSession()).thenReturn(httpSession); @@ -120,7 +114,6 @@ public class JwtHttpHandlerTest { verify(jwtSerializer).encode(jwtArgumentCaptor.capture()); verifyToken(jwtArgumentCaptor.getValue(), 3 * 24 * 60 * 60, NOW); - assertThat(threadLocalUserSession.get().isLoggedIn()).isTrue(); } @Test @@ -139,7 +132,7 @@ public class JwtHttpHandlerTest { int sessionTimeoutInHours = 10; settings.setProperty("sonar.auth.sessionTimeoutInHours", sessionTimeoutInHours); - underTest = new JwtHttpHandler(system2, dbClient, server, settings, jwtSerializer, jwtCsrfVerifier, threadLocalUserSession); + underTest = new JwtHttpHandler(system2, dbClient, server, settings, jwtSerializer, jwtCsrfVerifier); underTest.generateToken(userDto, response); verify(jwtSerializer).encode(jwtArgumentCaptor.capture()); @@ -151,7 +144,7 @@ public class JwtHttpHandlerTest { int firstSessionTimeoutInHours = 10; settings.setProperty("sonar.auth.sessionTimeoutInHours", firstSessionTimeoutInHours); - underTest = new JwtHttpHandler(system2, dbClient, server, settings, jwtSerializer, jwtCsrfVerifier, threadLocalUserSession); + underTest = new JwtHttpHandler(system2, dbClient, server, settings, jwtSerializer, jwtCsrfVerifier); underTest.generateToken(userDto, response); // The property is updated, but it won't be taking into account @@ -169,10 +162,9 @@ public class JwtHttpHandlerTest { Claims claims = createToken(USER_LOGIN, NOW); when(jwtSerializer.decode(JWT_TOKEN)).thenReturn(Optional.of(claims)); - underTest.validateToken(request, response); + assertThat(underTest.validateToken(request, response).isPresent()).isTrue(); verify(jwtSerializer, never()).encode(any(JwtSerializer.JwtSession.class)); - assertThat(threadLocalUserSession.get().isLoggedIn()).isTrue(); } @Test @@ -184,10 +176,9 @@ public class JwtHttpHandlerTest { claims.put("lastRefreshTime", SIX_MINUTES_AGO); when(jwtSerializer.decode(JWT_TOKEN)).thenReturn(Optional.of(claims)); - underTest.validateToken(request, response); + assertThat(underTest.validateToken(request, response).isPresent()).isTrue(); verify(jwtSerializer).refresh(any(Claims.class), eq(3 * 24 * 60 * 60)); - assertThat(threadLocalUserSession.get().isLoggedIn()).isTrue(); } @Test @@ -199,10 +190,9 @@ public class JwtHttpHandlerTest { claims.put("lastRefreshTime", FOUR_MINUTES_AGO); when(jwtSerializer.decode(JWT_TOKEN)).thenReturn(Optional.of(claims)); - underTest.validateToken(request, response); + assertThat(underTest.validateToken(request, response).isPresent()).isTrue(); verify(jwtSerializer, never()).refresh(any(Claims.class), anyInt()); - assertThat(threadLocalUserSession.get().isLoggedIn()).isTrue(); } @Test @@ -215,22 +205,22 @@ public class JwtHttpHandlerTest { claims.put("lastRefreshTime", FOUR_MINUTES_AGO); when(jwtSerializer.decode(JWT_TOKEN)).thenReturn(Optional.of(claims)); - underTest.validateToken(request, response); + assertThat(underTest.validateToken(request, response).isPresent()).isFalse(); verifyCookie(findCookie("JWT-SESSION").get(), null, 0); - assertThat(threadLocalUserSession.get().isLoggedIn()).isFalse(); } @Test - public void validate_token_fails_with_unauthorized_when_user_is_disabled() throws Exception { + public void validate_token_removes_session_when_user_is_disabled() throws Exception { addJwtCookie(); UserDto user = addUser(false); Claims claims = createToken(user.getLogin(), NOW); when(jwtSerializer.decode(JWT_TOKEN)).thenReturn(Optional.of(claims)); - thrown.expect(UnauthorizedException.class); - underTest.validateToken(request, response); + assertThat(underTest.validateToken(request, response).isPresent()).isFalse(); + + verifyCookie(findCookie("JWT-SESSION").get(), null, 0); } @Test @@ -239,10 +229,9 @@ public class JwtHttpHandlerTest { when(jwtSerializer.decode(JWT_TOKEN)).thenReturn(Optional.empty()); - underTest.validateToken(request, response); + assertThat(underTest.validateToken(request, response).isPresent()).isFalse(); verifyCookie(findCookie("JWT-SESSION").get(), null, 0); - assertThat(threadLocalUserSession.get().isLoggedIn()).isFalse(); } @Test @@ -250,7 +239,7 @@ public class JwtHttpHandlerTest { underTest.validateToken(request, response); verifyZeroInteractions(httpSession, jwtSerializer); - assertThat(threadLocalUserSession.get().isLoggedIn()).isFalse(); + assertThat(underTest.validateToken(request, response).isPresent()).isFalse(); } @Test @@ -260,7 +249,7 @@ public class JwtHttpHandlerTest { underTest.validateToken(request, response); verifyZeroInteractions(httpSession, jwtSerializer); - assertThat(threadLocalUserSession.get().isLoggedIn()).isFalse(); + assertThat(underTest.validateToken(request, response).isPresent()).isFalse(); } @Test @@ -287,7 +276,7 @@ public class JwtHttpHandlerTest { underTest.validateToken(request, response); verify(jwtSerializer).refresh(any(Claims.class), anyInt()); - verify(jwtCsrfVerifier).refreshState(response, "CSRF_STATE", 3 * 24 * 60 * 60); + verify(jwtCsrfVerifier).refreshState(response, "CSRF_STATE", 3 * 24 * 60 * 60); } @Test @@ -308,16 +297,6 @@ public class JwtHttpHandlerTest { verifyCookie(findCookie("JWT-SESSION").get(), null, 0); verify(jwtCsrfVerifier).removeState(response); - assertThat(threadLocalUserSession.get().isLoggedIn()).isFalse(); - } - - @Test - public void remove_token_is_removing_user_session() throws Exception { - threadLocalUserSession.set(ServerUserSession.createForUser(dbClient, userDto)); - - underTest.removeToken(response); - - assertThat(threadLocalUserSession.get().isLoggedIn()).isFalse(); } private void verifyToken(JwtSerializer.JwtSession token, int expectedExpirationTime, long expectedRefreshTime) { diff --git a/server/sonar-server/src/test/java/org/sonar/server/authentication/OAuth2ContextFactoryTest.java b/server/sonar-server/src/test/java/org/sonar/server/authentication/OAuth2ContextFactoryTest.java index 9afb606bf41..46dc9582542 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/authentication/OAuth2ContextFactoryTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/authentication/OAuth2ContextFactoryTest.java @@ -25,6 +25,7 @@ import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.sonar.db.user.UserTesting.newUserDto; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -37,7 +38,13 @@ import org.sonar.api.platform.Server; import org.sonar.api.server.authentication.OAuth2IdentityProvider; import org.sonar.api.server.authentication.UserIdentity; import org.sonar.api.utils.MessageException; +import org.sonar.api.utils.System2; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.DbTester; import org.sonar.db.user.UserDto; +import org.sonar.server.user.ThreadLocalUserSession; +import org.sonar.server.user.UserSession; public class OAuth2ContextFactoryTest { @@ -56,6 +63,14 @@ public class OAuth2ContextFactoryTest { .setEmail("john@email.com") .build(); + @Rule + public DbTester dbTester = DbTester.create(System2.INSTANCE); + + DbClient dbClient = dbTester.getDbClient(); + + DbSession dbSession = dbTester.getSession(); + + ThreadLocalUserSession threadLocalUserSession = mock(ThreadLocalUserSession.class); UserIdentityAuthenticator userIdentityAuthenticator = mock(UserIdentityAuthenticator.class); Server server = mock(Server.class); OAuthCsrfVerifier csrfVerifier = mock(OAuthCsrfVerifier.class); @@ -66,12 +81,16 @@ public class OAuth2ContextFactoryTest { HttpSession session = mock(HttpSession.class); OAuth2IdentityProvider identityProvider = mock(OAuth2IdentityProvider.class); - OAuth2ContextFactory underTest = new OAuth2ContextFactory(userIdentityAuthenticator, server, csrfVerifier, jwtHttpHandler); + OAuth2ContextFactory underTest = new OAuth2ContextFactory(dbClient, threadLocalUserSession, userIdentityAuthenticator, server, csrfVerifier, jwtHttpHandler); @Before public void setUp() throws Exception { + UserDto userDto = dbClient.userDao().insert(dbSession, newUserDto()); + dbSession.commit(); + when(request.getSession()).thenReturn(session); when(identityProvider.getKey()).thenReturn(PROVIDER_KEY); + when(userIdentityAuthenticator.authenticate(USER_IDENTITY, identityProvider)).thenReturn(userDto); } @Test @@ -133,6 +152,7 @@ public class OAuth2ContextFactoryTest { verify(userIdentityAuthenticator).authenticate(USER_IDENTITY, identityProvider); verify(jwtHttpHandler).generateToken(any(UserDto.class), eq(response)); + verify(threadLocalUserSession).set(any(UserSession.class)); } @Test diff --git a/server/sonar-server/src/test/java/org/sonar/server/authentication/UserSessionInitializerTest.java b/server/sonar-server/src/test/java/org/sonar/server/authentication/UserSessionInitializerTest.java new file mode 100644 index 00000000000..d78b73df527 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/authentication/UserSessionInitializerTest.java @@ -0,0 +1,198 @@ +/* + * 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 static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; +import static org.sonar.db.user.UserTesting.newUserDto; + +import java.util.Optional; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.sonar.api.config.Settings; +import org.sonar.api.utils.System2; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.DbTester; +import org.sonar.db.user.UserDto; +import org.sonar.server.exceptions.UnauthorizedException; +import org.sonar.server.user.ServerUserSession; +import org.sonar.server.user.ThreadLocalUserSession; +import org.sonar.server.user.UserSession; + +public class UserSessionInitializerTest { + + @Rule + public DbTester dbTester = DbTester.create(System2.INSTANCE); + + DbClient dbClient = dbTester.getDbClient(); + + DbSession dbSession = dbTester.getSession(); + + ThreadLocalUserSession userSession = mock(ThreadLocalUserSession.class); + + HttpServletRequest request = mock(HttpServletRequest.class); + HttpServletResponse response = mock(HttpServletResponse.class); + + JwtHttpHandler jwtHttpHandler = mock(JwtHttpHandler.class); + BasicAuthenticator basicAuthenticator = mock(BasicAuthenticator.class); + + Settings settings = new Settings(); + + UserDto user = newUserDto(); + + UserSessionInitializer underTest = new UserSessionInitializer(dbClient, settings, jwtHttpHandler, basicAuthenticator, userSession); + + @Before + public void setUp() throws Exception { + dbClient.userDao().insert(dbSession, user); + dbSession.commit(); + when(request.getContextPath()).thenReturn(""); + when(request.getRequestURI()).thenReturn("/measures"); + } + + @Test + public void check_urls() throws Exception { + assertPathIsNotIgnored("/"); + assertPathIsNotIgnored("/foo"); + + assertPathIsIgnored("/api/authentication/login"); + assertPathIsIgnored("/batch/index"); + assertPathIsIgnored("/batch/file"); + assertPathIsIgnored("/maintenance/index"); + assertPathIsIgnored("/setup/index"); + assertPathIsIgnored("/sessions/new"); + assertPathIsIgnored("/sessions/logout"); + assertPathIsIgnored("/api/system/db_migration_status"); + assertPathIsIgnored("/api/system/status"); + assertPathIsIgnored("/api/system/migrate_db"); + assertPathIsIgnored("/api/server/index"); + + // exclude static resources + assertPathIsIgnored("/css/style.css"); + assertPathIsIgnored("/fonts/font.ttf"); + assertPathIsIgnored("/images/logo.png"); + assertPathIsIgnored("/js/jquery.js"); + } + + @Test + public void validate_session_from_token() throws Exception { + when(userSession.isLoggedIn()).thenReturn(true); + when(jwtHttpHandler.validateToken(request, response)).thenReturn(Optional.of(user)); + + assertThat(underTest.initUserSession(request, response)).isTrue(); + + verify(jwtHttpHandler).validateToken(request, response); + verify(response, never()).setStatus(anyInt()); + } + + @Test + public void validate_session_from_basic_authentication() throws Exception { + when(userSession.isLoggedIn()).thenReturn(false).thenReturn(true); + when(basicAuthenticator.authenticate(request)).thenReturn(Optional.of(user)); + when(jwtHttpHandler.validateToken(request, response)).thenReturn(Optional.empty()); + + assertThat(underTest.initUserSession(request, response)).isTrue(); + + verify(jwtHttpHandler).validateToken(request, response); + verify(basicAuthenticator).authenticate(request); + verify(userSession).set(any(ServerUserSession.class)); + verify(response, never()).setStatus(anyInt()); + } + + @Test + public void return_code_401_when_invalid_token_exception() throws Exception { + doThrow(new UnauthorizedException("invalid token")).when(jwtHttpHandler).validateToken(request, response); + + assertThat(underTest.initUserSession(request, response)).isTrue(); + + verify(response).setStatus(401); + } + + @Test + public void return_code_401_when_not_authenticated_and_with_force_authentication() throws Exception { + when(userSession.isLoggedIn()).thenReturn(false); + when(basicAuthenticator.authenticate(request)).thenReturn(Optional.empty()); + when(jwtHttpHandler.validateToken(request, response)).thenReturn(Optional.empty()); + settings.setProperty("sonar.forceAuthentication", true); + + assertThat(underTest.initUserSession(request, response)).isTrue(); + + verify(response).setStatus(401); + } + + @Test + public void return_401_and_stop_on_ws() throws Exception { + when(request.getRequestURI()).thenReturn("/api/issues"); + doThrow(new UnauthorizedException("invalid token")).when(jwtHttpHandler).validateToken(request, response); + + assertThat(underTest.initUserSession(request, response)).isFalse(); + + verify(response).setStatus(401); + } + + @Test + public void return_401_and_stop_on_batch_ws() throws Exception { + when(request.getRequestURI()).thenReturn("/batch/global"); + doThrow(new UnauthorizedException("invalid token")).when(jwtHttpHandler).validateToken(request, response); + + assertThat(underTest.initUserSession(request, response)).isFalse(); + + verify(response).setStatus(401); + } + + @Test + public void remove_user_session() throws Exception { + underTest.removeUserSession(); + + verify(userSession).remove(); + } + + private void assertPathIsIgnored(String path) { + when(request.getRequestURI()).thenReturn(path); + + assertThat(underTest.initUserSession(request, response)).isTrue(); + + verifyZeroInteractions(userSession, jwtHttpHandler, basicAuthenticator); + reset(userSession, jwtHttpHandler, basicAuthenticator); + } + + private void assertPathIsNotIgnored(String path) { + when(request.getRequestURI()).thenReturn(path); + when(jwtHttpHandler.validateToken(request, response)).thenReturn(Optional.of(user)); + + assertThat(underTest.initUserSession(request, response)).isTrue(); + + verify(userSession).set(any(UserSession.class)); + reset(userSession, jwtHttpHandler, basicAuthenticator); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/authentication/ValidateJwtTokenFilterTest.java b/server/sonar-server/src/test/java/org/sonar/server/authentication/ValidateJwtTokenFilterTest.java deleted file mode 100644 index 210d940833d..00000000000 --- a/server/sonar-server/src/test/java/org/sonar/server/authentication/ValidateJwtTokenFilterTest.java +++ /dev/null @@ -1,149 +0,0 @@ -/* - * 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 static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyZeroInteractions; -import static org.mockito.Mockito.when; - -import 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.sonar.api.config.Settings; -import org.sonar.server.exceptions.UnauthorizedException; -import org.sonar.server.tester.UserSessionRule; - -public class ValidateJwtTokenFilterTest { - - @Rule - public UserSessionRule userSession = UserSessionRule.standalone(); - - HttpServletRequest request = mock(HttpServletRequest.class); - HttpServletResponse response = mock(HttpServletResponse.class); - FilterChain chain = mock(FilterChain.class); - - JwtHttpHandler jwtHttpHandler = mock(JwtHttpHandler.class); - - Settings settings = new Settings(); - - ValidateJwtTokenFilter underTest = new ValidateJwtTokenFilter(settings, jwtHttpHandler, userSession); - - @Before - public void setUp() throws Exception { - when(request.getContextPath()).thenReturn(""); - when(request.getRequestURI()).thenReturn("/measures"); - } - - @Test - public void do_get_pattern() throws Exception { - assertThat(underTest.doGetPattern().matches("/")).isTrue(); - assertThat(underTest.doGetPattern().matches("/foo")).isTrue(); - - assertThat(underTest.doGetPattern().matches("/api/authentication/login")).isFalse(); - assertThat(underTest.doGetPattern().matches("/batch/index")).isFalse(); - assertThat(underTest.doGetPattern().matches("/batch/file")).isFalse(); - assertThat(underTest.doGetPattern().matches("/batch_bootstrap/index")).isFalse(); - assertThat(underTest.doGetPattern().matches("/maintenance/index")).isFalse(); - assertThat(underTest.doGetPattern().matches("/setup/index")).isFalse(); - assertThat(underTest.doGetPattern().matches("/sessions/new")).isFalse(); - assertThat(underTest.doGetPattern().matches("/sessions/logout")).isFalse(); - assertThat(underTest.doGetPattern().matches("/api/system/db_migration_status")).isFalse(); - assertThat(underTest.doGetPattern().matches("/api/system/status")).isFalse(); - assertThat(underTest.doGetPattern().matches("/api/system/status")).isFalse(); - assertThat(underTest.doGetPattern().matches("/api/system/migrate_db")).isFalse(); - assertThat(underTest.doGetPattern().matches("/api/server/index")).isFalse(); - - // exclude static resources - assertThat(underTest.doGetPattern().matches("/css/style.css")).isFalse(); - assertThat(underTest.doGetPattern().matches("/fonts/font.ttf")).isFalse(); - assertThat(underTest.doGetPattern().matches("/images/logo.png")).isFalse(); - assertThat(underTest.doGetPattern().matches("/js/jquery.js")).isFalse(); - } - - @Test - public void validate_session() throws Exception { - userSession.login("john"); - underTest.doFilter(request, response, chain); - - verify(jwtHttpHandler).validateToken(request, response); - verify(chain).doFilter(request, response); - } - - @Test - public void return_code_401_when_invalid_token_exception() throws Exception { - userSession.login("john"); - doThrow(new UnauthorizedException("invalid token")).when(jwtHttpHandler).validateToken(request, response); - - underTest.doFilter(request, response, chain); - - verify(response).setStatus(401); - verify(chain).doFilter(request, response); - } - - @Test - public void return_code_401_when_not_authenticated_and_with_force_authentication() throws Exception { - settings.setProperty("sonar.forceAuthentication", true); - userSession.anonymous(); - - underTest.doFilter(request, response, chain); - - verify(response).setStatus(401); - verify(chain).doFilter(request, response); - } - - @Test - public void return_401_and_stop_on_ws() throws Exception { - when(request.getRequestURI()).thenReturn("/api/issues"); - doThrow(new UnauthorizedException("invalid token")).when(jwtHttpHandler).validateToken(request, response); - - underTest.doFilter(request, response, chain); - - verify(response).setStatus(401); - verifyZeroInteractions(chain); - } - - @Test - public void return_401_and_stop_on_batch_ws() throws Exception { - when(request.getRequestURI()).thenReturn("/batch/index"); - doThrow(new UnauthorizedException("invalid token")).when(jwtHttpHandler).validateToken(request, response); - - underTest.doFilter(request, response, chain); - - verify(response).setStatus(401); - verifyZeroInteractions(chain); - } - - @Test - public void ignore_old_batch_ws() throws Exception { - when(request.getRequestURI()).thenReturn("/batch/name.jar"); - - underTest.doFilter(request, response, chain); - - verify(chain).doFilter(request, response); - verifyZeroInteractions(jwtHttpHandler, response); - } -} diff --git a/server/sonar-server/src/test/java/org/sonar/server/authentication/ws/LoginActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/authentication/ws/LoginActionTest.java new file mode 100644 index 00000000000..3f6fd7c8fee --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/authentication/ws/LoginActionTest.java @@ -0,0 +1,149 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +package org.sonar.server.authentication.ws; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.sonar.api.utils.System2; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.DbTester; +import org.sonar.db.user.UserDto; +import org.sonar.db.user.UserTesting; +import org.sonar.server.authentication.CredentialsAuthenticator; +import org.sonar.server.authentication.JwtHttpHandler; +import org.sonar.server.exceptions.UnauthorizedException; +import org.sonar.server.user.ThreadLocalUserSession; + +public class LoginActionTest { + + static final String LOGIN = "LOGIN"; + static final String PASSWORD = "PASSWORD"; + + @Rule + public DbTester dbTester = DbTester.create(System2.INSTANCE); + + DbClient dbClient = dbTester.getDbClient(); + + DbSession dbSession = dbTester.getSession(); + + ThreadLocalUserSession threadLocalUserSession = new ThreadLocalUserSession(); + + HttpServletRequest request = mock(HttpServletRequest.class); + HttpServletResponse response = mock(HttpServletResponse.class); + FilterChain chain = mock(FilterChain.class); + + CredentialsAuthenticator credentialsAuthenticator = mock(CredentialsAuthenticator.class); + JwtHttpHandler jwtHttpHandler = mock(JwtHttpHandler.class); + + UserDto user = UserTesting.newUserDto().setLogin(LOGIN); + + LoginAction underTest = new LoginAction(dbClient, credentialsAuthenticator, jwtHttpHandler, threadLocalUserSession); + + @Before + public void setUp() throws Exception { + threadLocalUserSession.remove(); + dbClient.userDao().insert(dbSession, user); + dbSession.commit(); + } + + @Test + public void do_get_pattern() throws Exception { + assertThat(underTest.doGetPattern().matches("/api/authentication/login")).isTrue(); + assertThat(underTest.doGetPattern().matches("/api/authentication/logout")).isFalse(); + assertThat(underTest.doGetPattern().matches("/foo")).isFalse(); + } + + @Test + public void do_authenticate() throws Exception { + when(credentialsAuthenticator.authenticate(LOGIN, PASSWORD, request)).thenReturn(user); + + executeRequest(LOGIN, PASSWORD); + + assertThat(threadLocalUserSession.isLoggedIn()).isTrue(); + verify(credentialsAuthenticator).authenticate(LOGIN, PASSWORD, request); + verify(jwtHttpHandler).generateToken(user, response); + verifyZeroInteractions(chain); + } + + @Test + public void ignore_get_request() throws Exception { + when(request.getMethod()).thenReturn("GET"); + + underTest.doFilter(request, response, chain); + + verifyZeroInteractions(credentialsAuthenticator, jwtHttpHandler, chain); + } + + @Test + public void return_authorized_code_when_unauthorized_exception_is_thrown() throws Exception { + doThrow(new UnauthorizedException()).when(credentialsAuthenticator).authenticate(LOGIN, PASSWORD, request); + + executeRequest(LOGIN, PASSWORD); + + verify(response).setStatus(401); + assertThat(threadLocalUserSession.isLoggedIn()).isFalse(); + } + + @Test + public void return_unauthorized_code_when_no_login() throws Exception { + executeRequest(null, PASSWORD); + verify(response).setStatus(401); + } + + @Test + public void return_unauthorized_code_when_empty_login() throws Exception { + executeRequest("", PASSWORD); + verify(response).setStatus(401); + } + + @Test + public void return_unauthorized_code_when_no_password() throws Exception { + executeRequest(LOGIN, null); + verify(response).setStatus(401); + } + + @Test + public void return_unauthorized_code_when_empty_password() throws Exception { + executeRequest(LOGIN, ""); + verify(response).setStatus(401); + } + + private void executeRequest(String login, String password) throws IOException, ServletException { + when(request.getMethod()).thenReturn("POST"); + when(request.getParameter("login")).thenReturn(login); + when(request.getParameter("password")).thenReturn(password); + underTest.doFilter(request, response, chain); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/user/UserSessionFilterTest.java b/server/sonar-server/src/test/java/org/sonar/server/user/UserSessionFilterTest.java index 06a8be8d1fc..1e75fe2279a 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/user/UserSessionFilterTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/user/UserSessionFilterTest.java @@ -19,66 +19,76 @@ */ package org.sonar.server.user; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + import java.io.IOException; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; -import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; -import org.junit.After; +import javax.servlet.http.HttpServletResponse; import org.junit.Before; import org.junit.Test; import org.sonar.core.platform.ComponentContainer; +import org.sonar.server.authentication.UserSessionInitializer; import org.sonar.server.platform.Platform; -import org.sonar.server.tester.MockUserSession; - -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 UserSessionFilterTest { - private ThreadLocalUserSession threadLocalUserSession = new ThreadLocalUserSession(); - private Platform platform = mock(Platform.class); - private ComponentContainer componentContainer = mock(ComponentContainer.class); - private HttpServletRequest httpRequest = mock(HttpServletRequest.class); - private ServletResponse httpResponse = mock(ServletResponse.class); - private FilterChain chain = mock(FilterChain.class); + + UserSessionInitializer userSessionInitializer = mock(UserSessionInitializer.class); + Platform platform = mock(Platform.class); + ComponentContainer componentContainer = mock(ComponentContainer.class); + + HttpServletRequest request = mock(HttpServletRequest.class); + HttpServletResponse response = mock(HttpServletResponse.class); + FilterChain chain = mock(FilterChain.class); + + UserSessionFilter underTest = new UserSessionFilter(platform); @Before public void setUp() { when(platform.getContainer()).thenReturn(componentContainer); - // for test isolation - threadLocalUserSession.remove(); } - @After - public void tearDown() { - threadLocalUserSession.remove(); + @Test + public void cleanup_user_session_after_request_handling() throws IOException, ServletException { + when(componentContainer.getComponentByType(UserSessionInitializer.class)).thenReturn(userSessionInitializer); + when(userSessionInitializer.initUserSession(request, response)).thenReturn(true); + + underTest.doFilter(request, response, chain); + + verify(chain).doFilter(request, response); + verify(userSessionInitializer).initUserSession(request, response); + verify(userSessionInitializer).removeUserSession(); } @Test - public void should_cleanup_user_session_after_request_handling() throws IOException, ServletException { - when(componentContainer.getComponentByType(ThreadLocalUserSession.class)).thenReturn(threadLocalUserSession); + public void stop_when_user_session_return_false() throws Exception { + when(componentContainer.getComponentByType(UserSessionInitializer.class)).thenReturn(userSessionInitializer); + when(userSessionInitializer.initUserSession(request, response)).thenReturn(false); - threadLocalUserSession.set(new MockUserSession("karadoc").setUserId(123)); - assertThat(threadLocalUserSession.hasSession()).isTrue(); - UserSessionFilter filter = new UserSessionFilter(platform); - filter.doFilter(httpRequest, httpResponse, chain); + underTest.doFilter(request, response, chain); - verify(chain).doFilter(httpRequest, httpResponse); - assertThat(threadLocalUserSession.hasSession()).isFalse(); + verify(chain, never()).doFilter(request, response); + verify(userSessionInitializer).initUserSession(request, response); + verify(userSessionInitializer).removeUserSession(); } @Test - public void does_not_fail_if_container_has_no_ThreadLocalUserSession() throws Exception { - UserSessionFilter filter = new UserSessionFilter(platform); - filter.doFilter(httpRequest, httpResponse, chain); + public void does_nothing_when_not_initialized() throws Exception { + underTest.doFilter(request, response, chain); + + verify(chain).doFilter(request, response); + verifyZeroInteractions(userSessionInitializer); } @Test public void just_for_fun_and_coverage() throws ServletException { - UserSessionFilter filter = new UserSessionFilter(platform); + UserSessionFilter filter = new UserSessionFilter(); filter.init(mock(FilterConfig.class)); filter.destroy(); // do not fail diff --git a/server/sonar-server/src/test/java/org/sonar/server/usertoken/UserTokenAuthenticatorTest.java b/server/sonar-server/src/test/java/org/sonar/server/usertoken/UserTokenAuthenticatorTest.java index 74de38b0037..e0b4516aba4 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/usertoken/UserTokenAuthenticatorTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/usertoken/UserTokenAuthenticatorTest.java @@ -19,7 +19,12 @@ */ package org.sonar.server.usertoken; -import com.google.common.base.Optional; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.sonar.db.user.UserTokenTesting.newUserToken; + +import java.util.Optional; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; @@ -28,12 +33,6 @@ import org.sonar.db.DbClient; import org.sonar.db.DbSession; import org.sonar.db.DbTester; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; -import static org.sonar.db.user.UserTokenTesting.newUserToken; - - public class UserTokenAuthenticatorTest { static final String GRACE_HOPPER = "grace.hopper"; static final String ADA_LOVELACE = "ada.lovelace"; diff --git a/server/sonar-web/src/main/webapp/WEB-INF/lib/authenticated_system.rb b/server/sonar-web/src/main/webapp/WEB-INF/lib/authenticated_system.rb index 02f70471036..c9be7f12c8a 100644 --- a/server/sonar-web/src/main/webapp/WEB-INF/lib/authenticated_system.rb +++ b/server/sonar-web/src/main/webapp/WEB-INF/lib/authenticated_system.rb @@ -8,7 +8,7 @@ module AuthenticatedSystem # Accesses the current user from the session. # Future calls avoid the database because nil is not equal to false. def current_user - @current_user ||= (login_from_java_user_session || login_from_basic_auth) unless @current_user == false + @current_user ||= login_from_java_user_session unless @current_user == false end # Store the given user @@ -124,31 +124,6 @@ module AuthenticatedSystem self.current_user = User.find_by_id(user_id) if user_id end - # Called from #current_user. Now, attempt to login by basic authentication information. - def login_from_basic_auth - authenticate_with_http_basic do |login, password| - # The access token is sent as the login of Basic authentication. To distinguish with regular logins, - # the convention is that the password is empty - if password.empty? && login.present? - # authentication by access token - token_authenticator = Java::OrgSonarServerPlatform::Platform.component(Java::OrgSonarServerUsertoken::UserTokenAuthenticator.java_class) - authenticated_login = token_authenticator.authenticate(login) - if authenticated_login.isPresent() - user = User.find_active_by_login(authenticated_login.get()) - if user - user.token_authenticated=true - result = user - end - end - else - # regular Basic authentication with login and password - result = User.authenticate(login, password, servlet_request) - end - raise Errors::AccessDenied unless login.blank? || result - self.current_user = result - end - end - # # Logout #