Browse Source

SONAR-9741 add authentication to api/system/health

tags/6.6-RC1
Sébastien Lesaint 6 years ago
parent
commit
7e778849d1

+ 1
- 1
server/sonar-server/src/main/java/org/sonar/server/authentication/UserSessionInitializer.java View File

@@ -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);

+ 20
- 1
server/sonar-server/src/main/java/org/sonar/server/platform/ws/HealthAction.java View File

@@ -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);
}

}

+ 9
- 1
server/sonar-server/src/main/java/org/sonar/server/platform/ws/SafeModeHealthAction.java View File

@@ -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);
}
}

+ 0
- 1
server/sonar-server/src/test/java/org/sonar/server/authentication/UserSessionInitializerTest.java View File

@@ -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");

+ 126
- 1
server/sonar-server/src/test/java/org/sonar/server/platform/ws/HealthActionTest.java View File

@@ -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() {
@@ -76,8 +89,83 @@ public class HealthActionTest {
assertThat(definition.params()).isEmpty();
}

@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();
}
}

}

+ 58
- 1
server/sonar-server/src/test/java/org/sonar/server/platform/ws/SafeModeHealthActionTest.java View File

@@ -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() {
@@ -57,8 +67,43 @@ public class SafeModeHealthActionTest {
assertThat(definition.params()).isEmpty();
}

@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);
}

}

+ 3
- 1
tests/src/test/java/org/sonarqube/tests/cluster/Cluster.java View File

@@ -37,6 +37,7 @@ class Cluster implements AutoCloseable {
private final String clusterName;

private final List<Node> nodes = new ArrayList<>();
private final String systemPassCode = "fooBar2000";

Cluster(@Nullable String name) {
this.clusterName = name;
@@ -50,6 +51,7 @@ class Cluster implements AutoCloseable {

Node addNode(NodeConfig config, Consumer<OrchestratorBuilder> consumer) {
OrchestratorBuilder builder = newOrchestratorBuilder(config);
builder.setServerProperty("sonar.web.systemPasscode", systemPassCode);

switch (config.getType()) {
case SEARCH:
@@ -71,7 +73,7 @@ class Cluster implements AutoCloseable {
}
consumer.accept(builder);
Orchestrator orchestrator = builder.build();
Node node = new Node(config, orchestrator);
Node node = new Node(config, orchestrator, systemPassCode);
nodes.add(node);
return node;
}

+ 4
- 2
tests/src/test/java/org/sonarqube/tests/cluster/Node.java View File

@@ -42,12 +42,14 @@ class Node {

private final NodeConfig config;
private final Orchestrator orchestrator;
private final String systemPassCode;
private LogsTailer logsTailer;
private final LogsTailer.Content content = new LogsTailer.Content();

Node(NodeConfig config, Orchestrator orchestrator) {
Node(NodeConfig config, Orchestrator orchestrator, String systemPassCode) {
this.config = config;
this.orchestrator = orchestrator;
this.systemPassCode = systemPassCode;
}

NodeConfig getConfig() {
@@ -142,7 +144,7 @@ class Node {
return Optional.empty();
}
try {
return Optional.ofNullable(ItUtils.newAdminWsClient(orchestrator).system().health());
return Optional.ofNullable(ItUtils.newSystemUserWsClient(orchestrator, systemPassCode).system().health());
} catch (Exception e) {
return Optional.empty();
}

+ 9
- 4
tests/src/test/java/org/sonarqube/tests/serverSystem/SystemStateTest.java View File

@@ -24,9 +24,11 @@ import com.sonar.orchestrator.util.NetworkUtils;
import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.util.Arrays;
import java.util.Optional;
import java.util.function.Supplier;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.RandomStringUtils;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.DisableOnDebug;
@@ -38,8 +40,8 @@ import org.sonarqube.ws.WsSystem;
import org.sonarqube.ws.client.WsClient;

import static com.google.common.base.Preconditions.checkState;
import static java.util.stream.Collectors.joining;
import static org.assertj.core.api.Assertions.assertThat;
import static util.ItUtils.newSystemUserWsClient;
import static util.ItUtils.newWsClient;
import static util.ItUtils.pluginArtifact;

@@ -50,10 +52,11 @@ public class SystemStateTest {

@Rule
public TemporaryFolder temp = new TemporaryFolder();

@Rule
public TestRule safeguard = new DisableOnDebug(Timeout.seconds(300));

private final String systemPassCode = RandomStringUtils.randomAlphanumeric(15);

@Test
public void test_status_and_health_during_server_lifecycle() throws Exception {
try (Commander commander = new Commander()) {
@@ -130,6 +133,7 @@ public class SystemStateTest {
.setServerProperty("sonar.web.startupLock.path", lock.webFile.getAbsolutePath())
.setServerProperty("sonar.ce.startupLock.path", lock.ceFile.getAbsolutePath())
.setServerProperty("sonar.search.httpPort", "" + esHttpPort)
.setServerProperty("sonar.web.systemPasscode", systemPassCode)
.build();
elasticsearch = new Elasticsearch(esHttpPort);

@@ -185,7 +189,7 @@ public class SystemStateTest {

Optional<WsSystem.HealthResponse> healthResponse() {
if (orchestrator.getServer() != null) {
WsClient wsClient = newWsClient(orchestrator);
WsClient wsClient = newSystemUserWsClient(orchestrator, systemPassCode);
try {
return Optional.of(wsClient.system().health());
} catch (Exception e) {
@@ -203,10 +207,11 @@ public class SystemStateTest {
void verifyHealth(WsSystem.Health expectedHealth, String... expectedMessages) {
WsSystem.HealthResponse response = healthResponse().get();
assertThat(response.getHealth())
.as(response.getCausesList().stream().map(WsSystem.Cause::getMessage).collect(joining(",")))
.describedAs("Expected status %s in response %s", expectedHealth, response)
.isEqualTo(expectedHealth);
assertThat(response.getCausesList())
.extracting(WsSystem.Cause::getMessage)
.describedAs("Expected causes %s in response %s", Arrays.asList(expectedMessages), response)
.containsExactlyInAnyOrder(expectedMessages);
}


+ 12
- 0
tests/src/test/java/util/ItUtils.java View File

@@ -129,6 +129,18 @@ public class ItUtils {
.build());
}

/**
* @deprecated replaced by {@link Tester#wsClient()}
*/
@Deprecated
public static WsClient newSystemUserWsClient(Orchestrator orchestrator, @Nullable String systemPassCode) {
Server server = orchestrator.getServer();
return WsClientFactories.getDefault().newClient(HttpConnector.newBuilder()
.url(server.getUrl())
.systemPassCode(systemPassCode)
.build());
}

/**
* Locate the directory of sample project
*

Loading…
Cancel
Save