From: Julien Lancelot Date: Fri, 17 Jun 2016 16:01:48 +0000 (+0200) Subject: SONAR-7732 Authentication is now done in Java X-Git-Tag: 6.0-RC1~240 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=102faa7f46c509fd2bf7f3d933b78375ca2f4166;p=sonarqube.git SONAR-7732 Authentication is now done in Java --- diff --git a/it/it-plugins/security-plugin/src/main/java/FakeAuthenticator.java b/it/it-plugins/security-plugin/src/main/java/FakeAuthenticator.java index 6455d260b89..8e75d9c1fcf 100644 --- a/it/it-plugins/security-plugin/src/main/java/FakeAuthenticator.java +++ b/it/it-plugins/security-plugin/src/main/java/FakeAuthenticator.java @@ -90,7 +90,7 @@ public class FakeAuthenticator implements LoginPasswordAuthenticator { private void checkExistence(String username) { if (!data.containsKey(username + ".password")) { - throw new RuntimeException("No such user"); + throw new IllegalArgumentException("No such user : " + username); } } diff --git a/it/it-tests/src/test/java/it/user/RailsExternalAuthenticationTest.java b/it/it-tests/src/test/java/it/user/RailsExternalAuthenticationTest.java index 6ce7c2ec88f..c0692cd7820 100644 --- a/it/it-tests/src/test/java/it/user/RailsExternalAuthenticationTest.java +++ b/it/it-tests/src/test/java/it/user/RailsExternalAuthenticationTest.java @@ -19,6 +19,13 @@ */ package it.user; +import static java.net.HttpURLConnection.HTTP_OK; +import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.fail; +import static util.ItUtils.pluginArtifact; +import static util.ItUtils.setServerProperty; + import com.google.common.collect.ImmutableMap; import com.google.common.collect.Maps; import com.sonar.orchestrator.Orchestrator; @@ -51,13 +58,6 @@ import org.sonarqube.ws.client.WsResponse; import util.QaOnly; import util.selenium.SeleneseTest; -import static java.net.HttpURLConnection.HTTP_OK; -import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.Assert.fail; -import static util.ItUtils.pluginArtifact; -import static util.ItUtils.setServerProperty; - /** * Test deprecated authentication done by Rails. It's kept has every features has not bee migrated to java yet. * 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 new file mode 100644 index 00000000000..770cd6329ec --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/AuthLoginAction.java @@ -0,0 +1,93 @@ +/* + * 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 b2740cc6b9b..a55033f0e7e 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 @@ -34,10 +34,12 @@ public class AuthenticationModule extends Module { OAuth2ContextFactory.class, UserIdentityAuthenticator.class, OAuthCsrfVerifier.class, - GenerateJwtTokenFilter.class, ValidateJwtTokenFilter.class, JwtSerializer.class, JwtHttpHandler.class, - JwtCsrfVerifier.class); + JwtCsrfVerifier.class, + AuthLoginAction.class, + CredentialsAuthenticator.class, + RealmAuthenticator.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 9261f37edfd..ea7df897a90 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,15 +24,18 @@ 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.user.UserDto; public class BaseContextFactory { private final UserIdentityAuthenticator userIdentityAuthenticator; private final Server server; + private final JwtHttpHandler jwtHttpHandler; - public BaseContextFactory(UserIdentityAuthenticator userIdentityAuthenticator, Server server) { + public BaseContextFactory(UserIdentityAuthenticator userIdentityAuthenticator, Server server, JwtHttpHandler jwtHttpHandler) { this.userIdentityAuthenticator = userIdentityAuthenticator; this.server = server; + this.jwtHttpHandler = jwtHttpHandler; } public BaseIdentityProvider.Context newContext(HttpServletRequest request, HttpServletResponse response, BaseIdentityProvider identityProvider) { @@ -67,7 +70,8 @@ public class BaseContextFactory { @Override public void authenticate(UserIdentity userIdentity) { - userIdentityAuthenticator.authenticate(userIdentity, identityProvider, request, response); + UserDto userDto = userIdentityAuthenticator.authenticate(userIdentity, identityProvider); + jwtHttpHandler.generateToken(userDto, response); } } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/CredentialsAuthenticator.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/CredentialsAuthenticator.java new file mode 100644 index 00000000000..3706b8a072c --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/CredentialsAuthenticator.java @@ -0,0 +1,73 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +package org.sonar.server.authentication; + +import static org.sonar.db.user.UserDto.encryptPassword; + +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; + +public class CredentialsAuthenticator { + + private final DbClient dbClient; + private final RealmAuthenticator externalAuthenticator; + + public CredentialsAuthenticator(DbClient dbClient, RealmAuthenticator externalAuthenticator) { + this.dbClient = dbClient; + this.externalAuthenticator = externalAuthenticator; + } + + public UserDto authenticate(String userLogin, String userPassword, HttpServletRequest request) { + DbSession dbSession = dbClient.openSession(false); + try { + return authenticate(dbSession, userLogin, userPassword, request); + } finally { + dbClient.closeSession(dbSession); + } + } + + private UserDto authenticate(DbSession dbSession, String userLogin, String userPassword, HttpServletRequest request) { + UserDto user = dbClient.userDao().selectActiveUserByLogin(dbSession, userLogin); + if (user != null && user.isLocal()) { + return authenticateFromDb(user, userPassword); + } + Optional userDto = externalAuthenticator.authenticate(userLogin, userPassword, request); + if (userDto.isPresent()) { + return userDto.get(); + } + throw new UnauthorizedException(); + } + + private static UserDto authenticateFromDb(UserDto userDto, String userPassword) { + String cryptedPassword = userDto.getCryptedPassword(); + String salt = userDto.getSalt(); + if (cryptedPassword == null || salt == null + || !cryptedPassword.equals(encryptPassword(userPassword, salt))) { + throw new UnauthorizedException(); + } + return userDto; + } + +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/GenerateJwtTokenFilter.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/GenerateJwtTokenFilter.java deleted file mode 100644 index 24f206b64b8..00000000000 --- a/server/sonar-server/src/main/java/org/sonar/server/authentication/GenerateJwtTokenFilter.java +++ /dev/null @@ -1,141 +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 java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.PrintWriter; -import javax.servlet.FilterChain; -import javax.servlet.FilterConfig; -import javax.servlet.ServletException; -import javax.servlet.ServletOutputStream; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.WriteListener; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpServletResponseWrapper; -import org.sonar.api.web.ServletFilter; -import org.sonar.server.user.UserSession; - -/** - * This filter creates web session when user authenticates on URL "/sessions/login". - * The generated session is server stateless. - */ -public class GenerateJwtTokenFilter extends ServletFilter { - - private static final String POST = "POST"; - - private final JwtHttpHandler jwtHttpHandler; - private final UserSession userSession; - - public GenerateJwtTokenFilter(JwtHttpHandler jwtHttpHandler, UserSession userSession) { - this.jwtHttpHandler = jwtHttpHandler; - this.userSession = userSession; - } - - @Override - public UrlPattern doGetPattern() { - return UrlPattern.create("/sessions/login"); - } - - @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)) { - BufferResponseWrapper wrapper = new BufferResponseWrapper(response); - chain.doFilter(request, wrapper); - if (userSession.isLoggedIn()) { - jwtHttpHandler.generateToken(userSession.getLogin(), response); - } - response.getOutputStream().write(wrapper.getWrapperBytes()); - } else { - chain.doFilter(request, response); - } - } - - @Override - public void init(FilterConfig filterConfig) throws ServletException { - // Nothing to do - } - - @Override - public void destroy() { - // Nothing to do - } - - /** - * As the RackFilter is executed before this filter, the reponse is commited and it's not possible anymore to add cookie. - * So we're create a buffer response wrapper that will buffer the dat that should be send to the browser in order to not commit the response. - * It's then possible to add cookie before flushing data to the browser. - * - * See - * - * Note : this must be removed when authentication will not use rails anymore - */ - private static final class BufferResponseWrapper extends HttpServletResponseWrapper { - - private BufferServletOutputStream stream = new BufferServletOutputStream(); - - BufferResponseWrapper(HttpServletResponse httpServletResponse) { - super(httpServletResponse); - } - - @Override - public ServletOutputStream getOutputStream() throws IOException { - return stream; - } - - @Override - public PrintWriter getWriter() throws IOException { - return new PrintWriter(stream); - } - - byte[] getWrapperBytes() { - return stream.getBytes(); - } - } - - private static final class BufferServletOutputStream extends ServletOutputStream { - private ByteArrayOutputStream out = new ByteArrayOutputStream(); - - @Override - public void write(int b) throws IOException { - out.write(b); - } - - byte[] getBytes() { - return out.toByteArray(); - } - - @Override - public boolean isReady() { - return false; - } - - @Override - public void setWriteListener(WriteListener listener) { - // Nothing to do - } - } -} 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 4f77ffe8c07..d42349b5f6e 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,6 +23,7 @@ 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; @@ -40,6 +41,9 @@ 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 { @@ -59,8 +63,6 @@ public class JwtHttpHandler { // The value must be lower than sessionTimeoutInSeconds private static final int SESSION_REFRESH_IN_SECONDS = 5 * 60; - private static final String RAILS_USER_ID_SESSION = "user_id"; - private final System2 system2; private final DbClient dbClient; private final Server server; @@ -69,65 +71,85 @@ 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) { + public JwtHttpHandler(System2 system2, DbClient dbClient, Server server, Settings settings, JwtSerializer jwtSerializer, JwtCsrfVerifier jwtCsrfVerifier, + ThreadLocalUserSession threadLocalUserSession) { this.jwtSerializer = jwtSerializer; this.server = server; this.dbClient = dbClient; this.system2 = system2; this.sessionTimeoutInSeconds = getSessionTimeoutInSeconds(settings); this.jwtCsrfVerifier = jwtCsrfVerifier; + this.threadLocalUserSession = threadLocalUserSession; } - void generateToken(String userLogin, HttpServletResponse response) { + void generateToken(UserDto user, HttpServletResponse response) { String csrfState = jwtCsrfVerifier.generateState(response, sessionTimeoutInSeconds); String token = jwtSerializer.encode(new JwtSerializer.JwtSession( - userLogin, + user.getLogin(), sessionTimeoutInSeconds, ImmutableMap.of( 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)); + } + } + + private void validate(HttpServletRequest request, HttpServletResponse response) { + Optional token = getTokenFromCookie(request); + if (!token.isPresent()) { + return; + } + validateToken(token.get(), request, response); + } + + private static Optional getTokenFromCookie(HttpServletRequest request) { Optional jwtCookie = findCookie(JWT_COOKIE, request); - if (jwtCookie.isPresent()) { - Cookie cookie = jwtCookie.get(); - String token = cookie.getValue(); - if (!isNullOrEmpty(token)) { - validateToken(token, request, response); - } + if (!jwtCookie.isPresent()) { + return Optional.empty(); + } + Cookie cookie = jwtCookie.get(); + String token = cookie.getValue(); + if (isNullOrEmpty(token)) { + return Optional.empty(); } + return Optional.of(token); } private void validateToken(String tokenEncoded, HttpServletRequest request, HttpServletResponse response) { Optional claims = jwtSerializer.decode(tokenEncoded); if (!claims.isPresent()) { - removeSession(request, response); + removeToken(response); return; } Date now = new Date(system2.now()); - Claims token = claims.get(); if (now.after(DateUtils.addSeconds(token.getIssuedAt(), SESSION_DISCONNECT_IN_SECONDS))) { - removeSession(request, response); + removeToken(response); return; } - - Optional user = selectUserFromDb(token.getSubject()); - if (!user.isPresent()) { - removeSession(request, response); - return; - } - jwtCsrfVerifier.verifyState(request, (String) token.get(CSRF_JWT_PARAM)); - request.getSession().setAttribute(RAILS_USER_ID_SESSION, user.get().getId()); + if (now.after(DateUtils.addSeconds(getLastRefreshDate(token), SESSION_REFRESH_IN_SECONDS))) { refreshToken(token, response); } + + Optional user = selectUserFromDb(token.getSubject()); + if (!user.isPresent()) { + removeToken(response); + throw new UnauthorizedException("User does not exist"); + } + threadLocalUserSession.set(createForUser(dbClient, user.get())); } private static Date getLastRefreshDate(Claims token) { @@ -142,10 +164,10 @@ public class JwtHttpHandler { jwtCsrfVerifier.refreshState(response, (String) token.get(CSRF_JWT_PARAM), sessionTimeoutInSeconds); } - private void removeSession(HttpServletRequest request, HttpServletResponse response) { - request.getSession().removeAttribute(RAILS_USER_ID_SESSION); + 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 6135d6aff49..6f5948cc616 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,17 +30,20 @@ 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.user.UserDto; public class OAuth2ContextFactory { private final UserIdentityAuthenticator userIdentityAuthenticator; private final Server server; private final OAuthCsrfVerifier csrfVerifier; + private final JwtHttpHandler jwtHttpHandler; - public OAuth2ContextFactory(UserIdentityAuthenticator userIdentityAuthenticator, Server server, OAuthCsrfVerifier csrfVerifier) { + public OAuth2ContextFactory(UserIdentityAuthenticator userIdentityAuthenticator, Server server, OAuthCsrfVerifier csrfVerifier, JwtHttpHandler jwtHttpHandler) { this.userIdentityAuthenticator = userIdentityAuthenticator; this.server = server; this.csrfVerifier = csrfVerifier; + this.jwtHttpHandler = jwtHttpHandler; } public OAuth2IdentityProvider.InitContext newContext(HttpServletRequest request, HttpServletResponse response, OAuth2IdentityProvider identityProvider) { @@ -112,7 +115,8 @@ public class OAuth2ContextFactory { @Override public void authenticate(UserIdentity userIdentity) { - userIdentityAuthenticator.authenticate(userIdentity, identityProvider, request, response); + UserDto userDto = userIdentityAuthenticator.authenticate(userIdentity, identityProvider); + jwtHttpHandler.generateToken(userDto, response); } } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/RealmAuthenticator.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/RealmAuthenticator.java new file mode 100644 index 00000000000..d2ee0426bc4 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/RealmAuthenticator.java @@ -0,0 +1,159 @@ +/* + * 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.Objects.requireNonNull; +import static org.apache.commons.lang.StringUtils.trimToNull; +import static org.elasticsearch.common.Strings.isNullOrEmpty; +import static org.sonar.api.CoreProperties.CORE_AUTHENTICATOR_CREATE_USERS; +import static org.sonar.server.user.UserUpdater.SQ_AUTHORITY; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Locale; +import java.util.Optional; +import javax.servlet.http.HttpServletRequest; +import org.sonar.api.Startable; +import org.sonar.api.config.Settings; +import org.sonar.api.security.Authenticator; +import org.sonar.api.security.ExternalGroupsProvider; +import org.sonar.api.security.ExternalUsersProvider; +import org.sonar.api.security.SecurityRealm; +import org.sonar.api.security.UserDetails; +import org.sonar.api.server.authentication.Display; +import org.sonar.api.server.authentication.IdentityProvider; +import org.sonar.api.server.authentication.UserIdentity; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; +import org.sonar.db.user.UserDto; +import org.sonar.server.exceptions.UnauthorizedException; +import org.sonar.server.user.SecurityRealmFactory; + +public class RealmAuthenticator implements Startable { + + private static final Logger LOG = Loggers.get(RealmAuthenticator.class); + + private final Settings settings; + private final SecurityRealmFactory securityRealmFactory; + private final UserIdentityAuthenticator userIdentityAuthenticator; + + private SecurityRealm realm; + private Authenticator authenticator; + private ExternalUsersProvider externalUsersProvider; + private ExternalGroupsProvider externalGroupsProvider; + + public RealmAuthenticator(Settings settings, SecurityRealmFactory securityRealmFactory, UserIdentityAuthenticator userIdentityAuthenticator) { + this.settings = settings; + this.securityRealmFactory = securityRealmFactory; + this.userIdentityAuthenticator = userIdentityAuthenticator; + } + + @Override + public void start() { + realm = securityRealmFactory.getRealm(); + if (realm != null) { + authenticator = requireNonNull(realm.doGetAuthenticator(), "No authenticator available"); + externalUsersProvider = requireNonNull(realm.getUsersProvider(), "No users provider available"); + externalGroupsProvider = realm.getGroupsProvider(); + } + } + + public Optional authenticate(String userLogin, String userPassword, HttpServletRequest request) { + if (realm == null) { + return Optional.empty(); + } + return Optional.of(doAuthenticate(getLogin(userLogin), userPassword, request)); + } + + private UserDto doAuthenticate(String userLogin, String userPassword, HttpServletRequest request) { + try { + ExternalUsersProvider.Context externalUsersProviderContext = new ExternalUsersProvider.Context(userLogin, request); + UserDetails details = externalUsersProvider.doGetUserDetails(externalUsersProviderContext); + if (details == null) { + throw new UnauthorizedException("No user details"); + } + Authenticator.Context authenticatorContext = new Authenticator.Context(userLogin, userPassword, request); + boolean status = authenticator.doAuthenticate(authenticatorContext); + if (!status) { + throw new UnauthorizedException("Fail to authenticate from external provider"); + } + return synchronize(userLogin, details, request); + } catch (Exception e) { + // It seems that with Realm API it's expected to log the error and to not authenticate the user + LOG.error("Error during authentication", e); + throw new UnauthorizedException(); + } + } + + private UserDto synchronize(String userLogin, UserDetails details, HttpServletRequest request) { + String name = details.getName(); + UserIdentity.Builder userIdentityBuilder = UserIdentity.builder() + .setLogin(userLogin) + .setName(isNullOrEmpty(name) ? userLogin : name) + .setEmail(trimToNull(details.getEmail())) + .setProviderLogin(userLogin); + if (externalGroupsProvider != null) { + ExternalGroupsProvider.Context context = new ExternalGroupsProvider.Context(userLogin, request); + Collection groups = externalGroupsProvider.doGetGroups(context); + userIdentityBuilder.setGroups(new HashSet<>(groups)); + } + return userIdentityAuthenticator.authenticate(userIdentityBuilder.build(), new ExternalIdentityProvider()); + } + + private String getLogin(String userLogin) { + if (settings.getBoolean("sonar.authenticator.downcase")) { + return userLogin.toLowerCase(Locale.ENGLISH); + } + return userLogin; + } + + private class ExternalIdentityProvider implements IdentityProvider { + @Override + public String getKey() { + return SQ_AUTHORITY; + } + + @Override + public String getName() { + return SQ_AUTHORITY; + } + + @Override + public Display getDisplay() { + return null; + } + + @Override + public boolean isEnabled() { + return true; + } + + @Override + public boolean allowsUsersToSignUp() { + return settings.getBoolean(CORE_AUTHENTICATOR_CREATE_USERS); + } + } + + @Override + public void stop() { + // Nothing to do + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/UserIdentityAuthenticator.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/UserIdentityAuthenticator.java index 0e8069926d7..76a71966d90 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/authentication/UserIdentityAuthenticator.java +++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/UserIdentityAuthenticator.java @@ -31,8 +31,6 @@ import java.util.HashSet; import java.util.Map; import java.util.Set; import javax.annotation.Nonnull; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; import org.sonar.api.server.authentication.IdentityProvider; import org.sonar.api.server.authentication.UnauthorizedException; import org.sonar.api.server.authentication.UserIdentity; @@ -54,20 +52,14 @@ public class UserIdentityAuthenticator { private final DbClient dbClient; private final UserUpdater userUpdater; - private final JwtHttpHandler jwtHttpHandler; - public UserIdentityAuthenticator(DbClient dbClient, UserUpdater userUpdater, JwtHttpHandler jwtHttpHandler) { + public UserIdentityAuthenticator(DbClient dbClient, UserUpdater userUpdater) { this.dbClient = dbClient; this.userUpdater = userUpdater; - this.jwtHttpHandler = jwtHttpHandler; } - public void authenticate(UserIdentity user, IdentityProvider provider, HttpServletRequest request, HttpServletResponse response) { - UserDto userDb = register(user, provider); - - // hack to disable Ruby on Rails authentication - request.getSession().setAttribute("user_id", userDb.getId()); - jwtHttpHandler.generateToken(userDb.getLogin(), response); + public UserDto authenticate(UserIdentity user, IdentityProvider provider) { + return register(user, provider); } private UserDto register(UserIdentity user, IdentityProvider provider) { @@ -101,8 +93,7 @@ public class UserIdentityAuthenticator { .setLogin(userLogin) .setEmail(user.getEmail()) .setName(user.getName()) - .setExternalIdentity(new ExternalIdentity(provider.getKey(), user.getProviderLogin())) - ); + .setExternalIdentity(new ExternalIdentity(provider.getKey(), user.getProviderLogin()))); UserDto newUser = dbClient.userDao().selectOrFailByLogin(dbSession, userLogin); syncGroups(dbSession, user, newUser); return newUser; @@ -138,27 +129,19 @@ public class UserIdentityAuthenticator { } private void addGroups(DbSession dbSession, UserDto userDto, Collection groupsToAdd, Map groupsByName) { - if (!groupsToAdd.isEmpty()) { - for (String groupToAdd : groupsToAdd) { - GroupDto groupDto = groupsByName.get(groupToAdd); - if (groupDto != null) { - LOGGER.debug("Adding group '{}' to user '{}'", groupDto.getName(), userDto.getLogin()); - dbClient.userGroupDao().insert(dbSession, new UserGroupDto().setGroupId(groupDto.getId()).setUserId(userDto.getId())); - } - } - } + groupsToAdd.stream().map(groupsByName::get).filter(groupDto -> groupDto != null).forEach( + groupDto -> { + LOGGER.debug("Adding group '{}' to user '{}'", groupDto.getName(), userDto.getLogin()); + dbClient.userGroupDao().insert(dbSession, new UserGroupDto().setGroupId(groupDto.getId()).setUserId(userDto.getId())); + }); } private void removeGroups(DbSession dbSession, UserDto userDto, Collection groupsToRemove, Map groupsByName) { - if (!groupsToRemove.isEmpty()) { - for (String groupToRemove : groupsToRemove) { - GroupDto groupDto = groupsByName.get(groupToRemove); - if (groupDto != null) { - LOGGER.debug("Removing group '{}' from user '{}'", groupDto.getName(), userDto.getLogin()); - dbClient.userGroupDao().delete(dbSession, new UserGroupDto().setGroupId(groupDto.getId()).setUserId(userDto.getId())); - } - } - } + groupsToRemove.stream().map(groupsByName::get).filter(groupDto -> groupDto != null).forEach( + groupDto -> { + LOGGER.debug("Removing group '{}' from user '{}'", groupDto.getName(), userDto.getLogin()); + dbClient.userGroupDao().delete(dbSession, new UserGroupDto().setGroupId(groupDto.getId()).setUserId(userDto.getId())); + }); } private enum GroupDtoToName implements Function { 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 index 42ad84f697e..fd3878b665e 100644 --- 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 @@ -20,9 +20,14 @@ 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; @@ -30,17 +35,34 @@ 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(JwtHttpHandler jwtHttpHandler) { + public ValidateJwtTokenFilter(Settings settings, JwtHttpHandler jwtHttpHandler, UserSession userSession) { + this.settings = settings; this.jwtHttpHandler = jwtHttpHandler; + this.userSession = userSession; } @Override @@ -48,6 +70,7 @@ public class ValidateJwtTokenFilter extends ServletFilter { return UrlPattern.builder() .includes("/*") .excludes(staticResourcePatterns()) + .excludes(SKIPPED_URLS) .build(); } @@ -55,15 +78,41 @@ public class ValidateJwtTokenFilter extends ServletFilter { 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) { - response.setStatus(e.httpCode()); + 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 diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/ws/AuthenticationWs.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/ws/AuthenticationWs.java index 79600e01b5e..f35a25e1b0d 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/authentication/ws/AuthenticationWs.java +++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/ws/AuthenticationWs.java @@ -22,14 +22,16 @@ package org.sonar.server.authentication.ws; import com.google.common.io.Resources; import org.sonar.api.server.ws.RailsHandler; import org.sonar.api.server.ws.WebService; +import org.sonar.server.ws.ServletFilterHandler; public class AuthenticationWs implements WebService { @Override public void define(Context context) { NewController controller = context.createController("api/authentication"); - controller.setDescription("Check authentication credentials."); + controller.setDescription("Handle authentication."); + defineLoginAction(controller); defineValidateAction(controller); controller.done(); @@ -37,7 +39,7 @@ public class AuthenticationWs implements WebService { private void defineValidateAction(NewController controller) { NewAction action = controller.createAction("validate") - .setDescription("Check credentials") + .setDescription("Check credentials.") .setSince("3.3") .setHandler(RailsHandler.INSTANCE) .setResponseExample(Resources.getResource(this.getClass(), "example-validate.json")); @@ -45,4 +47,18 @@ public class AuthenticationWs implements WebService { RailsHandler.addFormatParam(action); } + private static void defineLoginAction(NewController controller) { + NewAction action = controller.createAction("login") + .setDescription("Authenticate a user.") + .setSince("6.0") + .setPost(true) + .setHandler(ServletFilterHandler.INSTANCE); + action.createParam("login") + .setDescription("Login of the user") + .setRequired(true); + action.createParam("password") + .setDescription("Password of the user") + .setRequired(true); + } + } diff --git a/server/sonar-server/src/main/java/org/sonar/server/user/AbstractUserSession.java b/server/sonar-server/src/main/java/org/sonar/server/user/AbstractUserSession.java index 00d2e2a9208..0dcb44b377e 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/user/AbstractUserSession.java +++ b/server/sonar-server/src/main/java/org/sonar/server/user/AbstractUserSession.java @@ -19,6 +19,9 @@ */ package org.sonar.server.user; +import static com.google.common.collect.Lists.newArrayList; +import static com.google.common.collect.Maps.newHashMap; + import com.google.common.base.MoreObjects; import com.google.common.base.Strings; import com.google.common.collect.HashMultimap; @@ -33,18 +36,19 @@ import java.util.Set; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.sonar.api.security.DefaultGroups; +import org.sonar.db.user.UserDto; import org.sonar.server.exceptions.ForbiddenException; import org.sonar.server.exceptions.UnauthorizedException; -import static com.google.common.collect.Lists.newArrayList; -import static com.google.common.collect.Maps.newHashMap; - public abstract class AbstractUserSession implements UserSession { protected static final String INSUFFICIENT_PRIVILEGES_MESSAGE = "Insufficient privileges"; private static final ForbiddenException INSUFFICIENT_PRIVILEGES_EXCEPTION = new ForbiddenException(INSUFFICIENT_PRIVILEGES_MESSAGE); + protected UserDto userDto; protected Integer userId; protected String login; + protected String name; + protected Set userGroups = Sets.newHashSet(DefaultGroups.ANYONE); protected List globalPermissions = Collections.emptyList(); protected HashMultimap projectKeyByPermission = HashMultimap.create(); @@ -52,7 +56,7 @@ public abstract class AbstractUserSession impleme protected Map projectUuidByComponentUuid = newHashMap(); protected List projectPermissionsCheckedByKey = newArrayList(); protected List projectPermissionsCheckedByUuid = newArrayList(); - protected String name; + protected Locale locale = Locale.ENGLISH; private final Class clazz; @@ -61,13 +65,14 @@ public abstract class AbstractUserSession impleme this.clazz = clazz; } + @Override @CheckForNull public String getLogin() { return login; } - protected T setLogin(@Nullable String s) { + public T setLogin(@Nullable String s) { this.login = Strings.emptyToNull(s); return clazz.cast(this); } @@ -78,7 +83,7 @@ public abstract class AbstractUserSession impleme return name; } - protected T setName(@Nullable String s) { + public T setName(@Nullable String s) { this.name = Strings.emptyToNull(s); return clazz.cast(this); } @@ -89,7 +94,7 @@ public abstract class AbstractUserSession impleme return userId; } - protected T setUserId(@Nullable Integer userId) { + public T setUserId(@Nullable Integer userId) { this.userId = userId; return clazz.cast(this); } @@ -99,7 +104,7 @@ public abstract class AbstractUserSession impleme return userGroups; } - protected T setUserGroups(@Nullable String... userGroups) { + public T setUserGroups(@Nullable String... userGroups) { if (userGroups != null) { this.userGroups.addAll(Arrays.asList(userGroups)); } diff --git a/server/sonar-server/src/main/java/org/sonar/server/user/RubyUserSession.java b/server/sonar-server/src/main/java/org/sonar/server/user/RubyUserSession.java deleted file mode 100644 index 92da6edc8f4..00000000000 --- a/server/sonar-server/src/main/java/org/sonar/server/user/RubyUserSession.java +++ /dev/null @@ -1,74 +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.user; - -import java.util.List; - -import javax.annotation.Nullable; - -import org.sonar.core.platform.ComponentContainer; -import org.sonar.db.component.ResourceDao; -import org.sonar.db.user.AuthorizationDao; -import org.sonar.server.platform.Platform; -import org.sonar.server.ui.JRubyI18n; - -import com.google.common.annotations.VisibleForTesting; - -public class RubyUserSession { - - private static RubyUserSession instance; - - private static RubyUserSession getInstance() { - if (instance == null) { - instance = new RubyUserSession(Platform.getInstance()); - } - return instance; - } - - private final Platform platform; - - /** - * Invoked by Ruby code - see application_controller.rb - */ - public static void setSession(@Nullable Integer userId, @Nullable String login, @Nullable String name, @Nullable List userGroups, @Nullable String localeRubyKey) { - getInstance().setSessionImpl(userId, login, name, userGroups, localeRubyKey); - } - - @VisibleForTesting - RubyUserSession(Platform platform) { - // Utility class - this.platform = platform; - } - - public void setSessionImpl(@Nullable Integer userId, @Nullable String login, @Nullable String name, @Nullable List userGroups, @Nullable String localeRubyKey) { - ComponentContainer container = platform.getContainer(); - ThreadLocalUserSession threadLocalUserSession = container.getComponentByType(ThreadLocalUserSession.class); - - UserSession session = new ServerUserSession(container.getComponentByType(AuthorizationDao.class), - container.getComponentByType(ResourceDao.class)) - .setLogin(login) - .setName(name) - .setUserId(userId) - .setUserGroups(userGroups != null ? userGroups.toArray(new String[userGroups.size()]) : null) - .setLocale(JRubyI18n.toLocale(localeRubyKey)); - threadLocalUserSession.set(session); - } - -} diff --git a/server/sonar-server/src/main/java/org/sonar/server/user/ServerUserSession.java b/server/sonar-server/src/main/java/org/sonar/server/user/ServerUserSession.java index 3fe9aaaca26..b4792f6b168 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/user/ServerUserSession.java +++ b/server/sonar-server/src/main/java/org/sonar/server/user/ServerUserSession.java @@ -19,36 +19,65 @@ */ package org.sonar.server.user; +import static com.google.common.collect.Maps.newHashMap; +import static java.util.Objects.requireNonNull; + import java.util.ArrayList; import java.util.Collection; +import java.util.HashSet; import java.util.List; import java.util.Map; -import org.sonar.api.security.DefaultGroups; +import java.util.Set; +import java.util.stream.Collectors; +import javax.annotation.Nullable; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; import org.sonar.db.component.ResourceDao; import org.sonar.db.component.ResourceDto; import org.sonar.db.user.AuthorizationDao; - -import static com.google.common.collect.Maps.newHashMap; -import static com.google.common.collect.Sets.newHashSet; +import org.sonar.db.user.GroupDto; +import org.sonar.db.user.UserDto; /** * Part of the current HTTP session */ public class ServerUserSession extends AbstractUserSession { - private Map projectKeyByComponentKey = newHashMap(); + private final DbClient dbClient; private final AuthorizationDao authorizationDao; private final ResourceDao resourceDao; - ServerUserSession(AuthorizationDao authorizationDao, ResourceDao resourceDao) { + private ServerUserSession(DbClient dbClient, @Nullable UserDto userDto) { super(ServerUserSession.class); + this.dbClient = dbClient; + this.authorizationDao = dbClient.authorizationDao(); + this.resourceDao = dbClient.resourceDao(); this.globalPermissions = null; - this.authorizationDao = authorizationDao; - this.resourceDao = resourceDao; - // Do not forget that when forceAuthentication is set to true, the Anyone group should not be set (but this will be check when - // authentication will be done in Java) - this.userGroups = newHashSet(DefaultGroups.ANYONE); + if(userDto != null){ + this.setLogin(userDto.getLogin()); + this.setName(userDto.getName()); + this.setUserId(userDto.getId().intValue()); + this.userGroups.addAll(getUserGroups(userDto.getLogin())); + } + } + + public static ServerUserSession createForUser(DbClient dbClient, UserDto userDto){ + requireNonNull(userDto, "UserDto must not be null"); + return new ServerUserSession(dbClient, userDto); + } + + public static ServerUserSession createForAnonymous(DbClient dbClient){ + return new ServerUserSession(dbClient, null); + } + + private Set getUserGroups(String userLogin) { + DbSession dbSession = dbClient.openSession(false); + try { + return new HashSet<>(dbClient.groupDao().selectByUserLogin(dbSession, userLogin).stream().map(GroupDto::getName).collect(Collectors.toSet())); + } finally { + dbClient.closeSession(dbSession); + } } @Override diff --git a/server/sonar-server/src/main/java/org/sonar/server/user/UpdateUser.java b/server/sonar-server/src/main/java/org/sonar/server/user/UpdateUser.java index 363001b6cf1..2598aaa307b 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/user/UpdateUser.java +++ b/server/sonar-server/src/main/java/org/sonar/server/user/UpdateUser.java @@ -19,12 +19,12 @@ */ package org.sonar.server.user; +import static com.google.common.base.Preconditions.checkNotNull; + import java.util.List; import javax.annotation.CheckForNull; import javax.annotation.Nullable; -import static com.google.common.base.Preconditions.checkNotNull; - public class UpdateUser { private String login; @@ -34,11 +34,11 @@ public class UpdateUser { private String password; private ExternalIdentity externalIdentity; - boolean nameChanged; - boolean emailChanged; - boolean scmAccountsChanged; - boolean passwordChanged; - boolean externalIdentityChanged; + private boolean nameChanged; + private boolean emailChanged; + private boolean scmAccountsChanged; + private boolean passwordChanged; + private boolean externalIdentityChanged; private UpdateUser(String login) { // No direct call to this constructor @@ -98,6 +98,9 @@ public class UpdateUser { return externalIdentity; } + /** + * This method should only be used when updating a none local user + */ public UpdateUser setExternalIdentity(@Nullable ExternalIdentity externalIdentity) { this.externalIdentity = externalIdentity; externalIdentityChanged = true; diff --git a/server/sonar-server/src/main/java/org/sonar/server/user/UserUpdater.java b/server/sonar-server/src/main/java/org/sonar/server/user/UserUpdater.java index d63d0b66c53..2e869f5cdd3 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/user/UserUpdater.java +++ b/server/sonar-server/src/main/java/org/sonar/server/user/UserUpdater.java @@ -19,6 +19,10 @@ */ package org.sonar.server.user; +import static com.google.common.base.Strings.isNullOrEmpty; +import static com.google.common.collect.Lists.newArrayList; +import static org.sonar.db.user.UserDto.encryptPassword; + import com.google.common.base.Joiner; import com.google.common.base.Predicate; import com.google.common.collect.Iterables; @@ -47,9 +51,6 @@ import org.sonar.server.exceptions.ServerException; import org.sonar.server.user.index.UserIndexer; import org.sonar.server.util.Validation; -import static com.google.common.base.Strings.isNullOrEmpty; -import static com.google.common.collect.Lists.newArrayList; - @ServerSide public class UserUpdater { @@ -115,11 +116,13 @@ public class UserUpdater { UpdateUser updateUser = UpdateUser.create(login) .setName(newUser.name()) .setEmail(newUser.email()) - .setScmAccounts(newUser.scmAccounts()) - .setExternalIdentity(newUser.externalIdentity()); + .setScmAccounts(newUser.scmAccounts()); if (newUser.password() != null) { updateUser.setPassword(newUser.password()); } + if (newUser.externalIdentity() != null) { + updateUser.setExternalIdentity(newUser.externalIdentity()); + } // Hack to allow to change the password of the user existingUser.setLocal(true); updateUserDto(dbSession, updateUser, existingUser); @@ -178,22 +181,22 @@ public class UserUpdater { List messages = newArrayList(); String login = newUser.login(); - validateLoginFormat(login, messages); - userDto.setLogin(login); + if (validateLoginFormat(login, messages)) { + userDto.setLogin(login); + } String name = newUser.name(); - validateNameFormat(name, messages); - userDto.setName(name); + if (validateNameFormat(name, messages)) { + userDto.setName(name); + } String email = newUser.email(); - if (email != null) { - validateEmailFormat(email, messages); + if (email != null && validateEmailFormat(email, messages)) { userDto.setEmail(email); } String password = newUser.password(); - if (password != null) { - validatePasswords(password, messages); + if (password != null && validatePasswords(password, messages)) { setEncryptedPassWord(password, userDto); } @@ -215,26 +218,22 @@ public class UserUpdater { List messages = newArrayList(); String name = updateUser.name(); - if (updateUser.isNameChanged()) { - validateNameFormat(name, messages); + if (updateUser.isNameChanged() && validateNameFormat(name, messages)) { userDto.setName(name); } String email = updateUser.email(); - if (updateUser.isEmailChanged()) { - validateEmailFormat(email, messages); + if (updateUser.isEmailChanged() && validateEmailFormat(email, messages)) { userDto.setEmail(email); } - if (isNewExternalIdentityNotEqualsToSonaQube(updateUser)) { + if (updateUser.isExternalIdentityChanged()) { setExternalIdentity(userDto, updateUser.externalIdentity()); userDto.setSalt(null); userDto.setCryptedPassword(null); } else { String password = updateUser.password(); - if (updateUser.isPasswordChanged()) { - validatePasswords(password, messages); - checkPasswordChangeAllowed(userDto, messages); + if (updateUser.isPasswordChanged() && validatePasswords(password, messages) && checkPasswordChangeAllowed(userDto, messages)) { setEncryptedPassWord(password, userDto); } } @@ -242,17 +241,15 @@ public class UserUpdater { if (updateUser.isScmAccountsChanged()) { List scmAccounts = sanitizeScmAccounts(updateUser.scmAccounts()); if (scmAccounts != null && !scmAccounts.isEmpty()) { - validateScmAccounts(dbSession, scmAccounts, userDto.getLogin(), email != null ? email : userDto.getEmail(), userDto, messages); - userDto.setScmAccounts(scmAccounts); + String newOrOldEmail = email != null ? email : userDto.getEmail(); + if (validateScmAccounts(dbSession, scmAccounts, userDto.getLogin(), newOrOldEmail, userDto, messages)) { + userDto.setScmAccounts(scmAccounts); + } } else { userDto.setScmAccounts((String) null); } } - if (updateUser.isExternalIdentityChanged()) { - setExternalIdentity(userDto, updateUser.externalIdentity()); - } - if (!messages.isEmpty()) { throw new BadRequestException(messages); } @@ -270,64 +267,71 @@ public class UserUpdater { } } - private static void checkNotEmptyParam(@Nullable String value, String param, List messages) { + private static boolean checkNotEmptyParam(@Nullable String value, String param, List messages) { if (isNullOrEmpty(value)) { messages.add(Message.of(Validation.CANT_BE_EMPTY_MESSAGE, param)); + return false; } + return true; } - private static String validateLoginFormat(@Nullable String login, List messages) { - checkNotEmptyParam(login, LOGIN_PARAM, messages); + private static boolean validateLoginFormat(@Nullable String login, List messages) { + boolean isValid = checkNotEmptyParam(login, LOGIN_PARAM, messages); if (!isNullOrEmpty(login)) { if (login.length() < LOGIN_MIN_LENGTH) { messages.add(Message.of(Validation.IS_TOO_SHORT_MESSAGE, LOGIN_PARAM, LOGIN_MIN_LENGTH)); + return false; } else if (login.length() > LOGIN_MAX_LENGTH) { messages.add(Message.of(Validation.IS_TOO_LONG_MESSAGE, LOGIN_PARAM, LOGIN_MAX_LENGTH)); + return false; } else if (!login.matches("\\A\\w[\\w\\.\\-_@]+\\z")) { messages.add(Message.of("user.bad_login")); + return false; } } - return login; + return isValid; } - private static void validateNameFormat(@Nullable String name, List messages) { - checkNotEmptyParam(name, NAME_PARAM, messages); + private static boolean validateNameFormat(@Nullable String name, List messages) { + boolean isValid = checkNotEmptyParam(name, NAME_PARAM, messages); if (name != null && name.length() > NAME_MAX_LENGTH) { messages.add(Message.of(Validation.IS_TOO_LONG_MESSAGE, NAME_PARAM, 200)); + return false; } + return isValid; } - private static void validateEmailFormat(@Nullable String email, List messages) { + private static boolean validateEmailFormat(@Nullable String email, List messages) { if (email != null && email.length() > EMAIL_MAX_LENGTH) { messages.add(Message.of(Validation.IS_TOO_LONG_MESSAGE, EMAIL_PARAM, 100)); + return false; } + return true; } - private static void checkPasswordChangeAllowed(UserDto userDto, List messages) { + private static boolean checkPasswordChangeAllowed(UserDto userDto, List messages) { if (!userDto.isLocal()) { messages.add(Message.of("user.password_cant_be_changed_on_external_auth")); + return false; } + return true; } - private static boolean isNewExternalIdentityNotEqualsToSonaQube(UpdateUser updateUser) { - ExternalIdentity externalIdentity = updateUser.externalIdentity(); - if (updateUser.isExternalIdentityChanged() && externalIdentity != null) { - return !externalIdentity.getProvider().equals(SQ_AUTHORITY); - } - return false; - } - - private static void validatePasswords(@Nullable String password, List messages) { + private static boolean validatePasswords(@Nullable String password, List messages) { if (password == null || password.length() == 0) { messages.add(Message.of(Validation.CANT_BE_EMPTY_MESSAGE, PASSWORD_PARAM)); + return false; } + return true; } - private void validateScmAccounts(DbSession dbSession, List scmAccounts, @Nullable String login, @Nullable String email, @Nullable UserDto existingUser, + private boolean validateScmAccounts(DbSession dbSession, List scmAccounts, @Nullable String login, @Nullable String email, @Nullable UserDto existingUser, List messages) { + boolean isValid = true; for (String scmAccount : scmAccounts) { if (scmAccount.equals(login) || scmAccount.equals(email)) { messages.add(Message.of("user.login_or_email_used_as_scm_account")); + isValid = false; } else { List matchingUsers = dbClient.userDao().selectByScmAccountOrLoginOrEmail(dbSession, scmAccount); List matchingUsersWithoutExistingUser = newArrayList(); @@ -338,9 +342,11 @@ public class UserUpdater { } if (!matchingUsersWithoutExistingUser.isEmpty()) { messages.add(Message.of("user.scm_account_already_used", scmAccount, Joiner.on(", ").join(matchingUsersWithoutExistingUser))); + isValid = false; } } } + return isValid; } @CheckForNull @@ -373,10 +379,6 @@ public class UserUpdater { userDto.setCryptedPassword(encryptPassword(password, saltHex)); } - private static String encryptPassword(String password, String salt) { - return DigestUtils.sha1Hex("--" + salt + "--" + password + "--"); - } - private void notifyNewUser(String login, String name, String email) { newUserNotifier.onNewUser(NewUserHandler.Context.builder() .setLogin(login) diff --git a/server/sonar-server/src/main/java/org/sonar/server/ws/ServletFilterHandler.java b/server/sonar-server/src/main/java/org/sonar/server/ws/ServletFilterHandler.java new file mode 100644 index 00000000000..3f1f7b87346 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/ws/ServletFilterHandler.java @@ -0,0 +1,42 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.ws; + +import org.sonar.api.server.ws.Request; +import org.sonar.api.server.ws.RequestHandler; +import org.sonar.api.server.ws.Response; + +/** + * Used to declare web services that are implemented by a servlet filter. + */ +public class ServletFilterHandler implements RequestHandler { + + public static final RequestHandler INSTANCE = new ServletFilterHandler(); + + private ServletFilterHandler() { + // Nothing + } + + @Override + public void handle(Request request, Response response) { + throw new UnsupportedOperationException("This web service is implemented as a servlet filter"); + } + +} 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 new file mode 100644 index 00000000000..cbd7f126d5f --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/authentication/AuthLoginActionTest.java @@ -0,0 +1,122 @@ +/* + * 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 b74ccaf628c..8128b3ac55e 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 @@ -20,6 +20,8 @@ package org.sonar.server.authentication; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -32,6 +34,7 @@ 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.db.user.UserDto; public class BaseContextFactoryTest { @@ -50,8 +53,9 @@ public class BaseContextFactoryTest { HttpServletRequest request = mock(HttpServletRequest.class); HttpServletResponse response = mock(HttpServletResponse.class); BaseIdentityProvider identityProvider = mock(BaseIdentityProvider.class); + JwtHttpHandler jwtHttpHandler = mock(JwtHttpHandler.class); - BaseContextFactory underTest = new BaseContextFactory(userIdentityAuthenticator, server); + BaseContextFactory underTest = new BaseContextFactory(userIdentityAuthenticator, server, jwtHttpHandler); @Before public void setUp() throws Exception { @@ -74,6 +78,7 @@ public class BaseContextFactoryTest { when(request.getSession()).thenReturn(session); context.authenticate(USER_IDENTITY); - verify(userIdentityAuthenticator).authenticate(USER_IDENTITY, identityProvider, request, response); + verify(userIdentityAuthenticator).authenticate(USER_IDENTITY, identityProvider); + verify(jwtHttpHandler).generateToken(any(UserDto.class), eq(response)); } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/authentication/CredentialsAuthenticatorTest.java b/server/sonar-server/src/test/java/org/sonar/server/authentication/CredentialsAuthenticatorTest.java new file mode 100644 index 00000000000..e4686805f2b --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/authentication/CredentialsAuthenticatorTest.java @@ -0,0 +1,146 @@ +/* + * 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.junit.rules.ExpectedException.none; +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 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.server.exceptions.UnauthorizedException; + +public class CredentialsAuthenticatorTest { + + static final String LOGIN = "LOGIN"; + static final String PASSWORD = "PASSWORD"; + static final String SALT = "0242b0b4c0a93ddfe09dd886de50bc25ba000b51"; + static final String CRYPTED_PASSWORD = "540e4fc4be4e047db995bc76d18374a5b5db08cc"; + + @Rule + public ExpectedException expectedException = none(); + + @Rule + public DbTester dbTester = DbTester.create(System2.INSTANCE); + + DbClient dbClient = dbTester.getDbClient(); + + DbSession dbSession = dbTester.getSession(); + + RealmAuthenticator externalAuthenticator = mock(RealmAuthenticator.class); + HttpServletRequest request = mock(HttpServletRequest.class); + HttpServletResponse response = mock(HttpServletResponse.class); + + CredentialsAuthenticator underTest = new CredentialsAuthenticator(dbClient, externalAuthenticator); + + @Test + public void authenticate_local_user() throws Exception { + insertUser(newUserDto() + .setLogin(LOGIN) + .setCryptedPassword(CRYPTED_PASSWORD) + .setSalt(SALT) + .setLocal(true)); + + UserDto userDto = executeAuthenticate(); + assertThat(userDto.getLogin()).isEqualTo(LOGIN); + } + + @Test + public void fail_to_authenticate_local_user_when_password_is_wrong() throws Exception { + insertUser(newUserDto() + .setLogin(LOGIN) + .setCryptedPassword("Wrong password") + .setSalt("Wrong salt") + .setLocal(true)); + + expectedException.expect(UnauthorizedException.class); + executeAuthenticate(); + } + + @Test + public void authenticate_external_user() throws Exception { + when(externalAuthenticator.authenticate(LOGIN, PASSWORD, request)).thenReturn(Optional.of(newUserDto())); + insertUser(newUserDto() + .setLogin(LOGIN) + .setLocal(false)); + + executeAuthenticate(); + + verify(externalAuthenticator).authenticate(LOGIN, PASSWORD, request); + } + + @Test + public void fail_to_authenticate_authenticate_external_user_when_no_external_authentication() throws Exception { + when(externalAuthenticator.authenticate(LOGIN, PASSWORD, request)).thenReturn(Optional.empty()); + insertUser(newUserDto() + .setLogin(LOGIN) + .setLocal(false)); + + expectedException.expect(UnauthorizedException.class); + executeAuthenticate(); + } + + @Test + public void fail_to_authenticate_local_user_that_have_no_password() throws Exception { + insertUser(newUserDto() + .setLogin(LOGIN) + .setCryptedPassword(null) + .setSalt(SALT) + .setLocal(true)); + + expectedException.expect(UnauthorizedException.class); + executeAuthenticate(); + } + + @Test + public void fail_to_authenticate_local_user_that_have_no_salt() throws Exception { + insertUser(newUserDto() + .setLogin(LOGIN) + .setCryptedPassword(CRYPTED_PASSWORD) + .setSalt(null) + .setLocal(true)); + + expectedException.expect(UnauthorizedException.class); + executeAuthenticate(); + } + + private UserDto executeAuthenticate(){ + return underTest.authenticate(LOGIN, PASSWORD, request); + } + + private UserDto insertUser(UserDto userDto){ + dbClient.userDao().insert(dbSession, userDto); + dbSession.commit(); + return userDto; + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/authentication/GenerateJwtTokenFilterTest.java b/server/sonar-server/src/test/java/org/sonar/server/authentication/GenerateJwtTokenFilterTest.java deleted file mode 100644 index b6f6c10aa34..00000000000 --- a/server/sonar-server/src/test/java/org/sonar/server/authentication/GenerateJwtTokenFilterTest.java +++ /dev/null @@ -1,106 +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.jboss.netty.handler.codec.http.HttpMethod.GET; -import static org.jboss.netty.handler.codec.http.HttpMethod.POST; -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.ServletOutputStream; -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.server.tester.UserSessionRule; - -public class GenerateJwtTokenFilterTest { - - @Rule - public ExpectedException thrown = ExpectedException.none(); - - @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); - - GenerateJwtTokenFilter underTest = new GenerateJwtTokenFilter(jwtHttpHandler, userSession); - - @Before - public void setUp() throws Exception { - when(response.getOutputStream()).thenReturn(mock(ServletOutputStream.class)); - } - - @Test - public void do_get_pattern() throws Exception { - assertThat(underTest.doGetPattern().matches("/sessions/login")).isTrue(); - assertThat(underTest.doGetPattern().matches("/")).isFalse(); - } - - @Test - public void create_session_when_post_request_and_user_is_authenticated() throws Exception { - executePostRequest(); - userSession.login("john"); - - underTest.doFilter(request, response, chain); - - verify(jwtHttpHandler).generateToken("john", response); - } - - @Test - public void does_nothing_on_get_request() throws Exception { - executeGetRequest(); - userSession.login("john"); - - underTest.doFilter(request, response, chain); - - verifyZeroInteractions(jwtHttpHandler); - } - - @Test - public void does_nothing_when_user_is_not_authenticated() throws Exception { - executePostRequest(); - userSession.anonymous(); - - underTest.doFilter(request, response, chain); - - verifyZeroInteractions(jwtHttpHandler); - } - - private void executePostRequest() { - when(request.getMethod()).thenReturn(POST.getName()); - } - - private void executeGetRequest() { - when(request.getMethod()).thenReturn(GET.getName()); - } - -} diff --git a/server/sonar-server/src/test/java/org/sonar/server/authentication/InitFilterTest.java b/server/sonar-server/src/test/java/org/sonar/server/authentication/InitFilterTest.java index 5fb48bb9ec2..e4b147416fe 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/authentication/InitFilterTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/authentication/InitFilterTest.java @@ -19,6 +19,11 @@ */ package org.sonar.server.authentication; +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; + import javax.servlet.FilterChain; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -35,11 +40,6 @@ import org.sonar.api.server.authentication.UnauthorizedException; 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"; 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 4523c24bf93..2f67f52021a 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 @@ -31,6 +31,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; import static org.sonar.api.utils.System2.INSTANCE; +import static org.sonar.db.user.UserTesting.newUserDto; import io.jsonwebtoken.Claims; import io.jsonwebtoken.impl.DefaultClaims; @@ -53,14 +54,15 @@ 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.tester.UserSessionRule; +import org.sonar.server.exceptions.UnauthorizedException; +import org.sonar.server.user.ServerUserSession; +import org.sonar.server.user.ThreadLocalUserSession; public class JwtHttpHandlerTest { static final String JWT_TOKEN = "TOKEN"; - static final String USER_LOGIN = "john"; static final String CSRF_STATE = "CSRF_STATE"; + static final String USER_LOGIN = "john"; static final long NOW = 10_000_000_000L; static final long FOUR_MINUTES_AGO = NOW - 4 * 60 * 1000L; @@ -70,12 +72,11 @@ public class JwtHttpHandlerTest { @Rule public ExpectedException thrown = ExpectedException.none(); - @Rule - public UserSessionRule userSession = UserSessionRule.standalone(); - @Rule public DbTester dbTester = DbTester.create(INSTANCE); + ThreadLocalUserSession threadLocalUserSession = new ThreadLocalUserSession(); + DbClient dbClient = dbTester.getDbClient(); DbSession dbSession = dbTester.getSession(); @@ -93,20 +94,25 @@ public class JwtHttpHandlerTest { JwtSerializer jwtSerializer = mock(JwtSerializer.class); JwtCsrfVerifier jwtCsrfVerifier = mock(JwtCsrfVerifier.class); - JwtHttpHandler underTest = new JwtHttpHandler(system2, dbClient, server, settings, jwtSerializer, jwtCsrfVerifier); + UserDto userDto = newUserDto().setLogin(USER_LOGIN); + + JwtHttpHandler underTest = new JwtHttpHandler(system2, dbClient, server, settings, jwtSerializer, jwtCsrfVerifier, threadLocalUserSession); @Before public void setUp() throws Exception { + threadLocalUserSession.remove(); when(system2.now()).thenReturn(NOW); when(server.isSecured()).thenReturn(true); when(request.getSession()).thenReturn(httpSession); when(jwtSerializer.encode(any(JwtSerializer.JwtSession.class))).thenReturn(JWT_TOKEN); when(jwtCsrfVerifier.generateState(eq(response), anyInt())).thenReturn(CSRF_STATE); + dbClient.userDao().insert(dbSession, userDto); + dbSession.commit(); } @Test - public void create_session() throws Exception { - underTest.generateToken(USER_LOGIN, response); + public void create_token() throws Exception { + underTest.generateToken(userDto, response); Optional jwtCookie = findCookie("JWT-SESSION"); assertThat(jwtCookie).isPresent(); @@ -114,11 +120,12 @@ public class JwtHttpHandlerTest { verify(jwtSerializer).encode(jwtArgumentCaptor.capture()); verifyToken(jwtArgumentCaptor.getValue(), 3 * 24 * 60 * 60, NOW); + assertThat(threadLocalUserSession.get().isLoggedIn()).isTrue(); } @Test - public void generate_csrf_state() throws Exception { - underTest.generateToken(USER_LOGIN, response); + public void generate_csrf_state_when_creating_token() throws Exception { + underTest.generateToken(userDto, response); verify(jwtCsrfVerifier).generateState(response, 3 * 24 * 60 * 60); @@ -128,26 +135,12 @@ public class JwtHttpHandlerTest { } @Test - public void validate_session() throws Exception { - addJwtCookie(); - UserDto user = addUser(); - - Claims claims = createToken(NOW); - when(jwtSerializer.decode(JWT_TOKEN)).thenReturn(Optional.of(claims)); - - underTest.validateToken(request, response); - - verify(httpSession).setAttribute("user_id", user.getId()); - verify(jwtSerializer, never()).encode(any(JwtSerializer.JwtSession.class)); - } - - @Test - public void use_session_timeout_from_settings() throws Exception { + public void generate_token_is_using_session_timeout_from_settings() throws Exception { int sessionTimeoutInHours = 10; settings.setProperty("sonar.auth.sessionTimeoutInHours", sessionTimeoutInHours); - underTest = new JwtHttpHandler(system2, dbClient, server, settings, jwtSerializer, jwtCsrfVerifier); - underTest.generateToken(USER_LOGIN, response); + underTest = new JwtHttpHandler(system2, dbClient, server, settings, jwtSerializer, jwtCsrfVerifier, threadLocalUserSession); + underTest.generateToken(userDto, response); verify(jwtSerializer).encode(jwtArgumentCaptor.capture()); verifyToken(jwtArgumentCaptor.getValue(), sessionTimeoutInHours * 60 * 60, NOW); @@ -158,113 +151,122 @@ public class JwtHttpHandlerTest { int firstSessionTimeoutInHours = 10; settings.setProperty("sonar.auth.sessionTimeoutInHours", firstSessionTimeoutInHours); - underTest = new JwtHttpHandler(system2, dbClient, server, settings, jwtSerializer, jwtCsrfVerifier); - underTest.generateToken(USER_LOGIN, response); + underTest = new JwtHttpHandler(system2, dbClient, server, settings, jwtSerializer, jwtCsrfVerifier, threadLocalUserSession); + underTest.generateToken(userDto, response); // The property is updated, but it won't be taking into account settings.setProperty("sonar.auth.sessionTimeoutInHours", 15); - underTest.generateToken(USER_LOGIN, response); + underTest.generateToken(userDto, response); verify(jwtSerializer, times(2)).encode(jwtArgumentCaptor.capture()); verifyToken(jwtArgumentCaptor.getAllValues().get(0), firstSessionTimeoutInHours * 60 * 60, NOW); verifyToken(jwtArgumentCaptor.getAllValues().get(1), firstSessionTimeoutInHours * 60 * 60, NOW); } @Test - public void refresh_session_when_refresh_time_is_reached() throws Exception { + public void validate_token() throws Exception { + addJwtCookie(); + + Claims claims = createToken(USER_LOGIN, NOW); + when(jwtSerializer.decode(JWT_TOKEN)).thenReturn(Optional.of(claims)); + + underTest.validateToken(request, response); + + verify(jwtSerializer, never()).encode(any(JwtSerializer.JwtSession.class)); + assertThat(threadLocalUserSession.get().isLoggedIn()).isTrue(); + } + + @Test + public void validate_token_refresh_session_when_refresh_time_is_reached() throws Exception { addJwtCookie(); - UserDto user = addUser(); // Token was created 10 days ago and refreshed 6 minutes ago - Claims claims = createToken(TEN_DAYS_AGO); + Claims claims = createToken(USER_LOGIN, TEN_DAYS_AGO); claims.put("lastRefreshTime", SIX_MINUTES_AGO); when(jwtSerializer.decode(JWT_TOKEN)).thenReturn(Optional.of(claims)); underTest.validateToken(request, response); - verify(httpSession).setAttribute("user_id", user.getId()); verify(jwtSerializer).refresh(any(Claims.class), eq(3 * 24 * 60 * 60)); + assertThat(threadLocalUserSession.get().isLoggedIn()).isTrue(); } @Test - public void does_not_refresh_session_when_refresh_time_is_not_reached() throws Exception { + public void validate_token_does_not_refresh_session_when_refresh_time_is_not_reached() throws Exception { addJwtCookie(); - UserDto user = addUser(); // Token was created 10 days ago and refreshed 4 minutes ago - Claims claims = createToken(TEN_DAYS_AGO); + Claims claims = createToken(USER_LOGIN, TEN_DAYS_AGO); claims.put("lastRefreshTime", FOUR_MINUTES_AGO); when(jwtSerializer.decode(JWT_TOKEN)).thenReturn(Optional.of(claims)); underTest.validateToken(request, response); - verify(httpSession).setAttribute("user_id", user.getId()); verify(jwtSerializer, never()).refresh(any(Claims.class), anyInt()); + assertThat(threadLocalUserSession.get().isLoggedIn()).isTrue(); } @Test - public void remove_session_when_disconnected_timeout_is_reached() throws Exception { + public void validate_token_removes_session_when_disconnected_timeout_is_reached() throws Exception { addJwtCookie(); - addUser(); // Token was created 4 months ago, refreshed 4 minutes ago, and it expired in 5 minutes - Claims claims = createToken(NOW - (4L * 30 * 24 * 60 * 60 * 1000)); + Claims claims = createToken(USER_LOGIN, NOW - (4L * 30 * 24 * 60 * 60 * 1000)); claims.setExpiration(new Date(NOW + 5 * 60 * 1000)); claims.put("lastRefreshTime", FOUR_MINUTES_AGO); when(jwtSerializer.decode(JWT_TOKEN)).thenReturn(Optional.of(claims)); underTest.validateToken(request, response); - verify(httpSession).removeAttribute("user_id"); verifyCookie(findCookie("JWT-SESSION").get(), null, 0); + assertThat(threadLocalUserSession.get().isLoggedIn()).isFalse(); } @Test - public void remove_session_when_user_is_disabled() throws Exception { + public void validate_token_fails_with_unauthorized_when_user_is_disabled() throws Exception { addJwtCookie(); - addUser(false); + UserDto user = addUser(false); - Claims claims = createToken(NOW); + Claims claims = createToken(user.getLogin(), NOW); when(jwtSerializer.decode(JWT_TOKEN)).thenReturn(Optional.of(claims)); + thrown.expect(UnauthorizedException.class); underTest.validateToken(request, response); - - verify(httpSession).removeAttribute("user_id"); - verifyCookie(findCookie("JWT-SESSION").get(), null, 0); } @Test - public void remove_session_when_token_is_no_more_valid() throws Exception { + public void validate_token_removes_session_when_token_is_no_more_valid() throws Exception { addJwtCookie(); when(jwtSerializer.decode(JWT_TOKEN)).thenReturn(Optional.empty()); underTest.validateToken(request, response); - verify(httpSession).removeAttribute("user_id"); verifyCookie(findCookie("JWT-SESSION").get(), null, 0); + assertThat(threadLocalUserSession.get().isLoggedIn()).isFalse(); } @Test - public void does_nothing_when_no_jwt_cookie() throws Exception { + public void validate_token_does_nothing_when_no_jwt_cookie() throws Exception { underTest.validateToken(request, response); verifyZeroInteractions(httpSession, jwtSerializer); + assertThat(threadLocalUserSession.get().isLoggedIn()).isFalse(); } @Test - public void does_nothing_when_empty_value_in_jwt_cookie() throws Exception { + public void validate_token_does_nothing_when_empty_value_in_jwt_cookie() throws Exception { when(request.getCookies()).thenReturn(new Cookie[] {new Cookie("JWT-SESSION", "")}); underTest.validateToken(request, response); verifyZeroInteractions(httpSession, jwtSerializer); + assertThat(threadLocalUserSession.get().isLoggedIn()).isFalse(); } @Test - public void verify_csrf_state() throws Exception { + public void validate_token_verify_csrf_state() throws Exception { addJwtCookie(); - addUser(); - Claims claims = createToken(NOW); + Claims claims = createToken(USER_LOGIN, NOW); claims.put("xsrfToken", CSRF_STATE); when(jwtSerializer.decode(JWT_TOKEN)).thenReturn(Optional.of(claims)); @@ -274,12 +276,11 @@ public class JwtHttpHandlerTest { } @Test - public void refresh_state_when_refreshing_token() throws Exception { + public void validate_token_refresh_state_when_refreshing_token() throws Exception { addJwtCookie(); - addUser(); // Token was created 10 days ago and refreshed 6 minutes ago - Claims claims = createToken(TEN_DAYS_AGO); + Claims claims = createToken(USER_LOGIN, TEN_DAYS_AGO); claims.put("xsrfToken", "CSRF_STATE"); when(jwtSerializer.decode(JWT_TOKEN)).thenReturn(Optional.of(claims)); @@ -290,7 +291,7 @@ public class JwtHttpHandlerTest { } @Test - public void remove_state_when_removing_token() throws Exception { + public void validate_token_remove_state_when_removing_token() throws Exception { addJwtCookie(); // Token is invalid => it will be removed when(jwtSerializer.decode(JWT_TOKEN)).thenReturn(Optional.empty()); @@ -301,6 +302,24 @@ public class JwtHttpHandlerTest { verify(jwtCsrfVerifier).removeState(response); } + @Test + public void remove_token() throws Exception { + underTest.removeToken(response); + + 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) { assertThat(token.getExpirationTimeInSeconds()).isEqualTo(expectedExpirationTime); assertThat(token.getUserLogin()).isEqualTo(USER_LOGIN); @@ -322,13 +341,8 @@ public class JwtHttpHandlerTest { assertThat(cookie.getValue()).isEqualTo(value); } - private UserDto addUser() { - return addUser(true); - } - private UserDto addUser(boolean active) { - UserDto user = UserTesting.newUserDto() - .setLogin(USER_LOGIN) + UserDto user = newUserDto() .setActive(active); dbClient.userDao().insert(dbSession, user); dbSession.commit(); @@ -341,15 +355,15 @@ public class JwtHttpHandlerTest { return cookie; } - private Claims createToken(long createdAt) { + private Claims createToken(String userLogin, long createdAt) { // Expired in 5 minutes by default - return createToken(createdAt, NOW + 5 * 60 * 1000); + return createToken(userLogin, createdAt, NOW + 5 * 60 * 1000); } - private Claims createToken(long createdAt, long expiredAt) { + private Claims createToken(String userLogin, long createdAt, long expiredAt) { DefaultClaims claims = new DefaultClaims(); claims.setId("ID"); - claims.setSubject(USER_LOGIN); + claims.setSubject(userLogin); claims.setIssuedAt(new Date(createdAt)); claims.setExpiration(new Date(expiredAt)); claims.put("lastRefreshTime", createdAt); diff --git a/server/sonar-server/src/test/java/org/sonar/server/authentication/OAuth2CallbackFilterTest.java b/server/sonar-server/src/test/java/org/sonar/server/authentication/OAuth2CallbackFilterTest.java index e07443f4c29..b0d6635464c 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/authentication/OAuth2CallbackFilterTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/authentication/OAuth2CallbackFilterTest.java @@ -19,6 +19,11 @@ */ package org.sonar.server.authentication; +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; + import javax.servlet.FilterChain; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -32,11 +37,6 @@ import org.sonar.api.server.authentication.UnauthorizedException; 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"; @@ -139,7 +139,7 @@ public class OAuth2CallbackFilterTest { assertError("Fail to callback authentication"); } - private void assertCallbackCalled(){ + private void assertCallbackCalled() { assertThat(logTester.logs(LoggerLevel.ERROR)).isEmpty(); assertThat(oAuth2IdentityProvider.isCallbackCalled()).isTrue(); } 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 677bfaf7dfb..9afb606bf41 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 @@ -20,6 +20,8 @@ package org.sonar.server.authentication; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -35,6 +37,7 @@ 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.user.UserDto; public class OAuth2ContextFactoryTest { @@ -56,13 +59,14 @@ public class OAuth2ContextFactoryTest { UserIdentityAuthenticator userIdentityAuthenticator = mock(UserIdentityAuthenticator.class); Server server = mock(Server.class); OAuthCsrfVerifier csrfVerifier = mock(OAuthCsrfVerifier.class); + JwtHttpHandler jwtHttpHandler = mock(JwtHttpHandler.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); + OAuth2ContextFactory underTest = new OAuth2ContextFactory(userIdentityAuthenticator, server, csrfVerifier, jwtHttpHandler); @Before public void setUp() throws Exception { @@ -74,7 +78,7 @@ public class OAuth2ContextFactoryTest { public void create_context() throws Exception { when(server.getPublicRootUrl()).thenReturn(SECURED_PUBLIC_ROOT_URL); - OAuth2IdentityProvider.InitContext context = underTest.newContext(request, response, identityProvider); + OAuth2IdentityProvider.InitContext context = newInitContext(); assertThat(context.getRequest()).isEqualTo(request); assertThat(context.getResponse()).isEqualTo(response); @@ -83,7 +87,7 @@ public class OAuth2ContextFactoryTest { @Test public void generate_csrf_state() throws Exception { - OAuth2IdentityProvider.InitContext context = underTest.newContext(request, response, identityProvider); + OAuth2IdentityProvider.InitContext context = newInitContext(); context.generateCsrfState(); @@ -92,7 +96,7 @@ public class OAuth2ContextFactoryTest { @Test public void redirect_to() throws Exception { - OAuth2IdentityProvider.InitContext context = underTest.newContext(request, response, identityProvider); + OAuth2IdentityProvider.InitContext context = newInitContext(); context.redirectTo("/test"); @@ -103,7 +107,7 @@ public class OAuth2ContextFactoryTest { 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); + OAuth2IdentityProvider.InitContext context = newInitContext(); thrown.expect(MessageException.class); thrown.expectMessage("The server url should be configured in https, please update the property 'sonar.core.serverBaseURL'"); @@ -114,7 +118,7 @@ public class OAuth2ContextFactoryTest { public void create_callback() throws Exception { when(server.getPublicRootUrl()).thenReturn(SECURED_PUBLIC_ROOT_URL); - OAuth2IdentityProvider.CallbackContext callback = underTest.newCallback(request, response, identityProvider); + OAuth2IdentityProvider.CallbackContext callback = newCallbackContext(); assertThat(callback.getRequest()).isEqualTo(request); assertThat(callback.getResponse()).isEqualTo(response); @@ -123,17 +127,18 @@ public class OAuth2ContextFactoryTest { @Test public void authenticate() throws Exception { - OAuth2IdentityProvider.CallbackContext callback = underTest.newCallback(request, response, identityProvider); + OAuth2IdentityProvider.CallbackContext callback = newCallbackContext(); callback.authenticate(USER_IDENTITY); - verify(userIdentityAuthenticator).authenticate(USER_IDENTITY, identityProvider, request, response); + verify(userIdentityAuthenticator).authenticate(USER_IDENTITY, identityProvider); + verify(jwtHttpHandler).generateToken(any(UserDto.class), eq(response)); } @Test public void redirect_to_requested_page() throws Exception { when(server.getContextPath()).thenReturn(""); - OAuth2IdentityProvider.CallbackContext callback = underTest.newCallback(request, response, identityProvider); + OAuth2IdentityProvider.CallbackContext callback = newCallbackContext(); callback.redirectToRequestedPage(); @@ -143,7 +148,7 @@ public class OAuth2ContextFactoryTest { @Test public void redirect_to_requested_page_with_context() throws Exception { when(server.getContextPath()).thenReturn("/sonarqube"); - OAuth2IdentityProvider.CallbackContext callback = underTest.newCallback(request, response, identityProvider); + OAuth2IdentityProvider.CallbackContext callback = newCallbackContext(); callback.redirectToRequestedPage(); @@ -152,10 +157,19 @@ public class OAuth2ContextFactoryTest { @Test public void verify_csrf_state() throws Exception { - OAuth2IdentityProvider.CallbackContext callback = underTest.newCallback(request, response, identityProvider); + OAuth2IdentityProvider.CallbackContext callback = newCallbackContext(); callback.verifyCsrfState(); verify(csrfVerifier).verifyState(request, response); } + + private OAuth2IdentityProvider.InitContext newInitContext() { + return underTest.newContext(request, response, identityProvider); + } + + private OAuth2IdentityProvider.CallbackContext newCallbackContext() { + return underTest.newCallback(request, response, identityProvider); + } + } diff --git a/server/sonar-server/src/test/java/org/sonar/server/authentication/RealmAuthenticatorTest.java b/server/sonar-server/src/test/java/org/sonar/server/authentication/RealmAuthenticatorTest.java new file mode 100644 index 00000000000..45433607acb --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/authentication/RealmAuthenticatorTest.java @@ -0,0 +1,298 @@ +/* + * 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.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.rules.ExpectedException.none; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.doThrow; +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 org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.mockito.ArgumentCaptor; +import org.sonar.api.config.Settings; +import org.sonar.api.security.Authenticator; +import org.sonar.api.security.ExternalGroupsProvider; +import org.sonar.api.security.ExternalUsersProvider; +import org.sonar.api.security.SecurityRealm; +import org.sonar.api.security.UserDetails; +import org.sonar.api.server.authentication.IdentityProvider; +import org.sonar.api.server.authentication.UserIdentity; +import org.sonar.db.user.UserDto; +import org.sonar.server.exceptions.UnauthorizedException; +import org.sonar.server.user.SecurityRealmFactory; + +public class RealmAuthenticatorTest { + + @Rule + public ExpectedException expectedException = none(); + + static final String LOGIN = "LOGIN"; + static final String PASSWORD = "PASSWORD"; + + static final UserDto USER = newUserDto(); + + ArgumentCaptor userIdentityArgumentCaptor = ArgumentCaptor.forClass(UserIdentity.class); + ArgumentCaptor identityProviderArgumentCaptor = ArgumentCaptor.forClass(IdentityProvider.class); + + Settings settings = new Settings(); + + SecurityRealmFactory securityRealmFactory = mock(SecurityRealmFactory.class); + SecurityRealm realm = mock(SecurityRealm.class); + Authenticator authenticator = mock(Authenticator.class); + ExternalUsersProvider externalUsersProvider = mock(ExternalUsersProvider.class); + ExternalGroupsProvider externalGroupsProvider = mock(ExternalGroupsProvider.class); + + UserIdentityAuthenticator userIdentityAuthenticator = mock(UserIdentityAuthenticator.class); + + HttpServletRequest request = mock(HttpServletRequest.class); + HttpServletResponse response = mock(HttpServletResponse.class); + + RealmAuthenticator underTest = new RealmAuthenticator(settings, securityRealmFactory, userIdentityAuthenticator); + + @Test + public void authenticate() throws Exception { + executeStartWithoutGroupSync(); + when(authenticator.doAuthenticate(any(Authenticator.Context.class))).thenReturn(true); + UserDetails userDetails = new UserDetails(); + userDetails.setName("name"); + userDetails.setEmail("email"); + when(externalUsersProvider.doGetUserDetails(any(ExternalUsersProvider.Context.class))).thenReturn(userDetails); + when(userIdentityAuthenticator.authenticate(any(UserIdentity.class), any(IdentityProvider.class))).thenReturn(USER); + + underTest.authenticate(LOGIN, PASSWORD, request); + + verify(userIdentityAuthenticator).authenticate(userIdentityArgumentCaptor.capture(), identityProviderArgumentCaptor.capture()); + UserIdentity userIdentity = userIdentityArgumentCaptor.getValue(); + assertThat(userIdentity.getLogin()).isEqualTo(LOGIN); + assertThat(userIdentity.getProviderLogin()).isEqualTo(LOGIN); + assertThat(userIdentity.getName()).isEqualTo("name"); + assertThat(userIdentity.getEmail()).isEqualTo("email"); + assertThat(userIdentity.shouldSyncGroups()).isFalse(); + } + + @Test + public void authenticate_with_sonarqube_identity_provider() throws Exception { + executeStartWithoutGroupSync(); + when(authenticator.doAuthenticate(any(Authenticator.Context.class))).thenReturn(true); + UserDetails userDetails = new UserDetails(); + userDetails.setName("name"); + userDetails.setEmail("email"); + when(externalUsersProvider.doGetUserDetails(any(ExternalUsersProvider.Context.class))).thenReturn(userDetails); + when(userIdentityAuthenticator.authenticate(any(UserIdentity.class), any(IdentityProvider.class))).thenReturn(USER); + + underTest.authenticate(LOGIN, PASSWORD, request); + + verify(userIdentityAuthenticator).authenticate(userIdentityArgumentCaptor.capture(), identityProviderArgumentCaptor.capture()); + + assertThat(identityProviderArgumentCaptor.getValue().getKey()).isEqualTo("sonarqube"); + assertThat(identityProviderArgumentCaptor.getValue().getName()).isEqualTo("sonarqube"); + assertThat(identityProviderArgumentCaptor.getValue().getDisplay()).isNull(); + assertThat(identityProviderArgumentCaptor.getValue().isEnabled()).isTrue(); + } + + @Test + public void login_is_used_when_no_name_provided() throws Exception { + executeStartWithoutGroupSync(); + when(authenticator.doAuthenticate(any(Authenticator.Context.class))).thenReturn(true); + UserDetails userDetails = new UserDetails(); + userDetails.setEmail("email"); + when(externalUsersProvider.doGetUserDetails(any(ExternalUsersProvider.Context.class))).thenReturn(userDetails); + when(userIdentityAuthenticator.authenticate(any(UserIdentity.class), any(IdentityProvider.class))).thenReturn(USER); + + underTest.authenticate(LOGIN, PASSWORD, request); + + verify(userIdentityAuthenticator).authenticate(userIdentityArgumentCaptor.capture(), identityProviderArgumentCaptor.capture()); + assertThat(identityProviderArgumentCaptor.getValue().getName()).isEqualTo("sonarqube"); + } + + @Test + public void authenticate_with_group_sync() throws Exception { + when(externalGroupsProvider.doGetGroups(any(ExternalGroupsProvider.Context.class))).thenReturn(asList("group1", "group2")); + when(userIdentityAuthenticator.authenticate(any(UserIdentity.class), any(IdentityProvider.class))).thenReturn(USER); + executeStartWithGroupSync(); + executeAuthenticate(); + + verify(userIdentityAuthenticator).authenticate(userIdentityArgumentCaptor.capture(), identityProviderArgumentCaptor.capture()); + + UserIdentity userIdentity = userIdentityArgumentCaptor.getValue(); + assertThat(userIdentity.shouldSyncGroups()).isTrue(); + assertThat(userIdentity.getGroups()).containsOnly("group1", "group2"); + } + + @Test + public void use_login_if_user_details_contains_no_name() throws Exception { + executeStartWithoutGroupSync(); + when(authenticator.doAuthenticate(any(Authenticator.Context.class))).thenReturn(true); + UserDetails userDetails = new UserDetails(); + userDetails.setName(null); + when(externalUsersProvider.doGetUserDetails(any(ExternalUsersProvider.Context.class))).thenReturn(userDetails); + when(userIdentityAuthenticator.authenticate(any(UserIdentity.class), any(IdentityProvider.class))).thenReturn(USER); + + underTest.authenticate(LOGIN, PASSWORD, request); + + verify(userIdentityAuthenticator).authenticate(userIdentityArgumentCaptor.capture(), identityProviderArgumentCaptor.capture()); + assertThat(userIdentityArgumentCaptor.getValue().getName()).isEqualTo(LOGIN); + } + + @Test + public void allow_to_sign_up_property() throws Exception { + settings.setProperty("sonar.authenticator.createUsers", true); + when(userIdentityAuthenticator.authenticate(any(UserIdentity.class), any(IdentityProvider.class))).thenReturn(USER); + executeStartWithoutGroupSync(); + executeAuthenticate(); + + verify(userIdentityAuthenticator).authenticate(userIdentityArgumentCaptor.capture(), identityProviderArgumentCaptor.capture()); + assertThat(identityProviderArgumentCaptor.getValue().allowsUsersToSignUp()).isTrue(); + } + + @Test + public void does_not_allow_to_sign_up_property() throws Exception { + settings.setProperty("sonar.authenticator.createUsers", false); + when(userIdentityAuthenticator.authenticate(any(UserIdentity.class), any(IdentityProvider.class))).thenReturn(USER); + executeStartWithoutGroupSync(); + executeAuthenticate(); + + verify(userIdentityAuthenticator).authenticate(userIdentityArgumentCaptor.capture(), identityProviderArgumentCaptor.capture()); + assertThat(identityProviderArgumentCaptor.getValue().allowsUsersToSignUp()).isFalse(); + } + + @Test + public void use_downcase_login() throws Exception { + settings.setProperty("sonar.authenticator.downcase", true); + when(userIdentityAuthenticator.authenticate(any(UserIdentity.class), any(IdentityProvider.class))).thenReturn(USER); + executeStartWithoutGroupSync(); + executeAuthenticate("LOGIN"); + + verify(userIdentityAuthenticator).authenticate(userIdentityArgumentCaptor.capture(), identityProviderArgumentCaptor.capture()); + UserIdentity userIdentity = userIdentityArgumentCaptor.getValue(); + assertThat(userIdentity.getLogin()).isEqualTo("login"); + assertThat(userIdentity.getProviderLogin()).isEqualTo("login"); + } + + @Test + public void does_not_user_downcase_login() throws Exception { + settings.setProperty("sonar.authenticator.downcase", false); + when(userIdentityAuthenticator.authenticate(any(UserIdentity.class), any(IdentityProvider.class))).thenReturn(USER); + executeStartWithoutGroupSync(); + executeAuthenticate("LoGiN"); + + verify(userIdentityAuthenticator).authenticate(userIdentityArgumentCaptor.capture(), identityProviderArgumentCaptor.capture()); + UserIdentity userIdentity = userIdentityArgumentCaptor.getValue(); + assertThat(userIdentity.getLogin()).isEqualTo("LoGiN"); + assertThat(userIdentity.getProviderLogin()).isEqualTo("LoGiN"); + } + + @Test + public void fail_to_authenticate_when_user_details_are_null() throws Exception { + executeStartWithoutGroupSync(); + when(authenticator.doAuthenticate(any(Authenticator.Context.class))).thenReturn(true); + + when(externalUsersProvider.doGetUserDetails(any(ExternalUsersProvider.Context.class))).thenReturn(null); + + expectedException.expect(UnauthorizedException.class); + underTest.authenticate(LOGIN, PASSWORD, request); + } + + @Test + public void fail_to_authenticate_when_external_authentication_fails() throws Exception { + executeStartWithoutGroupSync(); + when(externalUsersProvider.doGetUserDetails(any(ExternalUsersProvider.Context.class))).thenReturn(new UserDetails()); + + when(authenticator.doAuthenticate(any(Authenticator.Context.class))).thenReturn(false); + + expectedException.expect(UnauthorizedException.class); + underTest.authenticate(LOGIN, PASSWORD, request); + } + + @Test + public void fail_to_authenticate_when_any_exception_is_thrown() throws Exception { + executeStartWithoutGroupSync(); + doThrow(IllegalArgumentException.class).when(authenticator).doAuthenticate(any(Authenticator.Context.class)); + + when(externalUsersProvider.doGetUserDetails(any(ExternalUsersProvider.Context.class))).thenReturn(null); + + expectedException.expect(UnauthorizedException.class); + underTest.authenticate(LOGIN, PASSWORD, request); + } + + @Test + public void return_empty_user_when_no_realm() throws Exception { + assertThat(underTest.authenticate(LOGIN, PASSWORD, request)).isEmpty(); + } + + @Test + public void fail_to_start_when_no_authenticator() throws Exception { + when(realm.doGetAuthenticator()).thenReturn(null); + when(securityRealmFactory.getRealm()).thenReturn(realm); + + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("No authenticator available"); + underTest.start(); + } + + @Test + public void fail_to_start_when_no_user_provider() throws Exception { + when(realm.doGetAuthenticator()).thenReturn(authenticator); + when(realm.getUsersProvider()).thenReturn(null); + when(securityRealmFactory.getRealm()).thenReturn(realm); + + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("No users provider available"); + underTest.start(); + } + + private void executeStartWithoutGroupSync() { + when(realm.doGetAuthenticator()).thenReturn(authenticator); + when(realm.getUsersProvider()).thenReturn(externalUsersProvider); + when(securityRealmFactory.getRealm()).thenReturn(realm); + underTest.start(); + } + + private void executeStartWithGroupSync() { + when(realm.doGetAuthenticator()).thenReturn(authenticator); + when(realm.getUsersProvider()).thenReturn(externalUsersProvider); + when(realm.getGroupsProvider()).thenReturn(externalGroupsProvider); + when(securityRealmFactory.getRealm()).thenReturn(realm); + underTest.start(); + } + + private void executeAuthenticate() { + executeAuthenticate(LOGIN); + } + + private void executeAuthenticate(String login) { + when(authenticator.doAuthenticate(any(Authenticator.Context.class))).thenReturn(true); + UserDetails userDetails = new UserDetails(); + userDetails.setName("name"); + when(externalUsersProvider.doGetUserDetails(any(ExternalUsersProvider.Context.class))).thenReturn(userDetails); + underTest.authenticate(login, PASSWORD, request); + } + +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/authentication/UserIdentityAuthenticatorTest.java b/server/sonar-server/src/test/java/org/sonar/server/authentication/UserIdentityAuthenticatorTest.java index 645c468fc4f..51d31dc2baf 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/authentication/UserIdentityAuthenticatorTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/authentication/UserIdentityAuthenticatorTest.java @@ -23,15 +23,12 @@ import static com.google.common.collect.Sets.newHashSet; import static java.util.Collections.singletonList; 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; import java.util.Collections; import java.util.HashSet; import java.util.Set; 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; @@ -84,32 +81,28 @@ public class UserIdentityAuthenticatorTest { UserDao userDao = dbClient.userDao(); GroupDao groupDao = dbClient.groupDao(); Settings settings = new Settings(); - JwtHttpHandler jwtHttpHandler = mock(JwtHttpHandler.class); HttpServletRequest request = mock(HttpServletRequest.class); HttpServletResponse response = mock(HttpServletResponse.class); - HttpSession httpSession = mock(HttpSession.class); UserUpdater userUpdater = new UserUpdater( mock(NewUserNotifier.class), settings, dbClient, mock(UserIndexer.class), - system2 - ); + system2); - UserIdentityAuthenticator underTest = new UserIdentityAuthenticator(dbClient, userUpdater, jwtHttpHandler); + UserIdentityAuthenticator underTest = new UserIdentityAuthenticator(dbClient, userUpdater); @Before public void setUp() throws Exception { settings.setProperty("sonar.defaultGroup", DEFAULT_GROUP); addGroup(DEFAULT_GROUP); - when(request.getSession()).thenReturn(httpSession); } @Test public void authenticate_new_user() throws Exception { - underTest.authenticate(USER_IDENTITY, IDENTITY_PROVIDER, request, response); + underTest.authenticate(USER_IDENTITY, IDENTITY_PROVIDER); dbSession.commit(); UserDto userDto = userDao.selectByLogin(dbSession, USER_LOGIN); @@ -134,7 +127,7 @@ public class UserIdentityAuthenticatorTest { .setName("John") // group3 doesn't exist in db, it will be ignored .setGroups(newHashSet("group1", "group2", "group3")) - .build(), IDENTITY_PROVIDER, request, response); + .build(), IDENTITY_PROVIDER); dbSession.commit(); UserDto userDto = userDao.selectByLogin(dbSession, USER_LOGIN); @@ -151,11 +144,10 @@ public class UserIdentityAuthenticatorTest { .setName("Old name") .setEmail("Old email") .setExternalIdentity("old identity") - .setExternalIdentityProvider("old provide") - ); + .setExternalIdentityProvider("old provide")); dbSession.commit(); - underTest.authenticate(USER_IDENTITY, IDENTITY_PROVIDER, request, response); + underTest.authenticate(USER_IDENTITY, IDENTITY_PROVIDER); dbSession.commit(); UserDto userDto = userDao.selectByLogin(dbSession, USER_LOGIN); @@ -175,11 +167,10 @@ public class UserIdentityAuthenticatorTest { .setName("Old name") .setEmail("Old email") .setExternalIdentity("old identity") - .setExternalIdentityProvider("old provide") - ); + .setExternalIdentityProvider("old provide")); dbSession.commit(); - underTest.authenticate(USER_IDENTITY, IDENTITY_PROVIDER, request, response); + underTest.authenticate(USER_IDENTITY, IDENTITY_PROVIDER); dbSession.commit(); UserDto userDto = userDao.selectByLogin(dbSession, USER_LOGIN); @@ -196,8 +187,7 @@ public class UserIdentityAuthenticatorTest { userDao.insert(dbSession, new UserDto() .setLogin(USER_LOGIN) .setActive(true) - .setName("John") - ); + .setName("John")); addGroup("group1"); addGroup("group2"); dbSession.commit(); @@ -208,7 +198,7 @@ public class UserIdentityAuthenticatorTest { .setName("John") // group3 doesn't exist in db, it will be ignored .setGroups(newHashSet("group1", "group2", "group3")) - .build(), IDENTITY_PROVIDER, request, response); + .build(), IDENTITY_PROVIDER); dbSession.commit(); Set userGroups = new HashSet<>(dbClient.groupMembershipDao().selectGroupsByLogins(dbSession, singletonList(USER_LOGIN)).get(USER_LOGIN)); @@ -238,7 +228,7 @@ public class UserIdentityAuthenticatorTest { .setName("John") // Only group1 is returned by the id provider => group2 will be removed .setGroups(newHashSet("group1")) - .build(), IDENTITY_PROVIDER, request, response); + .build(), IDENTITY_PROVIDER); dbSession.commit(); verifyUserGroups(USER_LOGIN, "group1"); @@ -267,35 +257,12 @@ public class UserIdentityAuthenticatorTest { .setName("John") // No group => group1 and group2 will be removed .setGroups(Collections.emptySet()) - .build(), IDENTITY_PROVIDER, request, response); + .build(), IDENTITY_PROVIDER); dbSession.commit(); verifyNoUserGroups(USER_LOGIN); } - @Test - public void update_session_for_rails() throws Exception { - UserDto userDto = UserTesting.newUserDto().setLogin(USER_LOGIN); - userDao.insert(dbSession, userDto); - dbSession.commit(); - - underTest.authenticate(USER_IDENTITY, IDENTITY_PROVIDER, request, response); - - verify(httpSession).setAttribute("user_id", userDto.getId()); - } - - @Test - public void create_jwt_token() throws Exception { - UserDto userDto = UserTesting.newUserDto().setLogin(USER_LOGIN); - userDao.insert(dbSession, userDto); - dbSession.commit(); - - underTest.authenticate(USER_IDENTITY, IDENTITY_PROVIDER, request, response); - - verify(httpSession).setAttribute("user_id", userDto.getId()); - verify(jwtHttpHandler).generateToken(USER_LOGIN, response); - } - @Test public void fail_to_authenticate_new_user_when_allow_users_to_signup_is_false() throws Exception { TestIdentityProvider identityProvider = new TestIdentityProvider() @@ -306,7 +273,7 @@ public class UserIdentityAuthenticatorTest { thrown.expect(UnauthorizedException.class); thrown.expectMessage("'github' users are not allowed to sign up"); - underTest.authenticate(USER_IDENTITY, identityProvider, request, response); + underTest.authenticate(USER_IDENTITY, identityProvider); } @Test @@ -321,7 +288,7 @@ public class UserIdentityAuthenticatorTest { thrown.expect(UnauthorizedException.class); thrown.expectMessage("You can't sign up because email 'john@email.com' is already used by an existing user. " + "This means that you probably already registered with another account."); - underTest.authenticate(USER_IDENTITY, IDENTITY_PROVIDER, request, response); + underTest.authenticate(USER_IDENTITY, IDENTITY_PROVIDER); } private void verifyUserGroups(String userLogin, String... groups) { 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 index 03c1ea71e04..210d940833d 100644 --- 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 @@ -31,23 +31,31 @@ 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); - ValidateJwtTokenFilter underTest = new ValidateJwtTokenFilter(jwtHttpHandler); + 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("/test"); + when(request.getRequestURI()).thenReturn("/measures"); } @Test @@ -55,6 +63,20 @@ public class ValidateJwtTokenFilterTest { 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(); @@ -64,6 +86,7 @@ public class ValidateJwtTokenFilterTest { @Test public void validate_session() throws Exception { + userSession.login("john"); underTest.doFilter(request, response, chain); verify(jwtHttpHandler).validateToken(request, response); @@ -72,6 +95,29 @@ public class ValidateJwtTokenFilterTest { @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); @@ -80,4 +126,24 @@ public class ValidateJwtTokenFilterTest { 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/AuthenticationWsTest.java b/server/sonar-server/src/test/java/org/sonar/server/authentication/ws/AuthenticationWsTest.java index 4bb0ed6209f..728a1e61975 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/authentication/ws/AuthenticationWsTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/authentication/ws/AuthenticationWsTest.java @@ -19,13 +19,14 @@ */ package org.sonar.server.authentication.ws; +import static org.assertj.core.api.Assertions.assertThat; + import org.junit.Test; import org.sonar.api.server.ws.RailsHandler; import org.sonar.api.server.ws.WebService; +import org.sonar.server.ws.ServletFilterHandler; import org.sonar.server.ws.WsTester; -import static org.assertj.core.api.Assertions.assertThat; - public class AuthenticationWsTest { WsTester tester = new WsTester(new AuthenticationWs()); @@ -35,12 +36,18 @@ public class AuthenticationWsTest { WebService.Controller controller = tester.controller("api/authentication"); assertThat(controller).isNotNull(); assertThat(controller.description()).isNotEmpty(); - assertThat(controller.actions()).hasSize(1); + assertThat(controller.actions()).hasSize(2); WebService.Action validate = controller.action("validate"); assertThat(validate).isNotNull(); assertThat(validate.handler()).isInstanceOf(RailsHandler.class); assertThat(validate.responseExampleAsString()).isNotEmpty(); assertThat(validate.params()).hasSize(1); + + WebService.Action login = controller.action("login"); + assertThat(login).isNotNull(); + assertThat(login.handler()).isInstanceOf(ServletFilterHandler.class); + assertThat(login.isPost()).isTrue(); + assertThat(login.params()).hasSize(2); } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/user/RubyUserSessionTest.java b/server/sonar-server/src/test/java/org/sonar/server/user/RubyUserSessionTest.java deleted file mode 100644 index b5b58867330..00000000000 --- a/server/sonar-server/src/test/java/org/sonar/server/user/RubyUserSessionTest.java +++ /dev/null @@ -1,85 +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.user; - -import java.util.Locale; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.sonar.core.platform.ComponentContainer; -import org.sonar.server.platform.Platform; - -import static com.google.common.collect.Lists.newArrayList; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class RubyUserSessionTest { - Platform platform = mock(Platform.class); - ComponentContainer componentContainer = mock(ComponentContainer.class); - ThreadLocalUserSession threadLocalUserSession = new ThreadLocalUserSession(); - RubyUserSession underTest = new RubyUserSession(platform); - - @Before - public void setUp() { - // for test isolation - threadLocalUserSession.remove(); - - when(platform.getContainer()).thenReturn(componentContainer); - when(componentContainer.getComponentByType(ThreadLocalUserSession.class)).thenReturn(threadLocalUserSession); - } - - @After - public void tearDown() { - // clean up for next test - threadLocalUserSession.remove(); - } - - @Test - public void should_set_session() { - underTest.setSessionImpl(123, "karadoc", "Karadoc", newArrayList("sonar-users"), "fr"); - - UserSession session = threadLocalUserSession.get(); - - assertThat(session).isNotNull(); - assertThat(session.getLogin()).isEqualTo("karadoc"); - assertThat(session.getName()).isEqualTo("Karadoc"); - assertThat(session.getUserId()).isEqualTo(123); - assertThat(session.getUserGroups()).containsOnly("sonar-users", "Anyone"); - assertThat(session.isLoggedIn()).isTrue(); - assertThat(session.locale()).isEqualTo(Locale.FRENCH); - } - - @Test - public void should_set_anonymous_session() { - underTest.setSessionImpl(null, null, null, null, "fr"); - - UserSession session = threadLocalUserSession.get(); - - assertThat(session).isNotNull(); - assertThat(session.getLogin()).isNull(); - assertThat(session.getName()).isNull(); - assertThat(session.getUserId()).isNull(); - assertThat(session.getUserGroups()).containsOnly("Anyone"); - assertThat(session.isLoggedIn()).isFalse(); - assertThat(session.locale()).isEqualTo(Locale.FRENCH); - } - -} diff --git a/server/sonar-server/src/test/java/org/sonar/server/user/ServerUserSessionTest.java b/server/sonar-server/src/test/java/org/sonar/server/user/ServerUserSessionTest.java index 504136e065c..111d9c5460c 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/user/ServerUserSessionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/user/ServerUserSessionTest.java @@ -19,214 +19,285 @@ */ package org.sonar.server.user; -import java.util.Arrays; +import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.core.permission.GlobalPermissions.DASHBOARD_SHARING; +import static org.sonar.core.permission.GlobalPermissions.QUALITY_PROFILE_ADMIN; +import static org.sonar.core.permission.GlobalPermissions.SYSTEM_ADMIN; +import static org.sonar.db.user.UserTesting.newUserDto; +import static org.sonar.server.user.ServerUserSession.createForAnonymous; +import static org.sonar.server.user.ServerUserSession.createForUser; + +import javax.annotation.Nullable; +import org.junit.Before; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.api.utils.System2; import org.sonar.api.web.UserRole; import org.sonar.core.permission.GlobalPermissions; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.DbTester; +import org.sonar.db.component.ComponentDbTester; import org.sonar.db.component.ComponentDto; import org.sonar.db.component.ComponentTesting; -import org.sonar.db.component.ResourceDao; -import org.sonar.db.component.ResourceDto; -import org.sonar.db.user.AuthorizationDao; +import org.sonar.db.user.GroupRoleDto; +import org.sonar.db.user.UserDto; +import org.sonar.db.user.UserRoleDto; import org.sonar.server.exceptions.ForbiddenException; -import static com.google.common.collect.Lists.newArrayList; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - public class ServerUserSessionTest { static final String LOGIN = "marius"; - static final String PROJECT_KEY = "com.foo:Bar"; + static final String PROJECT_UUID = "ABCD"; static final String FILE_KEY = "com.foo:Bar:BarFile.xoo"; static final String FILE_UUID = "BCDE"; - AuthorizationDao authorizationDao = mock(AuthorizationDao.class); - ResourceDao resourceDao = mock(ResourceDao.class); + @Rule + public DbTester dbTester = DbTester.create(System2.INSTANCE); - @Test - public void login_should_not_be_empty() { - UserSession session = newServerUserSession().setLogin(""); - assertThat(session.getLogin()).isNull(); - assertThat(session.isLoggedIn()).isFalse(); + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + ComponentDbTester componentDbTester = new ComponentDbTester(dbTester); + + DbClient dbClient = dbTester.getDbClient(); + + DbSession dbSession = dbTester.getSession(); + + UserDto userDto = newUserDto().setLogin(LOGIN); + ComponentDto project, file; + + @Before + public void setUp() throws Exception { + project = componentDbTester.insertComponent(ComponentTesting.newProjectDto(PROJECT_UUID)); + file = componentDbTester.insertComponent(ComponentTesting.newFileDto(project, FILE_UUID).setKey(FILE_KEY)); + dbClient.userDao().insert(dbSession, userDto); + dbSession.commit(); } @Test public void has_global_permission() { - UserSession session = newServerUserSession().setLogin(LOGIN); + addGlobalPermissions("admin", "profileadmin"); + UserSession session = newUserSession(userDto); - when(authorizationDao.selectGlobalPermissions(LOGIN)).thenReturn(Arrays.asList("profileadmin", "admin")); - - assertThat(session.hasPermission(GlobalPermissions.QUALITY_PROFILE_ADMIN)).isTrue(); - assertThat(session.hasPermission(GlobalPermissions.SYSTEM_ADMIN)).isTrue(); - assertThat(session.hasPermission(GlobalPermissions.DASHBOARD_SHARING)).isFalse(); + assertThat(session.hasPermission(QUALITY_PROFILE_ADMIN)).isTrue(); + assertThat(session.hasPermission(SYSTEM_ADMIN)).isTrue(); + assertThat(session.hasPermission(DASHBOARD_SHARING)).isFalse(); } @Test public void check_global_Permission_ok() { - UserSession session = newServerUserSession().setLogin(LOGIN); + addGlobalPermissions("admin", "profileadmin"); + UserSession session = newUserSession(userDto); - when(authorizationDao.selectGlobalPermissions(LOGIN)).thenReturn(Arrays.asList("profileadmin", "admin")); - - session.checkPermission(GlobalPermissions.QUALITY_PROFILE_ADMIN); + session.checkPermission(QUALITY_PROFILE_ADMIN); } - @Test(expected = ForbiddenException.class) + @Test public void check_global_Permission_ko() { - UserSession session = newServerUserSession().setLogin(LOGIN); + addGlobalPermissions("admin", "profileadmin"); + UserSession session = newUserSession(userDto); - when(authorizationDao.selectGlobalPermissions(LOGIN)).thenReturn(Arrays.asList("profileadmin", "admin")); - - session.checkPermission(GlobalPermissions.DASHBOARD_SHARING); + expectedException.expect(ForbiddenException.class); + session.checkPermission(DASHBOARD_SHARING); } @Test public void has_component_permission() { - UserSession session = newServerUserSession().setLogin(LOGIN).setUserId(1); - - String componentKey = FILE_KEY; - when(resourceDao.getRootProjectByComponentKey(componentKey)).thenReturn(new ResourceDto().setKey(componentKey)); - when(authorizationDao.selectAuthorizedRootProjectsKeys(1, UserRole.USER)).thenReturn(newArrayList(componentKey)); + addProjectPermissions(project, UserRole.USER); + UserSession session = newUserSession(userDto); - assertThat(session.hasComponentPermission(UserRole.USER, componentKey)).isTrue(); - assertThat(session.hasComponentPermission(UserRole.CODEVIEWER, componentKey)).isFalse(); - assertThat(session.hasComponentPermission(UserRole.ADMIN, componentKey)).isFalse(); + assertThat(session.hasComponentPermission(UserRole.USER, FILE_KEY)).isTrue(); + assertThat(session.hasComponentPermission(UserRole.CODEVIEWER, FILE_KEY)).isFalse(); + assertThat(session.hasComponentPermission(UserRole.ADMIN, FILE_KEY)).isFalse(); } @Test public void has_component_uuid_permission() { - UserSession session = newServerUserSession().setLogin(LOGIN).setUserId(1); - - String componentUuid = FILE_UUID; - when(resourceDao.selectResource(componentUuid)).thenReturn(new ResourceDto().setUuid(componentUuid).setProjectUuid(PROJECT_UUID)); - when(authorizationDao.selectAuthorizedRootProjectsUuids(1, UserRole.USER)).thenReturn(newArrayList(PROJECT_UUID)); + addProjectPermissions(project, UserRole.USER); + UserSession session = newUserSession(userDto); - assertThat(session.hasComponentUuidPermission(UserRole.USER, componentUuid)).isTrue(); - assertThat(session.hasComponentUuidPermission(UserRole.CODEVIEWER, componentUuid)).isFalse(); - assertThat(session.hasComponentUuidPermission(UserRole.ADMIN, componentUuid)).isFalse(); + assertThat(session.hasComponentUuidPermission(UserRole.USER, FILE_UUID)).isTrue(); + assertThat(session.hasComponentUuidPermission(UserRole.CODEVIEWER, FILE_UUID)).isFalse(); + assertThat(session.hasComponentUuidPermission(UserRole.ADMIN, FILE_UUID)).isFalse(); } @Test public void has_component_permission_with_only_global_permission() { - UserSession session = newServerUserSession().setLogin(LOGIN).setUserId(1); + addGlobalPermissions(UserRole.USER); + UserSession session = newUserSession(userDto); - String componentKey = FILE_KEY; - when(resourceDao.getRootProjectByComponentKey(componentKey)).thenReturn(new ResourceDto().setKey(componentKey)); - when(authorizationDao.selectGlobalPermissions(LOGIN)).thenReturn(Arrays.asList(UserRole.USER)); - - assertThat(session.hasComponentPermission(UserRole.USER, componentKey)).isTrue(); - assertThat(session.hasComponentPermission(UserRole.CODEVIEWER, componentKey)).isFalse(); - assertThat(session.hasComponentPermission(UserRole.ADMIN, componentKey)).isFalse(); + assertThat(session.hasComponentPermission(UserRole.USER, FILE_KEY)).isTrue(); + assertThat(session.hasComponentPermission(UserRole.CODEVIEWER, FILE_KEY)).isFalse(); + assertThat(session.hasComponentPermission(UserRole.ADMIN, FILE_KEY)).isFalse(); } @Test public void has_component_uuid_permission_with_only_global_permission() { - UserSession session = newServerUserSession().setLogin(LOGIN).setUserId(1); - - String componentUuid = FILE_UUID; - when(resourceDao.selectResource(componentUuid)).thenReturn(new ResourceDto().setUuid(componentUuid).setProjectUuid(PROJECT_UUID)); - when(authorizationDao.selectGlobalPermissions(LOGIN)).thenReturn(Arrays.asList(UserRole.USER)); + addGlobalPermissions(UserRole.USER); + UserSession session = newUserSession(userDto); - assertThat(session.hasComponentUuidPermission(UserRole.USER, componentUuid)).isTrue(); - assertThat(session.hasComponentUuidPermission(UserRole.CODEVIEWER, componentUuid)).isFalse(); - assertThat(session.hasComponentUuidPermission(UserRole.ADMIN, componentUuid)).isFalse(); + assertThat(session.hasComponentUuidPermission(UserRole.USER, FILE_UUID)).isTrue(); + assertThat(session.hasComponentUuidPermission(UserRole.CODEVIEWER, FILE_UUID)).isFalse(); + assertThat(session.hasComponentUuidPermission(UserRole.ADMIN, FILE_UUID)).isFalse(); } @Test public void check_component_key_permission_ok() { - UserSession session = newServerUserSession().setLogin(LOGIN).setUserId(1); - - when(resourceDao.getRootProjectByComponentKey(FILE_KEY)).thenReturn(new ResourceDto().setKey(PROJECT_KEY)); - when(authorizationDao.selectAuthorizedRootProjectsKeys(1, UserRole.USER)).thenReturn(newArrayList(PROJECT_KEY)); + addProjectPermissions(project, UserRole.USER); + UserSession session = newUserSession(userDto); session.checkComponentPermission(UserRole.USER, FILE_KEY); } @Test public void check_component_key_permission_with_only_global_permission_ok() { - UserSession session = newServerUserSession().setLogin(LOGIN).setUserId(1); - - when(resourceDao.getRootProjectByComponentKey(FILE_KEY)).thenReturn(new ResourceDto().setKey(PROJECT_KEY)); - when(authorizationDao.selectGlobalPermissions(LOGIN)).thenReturn(Arrays.asList(UserRole.USER)); + addGlobalPermissions(UserRole.USER); + UserSession session = newUserSession(userDto); session.checkComponentPermission(UserRole.USER, FILE_KEY); } - @Test(expected = ForbiddenException.class) + @Test public void check_component_key_permission_ko() { - UserSession session = newServerUserSession().setLogin(LOGIN).setUserId(1); - - when(resourceDao.getRootProjectByComponentKey(FILE_KEY)).thenReturn(new ResourceDto().setKey("com.foo:Bar2")); - when(authorizationDao.selectAuthorizedRootProjectsKeys(1, UserRole.USER)).thenReturn(newArrayList(PROJECT_KEY)); + ComponentDto project2 = componentDbTester.insertComponent(ComponentTesting.newProjectDto()); + ComponentDto file2 = componentDbTester.insertComponent(ComponentTesting.newFileDto(project2)); + addProjectPermissions(project, UserRole.USER); + UserSession session = newUserSession(userDto); - session.checkComponentPermission(UserRole.USER, FILE_KEY); + expectedException.expect(ForbiddenException.class); + session.checkComponentPermission(UserRole.USER, file2.getKey()); } @Test public void check_component_uuid_permission_ok() { - UserSession session = newServerUserSession().setLogin(LOGIN).setUserId(1); - - ComponentDto project = ComponentTesting.newProjectDto(); - ComponentDto file = ComponentTesting.newFileDto(project, "file-uuid"); - when(resourceDao.selectResource("file-uuid")).thenReturn(new ResourceDto().setProjectUuid(project.uuid())); - when(authorizationDao.selectAuthorizedRootProjectsUuids(1, UserRole.USER)).thenReturn(newArrayList(project.uuid())); + addProjectPermissions(project, UserRole.USER); + UserSession session = newUserSession(userDto); - session.checkComponentUuidPermission(UserRole.USER, file.uuid()); + session.checkComponentUuidPermission(UserRole.USER, FILE_UUID); } - @Test(expected = ForbiddenException.class) + @Test public void check_component_uuid_permission_ko() { - UserSession session = newServerUserSession().setLogin(LOGIN).setUserId(1); - - ComponentDto project = ComponentTesting.newProjectDto(); - when(resourceDao.selectResource("file-uuid")).thenReturn(new ResourceDto().setProjectUuid(project.uuid())); - when(authorizationDao.selectAuthorizedRootProjectsUuids(1, UserRole.USER)).thenReturn(newArrayList(project.uuid())); + addProjectPermissions(project, UserRole.USER); + UserSession session = newUserSession(userDto); + expectedException.expect(ForbiddenException.class); session.checkComponentUuidPermission(UserRole.USER, "another-uuid"); } - @Test(expected = ForbiddenException.class) + @Test public void check_component_key_permission_when_project_not_found() { - UserSession session = newServerUserSession().setLogin(LOGIN).setUserId(1); - - when(resourceDao.getRootProjectByComponentKey(FILE_KEY)).thenReturn(null); - - session.checkComponentPermission(UserRole.USER, FILE_KEY); + ComponentDto project2 = componentDbTester.insertComponent(ComponentTesting.newProjectDto()); + ComponentDto file2 = componentDbTester.insertComponent(ComponentTesting.newFileDto(project2) + // Simulate file is linked to an invalid project + .setProjectUuid("INVALID")); + addProjectPermissions(project, UserRole.USER); + UserSession session = newUserSession(userDto); + + expectedException.expect(ForbiddenException.class); + session.checkComponentPermission(UserRole.USER, file2.getKey()); } - @Test(expected = ForbiddenException.class) + @Test public void check_component_dto_permission_ko() { - UserSession session = newServerUserSession().setLogin(LOGIN).setUserId(1); - - ComponentDto project = ComponentTesting.newProjectDto(); - when(authorizationDao.selectAuthorizedRootProjectsKeys(1, UserRole.USER)).thenReturn(newArrayList(project.uuid())); + addProjectPermissions(project, UserRole.USER); + UserSession session = newUserSession(userDto); + expectedException.expect(ForbiddenException.class); session.checkComponentPermission(UserRole.USER, "another"); } @Test public void deprecated_has_global_permission() throws Exception { - UserSession session = newServerUserSession().setLogin(LOGIN); - - when(authorizationDao.selectGlobalPermissions(LOGIN)).thenReturn(Arrays.asList("profileadmin", "admin")); + addGlobalPermissions("profileadmin", "admin"); + UserSession session = newUserSession(userDto); - assertThat(session.hasGlobalPermission(GlobalPermissions.QUALITY_PROFILE_ADMIN)).isTrue(); - assertThat(session.hasGlobalPermission(GlobalPermissions.SYSTEM_ADMIN)).isTrue(); - assertThat(session.hasGlobalPermission(GlobalPermissions.DASHBOARD_SHARING)).isFalse(); + assertThat(session.hasGlobalPermission(QUALITY_PROFILE_ADMIN)).isTrue(); + assertThat(session.hasGlobalPermission(SYSTEM_ADMIN)).isTrue(); + assertThat(session.hasGlobalPermission(DASHBOARD_SHARING)).isFalse(); } @Test public void deprecated_check_global_permission() throws Exception { - UserSession session = newServerUserSession().setLogin(LOGIN); + addGlobalPermissions("profileadmin", "admin"); + UserSession session = newUserSession(userDto); - when(authorizationDao.selectGlobalPermissions(LOGIN)).thenReturn(Arrays.asList("profileadmin", "admin")); + session.checkGlobalPermission(QUALITY_PROFILE_ADMIN); + } - session.checkGlobalPermission(GlobalPermissions.QUALITY_PROFILE_ADMIN); + @Test + public void fail_if_user_dto_is_null() throws Exception { + expectedException.expect(NullPointerException.class); + newUserSession(null); + } + + @Test + public void anonymous_user() throws Exception { + UserSession session = newAnonymousSession(); + + assertThat(session.getLogin()).isNull(); + assertThat(session.isLoggedIn()).isFalse(); } - private ServerUserSession newServerUserSession() { - return new ServerUserSession(authorizationDao, resourceDao); + @Test + public void has_global_permission_for_anonymous() throws Exception { + addAnonymousPermissions(null, "profileadmin", "admin"); + UserSession session = newAnonymousSession(); + + assertThat(session.getLogin()).isNull(); + assertThat(session.isLoggedIn()).isFalse(); + + assertThat(session.hasPermission(GlobalPermissions.QUALITY_PROFILE_ADMIN)).isTrue(); + assertThat(session.hasPermission(GlobalPermissions.SYSTEM_ADMIN)).isTrue(); + assertThat(session.hasPermission(GlobalPermissions.DASHBOARD_SHARING)).isFalse(); } + @Test + public void has_project_permission_for_anonymous() throws Exception { + addAnonymousPermissions(project, UserRole.USER); + UserSession session = newAnonymousSession(); + + assertThat(session.hasComponentPermission(UserRole.USER, FILE_KEY)).isTrue(); + assertThat(session.hasComponentPermission(UserRole.CODEVIEWER, FILE_KEY)).isFalse(); + assertThat(session.hasComponentPermission(UserRole.ADMIN, FILE_KEY)).isFalse(); + } + + private ServerUserSession newUserSession(UserDto userDto) { + return createForUser(dbClient, userDto); + } + + private ServerUserSession newAnonymousSession() { + return createForAnonymous(dbClient); + } + + private void addGlobalPermissions(String... permissions) { + addPermissions(null, permissions); + } + + private void addProjectPermissions(ComponentDto component, String... permissions) { + addPermissions(component, permissions); + } + + private void addPermissions( @Nullable ComponentDto component, String... permissions) { + for (String permission : permissions) { + dbClient.roleDao().insertUserRole(dbSession, new UserRoleDto() + .setRole(permission) + .setResourceId(component == null ? null : component.getId()) + .setUserId(userDto.getId())); + } + dbSession.commit(); + } + + private void addAnonymousPermissions(@Nullable ComponentDto component, String... permissions) { + for (String permission : permissions) { + dbClient.roleDao().insertGroupRole(dbSession, new GroupRoleDto() + .setRole(permission) + .setResourceId(component == null ? null : component.getId())); + } + dbSession.commit(); + } + + } diff --git a/server/sonar-server/src/test/java/org/sonar/server/user/UserUpdaterTest.java b/server/sonar-server/src/test/java/org/sonar/server/user/UserUpdaterTest.java index 88968af76e9..2820f85d3f7 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/user/UserUpdaterTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/user/UserUpdaterTest.java @@ -19,6 +19,17 @@ */ package org.sonar.server.user; +import static com.google.common.collect.Lists.newArrayList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.data.MapEntry.entry; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.sonar.api.CoreProperties.CORE_DEFAULT_GROUP; +import static org.sonar.db.user.UserTesting.newDisabledUser; +import static org.sonar.db.user.UserTesting.newUserDto; + import com.google.common.base.Strings; import java.util.List; import org.elasticsearch.search.SearchHit; @@ -46,17 +57,6 @@ import org.sonar.server.user.index.UserIndexDefinition; import org.sonar.server.user.index.UserIndexer; import org.sonar.server.util.Validation; -import static com.google.common.collect.Lists.newArrayList; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.data.MapEntry.entry; -import static org.junit.Assert.fail; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import static org.sonar.api.CoreProperties.CORE_DEFAULT_GROUP; -import static org.sonar.db.user.UserTesting.newDisabledUser; -import static org.sonar.db.user.UserTesting.newUserDto; - public class UserUpdaterTest { static final long NOW = 1418215735482L; @@ -104,7 +104,7 @@ public class UserUpdaterTest { .setLogin("user") .setName("User") .setEmail("user@mail.com") - .setPassword("password") + .setPassword("PASSWORD") .setScmAccounts(newArrayList("u1", "u_1", "User 1"))); UserDto dto = userDao.selectByLogin(session, "user"); @@ -502,8 +502,8 @@ public class UserUpdaterTest { assertThat(dto.getScmAccounts()).isNull(); assertThat(dto.isLocal()).isTrue(); - assertThat(dto.getSalt()).isNotEqualTo("79bd6a8e79fb8c76ac8b121cc7e8e11ad1af8365"); - assertThat(dto.getCryptedPassword()).isNotEqualTo("650d2261c98361e2f67f90ce5c65a95e7d8ea2fg"); + assertThat(dto.getSalt()).isNotNull().isNotEqualTo("79bd6a8e79fb8c76ac8b121cc7e8e11ad1af8365"); + assertThat(dto.getCryptedPassword()).isNotNull().isNotEqualTo("650d2261c98361e2f67f90ce5c65a95e7d8ea2fg"); assertThat(dto.getCreatedAt()).isEqualTo(PAST); assertThat(dto.getUpdatedAt()).isEqualTo(NOW); diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/api/authentication_controller.rb b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/api/authentication_controller.rb index d8194ad7ef6..a55e5363ac9 100644 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/api/authentication_controller.rb +++ b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/api/authentication_controller.rb @@ -58,7 +58,7 @@ class Api::AuthenticationController < Api::ApiController end def anonymous? - !session.has_key?('user_id') + current_user.nil? end def set_cache_buster diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/application_controller.rb b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/application_controller.rb index d1c1682b30e..83b7a6ebff8 100644 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/application_controller.rb +++ b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/application_controller.rb @@ -22,7 +22,7 @@ class ApplicationController < ActionController::Base include AuthenticatedSystem include NeedAuthorization::Helper - before_filter :check_database_version, :set_user_session, :check_authentication + before_filter :check_database_version, :set_i18n, :check_authentication # Required for JRuby 1.7 rescue_from 'Java::JavaLang::Exception', :with => :render_java_exception @@ -92,19 +92,13 @@ class ApplicationController < ActionController::Base end end - def set_user_session + def set_i18n + # TODO Is it really needed to do this ? if params[:locale] I18n.locale = request.compatible_language_from(available_locales, [params[:locale]]) else I18n.locale = request.compatible_language_from(available_locales) end - - if current_user && current_user.id - user_groups_name = current_user.groups.collect {|g| g.name}.to_a - Java::OrgSonarServerUser::RubyUserSession.setSession(current_user.id.to_i, current_user.login, current_user.name, user_groups_name, I18n.locale.to_s) - else - Java::OrgSonarServerUser::RubyUserSession.setSession(nil, nil, nil, nil, I18n.locale.to_s) - end end def check_authentication 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 3032f4038f8..02f70471036 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,16 +8,14 @@ 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_session || login_from_basic_auth) unless @current_user == false + @current_user ||= (login_from_java_user_session || login_from_basic_auth) unless @current_user == false end - # Store the given user id in the session. + # Store the given user def current_user=(new_user) if new_user - session['user_id'] = new_user.id @current_user = new_user else - session['user_id'] = nil @current_user = false end end @@ -120,8 +118,10 @@ module AuthenticatedSystem # # Called from #current_user. First attempt to login by the user id stored in the session. - def login_from_session - self.current_user = User.find_by_id(session['user_id']) if session['user_id'] + def login_from_java_user_session + userSession = Java::OrgSonarServerPlatform::Platform.component(Java::OrgSonarServerUser::UserSession.java_class) + user_id = userSession.getUserId() if userSession && userSession.isLoggedIn() + 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. diff --git a/sonar-db/src/main/java/org/sonar/db/user/UserDto.java b/sonar-db/src/main/java/org/sonar/db/user/UserDto.java index c6331c1c9dc..9b83805dbaa 100644 --- a/sonar-db/src/main/java/org/sonar/db/user/UserDto.java +++ b/sonar-db/src/main/java/org/sonar/db/user/UserDto.java @@ -19,12 +19,15 @@ */ package org.sonar.db.user; +import static java.util.Objects.requireNonNull; + import com.google.common.base.Splitter; import com.google.common.collect.Lists; import java.util.ArrayList; import java.util.List; import javax.annotation.CheckForNull; import javax.annotation.Nullable; +import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.lang.StringUtils; import org.sonar.core.user.DefaultUser; @@ -163,20 +166,22 @@ public class UserDto { return this; } + @CheckForNull public String getCryptedPassword() { return cryptedPassword; } - public UserDto setCryptedPassword(String cryptedPassword) { + public UserDto setCryptedPassword(@Nullable String cryptedPassword) { this.cryptedPassword = cryptedPassword; return this; } + @CheckForNull public String getSalt() { return salt; } - public UserDto setSalt(String salt) { + public UserDto setSalt(@Nullable String salt) { this.salt = salt; return this; } @@ -199,6 +204,12 @@ public class UserDto { return this; } + public static String encryptPassword(String password, String salt) { + requireNonNull(password, "Password cannot be empty"); + requireNonNull(salt, "Salt cannot be empty"); + return DigestUtils.sha1Hex("--" + salt + "--" + password + "--"); + } + public DefaultUser toUser() { return new DefaultUser() .setLogin(login) diff --git a/sonar-db/src/test/java/org/sonar/db/user/UserDtoTest.java b/sonar-db/src/test/java/org/sonar/db/user/UserDtoTest.java index 673c06260a1..08f9d76fe55 100644 --- a/sonar-db/src/test/java/org/sonar/db/user/UserDtoTest.java +++ b/sonar-db/src/test/java/org/sonar/db/user/UserDtoTest.java @@ -19,14 +19,19 @@ */ package org.sonar.db.user; +import static org.assertj.core.api.Assertions.assertThat; + import java.util.Arrays; import java.util.Collections; +import org.junit.Rule; import org.junit.Test; - -import static org.assertj.core.api.Assertions.assertThat; +import org.junit.rules.ExpectedException; public class UserDtoTest { + @Rule + public ExpectedException expectedException = ExpectedException.none(); + @Test public void encode_scm_accounts() { assertThat(UserDto.encodeScmAccounts(null)).isNull(); @@ -41,4 +46,21 @@ public class UserDtoTest { assertThat(UserDto.decodeScmAccounts("\nfoo\n")).containsOnly("foo"); assertThat(UserDto.decodeScmAccounts("\nfoo\nbar\n")).containsOnly("foo", "bar"); } + + @Test + public void encrypt_password() throws Exception { + assertThat(UserDto.encryptPassword("PASSWORD", "0242b0b4c0a93ddfe09dd886de50bc25ba000b51")).isEqualTo("540e4fc4be4e047db995bc76d18374a5b5db08cc"); + } + + @Test + public void fail_to_encrypt_password_when_password_is_null() throws Exception { + expectedException.expect(NullPointerException.class); + UserDto.encryptPassword(null, "salt"); + } + + @Test + public void fail_to_encrypt_password_when_salt_is_null() throws Exception { + expectedException.expect(NullPointerException.class); + UserDto.encryptPassword("password", null); + } } diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/server/authentication/UserIdentity.java b/sonar-plugin-api/src/main/java/org/sonar/api/server/authentication/UserIdentity.java index 6de9f513113..65753247e7c 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/server/authentication/UserIdentity.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/server/authentication/UserIdentity.java @@ -19,6 +19,12 @@ */ package org.sonar.api.server.authentication; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.collect.FluentIterable.from; +import static org.apache.commons.lang.StringUtils.isNotBlank; +import static org.sonar.api.user.UserGroupValidation.validateGroupName; + import com.google.common.base.Predicate; import java.util.HashSet; import java.util.Set; @@ -28,12 +34,6 @@ import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; import org.sonar.api.CoreProperties; -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.collect.FluentIterable.from; -import static org.apache.commons.lang.StringUtils.isNotBlank; -import static org.sonar.api.user.UserGroupValidation.validateGroupName; - /** * User information provided by the Identity Provider to be register into the platform. * @@ -171,7 +171,7 @@ public final class UserIdentity { * @since 5.5 */ public Builder setGroups(Set groups) { - checkNotNull(groups, "Groups cannot be null, please don't this method if groups should not be synchronized."); + checkNotNull(groups, "Groups cannot be null, please don't use this method if groups should not be synchronized."); from(groups).filter(ValidateGroupName.INSTANCE).toList(); this.groupsProvided = true; this.groups = groups; diff --git a/sonar-plugin-api/src/test/java/org/sonar/api/server/authentication/UserIdentityTest.java b/sonar-plugin-api/src/test/java/org/sonar/api/server/authentication/UserIdentityTest.java index 3ef0c9f9bbd..5c41e477d91 100644 --- a/sonar-plugin-api/src/test/java/org/sonar/api/server/authentication/UserIdentityTest.java +++ b/sonar-plugin-api/src/test/java/org/sonar/api/server/authentication/UserIdentityTest.java @@ -19,14 +19,14 @@ */ package org.sonar.api.server.authentication; +import static com.google.common.collect.Sets.newHashSet; +import static org.assertj.core.api.Assertions.assertThat; + import com.google.common.base.Strings; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; -import static com.google.common.collect.Sets.newHashSet; -import static org.assertj.core.api.Assertions.assertThat; - public class UserIdentityTest { @Rule @@ -195,7 +195,7 @@ public class UserIdentityTest { @Test public void fail_when_groups_are_null() throws Exception { thrown.expect(NullPointerException.class); - thrown.expectMessage("Groups cannot be null, please don't this method if groups should not be synchronized."); + thrown.expectMessage("Groups cannot be null, please don't use this method if groups should not be synchronized."); UserIdentity.builder() .setProviderLogin("john")