]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-23265 Fix SSF-623
authorJulien Camus <julien.camus@sonarsource.com>
Fri, 4 Oct 2024 14:59:43 +0000 (16:59 +0200)
committersonartech <sonartech@sonarsource.com>
Fri, 4 Oct 2024 20:03:46 +0000 (20:03 +0000)
server/sonar-db-dao/src/main/java/org/sonar/db/user/UserDao.java
server/sonar-webserver-api/src/main/java/org/sonar/server/exceptions/ResourceForbiddenException.java [new file with mode: 0644]
server/sonar-webserver-api/src/test/java/org/sonar/server/exceptions/ResourceForbiddenExceptionTest.java [new file with mode: 0644]
server/sonar-webserver-auth/src/main/java/org/sonar/server/user/AbstractUserSession.java
server/sonar-webserver-auth/src/main/java/org/sonar/server/user/ThreadLocalUserSession.java
server/sonar-webserver-auth/src/main/java/org/sonar/server/user/UserSession.java
server/sonar-webserver-auth/src/test/java/org/sonar/server/user/ThreadLocalUserSessionTest.java
server/sonar-webserver-auth/src/testFixtures/java/org/sonar/server/tester/UserSessionRule.java
server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/membership/controller/DefaultGroupMembershipController.java
server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projectbindings/controller/DefaultProjectBindingsController.java
server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/projectbindings/controller/DefaultProjectBindingsControllerTest.java

index 39d4954a4ef1136e2af54daa052dcc5851076c59..4172316e4441fa30199311bb65588af0d2688f79 100644 (file)
@@ -78,7 +78,7 @@ public class UserDao implements Dao {
    * Select users by uuids, including disabled users. An empty list is returned
    * if list of uuids is empty, without any db round trips.
    *
-   * @return
+   * @return a list of users, in the same order as input uuids
    */
   public List<UserDto> selectByUuids(DbSession session, Collection<String> uuids) {
     return executeLargeInputs(uuids, mapper(session)::selectByUuids);
@@ -113,8 +113,7 @@ public class UserDao implements Dao {
         userQuery.getUserUuids(),
         partialSetOfUsers -> mapper(dbSession).selectUsers(
           UserQuery.copyWithNewRangeOfUserUuids(userQuery, partialSetOfUsers),
-          pagination)
-      );
+          pagination));
     }
     return mapper(dbSession).selectUsers(userQuery, pagination);
   }
