Browse Source

SONAR-12000 add the signature to HTTP headers

tags/7.8
Simon Brandhof 5 years ago
parent
commit
1914f2976d

+ 2
- 1
server/sonar-server-common/src/main/java/org/sonar/server/webhook/WebHooksImpl.java View File

@@ -91,7 +91,8 @@ public class WebHooksImpl implements WebHooks {
@Override
public void sendProjectAnalysisUpdate(Analysis analysis, Supplier<WebhookPayload> payloadSupplier) {
List<Webhook> webhooks = readWebHooksFrom(analysis.getProjectUuid())
.map(dto -> new Webhook(dto.getUuid(), analysis.getProjectUuid(), analysis.getCeTaskUuid(), analysis.getAnalysisUuid(), dto.getName(), dto.getUrl()))
.map(dto -> new Webhook(dto.getUuid(), analysis.getProjectUuid(), analysis.getCeTaskUuid(), analysis.getAnalysisUuid(),
dto.getName(), dto.getUrl(), dto.getSecret()))
.collect(MoreCollectors.toList());
if (webhooks.isEmpty()) {
return;

+ 9
- 1
server/sonar-server-common/src/main/java/org/sonar/server/webhook/Webhook.java View File

@@ -37,14 +37,18 @@ public class Webhook {
private final String analysisUuid;
private final String name;
private final String url;
@Nullable
private final String secret;

public Webhook(String uuid, String componentUuid, @Nullable String ceTaskUuid, @Nullable String analysisUuid, String name, String url) {
public Webhook(String uuid, String componentUuid, @Nullable String ceTaskUuid,
@Nullable String analysisUuid, String name, String url, @Nullable String secret) {
this.uuid = uuid;
this.componentUuid = requireNonNull(componentUuid);
this.ceTaskUuid = ceTaskUuid;
this.analysisUuid = analysisUuid;
this.name = requireNonNull(name);
this.url = requireNonNull(url);
this.secret = secret;
}

public String getComponentUuid() {
@@ -70,4 +74,8 @@ public class Webhook {
public Optional<String> getAnalysisUuid() {
return ofNullable(analysisUuid);
}

public Optional<String> getSecret() {
return ofNullable(secret);
}
}

+ 11
- 2
server/sonar-server-common/src/main/java/org/sonar/server/webhook/WebhookCallerImpl.java View File

@@ -20,6 +20,7 @@
package org.sonar.server.webhook;

import java.io.IOException;
import java.util.Optional;
import okhttp3.Credentials;
import okhttp3.HttpUrl;
import okhttp3.MediaType;
@@ -27,6 +28,8 @@ import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import org.apache.commons.codec.digest.HmacAlgorithms;
import org.apache.commons.codec.digest.HmacUtils;
import org.sonar.api.ce.ComputeEngineSide;
import org.sonar.api.server.ServerSide;
import org.sonar.api.utils.System2;
@@ -69,7 +72,7 @@ public class WebhookCallerImpl implements WebhookCaller {
throw new IllegalArgumentException("Webhook URL is not valid: " + webhook.getUrl());
}
builder.setEffectiveUrl(HttpUrlHelper.obfuscateCredentials(webhook.getUrl(), url));
Request request = buildHttpRequest(url, payload);
Request request = buildHttpRequest(url, webhook, payload);
try (Response response = execute(request)) {
builder.setHttpStatus(response.code());
}
@@ -82,13 +85,14 @@ public class WebhookCallerImpl implements WebhookCaller {
.build();
}

private static Request buildHttpRequest(HttpUrl url, WebhookPayload payload) {
private static Request buildHttpRequest(HttpUrl url, Webhook webhook, WebhookPayload payload) {
Request.Builder request = new Request.Builder();
request.url(url);
request.header(PROJECT_KEY_HEADER, payload.getProjectKey());
if (isNotEmpty(url.username())) {
request.header("Authorization", Credentials.basic(url.username(), url.password(), UTF_8));
}
signatureOf(webhook, payload).ifPresent(signature -> request.header("X-Sonar-Webhook-HMAC-SHA256", signature));

RequestBody body = RequestBody.create(JSON, payload.getJson());
request.post(body);
@@ -141,4 +145,9 @@ public class WebhookCallerImpl implements WebhookCaller {
.followSslRedirects(false)
.build();
}

private static Optional<String> signatureOf(Webhook webhook, WebhookPayload payload) {
return webhook.getSecret()
.map(secret -> new HmacUtils(HmacAlgorithms.HMAC_SHA_256, secret).hmacHex(payload.getJson()));
}
}

+ 31
- 10
server/sonar-server-common/src/test/java/org/sonar/server/webhook/WebhookCallerImplTest.java View File

@@ -24,7 +24,6 @@ import okhttp3.HttpUrl;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
import org.apache.commons.lang.RandomStringUtils;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.DisableOnDebug;
@@ -39,6 +38,7 @@ import org.sonar.api.utils.Version;
import org.sonar.api.utils.internal.TestSystem2;
import org.sonar.server.util.OkHttpClientProvider;

import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
import static org.assertj.core.api.Assertions.assertThat;

public class WebhookCallerImplTest {
@@ -59,8 +59,9 @@ public class WebhookCallerImplTest {
private System2 system = new TestSystem2().setNow(NOW);

@Test
public void send_posts_payload_to_http_server() throws Exception {
Webhook webhook = new Webhook(WEBHOOK_UUID, PROJECT_UUID, CE_TASK_UUID, RandomStringUtils.randomAlphanumeric(40),"my-webhook", server.url("/ping").toString());
public void post_payload_to_http_server() throws Exception {
Webhook webhook = new Webhook(WEBHOOK_UUID, PROJECT_UUID, CE_TASK_UUID, randomAlphanumeric(40),
"my-webhook", server.url("/ping").toString(), null);

server.enqueue(new MockResponse().setBody("pong").setResponseCode(201));
WebhookDelivery delivery = newSender().call(webhook, PAYLOAD);
@@ -80,11 +81,25 @@ public class WebhookCallerImplTest {
assertThat(recordedRequest.getHeader("User-Agent")).isEqualTo("SonarQube/6.2");
assertThat(recordedRequest.getHeader("Content-Type")).isEqualTo("application/json; charset=utf-8");
assertThat(recordedRequest.getHeader("X-SonarQube-Project")).isEqualTo(PAYLOAD.getProjectKey());
assertThat(recordedRequest.getHeader("X-Sonar-Webhook-HMAC-SHA256")).isNull();
}

@Test
public void sign_payload_if_secret_is_set() throws Exception {
Webhook webhook = new Webhook(WEBHOOK_UUID, PROJECT_UUID, CE_TASK_UUID, randomAlphanumeric(40),
"my-webhook", server.url("/ping").toString(), "my_secret");
server.enqueue(new MockResponse().setBody("pong").setResponseCode(201));

newSender().call(webhook, PAYLOAD);

RecordedRequest recordedRequest = server.takeRequest();
assertThat(recordedRequest.getHeader("X-Sonar-Webhook-HMAC-SHA256")).isEqualTo("ef35d3420a3df3d05f8f7eb3b53384abc41395f164245d6c7e78a70e61703dde");
}

@Test
public void silently_catch_error_when_external_server_does_not_answer() throws Exception {
Webhook webhook = new Webhook(WEBHOOK_UUID, PROJECT_UUID, CE_TASK_UUID, RandomStringUtils.randomAlphanumeric(40),"my-webhook", server.url("/ping").toString());
Webhook webhook = new Webhook(WEBHOOK_UUID, PROJECT_UUID, CE_TASK_UUID,
randomAlphanumeric(40), "my-webhook", server.url("/ping").toString(), null);

server.shutdown();
WebhookDelivery delivery = newSender().call(webhook, PAYLOAD);
@@ -100,7 +115,8 @@ public class WebhookCallerImplTest {

@Test
public void silently_catch_error_when_url_is_incorrect() {
Webhook webhook = new Webhook(WEBHOOK_UUID, PROJECT_UUID, CE_TASK_UUID, RandomStringUtils.randomAlphanumeric(40),"my-webhook", "this_is_not_an_url");
Webhook webhook = new Webhook(WEBHOOK_UUID, PROJECT_UUID, CE_TASK_UUID,
randomAlphanumeric(40), "my-webhook", "this_is_not_an_url", null);

WebhookDelivery delivery = newSender().call(webhook, PAYLOAD);

@@ -118,7 +134,8 @@ public class WebhookCallerImplTest {
*/
@Test
public void redirects_should_be_followed_with_POST_method() throws Exception {
Webhook webhook = new Webhook(WEBHOOK_UUID, PROJECT_UUID, CE_TASK_UUID, RandomStringUtils.randomAlphanumeric(40),"my-webhook", server.url("/redirect").toString());
Webhook webhook = new Webhook(WEBHOOK_UUID, PROJECT_UUID, CE_TASK_UUID,
randomAlphanumeric(40), "my-webhook", server.url("/redirect").toString(), null);

// /redirect redirects to /target
server.enqueue(new MockResponse().setResponseCode(307).setHeader("Location", server.url("target")));
@@ -140,7 +157,8 @@ public class WebhookCallerImplTest {
@Test
public void credentials_are_propagated_to_POST_redirects() throws Exception {
HttpUrl url = server.url("/redirect").newBuilder().username("theLogin").password("thePassword").build();
Webhook webhook = new Webhook(WEBHOOK_UUID, PROJECT_UUID, CE_TASK_UUID, RandomStringUtils.randomAlphanumeric(40),"my-webhook", url.toString());
Webhook webhook = new Webhook(WEBHOOK_UUID, PROJECT_UUID, CE_TASK_UUID,
randomAlphanumeric(40), "my-webhook", url.toString(), null);

// /redirect redirects to /target
server.enqueue(new MockResponse().setResponseCode(307).setHeader("Location", server.url("target")));
@@ -160,7 +178,8 @@ public class WebhookCallerImplTest {
@Test
public void redirects_throws_ISE_if_header_Location_is_missing() {
HttpUrl url = server.url("/redirect");
Webhook webhook = new Webhook(WEBHOOK_UUID, PROJECT_UUID, CE_TASK_UUID, RandomStringUtils.randomAlphanumeric(40),"my-webhook", url.toString());
Webhook webhook = new Webhook(WEBHOOK_UUID, PROJECT_UUID, CE_TASK_UUID,
randomAlphanumeric(40), "my-webhook", url.toString(), null);

server.enqueue(new MockResponse().setResponseCode(307));

@@ -175,7 +194,8 @@ public class WebhookCallerImplTest {
@Test
public void redirects_throws_ISE_if_header_Location_does_not_relate_to_a_supported_protocol() {
HttpUrl url = server.url("/redirect");
Webhook webhook = new Webhook(WEBHOOK_UUID, PROJECT_UUID, CE_TASK_UUID, RandomStringUtils.randomAlphanumeric(40),"my-webhook", url.toString());
Webhook webhook = new Webhook(WEBHOOK_UUID, PROJECT_UUID, CE_TASK_UUID,
randomAlphanumeric(40), "my-webhook", url.toString(), null);

server.enqueue(new MockResponse().setResponseCode(307).setHeader("Location", "ftp://foo"));

@@ -190,7 +210,8 @@ public class WebhookCallerImplTest {
@Test
public void send_basic_authentication_header_if_url_contains_credentials() throws Exception {
HttpUrl url = server.url("/ping").newBuilder().username("theLogin").password("thePassword").build();
Webhook webhook = new Webhook(WEBHOOK_UUID, PROJECT_UUID, CE_TASK_UUID, RandomStringUtils.randomAlphanumeric(40),"my-webhook", url.toString());
Webhook webhook = new Webhook(WEBHOOK_UUID, PROJECT_UUID, CE_TASK_UUID,
randomAlphanumeric(40), "my-webhook", url.toString(), null);
server.enqueue(new MockResponse().setBody("pong"));

WebhookDelivery delivery = newSender().call(webhook, PAYLOAD);

+ 1
- 1
server/sonar-server-common/src/test/java/org/sonar/server/webhook/WebhookDeliveryStorageTest.java View File

@@ -118,7 +118,7 @@ public class WebhookDeliveryStorageTest {

private static WebhookDelivery.Builder newBuilderTemplate() {
return new WebhookDelivery.Builder()
.setWebhook(new Webhook("WEBHOOK_UUID_1", "COMPONENT1", "TASK1", RandomStringUtils.randomAlphanumeric(40),"Jenkins", "http://jenkins"))
.setWebhook(new Webhook("WEBHOOK_UUID_1", "COMPONENT1", "TASK1", RandomStringUtils.randomAlphanumeric(40),"Jenkins", "http://jenkins", null))
.setPayload(new WebhookPayload("my-project", "{json}"))
.setAt(1_000_000L)
.setHttpStatus(200)

+ 20
- 45
server/sonar-server-common/src/test/java/org/sonar/server/webhook/WebhookTest.java View File

@@ -19,60 +19,35 @@
*/
package org.sonar.server.webhook;

import java.util.Optional;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;

import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
import static org.assertj.core.api.Assertions.assertThat;

public class WebhookTest {

@Rule
public ExpectedException expectedException = ExpectedException.none();

@Test
public void constructor_with_null_componentUuid_should_throw_NPE() {
expectedException.expect(NullPointerException.class);

new Webhook(randomAlphanumeric(40), null, null, null, randomAlphanumeric(10), randomAlphanumeric(10));
}

@Test
public void constructor_with_null_name_should_throw_NPE() {
expectedException.expect(NullPointerException.class);

new Webhook(randomAlphanumeric(40), randomAlphanumeric(10), null, null, null, randomAlphanumeric(10));
}

@Test
public void constructor_with_null_url_should_throw_NPE() {
expectedException.expect(NullPointerException.class);

new Webhook(randomAlphanumeric(40), randomAlphanumeric(10), null, null, randomAlphanumeric(10), null);
public void webhook_with_only_required_fields() {
Webhook underTest = new Webhook("a_uuid", "a_component_uuid", null, null, "a_name", "an_url", null);

assertThat(underTest.getUuid()).isEqualTo("a_uuid");
assertThat(underTest.getComponentUuid()).isEqualTo("a_component_uuid");
assertThat(underTest.getCeTaskUuid()).isEmpty();
assertThat(underTest.getAnalysisUuid()).isEmpty();
assertThat(underTest.getName()).isEqualTo("a_name");
assertThat(underTest.getUrl()).isEqualTo("an_url");
assertThat(underTest.getSecret()).isEmpty();
}

@Test
public void constructor_with_null_ceTaskUuid_or_analysisUuidurl_should_return_Optional_empty() {
String componentUuid = randomAlphanumeric(10);
String name = randomAlphanumeric(10);
String url = randomAlphanumeric(10);
Webhook underTest = new Webhook(randomAlphanumeric(40), componentUuid, null, null, name, url);

assertThat(underTest.getComponentUuid()).isEqualTo(componentUuid);
assertThat(underTest.getName()).isEqualTo(name);
assertThat(underTest.getUrl()).isEqualTo(url);
assertThat(underTest.getCeTaskUuid()).isEqualTo(Optional.empty());
assertThat(underTest.getAnalysisUuid()).isEqualTo(Optional.empty());

String ceTaskUuid = randomAlphanumeric(10);
String analysisUuid = randomAlphanumeric(10);
underTest = new Webhook(randomAlphanumeric(40), componentUuid, ceTaskUuid, analysisUuid, name, url);
assertThat(underTest.getComponentUuid()).isEqualTo(componentUuid);
assertThat(underTest.getName()).isEqualTo(name);
assertThat(underTest.getUrl()).isEqualTo(url);
assertThat(underTest.getCeTaskUuid().get()).isEqualTo(ceTaskUuid);
assertThat(underTest.getAnalysisUuid().get()).isEqualTo(analysisUuid);
public void webhook_with_all_fields() {
Webhook underTest = new Webhook("a_uuid", "a_component_uuid", "a_task_uuid", "an_analysis", "a_name", "an_url", "a_secret");

assertThat(underTest.getUuid()).isEqualTo("a_uuid");
assertThat(underTest.getComponentUuid()).isEqualTo("a_component_uuid");
assertThat(underTest.getCeTaskUuid()).hasValue("a_task_uuid");
assertThat(underTest.getAnalysisUuid()).hasValue("an_analysis");
assertThat(underTest.getName()).isEqualTo("a_name");
assertThat(underTest.getUrl()).isEqualTo("an_url");
assertThat(underTest.getSecret()).hasValue("a_secret");
}
}

Loading…
Cancel
Save