diff options
author | Sébastien Lesaint <sebastien.lesaint@sonarsource.com> | 2017-09-06 15:52:43 +0200 |
---|---|---|
committer | Sébastien Lesaint <sebastien.lesaint@sonarsource.com> | 2017-09-13 15:50:55 +0200 |
commit | 7e778849d1c669cd87c90de21cb3d1ea6494e759 (patch) | |
tree | 0545591c3034fc933cea52b1f63bda9f64b58e90 /server | |
parent | 3de079f32f95e92f08f03d599d7b483086b87cfa (diff) | |
download | sonarqube-7e778849d1c669cd87c90de21cb3d1ea6494e759.tar.gz sonarqube-7e778849d1c669cd87c90de21cb3d1ea6494e759.zip |
SONAR-9741 add authentication to api/system/health
Diffstat (limited to 'server')
6 files changed, 214 insertions, 6 deletions
diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/UserSessionInitializer.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/UserSessionInitializer.java index e2a45829e09..4cedd539219 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/authentication/UserSessionInitializer.java +++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/UserSessionInitializer.java @@ -59,7 +59,7 @@ public class UserSessionInitializer { "/batch/index", "/batch/file", "/maintenance/*", "/setup/*", "/sessions/*", "/oauth2/callback/*", - "/api/system/db_migration_status", "/api/system/status", "/api/system/migrate_db", "/api/system/health", + "/api/system/db_migration_status", "/api/system/status", "/api/system/migrate_db", "/api/server/version", "/api/users/identity_providers", "/api/l10n/index", LOGIN_URL, LOGOUT_URL, VALIDATE_URL); diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/ws/HealthAction.java b/server/sonar-server/src/main/java/org/sonar/server/platform/ws/HealthAction.java index b4931a745cb..f71f03d3be9 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/ws/HealthAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/ws/HealthAction.java @@ -22,16 +22,23 @@ package org.sonar.server.platform.ws; import org.sonar.api.server.ws.Request; import org.sonar.api.server.ws.Response; import org.sonar.api.server.ws.WebService; +import org.sonar.server.exceptions.ForbiddenException; import org.sonar.server.platform.WebServer; +import org.sonar.server.user.SystemPasscode; +import org.sonar.server.user.UserSession; import org.sonar.server.ws.WsUtils; public class HealthAction implements SystemWsAction { private final WebServer webServer; private final HealthActionSupport support; + private final SystemPasscode systemPasscode; + private final UserSession userSession; - public HealthAction(WebServer webServer, HealthActionSupport support) { + public HealthAction(WebServer webServer, HealthActionSupport support, SystemPasscode systemPasscode, UserSession userSession) { this.webServer = webServer; this.support = support; + this.systemPasscode = systemPasscode; + this.userSession = userSession; } @Override @@ -41,6 +48,10 @@ public class HealthAction implements SystemWsAction { @Override public void handle(Request request, Response response) throws Exception { + if (!isPassCodeAuthenticated(request) && !isSystemAdmin()) { + throw new ForbiddenException("Insufficient privileges"); + } + if (webServer.isStandalone()) { WsUtils.writeProtobuf(support.checkNodeHealth(), request, response); } else { @@ -48,4 +59,12 @@ public class HealthAction implements SystemWsAction { } } + private boolean isSystemAdmin() { + return userSession.isSystemAdministrator(); + } + + private boolean isPassCodeAuthenticated(Request request) { + return systemPasscode.isConfigured() && systemPasscode.isValid(request); + } + } diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/ws/SafeModeHealthAction.java b/server/sonar-server/src/main/java/org/sonar/server/platform/ws/SafeModeHealthAction.java index 1c7f953354a..6793f98ae51 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/ws/SafeModeHealthAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/ws/SafeModeHealthAction.java @@ -22,13 +22,17 @@ package org.sonar.server.platform.ws; import org.sonar.api.server.ws.Request; import org.sonar.api.server.ws.Response; import org.sonar.api.server.ws.WebService; +import org.sonar.server.exceptions.ForbiddenException; +import org.sonar.server.user.SystemPasscode; import org.sonar.server.ws.WsUtils; public class SafeModeHealthAction implements SystemWsAction { private final HealthActionSupport support; + private final SystemPasscode systemPasscode; - public SafeModeHealthAction(HealthActionSupport support) { + public SafeModeHealthAction(HealthActionSupport support, SystemPasscode systemPasscode) { this.support = support; + this.systemPasscode = systemPasscode; } @Override @@ -38,6 +42,10 @@ public class SafeModeHealthAction implements SystemWsAction { @Override public void handle(Request request, Response response) throws Exception { + if (!systemPasscode.isConfigured() || !systemPasscode.isValid(request)) { + throw new ForbiddenException("Insufficient privileges"); + } + WsUtils.writeProtobuf(support.checkNodeHealth(), request, response); } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/authentication/UserSessionInitializerTest.java b/server/sonar-server/src/test/java/org/sonar/server/authentication/UserSessionInitializerTest.java index 7697b6ff590..b8fcf9fd7ce 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/authentication/UserSessionInitializerTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/authentication/UserSessionInitializerTest.java @@ -110,7 +110,6 @@ public class UserSessionInitializerTest { assertPathIsIgnored("/api/system/db_migration_status"); assertPathIsIgnored("/api/system/status"); assertPathIsIgnored("/api/system/migrate_db"); - assertPathIsIgnored("/api/system/health"); assertPathIsIgnored("/api/server/version"); assertPathIsIgnored("/api/users/identity_providers"); assertPathIsIgnored("/api/l10n/index"); diff --git a/server/sonar-server/src/test/java/org/sonar/server/platform/ws/HealthActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/platform/ws/HealthActionTest.java index 11522e7aabf..33ed30afca1 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/platform/ws/HealthActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/platform/ws/HealthActionTest.java @@ -29,14 +29,20 @@ import java.util.List; import java.util.Random; import java.util.stream.IntStream; import org.apache.commons.lang.RandomStringUtils; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.api.server.ws.Request; import org.sonar.api.server.ws.WebService; import org.sonar.cluster.health.NodeDetails; import org.sonar.cluster.health.NodeHealth; +import org.sonar.server.exceptions.ForbiddenException; import org.sonar.server.health.ClusterHealth; import org.sonar.server.health.Health; import org.sonar.server.health.HealthChecker; import org.sonar.server.platform.WebServer; +import org.sonar.server.tester.UserSessionRule; +import org.sonar.server.user.SystemPasscode; import org.sonar.server.ws.TestRequest; import org.sonar.server.ws.TestResponse; import org.sonar.server.ws.WsActionTester; @@ -47,6 +53,7 @@ import static java.util.Collections.singleton; import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic; import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.sonar.api.utils.DateUtils.formatDateTime; @@ -58,10 +65,16 @@ import static org.sonar.server.health.Health.newHealthCheckBuilder; import static org.sonar.test.JsonAssert.assertJson; public class HealthActionTest { + @Rule + public ExpectedException expectedException = ExpectedException.none(); + @Rule + public UserSessionRule userSessionRule = UserSessionRule.standalone(); + private final Random random = new Random(); private HealthChecker healthChecker = mock(HealthChecker.class); private WebServer webServer = mock(WebServer.class); - private WsActionTester underTest = new WsActionTester(new HealthAction(webServer, new HealthActionSupport(healthChecker))); + private SystemPasscode systemPasscode = mock(SystemPasscode.class); + private WsActionTester underTest = new WsActionTester(new HealthAction(webServer, new HealthActionSupport(healthChecker), systemPasscode, userSessionRule)); @Test public void verify_definition() { @@ -77,7 +90,82 @@ public class HealthActionTest { } @Test + public void request_fails_with_ForbiddenException_when_anonymous() { + TestRequest request = underTest.newRequest(); + + expectForbiddenException(); + + request.execute(); + } + + @Test + public void request_fails_with_SystemPasscode_enabled_and_anonymous() { + when(systemPasscode.isConfigured()).thenReturn(true); + TestRequest request = underTest.newRequest(); + + expectForbiddenException(); + + request.execute(); + } + + @Test + public void request_fails_with_SystemPasscode_enabled_but_no_passcode_and_user_is_not_system_administrator() { + when(systemPasscode.isConfigured()).thenReturn(true); + when(systemPasscode.isValid(any(Request.class))).thenReturn(false); + userSessionRule.logIn(); + when(healthChecker.checkCluster()).thenReturn(randomStatusMinimalClusterHealth()); + TestRequest request = underTest.newRequest(); + + expectForbiddenException(); + + request.execute(); + } + + @Test + public void request_succeeds_with_SystemPasscode_enabled_and_passcode() { + when(systemPasscode.isConfigured()).thenReturn(true); + when(systemPasscode.isValid(any(Request.class))).thenReturn(true); + when(healthChecker.checkCluster()).thenReturn(randomStatusMinimalClusterHealth()); + TestRequest request = underTest.newRequest(); + + request.execute(); + } + + @Test + public void request_succeeds_with_SystemPasscode_disabled_and_user_is_system_administrator() { + when(systemPasscode.isConfigured()).thenReturn(false); + userSessionRule.logIn().setSystemAdministrator(); + when(healthChecker.checkCluster()).thenReturn(randomStatusMinimalClusterHealth()); + TestRequest request = underTest.newRequest(); + + request.execute(); + } + + @Test + public void request_succeeds_with_SystemPasscode_enabled_but_no_passcode_and_user_is_system_administrator() { + when(systemPasscode.isConfigured()).thenReturn(true); + when(systemPasscode.isValid(any(Request.class))).thenReturn(false); + userSessionRule.logIn().setSystemAdministrator(); + when(healthChecker.checkCluster()).thenReturn(randomStatusMinimalClusterHealth()); + TestRequest request = underTest.newRequest(); + + request.execute(); + } + + @Test + public void request_succeeds_with_SystemPasscode_enabled_and_passcode_and_user_is_system_administrator() { + when(systemPasscode.isConfigured()).thenReturn(true); + when(systemPasscode.isValid(any(Request.class))).thenReturn(true); + userSessionRule.logIn().setSystemAdministrator(); + when(healthChecker.checkCluster()).thenReturn(randomStatusMinimalClusterHealth()); + TestRequest request = underTest.newRequest(); + + request.execute(); + } + + @Test public void verify_response_example() { + authenticateWithRandomMethod(); when(webServer.isStandalone()).thenReturn(false); long time = parseDateTime("2015-08-13T23:34:59+0200").getTime(); when(healthChecker.checkCluster()) @@ -153,6 +241,7 @@ public class HealthActionTest { @Test public void request_returns_status_and_causes_from_HealthChecker_checkNode_method_when_standalone() { + authenticateWithRandomMethod(); Health.Status randomStatus = Health.Status.values()[new Random().nextInt(Health.Status.values().length)]; Health.Builder builder = newHealthCheckBuilder() .setStatus(randomStatus); @@ -169,6 +258,7 @@ public class HealthActionTest { @Test public void response_contains_status_and_causes_from_HealthChecker_checkCluster_when_standalone() { + authenticateWithRandomMethod(); Health.Status randomStatus = Health.Status.values()[random.nextInt(Health.Status.values().length)]; String[] causes = IntStream.range(0, random.nextInt(33)).mapToObj(i -> randomAlphanumeric(4)).toArray(String[]::new); Health.Builder healthBuilder = newHealthCheckBuilder() @@ -186,6 +276,7 @@ public class HealthActionTest { @Test public void response_contains_information_of_nodes_when_clustered() { + authenticateWithRandomMethod(); NodeHealth nodeHealth = randomNodeHealth(); when(webServer.isStandalone()).thenReturn(false); when(healthChecker.checkCluster()).thenReturn(new ClusterHealth(GREEN, singleton(nodeHealth))); @@ -208,6 +299,7 @@ public class HealthActionTest { @Test public void response_sort_nodes_by_type_name_host_then_port_when_clustered() { + authenticateWithRandomMethod(); // using created field as a unique identifier. pseudo random value to ensure sorting is not based on created field List<NodeHealth> nodeHealths = new ArrayList<>(Arrays.asList( randomNodeHealth(NodeDetails.Type.APPLICATION, "1_name", "1_host", 1, 99), @@ -268,4 +360,37 @@ public class HealthActionTest { .build(); } + private ClusterHealth randomStatusMinimalClusterHealth() { + return new ClusterHealth(newHealthCheckBuilder() + .setStatus(Health.Status.values()[random.nextInt(Health.Status.values().length)]) + .build(), emptySet()); + } + + private void expectForbiddenException() { + expectedException.expect(ForbiddenException.class); + expectedException.expectMessage("Insufficient privileges"); + } + + /** + * Randomly choose of one the valid authentication method: + * <ul> + * <li>system administrator and passcode disabled</li> + * <li>system administrator and passcode enabled</li> + * <li>passcode</li> + * </ul> + */ + private void authenticateWithRandomMethod() { + if (random.nextBoolean()) { + when(systemPasscode.isConfigured()).thenReturn(true); + if (random.nextBoolean()) { + when(systemPasscode.isValid(any(Request.class))).thenReturn(true); + } else { + when(systemPasscode.isValid(any(Request.class))).thenReturn(false); + userSessionRule.logIn().setSystemAdministrator(); + } + } else { + userSessionRule.logIn().setSystemAdministrator(); + } + } + } diff --git a/server/sonar-server/src/test/java/org/sonar/server/platform/ws/SafeModeHealthActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/platform/ws/SafeModeHealthActionTest.java index 340171ce67c..0ee02bbd5f5 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/platform/ws/SafeModeHealthActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/platform/ws/SafeModeHealthActionTest.java @@ -23,10 +23,15 @@ import java.util.Arrays; import java.util.Random; import java.util.stream.IntStream; import org.apache.commons.lang.RandomStringUtils; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.api.server.ws.Request; import org.sonar.api.server.ws.WebService; +import org.sonar.server.exceptions.ForbiddenException; import org.sonar.server.health.Health; import org.sonar.server.health.HealthChecker; +import org.sonar.server.user.SystemPasscode; import org.sonar.server.ws.TestRequest; import org.sonar.server.ws.TestResponse; import org.sonar.server.ws.WsActionTester; @@ -34,15 +39,20 @@ import org.sonarqube.ws.WsSystem; import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.sonar.server.health.Health.newHealthCheckBuilder; import static org.sonar.test.JsonAssert.assertJson; public class SafeModeHealthActionTest { + @Rule + public ExpectedException expectedException = ExpectedException.none(); + private final Random random = new Random(); private HealthChecker healthChecker = mock(HealthChecker.class); - private WsActionTester underTest = new WsActionTester(new SafeModeHealthAction(new HealthActionSupport(healthChecker))); + private SystemPasscode systemPasscode = mock(SystemPasscode.class); + private WsActionTester underTest = new WsActionTester(new SafeModeHealthAction(new HealthActionSupport(healthChecker), systemPasscode)); @Test public void verify_definition() { @@ -58,7 +68,42 @@ public class SafeModeHealthActionTest { } @Test + public void request_fails_with_ForbiddenException_when_PassCode_disabled() { + when(systemPasscode.isConfigured()).thenReturn(false); + when(systemPasscode.isValid(any(Request.class))).thenReturn(random.nextBoolean()); + TestRequest request = underTest.newRequest(); + + expectForbiddenException(); + + request.execute(); + } + + @Test + public void request_fails_with_ForbiddenException_when_PassCode_enabled_but_no_passcode() { + when(systemPasscode.isConfigured()).thenReturn(true); + when(systemPasscode.isValid(any(Request.class))).thenReturn(false); + TestRequest request = underTest.newRequest(); + + expectForbiddenException(); + + request.execute(); + } + + @Test + public void request_succeeds_when_PassCode_enabled_and_valid_passcode() { + authenticateWithPasscode(); + when(healthChecker.checkNode()) + .thenReturn(newHealthCheckBuilder() + .setStatus(Health.Status.values()[random.nextInt(Health.Status.values().length)]) + .build()); + TestRequest request = underTest.newRequest(); + + request.execute(); + } + + @Test public void verify_response_example() { + authenticateWithPasscode(); when(healthChecker.checkNode()) .thenReturn(newHealthCheckBuilder() .setStatus(Health.Status.RED) @@ -74,6 +119,7 @@ public class SafeModeHealthActionTest { @Test public void request_returns_status_and_causes_from_HealthChecker_checkNode_method() { + authenticateWithPasscode(); Health.Status randomStatus = Health.Status.values()[new Random().nextInt(Health.Status.values().length)]; Health.Builder builder = newHealthCheckBuilder() .setStatus(randomStatus); @@ -89,6 +135,7 @@ public class SafeModeHealthActionTest { @Test public void response_contains_status_and_causes_from_HealthChecker_checkCluster() { + authenticateWithPasscode(); Health.Status randomStatus = Health.Status.values()[random.nextInt(Health.Status.values().length)]; String[] causes = IntStream.range(0, random.nextInt(33)).mapToObj(i -> randomAlphanumeric(4)).toArray(String[]::new); Health.Builder healthBuilder = newHealthCheckBuilder() @@ -103,4 +150,14 @@ public class SafeModeHealthActionTest { .containsOnly(causes); } + private void expectForbiddenException() { + expectedException.expect(ForbiddenException.class); + expectedException.expectMessage("Insufficient privileges"); + } + + private void authenticateWithPasscode() { + when(systemPasscode.isConfigured()).thenReturn(true); + when(systemPasscode.isValid(any(Request.class))).thenReturn(true); + } + } |