From: Jacek Poreda Date: Tue, 17 Sep 2024 12:59:33 +0000 (+0200) Subject: SONAR-23070 Fix SSF-635 X-Git-Tag: 9.9.7.96285~5 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=a139d1dc705cfcccfa39ab5263fb37ff8d9cf3c6;p=sonarqube.git SONAR-23070 Fix SSF-635 --- diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/user/AbstractUserSession.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/user/AbstractUserSession.java index adff5727a82..9a69f7c5e3c 100644 --- a/server/sonar-webserver-auth/src/main/java/org/sonar/server/user/AbstractUserSession.java +++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/user/AbstractUserSession.java @@ -141,7 +141,7 @@ public abstract class AbstractUserSession implements UserSession { } @Override - public List keepAuthorizedProjects(String permission, Collection projects) { + public final List keepAuthorizedProjects(String permission, Collection projects) { return doKeepAuthorizedProjects(permission, projects); } diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/user/ServerUserSession.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/user/ServerUserSession.java index 8c46370819a..e58e07e1ebb 100644 --- a/server/sonar-webserver-auth/src/main/java/org/sonar/server/user/ServerUserSession.java +++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/user/ServerUserSession.java @@ -182,7 +182,7 @@ public class ServerUserSession extends AbstractUserSession { } @Override - public List keepAuthorizedProjects(String permission, Collection projects) { + protected List doKeepAuthorizedProjects(String permission, Collection projects) { Set projectsUuids = projects.stream().map(ProjectDto::getUuid).collect(Collectors.toSet()); Set authorizedProjectsUuids = keepProjectsUuidsByPermission(permission, projectsUuids); @@ -325,7 +325,7 @@ public class ServerUserSession extends AbstractUserSession { Set allProjectUuids = new HashSet<>(projectUuids); allProjectUuids.addAll(originalComponentsProjectUuids); - Set authorizedProjectUuids = dbClient.authorizationDao().keepAuthorizedProjectUuids(dbSession, allProjectUuids, getUuid(), permission); + Set authorizedProjectUuids = keepAuthorizedProjectsUuids(dbSession, permission, allProjectUuids); return components.stream() .filter(c -> { @@ -341,6 +341,10 @@ public class ServerUserSession extends AbstractUserSession { } } + protected Set keepAuthorizedProjectsUuids(DbSession dbSession, String permission, Collection entityUuids) { + return dbClient.authorizationDao().keepAuthorizedProjectUuids(dbSession, entityUuids, getUuid(), permission); + } + private Map findComponentsByCopyComponentUuid(Collection components, DbSession dbSession) { Set copyComponentsUuid = components.stream() .map(ComponentDto::getCopyComponentUuid) 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 index ffa05172d7a..2e665eef08b 100644 --- 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 @@ -19,16 +19,25 @@ */ package org.sonar.server.user; +import java.util.Collection; +import java.util.Collections; import java.util.EnumSet; +import java.util.List; import java.util.Set; import org.sonar.db.DbClient; +import org.sonar.db.DbSession; import org.sonar.db.permission.GlobalPermission; +import org.sonar.db.project.ProjectDto; import org.sonar.db.user.TokenType; import org.sonar.db.user.UserDto; import org.sonar.db.user.UserTokenDto; +import static java.lang.String.format; +import static org.sonar.api.web.UserRole.USER; + public class TokenUserSession extends ServerUserSession { + private static final String TOKEN_ASSERTION_ERROR_MESSAGE = "Unsupported token type %s"; private static final String SCAN = "scan"; private static final Set GLOBAL_ANALYSIS_TOKEN_SUPPORTED_PERMISSIONS = EnumSet.of(GlobalPermission.SCAN, GlobalPermission.PROVISION_PROJECTS); private final UserTokenDto userToken; @@ -41,41 +50,70 @@ public class TokenUserSession extends ServerUserSession { @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) && + return switch (tokenType) { + case USER_TOKEN -> super.hasProjectUuidPermission(permission, projectUuid); + case PROJECT_ANALYSIS_TOKEN -> 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 + 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()); - } + false; + default -> throw new IllegalArgumentException(format(TOKEN_ASSERTION_ERROR_MESSAGE, 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 GLOBAL_ANALYSIS_TOKEN_SUPPORTED_PERMISSIONS.contains(permission) && super.hasPermissionImpl(permission); - default: - throw new IllegalArgumentException("Unsupported token type " + tokenType.name()); - } + return switch (tokenType) { + case USER_TOKEN -> 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. + false; + case GLOBAL_ANALYSIS_TOKEN -> + GLOBAL_ANALYSIS_TOKEN_SUPPORTED_PERMISSIONS.contains(permission) && super.hasPermissionImpl(permission); + default -> throw new IllegalArgumentException(format(TOKEN_ASSERTION_ERROR_MESSAGE, tokenType.name())); + }; + } + + @Override + protected List doKeepAuthorizedProjects(String permission, Collection projects) { + TokenType tokenType = TokenType.valueOf(userToken.getType()); + return switch (tokenType) { + case USER_TOKEN, GLOBAL_ANALYSIS_TOKEN -> super.doKeepAuthorizedProjects(permission, projects); + case PROJECT_ANALYSIS_TOKEN -> + (SCAN.equals(permission) || USER.equals(permission)) ? projects.stream() + .filter(project -> project.getUuid().equals(userToken.getProjectUuid())) + .toList() : Collections.emptyList(); + default -> throw new IllegalArgumentException(format(TOKEN_ASSERTION_ERROR_MESSAGE, tokenType.name())); + }; + } + + /** + * Required to override doKeepAuthorizedComponents to handle the case of a project analysis token + */ + @Override + protected Set keepAuthorizedProjectsUuids(DbSession dbSession, String permission, Collection entityUuids) { + TokenType tokenType = TokenType.valueOf(userToken.getType()); + return switch (tokenType) { + case USER_TOKEN, GLOBAL_ANALYSIS_TOKEN -> super.keepAuthorizedProjectsUuids(dbSession, permission, entityUuids); + case PROJECT_ANALYSIS_TOKEN -> + (SCAN.equals(permission) || USER.equals(permission)) ? Collections.singleton(userToken.getProjectUuid()) : Collections.emptySet(); + default -> throw new IllegalArgumentException(format(TOKEN_ASSERTION_ERROR_MESSAGE, tokenType.name())); + }; } public UserTokenDto getUserToken() { return userToken; } + + /** + * @return the type of the token, based on the {@link TokenType} enum + */ + public TokenType getTokenType() { + return TokenType.valueOf(userToken.getType()); + } } 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 index 75213f72a44..3df6febcc02 100644 --- 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 @@ -19,22 +19,36 @@ */ package org.sonar.server.user; +import com.tngtech.java.junit.dataprovider.DataProvider; +import com.tngtech.java.junit.dataprovider.DataProviderRunner; +import com.tngtech.java.junit.dataprovider.UseDataProvider; +import java.util.List; +import java.util.Set; import org.junit.Rule; import org.junit.Test; +import org.junit.runner.RunWith; import org.sonar.api.utils.System2; +import org.sonar.api.web.UserRole; 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.project.ProjectDto; import org.sonar.db.user.UserDto; import org.sonar.db.user.UserTokenDto; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.sonar.api.web.UserRole.CODEVIEWER; import static org.sonar.api.web.UserRole.SCAN; +import static org.sonar.api.web.UserRole.USER; +import static org.sonar.db.component.ComponentDbTester.toProjectDto; 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.PROJECT_BADGE_TOKEN; import static org.sonar.db.user.TokenType.USER_TOKEN; +@RunWith(DataProviderRunner.class) public class TokenUserSessionTest { @Rule @@ -82,7 +96,7 @@ public class TokenUserSessionTest { db.users().insertProjectPermissionOnUser(user, SCAN, project1); db.users().insertProjectPermissionOnUser(user, SCAN, project2); - TokenUserSession userSession = mockProjectAnalysisTokenUserSession(user,project1); + TokenUserSession userSession = mockProjectAnalysisTokenUserSession(user, project1); assertThat(userSession.hasProjectUuidPermission(SCAN, project1.branchUuid())).isTrue(); assertThat(userSession.hasProjectUuidPermission(SCAN, project2.branchUuid())).isFalse(); @@ -97,7 +111,7 @@ public class TokenUserSessionTest { db.users().insertPermissionOnUser(user, GlobalPermission.SCAN); - TokenUserSession userSession = mockProjectAnalysisTokenUserSession(user,project1); + TokenUserSession userSession = mockProjectAnalysisTokenUserSession(user, project1); assertThat(userSession.hasProjectUuidPermission(SCAN, project1.branchUuid())).isTrue(); assertThat(userSession.hasProjectUuidPermission(SCAN, project2.branchUuid())).isFalse(); @@ -125,7 +139,7 @@ public class TokenUserSessionTest { db.users().insertPermissionOnUser(user, GlobalPermission.SCAN); - TokenUserSession userSession = mockProjectAnalysisTokenUserSession(user,project1); + TokenUserSession userSession = mockProjectAnalysisTokenUserSession(user, project1); assertThat(userSession.hasPermission(GlobalPermission.SCAN)).isFalse(); } @@ -178,6 +192,207 @@ public class TokenUserSessionTest { assertThat(userSession.hasPermission(GlobalPermission.ADMINISTER)).isFalse(); } + @Test + public void keepAuthorizedEntities_shouldFilterProjects_whenGlobalAnalysisToken() { + UserDto user = db.users().insertUser(); + + ComponentDto privateProjectComponent = db.components().insertPrivateProject(); + ProjectDto publicProject = db.components().insertPublicProjectDto(); + ProjectDto privateProject = toProjectDto(privateProjectComponent, 0L); + ProjectDto privateProjectWithoutAccess = db.components().insertPrivateProjectDto(); + + db.users().insertProjectPermissionOnUser(user, USER, privateProjectComponent); + + Set projectDto = Set.of(publicProject, privateProject); + List projectDtos = mockGlobalAnalysisTokenUserSession(user).doKeepAuthorizedProjects(USER, projectDto); + + assertThat(projectDtos).containsExactlyInAnyOrder(privateProject, publicProject) + .doesNotContain(privateProjectWithoutAccess); + } + + @Test + @UseDataProvider("validPermissions") + public void keepAuthorizedEntities_shouldFilterPrivateProjects_whenProjectAnalysisToken(String permission) { + UserDto user = db.users().insertUser(); + + ComponentDto privateProjectComponent = db.components().insertPrivateProject(); + ProjectDto publicProject = db.components().insertPublicProjectDto(); + ProjectDto privateProject = toProjectDto(privateProjectComponent, 0L); + ProjectDto privateProjectWithoutAccess = db.components().insertPrivateProjectDto(); + + db.users().insertProjectPermissionOnUser(user, permission, privateProjectComponent); + + Set projectDto = Set.of(publicProject, privateProject); + List projectDtos = mockProjectAnalysisTokenUserSession(user, privateProjectComponent).keepAuthorizedProjects(permission, projectDto); + + assertThat(projectDtos).containsExactly(privateProject) + .doesNotContain(privateProjectWithoutAccess); + } + + @Test + public void keepAuthorizedEntities_shouldFilterPrivateProjects_whenUserToken() { + UserDto user = db.users().insertUser(); + + ComponentDto privateProjectComponent = db.components().insertPrivateProject(); + ProjectDto publicProject = db.components().insertPublicProjectDto(); + ProjectDto privateProject = toProjectDto(privateProjectComponent, 0L); + ProjectDto privateProjectWithoutAccess = db.components().insertPrivateProjectDto(); + + db.users().insertProjectPermissionOnUser(user, USER, privateProjectComponent); + + Set projectDto = Set.of(publicProject, privateProject); + List projectDtos = mockTokenUserSession(user).keepAuthorizedProjects(USER, projectDto); + + assertThat(projectDtos).containsExactlyInAnyOrder(privateProject, publicProject) + .doesNotContain(privateProjectWithoutAccess); + } + + @Test + public void keepAuthorizedEntities_shouldFilterPrivateProjects_returnEmptyListForPermissionOtherThanScanOrBrowse() { + UserDto user = db.users().insertUser(); + + ProjectDto publicProject = db.components().insertPublicProjectDto(); + ComponentDto privateProjectComponent = db.components().insertPrivateProject(); + ProjectDto privateProject = toProjectDto(privateProjectComponent, 0L); + + db.users().insertProjectPermissionOnUser(user, CODEVIEWER, privateProjectComponent); + + Set projectDto = Set.of(publicProject, privateProject); + List projectDtos = mockProjectAnalysisTokenUserSession(user, privateProjectComponent).keepAuthorizedProjects(CODEVIEWER, projectDto); + + assertThat(projectDtos).isEmpty(); + } + + @Test + public void keepAuthorizedEntities_shouldFailForUnsupportedTokenSession() { + UserDto user = db.users().insertUser(); + + ProjectDto publicProject = db.components().insertPublicProjectDto(); + ComponentDto privateProjectComponent = db.components().insertPrivateProject(); + ProjectDto privateProject = toProjectDto(privateProjectComponent, 0L); + + db.users().insertProjectPermissionOnUser(user, USER, privateProjectComponent); + + Set projectDto = Set.of(publicProject, privateProject); + + TokenUserSession tokenUserSession = mockProjectBadgeTokenSession(user); + assertThatThrownBy(() -> tokenUserSession.keepAuthorizedProjects(USER, projectDto)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unsupported token type PROJECT_BADGE_TOKEN"); + } + + @Test + public void keepAuthorizedComponents_shouldFilterProjects_whenGlobalAnalysisToken() { + UserDto user = db.users().insertUser(); + + ComponentDto publicProject = db.components().insertPublicProject(); + ComponentDto privateProject = db.components().insertPrivateProject(); + ComponentDto privateProjectWithoutAccess = db.components().insertPrivateProject(); + + db.users().insertProjectPermissionOnUser(user, USER, privateProject); + + Set componentDtos = Set.of(publicProject, privateProject); + List authorizedComponents = mockGlobalAnalysisTokenUserSession(user).keepAuthorizedComponents(USER, componentDtos); + + assertThat(authorizedComponents).containsExactlyInAnyOrder(privateProject, publicProject) + .doesNotContain(privateProjectWithoutAccess); + } + + @Test + @UseDataProvider("validPermissions") + public void keepAuthorizedComponents_shouldFilterPrivateProjects_whenProjectAnalysisToken(String permission) { + UserDto user = db.users().insertUser(); + + ComponentDto publicProject = db.components().insertPublicProject(); + ComponentDto privateProject = db.components().insertPrivateProject(); + ComponentDto privateProjectWithoutAccess = db.components().insertPrivateProject(); + + db.users().insertProjectPermissionOnUser(user, permission, privateProject); + + Set componentDtos = Set.of(publicProject, privateProject); + List authorizedComponents = mockProjectAnalysisTokenUserSession(user, privateProject) + .keepAuthorizedComponents(permission, componentDtos); + + assertThat(authorizedComponents).containsExactly(privateProject) + .doesNotContain(privateProjectWithoutAccess, publicProject); + } + + @Test + public void keepAuthorizedComponents_shouldFilterPrivateProjects_whenUserToken() { + UserDto user = db.users().insertUser(); + + ComponentDto publicProject = db.components().insertPublicProject(); + ComponentDto privateProject = db.components().insertPrivateProject(); + ComponentDto privateProjectWithoutAccess = db.components().insertPrivateProject(); + + db.users().insertProjectPermissionOnUser(user, USER, privateProject); + + Set componentDtos = Set.of(publicProject, privateProject); + List authorizedComponents = mockTokenUserSession(user).keepAuthorizedComponents(USER, componentDtos); + + assertThat(authorizedComponents).containsExactlyInAnyOrder(privateProject, publicProject) + .doesNotContain(privateProjectWithoutAccess); + } + + @Test + public void keepAuthorizedComponents_returnEmptyListForPermissionOtherThanScanOrBrowse() { + UserDto user = db.users().insertUser(); + + ComponentDto publicProject = db.components().insertPublicProject(); + ComponentDto privateProject = db.components().insertPrivateProject(); + + db.users().insertProjectPermissionOnUser(user, UserRole.CODEVIEWER, privateProject); + + Set componentDtos = Set.of(publicProject, privateProject); + List authorizedComponents = mockProjectAnalysisTokenUserSession(user, privateProject) + .keepAuthorizedComponents(UserRole.CODEVIEWER, componentDtos); + + assertThat(authorizedComponents).isEmpty(); + } + + @Test + public void keepAuthorizedComponents_shouldFailForUnsupportedTokenSession() { + UserDto user = db.users().insertUser(); + + ComponentDto publicProject = db.components().insertPublicProject(); + ComponentDto privateProject = db.components().insertPrivateProject(); + + db.users().insertProjectPermissionOnUser(user, USER, privateProject); + + Set componentDtos = Set.of(publicProject, privateProject); + + TokenUserSession tokenUserSession = mockProjectBadgeTokenSession(user); + assertThatThrownBy(() -> tokenUserSession.keepAuthorizedComponents(USER, componentDtos)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unsupported token type PROJECT_BADGE_TOKEN"); + } + + @DataProvider + public static Object[][] validPermissions() { + return new Object[][] { + {USER}, + {SCAN}, + }; + } + + @Test + public void getType_shouldReturnValidTypeOfToken() { + UserDto user = db.users().insertUser(); + ComponentDto privateComponent = db.components().insertPrivateProject(); + + TokenUserSession projectBadgeTokenSession = mockProjectBadgeTokenSession(user); + assertThat(projectBadgeTokenSession.getTokenType()).isEqualTo(PROJECT_BADGE_TOKEN); + + TokenUserSession tokenUserSession = mockTokenUserSession(user); + assertThat(tokenUserSession.getTokenType()).isEqualTo(USER_TOKEN); + + TokenUserSession projectAnalysisTokenUserSession = mockProjectAnalysisTokenUserSession(user, privateComponent); + assertThat(projectAnalysisTokenUserSession.getTokenType()).isEqualTo(PROJECT_ANALYSIS_TOKEN); + + TokenUserSession globalAnalysisTokenUserSession = mockGlobalAnalysisTokenUserSession(user); + assertThat(globalAnalysisTokenUserSession.getTokenType()).isEqualTo(GLOBAL_ANALYSIS_TOKEN); + } + private TokenUserSession mockTokenUserSession(UserDto userDto) { return new TokenUserSession(dbClient, userDto, mockUserTokenDto()); } @@ -190,6 +405,10 @@ public class TokenUserSessionTest { return new TokenUserSession(dbClient, userDto, mockGlobalAnalysisTokenDto()); } + private TokenUserSession mockProjectBadgeTokenSession(UserDto userDto) { + return new TokenUserSession(dbClient, userDto, mockBadgeTokenDto()); + } + private static UserTokenDto mockUserTokenDto() { UserTokenDto userTokenDto = new UserTokenDto(); userTokenDto.setType(USER_TOKEN.name()); @@ -217,4 +436,12 @@ public class TokenUserSessionTest { return userTokenDto; } + private static UserTokenDto mockBadgeTokenDto() { + UserTokenDto userTokenDto = new UserTokenDto(); + userTokenDto.setType(PROJECT_BADGE_TOKEN.name()); + userTokenDto.setName("Badge token"); + userTokenDto.setUserUuid("userUid"); + return userTokenDto; + } + } diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/component/ws/SearchProjectsAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/component/ws/SearchProjectsAction.java index a2e8b11ea63..74f3355eece 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/component/ws/SearchProjectsAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/component/ws/SearchProjectsAction.java @@ -45,6 +45,7 @@ import org.sonar.api.server.ws.Request; import org.sonar.api.server.ws.Response; import org.sonar.api.server.ws.WebService; import org.sonar.api.server.ws.WebService.Param; +import org.sonar.api.web.UserRole; import org.sonar.core.platform.EditionProvider.Edition; import org.sonar.core.platform.PlatformEditionProvider; import org.sonar.core.util.stream.MoreCollectors; @@ -55,6 +56,7 @@ import org.sonar.db.component.SnapshotDto; import org.sonar.db.project.ProjectDto; import org.sonar.db.property.PropertyDto; import org.sonar.db.property.PropertyQuery; +import org.sonar.db.user.TokenType; import org.sonar.server.component.ws.FilterParser.Criterion; import org.sonar.server.component.ws.SearchProjectsAction.SearchResults.SearchResultsBuilder; import org.sonar.server.es.Facets; @@ -65,6 +67,7 @@ import org.sonar.server.measure.index.ProjectMeasuresIndex; import org.sonar.server.measure.index.ProjectMeasuresQuery; import org.sonar.server.project.Visibility; import org.sonar.server.qualitygate.ProjectsInWarning; +import org.sonar.server.user.TokenUserSession; import org.sonar.server.user.UserSession; import org.sonarqube.ws.Common; import org.sonarqube.ws.Components.Component; @@ -73,6 +76,7 @@ import org.sonarqube.ws.Components.SearchProjectsWsResponse; import static com.google.common.base.MoreObjects.firstNonNull; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.collect.Sets.newHashSet; +import static java.util.Collections.emptyList; import static java.util.Collections.emptyMap; import static java.util.Objects.requireNonNull; import static java.util.Optional.ofNullable; @@ -243,12 +247,15 @@ public class SearchProjectsAction implements ComponentsWsAction { ProjectMeasuresQueryValidator.validate(query); SearchIdResult esResults = index.search(query, new SearchOptions() - .addFacets(request.getFacets()) + // skip facets for project token authorization, avoid exposing unauthorized projects count + .addFacets(isProjectAnalysisToken() ? emptyList() : request.getFacets()) .setPage(request.getPage(), request.getPageSize())); List projectUuids = esResults.getUuids(); Ordering ordering = Ordering.explicit(projectUuids).onResultOf(ProjectDto::getUuid); List projects = ordering.immutableSortedCopy(dbClient.projectDao().selectByUuids(dbSession, new HashSet<>(projectUuids))); + projects = userSession.keepAuthorizedProjects(UserRole.USER, projects); + Map analysisByProjectUuid = getSnapshots(dbSession, request, projectUuids); Map applicationsLeakPeriod = getApplicationsLeakPeriod(dbSession, request, qualifiersBasedOnEdition, projectUuids); @@ -287,7 +294,7 @@ public class SearchProjectsAction implements ComponentsWsAction { private Set getQualifiersFromEdition() { Optional edition = editionProvider.get(); - if (!edition.isPresent()) { + if (edition.isEmpty()) { return Sets.newHashSet(Qualifiers.PROJECT); } @@ -377,7 +384,8 @@ public class SearchProjectsAction implements ComponentsWsAction { .map(response -> response.setPaging(Common.Paging.newBuilder() .setPageIndex(request.getPage()) .setPageSize(request.getPageSize()) - .setTotal(searchResults.total))) + // skip total for project token authorization, avoid exposing unauthorized projects count + .setTotal(isProjectAnalysisToken() ? searchResults.projects.size() : searchResults.total))) .map(response -> { searchResults.projects.stream() .map(dbToWsComponent) @@ -719,4 +727,11 @@ public class SearchProjectsAction implements ComponentsWsAction { return new SearchProjectsRequest(this); } } + + private boolean isProjectAnalysisToken() { + if (userSession instanceof TokenUserSession tokenUserSession) { + return TokenType.PROJECT_ANALYSIS_TOKEN.equals(tokenUserSession.getTokenType()); + } + return false; + } } diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/component/ws/SearchProjectsActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/component/ws/SearchProjectsActionTest.java index 0f32005b93b..160ce5c35f8 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/component/ws/SearchProjectsActionTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/component/ws/SearchProjectsActionTest.java @@ -42,6 +42,7 @@ import org.sonar.api.resources.Qualifiers; import org.sonar.api.server.ws.WebService; import org.sonar.api.server.ws.WebService.Param; import org.sonar.api.utils.System2; +import org.sonar.api.web.UserRole; import org.sonar.core.platform.EditionProvider.Edition; import org.sonar.core.platform.PlatformEditionProvider; import org.sonar.db.DbClient; @@ -126,27 +127,27 @@ public class SearchProjectsActionTest { @DataProvider public static Object[][] rating_metric_keys() { - return new Object[][]{{SQALE_RATING_KEY}, {RELIABILITY_RATING_KEY}, {SECURITY_RATING_KEY}}; + return new Object[][] {{SQALE_RATING_KEY}, {RELIABILITY_RATING_KEY}, {SECURITY_RATING_KEY}}; } @DataProvider public static Object[][] new_rating_metric_keys() { - return new Object[][]{{NEW_MAINTAINABILITY_RATING_KEY}, {NEW_RELIABILITY_RATING_KEY}, {NEW_SECURITY_RATING_KEY}}; + return new Object[][] {{NEW_MAINTAINABILITY_RATING_KEY}, {NEW_RELIABILITY_RATING_KEY}, {NEW_SECURITY_RATING_KEY}}; } @DataProvider public static Object[][] component_qualifiers_for_valid_editions() { - return new Object[][]{ - {new String[]{Qualifiers.PROJECT}, Edition.COMMUNITY}, - {new String[]{Qualifiers.APP, Qualifiers.PROJECT}, Edition.DEVELOPER}, - {new String[]{Qualifiers.APP, Qualifiers.PROJECT}, Edition.ENTERPRISE}, - {new String[]{Qualifiers.APP, Qualifiers.PROJECT}, Edition.DATACENTER}, + return new Object[][] { + {new String[] {Qualifiers.PROJECT}, Edition.COMMUNITY}, + {new String[] {Qualifiers.APP, Qualifiers.PROJECT}, Edition.DEVELOPER}, + {new String[] {Qualifiers.APP, Qualifiers.PROJECT}, Edition.ENTERPRISE}, + {new String[] {Qualifiers.APP, Qualifiers.PROJECT}, Edition.DATACENTER}, }; } @DataProvider public static Object[][] community_or_developer_edition() { - return new Object[][]{ + return new Object[][] { {Edition.COMMUNITY}, {Edition.DEVELOPER}, }; @@ -154,25 +155,25 @@ public class SearchProjectsActionTest { @DataProvider public static Object[][] enterprise_or_datacenter_edition() { - return new Object[][]{ + return new Object[][] { {Edition.ENTERPRISE}, {Edition.DATACENTER}, }; } - private DbClient dbClient = db.getDbClient(); - private DbSession dbSession = db.getSession(); + private final DbClient dbClient = db.getDbClient(); + private final DbSession dbSession = db.getSession(); - private PlatformEditionProvider editionProviderMock = mock(PlatformEditionProvider.class); - private PermissionIndexerTester authorizationIndexerTester = new PermissionIndexerTester(es, new ProjectMeasuresIndexer(dbClient, es.client())); - private ProjectMeasuresIndex index = new ProjectMeasuresIndex(es.client(), new WebAuthorizationTypeSupport(userSession), System2.INSTANCE); - private ProjectMeasuresIndexer projectMeasuresIndexer = new ProjectMeasuresIndexer(db.getDbClient(), es.client()); - private ProjectsInWarning projectsInWarning = new ProjectsInWarning(); + private final PlatformEditionProvider editionProviderMock = mock(PlatformEditionProvider.class); + private final PermissionIndexerTester authorizationIndexerTester = new PermissionIndexerTester(es, new ProjectMeasuresIndexer(dbClient, es.client())); + private final ProjectMeasuresIndex index = new ProjectMeasuresIndex(es.client(), new WebAuthorizationTypeSupport(userSession), System2.INSTANCE); + private final ProjectMeasuresIndexer projectMeasuresIndexer = new ProjectMeasuresIndexer(db.getDbClient(), es.client()); + private final ProjectsInWarning projectsInWarning = new ProjectsInWarning(); - private WsActionTester ws = new WsActionTester(new SearchProjectsAction(dbClient, index, userSession, projectsInWarning, editionProviderMock, + private final WsActionTester ws = new WsActionTester(new SearchProjectsAction(dbClient, index, userSession, projectsInWarning, editionProviderMock, new IssueIndexSyncProgressChecker(db.getDbClient()))); - private RequestBuilder request = SearchProjectsRequest.builder(); + private final RequestBuilder request = SearchProjectsRequest.builder(); @Before public void setUp() { @@ -1265,12 +1266,14 @@ public class SearchProjectsActionTest { @Test public void return_visibility_flag() { userSession.logIn(); - ComponentDto privateProject = db.components().insertPublicProject(); + ComponentDto privateProject = db.components().insertPrivateProject(); authorizationIndexerTester.allowOnlyAnyone(privateProject); - ComponentDto publicProject = db.components().insertPrivateProject(); + ComponentDto publicProject = db.components().insertPublicProject(); authorizationIndexerTester.allowOnlyAnyone(publicProject); index(); + userSession.addProjectPermission(UserRole.USER, privateProject); + SearchProjectsWsResponse result = call(request); assertThat(result.getComponentsList()).extracting(Component::getKey, Component::getVisibility)