diff options
author | Lukasz Jarocki <lukasz.jarocki@sonarsource.com> | 2022-04-22 10:37:33 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2022-04-29 20:03:18 +0000 |
commit | e2f49b822078b776bdac89410c191af775a330fe (patch) | |
tree | 8b2832a80a4d8ab2dffe791dbf2d898b7a91dbb3 /server | |
parent | 52b24b06255c984d5a9638c4e30f16a440dc9217 (diff) | |
download | sonarqube-e2f49b822078b776bdac89410c191af775a330fe.tar.gz sonarqube-e2f49b822078b776bdac89410c191af775a330fe.zip |
SONAR-16260 authentication now takes into account token type
Diffstat (limited to 'server')
5 files changed, 221 insertions, 28 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 1f6d5ea3399..b5ac6571cba 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 @@ -33,6 +33,7 @@ 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}. @@ -43,6 +44,8 @@ import static org.sonar.server.authentication.event.AuthenticationEvent.Source; */ 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; @@ -94,30 +97,33 @@ public class BasicAuthentication { } private UserDto authenticate(Credentials credentials, HttpServletRequest request) { - if (!credentials.getPassword().isPresent()) { - UserDto userDto = authenticateFromUserToken(credentials.getLogin()); + 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) { - Optional<String> authenticatedUserUuid = userTokenAuthentication.authenticate(token); - if (!authenticatedUserUuid.isPresent()) { + 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("Token doesn't exist") + .setMessage(result.getErrorMessage()) .build(); } try (DbSession dbSession = dbClient.openSession(false)) { - UserDto userDto = dbClient.userDao().selectByUuid(dbSession, authenticatedUserUuid.get()); + UserDto userDto = dbClient.userDao().selectByUuid(dbSession, result.getAuthenticatedUserUuid()); if (userDto == null || !userDto.isActive()) { throw AuthenticationException.newBuilder() .setSource(Source.local(Method.BASIC_TOKEN)) .setMessage("User doesn't exist") .build(); } + request.setAttribute(ACCESS_LOG_TOKEN_NAME, result.getTokenName()); return userDto; } } diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/TokenType.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/TokenType.java index 8581b9ad6b6..0fcfbd7626a 100644 --- a/server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/TokenType.java +++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/usertoken/TokenType.java @@ -20,6 +20,7 @@ package org.sonar.server.usertoken; public enum TokenType { + USER_TOKEN("u"), GLOBAL_ANALYSIS_TOKEN("a"), PROJECT_ANALYSIS_TOKEN("p"); 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 f35e117602a..1bed33677d6 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,16 +19,37 @@ */ package org.sonar.server.usertoken; -import java.util.Optional; +import java.util.EnumMap; +import java.util.Set; +import javax.annotation.Nullable; import org.sonar.db.DbClient; import org.sonar.db.DbSession; import org.sonar.db.user.UserTokenDto; import org.sonar.server.authentication.UserLastConnectionDatesUpdater; -import static java.util.Optional.empty; -import static java.util.Optional.of; - 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/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"); + + 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); + } + private final TokenGenerator tokenGenerator; private final DbClient dbClient; private final UserLastConnectionDatesUpdater userLastConnectionDatesUpdater; @@ -40,19 +61,76 @@ public class UserTokenAuthentication { } /** - * Returns the user uuid if the token hash is found, else {@code Optional.absent()}. - * The returned uuid is not validated. If database is corrupted (table USER_TOKENS badly purged - * for instance), then the uuid may not relate to a valid user. + * 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 Optional<String> authenticate(String token) { + 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 empty(); + return new UserTokenAuthenticationResult("Token doesn't exist"); + } + if (!isValidTokenType(userToken, analyzedProjectKey, requestedEndpoint)) { + return new UserTokenAuthenticationResult("Invalid token"); } userLastConnectionDatesUpdater.updateLastConnectionDateIfNeeded(userToken); - return of(userToken.getUserUuid()); + return new UserTokenAuthenticationResult(userToken.getUserUuid(), userToken.getName()); } } + + 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); + } + + 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 + } + 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; + } + return analyzedProjectKey != null && analyzedProjectKey.equals(userToken.getProjectKey()); + } + + 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; + } + + } } 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 d80ff6aa193..05c2231f4c7 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 @@ -22,6 +22,7 @@ package org.sonar.server.authentication; 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; @@ -36,14 +37,16 @@ import org.sonar.server.usertoken.UserTokenAuthentication; 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.anyString; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; +import static org.sonar.server.authentication.event.AuthenticationEvent.Source; import static org.sonar.server.authentication.event.AuthenticationEvent.Method.BASIC; import static org.sonar.server.authentication.event.AuthenticationEvent.Method.BASIC_TOKEN; -import static org.sonar.server.authentication.event.AuthenticationEvent.Source; public class BasicAuthenticationTest { @@ -55,6 +58,7 @@ public class BasicAuthenticationTest { private static final UserDto USER = UserTesting.newUserDto().setLogin(A_LOGIN); + private static final String EXAMPLE_ENDPOINT = "/api/ce/submit"; @Rule public DbTester db = DbTester.create(System2.INSTANCE); @@ -70,6 +74,13 @@ public class BasicAuthenticationTest { private BasicAuthentication underTest = new BasicAuthentication(dbClient, credentialsAuthentication, userTokenAuthentication, authenticationEvent); + @Before + public void before() { + String contextPath = "localhost"; + when(request.getRequestURI()).thenReturn(contextPath + EXAMPLE_ENDPOINT); + when(request.getContextPath()).thenReturn(contextPath); + } + @Test public void authenticate_from_basic_http_header() { when(request.getHeader("Authorization")).thenReturn("Basic " + CREDENTIALS_IN_BASE64); @@ -134,7 +145,8 @@ public class BasicAuthenticationTest { @Test public void authenticate_from_user_token() { UserDto user = db.users().insertUser(); - when(userTokenAuthentication.authenticate("token")).thenReturn(Optional.of(user.getUuid())); + 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:")); Optional<UserDto> userAuthenticated = underTest.authenticate(request); @@ -142,11 +154,13 @@ public class BasicAuthenticationTest { 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() { - when(userTokenAuthentication.authenticate("token")).thenReturn(Optional.empty()); + 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:")); assertThatThrownBy(() -> underTest.authenticate(request)) @@ -155,11 +169,13 @@ public class BasicAuthenticationTest { .hasFieldOrPropertyWithValue("source", Source.local(BASIC_TOKEN)); verifyNoInteractions(authenticationEvent); + verify(request, times(0)).setAttribute(anyString(), anyString()); } @Test public void does_not_authenticate_from_user_token_when_token_does_not_match_existing_user() { - when(userTokenAuthentication.authenticate("token")).thenReturn(Optional.of("Unknown 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)) @@ -173,7 +189,8 @@ public class BasicAuthenticationTest { @Test public void does_not_authenticate_from_user_token_when_token_does_not_match_active_user() { UserDto user = db.users().insertDisabledUser(); - when(userTokenAuthentication.authenticate("token")).thenReturn(Optional.of(user.getUuid())); + 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:")); assertThatThrownBy(() -> underTest.authenticate(request)) 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 20453ccdb31..7957812dab1 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,7 +19,7 @@ */ package org.sonar.server.usertoken; -import java.util.Optional; +import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.sonar.api.utils.System2; @@ -27,6 +27,7 @@ import org.sonar.db.DbTester; import org.sonar.db.user.UserDto; import org.sonar.db.user.UserTokenDto; import org.sonar.server.authentication.UserLastConnectionDatesUpdater; +import org.sonar.server.usertoken.UserTokenAuthentication.UserTokenAuthenticationResult; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -37,6 +38,20 @@ import static org.mockito.Mockito.when; 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 String EXAMPLE_PROJECT_KEY = "my-project-key"; + + private static final String EXAMPLE_OLD_USER_TOKEN = "StringWith40CharactersThatIsOldUserToken"; + private static final String EXAMPLE_NEW_USER_TOKEN = "squ_StringWith44CharactersThatIsNewUserToken"; + private static final String EXAMPLE_GLOBAL_ANALYSIS_TOKEN = "sqa_StringWith44CharactersWhichIsGlobalToken"; + private static final String EXAMPLE_PROJECT_ANALYSIS_TOKEN = "sqp_StringWith44CharactersThatIsProjectToken"; + + private static final String OLD_USER_TOKEN_HASH = "old-user-token-hash"; + private static final String NEW_USER_TOKEN_HASH = "new-user-token-hash"; + private static final String PROJECT_ANALYSIS_TOKEN_HASH = "project-analysis-token-hash"; + private static final String GLOBAL_ANALYSIS_TOKEN_HASH = "global-analysis-token-hash"; @Rule public DbTester db = DbTester.create(System2.INSTANCE); @@ -46,6 +61,14 @@ public class UserTokenAuthenticationTest { private UserTokenAuthentication underTest = new UserTokenAuthentication(tokenGenerator, db.getDbClient(), userLastConnectionDatesUpdater); + @Before + public void before() { + 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); + when(tokenGenerator.hash(EXAMPLE_GLOBAL_ANALYSIS_TOKEN)).thenReturn(GLOBAL_ANALYSIS_TOKEN_HASH); + } + @Test public void return_login_when_token_hash_found_in_db() { String token = "known-token"; @@ -56,19 +79,87 @@ public class UserTokenAuthenticationTest { UserDto user2 = db.users().insertUser(); db.users().insertToken(user2, t -> t.setTokenHash("another-token-hash")); - Optional<String> login = underTest.authenticate(token); + UserTokenAuthenticationResult result = underTest.authenticate(token, EXAMPLE_USER_ENDPOINT, null); - assertThat(login) - .isPresent() + assertThat(result.getAuthenticatedUserUuid()) + .isNotNull() .contains(user1.getUuid()); verify(userLastConnectionDatesUpdater).updateLastConnectionDateIfNeeded(any(UserTokenDto.class)); } @Test public void return_absent_if_token_hash_is_not_found() { - Optional<String> login = underTest.authenticate("unknown-token"); + var result = underTest.authenticate(EXAMPLE_OLD_USER_TOKEN, EXAMPLE_USER_ENDPOINT, null); - assertThat(login).isEmpty(); + assertThat(result.getAuthenticatedUserUuid()).isNull(); verify(userLastConnectionDatesUpdater, never()).updateLastConnectionDateIfNeeded(any(UserTokenDto.class)); } + + @Test + public void authenticate_givenProjectTokenAndUserEndpoint_fillErrorMessage() { + UserDto user = db.users().insertUser(); + db.users().insertToken(user, t -> t.setTokenHash(PROJECT_ANALYSIS_TOKEN_HASH).setType(TokenType.PROJECT_ANALYSIS_TOKEN.name())); + + var authenticate = underTest.authenticate(EXAMPLE_PROJECT_ANALYSIS_TOKEN, EXAMPLE_USER_ENDPOINT, EXAMPLE_PROJECT_KEY); + + assertThat(authenticate.getErrorMessage()).isNotNull().contains("Invalid token"); + } + + @Test + public void authenticate_givenProjectTokenAndUserEndpoint_InvalidTokenErrorMessage() { + UserDto user = db.users().insertUser(); + db.users().insertToken(user, t -> t.setTokenHash(PROJECT_ANALYSIS_TOKEN_HASH).setType(TokenType.PROJECT_ANALYSIS_TOKEN.name())); + + var result = underTest.authenticate(EXAMPLE_PROJECT_ANALYSIS_TOKEN, EXAMPLE_USER_ENDPOINT, EXAMPLE_PROJECT_KEY); + + assertThat(result.getErrorMessage()).isNotNull().contains("Invalid token"); + } + + @Test + public void authenticate_givenGlobalTokenAndScannerEndpoint_resultContainsUuid() { + UserDto user = db.users().insertUser(); + db.users().insertToken(user, t -> t.setTokenHash(GLOBAL_ANALYSIS_TOKEN_HASH).setType(TokenType.GLOBAL_ANALYSIS_TOKEN.name())); + + var result = underTest.authenticate(EXAMPLE_GLOBAL_ANALYSIS_TOKEN, EXAMPLE_SCANNER_ENDPOINT, EXAMPLE_PROJECT_KEY); + + assertThat(result.getAuthenticatedUserUuid()).isNotNull(); + assertThat(result.getErrorMessage()).isNull(); + } + + @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())); + + var result = underTest.authenticate(EXAMPLE_NEW_USER_TOKEN, EXAMPLE_SCANNER_ENDPOINT, null); + + assertThat(result.getAuthenticatedUserUuid()).isNotNull(); + assertThat(result.getErrorMessage()).isNull(); + } + + @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())); + + var result = underTest.authenticate(EXAMPLE_PROJECT_ANALYSIS_TOKEN, EXAMPLE_SCANNER_ENDPOINT, "project-key"); + + assertThat(result.getAuthenticatedUserUuid()).isNotNull(); + assertThat(result.getErrorMessage()).isNull(); + } + + @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())); + + var result = underTest.authenticate(EXAMPLE_PROJECT_ANALYSIS_TOKEN, EXAMPLE_SCANNER_ENDPOINT, "project-key-2"); + + assertThat(result.getAuthenticatedUserUuid()).isNull(); + assertThat(result.getErrorMessage()).isNotNull(); + } } |