]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-15985 implemented security checks before pushing a message to SL clients
authorLukasz Jarocki <lukasz.jarocki@sonarsource.com>
Thu, 10 Feb 2022 06:48:40 +0000 (07:48 +0100)
committersonartech <sonartech@sonarsource.com>
Fri, 18 Feb 2022 15:48:04 +0000 (15:48 +0000)
24 files changed:
server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/SafeModeUserSession.java
server/sonar-webserver-auth/src/main/java/org/sonar/server/user/DoPrivileged.java
server/sonar-webserver-auth/src/main/java/org/sonar/server/user/ServerUserSession.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/authentication/SafeModeUserSessionTest.java
server/sonar-webserver-auth/src/test/java/org/sonar/server/user/DoPrivilegedTest.java
server/sonar-webserver-auth/src/test/java/org/sonar/server/user/ServerUserSessionTest.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/AnonymousMockUserSession.java
server/sonar-webserver-auth/src/testFixtures/java/org/sonar/server/tester/MockUserSession.java
server/sonar-webserver-auth/src/testFixtures/java/org/sonar/server/tester/UserSessionRule.java
server/sonar-webserver-auth/src/testFixtures/java/org/sonar/server/user/TestUserSessionFactory.java
server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/ServerPushClient.java
server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/ServerPushWsModule.java
server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/sonarlint/SonarLintClient.java
server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/sonarlint/SonarLintClientPermissionsValidator.java [new file with mode: 0644]
server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/sonarlint/SonarLintClientsRegistry.java
server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/sonarlint/SonarLintPushAction.java
server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/ServerPushClientTest.java
server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/sonarlint/SonarLintClientPermissionsValidatorTest.java [new file with mode: 0644]
server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/sonarlint/SonarLintClientTest.java
server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/sonarlint/SonarLintClientsRegistryTest.java
server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/sonarlint/SonarLintPushActionTest.java

index 78601b048788a87a8290f6a651598d26480d71c0..6864bce84244a1cf06708f593f7edc4e1ce90a8c 100644 (file)
@@ -108,4 +108,9 @@ public class SafeModeUserSession extends AbstractUserSession {
   public boolean isSystemAdministrator() {
     return false;
   }
+
+  @Override
+  public boolean isActive() {
+    return false;
+  }
 }
index 0e4e8a70affec79e6a57c58158cc961445c64d92..0ac01236152c4f6ad05a2bcebd65f469b4490864 100644 (file)
@@ -142,6 +142,11 @@ public final class DoPrivileged {
         return true;
       }
 
