diff options
24 files changed, 363 insertions, 50 deletions
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/SafeModeUserSession.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/SafeModeUserSession.java index 78601b04878..6864bce8424 100644 --- a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/SafeModeUserSession.java +++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/SafeModeUserSession.java @@ -108,4 +108,9 @@ public class SafeModeUserSession extends AbstractUserSession { public boolean isSystemAdministrator() { return false; } + + @Override + public boolean isActive() { + return false; + } } diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/user/DoPrivileged.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/user/DoPrivileged.java index 0e4e8a70aff..0ac01236152 100644 --- a/server/sonar-webserver-auth/src/main/java/org/sonar/server/user/DoPrivileged.java +++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/user/DoPrivileged.java @@ -142,6 +142,11 @@ public final class DoPrivileged { return true; } + @Override + public boolean isActive() { + return true; + } + } private void start() { 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 9d51a52211c..b8d9dfa21b5 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 @@ -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; 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 1ae45a0fa90..1d8866c1b16 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 @@ -174,6 +174,11 @@ public class ThreadLocalUserSession implements UserSession { } @Override + public boolean isActive() { + return get().isActive(); + } + + @Override public boolean hasComponentPermission(String permission, ComponentDto component) { return get().hasComponentPermission(permission, component); } 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 116dc6341c0..f10db0d1dfc 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 @@ -274,4 +274,6 @@ public interface UserSession { * otherwise throws {@link org.sonar.server.exceptions.ForbiddenException}. */ UserSession checkIsSystemAdministrator(); + + boolean isActive(); } diff --git a/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/SafeModeUserSessionTest.java b/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/SafeModeUserSessionTest.java index ad1cc032f00..0278a2eab3f 100644 --- a/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/SafeModeUserSessionTest.java +++ b/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/SafeModeUserSessionTest.java @@ -37,6 +37,7 @@ public class SafeModeUserSessionTest { assertThat(underTest.shouldResetPassword()).isFalse(); assertThat(underTest.getName()).isNull(); assertThat(underTest.getGroups()).isEmpty(); + assertThat(underTest.isActive()).isFalse(); } @Test diff --git a/server/sonar-webserver-auth/src/test/java/org/sonar/server/user/DoPrivilegedTest.java b/server/sonar-webserver-auth/src/test/java/org/sonar/server/user/DoPrivilegedTest.java index 9275a3ce5ed..b45fe1547d6 100644 --- a/server/sonar-webserver-auth/src/test/java/org/sonar/server/user/DoPrivilegedTest.java +++ b/server/sonar-webserver-auth/src/test/java/org/sonar/server/user/DoPrivilegedTest.java @@ -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(); 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 c43ee7ee538..c3ea59725e7 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 @@ -127,6 +127,17 @@ public class ServerUserSessionTest { } @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(); root = db.users().makeRoot(root); 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 c831d7bfe24..4f8a5e463c9 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 @@ -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(); diff --git a/server/sonar-webserver-auth/src/testFixtures/java/org/sonar/server/tester/AnonymousMockUserSession.java b/server/sonar-webserver-auth/src/testFixtures/java/org/sonar/server/tester/AnonymousMockUserSession.java index b1495fe6f22..704345cf6eb 100644 --- a/server/sonar-webserver-auth/src/testFixtures/java/org/sonar/server/tester/AnonymousMockUserSession.java +++ b/server/sonar-webserver-auth/src/testFixtures/java/org/sonar/server/tester/AnonymousMockUserSession.java @@ -36,6 +36,11 @@ public class AnonymousMockUserSession extends AbstractMockUserSession<AnonymousM } @Override + public boolean isActive() { + return false; + } + + @Override public String getLogin() { return null; } diff --git a/server/sonar-webserver-auth/src/testFixtures/java/org/sonar/server/tester/MockUserSession.java b/server/sonar-webserver-auth/src/testFixtures/java/org/sonar/server/tester/MockUserSession.java index ecc6f906844..b199c30275a 100644 --- a/server/sonar-webserver-auth/src/testFixtures/java/org/sonar/server/tester/MockUserSession.java +++ b/server/sonar-webserver-auth/src/testFixtures/java/org/sonar/server/tester/MockUserSession.java @@ -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; } 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 c50fdfbb4b0..c8fc0b37f98 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 @@ -412,4 +412,9 @@ public class UserSessionRule implements TestRule, UserSession { currentUserSession.checkIsSystemAdministrator(); return this; } + + @Override + public boolean isActive() { + return currentUserSession.isActive(); + } } diff --git a/server/sonar-webserver-auth/src/testFixtures/java/org/sonar/server/user/TestUserSessionFactory.java b/server/sonar-webserver-auth/src/testFixtures/java/org/sonar/server/user/TestUserSessionFactory.java index 892ed01a0d9..d09d9ea662d 100644 --- a/server/sonar-webserver-auth/src/testFixtures/java/org/sonar/server/user/TestUserSessionFactory.java +++ b/server/sonar-webserver-auth/src/testFixtures/java/org/sonar/server/user/TestUserSessionFactory.java @@ -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"); } diff --git a/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/ServerPushClient.java b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/ServerPushClient.java index a726ee1197f..458b716a3af 100644 --- a/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/ServerPushClient.java +++ b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/ServerPushClient.java @@ -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 { diff --git a/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/ServerPushWsModule.java b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/ServerPushWsModule.java index c6f194b5414..0a48485466c 100644 --- a/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/ServerPushWsModule.java +++ b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/ServerPushWsModule.java @@ -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); } diff --git a/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/sonarlint/SonarLintClient.java b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/sonarlint/SonarLintClient.java index 93512a974ec..3d0f5cff07f 100644 --- a/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/sonarlint/SonarLintClient.java +++ b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/sonarlint/SonarLintClient.java @@ -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 index 00000000000..d8765332e90 --- /dev/null +++ b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/sonarlint/SonarLintClientPermissionsValidator.java @@ -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."); + } + } +} diff --git a/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/sonarlint/SonarLintClientsRegistry.java b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/sonarlint/SonarLintClientsRegistry.java index 51146f28510..bcf88efcad5 100644 --- a/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/sonarlint/SonarLintClientsRegistry.java +++ b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/sonarlint/SonarLintClientsRegistry.java @@ -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 diff --git a/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/sonarlint/SonarLintPushAction.java b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/sonarlint/SonarLintPushAction.java index ca30b37f478..6a63de5f20a 100644 --- a/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/sonarlint/SonarLintPushAction.java +++ b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/sonarlint/SonarLintPushAction.java @@ -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); - } } + } } diff --git a/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/ServerPushClientTest.java b/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/ServerPushClientTest.java index 8c050f1ec82..47de0e39b9e 100644 --- a/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/ServerPushClientTest.java +++ b/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/ServerPushClientTest.java @@ -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 index 00000000000..0dea66827e5 --- /dev/null +++ b/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/sonarlint/SonarLintClientPermissionsValidatorTest.java @@ -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)); + } +} diff --git a/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/sonarlint/SonarLintClientTest.java b/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/sonarlint/SonarLintClientTest.java index a99cb7ff571..cc890386a5b 100644 --- a/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/sonarlint/SonarLintClientTest.java +++ b/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/sonarlint/SonarLintClientTest.java @@ -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); } diff --git a/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/sonarlint/SonarLintClientsRegistryTest.java b/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/sonarlint/SonarLintClientsRegistryTest.java index c121bf82609..3021e0850b3 100644 --- a/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/sonarlint/SonarLintClientsRegistryTest.java +++ b/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/sonarlint/SonarLintClientsRegistryTest.java @@ -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); diff --git a/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/sonarlint/SonarLintPushActionTest.java b/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/sonarlint/SonarLintPushActionTest.java index d1ecab10fda..b6597478b60 100644 --- a/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/sonarlint/SonarLintPushActionTest.java +++ b/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/sonarlint/SonarLintPushActionTest.java @@ -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() { |