]> source.dussan.org Git - sonarqube.git/commitdiff
Add method UserSession#keepAuthorizedComponents()
authorSimon Brandhof <simon.brandhof@sonarsource.com>
Wed, 10 May 2017 14:00:35 +0000 (16:00 +0200)
committerSimon Brandhof <simon.brandhof@sonarsource.com>
Wed, 10 May 2017 21:08:31 +0000 (23:08 +0200)
to be able to fix authorization performance hotspot

14 files changed:
server/sonar-ce/src/main/java/org/sonar/ce/user/CeUserSession.java
server/sonar-ce/src/test/java/org/sonar/ce/user/CeUserSessionTest.java
server/sonar-db-dao/src/main/java/org/sonar/db/permission/AuthorizationDao.java
server/sonar-db-dao/src/main/java/org/sonar/db/permission/AuthorizationMapper.java
server/sonar-db-dao/src/main/resources/org/sonar/db/permission/AuthorizationMapper.xml
server/sonar-db-dao/src/test/java/org/sonar/db/component/ComponentTesting.java
server/sonar-db-dao/src/test/java/org/sonar/db/permission/AuthorizationDaoTest.java
server/sonar-server/src/main/java/org/sonar/server/user/AbstractUserSession.java
server/sonar-server/src/main/java/org/sonar/server/user/DoPrivileged.java
server/sonar-server/src/main/java/org/sonar/server/user/ServerUserSession.java
server/sonar-server/src/main/java/org/sonar/server/user/ThreadLocalUserSession.java
server/sonar-server/src/main/java/org/sonar/server/user/UserSession.java
server/sonar-server/src/test/java/org/sonar/server/tester/UserSessionRule.java
server/sonar-server/src/test/java/org/sonar/server/user/ServerUserSessionTest.java

index 77c78ce31c9fd77bac8d2bcd434c552bce38cbfa..36d4c8c29289b6f0e4d63e57a4fa32689c93463f 100644 (file)
@@ -20,6 +20,7 @@
 package org.sonar.ce.user;
 
 import java.util.Collection;
+import java.util.List;
 import org.sonar.db.component.ComponentDto;
 import org.sonar.db.organization.OrganizationDto;
 import org.sonar.db.user.GroupDto;
@@ -121,6 +122,11 @@ public class CeUserSession implements UserSession {
     throw notImplemented();
   }
 
+  @Override
+  public List<ComponentDto> keepAuthorizedComponents(String permission, Collection<ComponentDto> components) {
+    throw notImplemented();
+  }
+
   private static RuntimeException notImplemented() {
     throw new UnsupportedOperationException(UOE_MESSAGE);
   }
index fc9063f5ccac028a66b04d951d7da35867bac1f2..ed75e2a1e257edcbd36e9f29f9303fbb2eee7d8f 100644 (file)
  */
 package org.sonar.ce.user;
 
-import com.google.common.base.Predicate;
 import com.tngtech.java.junit.dataprovider.DataProvider;
 import com.tngtech.java.junit.dataprovider.DataProviderRunner;
 import com.tngtech.java.junit.dataprovider.UseDataProvider;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.lang.reflect.Modifier;
+import java.util.Arrays;
 import java.util.List;
+import java.util.stream.Collectors;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
 import org.junit.runner.RunWith;
 
-import static com.google.common.collect.FluentIterable.from;
-import static java.util.Arrays.asList;
 import static org.sonar.test.ExceptionCauseMatcher.hasType;
 
 @RunWith(DataProviderRunner.class)
