diff options
10 files changed, 406 insertions, 3 deletions
diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel1.java b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel1.java index 6de6ea8288a..4f67d5f0bb3 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel1.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel1.java @@ -53,6 +53,7 @@ import org.sonar.server.platform.db.EmbeddedDatabaseFactory; import org.sonar.server.rule.index.RuleIndex; import org.sonar.server.search.EsSearchModule; import org.sonar.server.setting.ThreadLocalSettings; +import org.sonar.server.user.SystemPasscodeImpl; import org.sonar.server.user.ThreadLocalUserSession; import org.sonar.server.util.OkHttpClientProvider; @@ -101,6 +102,7 @@ public class PlatformLevel1 extends PlatformLevel { // user session ThreadLocalUserSession.class, + SystemPasscodeImpl.class, // DB DBSessionsImpl.class, diff --git a/server/sonar-server/src/main/java/org/sonar/server/user/SystemPasscode.java b/server/sonar-server/src/main/java/org/sonar/server/user/SystemPasscode.java new file mode 100644 index 00000000000..4174446eb69 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/user/SystemPasscode.java @@ -0,0 +1,43 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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.user; + +import org.sonar.api.server.ws.Request; + +/** + * Passcode for accessing some web services, usually for connecting + * monitoring tools without using the credentials + * of a system administrator. + */ +public interface SystemPasscode { + + /** + * Whether the system passcode is configured in sonar.properties or not. + * By default passcode is not defined and {@code false} is returned. + */ + boolean isConfigured(); + + /** + * Whether the configured system passcode is provided by the HTTP request or not. + * Returns {@code false} if {@link #isConfigured()} is {@code false}. + */ + boolean isValid(Request request); + +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/user/SystemPasscodeImpl.java b/server/sonar-server/src/main/java/org/sonar/server/user/SystemPasscodeImpl.java new file mode 100644 index 00000000000..5a04178479b --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/user/SystemPasscodeImpl.java @@ -0,0 +1,81 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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.user; + +import java.util.Optional; +import org.apache.commons.lang.StringUtils; +import org.sonar.api.Startable; +import org.sonar.api.config.Configuration; +import org.sonar.api.server.ServerSide; +import org.sonar.api.server.ws.Request; +import org.sonar.api.utils.log.Loggers; + +@ServerSide +public class SystemPasscodeImpl implements SystemPasscode, Startable { + + public static final String PASSCODE_HTTP_HEADER = "X-Sonar-Passcode"; + public static final String PASSCODE_CONF_PROPERTY = "sonar.web.systemPasscode"; + + private final Configuration configuration; + private String configuredPasscode; + + public SystemPasscodeImpl(Configuration configuration) { + this.configuration = configuration; + } + + @Override + public boolean isConfigured() { + return configuredPasscode != null; + } + + @Override + public boolean isValid(Request request) { + if (configuredPasscode == null) { + return false; + } + return request.header(PASSCODE_HTTP_HEADER) + .map(s -> configuredPasscode.equals(s)) + .orElse(false); + } + + @Override + public void start() { + Optional<String> passcodeOpt = configuration.get(PASSCODE_CONF_PROPERTY) + // if present, result is never empty string + .map(StringUtils::trimToNull); + + if (passcodeOpt.isPresent()) { + logState("enabled"); + configuredPasscode = passcodeOpt.get(); + } else { + logState("disabled"); + configuredPasscode = null; + } + } + + private void logState(String state) { + Loggers.get(getClass()).info("System authentication by passcode is {}", state); + } + + @Override + public void stop() { + // nothing to do + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookCallerImplTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookCallerImplTest.java index 5685f9fd064..aff151adb48 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookCallerImplTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookCallerImplTest.java @@ -62,7 +62,7 @@ public class WebhookCallerImplTest { server.enqueue(new MockResponse().setBody("pong").setResponseCode(201)); WebhookDelivery delivery = newSender().call(webhook, PAYLOAD); - assertThat(delivery.getHttpStatus().get()).isEqualTo(201); + assertThat(delivery.getHttpStatus()).hasValue(201); assertThat(delivery.getDurationInMs().get()).isGreaterThanOrEqualTo(0); assertThat(delivery.getError()).isEmpty(); assertThat(delivery.getAt()).isEqualTo(NOW); diff --git a/server/sonar-server/src/test/java/org/sonar/server/user/SystemPasscodeImplTest.java b/server/sonar-server/src/test/java/org/sonar/server/user/SystemPasscodeImplTest.java new file mode 100644 index 00000000000..9c37c5467cc --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/user/SystemPasscodeImplTest.java @@ -0,0 +1,123 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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.user; + +import org.junit.After; +import org.junit.Rule; +import org.junit.Test; +import org.sonar.api.config.internal.MapSettings; +import org.sonar.api.utils.log.LogTester; +import org.sonar.api.utils.log.LoggerLevel; +import org.sonar.server.ws.TestRequest; + +import static org.assertj.core.api.Assertions.assertThat; + +public class SystemPasscodeImplTest { + + @Rule + public LogTester logTester = new LogTester(); + + private MapSettings settings = new MapSettings(); + private SystemPasscodeImpl underTest = new SystemPasscodeImpl(settings.asConfig()); + + @After + public void tearDown() { + underTest.stop(); + } + + @Test + public void isConfigured_is_true_if_property_is_not_blank() { + verifyIsConfigured("foo", true); + } + + @Test + public void isConfigured_is_false_if_property_value_is_blank() { + verifyIsConfigured(" ", false); + } + + @Test + public void isConfigured_is_false_if_property_value_is_empty() { + verifyIsConfigured("", false); + } + + @Test + public void isConfigured_is_false_if_property_is_not_defined() { + assertThat(underTest.isConfigured()).isFalse(); + } + + @Test + public void startup_logs_show_that_feature_is_enabled() { + configurePasscode("foo"); + underTest.start(); + + assertThat(logTester.logs(LoggerLevel.INFO)).contains("System authentication by passcode is enabled"); + } + + @Test + public void startup_logs_show_that_feature_is_disabled() { + underTest.start(); + + assertThat(logTester.logs(LoggerLevel.INFO)).contains("System authentication by passcode is disabled"); + } + + @Test + public void isValid_is_true_if_request_header_matches_configured_passcode() { + verifyIsValid(true, "foo", "foo"); + } + + @Test + public void isValid_is_false_if_request_header_matches_configured_passcode_with_different_case() { + verifyIsValid(false, "foo", "FOO"); + } + + @Test + public void isValid_is_false_if_request_header_does_not_match_configured_passcode() { + verifyIsValid(false, "foo", "bar"); + } + + @Test + public void isValid_is_false_if_request_header_is_defined_but_passcode_is_not_configured() { + verifyIsValid(false, null, "foo"); + } + + @Test + public void isValid_is_false_if_request_header_is_empty() { + verifyIsValid(false, "foo", ""); + } + + private void verifyIsValid(boolean expectedResult, String configuredPasscode, String header) { + configurePasscode(configuredPasscode); + + TestRequest request = new TestRequest(); + request.setHeader("X-Sonar-Passcode", header); + + assertThat(underTest.isValid(request)).isEqualTo(expectedResult); + } + + private void verifyIsConfigured(String propertyValue, boolean expectedResult) { + configurePasscode(propertyValue); + assertThat(underTest.isConfigured()).isEqualTo(expectedResult); + } + + private void configurePasscode(String propertyValue) { + settings.setProperty("sonar.web.systemPasscode", propertyValue); + underTest.start(); + } +} diff --git a/sonar-application/src/main/assembly/conf/sonar.properties b/sonar-application/src/main/assembly/conf/sonar.properties index fdc340fbb6b..a970e7b8010 100644 --- a/sonar-application/src/main/assembly/conf/sonar.properties +++ b/sonar-application/src/main/assembly/conf/sonar.properties @@ -137,6 +137,13 @@ # and cannot be greater than 3 months. Value must be strictly positive. #sonar.web.sessionTimeoutInMinutes=4320 +# A passcode can be defined to access some web services from monitoring +# tools without having to use the credentials of a system administrator. +# Check the Web API documentation to know which web services are supporting this authentication mode. +# The passcode should be provided in HTTP requests with the header "X-Sonar-Passcode". +# By default feature is disabled. +#sonar.web.systemPasscode= + #-------------------------------------------------------------------------------------------------- # SSO AUTHENTICATION diff --git a/tests/plugins/fake-governance-plugin/src/main/java/FakeGovernancePlugin.java b/tests/plugins/fake-governance-plugin/src/main/java/FakeGovernancePlugin.java index 9460af58547..19f6fdf68c3 100644 --- a/tests/plugins/fake-governance-plugin/src/main/java/FakeGovernancePlugin.java +++ b/tests/plugins/fake-governance-plugin/src/main/java/FakeGovernancePlugin.java @@ -1,4 +1,3 @@ - /* * SonarQube * Copyright (C) 2009-2017 SonarSource SA @@ -20,6 +19,7 @@ */ import org.sonar.api.Plugin; +import systemPasscode.SystemPasscodeWebService; import workerCount.FakeWorkerCountProviderImpl; import workerCount.RefreshWorkerCountAction; import workerlatch.LatchControllerWorkerMeasureComputer; @@ -35,6 +35,7 @@ public class FakeGovernancePlugin implements Plugin { context.addExtension(WorkerLatchMetrics.class); context.addExtension(LatchControllerWorkerMeasureComputer.class); context.addExtension(RefreshWorkerCountAction.class); + context.addExtension(SystemPasscodeWebService.class); } } diff --git a/tests/plugins/fake-governance-plugin/src/main/java/systemPasscode/SystemPasscodeWebService.java b/tests/plugins/fake-governance-plugin/src/main/java/systemPasscode/SystemPasscodeWebService.java new file mode 100644 index 00000000000..cdeadf75de0 --- /dev/null +++ b/tests/plugins/fake-governance-plugin/src/main/java/systemPasscode/SystemPasscodeWebService.java @@ -0,0 +1,49 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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 systemPasscode; + +import java.net.HttpURLConnection; +import org.sonar.api.server.ws.Request; +import org.sonar.api.server.ws.RequestHandler; +import org.sonar.api.server.ws.Response; +import org.sonar.api.server.ws.WebService; +import org.sonar.server.user.SystemPasscode; + +public class SystemPasscodeWebService implements WebService, RequestHandler { + private final SystemPasscode passcode; + + public SystemPasscodeWebService(SystemPasscode passcode) { + this.passcode = passcode; + } + + @Override + public void define(Context context) { + NewController controller = context.createController("api/system_passcode"); + controller.createAction("check").setHandler(this); + controller.done(); + } + + @Override + public void handle(Request request, Response response) throws Exception { + if (!passcode.isValid(request)) { + response.stream().setStatus(HttpURLConnection.HTTP_UNAUTHORIZED); + } + } +} diff --git a/tests/src/test/java/org/sonarqube/tests/Category5Suite.java b/tests/src/test/java/org/sonarqube/tests/Category5Suite.java index 298d51dcabe..951185485a8 100644 --- a/tests/src/test/java/org/sonarqube/tests/Category5Suite.java +++ b/tests/src/test/java/org/sonarqube/tests/Category5Suite.java @@ -21,6 +21,7 @@ package org.sonarqube.tests; import org.junit.runner.RunWith; import org.junit.runners.Suite; +import org.sonarqube.tests.authorisation.SystemPasscodeTest; import org.sonarqube.tests.ce.CeShutdownTest; import org.sonarqube.tests.ce.CeWorkersTest; import org.sonarqube.tests.cluster.ClusterTest; @@ -73,7 +74,9 @@ import org.sonarqube.tests.user.UserEsResilienceTest; IssueCreationDatePluginChangedTest.class, // elasticsearch - ElasticsearchSettingsTest.class + ElasticsearchSettingsTest.class, + + SystemPasscodeTest.class }) public class Category5Suite { diff --git a/tests/src/test/java/org/sonarqube/tests/authorisation/SystemPasscodeTest.java b/tests/src/test/java/org/sonarqube/tests/authorisation/SystemPasscodeTest.java new file mode 100644 index 00000000000..32fa0cba328 --- /dev/null +++ b/tests/src/test/java/org/sonarqube/tests/authorisation/SystemPasscodeTest.java @@ -0,0 +1,94 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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.sonarqube.tests.authorisation; + +import com.sonar.orchestrator.Orchestrator; +import com.sonar.orchestrator.OrchestratorBuilder; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.sonarqube.tests.Tester; +import org.sonarqube.ws.client.GetRequest; +import org.sonarqube.ws.client.WsRequest; +import org.sonarqube.ws.client.WsResponse; + +import static org.assertj.core.api.Assertions.assertThat; +import static util.ItUtils.pluginArtifact; + +public class SystemPasscodeTest { + + private static final String VALID_PASSCODE = "123456"; + private static final String INVALID_PASSCODE = "not" + VALID_PASSCODE; + private static final String PASSCODE_HEADER = "X-Sonar-Passcode"; + + private static Orchestrator orchestrator; + + @BeforeClass + public static void setUp() throws Exception { + OrchestratorBuilder builder = Orchestrator.builderEnv() + // this privileged plugin provides the WS api/system_passcode/check + // that is used by the tests + .addPlugin(pluginArtifact("fake-governance-plugin")) + .setServerProperty("sonar.web.systemPasscode", VALID_PASSCODE); + orchestrator = builder.build(); + orchestrator.start(); + } + + @AfterClass + public static void stop() { + if (orchestrator != null) { + orchestrator.stop(); + } + } + + @Rule + public Tester tester = new Tester(orchestrator); + + @Test + public void system_access_is_granted_if_valid_passcode_is_sent_through_http_header() { + WsRequest request = newRequest() + .setHeader(PASSCODE_HEADER, VALID_PASSCODE); + + WsResponse response = tester.asAnonymous().wsClient().wsConnector().call(request); + assertThat(response.code()).isEqualTo(200); + } + + @Test + public void system_access_is_rejected_if_invalid_passcode_is_sent_through_http_header() { + WsRequest request = newRequest() + .setHeader(PASSCODE_HEADER, INVALID_PASSCODE); + + WsResponse response = tester.asAnonymous().wsClient().wsConnector().call(request); + assertThat(response.code()).isEqualTo(401); + } + + @Test + public void system_access_is_rejected_if_passcode_is_not_sent() { + WsRequest request = newRequest(); + + WsResponse response = tester.asAnonymous().wsClient().wsConnector().call(request); + assertThat(response.code()).isEqualTo(401); + } + + private static GetRequest newRequest() { + return new GetRequest("api/system_passcode/check"); + } +} |