From 90a5b893f38dba0cea390bb44d18e334c53fa114 Mon Sep 17 00:00:00 2001 From: Klaudio Sinani Date: Wed, 12 Jan 2022 09:37:17 +0100 Subject: [PATCH] SONAR-15880 Filter authorized components from `api/measures/component_tree` & `api/components/tree` endpoints. --- .../server/user/AbstractUserSession.java | 18 ++ .../sonar/server/user/ServerUserSession.java | 14 +- .../server/user/ThreadLocalUserSession.java | 5 + .../org/sonar/server/user/UserSession.java | 2 + .../server/user/ServerUserSessionTest.java | 196 ++++++++++++++++++ .../user/ThreadLocalUserSessionTest.java | 6 +- .../sonar/server/tester/UserSessionRule.java | 5 + .../sonar/server/component/ws/TreeAction.java | 6 + .../measure/ws/ComponentTreeAction.java | 5 + .../measure/ws/component_tree-example.json | 2 +- 10 files changed, 254 insertions(+), 5 deletions(-) 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 29914eb211f..61baa640090 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 @@ -174,6 +174,15 @@ public abstract class AbstractUserSession implements UserSession { return doKeepAuthorizedProjects(permission, projects); } + @Override + public List filterAuthorizedComponents(String permission, Collection components) { + if (isRoot()) { + return new ArrayList<>(components); + } + + return doFilterAuthorizedComponents(permission, components); + } + /** * Naive implementation, to be overridden if needed */ @@ -194,6 +203,15 @@ public abstract class AbstractUserSession implements UserSession { .collect(MoreCollectors.toList()); } + /** + * Naive implementation, to be overridden if needed + */ + protected List doFilterAuthorizedComponents(String permission, Collection components) { + return components.stream() + .filter(c -> (PUBLIC_PERMISSIONS.contains(permission) && !c.isPrivate()) || hasComponentPermission(permission, c)) + .collect(MoreCollectors.toList()); + } + @Override public UserSession checkIsRoot() { if (!isRoot()) { 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 7acb465d77d..c6115691643 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 @@ -248,9 +248,7 @@ public class ServerUserSession extends AbstractUserSession { private Set resolvePortfolioHierarchyComponents(String parentComponentUuid) { Set portfolioHierarchyProjects = new HashSet<>(); - resolvePortfolioHierarchyComponents(parentComponentUuid, portfolioHierarchyProjects); - return portfolioHierarchyProjects; } @@ -307,6 +305,18 @@ public class ServerUserSession extends AbstractUserSession { } } + @Override + protected List doFilterAuthorizedComponents(String permission, Collection components) { + if (permissionsByProjectUuid == null) { + permissionsByProjectUuid = new HashMap<>(); + } + + return components + .stream() + .filter(x -> hasPermission(permission, defaultIfEmpty(x.getCopyComponentUuid(), x.uuid()))) + .collect(Collectors.toList()); + } + @Override public boolean isSystemAdministrator() { if (isSystemAdministrator == null) { diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/user/ThreadLocalUserSession.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/user/ThreadLocalUserSession.java index 3dec0032d96..bc613ae6332 100644 --- a/server/sonar-webserver-auth/src/main/java/org/sonar/server/user/ThreadLocalUserSession.java +++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/user/ThreadLocalUserSession.java @@ -217,4 +217,9 @@ public class ThreadLocalUserSession implements UserSession { public List keepAuthorizedProjects(String permission, Collection projects) { return get().keepAuthorizedProjects(permission, projects); } + + @Override + public List filterAuthorizedComponents(String permission, Collection components) { + return get().filterAuthorizedComponents(permission, components); + } } diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/user/UserSession.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/user/UserSession.java index 5350e578539..e2d77c8d8d2 100644 --- a/server/sonar-webserver-auth/src/main/java/org/sonar/server/user/UserSession.java +++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/user/UserSession.java @@ -224,6 +224,8 @@ public interface UserSession { List keepAuthorizedProjects(String permission, Collection projects); + List filterAuthorizedComponents(String permission, Collection components); + /** * Ensures that {@link #hasComponentPermission(String, ComponentDto)} is {@code true}, * otherwise throws a {@link org.sonar.server.exceptions.ForbiddenException}. diff --git a/server/sonar-webserver-auth/src/test/java/org/sonar/server/user/ServerUserSessionTest.java b/server/sonar-webserver-auth/src/test/java/org/sonar/server/user/ServerUserSessionTest.java index 8fdd4ed8d83..7c53df2434d 100644 --- a/server/sonar-webserver-auth/src/test/java/org/sonar/server/user/ServerUserSessionTest.java +++ b/server/sonar-webserver-auth/src/test/java/org/sonar/server/user/ServerUserSessionTest.java @@ -743,6 +743,202 @@ public class ServerUserSessionTest { .containsExactlyInAnyOrder(privateProject, privateBranchProject); } + @Test + public void filterAuthorizedComponents_returns_empty_list_if_no_permissions_are_granted() { + ComponentDto publicProject = db.components().insertPublicProject(); + ComponentDto privateProject = db.components().insertPrivateProject(); + + UserSession underTest = newAnonymousSession(); + + assertThat(underTest.filterAuthorizedComponents(ADMIN, Arrays.asList(privateProject, publicProject))).isEmpty(); + } + + @Test + public void filterAuthorizedComponents_filters_components_with_granted_permissions_for_logged_in_user() { + ComponentDto project1 = db.components().insertPublicProject(); + ComponentDto project2 = db.components().insertPrivateProject(); + ComponentDto project3 = db.components().insertPrivateProject(); + ComponentDto project4 = db.components().insertPrivateProject(); + ComponentDto project5 = db.components().insertPrivateProject(); + ComponentDto project6 = db.components().insertPrivateProject(); + + UserDto user = db.users().insertUser(); + UserSession underTest = newUserSession(user); + + ComponentDto portfolio = db.components().insertPrivatePortfolio(); + db.users().insertProjectPermissionOnUser(user, USER, portfolio); + + ComponentDto subPortfolio = db.components().insertComponent(newSubPortfolio(portfolio)); + db.users().insertProjectPermissionOnUser(user, USER, subPortfolio); + + ComponentDto app = db.components().insertPrivateApplication(); + db.users().insertProjectPermissionOnUser(user, USER, app); + + ComponentDto app2 = db.components().insertPrivateApplication(); + + // Add public project1 to private portfolio + db.components().addPortfolioProject(portfolio, project1); + db.components().insertComponent(newProjectCopy(project1, portfolio)); + + // Add private project2 with USER permissions to private portfolio + db.users().insertProjectPermissionOnUser(user, USER, project2); + db.components().addPortfolioProject(portfolio, project2); + db.components().insertComponent(newProjectCopy(project2, portfolio)); + + // Add private project4 with USER permissions to sub-portfolio + db.users().insertProjectPermissionOnUser(user, USER, project4); + db.components().addPortfolioProject(subPortfolio, project4); + db.components().insertComponent(newProjectCopy(project4, subPortfolio)); + db.components().addPortfolioReference(portfolio, subPortfolio.uuid()); + + // Add private project3 without permissions to private portfolio + db.components().addPortfolioProject(portfolio, project3); + db.components().insertComponent(newProjectCopy(project3, portfolio)); + + // Add private project5 with USER permissions to app + db.users().insertProjectPermissionOnUser(user, USER, project5); + db.components().addApplicationProject(app, project5); + db.components().insertComponent(newProjectCopy(project5, app)); + db.components().addPortfolioReference(portfolio, app.uuid()); + + // Add private project6 to private app2 + db.components().addApplicationProject(app2, project6); + db.components().insertComponent(newProjectCopy(project6, app2)); + db.components().addPortfolioReference(portfolio, app2.uuid()); + + assertThat(underTest.filterAuthorizedComponents(ADMIN, Arrays.asList(portfolio))).isEmpty(); + assertThat(underTest.filterAuthorizedComponents(USER, Arrays.asList(portfolio))).hasSize(1); + assertThat(underTest.filterAuthorizedComponents(USER, Arrays.asList(portfolio))).containsExactly(portfolio); + + assertThat(underTest.filterAuthorizedComponents(ADMIN, Arrays.asList(app, subPortfolio, app2))).isEmpty(); + assertThat(underTest.filterAuthorizedComponents(USER, Arrays.asList(app, subPortfolio, app2))).hasSize(2); + assertThat(underTest.filterAuthorizedComponents(USER, Arrays.asList(app, subPortfolio, app2))).containsExactly(app, subPortfolio); + + assertThat(underTest.filterAuthorizedComponents(ADMIN, Arrays.asList(project1, project2, project3, project4, project5, project6))).isEmpty(); + assertThat(underTest.filterAuthorizedComponents(USER, Arrays.asList(project1, project2, project3, project4, project5, project6))).hasSize(4); + assertThat(underTest.filterAuthorizedComponents(USER, Arrays.asList(project1, project2, project3, project4, project5, project6))).containsExactly(project1, project2, project4, project5); + } + + @Test + public void filterAuthorizedComponents_returns_all_specified_components_if_root() { + UserDto root = db.users().insertUser(); + root = db.users().makeRoot(root); + UserSession underTest = newUserSession(root); + + ComponentDto project1 = db.components().insertPublicProject(); + ComponentDto project2 = db.components().insertPrivateProject(); + ComponentDto project3 = db.components().insertPrivateProject(); + ComponentDto project4 = db.components().insertPrivateProject(); + ComponentDto project5 = db.components().insertPrivateProject(); + ComponentDto project6 = db.components().insertPrivateProject(); + + ComponentDto portfolio = db.components().insertPrivatePortfolio(); + + ComponentDto subPortfolio = db.components().insertComponent(newSubPortfolio(portfolio)); + + ComponentDto app = db.components().insertPrivateApplication(); + + ComponentDto app2 = db.components().insertPrivateApplication(); + + // Add public project1 to private portfolio + db.components().addPortfolioProject(portfolio, project1); + db.components().insertComponent(newProjectCopy(project1, portfolio)); + + // Add private project2 to private portfolio + db.components().addPortfolioProject(portfolio, project2); + db.components().insertComponent(newProjectCopy(project2, portfolio)); + + // Add private project4 to sub-portfolio + db.components().addPortfolioProject(subPortfolio, project4); + db.components().insertComponent(newProjectCopy(project4, subPortfolio)); + db.components().addPortfolioReference(portfolio, subPortfolio.uuid()); + + // Add private project3 without permissions to private portfolio + db.components().addPortfolioProject(portfolio, project3); + db.components().insertComponent(newProjectCopy(project3, portfolio)); + + // Add private project5 to app + db.components().addApplicationProject(app, project5); + db.components().insertComponent(newProjectCopy(project5, app)); + db.components().addPortfolioReference(portfolio, app.uuid()); + + // Add private project6 to private app2 + db.components().addApplicationProject(app2, project6); + db.components().insertComponent(newProjectCopy(project6, app2)); + db.components().addPortfolioReference(portfolio, app2.uuid()); + + assertThat(underTest.filterAuthorizedComponents(ADMIN, Arrays.asList(portfolio))).hasSize(1); + assertThat(underTest.filterAuthorizedComponents(ADMIN, Arrays.asList(portfolio))).containsExactly(portfolio); + + assertThat(underTest.filterAuthorizedComponents(ADMIN, Arrays.asList(app, subPortfolio, app2))).hasSize(3); + assertThat(underTest.filterAuthorizedComponents(ADMIN, Arrays.asList(app, subPortfolio, app2))).containsExactly(app, subPortfolio, app2); + + assertThat(underTest.filterAuthorizedComponents(ADMIN, Arrays.asList(project1, project2, project3, project4, project5, project6))).hasSize(6); + assertThat(underTest.filterAuthorizedComponents(ADMIN, Arrays.asList(project1, project2, project3, project4, project5, project6))).containsExactly(project1, project2, project3, project4, project5, project6); + } + + @Test + public void filterAuthorizedComponents_filters_components_with_granted_permissions_for_anonymous() { + UserSession underTest = newAnonymousSession(); + + ComponentDto project1 = db.components().insertPublicProject(); + ComponentDto project2 = db.components().insertPrivateProject(); + ComponentDto project3 = db.components().insertPrivateProject(); + ComponentDto project4 = db.components().insertPrivateProject(); + ComponentDto project5 = db.components().insertPrivateProject(); + ComponentDto project6 = db.components().insertPrivateProject(); + + ComponentDto portfolio = db.components().insertPublicPortfolio(); + db.users().insertProjectPermissionOnAnyone(ISSUE_ADMIN, portfolio); + + ComponentDto subPortfolio = db.components().insertComponent(newSubPortfolio(portfolio)); + db.users().insertProjectPermissionOnAnyone(ISSUE_ADMIN, subPortfolio); + + ComponentDto app = db.components().insertPrivateApplication(); + + ComponentDto app2 = db.components().insertPublicApplication(); + db.users().insertProjectPermissionOnAnyone(ISSUE_ADMIN, app2); + + // Add public project1 to portfolio + db.components().addPortfolioProject(portfolio, project1); + db.components().insertComponent(newProjectCopy(project1, portfolio)); + db.users().insertProjectPermissionOnAnyone(ISSUE_ADMIN, project1); + + // Add private project2 to portfolio + db.components().addPortfolioProject(portfolio, project2); + db.components().insertComponent(newProjectCopy(project2, portfolio)); + + // Add private project4 to sub-portfolio + db.components().addPortfolioProject(subPortfolio, project4); + db.components().insertComponent(newProjectCopy(project4, subPortfolio)); + db.components().addPortfolioReference(portfolio, subPortfolio.uuid()); + + // Add private project3 to portfolio + db.components().addPortfolioProject(portfolio, project3); + db.components().insertComponent(newProjectCopy(project3, portfolio)); + + // Add private project5 to app + db.components().addApplicationProject(app, project5); + db.components().insertComponent(newProjectCopy(project5, app)); + db.components().addPortfolioReference(portfolio, app.uuid()); + + // Add private project6 to app2 + db.components().addApplicationProject(app2, project6); + db.components().insertComponent(newProjectCopy(project6, app2)); + db.components().addPortfolioReference(portfolio, app2.uuid()); + + assertThat(underTest.filterAuthorizedComponents(ADMIN, Arrays.asList(portfolio))).isEmpty(); + assertThat(underTest.filterAuthorizedComponents(ISSUE_ADMIN, Arrays.asList(portfolio))).hasSize(1); + + assertThat(underTest.filterAuthorizedComponents(ADMIN, Arrays.asList(app, subPortfolio, app2))).isEmpty(); + assertThat(underTest.filterAuthorizedComponents(USER, Arrays.asList(app, subPortfolio, app2))).hasSize(2); + assertThat(underTest.filterAuthorizedComponents(USER, Arrays.asList(app, subPortfolio, app2))).containsExactly(subPortfolio, app2); + + assertThat(underTest.filterAuthorizedComponents(ADMIN, Arrays.asList(project1, project2, project3, project4, project5, project6))).isEmpty(); + assertThat(underTest.filterAuthorizedComponents(USER, Arrays.asList(project1, project2, project3, project4, project5, project6))).hasSize(1); + assertThat(underTest.filterAuthorizedComponents(USER, Arrays.asList(project1, project2, project3, project4, project5, project6))).containsExactly(project1); + } + @Test public void isSystemAdministrator_returns_true_if_org_feature_is_enabled_and_user_is_root() { UserDto root = db.users().insertUser(); diff --git a/server/sonar-webserver-auth/src/test/java/org/sonar/server/user/ThreadLocalUserSessionTest.java b/server/sonar-webserver-auth/src/test/java/org/sonar/server/user/ThreadLocalUserSessionTest.java index 1ef29694dd6..9fdc9d6ae4e 100644 --- a/server/sonar-webserver-auth/src/test/java/org/sonar/server/user/ThreadLocalUserSessionTest.java +++ b/server/sonar-webserver-auth/src/test/java/org/sonar/server/user/ThreadLocalUserSessionTest.java @@ -19,12 +19,12 @@ */ package org.sonar.server.user; +import java.util.Arrays; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.sonar.api.resources.Qualifiers; import org.sonar.db.component.ComponentDto; -import org.sonar.db.portfolio.PortfolioDto; import org.sonar.db.project.ProjectDto; import org.sonar.db.user.GroupDto; import org.sonar.db.user.GroupTesting; @@ -75,6 +75,7 @@ public class ThreadLocalUserSessionTest { assertThat(threadLocalUserSession.hasChildProjectsPermission(USER, new ProjectDto())).isFalse(); assertThat(threadLocalUserSession.hasPortfolioChildProjectsPermission(USER, new ComponentDto())).isFalse(); assertThat(threadLocalUserSession.hasProjectPermission(USER, new ProjectDto().getUuid())).isFalse(); + assertThat(threadLocalUserSession.filterAuthorizedComponents(USER, Arrays.asList(new ComponentDto().setPrivate(true)))).isEmpty(); } @Test @@ -100,6 +101,8 @@ public class ThreadLocalUserSessionTest { assertThat(threadLocalUserSession.hasChildProjectsPermission(USER, new ProjectDto())).isTrue(); assertThat(threadLocalUserSession.hasPortfolioChildProjectsPermission(USER, new ComponentDto())).isTrue(); assertThat(threadLocalUserSession.hasProjectPermission(USER, new ProjectDto().getUuid())).isTrue(); + assertThat(threadLocalUserSession.filterAuthorizedComponents(USER, Arrays.asList(new ComponentDto().setPrivate(true)))).hasSize(1); + assertThat(threadLocalUserSession.filterAuthorizedComponents(USER, Arrays.asList(new ComponentDto().setPrivate(true)))).containsExactly(new ComponentDto()); } @Test @@ -160,5 +163,4 @@ public class ThreadLocalUserSessionTest { assertThat(threadLocalUserSession.checkChildProjectsPermission(USER, applicationAsComponentDto)).isEqualTo(threadLocalUserSession); assertThat(threadLocalUserSession.checkChildProjectsPermission(USER, applicationAsProjectDto)).isEqualTo(threadLocalUserSession); } - } diff --git a/server/sonar-webserver-auth/src/testFixtures/java/org/sonar/server/tester/UserSessionRule.java b/server/sonar-webserver-auth/src/testFixtures/java/org/sonar/server/tester/UserSessionRule.java index c762d013731..c6ae4eec214 100644 --- a/server/sonar-webserver-auth/src/testFixtures/java/org/sonar/server/tester/UserSessionRule.java +++ b/server/sonar-webserver-auth/src/testFixtures/java/org/sonar/server/tester/UserSessionRule.java @@ -296,6 +296,11 @@ public class UserSessionRule implements TestRule, UserSession { return currentUserSession.keepAuthorizedProjects(permission, projects); } + @Override + public List filterAuthorizedComponents(String permission, Collection components) { + return currentUserSession.filterAuthorizedComponents(permission, components); + } + @Override @CheckForNull public String getLogin() { diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/component/ws/TreeAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/component/ws/TreeAction.java index a9c8b5ec9db..357bafe70ae 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/component/ws/TreeAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/component/ws/TreeAction.java @@ -177,6 +177,8 @@ public class TreeAction implements ComponentsWsAction { ComponentTreeQuery query = toComponentTreeQuery(treeRequest, baseComponent); List components = dbClient.componentDao().selectDescendants(dbSession, query); + components = filterAuthorizedComponents(components); + int total = components.size(); components = sortComponents(components, treeRequest); components = paginateComponents(components, treeRequest); @@ -188,6 +190,10 @@ public class TreeAction implements ComponentsWsAction { } } + private List filterAuthorizedComponents(List components) { + return userSession.filterAuthorizedComponents(UserRole.USER, components); + } + private ComponentDto loadComponent(DbSession dbSession, Request request) { String componentKey = request.getComponent(); String branch = request.getBranch(); diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/ws/ComponentTreeAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/ws/ComponentTreeAction.java index caeee46a833..c6fc0f6501f 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/ws/ComponentTreeAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/ws/ComponentTreeAction.java @@ -434,6 +434,7 @@ public class ComponentTreeAction implements MeasuresWsAction { } components = filterComponents(components, measuresByComponentUuidAndMetric, metrics, wsRequest); + components = filterAuthorizedComponents(components); components = sortComponents(components, wsRequest, metrics, measuresByComponentUuidAndMetric); int componentCount = components.size(); @@ -574,6 +575,10 @@ public class ComponentTreeAction implements MeasuresWsAction { .collect(MoreCollectors.toList(components.size())); } + private List filterAuthorizedComponents(List components) { + return userSession.filterAuthorizedComponents(UserRole.USER, components); + } + private static boolean componentWithMeasuresOnly(ComponentTreeRequest wsRequest) { return WITH_MEASURES_ONLY_METRIC_SORT_FILTER.equals(wsRequest.getMetricSortFilter()); } diff --git a/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/measure/ws/component_tree-example.json b/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/measure/ws/component_tree-example.json index 7ba459d51c6..f87909519e9 100644 --- a/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/measure/ws/component_tree-example.json +++ b/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/measure/ws/component_tree-example.json @@ -122,7 +122,7 @@ "type": "INT", "higherValuesAreBetter": false, "qualitative": true, - "hidden": false + "hidden": false, "bestValue": "0" } ], -- 2.39.5