@@ -45,8 +44,9 @@ public class CeUserSessionTest {
 
   @DataProvider
   public static Object[][] ceUserSessionPublicMethods() {
-    List<Method> declaredMethods = from(asList(CeUserSession.class.getDeclaredMethods()))
-      .filter(PublicMethodPredicate.INSTANCE).toList();
+    List<Method> declaredMethods = Arrays.stream(CeUserSession.class.getDeclaredMethods())
+      .filter(m -> Modifier.isPublic(m.getModifiers()))
+      .collect(Collectors.toList());
     Object[][] res = new Object[declaredMethods.size()][1];
     int i = 0;
     for (Method declaredMethod : declaredMethods) {
@@ -82,16 +82,6 @@ public class CeUserSessionTest {
     expectedException.expect(InvocationTargetException.class);
     expectedException.expectCause(
       hasType(UnsupportedOperationException.class)
-        .andMessage("UserSession must not be used from within the Compute Engine")
-      );
-  }
-
-  private enum PublicMethodPredicate implements Predicate<Method> {
-    INSTANCE;
-
-    @Override
-    public boolean apply(Method input) {
-      return Modifier.isPublic(input.getModifiers());
-    }
+        .andMessage("UserSession must not be used from within the Compute Engine"));
   }
 }
index b204593a0e98d65ef55e130a34c39ff9e243f7e5..d55f13ce0e7769b2d74f0b41a634a83d201389ea 100644 (file)
@@ -135,6 +135,17 @@ public class AuthorizationDao implements Dao {
       });
   }
 
