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