aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorSébastien Lesaint <sebastien.lesaint@sonarsource.com>2017-09-06 15:52:43 +0200
committerSébastien Lesaint <sebastien.lesaint@sonarsource.com>2017-09-13 15:50:55 +0200
commit7e778849d1c669cd87c90de21cb3d1ea6494e759 (patch)
tree0545591c3034fc933cea52b1f63bda9f64b58e90 /server
parent3de079f32f95e92f08f03d599d7b483086b87cfa (diff)
downloadsonarqube-7e778849d1c669cd87c90de21cb3d1ea6494e759.tar.gz
sonarqube-7e778849d1c669cd87c90de21cb3d1ea6494e759.zip
SONAR-9741 add authentication to api/system/health
Diffstat (limited to 'server')
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/authentication/UserSessionInitializer.java2
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/platform/ws/HealthAction.java21
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/platform/ws/SafeModeHealthAction.java10
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/authentication/UserSessionInitializerTest.java1
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/platform/ws/HealthActionTest.java127
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/platform/ws/SafeModeHealthActionTest.java59
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);
+ }
+
}