+  public Set<String> keepAuthorizedProjectUuids(DbSession dbSession, Collection<String> projectUuids, @Nullable Integer userId, String permission) {
+    return executeLargeInputsIntoSet(
+      projectUuids,
+      partition -> {
+        if (userId == null) {
+          return mapper(dbSession).keepAuthorizedProjectUuidsForAnonymous(permission, partition);
+        }
+        return mapper(dbSession).keepAuthorizedProjectUuidsForUser(userId, permission, partition);
+      });
+  }
+
   /**
    * Keep only authorized user that have the given permission on a given project.
    * Please Note that if the permission is 'Anyone' is NOT taking into account by this method.
index 8ca11da74207dd117f38abc1a85ab32258e9c1cd..e60588cf1bd76fe38d78e98cdcf81ecd9c2b899c 100644 (file)
@@ -53,6 +53,10 @@ public interface AuthorizationMapper {
 
   List<Integer> keepAuthorizedUsersForRoleAndProject(@Param("role") String role, @Param("componentId") long componentId, @Param("userIds") List<Integer> userIds);
 
+  Set<String> keepAuthorizedProjectUuidsForUser(@Param("userId") int userId, @Param("permission") String permission, @Param("projectUuids") Collection<String> projectUuids);
+
+  Set<String> keepAuthorizedProjectUuidsForAnonymous(@Param("permission") String permission, @Param("projectUuids") Collection<String> projectUuids);
+
   Set<String> selectProjectPermissions(@Param("projectUuid") String projectUuid, @Param("userId") long userId);
 
   Set<String> selectProjectPermissionsOfAnonymous(@Param("projectUuid") String projectUuid);
index 8bb72b6a63d48028db9f572dad9acd726e156dcf..181292874bbbf5e3b0609a5a0de6acd8f6a6d8b8 100644 (file)
 
   <sql id="sqlSelectPublicProjectsIfRole">
     select
-      p.id
+    p.id
     from
-      projects p
+    projects p
     where
-      <foreach collection="componentIds" open="(" close=")" item="element" index="index" separator=" or ">
-        p.id=#{element,jdbcType=BIGINT}
-      </foreach>
-      and p.private = ${_false}
-      and #{role,jdbcType=VARCHAR} in ('user','codeviewer')
+    <foreach collection="componentIds" open="(" close=")" item="element" index="index" separator=" or ">
+      p.id=#{element,jdbcType=BIGINT}
+    </foreach>
+    and p.private = ${_false}
+    and #{role,jdbcType=VARCHAR} in ('user','codeviewer')
   </sql>
 
+  <select id="keepAuthorizedProjectUuidsForUser" parameterType="map" resultType="String">
+    select p.uuid
+    from projects p
+    inner join group_roles gr on p.id = gr.resource_id
+    where
+      gr.role = #{permission,jdbcType=VARCHAR}
+      and (gr.group_id is null or exists (
+        select 1 from groups_users gu
+        where
+          gu.user_id = #{userId, jdbcType=INTEGER}
+          and gr.group_id = gu.group_id)
+      )
+      and p.uuid in <foreach collection="projectUuids" open="(" close=")" item="projectUuid" index="index" separator=",">#{projectUuid,jdbcType=VARCHAR}</foreach>
+
+    union
+
+    select p.uuid
+    from projects p
+    inner join user_roles ur on p.id = ur.resource_id
+    where
+      ur.role=#{permission,jdbcType=VARCHAR}
+      and ur.user_id=#{userId,jdbcType=INTEGER}
+      and p.uuid in <foreach collection="projectUuids" open="(" close=")" item="projectUuid" index="index" separator=",">#{projectUuid,jdbcType=VARCHAR}</foreach>
+
+    <if test="permission == 'user' or permission == 'codeviewer'">
+      union
+
+      select p.uuid
+      from projects p
+      where
+        p.uuid in <foreach collection="projectUuids" open="(" close=")" item="projectUuid" index="index" separator=",">#{projectUuid,jdbcType=VARCHAR}</foreach>
+        and p.private = ${_false}
+    </if>
+  </select>
+
+  <select id="keepAuthorizedProjectUuidsForAnonymous" parameterType="map" resultType="String">
+    select p.uuid
+    from projects p
+    inner join group_roles gr on p.id = gr.resource_id
+    where
+      gr.role=#{permission,jdbcType=VARCHAR}
+      and gr.group_id is null
+      and p.uuid in <foreach collection="projectUuids" open="(" close=")" item="projectUuid" index="index" separator=",">#{projectUuid,jdbcType=VARCHAR}</foreach>
+
+    <if test="permission == 'user' or permission == 'codeviewer'">
+      union
+
+      select p.uuid
+      from projects p
+      where
+      p.uuid in <foreach collection="projectUuids" open="(" close=")" item="projectUuid" index="index" separator=",">#{projectUuid,jdbcType=VARCHAR}</foreach>
+      and p.private = ${_false}
+    </if>
+  </select>
+
   <select id="keepAuthorizedUsersForRoleAndProject" parameterType="map" resultType="int">
     select
       gu.user_id
index dc8a2130845a8634a1106f8f2194d38a0ee1ff53..f15288642bc0c1464f6bff74341e43a74484117e 100644 (file)
@@ -130,7 +130,7 @@ public class ComponentTesting {
       .setPath(null)
       .setLanguage(null)
       .setEnabled(true)
-        .setPrivate(isPrivate);
+      .setPrivate(isPrivate);
   }
 
   public static ComponentDto newView(OrganizationDto organizationDto) {
index d7fffb1e3cac754ad41b81019ac35b758e956b75..174199c28f276dc12eed9ddc274abbd1c6875105 100644 (file)
@@ -855,4 +855,79 @@ public class AuthorizationDaoTest {
 
     assertThat(underTest.selectProjectPermissions(dbSession, project.uuid(), user.getId())).containsOnly("p1", "p2", "p3");
   }
+
+  @Test
+  public void keepAuthorizedProjectUuids_filters_projects_authorized_to_logged_in_user_by_direct_permission() {
+    ComponentDto privateProject = db.components().insertPrivateProject(organization);
+    ComponentDto publicProject = db.components().insertPublicProject(organization);
+    UserDto user = db.users().insertUser();
+    db.users().insertProjectPermissionOnUser(user, UserRole.ADMIN, privateProject);
+
+    assertThat(underTest.keepAuthorizedProjectUuids(dbSession, newHashSet(privateProject.uuid(), publicProject.uuid()), user.getId(), UserRole.ADMIN))
+      .containsOnly(privateProject.uuid());
+    // user does not have the permission "issueadmin"
+    assertThat(underTest.keepAuthorizedProjectUuids(dbSession, newHashSet(privateProject.uuid(), publicProject.uuid()), user.getId(), UserRole.ISSUE_ADMIN))
+      .isEmpty();
+  }
+
+  @Test
+  public void keepAuthorizedProjectUuids_filters_projects_authorized_to_logged_in_user_by_group_permission() {
+    ComponentDto privateProject = db.components().insertPrivateProject(organization);
+    ComponentDto publicProject = db.components().insertPublicProject(organization);
+    UserDto user = db.users().insertUser();
+    GroupDto group = db.users().insertGroup(organization);
+    db.users().insertMember(group, user);
+    db.users().insertProjectPermissionOnGroup(group, UserRole.ADMIN, privateProject);
+
+    assertThat(underTest.keepAuthorizedProjectUuids(dbSession, newHashSet(privateProject.uuid(), publicProject.uuid()), user.getId(), UserRole.ADMIN))
+      .containsOnly(privateProject.uuid());
+    // user does not have the permission "issueadmin"
+    assertThat(underTest.keepAuthorizedProjectUuids(dbSession, newHashSet(privateProject.uuid(), publicProject.uuid()), user.getId(), UserRole.ISSUE_ADMIN))
+      .isEmpty();
+  }
+
+  @Test
+  public void keepAuthorizedProjectUuids_returns_empty_list_if_input_is_empty() {
+    ComponentDto publicProject = db.components().insertPublicProject(organization);
+    UserDto user = db.users().insertUser();
+
+    assertThat(underTest.keepAuthorizedProjectUuids(dbSession, Collections.emptySet(), user.getId(), UserRole.USER))
+      .isEmpty();
+
+    // projects do not exist
+    assertThat(underTest.keepAuthorizedProjectUuids(dbSession, newHashSet("does_not_exist"), user.getId(), UserRole.USER))
+      .isEmpty();
+  }
+
+  @Test
+  public void keepAuthorizedProjectUuids_returns_empty_list_if_input_does_not_reference_existing_projects() {
+    ComponentDto publicProject = db.components().insertPublicProject(organization);
+    UserDto user = db.users().insertUser();
+
+    assertThat(underTest.keepAuthorizedProjectUuids(dbSession, newHashSet("does_not_exist"), user.getId(), UserRole.USER))
+      .isEmpty();
+  }
+
+  @Test
+  public void keepAuthorizedProjectUuids_returns_public_projects_if_permission_USER_or_CODEVIEWER() {
+    ComponentDto publicProject = db.components().insertPublicProject(organization);
+    UserDto user = db.users().insertUser();
+
+    // logged-in user
+    assertThat(underTest.keepAuthorizedProjectUuids(dbSession, newHashSet(publicProject.uuid()), user.getId(), UserRole.CODEVIEWER))
+      .containsOnly(publicProject.uuid());
+    assertThat(underTest.keepAuthorizedProjectUuids(dbSession, newHashSet(publicProject.uuid()), user.getId(), UserRole.USER))
+      .containsOnly(publicProject.uuid());
+    assertThat(underTest.keepAuthorizedProjectUuids(dbSession, newHashSet(publicProject.uuid()), user.getId(), UserRole.ADMIN))
+      .isEmpty();
+
+    // anonymous
+    assertThat(underTest.keepAuthorizedProjectUuids(dbSession, newHashSet(publicProject.uuid()), null, UserRole.CODEVIEWER))
+      .containsOnly(publicProject.uuid());
+    assertThat(underTest.keepAuthorizedProjectUuids(dbSession, newHashSet(publicProject.uuid()), null, UserRole.USER))
+      .containsOnly(publicProject.uuid());
+    assertThat(underTest.keepAuthorizedProjectUuids(dbSession, newHashSet(publicProject.uuid()), null, UserRole.ADMIN))
+      .isEmpty();
+
+  }
 }
index 7499f1b519c2a968033577355ba07043a020efd3..db7784ff6f72872f41c5368a25947a4affae7449 100644 (file)
  */
 package org.sonar.server.user;
 
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
 import java.util.Optional;
