Skip to content

Commit

Permalink
SONAR-12000 add secret to WS api/webhooks/create and api/webhooks/update
Browse files Browse the repository at this point in the history
  • Loading branch information
Simon Brandhof authored and SonarTech committed Apr 29, 2019
1 parent 1914f29 commit d79cd20
Show file tree
Hide file tree
Showing 11 changed files with 193 additions and 145 deletions.
Expand Up @@ -32,19 +32,19 @@
import org.sonar.db.webhook.WebhookDto;
import org.sonar.server.organization.DefaultOrganizationProvider;
import org.sonar.server.user.UserSession;
import org.sonarqube.ws.Webhooks;

import static com.google.common.base.Preconditions.checkState;
import static java.lang.String.format;
import static java.util.Optional.ofNullable;
import static org.apache.commons.lang.StringUtils.isNotBlank;
import static org.sonar.server.webhook.ws.WebhooksWsParameters.ACTION_CREATE;
import static org.sonar.server.webhook.ws.WebhooksWsParameters.NAME_PARAM;
import static org.sonar.server.webhook.ws.WebhooksWsParameters.NAME_PARAM_MAXIMUM_LENGTH;
import static org.sonar.server.webhook.ws.WebhooksWsParameters.ORGANIZATION_KEY_PARAM;
import static org.sonar.server.webhook.ws.WebhooksWsParameters.ORGANIZATION_KEY_PARAM_MAXIMUM_LENGTH;
import static org.sonar.server.webhook.ws.WebhooksWsParameters.PROJECT_KEY_PARAM;
import static org.sonar.server.webhook.ws.WebhooksWsParameters.PROJECT_KEY_PARAM_MAXIMUN_LENGTH;
import static org.sonar.server.webhook.ws.WebhooksWsParameters.PROJECT_KEY_PARAM_MAXIMUM_LENGTH;
import static org.sonar.server.webhook.ws.WebhooksWsParameters.SECRET_PARAM;
import static org.sonar.server.webhook.ws.WebhooksWsParameters.SECRET_PARAM_MAXIMUM_LENGTH;
import static org.sonar.server.webhook.ws.WebhooksWsParameters.URL_PARAM;
import static org.sonar.server.webhook.ws.WebhooksWsParameters.URL_PARAM_MAXIMUM_LENGTH;
import static org.sonar.server.ws.KeyExamples.KEY_ORG_EXAMPLE_001;
Expand All @@ -68,7 +68,7 @@ public class CreateAction implements WebhooksWsAction {
private final WebhookSupport webhookSupport;

public CreateAction(DbClient dbClient, UserSession userSession, DefaultOrganizationProvider defaultOrganizationProvider,
UuidFactory uuidFactory, WebhookSupport webhookSupport) {
UuidFactory uuidFactory, WebhookSupport webhookSupport) {
this.dbClient = dbClient;
this.userSession = userSession;
this.defaultOrganizationProvider = defaultOrganizationProvider;
Expand All @@ -78,7 +78,6 @@ public CreateAction(DbClient dbClient, UserSession userSession, DefaultOrganizat

@Override
public void define(WebService.NewController controller) {

WebService.NewAction action = controller.createAction(ACTION_CREATE)
.setPost(true)
.setDescription("Create a Webhook.<br>" +
Expand All @@ -103,7 +102,7 @@ public void define(WebService.NewController controller) {

action.createParam(PROJECT_KEY_PARAM)
.setRequired(false)
.setMaximumLength(PROJECT_KEY_PARAM_MAXIMUN_LENGTH)
.setMaximumLength(PROJECT_KEY_PARAM_MAXIMUM_LENGTH)
.setDescription("The key of the project that will own the webhook")
.setExampleValue(KEY_PROJECT_EXAMPLE_001);

Expand All @@ -114,20 +113,26 @@ public void define(WebService.NewController controller) {
.setDescription("The key of the organization that will own the webhook")
.setExampleValue(KEY_ORG_EXAMPLE_001);

action.createParam(SECRET_PARAM)
.setRequired(false)
.setMinimumLength(1)
.setMaximumLength(SECRET_PARAM_MAXIMUM_LENGTH)
.setDescription("If provided, secret will be used as the key to generate the HMAC hex (lowercase) digest value in the 'X-Sonar-Webhook-HMAC-SHA256' header")
.setExampleValue("your_secret")
.setSince("7.8");
}

@Override
public void handle(Request request, Response response) throws Exception {

userSession.checkLoggedIn();

String name = request.mandatoryParam(NAME_PARAM);
String url = request.mandatoryParam(URL_PARAM);
String projectKey = request.param(PROJECT_KEY_PARAM);
String organizationKey = request.param(ORGANIZATION_KEY_PARAM);
String secret = request.param(SECRET_PARAM);

try (DbSession dbSession = dbClient.openSession(false)) {

OrganizationDto organizationDto;
if (isNotBlank(organizationKey)) {
Optional<OrganizationDto> dtoOptional = dbClient.organizationDao().selectByKey(dbSession, organizationKey);
Expand All @@ -138,7 +143,7 @@ public void handle(Request request, Response response) throws Exception {

ComponentDto projectDto = null;
if (isNotBlank(projectKey)) {
Optional<ComponentDto> dtoOptional = ofNullable(dbClient.componentDao().selectByKey(dbSession, projectKey).orElse(null));
Optional<ComponentDto> dtoOptional = dbClient.componentDao().selectByKey(dbSession, projectKey);
ComponentDto componentDto = checkFoundWithOptional(dtoOptional, "No project with key '%s'", projectKey);
webhookSupport.checkThatProjectBelongsToOrganization(componentDto, organizationDto, "Project '%s' does not belong to organisation '%s'", projectKey, organizationKey);
webhookSupport.checkPermission(componentDto);
Expand All @@ -149,26 +154,27 @@ public void handle(Request request, Response response) throws Exception {

webhookSupport.checkUrlPattern(url, "Url parameter with value '%s' is not a valid url", url);

WebhookDto webhookDto = doHandle(dbSession, organizationDto, projectDto, name, url);

dbClient.webhookDao().insert(dbSession, webhookDto);
WebhookDto dto = doHandle(dbSession, organizationDto, projectDto, name, url, secret);
dbClient.webhookDao().insert(dbSession, dto);

dbSession.commit();

writeResponse(request, response, webhookDto);
writeResponse(request, response, dto);
}

}

private WebhookDto doHandle(DbSession dbSession, @Nullable OrganizationDto organization, @Nullable ComponentDto project, String name, String url) {
private WebhookDto doHandle(DbSession dbSession, @Nullable OrganizationDto organization,
@Nullable ComponentDto project, String name, String url, @Nullable String secret) {

checkState(organization != null || project != null,
"A webhook can not be created if not linked to an organization or a project.");

WebhookDto dto = new WebhookDto()
.setUuid(uuidFactory.create())
.setName(name)
.setUrl(url);
.setUrl(url)
.setSecret(secret);

if (project != null) {
checkNumberOfWebhook(numberOfWebhookOf(dbSession, project), "Maximum number of webhook reached for project '%s'", project.getKey());
Expand All @@ -181,18 +187,20 @@ private WebhookDto doHandle(DbSession dbSession, @Nullable OrganizationDto organ
return dto;
}

private static void writeResponse(Request request, Response response, WebhookDto element) {
Webhooks.CreateWsResponse.Builder responseBuilder = newBuilder();
responseBuilder.setWebhook(Webhook.newBuilder()
.setKey(element.getUuid())
.setName(element.getName())
.setUrl(element.getUrl()));

writeProtobuf(responseBuilder.build(), request, response);
private static void writeResponse(Request request, Response response, WebhookDto dto) {
Webhook.Builder webhookBuilder = Webhook.newBuilder();
webhookBuilder
.setKey(dto.getUuid())
.setName(dto.getName())
.setUrl(dto.getUrl());
if (dto.getSecret() != null) {
webhookBuilder.setSecret(dto.getSecret());
}
writeProtobuf(newBuilder().setWebhook(webhookBuilder).build(), request, response);
}

private static void checkNumberOfWebhook(int nbOfWebhooks, String message, Object... messageArguments) {
if (nbOfWebhooks >= MAX_NUMBER_OF_WEBHOOKS){
if (nbOfWebhooks >= MAX_NUMBER_OF_WEBHOOKS) {
throw new IllegalArgumentException(format(message, messageArguments));
}
}
Expand Down
Expand Up @@ -33,7 +33,7 @@
import static java.util.Optional.ofNullable;
import static org.sonar.server.webhook.ws.WebhooksWsParameters.DELETE_ACTION;
import static org.sonar.server.webhook.ws.WebhooksWsParameters.KEY_PARAM;
import static org.sonar.server.webhook.ws.WebhooksWsParameters.KEY_PARAM_MAXIMUN_LENGTH;
import static org.sonar.server.webhook.ws.WebhooksWsParameters.KEY_PARAM_MAXIMUM_LENGTH;
import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001;
import static org.sonar.server.ws.WsUtils.checkFoundWithOptional;
import static org.sonar.server.ws.WsUtils.checkStateWithOptional;
Expand Down Expand Up @@ -61,7 +61,7 @@ public void define(WebService.NewController controller) {

action.createParam(KEY_PARAM)
.setRequired(true)
.setMaximumLength(KEY_PARAM_MAXIMUN_LENGTH)
.setMaximumLength(KEY_PARAM_MAXIMUM_LENGTH)
.setDescription("The key of the webhook to be deleted, "+
"auto-generated value can be obtained through api/webhooks/create or api/webhooks/list")
.setExampleValue(KEY_PROJECT_EXAMPLE_001);
Expand Down
Expand Up @@ -20,6 +20,7 @@
package org.sonar.server.webhook.ws;

import java.util.Optional;
import javax.annotation.Nullable;
import org.sonar.api.server.ws.Request;
import org.sonar.api.server.ws.Response;
import org.sonar.api.server.ws.WebService;
Expand All @@ -32,9 +33,11 @@

import static java.util.Optional.ofNullable;
import static org.sonar.server.webhook.ws.WebhooksWsParameters.KEY_PARAM;
import static org.sonar.server.webhook.ws.WebhooksWsParameters.KEY_PARAM_MAXIMUN_LENGTH;
import static org.sonar.server.webhook.ws.WebhooksWsParameters.KEY_PARAM_MAXIMUM_LENGTH;
import static org.sonar.server.webhook.ws.WebhooksWsParameters.NAME_PARAM;
import static org.sonar.server.webhook.ws.WebhooksWsParameters.NAME_PARAM_MAXIMUM_LENGTH;
import static org.sonar.server.webhook.ws.WebhooksWsParameters.SECRET_PARAM;
import static org.sonar.server.webhook.ws.WebhooksWsParameters.SECRET_PARAM_MAXIMUM_LENGTH;
import static org.sonar.server.webhook.ws.WebhooksWsParameters.UPDATE_ACTION;
import static org.sonar.server.webhook.ws.WebhooksWsParameters.URL_PARAM;
import static org.sonar.server.webhook.ws.WebhooksWsParameters.URL_PARAM_MAXIMUM_LENGTH;
Expand All @@ -58,7 +61,6 @@ public UpdateAction(DbClient dbClient, UserSession userSession, WebhookSupport w

@Override
public void define(WebService.NewController controller) {

WebService.NewAction action = controller.createAction(UPDATE_ACTION)
.setPost(true)
.setDescription("Update a Webhook.<br>" +
Expand All @@ -68,8 +70,8 @@ public void define(WebService.NewController controller) {

action.createParam(KEY_PARAM)
.setRequired(true)
.setMaximumLength(KEY_PARAM_MAXIMUN_LENGTH)
.setDescription("The key of the webhook to be updated, "+
.setMaximumLength(KEY_PARAM_MAXIMUM_LENGTH)
.setDescription("The key of the webhook to be updated, " +
"auto-generated value can be obtained through api/webhooks/create or api/webhooks/list")
.setExampleValue(KEY_PROJECT_EXAMPLE_001);

Expand All @@ -85,6 +87,13 @@ public void define(WebService.NewController controller) {
.setDescription("new url to be called by the webhook")
.setExampleValue(URL_WEBHOOK_EXAMPLE_001);

action.createParam(SECRET_PARAM)
.setRequired(false)
.setMinimumLength(1)
.setMaximumLength(SECRET_PARAM_MAXIMUM_LENGTH)
.setDescription("If provided, secret will be used as the key to generate the HMAC hex (lowercase) digest value in the 'X-Sonar-Webhook-HMAC-SHA256' header")
.setExampleValue("your_secret")
.setSince("7.8");
}

@Override
Expand All @@ -94,6 +103,7 @@ public void handle(Request request, Response response) throws Exception {
String webhookKey = request.param(KEY_PARAM);
String name = request.mandatoryParam(NAME_PARAM);
String url = request.mandatoryParam(URL_PARAM);
String secret = request.param(SECRET_PARAM);

webhookSupport.checkUrlPattern(url, "Url parameter with value '%s' is not a valid url", url);

Expand All @@ -107,15 +117,15 @@ public void handle(Request request, Response response) throws Exception {
Optional<OrganizationDto> optionalDto = dbClient.organizationDao().selectByUuid(dbSession, organizationUuid);
OrganizationDto organizationDto = checkStateWithOptional(optionalDto, "the requested organization '%s' was not found", organizationUuid);
webhookSupport.checkPermission(organizationDto);
updateWebhook(dbSession, webhookDto, name, url);
updateWebhook(dbSession, webhookDto, name, url, secret);
}

String projectUuid = webhookDto.getProjectUuid();
if (projectUuid != null) {
Optional<ComponentDto> optionalDto = ofNullable(dbClient.componentDao().selectByUuid(dbSession, projectUuid).orElse(null));
ComponentDto componentDto = checkStateWithOptional(optionalDto, "the requested project '%s' was not found", projectUuid);
webhookSupport.checkPermission(componentDto);
updateWebhook(dbSession, webhookDto, name, url);
updateWebhook(dbSession, webhookDto, name, url, secret);
}

dbSession.commit();
Expand All @@ -124,8 +134,12 @@ public void handle(Request request, Response response) throws Exception {
response.noContent();
}

private void updateWebhook(DbSession dbSession, WebhookDto webhookDto, String name, String url) {
dbClient.webhookDao().update(dbSession, webhookDto.setName(name).setUrl(url));
private void updateWebhook(DbSession dbSession, WebhookDto dto, String name, String url, @Nullable String secret) {
dto
.setName(name)
.setUrl(url)
.setSecret(secret);
dbClient.webhookDao().update(dbSession, dto);
}

}
Expand Up @@ -33,13 +33,15 @@ class WebhooksWsParameters {
static final String ORGANIZATION_KEY_PARAM = "organization";
static final int ORGANIZATION_KEY_PARAM_MAXIMUM_LENGTH = 255;
static final String PROJECT_KEY_PARAM = "project";
static final int PROJECT_KEY_PARAM_MAXIMUN_LENGTH = 100;
static final int PROJECT_KEY_PARAM_MAXIMUM_LENGTH = 100;
static final String NAME_PARAM = "name";
static final int NAME_PARAM_MAXIMUM_LENGTH = 100;
static final String URL_PARAM = "url";
static final int URL_PARAM_MAXIMUM_LENGTH = 512;
static final String KEY_PARAM = "webhook";
static final int KEY_PARAM_MAXIMUN_LENGTH = 40;
static final int KEY_PARAM_MAXIMUM_LENGTH = 40;
static final String SECRET_PARAM = "secret";
static final int SECRET_PARAM_MAXIMUM_LENGTH = 200;

private WebhooksWsParameters() {
// prevent instantiation
Expand Down
Expand Up @@ -2,6 +2,7 @@
"webhook": {
"key": "uuid",
"name": "My webhook",
"url": "https://www.my-webhook-listener.com/sonar"
"url": "https://www.my-webhook-listener.com/sonar",
"secret": "your_secret"
}
}
}

0 comments on commit d79cd20

Please sign in to comment.