@@ -191,7 +190,7 @@ public class UserDao implements Dao {
 
   /**
    * Search for an active user with the given emailCaseInsensitive exits in database
-   * Select is case insensitive. Result for searching 'mail@emailCaseInsensitive.com' or 'Mail@Email.com' is the same
+   * Select is case-insensitive. Result for searching 'mail@emailCaseInsensitive.com' or 'Mail@Email.com' is the same
    */
   public List<UserDto> selectByEmail(DbSession dbSession, String emailCaseInsensitive) {
     return mapper(dbSession).selectByEmail(emailCaseInsensitive.toLowerCase(ENGLISH));
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/exceptions/ResourceForbiddenException.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/exceptions/ResourceForbiddenException.java
new file mode 100644 (file)
index 0000000..10eca89
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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.exceptions;
+
+import static java.net.HttpURLConnection.HTTP_NOT_FOUND;
+
+public class ResourceForbiddenException extends ServerException {
+
+  private static final String ERROR_MESSAGE = "You do not have access to this resource or it does not exist.";
+
+  public ResourceForbiddenException() {
+    super(HTTP_NOT_FOUND, ERROR_MESSAGE);
+  }
+
+}
diff --git a/server/sonar-webserver-api/src/test/java/org/sonar/server/exceptions/ResourceForbiddenExceptionTest.java b/server/sonar-webserver-api/src/test/java/org/sonar/server/exceptions/ResourceForbiddenExceptionTest.java
new file mode 100644 (file)
index 0000000..0466fb5
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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.exceptions;
+
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class ResourceForbiddenExceptionTest {
+
+  // Test the default constructor
+  @Test
+  public void defaultConstructor_shouldSetDefaultErrorMessage() {
+    ResourceForbiddenException exception = new ResourceForbiddenException();
+
+    assertThat(exception.getMessage()).isEqualTo("You do not have access to this resource or it does not exist.");
+  }
+
+}
index 9074b5d5cc9e3f7e0f828dd1bfa3e61b40c5038b..a4ccfeef44dbbfbf6e3c8d0325e771f2326ed6f6 100644 (file)
@@ -31,6 +31,7 @@ import org.sonar.db.entity.EntityDto;
 import org.sonar.db.permission.GlobalPermission;
 import org.sonar.db.user.UserDto;
 import org.sonar.server.exceptions.ForbiddenException;
+import org.sonar.server.exceptions.ResourceForbiddenException;
 import org.sonar.server.exceptions.UnauthorizedException;
 
 import static org.sonar.api.resources.Qualifiers.APP;
@@ -142,14 +143,14 @@ public abstract class AbstractUserSession implements UserSession {
   }
 
   @Override
-  public final <T extends EntityDto>  List<T> keepAuthorizedEntities(String permission, Collection<T> projects) {
+  public final <T extends EntityDto> List<T> keepAuthorizedEntities(String permission, Collection<T> projects) {
     return doKeepAuthorizedEntities(permission, projects);
   }
 
   /**
    * Naive implementation, to be overridden if needed
    */
-  protected  <T extends EntityDto> List<T> doKeepAuthorizedEntities(String permission, Collection<T> entities) {
+  protected <T extends EntityDto> List<T> doKeepAuthorizedEntities(String permission, Collection<T> entities) {
     boolean allowPublicComponent = PUBLIC_PERMISSIONS.contains(permission);
     return entities.stream()
       .filter(c -> (allowPublicComponent && !c.isPrivate()) || hasEntityPermission(permission, c.getUuid()))
@@ -199,6 +200,15 @@ public abstract class AbstractUserSession implements UserSession {
     throw new ForbiddenException(INSUFFICIENT_PRIVILEGES_MESSAGE);
   }
 
+  @Override
+  public UserSession checkEntityPermissionOrElseThrowResourceForbiddenException(String projectPermission, EntityDto entity) {
+    if (hasEntityPermission(projectPermission, entity)) {
+      return this;
+    }
+
+    throw new ResourceForbiddenException();
+  }
+
   @Override
   public UserSession checkChildProjectsPermission(String projectPermission, ComponentDto component) {
     if (!APP.equals(component.qualifier()) || hasChildProjectsPermission(projectPermission, component)) {
index db9ac4caf57bc5866c831ff061d546af75cf319c..cbab0d8faf7432259ece2974939e69f43b35bc69 100644 (file)
@@ -134,6 +134,12 @@ public class ThreadLocalUserSession implements UserSession {
     return this;
   }
 
+  @Override
+  public UserSession checkEntityPermissionOrElseThrowResourceForbiddenException(String projectPermission, EntityDto entity) {
+    get().checkEntityPermissionOrElseThrowResourceForbiddenException(projectPermission, entity);
+    return this;
+  }
+
   @Override
   public UserSession checkChildProjectsPermission(String projectPermission, ComponentDto component) {
     get().checkChildProjectsPermission(projectPermission, component);
index ab02c6b88f11c4c0b837a98e828b27f9fdfc7878..195c3fb8ec5b6d1292dfa54edf225381ad40efc5 100644 (file)
@@ -71,7 +71,7 @@ public interface UserSession {
   enum IdentityProvider {
     SONARQUBE("sonarqube"), GITHUB("github"), BITBUCKETCLOUD("bitbucket"), OTHER("other");
 
-    String key;
+    final String key;
 
     IdentityProvider(String key) {
       this.key = key;
@@ -189,6 +189,14 @@ public interface UserSession {
    */
   UserSession checkEntityPermission(String projectPermission, EntityDto entity);
 
+  /**
+   * Ensures that {@link #hasEntityPermission(String, EntityDto)} is {@code true},
+   * otherwise throws a {@link org.sonar.server.exceptions.ResourceForbiddenException}.
+   * <p>
+   * Differs from {@link #checkEntityPermission(String, EntityDto)} by throwing a different exception (ensuring no resource listing is possible).
+   */
+  UserSession checkEntityPermissionOrElseThrowResourceForbiddenException(String projectPermission, EntityDto entity);
+
   /**
    * Ensures that {@link #hasChildProjectsPermission(String, ComponentDto)} is {@code true}
    * otherwise throws a {@link org.sonar.server.exceptions.ForbiddenException}.
index 9889aa18c042b055e86e29e5c508991ecbd83525..53c0269875c109da4d7ad0084fa00da69a3bd580 100644 (file)
@@ -24,10 +24,12 @@ import org.junit.Before;
 import org.junit.Test;
 import org.sonar.api.resources.Qualifiers;
 import org.sonar.db.component.ComponentDto;
+import org.sonar.db.entity.EntityDto;
 import org.sonar.db.project.ProjectDto;
 import org.sonar.db.user.GroupDto;
 import org.sonar.db.user.GroupTesting;
 import org.sonar.server.exceptions.ForbiddenException;
+import org.sonar.server.exceptions.ResourceForbiddenException;
 import org.sonar.server.exceptions.UnauthorizedException;
 import org.sonar.server.tester.AnonymousMockUserSession;
 import org.sonar.server.tester.MockUserSession;
@@ -97,6 +99,30 @@ public class ThreadLocalUserSessionTest {
       .isInstanceOf(UnauthorizedException.class);
   }
 
+  @Test
+  public void checkEntityPermissionOrElseThrowResourceForbiddenException_returns_session_when_permission_to_entity() {
+    MockUserSession expected = new MockUserSession("jean-michel");
+
+    ProjectDto subProjectDto = new ProjectDto().setQualifier(Qualifiers.PROJECT).setUuid("subproject-uuid");
+    ProjectDto applicationAsProjectDto = new ProjectDto().setQualifier(Qualifiers.APP).setUuid("application-project-uuid");
+
+    expected.registerProjects(subProjectDto);
+    expected.registerApplication(applicationAsProjectDto, subProjectDto);
+    threadLocalUserSession.set(expected);
+
+    assertThat(threadLocalUserSession.checkEntityPermissionOrElseThrowResourceForbiddenException(USER, applicationAsProjectDto)).isEqualTo(threadLocalUserSession);
+  }
+
+  @Test
+  public void checkEntityPermissionOrElseThrowResourceForbiddenException_throws_ResourceForbiddenException_when_no_permission_to_entity() {
+    MockUserSession expected = new MockUserSession("jean-michel");
+    threadLocalUserSession.set(expected);
+    EntityDto entity = new ProjectDto();
+
+    assertThatThrownBy(() -> threadLocalUserSession.checkEntityPermissionOrElseThrowResourceForbiddenException(USER, entity))
+      .isInstanceOf(ResourceForbiddenException.class);
+  }
+
   @Test
   public void throw_ForbiddenException_when_no_access_to_applications_projects() {
     GroupDto group = GroupTesting.newGroupDto();
index 586e1459a82079862fa1870fb775cece83b84859..942b8c34104a0398b428fad88ad973e6727cb586 100644 (file)
@@ -394,6 +394,12 @@ public class UserSessionRule implements TestRule, UserSession, BeforeTestExecuti
     return this;
   }
 
+  @Override
+  public UserSession checkEntityPermissionOrElseThrowResourceForbiddenException(String projectPermission, EntityDto entity) {
+    currentUserSession.checkEntityPermissionOrElseThrowResourceForbiddenException(projectPermission, entity);
+    return this;
+  }
+
   @Override
   public UserSession checkChildProjectsPermission(String projectPermission, ComponentDto component) {
     currentUserSession.checkChildProjectsPermission(projectPermission, component);
index a98ca3653988df615c84e5f3ca6ff75f166b347f..354c80b748f2a2311bad67d78cd89543daa46bfa 100644 (file)
@@ -28,8 +28,8 @@ import org.sonar.server.common.management.ManagedInstanceChecker;
 import org.sonar.server.user.UserSession;
 import org.sonar.server.v2.api.membership.request.GroupMembershipCreateRestRequest;
 import org.sonar.server.v2.api.membership.request.GroupsMembershipSearchRestRequest;
-import org.sonar.server.v2.api.membership.response.GroupsMembershipSearchRestResponse;
 import org.sonar.server.v2.api.membership.response.GroupMembershipRestResponse;
+import org.sonar.server.v2.api.membership.response.GroupsMembershipSearchRestResponse;
 import org.sonar.server.v2.api.model.RestPage;
 import org.sonar.server.v2.api.response.PageRestResponse;
 
@@ -50,10 +50,9 @@ public class DefaultGroupMembershipController implements GroupMembershipControll
     userSession.checkLoggedIn().checkIsSystemAdministrator();
     SearchResults<UserGroupDto> groupMembershipSearchResults = searchMembership(groupsSearchRestRequest, restPage);
 
-    List<GroupMembershipRestResponse> groupMembershipRestRespons = toRestGroupMembershipResponse(groupMembershipSearchResults);
-    return new GroupsMembershipSearchRestResponse(groupMembershipRestRespons,
-      new PageRestResponse(restPage.pageIndex(), restPage.pageSize(), groupMembershipSearchResults.total())
-    );
+    List<GroupMembershipRestResponse> groupMembershipRestResponse = toRestGroupMembershipResponse(groupMembershipSearchResults);
+    return new GroupsMembershipSearchRestResponse(groupMembershipRestResponse,
+      new PageRestResponse(restPage.pageIndex(), restPage.pageSize(), groupMembershipSearchResults.total()));
   }
 
   private SearchResults<UserGroupDto> searchMembership(GroupsMembershipSearchRestRequest groupsSearchRestRequest, RestPage restPage) {
index cef1c2ab5746cc560a18c3bd14b77b38803e5dc1..e5f83cdca9c2f633c42c3fd32b8ca7ce472b9153 100644 (file)
@@ -27,7 +27,7 @@ import org.sonar.server.common.SearchResults;
 import org.sonar.server.common.projectbindings.service.ProjectBindingInformation;
 import org.sonar.server.common.projectbindings.service.ProjectBindingsSearchRequest;
 import org.sonar.server.common.projectbindings.service.ProjectBindingsService;
-import org.sonar.server.exceptions.NotFoundException;
+import org.sonar.server.exceptions.ResourceForbiddenException;
 import org.sonar.server.user.UserSession;
 import org.sonar.server.v2.api.model.RestPage;
 import org.sonar.server.v2.api.projectbindings.model.ProjectBinding;
@@ -54,10 +54,10 @@ public class DefaultProjectBindingsController implements ProjectBindingsControll
     if (projectAlmSettingDto.isPresent()) {
       ProjectDto projectDto = projectBindingsService.findProjectFromBinding(projectAlmSettingDto.get())
         .orElseThrow(() -> new IllegalStateException(String.format("Project (uuid '%s') not found for binding '%s'", projectAlmSettingDto.get().getProjectUuid(), id)));
-      userSession.checkEntityPermission(USER, projectDto);
+      userSession.checkEntityPermissionOrElseThrowResourceForbiddenException(USER, projectDto);
       return toProjectBinding(projectDto, projectAlmSettingDto.get());
     } else {
-      throw new NotFoundException(String.format("Project binding '%s' not found", id));
+      throw new ResourceForbiddenException();
     }
   }
 
index 24216f7dced851e2f665c942f38f1131c64ae2c6..2c8aa1fe4c38d966977ca97b7ac40f0f67b072e4 100644 (file)
@@ -68,7 +68,7 @@ class DefaultProjectBindingsControllerTest {
         status().isNotFound(),
         content().json("""
           {
-            "message": "Project binding 'uuid' not found"
+            "message": "You do not have access to this resource or it does not exist."
           }
           """));
   }
@@ -93,7 +93,7 @@ class DefaultProjectBindingsControllerTest {
   }
 
   @Test
-  void getProjectBinding_whenUserDoesntHaveProjectAdminPermissions_returnsForbidden() throws Exception {
+  void getProjectBinding_whenUserDoesntHaveProjectAdminPermissions_returnsNotFound() throws Exception {
     userSession.logIn();
     ProjectAlmSettingDto projectAlmSettingDto = mock();
     when(projectBindingsService.findProjectBindingByUuid(UUID)).thenReturn(Optional.of(projectAlmSettingDto));
@@ -102,10 +102,10 @@ class DefaultProjectBindingsControllerTest {
     mockMvc
       .perform(get(PROJECT_BINDINGS_ENDPOINT + "/uuid"))
       .andExpectAll(
-        status().isForbidden(),
+        status().isNotFound(),
         content().json("""
           {
-            "message": "Insufficient privileges"
+            "message": "You do not have access to this resource or it does not exist."
           }
           """));
   }