diff options
author | Matteo Mara <matteo.mara@sonarsource.com> | 2022-05-20 12:18:46 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2022-05-31 20:02:50 +0000 |
commit | 5dc735487ccaa4fd9557a2bc99b1654756c65d8d (patch) | |
tree | 163d8c5faf74096c50a87bc6247c0ffac76333e7 /server/sonar-webserver-auth/src | |
parent | cc058a386950f69d40a6d422f2927c3f92857724 (diff) | |
download | sonarqube-5dc735487ccaa4fd9557a2bc99b1654756c65d8d.tar.gz sonarqube-5dc735487ccaa4fd9557a2bc99b1654756c65d8d.zip |
SONAR-16260 improve project analysis when using project analysis token
Diffstat (limited to 'server/sonar-webserver-auth/src')
13 files changed, 590 insertions, 232 deletions
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/BasicAuthentication.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/BasicAuthentication.java index b5ac6571cba..b95eab152b9 100644 --- a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/BasicAuthentication.java +++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/BasicAuthentication.java @@ -22,8 +22,6 @@ package org.sonar.server.authentication; import java.util.Base64; import java.util.Optional; import javax.servlet.http.HttpServletRequest; -import org.sonar.db.DbClient; -import org.sonar.db.DbSession; import org.sonar.db.user.UserDto; import org.sonar.server.authentication.event.AuthenticationEvent; import org.sonar.server.authentication.event.AuthenticationException; @@ -33,7 +31,6 @@ import static java.nio.charset.StandardCharsets.UTF_8; import static org.apache.commons.lang.StringUtils.startsWithIgnoreCase; import static org.sonar.server.authentication.event.AuthenticationEvent.Method; import static org.sonar.server.authentication.event.AuthenticationEvent.Source; -import static org.sonar.server.usertoken.UserTokenAuthentication.PROJECT_KEY_SCANNER_HEADER; /** * HTTP BASIC authentication relying on tuple {login, password}. @@ -44,24 +41,17 @@ import static org.sonar.server.usertoken.UserTokenAuthentication.PROJECT_KEY_SCA */ public class BasicAuthentication { - private static final String ACCESS_LOG_TOKEN_NAME = "TOKEN_NAME"; - - private final DbClient dbClient; private final CredentialsAuthentication credentialsAuthentication; private final UserTokenAuthentication userTokenAuthentication; - private final AuthenticationEvent authenticationEvent; - public BasicAuthentication(DbClient dbClient, CredentialsAuthentication credentialsAuthentication, - UserTokenAuthentication userTokenAuthentication, AuthenticationEvent authenticationEvent) { - this.dbClient = dbClient; + public BasicAuthentication(CredentialsAuthentication credentialsAuthentication, UserTokenAuthentication userTokenAuthentication) { this.credentialsAuthentication = credentialsAuthentication; this.userTokenAuthentication = userTokenAuthentication; - this.authenticationEvent = authenticationEvent; } public Optional<UserDto> authenticate(HttpServletRequest request) { return extractCredentialsFromHeader(request) - .flatMap(credentials -> Optional.of(authenticate(credentials, request))); + .flatMap(credentials -> Optional.ofNullable(authenticate(credentials, request))); } public static Optional<Credentials> extractCredentialsFromHeader(HttpServletRequest request) { @@ -98,34 +88,17 @@ public class BasicAuthentication { private UserDto authenticate(Credentials credentials, HttpServletRequest request) { if (credentials.getPassword().isEmpty()) { - String projectKeyScannerHeader = request.getHeader(PROJECT_KEY_SCANNER_HEADER); - UserDto userDto = authenticateFromUserToken(credentials.getLogin(), request, projectKeyScannerHeader); - authenticationEvent.loginSuccess(request, userDto.getLogin(), Source.local(Method.BASIC_TOKEN)); - return userDto; - } - return credentialsAuthentication.authenticate(credentials, request, Method.BASIC); - } - - private UserDto authenticateFromUserToken(String token, HttpServletRequest request, String projectKey) { - String path = request.getRequestURI().substring(request.getContextPath().length()); - UserTokenAuthentication.UserTokenAuthenticationResult result = userTokenAuthentication.authenticate(token, path, projectKey); - if (result.getErrorMessage() != null) { - throw AuthenticationException.newBuilder() - .setSource(Source.local(Method.BASIC_TOKEN)) - .setMessage(result.getErrorMessage()) - .build(); - } - try (DbSession dbSession = dbClient.openSession(false)) { - UserDto userDto = dbClient.userDao().selectByUuid(dbSession, result.getAuthenticatedUserUuid()); - if (userDto == null || !userDto.isActive()) { + Optional<UserAuthResult> userAuthResult = userTokenAuthentication.authenticate(request); + if (userAuthResult.isPresent()) { + return userAuthResult.get().getUserDto(); + } else { throw AuthenticationException.newBuilder() - .setSource(Source.local(Method.BASIC_TOKEN)) + .setSource(AuthenticationEvent.Source.local(AuthenticationEvent.Method.BASIC_TOKEN)) .setMessage("User doesn't exist") .build(); } - request.setAttribute(ACCESS_LOG_TOKEN_NAME, result.getTokenName()); - return userDto; } + return credentialsAuthentication.authenticate(credentials, request, Method.BASIC); } } diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/HttpHeadersAuthentication.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/HttpHeadersAuthentication.java index 1562bd7e3b3..f24e9d721ce 100644 --- a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/HttpHeadersAuthentication.java +++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/HttpHeadersAuthentication.java @@ -20,7 +20,6 @@ package org.sonar.server.authentication; import com.google.common.base.Splitter; -import com.google.common.collect.ImmutableMap; import java.util.Collections; import java.util.Date; import java.util.EnumSet; @@ -133,14 +132,14 @@ public class HttpHeadersAuthentication implements Startable { } UserDto userDto = doAuthenticate(headerValuesByNames, login); - jwtHttpHandler.generateToken(userDto, ImmutableMap.of(LAST_REFRESH_TIME_TOKEN_PARAM, system2.now()), request, response); + jwtHttpHandler.generateToken(userDto, Map.of(LAST_REFRESH_TIME_TOKEN_PARAM, system2.now()), request, response); authenticationEvent.loginSuccess(request, userDto.getLogin(), Source.sso()); return Optional.of(userDto); } private Optional<UserDto> getUserFromToken(HttpServletRequest request, HttpServletResponse response) { Optional<JwtHttpHandler.Token> token = jwtHttpHandler.getToken(request, response); - if (!token.isPresent()) { + if (token.isEmpty()) { return Optional.empty(); } Date now = new Date(system2.now()); diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/RequestAuthenticatorImpl.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/RequestAuthenticatorImpl.java index 4f7871f594a..ade1d51f905 100644 --- a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/RequestAuthenticatorImpl.java +++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/RequestAuthenticatorImpl.java @@ -27,30 +27,41 @@ import javax.servlet.http.HttpServletResponse; import org.sonar.db.user.UserDto; import org.sonar.server.user.UserSession; import org.sonar.server.user.UserSessionFactory; +import org.sonar.server.usertoken.UserTokenAuthentication; import org.springframework.beans.factory.annotation.Autowired; +import static java.util.Objects.nonNull; +import static org.sonar.server.authentication.UserAuthResult.AuthType.BASIC; +import static org.sonar.server.authentication.UserAuthResult.AuthType.JWT; +import static org.sonar.server.authentication.UserAuthResult.AuthType.SSO; +import static org.sonar.server.authentication.UserAuthResult.AuthType.TOKEN; + public class RequestAuthenticatorImpl implements RequestAuthenticator { private final JwtHttpHandler jwtHttpHandler; private final BasicAuthentication basicAuthentication; + private final UserTokenAuthentication userTokenAuthentication; private final HttpHeadersAuthentication httpHeadersAuthentication; private final UserSessionFactory userSessionFactory; private final List<CustomAuthentication> customAuthentications; @Autowired(required = false) - public RequestAuthenticatorImpl(JwtHttpHandler jwtHttpHandler, BasicAuthentication basicAuthentication, HttpHeadersAuthentication httpHeadersAuthentication, + public RequestAuthenticatorImpl(JwtHttpHandler jwtHttpHandler, BasicAuthentication basicAuthentication, UserTokenAuthentication userTokenAuthentication, + HttpHeadersAuthentication httpHeadersAuthentication, UserSessionFactory userSessionFactory, CustomAuthentication[] customAuthentications) { this.jwtHttpHandler = jwtHttpHandler; this.basicAuthentication = basicAuthentication; + this.userTokenAuthentication = userTokenAuthentication; this.httpHeadersAuthentication = httpHeadersAuthentication; this.userSessionFactory = userSessionFactory; this.customAuthentications = Arrays.asList(customAuthentications); } @Autowired(required = false) - public RequestAuthenticatorImpl(JwtHttpHandler jwtHttpHandler, BasicAuthentication basicAuthentication, HttpHeadersAuthentication httpHeadersAuthentication, + public RequestAuthenticatorImpl(JwtHttpHandler jwtHttpHandler, BasicAuthentication basicAuthentication, UserTokenAuthentication userTokenAuthentication, + HttpHeadersAuthentication httpHeadersAuthentication, UserSessionFactory userSessionFactory) { - this(jwtHttpHandler, basicAuthentication, httpHeadersAuthentication, userSessionFactory, new CustomAuthentication[0]); + this(jwtHttpHandler, basicAuthentication, userTokenAuthentication, httpHeadersAuthentication, userSessionFactory, new CustomAuthentication[0]); } @Override @@ -62,25 +73,38 @@ public class RequestAuthenticatorImpl implements RequestAuthenticator { } } - Optional<UserDto> userOpt = loadUser(request, response); - if (userOpt.isPresent()) { - return userSessionFactory.create(userOpt.get()); + UserAuthResult userAuthResult = loadUser(request, response); + if (nonNull(userAuthResult.getUserDto())) { + if (TOKEN.equals(userAuthResult.getAuthType())) { + return userSessionFactory.create(userAuthResult.getUserDto(), userAuthResult.getTokenDto()); + } + return userSessionFactory.create(userAuthResult.getUserDto()); } return userSessionFactory.createAnonymous(); } - private Optional<UserDto> loadUser(HttpServletRequest request, HttpServletResponse response) { + private UserAuthResult loadUser(HttpServletRequest request, HttpServletResponse response) { // Try first to authenticate from SSO, then JWT token, then try from basic http header // SSO authentication should come first in order to update JWT if user from header is not the same is user from JWT Optional<UserDto> user = httpHeadersAuthentication.authenticate(request, response); if (user.isPresent()) { - return user; + return new UserAuthResult(user.get(), SSO); } user = jwtHttpHandler.validateToken(request, response); if (user.isPresent()) { - return user; + return new UserAuthResult(user.get(), JWT); + } + + // Check if the authentication is token based + Optional<UserAuthResult> userAuthResult = userTokenAuthentication.authenticate(request); + if (userAuthResult.isPresent()) { + return userAuthResult.get(); } - return basicAuthentication.authenticate(request); + + user = basicAuthentication.authenticate(request); + return user.map(userDto -> new UserAuthResult(userDto, BASIC)) + .orElseGet(UserAuthResult::new); } + } diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/UserAuthResult.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/UserAuthResult.java new file mode 100644 index 00000000000..7f5cafcbecc --- /dev/null +++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/UserAuthResult.java @@ -0,0 +1,63 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.authentication; + +import org.sonar.db.user.UserDto; +import org.sonar.db.user.UserTokenDto; + +public class UserAuthResult { + + public enum AuthType { + SSO, + JWT, + TOKEN, + BASIC + } + + UserDto userDto; + UserTokenDto tokenDto; + AuthType authType; + + public UserAuthResult() { + } + + public UserAuthResult(UserDto userDto, AuthType authType) { + this.userDto = userDto; + this.authType = authType; + } + + public UserAuthResult(UserDto userDto, UserTokenDto tokenDto, AuthType authType) { + this.userDto = userDto; + this.tokenDto = tokenDto; + this.authType = authType; + } + + public UserDto getUserDto() { + return userDto; + } + + public AuthType getAuthType() { + return authType; + } + + public UserTokenDto getTokenDto() { + return tokenDto; + } +} diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/user/TokenUserSession.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/user/TokenUserSession.java new file mode 100644 index 00000000000..7f0b730aa7e --- /dev/null +++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/user/TokenUserSession.java @@ -0,0 +1,76 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 SonarSource SA + * mailto:info 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 org.sonar.db.DbClient; +import org.sonar.db.permission.GlobalPermission; +import org.sonar.db.user.TokenType; +import org.sonar.db.user.UserDto; +import org.sonar.db.user.UserTokenDto; + +public class TokenUserSession extends ServerUserSession { + + private static final String SCAN = "scan"; + private final UserTokenDto userToken; + + public TokenUserSession(DbClient dbClient, UserDto user, UserTokenDto userToken) { + super(dbClient, user); + this.userToken = userToken; + } + + @Override + protected boolean hasProjectUuidPermission(String permission, String projectUuid) { + TokenType tokenType = TokenType.valueOf(userToken.getType()); + switch (tokenType) { + case USER_TOKEN: + return super.hasProjectUuidPermission(permission, projectUuid); + case PROJECT_ANALYSIS_TOKEN: + return SCAN.equals(permission) && + projectUuid.equals(userToken.getProjectUuid()) && + (super.hasProjectUuidPermission(SCAN, projectUuid) || super.hasPermissionImpl(GlobalPermission.SCAN)); + case GLOBAL_ANALYSIS_TOKEN: + //The case with a global analysis token has to return false always, since it is based on the assumption that the user + // has global analysis privileges + return false; + default: + throw new IllegalArgumentException("Unsupported token type " + tokenType.name()); + } + + } + + @Override + protected boolean hasPermissionImpl(GlobalPermission permission) { + TokenType tokenType = TokenType.valueOf(userToken.getType()); + switch (tokenType) { + case USER_TOKEN: + return super.hasPermissionImpl(permission); + case PROJECT_ANALYSIS_TOKEN: + //The case with a project analysis token has to return false always, delegating the result to the super class would allow + //the project analysis token to work for multiple projects in case the user has Global Permissions. + return false; + case GLOBAL_ANALYSIS_TOKEN: + return GlobalPermission.SCAN.equals(permission) && + super.hasPermissionImpl(permission); + default: + throw new IllegalArgumentException("Unsupported token type " + tokenType.name()); + } + } + +} diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/user/UserSessionFactory.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/user/UserSessionFactory.java index 1995b8cfbee..c7cf98fccd7 100644 --- a/server/sonar-webserver-auth/src/main/java/org/sonar/server/user/UserSessionFactory.java +++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/user/UserSessionFactory.java @@ -21,12 +21,15 @@ package org.sonar.server.user; import org.sonar.api.server.ServerSide; import org.sonar.db.user.UserDto; +import org.sonar.db.user.UserTokenDto; @ServerSide public interface UserSessionFactory { UserSession create(UserDto user); + UserSession create(UserDto user, UserTokenDto userToken); + UserSession createAnonymous(); } diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/user/UserSessionFactoryImpl.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/user/UserSessionFactoryImpl.java index 3e049d4ae23..ca32c8afd51 100644 --- a/server/sonar-webserver-auth/src/main/java/org/sonar/server/user/UserSessionFactoryImpl.java +++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/user/UserSessionFactoryImpl.java @@ -22,6 +22,7 @@ package org.sonar.server.user; import org.sonar.api.server.ServerSide; import org.sonar.db.DbClient; import org.sonar.db.user.UserDto; +import org.sonar.db.user.UserTokenDto; import org.sonar.server.authentication.UserLastConnectionDatesUpdater; import static java.util.Objects.requireNonNull; @@ -45,6 +46,14 @@ public class UserSessionFactoryImpl implements UserSessionFactory { } @Override + public TokenUserSession create(UserDto user, UserTokenDto userToken) { + requireNonNull(user, "UserDto must not be null"); + requireNonNull(userToken, "UserTokenDto must not be null"); + userLastConnectionDatesUpdater.updateLastConnectionDateIfNeeded(user); + return new TokenUserSession(dbClient, user, userToken); + } + + @Override public ServerUserSession createAnonymous() { return new ServerUserSession(dbClient, null); } diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/UserTokenAuthentication.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/UserTokenAuthentication.java index 77eba8c2def..be066b6a422 100644 --- a/server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/UserTokenAuthentication.java +++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/UserTokenAuthentication.java @@ -19,120 +19,88 @@ */ package org.sonar.server.usertoken; -import java.util.EnumMap; -import java.util.Set; +import java.util.Optional; import javax.annotation.Nullable; +import javax.servlet.http.HttpServletRequest; import org.sonar.db.DbClient; import org.sonar.db.DbSession; -import org.sonar.db.user.TokenType; +import org.sonar.db.user.UserDto; import org.sonar.db.user.UserTokenDto; +import org.sonar.server.authentication.Credentials; +import org.sonar.server.authentication.UserAuthResult; import org.sonar.server.authentication.UserLastConnectionDatesUpdater; +import org.sonar.server.authentication.event.AuthenticationEvent; +import org.sonar.server.authentication.event.AuthenticationException; +import org.sonar.server.exceptions.NotFoundException; -public class UserTokenAuthentication { - - public static final String PROJECT_KEY_SCANNER_HEADER = "PROJECT_KEY"; - - private static final Set<String> SCANNER_ENDPOINTS = Set.of( - "/api/settings/values", - "/api/plugins/installed", - "/api/analysis_cache/get", - "/api/project_branches/list", - "/api/project_pull_requests/list", - "/api/qualityprofiles/search", - "/api/rules/search", - "/batch/project", - "/api/metrics/search", - "/api/new_code_periods/show", - "/api/ce/submit"); +import static org.sonar.server.authentication.BasicAuthentication.extractCredentialsFromHeader; - private static final EnumMap<TokenType, Set<String>> ALLOWLIST_ENDPOINTS_FOR_TOKEN_TYPES = new EnumMap<>(TokenType.class); - - static { - ALLOWLIST_ENDPOINTS_FOR_TOKEN_TYPES.put(TokenType.GLOBAL_ANALYSIS_TOKEN, SCANNER_ENDPOINTS); - ALLOWLIST_ENDPOINTS_FOR_TOKEN_TYPES.put(TokenType.PROJECT_ANALYSIS_TOKEN, SCANNER_ENDPOINTS); - } +public class UserTokenAuthentication { + private static final String ACCESS_LOG_TOKEN_NAME = "TOKEN_NAME"; private final TokenGenerator tokenGenerator; private final DbClient dbClient; private final UserLastConnectionDatesUpdater userLastConnectionDatesUpdater; + private final AuthenticationEvent authenticationEvent; - public UserTokenAuthentication(TokenGenerator tokenGenerator, DbClient dbClient, UserLastConnectionDatesUpdater userLastConnectionDatesUpdater) { + public UserTokenAuthentication(TokenGenerator tokenGenerator, DbClient dbClient, UserLastConnectionDatesUpdater userLastConnectionDatesUpdater, + AuthenticationEvent authenticationEvent) { this.tokenGenerator = tokenGenerator; this.dbClient = dbClient; this.userLastConnectionDatesUpdater = userLastConnectionDatesUpdater; + this.authenticationEvent = authenticationEvent; } - /** - * Returns the user token details including if the token hash is found and the user has provided valid token type. - * - * The returned uuid included in the UserTokenAuthenticationResult is not validated. If database is corrupted - * (table USER_TOKENS badly purged for instance), then the uuid may not relate to a valid user. - * - * In case of any issues only the error message is included in UserTokenAuthenticationResult - */ - public UserTokenAuthenticationResult authenticate(String token, String requestedEndpoint, @Nullable String analyzedProjectKey) { - String tokenHash = tokenGenerator.hash(token); - try (DbSession dbSession = dbClient.openSession(false)) { - UserTokenDto userToken = dbClient.userTokenDao().selectByTokenHash(dbSession, tokenHash); - if (userToken == null) { - return new UserTokenAuthenticationResult("Token doesn't exist"); - } - if (!isValidTokenType(userToken, analyzedProjectKey, requestedEndpoint)) { - return new UserTokenAuthenticationResult("Invalid token"); + public Optional<UserAuthResult> authenticate(HttpServletRequest request) { + if (isTokenBasedAuthentication(request)) { + Optional<Credentials> credentials = extractCredentialsFromHeader(request); + if (credentials.isPresent()) { + UserAuthResult userAuthResult = authenticateFromUserToken(credentials.get().getLogin(), request); + authenticationEvent.loginSuccess(request, userAuthResult.getUserDto().getLogin(), AuthenticationEvent.Source.local(AuthenticationEvent.Method.BASIC_TOKEN)); + return Optional.of(userAuthResult); } - userLastConnectionDatesUpdater.updateLastConnectionDateIfNeeded(userToken); - return new UserTokenAuthenticationResult(userToken.getUserUuid(), userToken.getName()); } + return Optional.empty(); } - private static boolean isValidTokenType(UserTokenDto userToken, @Nullable String analyzedProjectKey, String requestedEndpoint) { - TokenType tokenType = TokenType.valueOf(userToken.getType()); - - return validateProjectKeyForScannerToken(tokenType, userToken, analyzedProjectKey) - && shouldBeAbleToAccessEndpoint(tokenType, requestedEndpoint); + public static boolean isTokenBasedAuthentication(HttpServletRequest request) { + Optional<Credentials> credentialsOptional = extractCredentialsFromHeader(request); + return credentialsOptional.map(credentials -> credentials.getPassword().isEmpty()).orElse(false); } - private static boolean shouldBeAbleToAccessEndpoint(TokenType tokenType, String requestedEndpoint) { - Set<String> allowedEndpoints = ALLOWLIST_ENDPOINTS_FOR_TOKEN_TYPES.get(tokenType); - if (allowedEndpoints == null) { - return true; // no allowlist configured for the token type - all endpoints are allowed + private UserAuthResult authenticateFromUserToken(String token, HttpServletRequest request) { + try (DbSession dbSession = dbClient.openSession(false)) { + UserTokenDto userToken = authenticate(token); + UserDto userDto = dbClient.userDao().selectByUuid(dbSession, userToken.getUserUuid()); + if (userDto == null || !userDto.isActive()) { + throw AuthenticationException.newBuilder() + .setSource(AuthenticationEvent.Source.local(AuthenticationEvent.Method.BASIC_TOKEN)) + .setMessage("User doesn't exist") + .build(); + } + request.setAttribute(ACCESS_LOG_TOKEN_NAME, userToken.getName()); + return new UserAuthResult(userDto, userToken, UserAuthResult.AuthType.TOKEN); + } catch (NotFoundException exception) { + throw AuthenticationException.newBuilder() + .setSource(AuthenticationEvent.Source.local(AuthenticationEvent.Method.BASIC_TOKEN)) + .setMessage(exception.getMessage()) + .build(); } - return allowedEndpoints.stream().anyMatch(requestedEndpoint::startsWith); } - private static boolean validateProjectKeyForScannerToken(TokenType tokenType, UserTokenDto userToken, @Nullable String analyzedProjectKey) { - if (tokenType != TokenType.PROJECT_ANALYSIS_TOKEN) { - return true; + private UserTokenDto authenticate(String token) { + UserTokenDto userToken = getUserToken(token); + if (userToken == null) { + throw new NotFoundException("Token doesn't exist"); } - return analyzedProjectKey != null && analyzedProjectKey.equals(userToken.getProjectKey()); + userLastConnectionDatesUpdater.updateLastConnectionDateIfNeeded(userToken); + return userToken; } - public static class UserTokenAuthenticationResult { - - String authenticatedUserUuid; - String errorMessage; - String tokenName; - - public UserTokenAuthenticationResult(String authenticatedUserUuid, String tokenName) { - this.authenticatedUserUuid = authenticatedUserUuid; - this.tokenName = tokenName; - } - - public UserTokenAuthenticationResult(String errorMessage) { - this.errorMessage = errorMessage; - } - - public String getAuthenticatedUserUuid() { - return authenticatedUserUuid; - } - - public String getErrorMessage() { - return errorMessage; - } - - public String getTokenName() { - return tokenName; + @Nullable + public UserTokenDto getUserToken(String token) { + try (DbSession dbSession = dbClient.openSession(false)) { + return dbClient.userTokenDao().selectByTokenHash(dbSession, tokenGenerator.hash(token)); } - } } diff --git a/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/BasicAuthenticationTest.java b/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/BasicAuthenticationTest.java index 05c2231f4c7..01cde7bb264 100644 --- a/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/BasicAuthenticationTest.java +++ b/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/BasicAuthenticationTest.java @@ -26,10 +26,10 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.sonar.api.utils.System2; -import org.sonar.db.DbClient; import org.sonar.db.DbTester; import org.sonar.db.user.UserDto; import org.sonar.db.user.UserTesting; +import org.sonar.db.user.UserTokenDto; import org.sonar.server.authentication.event.AuthenticationEvent; import org.sonar.server.authentication.event.AuthenticationException; import org.sonar.server.usertoken.UserTokenAuthentication; @@ -59,20 +59,19 @@ public class BasicAuthenticationTest { private static final UserDto USER = UserTesting.newUserDto().setLogin(A_LOGIN); private static final String EXAMPLE_ENDPOINT = "/api/ce/submit"; + private static final String AUTHORIZATION_HEADER = "Authorization"; @Rule public DbTester db = DbTester.create(System2.INSTANCE); - private DbClient dbClient = db.getDbClient(); + private final CredentialsAuthentication credentialsAuthentication = mock(CredentialsAuthentication.class); + private final UserTokenAuthentication userTokenAuthentication = mock(UserTokenAuthentication.class); - private CredentialsAuthentication credentialsAuthentication = mock(CredentialsAuthentication.class); - private UserTokenAuthentication userTokenAuthentication = mock(UserTokenAuthentication.class); + private final HttpServletRequest request = mock(HttpServletRequest.class); - private HttpServletRequest request = mock(HttpServletRequest.class); + private final AuthenticationEvent authenticationEvent = mock(AuthenticationEvent.class); - private AuthenticationEvent authenticationEvent = mock(AuthenticationEvent.class); - - private BasicAuthentication underTest = new BasicAuthentication(dbClient, credentialsAuthentication, userTokenAuthentication, authenticationEvent); + private final BasicAuthentication underTest = new BasicAuthentication(credentialsAuthentication, userTokenAuthentication); @Before public void before() { @@ -83,7 +82,7 @@ public class BasicAuthenticationTest { @Test public void authenticate_from_basic_http_header() { - when(request.getHeader("Authorization")).thenReturn("Basic " + CREDENTIALS_IN_BASE64); + when(request.getHeader(AUTHORIZATION_HEADER)).thenReturn("Basic " + CREDENTIALS_IN_BASE64); Credentials credentials = new Credentials(A_LOGIN, A_PASSWORD); when(credentialsAuthentication.authenticate(credentials, request, BASIC)).thenReturn(USER); @@ -96,7 +95,7 @@ public class BasicAuthenticationTest { @Test public void authenticate_from_basic_http_header_with_password_containing_semi_colon() { String password = "!ascii-only:-)@"; - when(request.getHeader("Authorization")).thenReturn("Basic " + toBase64(A_LOGIN + ":" + password)); + when(request.getHeader(AUTHORIZATION_HEADER)).thenReturn("Basic " + toBase64(A_LOGIN + ":" + password)); when(credentialsAuthentication.authenticate(new Credentials(A_LOGIN, password), request, BASIC)).thenReturn(USER); underTest.authenticate(request); @@ -114,7 +113,7 @@ public class BasicAuthenticationTest { @Test public void does_not_authenticate_when_authorization_header_is_not_BASIC() { - when(request.getHeader("Authorization")).thenReturn("OTHER " + CREDENTIALS_IN_BASE64); + when(request.getHeader(AUTHORIZATION_HEADER)).thenReturn("OTHER " + CREDENTIALS_IN_BASE64); underTest.authenticate(request); @@ -123,7 +122,7 @@ public class BasicAuthenticationTest { @Test public void fail_to_authenticate_when_no_login() { - when(request.getHeader("Authorization")).thenReturn("Basic " + toBase64(":" + A_PASSWORD)); + when(request.getHeader(AUTHORIZATION_HEADER)).thenReturn("Basic " + toBase64(":" + A_PASSWORD)); assertThatThrownBy(() -> underTest.authenticate(request)) .isInstanceOf(AuthenticationException.class) @@ -134,7 +133,7 @@ public class BasicAuthenticationTest { @Test public void fail_to_authenticate_when_invalid_header() { - when(request.getHeader("Authorization")).thenReturn("Basic Invàlid"); + when(request.getHeader(AUTHORIZATION_HEADER)).thenReturn("Basic Invàlid"); assertThatThrownBy(() -> underTest.authenticate(request)) .hasMessage("Invalid basic header") @@ -145,26 +144,22 @@ public class BasicAuthenticationTest { @Test public void authenticate_from_user_token() { UserDto user = db.users().insertUser(); - var result = new UserTokenAuthentication.UserTokenAuthenticationResult(user.getUuid(), "my-token"); - when(userTokenAuthentication.authenticate("token", EXAMPLE_ENDPOINT, null)).thenReturn(result); - when(request.getHeader("Authorization")).thenReturn("Basic " + toBase64("token:")); + when(userTokenAuthentication.authenticate(request)).thenReturn(Optional.of(new UserAuthResult(user, new UserTokenDto().setName("my-token"), UserAuthResult.AuthType.TOKEN))); + when(request.getHeader(AUTHORIZATION_HEADER)).thenReturn("Basic " + toBase64("token:")); Optional<UserDto> userAuthenticated = underTest.authenticate(request); assertThat(userAuthenticated).isPresent(); assertThat(userAuthenticated.get().getLogin()).isEqualTo(user.getLogin()); - verify(authenticationEvent).loginSuccess(request, user.getLogin(), Source.local(BASIC_TOKEN)); - verify(request).setAttribute("TOKEN_NAME", "my-token"); } @Test public void does_not_authenticate_from_user_token_when_token_is_invalid() { - var result = new UserTokenAuthentication.UserTokenAuthenticationResult("Token doesn't exist"); - when(userTokenAuthentication.authenticate("token", EXAMPLE_ENDPOINT, null)).thenReturn(result); - when(request.getHeader("Authorization")).thenReturn("Basic " + toBase64("token:")); + when(userTokenAuthentication.authenticate(request)).thenReturn(Optional.empty()); + when(request.getHeader(AUTHORIZATION_HEADER)).thenReturn("Basic " + toBase64("token:")); assertThatThrownBy(() -> underTest.authenticate(request)) - .hasMessage("Token doesn't exist") + .hasMessage("User doesn't exist") .isInstanceOf(AuthenticationException.class) .hasFieldOrPropertyWithValue("source", Source.local(BASIC_TOKEN)); @@ -174,24 +169,11 @@ public class BasicAuthenticationTest { @Test public void does_not_authenticate_from_user_token_when_token_does_not_match_existing_user() { - var result = new UserTokenAuthentication.UserTokenAuthenticationResult("unknown-user-uuid", "my-token"); - when(userTokenAuthentication.authenticate("token", EXAMPLE_ENDPOINT, null)).thenReturn(result); - when(request.getHeader("Authorization")).thenReturn("Basic " + toBase64("token:")); - - assertThatThrownBy(() -> underTest.authenticate(request)) - .hasMessageContaining("User doesn't exist") - .isInstanceOf(AuthenticationException.class) - .hasFieldOrPropertyWithValue("source", Source.local(BASIC_TOKEN)); - - verifyNoInteractions(authenticationEvent); - } - - @Test - public void does_not_authenticate_from_user_token_when_token_does_not_match_active_user() { - UserDto user = db.users().insertDisabledUser(); - var result = new UserTokenAuthentication.UserTokenAuthenticationResult(user.getUuid(), "my-token"); - when(userTokenAuthentication.authenticate("token", EXAMPLE_ENDPOINT, null)).thenReturn(result); - when(request.getHeader("Authorization")).thenReturn("Basic " + toBase64("token:")); + when(userTokenAuthentication.authenticate(request)).thenThrow(AuthenticationException.newBuilder() + .setSource(AuthenticationEvent.Source.local(AuthenticationEvent.Method.BASIC_TOKEN)) + .setMessage("User doesn't exist") + .build()); + when(request.getHeader(AUTHORIZATION_HEADER)).thenReturn("Basic " + toBase64("token:")); assertThatThrownBy(() -> underTest.authenticate(request)) .hasMessageContaining("User doesn't exist") diff --git a/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/RequestAuthenticatorImplTest.java b/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/RequestAuthenticatorImplTest.java index 2f79f6028ca..9fa95233e55 100644 --- a/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/RequestAuthenticatorImplTest.java +++ b/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/RequestAuthenticatorImplTest.java @@ -25,12 +25,14 @@ import javax.servlet.http.HttpServletResponse; import org.junit.Before; import org.junit.Test; import org.sonar.db.user.UserDto; +import org.sonar.db.user.UserTokenDto; import org.sonar.server.authentication.event.AuthenticationEvent; import org.sonar.server.authentication.event.AuthenticationException; import org.sonar.server.tester.AnonymousMockUserSession; import org.sonar.server.tester.MockUserSession; import org.sonar.server.user.UserSession; import org.sonar.server.user.UserSessionFactory; +import org.sonar.server.usertoken.UserTokenAuthentication; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.anyInt; @@ -38,26 +40,30 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.sonar.db.user.TokenType.USER_TOKEN; import static org.sonar.db.user.UserTesting.newUserDto; public class RequestAuthenticatorImplTest { private static final UserDto A_USER = newUserDto(); - - private HttpServletRequest request = mock(HttpServletRequest.class); - private HttpServletResponse response = mock(HttpServletResponse.class); - private JwtHttpHandler jwtHttpHandler = mock(JwtHttpHandler.class); - private BasicAuthentication basicAuthentication = mock(BasicAuthentication.class); - private HttpHeadersAuthentication httpHeadersAuthentication = mock(HttpHeadersAuthentication.class); - private UserSessionFactory sessionFactory = mock(UserSessionFactory.class); - private CustomAuthentication customAuthentication1 = mock(CustomAuthentication.class); - private CustomAuthentication customAuthentication2 = mock(CustomAuthentication.class); - private RequestAuthenticator underTest = new RequestAuthenticatorImpl(jwtHttpHandler, basicAuthentication, httpHeadersAuthentication, sessionFactory, + private static final UserTokenDto A_USER_TOKEN = mockUserTokenDto(A_USER); + + private final HttpServletRequest request = mock(HttpServletRequest.class); + private final HttpServletResponse response = mock(HttpServletResponse.class); + private final JwtHttpHandler jwtHttpHandler = mock(JwtHttpHandler.class); + private final BasicAuthentication basicAuthentication = mock(BasicAuthentication.class); + private final UserTokenAuthentication userTokenAuthentication = mock(UserTokenAuthentication.class); + private final HttpHeadersAuthentication httpHeadersAuthentication = mock(HttpHeadersAuthentication.class); + private final UserSessionFactory sessionFactory = mock(UserSessionFactory.class); + private final CustomAuthentication customAuthentication1 = mock(CustomAuthentication.class); + private final CustomAuthentication customAuthentication2 = mock(CustomAuthentication.class); + private final RequestAuthenticator underTest = new RequestAuthenticatorImpl(jwtHttpHandler, basicAuthentication, userTokenAuthentication, httpHeadersAuthentication, sessionFactory, new CustomAuthentication[]{customAuthentication1, customAuthentication2}); @Before public void setUp() { when(sessionFactory.create(A_USER)).thenReturn(new MockUserSession(A_USER)); + when(sessionFactory.create(A_USER, A_USER_TOKEN)).thenReturn(new MockUserSession(A_USER)); when(sessionFactory.createAnonymous()).thenReturn(new AnonymousMockUserSession()); } @@ -84,6 +90,21 @@ public class RequestAuthenticatorImplTest { } @Test + public void authenticate_from_basic_token() { + when(request.getHeader("Authorization")).thenReturn("Basic dGVzdDo="); + when(userTokenAuthentication.getUserToken("test")).thenReturn(A_USER_TOKEN); + when(userTokenAuthentication.authenticate(request)).thenReturn(Optional.of(new UserAuthResult(A_USER, A_USER_TOKEN, UserAuthResult.AuthType.TOKEN))); + when(httpHeadersAuthentication.authenticate(request, response)).thenReturn(Optional.empty()); + when(jwtHttpHandler.validateToken(request, response)).thenReturn(Optional.empty()); + + assertThat(underTest.authenticate(request, response).getUuid()).isEqualTo(A_USER.getUuid()); + + verify(jwtHttpHandler).validateToken(request, response); + verify(userTokenAuthentication).authenticate(request); + verify(response, never()).setStatus(anyInt()); + } + + @Test public void authenticate_from_sso() { when(httpHeadersAuthentication.authenticate(request, response)).thenReturn(Optional.of(A_USER)); when(jwtHttpHandler.validateToken(request, response)).thenReturn(Optional.empty()); @@ -131,4 +152,13 @@ public class RequestAuthenticatorImplTest { assertThat(session.getLogin()).isEqualTo("foo"); } + + private static UserTokenDto mockUserTokenDto(UserDto userDto) { + UserTokenDto userTokenDto = new UserTokenDto(); + userTokenDto.setType(USER_TOKEN.name()); + userTokenDto.setName("User Token"); + userTokenDto.setUserUuid(userDto.getUuid()); + return userTokenDto; + } + } diff --git a/server/sonar-webserver-auth/src/test/java/org/sonar/server/user/TokenUserSessionTest.java b/server/sonar-webserver-auth/src/test/java/org/sonar/server/user/TokenUserSessionTest.java new file mode 100644 index 00000000000..6ffe52c179c --- /dev/null +++ b/server/sonar-webserver-auth/src/test/java/org/sonar/server/user/TokenUserSessionTest.java @@ -0,0 +1,170 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 SonarSource SA + * mailto:info 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 org.junit.Rule; +import org.junit.Test; +import org.sonar.api.utils.System2; +import org.sonar.db.DbClient; +import org.sonar.db.DbTester; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.permission.GlobalPermission; +import org.sonar.db.user.UserDto; +import org.sonar.db.user.UserTokenDto; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.api.web.UserRole.SCAN; +import static org.sonar.db.user.TokenType.GLOBAL_ANALYSIS_TOKEN; +import static org.sonar.db.user.TokenType.PROJECT_ANALYSIS_TOKEN; +import static org.sonar.db.user.TokenType.USER_TOKEN; + +public class TokenUserSessionTest { + + @Rule + public final DbTester db = DbTester.create(System2.INSTANCE); + private final DbClient dbClient = db.getDbClient(); + + @Test + public void test_hasProjectsPermission_for_UserToken() { + ComponentDto project1 = db.components().insertPrivateProject(); + ComponentDto project2 = db.components().insertPrivateProject(); + + UserDto user = db.users().insertUser(); + + db.users().insertProjectPermissionOnUser(user, SCAN, project1); + + TokenUserSession userSession = mockTokenUserSession(user); + + assertThat(userSession.hasProjectUuidPermission(SCAN, project1.projectUuid())).isTrue(); + assertThat(userSession.hasProjectUuidPermission(SCAN, project2.projectUuid())).isFalse(); + } + + @Test + public void test_hasProjectsPermission_for_ProjecAnalysisToken() { + ComponentDto project1 = db.components().insertPrivateProject(); + ComponentDto project2 = db.components().insertPrivateProject(); + + UserDto user = db.users().insertUser(); + + db.users().insertProjectPermissionOnUser(user, SCAN, project1); + db.users().insertProjectPermissionOnUser(user, SCAN, project2); + + TokenUserSession userSession = mockProjectAnalysisTokenUserSession(user,project1); + + assertThat(userSession.hasProjectUuidPermission(SCAN, project1.projectUuid())).isTrue(); + assertThat(userSession.hasProjectUuidPermission(SCAN, project2.projectUuid())).isFalse(); + } + + @Test + public void test_hasProjectsPermission_for_ProjectAnalysisToken_with_global_permission() { + ComponentDto project1 = db.components().insertPrivateProject(); + ComponentDto project2 = db.components().insertPrivateProject(); + + UserDto user = db.users().insertUser(); + + db.users().insertPermissionOnUser(user, GlobalPermission.SCAN); + + TokenUserSession userSession = mockProjectAnalysisTokenUserSession(user,project1); + + assertThat(userSession.hasProjectUuidPermission(SCAN, project1.projectUuid())).isTrue(); + assertThat(userSession.hasProjectUuidPermission(SCAN, project2.projectUuid())).isFalse(); + } + + @Test + public void test_hasGlobalPermission_for_UserToken() { + UserDto user = db.users().insertUser(); + db.users().insertPermissionOnUser(user, GlobalPermission.SCAN); + + TokenUserSession userSession = mockTokenUserSession(user); + + assertThat(userSession.hasPermission(GlobalPermission.SCAN)).isTrue(); + } + + @Test + public void test_hasGlobalPermission_for_ProjecAnalysisToken() { + ComponentDto project1 = db.components().insertPrivateProject(); + ComponentDto project2 = db.components().insertPrivateProject(); + + UserDto user = db.users().insertUser(); + + db.users().insertProjectPermissionOnUser(user, SCAN, project1); + db.users().insertProjectPermissionOnUser(user, SCAN, project2); + + db.users().insertPermissionOnUser(user, GlobalPermission.SCAN); + + TokenUserSession userSession = mockProjectAnalysisTokenUserSession(user,project1); + + assertThat(userSession.hasPermission(GlobalPermission.SCAN)).isFalse(); + } + + @Test + public void test_hasGlobalPermission_for_GlobalAnalysisToken() { + ComponentDto project1 = db.components().insertPrivateProject(); + + UserDto user = db.users().insertUser(); + + db.users().insertPermissionOnUser(user, GlobalPermission.SCAN); + + TokenUserSession userSession = mockGlobalAnalysisTokenUserSession(user); + + assertThat(userSession.hasProjectUuidPermission(SCAN, project1.projectUuid())).isFalse(); + assertThat(userSession.hasPermission(GlobalPermission.SCAN)).isTrue(); + } + + private TokenUserSession mockTokenUserSession(UserDto userDto) { + return new TokenUserSession(dbClient, userDto, mockUserTokenDto()); + } + + private TokenUserSession mockProjectAnalysisTokenUserSession(UserDto userDto, ComponentDto componentDto) { + return new TokenUserSession(dbClient, userDto, mockProjectAnalysisTokenDto(componentDto)); + } + + private TokenUserSession mockGlobalAnalysisTokenUserSession(UserDto userDto) { + return new TokenUserSession(dbClient, userDto, mockGlobalAnalysisTokenDto()); + } + + private UserTokenDto mockUserTokenDto() { + UserTokenDto userTokenDto = new UserTokenDto(); + userTokenDto.setType(USER_TOKEN.name()); + userTokenDto.setName("User Token"); + userTokenDto.setUserUuid("userUid"); + return userTokenDto; + } + + private UserTokenDto mockProjectAnalysisTokenDto(ComponentDto componentDto) { + UserTokenDto userTokenDto = new UserTokenDto(); + userTokenDto.setType(PROJECT_ANALYSIS_TOKEN.name()); + userTokenDto.setName("Project Analysis Token"); + userTokenDto.setUserUuid("userUid"); + userTokenDto.setProjectKey(componentDto.getKey()); + userTokenDto.setProjectName(componentDto.name()); + userTokenDto.setProjectUuid(componentDto.projectUuid()); + return userTokenDto; + } + + private UserTokenDto mockGlobalAnalysisTokenDto() { + UserTokenDto userTokenDto = new UserTokenDto(); + userTokenDto.setType(GLOBAL_ANALYSIS_TOKEN.name()); + userTokenDto.setName("Global Analysis Token"); + userTokenDto.setUserUuid("userUid"); + return userTokenDto; + } + +} diff --git a/server/sonar-webserver-auth/src/test/java/org/sonar/server/usertoken/UserTokenAuthenticationTest.java b/server/sonar-webserver-auth/src/test/java/org/sonar/server/usertoken/UserTokenAuthenticationTest.java index 32b2c3948ba..3e353f20e28 100644 --- a/server/sonar-webserver-auth/src/test/java/org/sonar/server/usertoken/UserTokenAuthenticationTest.java +++ b/server/sonar-webserver-auth/src/test/java/org/sonar/server/usertoken/UserTokenAuthenticationTest.java @@ -19,30 +19,41 @@ */ package org.sonar.server.usertoken; +import java.util.Base64; +import java.util.Optional; +import javax.servlet.http.HttpServletRequest; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.sonar.api.utils.System2; import org.sonar.db.DbTester; -import org.sonar.db.user.TokenType; import org.sonar.db.user.UserDto; import org.sonar.db.user.UserTokenDto; +import org.sonar.server.authentication.UserAuthResult; import org.sonar.server.authentication.UserLastConnectionDatesUpdater; -import org.sonar.server.usertoken.UserTokenAuthentication.UserTokenAuthenticationResult; +import org.sonar.server.authentication.event.AuthenticationEvent; +import org.sonar.server.authentication.event.AuthenticationException; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; +import static org.sonar.db.user.TokenType.GLOBAL_ANALYSIS_TOKEN; +import static org.sonar.db.user.TokenType.PROJECT_ANALYSIS_TOKEN; +import static org.sonar.db.user.TokenType.USER_TOKEN; +import static org.sonar.server.authentication.event.AuthenticationEvent.Method.BASIC_TOKEN; +import static org.sonar.server.usertoken.UserTokenAuthentication.isTokenBasedAuthentication; public class UserTokenAuthenticationTest { - private static final String EXAMPLE_SCANNER_ENDPOINT = "/api/settings/values.protobuf"; - private static final String EXAMPLE_USER_ENDPOINT = "/api/editions/set_license"; + private static final Base64.Encoder BASE64_ENCODER = Base64.getEncoder(); - private static final String EXAMPLE_PROJECT_KEY = "my-project-key"; + private static final String AUTHORIZATION_HEADER = "Authorization"; private static final String EXAMPLE_OLD_USER_TOKEN = "StringWith40CharactersThatIsOldUserToken"; private static final String EXAMPLE_NEW_USER_TOKEN = "squ_StringWith44CharactersThatIsNewUserToken"; @@ -57,13 +68,15 @@ public class UserTokenAuthenticationTest { @Rule public DbTester db = DbTester.create(System2.INSTANCE); - private TokenGenerator tokenGenerator = mock(TokenGenerator.class); - private UserLastConnectionDatesUpdater userLastConnectionDatesUpdater = mock(UserLastConnectionDatesUpdater.class); - - private UserTokenAuthentication underTest = new UserTokenAuthentication(tokenGenerator, db.getDbClient(), userLastConnectionDatesUpdater); + private final TokenGenerator tokenGenerator = mock(TokenGenerator.class); + private final UserLastConnectionDatesUpdater userLastConnectionDatesUpdater = mock(UserLastConnectionDatesUpdater.class); + private final AuthenticationEvent authenticationEvent = mock(AuthenticationEvent.class); + private final HttpServletRequest request = mock(HttpServletRequest.class); + private final UserTokenAuthentication underTest = new UserTokenAuthentication(tokenGenerator, db.getDbClient(), userLastConnectionDatesUpdater, authenticationEvent); @Before public void before() { + when(request.getHeader(AUTHORIZATION_HEADER)).thenReturn("Basic " + toBase64("token:")); when(tokenGenerator.hash(EXAMPLE_OLD_USER_TOKEN)).thenReturn(OLD_USER_TOKEN_HASH); when(tokenGenerator.hash(EXAMPLE_NEW_USER_TOKEN)).thenReturn(NEW_USER_TOKEN_HASH); when(tokenGenerator.hash(EXAMPLE_PROJECT_ANALYSIS_TOKEN)).thenReturn(PROJECT_ANALYSIS_TOKEN_HASH); @@ -74,93 +87,135 @@ public class UserTokenAuthenticationTest { public void return_login_when_token_hash_found_in_db() { String token = "known-token"; String tokenHash = "123456789"; + when(request.getHeader(AUTHORIZATION_HEADER)).thenReturn("Basic " + toBase64(token + ":")); when(tokenGenerator.hash(token)).thenReturn(tokenHash); UserDto user1 = db.users().insertUser(); - db.users().insertToken(user1, t -> t.setTokenHash(tokenHash)); + UserTokenDto userTokenDto = db.users().insertToken(user1, t -> t.setTokenHash(tokenHash)); UserDto user2 = db.users().insertUser(); db.users().insertToken(user2, t -> t.setTokenHash("another-token-hash")); - UserTokenAuthenticationResult result = underTest.authenticate(token, EXAMPLE_USER_ENDPOINT, null); + Optional<UserAuthResult> result = underTest.authenticate(request); - assertThat(result.getAuthenticatedUserUuid()) + assertThat(result).isPresent(); + assertThat(result.get().getTokenDto().getUuid()).isEqualTo(userTokenDto.getUuid()); + assertThat(result.get().getUserDto().getUuid()) .isNotNull() .contains(user1.getUuid()); verify(userLastConnectionDatesUpdater).updateLastConnectionDateIfNeeded(any(UserTokenDto.class)); } @Test + public void return_absent_if_username_password_used() { + when(request.getHeader(AUTHORIZATION_HEADER)).thenReturn("Basic " + toBase64("login:password")); + + Optional<UserAuthResult> result = underTest.authenticate(request); + + assertThat(result).isEmpty(); + verify(userLastConnectionDatesUpdater, never()).updateLastConnectionDateIfNeeded(any(UserTokenDto.class)); + verifyNoInteractions(authenticationEvent); + } + + @Test public void return_absent_if_token_hash_is_not_found() { - var result = underTest.authenticate(EXAMPLE_OLD_USER_TOKEN, EXAMPLE_USER_ENDPOINT, null); + when(request.getHeader(AUTHORIZATION_HEADER)).thenReturn("Basic " + toBase64(EXAMPLE_OLD_USER_TOKEN + ":")); - assertThat(result.getAuthenticatedUserUuid()).isNull(); + assertThatThrownBy(() -> underTest.authenticate(request)) + .hasMessageContaining("Token doesn't exist") + .isInstanceOf(AuthenticationException.class); verify(userLastConnectionDatesUpdater, never()).updateLastConnectionDateIfNeeded(any(UserTokenDto.class)); + verifyNoInteractions(authenticationEvent); } @Test - public void authenticate_givenProjectTokenAndUserEndpoint_fillErrorMessage() { + public void authenticate_givenGlobalToken_resultContainsUuid() { UserDto user = db.users().insertUser(); - db.users().insertToken(user, t -> t.setTokenHash(PROJECT_ANALYSIS_TOKEN_HASH).setType(TokenType.PROJECT_ANALYSIS_TOKEN.name())); + String tokenName = db.users().insertToken(user, t -> t.setTokenHash(GLOBAL_ANALYSIS_TOKEN_HASH).setType(GLOBAL_ANALYSIS_TOKEN.name())).getName(); - var authenticate = underTest.authenticate(EXAMPLE_PROJECT_ANALYSIS_TOKEN, EXAMPLE_USER_ENDPOINT, EXAMPLE_PROJECT_KEY); + when(request.getHeader(AUTHORIZATION_HEADER)).thenReturn("Basic " + toBase64(EXAMPLE_GLOBAL_ANALYSIS_TOKEN + ":")); + var result = underTest.authenticate(request); - assertThat(authenticate.getErrorMessage()).isNotNull().contains("Invalid token"); + assertThat(result).isPresent(); + assertThat(result.get().getTokenDto().getUuid()).isNotNull(); + assertThat(result.get().getTokenDto().getType()).isEqualTo(GLOBAL_ANALYSIS_TOKEN.name()); + verify(authenticationEvent).loginSuccess(request, user.getLogin(), AuthenticationEvent.Source.local(BASIC_TOKEN)); + verify(request).setAttribute("TOKEN_NAME",tokenName); } @Test - public void authenticate_givenProjectTokenAndUserEndpoint_InvalidTokenErrorMessage() { + public void authenticate_givenNewUserToken_resultContainsUuid() { UserDto user = db.users().insertUser(); - db.users().insertToken(user, t -> t.setTokenHash(PROJECT_ANALYSIS_TOKEN_HASH).setType(TokenType.PROJECT_ANALYSIS_TOKEN.name())); + String tokenName = db.users().insertToken(user, t -> t.setTokenHash(NEW_USER_TOKEN_HASH).setType(USER_TOKEN.name())).getName(); - var result = underTest.authenticate(EXAMPLE_PROJECT_ANALYSIS_TOKEN, EXAMPLE_USER_ENDPOINT, EXAMPLE_PROJECT_KEY); + when(request.getHeader(AUTHORIZATION_HEADER)).thenReturn("Basic " + toBase64(EXAMPLE_NEW_USER_TOKEN + ":")); + var result = underTest.authenticate(request); - assertThat(result.getErrorMessage()).isNotNull().contains("Invalid token"); + assertThat(result).isPresent(); + assertThat(result.get().getTokenDto().getUuid()).isNotNull(); + assertThat(result.get().getTokenDto().getType()).isEqualTo(USER_TOKEN.name()); + verify(authenticationEvent).loginSuccess(request, user.getLogin(), AuthenticationEvent.Source.local(BASIC_TOKEN)); + verify(request).setAttribute("TOKEN_NAME",tokenName); } @Test - public void authenticate_givenGlobalTokenAndScannerEndpoint_resultContainsUuid() { + public void authenticate_givenProjectToken_resultContainsUuid() { UserDto user = db.users().insertUser(); - db.users().insertToken(user, t -> t.setTokenHash(GLOBAL_ANALYSIS_TOKEN_HASH).setType(TokenType.GLOBAL_ANALYSIS_TOKEN.name())); + String tokenName = db.users().insertToken(user, t -> t.setTokenHash(PROJECT_ANALYSIS_TOKEN_HASH) + .setProjectKey("project-key") + .setType(PROJECT_ANALYSIS_TOKEN.name())).getName(); - var result = underTest.authenticate(EXAMPLE_GLOBAL_ANALYSIS_TOKEN, EXAMPLE_SCANNER_ENDPOINT, EXAMPLE_PROJECT_KEY); + when(request.getHeader(AUTHORIZATION_HEADER)).thenReturn("Basic " + toBase64(EXAMPLE_PROJECT_ANALYSIS_TOKEN + ":")); + var result = underTest.authenticate(request); - assertThat(result.getAuthenticatedUserUuid()).isNotNull(); - assertThat(result.getErrorMessage()).isNull(); + assertThat(result).isPresent(); + assertThat(result.get().getTokenDto().getUuid()).isNotNull(); + assertThat(result.get().getTokenDto().getType()).isEqualTo(PROJECT_ANALYSIS_TOKEN.name()); + assertThat(result.get().getTokenDto().getProjectKey()).isEqualTo("project-key"); + verify(authenticationEvent).loginSuccess(request, user.getLogin(), AuthenticationEvent.Source.local(BASIC_TOKEN)); + verify(request).setAttribute("TOKEN_NAME",tokenName); } + @Test - public void authenticate_givenNewUserTokenAndScannerEndpoint_resultContainsUuid() { - UserDto user = db.users().insertUser(); - db.users().insertToken(user, t -> t.setTokenHash(NEW_USER_TOKEN_HASH).setType(TokenType.USER_TOKEN.name())); + public void does_not_authenticate_from_user_token_when_token_does_not_match_active_user() { + UserDto user = db.users().insertDisabledUser(); + String tokenName = db.users().insertToken(user, t -> t.setTokenHash(NEW_USER_TOKEN_HASH).setType(USER_TOKEN.name())).getName(); + + when(request.getHeader(AUTHORIZATION_HEADER)).thenReturn("Basic " + toBase64(EXAMPLE_NEW_USER_TOKEN + ":")); - var result = underTest.authenticate(EXAMPLE_NEW_USER_TOKEN, EXAMPLE_SCANNER_ENDPOINT, null); + assertThatThrownBy(() -> underTest.authenticate(request)) + .hasMessageContaining("User doesn't exist") + .isInstanceOf(AuthenticationException.class) + .hasFieldOrPropertyWithValue("source", AuthenticationEvent.Source.local(BASIC_TOKEN)); - assertThat(result.getAuthenticatedUserUuid()).isNotNull(); - assertThat(result.getErrorMessage()).isNull(); + verifyNoInteractions(authenticationEvent); } @Test - public void authenticate_givenProjectTokenAndScannerEndpointAndValidProjectKey_resultContainsUuid() { - UserDto user = db.users().insertUser(); - db.users().insertToken(user, t -> t.setTokenHash(PROJECT_ANALYSIS_TOKEN_HASH) - .setProjectKey("project-key") - .setType(TokenType.PROJECT_ANALYSIS_TOKEN.name())); + public void return_token_from_db() { + String token = "known-token"; + String tokenHash = "123456789"; + when(tokenGenerator.hash(token)).thenReturn(tokenHash); + UserDto user1 = db.users().insertUser(); + UserTokenDto userTokenDto = db.users().insertToken(user1, t -> t.setTokenHash(tokenHash)); - var result = underTest.authenticate(EXAMPLE_PROJECT_ANALYSIS_TOKEN, EXAMPLE_SCANNER_ENDPOINT, "project-key"); + UserTokenDto result = underTest.getUserToken(token); - assertThat(result.getAuthenticatedUserUuid()).isNotNull(); - assertThat(result.getErrorMessage()).isNull(); + assertThat(result.getUuid()).isEqualTo(userTokenDto.getUuid()); } @Test - public void authenticate_givenProjectTokenAndScannerEndpointAndWrongProjectKey_resultContainsErrorMessage() { - UserDto user = db.users().insertUser(); - db.users().insertToken(user, t -> t.setTokenHash(PROJECT_ANALYSIS_TOKEN_HASH) - .setProjectKey("project-key") - .setType(TokenType.PROJECT_ANALYSIS_TOKEN.name())); + public void identifies_if_request_uses_token_based_authentication() { + when(request.getHeader(AUTHORIZATION_HEADER)).thenReturn("Basic " + toBase64("token:")); + assertThat(isTokenBasedAuthentication(request)).isTrue(); - var result = underTest.authenticate(EXAMPLE_PROJECT_ANALYSIS_TOKEN, EXAMPLE_SCANNER_ENDPOINT, "project-key-2"); + when(request.getHeader(AUTHORIZATION_HEADER)).thenReturn("Basic " + toBase64("login:password")); + assertThat(isTokenBasedAuthentication(request)).isFalse(); + + when(request.getHeader(AUTHORIZATION_HEADER)).thenReturn(null); + assertThat(isTokenBasedAuthentication(request)).isFalse(); + } - assertThat(result.getAuthenticatedUserUuid()).isNull(); - assertThat(result.getErrorMessage()).isNotNull(); + private static String toBase64(String text) { + return new String(BASE64_ENCODER.encode(text.getBytes(UTF_8))); } } diff --git a/server/sonar-webserver-auth/src/testFixtures/java/org/sonar/server/user/TestUserSessionFactory.java b/server/sonar-webserver-auth/src/testFixtures/java/org/sonar/server/user/TestUserSessionFactory.java index d09d9ea662d..92d1d056781 100644 --- a/server/sonar-webserver-auth/src/testFixtures/java/org/sonar/server/user/TestUserSessionFactory.java +++ b/server/sonar-webserver-auth/src/testFixtures/java/org/sonar/server/user/TestUserSessionFactory.java @@ -25,6 +25,7 @@ import javax.annotation.Nullable; import org.sonar.db.permission.GlobalPermission; import org.sonar.db.user.GroupDto; import org.sonar.db.user.UserDto; +import org.sonar.db.user.UserTokenDto; import static java.util.Objects.requireNonNull; @@ -46,6 +47,11 @@ public class TestUserSessionFactory implements UserSessionFactory { } @Override + public UserSession create(UserDto user, UserTokenDto userToken) { + return new TestUserSession(requireNonNull(user)); + } + + @Override public UserSession createAnonymous() { return new TestUserSession(null); } |