+import org.sonar.core.permission.ProjectPermissions;
+import org.sonar.core.util.stream.MoreCollectors;
 import org.sonar.db.component.ComponentDto;
 import org.sonar.db.organization.OrganizationDto;
 import org.sonar.server.exceptions.ForbiddenException;
@@ -104,6 +109,24 @@ public abstract class AbstractUserSession implements UserSession {
     return INSUFFICIENT_PRIVILEGES_EXCEPTION;
   }
 
+  @Override
+  public final List<ComponentDto> keepAuthorizedComponents(String permission, Collection<ComponentDto> components) {
+    if (isRoot()) {
+      return new ArrayList<>(components);
+    }
+    return doKeepAuthorizedComponents(permission, components);
+  }
+
+  /**
+   * Naive implementation, to be overridden if needed
+   */
+  protected List<ComponentDto> doKeepAuthorizedComponents(String permission, Collection<ComponentDto> components) {
+    boolean allowPublicComponent = ProjectPermissions.PUBLIC_PERMISSIONS.contains(permission);
+    return components.stream()
+      .filter(c -> (allowPublicComponent && !c.isPrivate()) || hasProjectUuidPermission(permission, c.projectUuid()))
+      .collect(MoreCollectors.toList());
+  }
+
   @Override
   public final UserSession checkIsSystemAdministrator() {
     if (!isSystemAdministrator()) {
index e024720bf918973f939effdb031197e34a88ee9b..bf3820d1058c4eb753cc8ea3f28f60979e9b18ea 100644 (file)
@@ -22,8 +22,8 @@ package org.sonar.server.user;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Optional;
-import org.sonar.db.user.GroupDto;
 import org.sonar.db.permission.OrganizationPermission;
+import org.sonar.db.user.GroupDto;
 
 /**
  * Allow code to be executed with the highest privileges possible, as if executed by a {@link OrganizationPermission#ADMINISTER} account.
@@ -111,7 +111,7 @@ public final class DoPrivileged {
       protected boolean hasProjectUuidPermission(String permission, String projectUuid) {
         return true;
       }
-
+      
       @Override
       public boolean isSystemAdministrator() {
         return true;
index 285fd96a48ad639bba9ed8bf2295afa89a053421..7f0fa79d67277b04a2900ed4e6d1c8ae0ffcbb4d 100644 (file)
@@ -25,6 +25,7 @@ import com.google.common.collect.ImmutableSet;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
@@ -180,6 +181,20 @@ public class ServerUserSession extends AbstractUserSession {
     return dbClient.authorizationDao().selectProjectPermissionsOfAnonymous(dbSession, projectUuid);
   }
 
+  @Override
+  protected List<ComponentDto> doKeepAuthorizedComponents(String permission, Collection<ComponentDto> components) {
+    try (DbSession dbSession = dbClient.openSession(false)) {
+      Set<String> projectUuids = components.stream()
+        .map(ComponentDto::projectUuid)
+        .collect(MoreCollectors.toSet(components.size()));
+      Set<String> authorizedProjectUuids = dbClient.authorizationDao().keepAuthorizedProjectUuids(dbSession, projectUuids, getUserId(), permission);
+
+      return components.stream()
+        .filter(c -> authorizedProjectUuids.contains(c.projectUuid()))
+        .collect(MoreCollectors.toList(components.size()));
+    }
+  }
+
   @Override
   public boolean isSystemAdministrator() {
     return isSystemAdministratorSupplier.get();
index 485b32e904eea4a548018d1104bc1c90cdca624f..25069b05ebef5f8ad3049ede63d4851c002223dc 100644 (file)
@@ -20,6 +20,7 @@
 package org.sonar.server.user;
 
 import java.util.Collection;
+import java.util.List;
 import javax.annotation.CheckForNull;
 import org.sonar.db.component.ComponentDto;
 import org.sonar.db.organization.OrganizationDto;
@@ -147,4 +148,9 @@ public class ThreadLocalUserSession implements UserSession {
   public boolean hasPermission(OrganizationPermission permission, OrganizationDto organization) {
     return get().hasPermission(permission, organization);
   }
+
+  @Override
+  public List<ComponentDto> keepAuthorizedComponents(String permission, Collection<ComponentDto> components) {
+    return get().keepAuthorizedComponents(permission, components);
+  }
 }
index 06aa9ca0004176a49753554e22d3e2f824cae688..23b16f560a2bb3d860f29cba74f7d1aa893cf46a 100644 (file)
 package org.sonar.server.user;
 
 import java.util.Collection;
+import java.util.List;
 import javax.annotation.CheckForNull;
 import org.sonar.db.component.ComponentDto;
 import org.sonar.db.organization.OrganizationDto;
-import org.sonar.db.user.GroupDto;
 import org.sonar.db.permission.OrganizationPermission;
+import org.sonar.db.user.GroupDto;
 
 public interface UserSession {
 
@@ -117,6 +118,15 @@ public interface UserSession {
   @Deprecated
   boolean hasComponentUuidPermission(String permission, String componentUuid);
 
+  /**
+   * Return the subset of specified components which the user has granted permission.
+   * An empty list is returned if input is empty or if no components are allowed to be
+   * accessed.
+   * If the input is ordered, then the returned components are in the same order.
+   * The duplicated components are returned duplicated too.
+   */
+  List<ComponentDto> keepAuthorizedComponents(String permission, Collection<ComponentDto> components);
+
   /**
    * Ensures that {@link #hasComponentPermission(String, ComponentDto)} is {@code true},
    * otherwise throws a {@link org.sonar.server.exceptions.ForbiddenException}.
index d048bb536cf02a43eadb411abdd07e8683db9260..8383654fb06f9e61eb9b6a551bcd363af6c25bd5 100644 (file)
@@ -21,6 +21,7 @@ package org.sonar.server.tester;
 
 import com.google.common.base.Preconditions;
 import java.util.Collection;
+import java.util.List;
 import javax.annotation.CheckForNull;
 import javax.annotation.Nullable;
 import org.junit.rules.TestRule;
@@ -253,6 +254,11 @@ public class UserSessionRule implements TestRule, UserSession {
     return currentUserSession.hasComponentUuidPermission(permission, componentUuid);
   }
 
+  @Override
+  public List<ComponentDto> keepAuthorizedComponents(String permission, Collection<ComponentDto> components) {
+    return currentUserSession.keepAuthorizedComponents(permission, components);
+  }
+
   @Override
   @CheckForNull
   public String getLogin() {
index 73531b048d353e7db19f109158a7db5d76a15d23..f8a2f50108bca32728cd0397a22bd392cd1f11d4 100644 (file)
@@ -19,6 +19,7 @@
  */
 package org.sonar.server.user;
 
+import java.util.Arrays;
 import java.util.Random;
 import javax.annotation.Nullable;
 import org.junit.Before;
@@ -392,6 +393,40 @@ public class ServerUserSessionTest {
     return new Random().nextBoolean() ? underTest.hasComponentPermission(permission, component) : underTest.hasComponentUuidPermission(permission, component.uuid());
   }
 
+  @Test
+  public void keepAuthorizedComponents_returns_empty_list_if_no_permissions_are_granted() {
+    UserSession underTest = newAnonymousSession();
+
+    assertThat(underTest.keepAuthorizedComponents(UserRole.ADMIN, Arrays.asList(privateProject, publicProject))).isEmpty();
+  }
+
+  @Test
+  public void keepAuthorizedComponents_filters_components_with_granted_permissions_for_logged_in_user() {
+    UserSession underTest = newUserSession(user);
+    db.users().insertProjectPermissionOnUser(user, UserRole.ADMIN, privateProject);
+
+    assertThat(underTest.keepAuthorizedComponents(UserRole.ISSUE_ADMIN, Arrays.asList(privateProject, publicProject))).isEmpty();
+    assertThat(underTest.keepAuthorizedComponents(UserRole.ADMIN, Arrays.asList(privateProject, publicProject))).containsExactly(privateProject);
+  }
+
+  @Test
+  public void keepAuthorizedComponents_filters_components_with_granted_permissions_for_anonymous() {
+    UserSession underTest = newAnonymousSession();
+    db.users().insertProjectPermissionOnAnyone(UserRole.ISSUE_ADMIN, publicProject);
+
+    assertThat(underTest.keepAuthorizedComponents(UserRole.ADMIN, Arrays.asList(privateProject, publicProject))).isEmpty();
+    assertThat(underTest.keepAuthorizedComponents(UserRole.ISSUE_ADMIN, Arrays.asList(privateProject, publicProject))).containsExactly(publicProject);
+  }
+
+  @Test
+  public void keepAuthorizedComponents_returns_all_specified_components_if_root() {
+    user = db.users().makeRoot(user);
+    UserSession underTest = newUserSession(user);
+
+    assertThat(underTest.keepAuthorizedComponents(UserRole.ADMIN, Arrays.asList(privateProject, publicProject)))
+      .containsExactly(privateProject, publicProject);
+  }
+
   @Test
   public void isSystemAdministrator_returns_true_if_org_feature_is_enabled_and_user_is_root() {
     organizationFlags.setEnabled(true);