+      @Override
+      public boolean isActive() {
+        return true;
+      }
+
     }
 
     private void start() {
index 9d51a52211c4cf56cbe96748de9ab2e26613d076..b8d9dfa21b5ea786b786010c0de792726785f378 100644 (file)
@@ -342,6 +342,11 @@ public class ServerUserSession extends AbstractUserSession {
     return isSystemAdministrator;
   }
 
+  @Override
+  public boolean isActive() {
+    return userDto.isActive();
+  }
+
   private boolean loadIsSystemAdministrator() {
     if (isRoot()) {
       return true;
index 1ae45a0fa908943bd30f7a8908d5569c3557d249..1d8866c1b1688efbe5d63de2543a56128aba67f2 100644 (file)
@@ -173,6 +173,11 @@ public class ThreadLocalUserSession implements UserSession {
     return this;
   }
 
+  @Override
+  public boolean isActive() {
+    return get().isActive();
+  }
+
   @Override
   public boolean hasComponentPermission(String permission, ComponentDto component) {
     return get().hasComponentPermission(permission, component);
index 116dc6341c0a916509624390ca1d4f8b4d5ae493..f10db0d1dfc4977bee5acdb6fbffa8cedad1df0c 100644 (file)
@@ -274,4 +274,6 @@ public interface UserSession {
    * otherwise throws {@link org.sonar.server.exceptions.ForbiddenException}.
    */
   UserSession checkIsSystemAdministrator();
+
+  boolean isActive();
 }
index ad1cc032f001da57bc71e3b79df09c4ba46e6b30..0278a2eab3f5e625c91b898632a0e86143c8e2a1 100644 (file)
@@ -37,6 +37,7 @@ public class SafeModeUserSessionTest {
     assertThat(underTest.shouldResetPassword()).isFalse();
     assertThat(underTest.getName()).isNull();
     assertThat(underTest.getGroups()).isEmpty();
+    assertThat(underTest.isActive()).isFalse();
   }
 
   @Test
index 9275a3ce5ed8e7f9b6a60d0e1fc6b8b83b61cf82..b45fe1547d6b467706cde18861783abade44558a 100644 (file)
@@ -51,6 +51,7 @@ public class DoPrivilegedTest {
     assertThat(catcher.userSession.hasComponentPermission("any permission", new ComponentDto())).isTrue();
     assertThat(catcher.userSession.isSystemAdministrator()).isTrue();
     assertThat(catcher.userSession.shouldResetPassword()).isFalse();
+    assertThat(catcher.userSession.isActive()).isTrue();
     assertThat(catcher.userSession.hasChildProjectsPermission(USER, new ComponentDto())).isTrue();
     assertThat(catcher.userSession.hasPortfolioChildProjectsPermission(USER, new ComponentDto())).isTrue();
 
index c43ee7ee538710b32d7d46be71345687b579756f..c3ea59725e72a4eb1a0899c9ef60d5d64f56159a 100644 (file)
@@ -126,6 +126,17 @@ public class ServerUserSessionTest {
     assertThat(session.getGroups()).extracting(GroupDto::getUuid).containsOnly(group1.getUuid());
   }
 
+  @Test
+  public void isActive_redirectsValueFromUserDto() {
+    UserDto active = db.users().insertUser();
+    active.setActive(true);
+    assertThat(newUserSession(active).isActive()).isTrue();
+
+    UserDto notActive = db.users().insertUser();
+    notActive.setActive(false);
+    assertThat(newUserSession(notActive).isActive()).isFalse();
+  }
+
   @Test
   public void isRoot_is_false_is_flag_root_is_false_on_UserDto() {
     UserDto root = db.users().insertUser();
index c831d7bfe24f0cb016d4a06bcfeda8a13f6eb316..4f8a5e463c944e4660f7a4b85f9809d46dc89a2a 100644 (file)
@@ -69,6 +69,7 @@ public class ThreadLocalUserSessionTest {
     assertThat(threadLocalUserSession.getLogin()).isEqualTo("karadoc");
     assertThat(threadLocalUserSession.getUuid()).isEqualTo("karadoc-uuid");
     assertThat(threadLocalUserSession.isLoggedIn()).isTrue();
+    assertThat(threadLocalUserSession.isActive()).isTrue();
     assertThat(threadLocalUserSession.shouldResetPassword()).isTrue();
     assertThat(threadLocalUserSession.getGroups()).extracting(GroupDto::getUuid).containsOnly(group.getUuid());
     assertThat(threadLocalUserSession.hasChildProjectsPermission(USER, new ComponentDto())).isFalse();
index b1495fe6f22f3a8afd067e66f582daad9518be6a..704345cf6eb17afdff9ede03d7c2f74709cbdc00 100644 (file)
@@ -35,6 +35,11 @@ public class AnonymousMockUserSession extends AbstractMockUserSession<AnonymousM
     return false;
   }
 
+  @Override
+  public boolean isActive() {
+    return false;
+  }
+
   @Override
   public String getLogin() {
     return null;
index ecc6f9068440b7e65b75f6c36724c81f81a184cf..b199c30275aa1a5e86cbb39f335a6e5f6c315392 100644 (file)
@@ -86,6 +86,11 @@ public class MockUserSession extends AbstractMockUserSession<MockUserSession> {
     return root;
   }
 
+  @Override
+  public boolean isActive() {
+    return true;
+  }
+
   public void setRoot(boolean root) {
     this.root = root;
   }
index c50fdfbb4b009343713f6a62a1af169e1e9ea2c1..c8fc0b37f98492565256c120bb09d3fe241615a9 100644 (file)
@@ -412,4 +412,9 @@ public class UserSessionRule implements TestRule, UserSession {
     currentUserSession.checkIsSystemAdministrator();
     return this;
   }
+
+  @Override
+  public boolean isActive() {
+    return currentUserSession.isActive();
+  }
 }
index 892ed01a0d901c6fabb3ee0c46699f38427895c8..d09d9ea662d240b49b212a0d229e43f7865f7cb1 100644 (file)
@@ -134,6 +134,11 @@ public class TestUserSessionFactory implements UserSessionFactory {
       throw notImplemented();
     }
 
+    @Override
+    public boolean isActive() {
+      throw notImplemented();
+    }
+
     private static RuntimeException notImplemented() {
       return new UnsupportedOperationException("not implemented");
     }
index a726ee1197fed5e0a2129bcaa1e0e0cbd9211660..458b716a3aff939bf576aefab361e4bf44b6310f 100644 (file)
@@ -83,9 +83,13 @@ public abstract class ServerPushClient {
     close();
   }
 
-  private synchronized void close() {
+  public synchronized void close() {
     startedHeartbeat.cancel(false);
-    asyncContext.complete();
+    try {
+      asyncContext.complete();
+    } catch (IllegalStateException ex) {
+      LOG.trace("Push connection was already closed");
+    }
   }
 
   private ServletOutputStream output() throws IOException {
index c6f194b54148fc0c66af95f5ce8cff42dc8d7f31..0a48485466c46496e6a1caf9daa3c4c6eaa2915e 100644 (file)
@@ -20,6 +20,7 @@
 package org.sonar.server.pushapi;
 
 import org.sonar.core.platform.Module;
+import org.sonar.server.pushapi.sonarlint.SonarLintClientPermissionsValidator;
 import org.sonar.server.pushapi.sonarlint.SonarLintClientsRegistry;
 import org.sonar.server.pushapi.sonarlint.SonarLintPushAction;
 
@@ -29,7 +30,7 @@ public class ServerPushWsModule extends Module {
   protected void configureModule() {
     add(
       ServerPushWs.class,
-
+      SonarLintClientPermissionsValidator.class,
       SonarLintClientsRegistry.class,
       SonarLintPushAction.class);
   }
index 93512a974ecd36fc6472d10bda0b3963754b759d..3d0f5cff07f986b409abc967276ba15f6522b7b6 100644 (file)
@@ -33,10 +33,13 @@ public class SonarLintClient extends ServerPushClient {
   private final Set<String> languages;
   private final Set<String> projectKeys;
 
-  public SonarLintClient(AsyncContext asyncContext, Set<String> projectKeys, Set<String> languages) {
+  private final String userUuid;
+
+  public SonarLintClient(AsyncContext asyncContext, Set<String> projectKeys, Set<String> languages, String userUuid) {
     super(scheduledExecutorService, asyncContext);
     this.projectKeys = projectKeys;
     this.languages = languages;
+    this.userUuid = userUuid;
   }
 
   public Set<String> getLanguages() {
@@ -65,4 +68,8 @@ public class SonarLintClient extends ServerPushClient {
   public int hashCode() {
     return Objects.hash(languages, projectKeys);
   }
+
+  public String getUserUuid() {
+    return userUuid;
+  }
 }
diff --git a/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/sonarlint/SonarLintClientPermissionsValidator.java b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/sonarlint/SonarLintClientPermissionsValidator.java
new file mode 100644 (file)
index 0000000..d876533
--- /dev/null
@@ -0,0 +1,72 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.pushapi.sonarlint;
+
+import java.util.List;
+import java.util.Set;
+import org.sonar.api.server.ServerSide;
+import org.sonar.api.web.UserRole;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.project.ProjectDto;
+import org.sonar.db.user.UserDto;
+import org.sonar.server.exceptions.ForbiddenException;
+import org.sonar.server.user.UserSession;
+import org.sonar.server.user.UserSessionFactory;
+
+@ServerSide
+public class SonarLintClientPermissionsValidator {
+
+  private final DbClient dbClient;
+  private final UserSessionFactory userSessionFactory;
+
+  public SonarLintClientPermissionsValidator(DbClient dbClient, UserSessionFactory userSessionFactory) {
+    this.dbClient = dbClient;
+    this.userSessionFactory = userSessionFactory;
+  }
+
+  public void validateUserCanReceivePushEventForProjects(UserSession userSession, Set<String> projectKeys) {
+    List<ProjectDto> projectDtos;
+    try (DbSession dbSession = dbClient.openSession(false)) {
+      projectDtos = dbClient.projectDao().selectProjectsByKeys(dbSession, projectKeys);
+    }
+    validateUsersDeactivationStatus(userSession);
+    for (ProjectDto projectDto : projectDtos) {
+      userSession.checkProjectPermission(UserRole.USER, projectDto);
+    }
+  }
+
+  public void validateUserCanReceivePushEventForProjects(String userUUID, Set<String> projectKeys) {
+    UserDto userDto;
+    try (DbSession dbSession = dbClient.openSession(false)) {
+      userDto = dbClient.userDao().selectByUuid(dbSession, userUUID);
+    }
+    if (userDto == null) {
+      throw new ForbiddenException("User does not exist");
+    }
+    validateUserCanReceivePushEventForProjects(userSessionFactory.create(userDto), projectKeys);
+  }
+
+  private static void validateUsersDeactivationStatus(UserSession userSession) {
+    if (!userSession.isActive()) {
+      throw new ForbiddenException("User doesn't have rights to requested resource anymore.");
+    }
+  }
+}
index 51146f28510fdac96c3e58cd0f1525d8445e22fa..bcf88efcad5f8f981c1f019bb82d4389c388e2f9 100644 (file)
@@ -21,7 +21,9 @@ package org.sonar.server.pushapi.sonarlint;
 
 import java.io.IOException;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.function.Predicate;
 import javax.servlet.AsyncEvent;
@@ -36,6 +38,7 @@ import org.sonar.core.util.RuleActivationListener;
 import org.sonar.core.util.RuleChange;
 import org.sonar.core.util.RuleSetChangeEvent;
 import org.sonar.server.pushapi.qualityprofile.RuleActivatorEventsDistributor;
+import org.sonar.server.exceptions.ForbiddenException;
 
 import static java.util.Arrays.asList;
 
@@ -45,12 +48,13 @@ public class SonarLintClientsRegistry implements RuleActivationListener {
   private static final Logger LOG = Loggers.get(SonarLintClientsRegistry.class);
 
   private final RuleActivatorEventsDistributor ruleActivatorEventsDistributor;
+  private final SonarLintClientPermissionsValidator sonarLintClientPermissionsValidator;
 
-  public SonarLintClientsRegistry(RuleActivatorEventsDistributor ruleActivatorEventsDistributor) {
+  public SonarLintClientsRegistry(RuleActivatorEventsDistributor ruleActivatorEventsDistributor, SonarLintClientPermissionsValidator permissionsValidator) {
     this.ruleActivatorEventsDistributor = ruleActivatorEventsDistributor;
+    this.sonarLintClientPermissionsValidator = permissionsValidator;
   }
 
-
   private final List<SonarLintClient> clients = new CopyOnWriteArrayList<>();
 
   public void registerClient(SonarLintClient sonarLintClient) {
@@ -63,6 +67,7 @@ public class SonarLintClientsRegistry implements RuleActivationListener {
   }
 
   public void unregisterClient(SonarLintClient client) {
+    client.close();
     clients.remove(client);
     LOG.debug("Removing SonarLint client");
   }
@@ -79,15 +84,25 @@ public class SonarLintClientsRegistry implements RuleActivationListener {
 
   private static Predicate<SonarLintClient> getFilterForEvent(RuleSetChangeEvent ruleChangeEvent) {
     List<String> affectedProjects = asList(ruleChangeEvent.getProjects());
-    return f -> !Collections.disjoint(f.getClientProjectKeys(), affectedProjects) && f.getLanguages().contains(ruleChangeEvent.getLanguage());
+    return client -> {
+      Set<String> clientProjectKeys = client.getClientProjectKeys();
+      Set<String> languages = client.getLanguages();
+      return !Collections.disjoint(clientProjectKeys, affectedProjects) && languages.contains(ruleChangeEvent.getLanguage());
+    };
   }
 
-
-  public void broadcastMessage(RuleSetChangeEvent event, Predicate<SonarLintClient> projectsFilter) {
-    clients.stream().filter(projectsFilter).forEach(c -> {
+  public void broadcastMessage(RuleSetChangeEvent message, Predicate<SonarLintClient> filter) {
+    clients.stream().filter(filter).forEach(c -> {
+      Set<String> projectKeysInterestingForClient = new HashSet<>(c.getClientProjectKeys());
+      projectKeysInterestingForClient.retainAll(Set.of(message.getProjects()));
       try {
-        String jsonString = getJSONString(event);
+        sonarLintClientPermissionsValidator.validateUserCanReceivePushEventForProjects(c.getUserUuid(), projectKeysInterestingForClient);
+        RuleSetChangeEvent personalizedEvent = new RuleSetChangeEvent(projectKeysInterestingForClient.toArray(String[]::new), message.getActivatedRules(),
+          message.getDeactivatedRules());
+        String jsonString = getJSONString(personalizedEvent);
         c.writeAndFlush(jsonString);
+      } catch (ForbiddenException forbiddenException) {
+        unregisterClient(c);
       } catch (IOException e) {
         LOG.error("Unable to send message to a client: " + e.getMessage());
       }
@@ -158,7 +173,7 @@ public class SonarLintClientsRegistry implements RuleActivationListener {
 
     @Override
     public void onStartAsync(AsyncEvent event) {
-      //nothing to do on start
+      // nothing to do on start
     }
 
     @Override
index ca30b37f478281a6c469754ae264b834ab32bacf..6a63de5f20a0895427798be100dc752fcd2702b5 100644 (file)
@@ -27,7 +27,6 @@ import javax.servlet.http.HttpServletResponse;
 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.web.UserRole;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
 import org.sonar.db.project.ProjectDto;
@@ -41,13 +40,16 @@ public class SonarLintPushAction extends ServerPushAction {
   private static final String PROJECT_PARAM_KEY = "projectKeys";
   private static final String LANGUAGE_PARAM_KEY = "languages";
   private final SonarLintClientsRegistry clientsRegistry;
+  private final SonarLintClientPermissionsValidator permissionsValidator;
   private final UserSession userSession;
   private final DbClient dbClient;
 
-  public SonarLintPushAction(SonarLintClientsRegistry sonarLintClientRegistry, UserSession userSession, DbClient dbClient) {
+  public SonarLintPushAction(SonarLintClientsRegistry sonarLintClientRegistry, UserSession userSession, DbClient dbClient,
+    SonarLintClientPermissionsValidator permissionsValidator) {
     this.clientsRegistry = sonarLintClientRegistry;
     this.userSession = userSession;
     this.dbClient = dbClient;
+    this.permissionsValidator = permissionsValidator;
   }
 
   @Override
@@ -80,7 +82,9 @@ public class SonarLintPushAction extends ServerPushAction {
     ServletResponse servletResponse = (ServletResponse) response;
 
     var params = new SonarLintPushActionParamsValidator(request);
-    params.validateProjectsPermissions();
+    params.validateParams();
+
+    permissionsValidator.validateUserCanReceivePushEventForProjects(userSession, params.projectKeys);
 
     if (!isServerSideEventsRequest(servletRequest)) {
       servletResponse.stream().setStatus(HttpServletResponse.SC_NOT_ACCEPTABLE);
@@ -92,7 +96,7 @@ public class SonarLintPushAction extends ServerPushAction {
     AsyncContext asyncContext = servletRequest.startAsync();
     asyncContext.setTimeout(0);
 
-    SonarLintClient sonarLintClient = new SonarLintClient(asyncContext, params.getProjectKeys(), params.getLanguages());
+    SonarLintClient sonarLintClient = new SonarLintClient(asyncContext, params.getProjectKeys(), params.getLanguages(), userSession.getUuid());
 
     clientsRegistry.registerClient(sonarLintClient);
   }
@@ -103,8 +107,6 @@ public class SonarLintPushAction extends ServerPushAction {
     private final Set<String> projectKeys;
     private final Set<String> languages;
 
-    private List<ProjectDto> projectDtos;
-
     SonarLintPushActionParamsValidator(Request request) {
       this.request = request;
       this.projectKeys = parseParam(PROJECT_PARAM_KEY);
@@ -127,19 +129,16 @@ public class SonarLintPushAction extends ServerPushAction {
       return Set.of(paramProjectKeys.trim().split(","));
     }
 
-    public void validateProjectsPermissions() {
-      if (projectDtos == null) {
-        try (DbSession dbSession = dbClient.openSession(false)) {
-          projectDtos = dbClient.projectDao().selectProjectsByKeys(dbSession, projectKeys);
-        }
+    private void validateParams() {
+      List<ProjectDto> projectDtos;
+      try (DbSession dbSession = dbClient.openSession(false)) {
+        projectDtos = dbClient.projectDao().selectProjectsByKeys(dbSession, projectKeys);
       }
       if (projectDtos.size() < projectKeys.size() || projectDtos.isEmpty()) {
         throw new IllegalArgumentException("Param " + PROJECT_PARAM_KEY + " is invalid.");
       }
-      for (ProjectDto projectDto : projectDtos) {
-        userSession.checkProjectPermission(UserRole.USER, projectDto);
-      }
     }
+
   }
 
 }
index 8c050f1ec82134a1db30d48c4aacdd30f4f1f23c..47de0e39b9ea2a7a2427fc44fbb920da60638824 100644 (file)
@@ -28,6 +28,7 @@ import javax.servlet.AsyncListener;
 import javax.servlet.ServletOutputStream;
 import javax.servlet.ServletRequest;
 import javax.servlet.ServletResponse;
+import org.assertj.core.api.Assertions;
 import org.junit.Before;
 import org.junit.Test;
 import org.mockito.Mockito;
@@ -35,6 +36,7 @@ import org.mockito.Mockito;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -47,6 +49,7 @@ public class ServerPushClientTest {
   private final ServerPushClient underTest = new ServerPushClient(executorService, asyncContext) {};
 
   private final ServletOutputStream outputStream = mock(ServletOutputStream.class);
+  private final ScheduledFuture task = mock(ScheduledFuture.class);
   private ServletResponse servletResponse;
 
   @Before
@@ -101,7 +104,6 @@ public class ServerPushClientTest {
   @Test
   public void write_exceptionCausesConnectionToClose() throws IOException {
     when(servletResponse.getOutputStream()).thenThrow(new IOException("mock exception"));
-    ScheduledFuture task = mock(ScheduledFuture.class);
     when(executorService.schedule(any(HeartbeatTask.class), anyLong(), any(TimeUnit.class))).thenReturn(task);
     underTest.scheduleHeartbeat();
 
@@ -113,7 +115,6 @@ public class ServerPushClientTest {
   @Test
   public void flush_exceptionCausesConnectionToClose() throws IOException {
     when(servletResponse.getOutputStream()).thenThrow(new IOException("mock exception"));
-    ScheduledFuture task = mock(ScheduledFuture.class);
     when(executorService.schedule(any(HeartbeatTask.class), anyLong(), any(TimeUnit.class))).thenReturn(task);
     underTest.scheduleHeartbeat();
 
@@ -121,4 +122,14 @@ public class ServerPushClientTest {
 
     verify(asyncContext).complete();
   }
+
+  @Test
+  public void close_exceptionOnComplete_doesNotThrowException() {
+    when(executorService.schedule(any(HeartbeatTask.class), anyLong(), any(TimeUnit.class))).thenReturn(task);
+    doThrow(new IllegalStateException()).when(asyncContext).complete();
+    underTest.scheduleHeartbeat();
+
+    Assertions.assertThatCode(underTest::close)
+      .doesNotThrowAnyException();
+  }
 }
diff --git a/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/sonarlint/SonarLintClientPermissionsValidatorTest.java b/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/sonarlint/SonarLintClientPermissionsValidatorTest.java
new file mode 100644 (file)
index 0000000..0dea668
--- /dev/null
@@ -0,0 +1,94 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.pushapi.sonarlint;
+
+import java.util.List;
+import java.util.Set;
+import org.assertj.core.api.Assertions;
+import org.junit.Before;
+import org.junit.Test;
+import org.sonar.db.DbClient;
+import org.sonar.db.project.ProjectDao;
+import org.sonar.db.project.ProjectDto;
+import org.sonar.db.user.UserDao;
+import org.sonar.db.user.UserDto;
+import org.sonar.server.exceptions.ForbiddenException;
+import org.sonar.server.user.UserSession;
+import org.sonar.server.user.UserSessionFactory;
+
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class SonarLintClientPermissionsValidatorTest {
+
+  private final static String USER_UUID = "USER_UUID";
+
+  private final Set<String> exampleProjectKeys = Set.of("project1", "project2");
+  private final List<ProjectDto> projectDtos = List.of(mock(ProjectDto.class), mock(ProjectDto.class));
+  private final DbClient dbClient = mock(DbClient.class);
+  private final UserSessionFactory userSessionFactory = mock(UserSessionFactory.class);
+  private final UserDao userDao = mock(UserDao.class);
+  private final ProjectDao projectDao = mock(ProjectDao.class);
+  private final UserSession userSession = mock(UserSession.class);
+
+  private SonarLintClientPermissionsValidator underTest = new SonarLintClientPermissionsValidator(dbClient, userSessionFactory);
+
+  @Before
+  public void before() {
+    when(dbClient.userDao()).thenReturn(userDao);
+    when(dbClient.projectDao()).thenReturn(projectDao);
+    when(userSessionFactory.create(any())).thenReturn(userSession);
+    when(projectDao.selectProjectsByKeys(any(), any())).thenReturn(projectDtos);
+  }
+
+  @Test
+  public void validate_givenUserActivatedAndWithRequiredPermissions_dontThrowException() {
+    UserDto userDto = new UserDto();
+    when(userDao.selectByUuid(any(), any())).thenReturn(userDto);
+    when(userSession.isActive()).thenReturn(true);
+
+    assertThatCode(() -> underTest.validateUserCanReceivePushEventForProjects(USER_UUID, exampleProjectKeys))
+      .doesNotThrowAnyException();
+  }
+
+  @Test
+  public void validate_givenUserNotActivated_throwException() {
+    UserDto userDto = new UserDto();
+    when(userDao.selectByUuid(any(), any())).thenReturn(userDto);
+    when(userSession.isActive()).thenReturn(false);
+
+    assertThrows(ForbiddenException.class,
+      () -> underTest.validateUserCanReceivePushEventForProjects(USER_UUID, exampleProjectKeys));
+  }
+
+  @Test
+  public void validate_givenUserNotGrantedProjectPermissions_throwException() {
+    UserDto userDto = new UserDto();
+    when(userDao.selectByUuid(any(), any())).thenReturn(userDto);
+    when(userSession.isActive()).thenReturn(true);
+    when(userSession.checkProjectPermission(any(), any())).thenThrow(ForbiddenException.class);
+
+    assertThrows(ForbiddenException.class,
+      () -> underTest.validateUserCanReceivePushEventForProjects(USER_UUID, exampleProjectKeys));
+  }
+}
index a99cb7ff57109708ce627c3038e16a517e3206a3..cc890386a5b3e0b934cc042ec957a9c40c818b66 100644 (file)
@@ -31,49 +31,51 @@ public class SonarLintClientTest {
   private final AsyncContext firstContext = mock(AsyncContext.class);
   private final AsyncContext secondContext = mock(AsyncContext.class);
 
+  private final String USER_UUID = "userUUID";
+
   @Test
   public void equals_twoClientsWithSameArgumentsAreEqual() {
-    SonarLintClient first = new SonarLintClient(firstContext, Set.of(), Set.of());
-    SonarLintClient second = new SonarLintClient(firstContext, Set.of(), Set.of());
+    SonarLintClient first = new SonarLintClient(firstContext, Set.of(), Set.of(), USER_UUID);
+    SonarLintClient second = new SonarLintClient(firstContext, Set.of(), Set.of(), USER_UUID);
 
     assertThat(first).isEqualTo(second);
   }
 
   @Test
   public void equals_twoClientsWithDifferentAsyncObjects() {
-    SonarLintClient first = new SonarLintClient(firstContext, Set.of(), Set.of());
-    SonarLintClient second = new SonarLintClient(secondContext, Set.of(), Set.of());
+    SonarLintClient first = new SonarLintClient(firstContext, Set.of(), Set.of(), USER_UUID);
+    SonarLintClient second = new SonarLintClient(secondContext, Set.of(), Set.of(), USER_UUID);
 
     assertThat(first).isNotEqualTo(second);
   }
 
   @Test
   public void equals_twoClientsWithDifferentLanguages() {
-    SonarLintClient first = new SonarLintClient(firstContext, Set.of(), Set.of("java"));
-    SonarLintClient second = new SonarLintClient(firstContext, Set.of(), Set.of("cobol"));
+    SonarLintClient first = new SonarLintClient(firstContext, Set.of(), Set.of("java"), USER_UUID);
+    SonarLintClient second = new SonarLintClient(firstContext, Set.of(), Set.of("cobol"), USER_UUID);
 
     assertThat(first).isNotEqualTo(second);
   }
 
   @Test
   public void equals_twoClientsWithDifferentProjectKeys() {
-    SonarLintClient first = new SonarLintClient(firstContext, Set.of("project1", "project2"), Set.of());
-    SonarLintClient second = new SonarLintClient(firstContext, Set.of("project1"), Set.of());
+    SonarLintClient first = new SonarLintClient(firstContext, Set.of("project1", "project2"), Set.of(), USER_UUID);
+    SonarLintClient second = new SonarLintClient(firstContext, Set.of("project1"), Set.of(), USER_UUID);
 
     assertThat(first).isNotEqualTo(second);
   }
 
   @Test
   public void equals_secondClientIsNull() {
-    SonarLintClient first = new SonarLintClient(firstContext, Set.of("project1", "project2"), Set.of());
+    SonarLintClient first = new SonarLintClient(firstContext, Set.of("project1", "project2"), Set.of(), USER_UUID);
 
     assertThat(first).isNotEqualTo(null);
   }
 
   @Test
   public void hashCode_producesSameHashesForEqualObjects() {
-    SonarLintClient first = new SonarLintClient(firstContext, Set.of(), Set.of());
-    SonarLintClient second = new SonarLintClient(firstContext, Set.of(), Set.of());
+    SonarLintClient first = new SonarLintClient(firstContext, Set.of(), Set.of(), USER_UUID);
+    SonarLintClient second = new SonarLintClient(firstContext, Set.of(), Set.of(), USER_UUID);
 
     assertThat(first).hasSameHashCodeAs(second);
   }
index c121bf82609ffd1843a3aad358ea665fd62e3a74..3021e0850b3fe000ab1465bc0b52ad0b5b3e2295 100644 (file)
@@ -29,9 +29,14 @@ import org.junit.Test;
 import org.mockito.ArgumentCaptor;
 import org.sonar.core.util.RuleChange;
 import org.sonar.core.util.RuleSetChangeEvent;
+import org.sonar.server.exceptions.ForbiddenException;
 import org.sonar.server.pushapi.qualityprofile.StandaloneRuleActivatorEventsDistributor;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anySet;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoInteractions;
@@ -42,19 +47,23 @@ public class SonarLintClientsRegistryTest {
   private final AsyncContext defaultAsyncContext = mock(AsyncContext.class);
 
   private final Set<String> exampleKeys = Set.of("project1", "project2", "project3");
-
   private final Set<String> languageKeys = Set.of("language1", "language2", "language3");
+  private final String USER_UUID = "userUuid";
+  private final ServletResponse response = mock(ServletResponse.class);
+  private final ServletOutputStream outputStream = mock(ServletOutputStream.class);
+
+  private final SonarLintClientPermissionsValidator permissionsValidator = mock(SonarLintClientPermissionsValidator.class);
 
   private SonarLintClientsRegistry underTest;
 
   @Before
   public void before() {
-    underTest = new SonarLintClientsRegistry(mock(StandaloneRuleActivatorEventsDistributor.class));
+    underTest = new SonarLintClientsRegistry(mock(StandaloneRuleActivatorEventsDistributor.class), permissionsValidator);
   }
 
   @Test
   public void registerClientAndUnregister_changesNumberOfClients() {
-    SonarLintClient sonarLintClient = new SonarLintClient(defaultAsyncContext, exampleKeys, languageKeys);
+    SonarLintClient sonarLintClient = new SonarLintClient(defaultAsyncContext, exampleKeys, languageKeys, USER_UUID);
 
     underTest.registerClient(sonarLintClient);
 
@@ -69,7 +78,7 @@ public class SonarLintClientsRegistryTest {
   public void registering10Clients_10ClientsAreRegistered() {
     for (int i = 0; i < 10; i++) {
       AsyncContext newAsyncContext = mock(AsyncContext.class);
-      SonarLintClient sonarLintClient = new SonarLintClient(newAsyncContext, exampleKeys, languageKeys);
+      SonarLintClient sonarLintClient = new SonarLintClient(newAsyncContext, exampleKeys, languageKeys, USER_UUID);
       underTest.registerClient(sonarLintClient);
     }
 
@@ -79,11 +88,9 @@ public class SonarLintClientsRegistryTest {
   @Test
   public void listen_givenOneClientInterestedInJavaEvents_sendOneJavaEvent() throws IOException {
     Set<String> javaLanguageKey = Set.of("java");
-    ServletResponse response = mock(ServletResponse.class);
     when(defaultAsyncContext.getResponse()).thenReturn(response);
-    ServletOutputStream outputStream = mock(ServletOutputStream.class);
     when(response.getOutputStream()).thenReturn(outputStream);
-    SonarLintClient sonarLintClient = new SonarLintClient(defaultAsyncContext, exampleKeys, javaLanguageKey);
+    SonarLintClient sonarLintClient = new SonarLintClient(defaultAsyncContext, exampleKeys, javaLanguageKey, USER_UUID);
 
     underTest.registerClient(sonarLintClient);
 
@@ -103,11 +110,9 @@ public class SonarLintClientsRegistryTest {
   @Test
   public void listen_givenOneClientInterestedInJsEventsAndJavaEventGenerated_sendZeroEvents() throws IOException {
     Set<String> jsLanguageKey = Set.of("js");
-    ServletResponse response = mock(ServletResponse.class);
     when(defaultAsyncContext.getResponse()).thenReturn(response);
-    ServletOutputStream outputStream = mock(ServletOutputStream.class);
     when(response.getOutputStream()).thenReturn(outputStream);
-    SonarLintClient sonarLintClient = new SonarLintClient(defaultAsyncContext, exampleKeys, jsLanguageKey);
+    SonarLintClient sonarLintClient = new SonarLintClient(defaultAsyncContext, exampleKeys, jsLanguageKey, USER_UUID);
 
     underTest.registerClient(sonarLintClient);
 
@@ -121,6 +126,52 @@ public class SonarLintClientsRegistryTest {
     verifyNoInteractions(outputStream);
   }
 
+  @Test
+  public void listen_givenOneClientInterestedInProjA_DontCheckPermissionsForProjB() throws IOException {
+    when(defaultAsyncContext.getResponse()).thenReturn(response);
+    when(response.getOutputStream()).thenReturn(outputStream);
+    Set<String> clientProjectKeys = Set.of("projA");
+    Set<String> eventProjectKeys = Set.of("projA", "projB");
+    SonarLintClient sonarLintClient = new SonarLintClient(defaultAsyncContext, clientProjectKeys, Set.of("java"), USER_UUID);
+
+    underTest.registerClient(sonarLintClient);
+
+    RuleChange javaRuleChange = createRuleChange("java");
+
+    RuleChange[] activatedRules = {};
+    RuleChange[] deactivatedRules = {javaRuleChange};
+    RuleSetChangeEvent ruleChangeEvent = new RuleSetChangeEvent(eventProjectKeys.toArray(String[]::new), activatedRules, deactivatedRules);
+    underTest.listen(ruleChangeEvent);
+
+    ArgumentCaptor<Set<String>> argument = ArgumentCaptor.forClass(Set.class);
+    verify(permissionsValidator).validateUserCanReceivePushEventForProjects(anyString(), argument.capture());
+    assertThat(argument.getValue()).isEqualTo(clientProjectKeys);
+  }
+
+  @Test
+  public void listen_givenUserNotPermittedToReceiveEvent_closeConnection() {
+    RuleChange javaRuleChange = createRuleChange("java");
+    RuleChange[] activatedRules = {};
+    RuleChange[] deactivatedRules = {javaRuleChange};
+    RuleSetChangeEvent ruleChangeEvent = new RuleSetChangeEvent(exampleKeys.toArray(String[]::new), activatedRules, deactivatedRules);
+
+    SonarLintClient sonarLintClient = createSampleSLClient();
+    underTest.registerClient(sonarLintClient);
+    doThrow(new ForbiddenException("Access forbidden")).when(permissionsValidator).validateUserCanReceivePushEventForProjects(anyString(), anySet());
+
+    underTest.listen(ruleChangeEvent);
+
+    verify(sonarLintClient).close();
+  }
+
+  private SonarLintClient createSampleSLClient() {
+    SonarLintClient mock = mock(SonarLintClient.class);
+    when(mock.getLanguages()).thenReturn(Set.of("java"));
+    when(mock.getClientProjectKeys()).thenReturn(exampleKeys);
+    when(mock.getUserUuid()).thenReturn("userUuid");
+    return mock;
+  }
+
   private RuleChange createRuleChange(String language) {
     RuleChange javaRule = new RuleChange();
     javaRule.setLanguage(language);
index d1ecab10fda8b89fc5144c64e4af1a0dddc1e173..b6597478b6081883c6bb17dceb4555ed20a6c9f5 100644 (file)
@@ -46,8 +46,9 @@ public class SonarLintPushActionTest {
   private final UserSession userSession = mock(UserSession.class);
   private final DbClient dbClient = mock(DbClient.class);
   private final ProjectDao projectDao = mock(ProjectDao.class);
+  private final SonarLintClientPermissionsValidator permissionsValidator = mock(SonarLintClientPermissionsValidator.class);
 
-  private final WsPushActionTester ws = new WsPushActionTester(new SonarLintPushAction(registry, userSession, dbClient));
+  private final WsPushActionTester ws = new WsPushActionTester(new SonarLintPushAction(registry, userSession, dbClient, permissionsValidator));
 
   @Before
   public void before() {