Skip to content

Commit

Permalink
feat(webhook): Support response expressions for Webhooks (#2309)
Browse files Browse the repository at this point in the history
* feat(webhook): Support response expressions for Webhooks
Refactor verification and response body expression handling

* Hide response body expression and bump version number

* Remove response body expression from template
  • Loading branch information
sbuettner committed Apr 16, 2024
1 parent 585116f commit bd786e5
Show file tree
Hide file tree
Showing 19 changed files with 368 additions and 372 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@
import io.camunda.connector.api.inbound.CorrelationResult.Success.MessagePublished;
import io.camunda.connector.api.inbound.CorrelationResult.Success.ProcessInstanceCreated;
import io.camunda.connector.api.inbound.webhook.MappedHttpRequest;
import io.camunda.connector.api.inbound.webhook.VerifiableWebhook;
import io.camunda.connector.api.inbound.webhook.WebhookConnectorException;
import io.camunda.connector.api.inbound.webhook.WebhookConnectorException.WebhookSecurityException;
import io.camunda.connector.api.inbound.webhook.WebhookConnectorExecutable;
import io.camunda.connector.api.inbound.webhook.WebhookHttpResponse;
import io.camunda.connector.api.inbound.webhook.WebhookProcessingPayload;
import io.camunda.connector.api.inbound.webhook.WebhookResult;
import io.camunda.connector.api.inbound.webhook.WebhookResultContext;
Expand All @@ -40,6 +40,7 @@
import io.camunda.connector.runtime.inbound.webhook.model.HttpServletRequestWebhookProcessingPayload;
import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
import org.slf4j.Logger;
Expand Down Expand Up @@ -117,23 +118,12 @@ private ResponseEntity<?> processWebhook(
}

protected ResponseEntity<?> verify(
WebhookConnectorExecutable connectorHook, WebhookProcessingPayload payload) throws Exception {
WebhookConnectorExecutable connectorHook, WebhookProcessingPayload payload) {
WebhookHttpResponse verificationResponse = connectorHook.verify(payload);
ResponseEntity<?> response = null;

VerifiableWebhook.WebhookHttpVerificationResult verificationResult = null;
if (connectorHook instanceof VerifiableWebhook verifiableWebhook) {
verificationResult = verifiableWebhook.verify(payload);
}

if (verificationResult != null) {
HttpHeaders headers = new HttpHeaders();
Optional.of(verificationResult)
.map(VerifiableWebhook.WebhookHttpVerificationResult::headers)
.ifPresent(map -> map.forEach(headers::add));
response =
new ResponseEntity<>(verificationResult.body(), headers, verificationResult.statusCode());
if (verificationResponse != null) {
response = toResponseEntity(verificationResponse);
}

return response;
}

Expand Down Expand Up @@ -176,27 +166,28 @@ private ResponseEntity<?> buildErrorResponse(CorrelationResult.Failure failure)
private ResponseEntity<?> buildSuccessfulResponse(
WebhookResult webhookResult, CorrelationResult.Success correlationResult) {
ResponseEntity<?> response;
var processVariablesContext = toWebhookResultContext(webhookResult, correlationResult);
if (webhookResult.response() != null) {
// Step 3a: return response crafted by webhook itself
response = ResponseEntity.ok(webhookResult.response().body());
var processVariablesContext = toWebhookResultContext(webhookResult, correlationResult);
var httpResponseData = webhookResult.response().apply(processVariablesContext);
if (httpResponseData != null) {
response = toResponseEntity(httpResponseData);
} else {
response = ResponseEntity.ok().build();
}
} else {
// Step 3b: response body expression was defined and evaluated, or 200 OK otherwise
response = buildBodyExpressionResponseOrOk(webhookResult, processVariablesContext);
response = ResponseEntity.ok().build();
}
return response;
}

protected ResponseEntity<?> buildBodyExpressionResponseOrOk(
WebhookResult webhookResult, WebhookResultContext processVariablesContext) {
ResponseEntity<?> response;
if (webhookResult.responseBodyExpression() != null) {
var httpResponseData = webhookResult.responseBodyExpression().apply(processVariablesContext);
response = ResponseEntity.ok(httpResponseData);
} else {
response = ResponseEntity.ok().build();
}
return response;
protected static ResponseEntity<?> toResponseEntity(WebhookHttpResponse webhookHttpResponse) {
int status =
Optional.ofNullable(webhookHttpResponse.statusCode()).orElse(HttpStatus.OK.value());
HttpHeaders headers = new HttpHeaders();
Optional.ofNullable(webhookHttpResponse.headers())
.orElse(Collections.emptyMap())
.forEach(headers::add);
return ResponseEntity.status(status).headers(headers).body(webhookHttpResponse.body());
}

protected ResponseEntity<?> buildErrorResponse(Exception e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
package io.camunda.connector.runtime.app;

import io.camunda.connector.api.annotation.InboundConnector;
import io.camunda.connector.api.inbound.InboundConnectorContext;
import io.camunda.connector.api.inbound.webhook.MappedHttpRequest;
import io.camunda.connector.api.inbound.webhook.WebhookConnectorExecutable;
import io.camunda.connector.api.inbound.webhook.WebhookProcessingPayload;
Expand All @@ -44,10 +43,4 @@ public Map<String, Object> connectorData() {
}
};
}

@Override
public void activate(InboundConnectorContext context) throws Exception {}

@Override
public void deactivate() throws Exception {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,10 @@
import io.camunda.connector.api.inbound.CorrelationResult;
import io.camunda.connector.api.inbound.ProcessElement;
import io.camunda.connector.api.inbound.webhook.MappedHttpRequest;
import io.camunda.connector.api.inbound.webhook.VerifiableWebhook;
import io.camunda.connector.api.inbound.webhook.WebhookConnectorExecutable;
import io.camunda.connector.api.inbound.webhook.WebhookHttpResponse;
import io.camunda.connector.api.inbound.webhook.WebhookProcessingPayload;
import io.camunda.connector.api.inbound.webhook.WebhookResult;
import io.camunda.connector.api.inbound.webhook.WebhookResultContext;
import io.camunda.connector.feel.FeelEngineWrapperException;
import io.camunda.connector.runtime.app.TestConnectorRuntimeApplication;
import io.camunda.connector.runtime.core.inbound.DefaultProcessElementContextFactory;
Expand Down Expand Up @@ -136,7 +134,8 @@ public void testSuccessfulProcessingWithActivationAndStrictResponse() throws Exc
WebhookResult webhookResult = mock(WebhookResult.class);
when(webhookResult.request()).thenReturn(new MappedHttpRequest(Map.of(), Map.of(), Map.of()));
when(webhookResult.response())
.thenReturn(new WebhookHttpResponse(Map.of("keyResponse", "valueResponse"), null));
.thenReturn(
(c) -> new WebhookHttpResponse(Map.of("keyResponse", "valueResponse"), null, 201));
when(webhookConnectorExecutable.triggerWebhook(any(WebhookProcessingPayload.class)))
.thenReturn(webhookResult);

Expand Down Expand Up @@ -166,7 +165,7 @@ public void testSuccessfulProcessingWithActivationAndStrictResponse() throws Exc
new HashMap<>(),
new MockHttpServletRequest());

assertEquals(200, responseEntity.getStatusCode().value());
assertEquals(201, responseEntity.getStatusCode().value());
assertEquals("valueResponse", responseEntity.getBody().get("keyResponse"));

var result = responseEntity.getBody();
Expand All @@ -178,7 +177,8 @@ public void testSuccessfulProcessingWithFailedActivation() throws Exception {
WebhookConnectorExecutable webhookConnectorExecutable = mock(WebhookConnectorExecutable.class);
WebhookResult webhookResult = mock(WebhookResult.class);
when(webhookResult.request()).thenReturn(new MappedHttpRequest(Map.of(), Map.of(), Map.of()));
when(webhookResult.responseBodyExpression()).thenReturn((WebhookResultContext) -> Map.of());
when(webhookResult.response())
.thenReturn((WebhookResultContext) -> new WebhookHttpResponse(Map.of(), null, null));
when(webhookConnectorExecutable.triggerWebhook(any(WebhookProcessingPayload.class)))
.thenReturn(webhookResult);

Expand Down Expand Up @@ -218,7 +218,8 @@ public void testSuccessfulProcessingWithDuplicateMessage() throws Exception {
WebhookConnectorExecutable webhookConnectorExecutable = mock(WebhookConnectorExecutable.class);
WebhookResult webhookResult = mock(WebhookResult.class);
when(webhookResult.request()).thenReturn(new MappedHttpRequest(Map.of(), Map.of(), Map.of()));
when(webhookResult.responseBodyExpression()).thenReturn((WebhookResultContext) -> Map.of());
when(webhookResult.response())
.thenReturn((WebhookResultContext) -> WebhookHttpResponse.ok(Map.of()));
when(webhookConnectorExecutable.triggerWebhook(any(WebhookProcessingPayload.class)))
.thenReturn(webhookResult);

Expand Down Expand Up @@ -268,7 +269,7 @@ public void testSuccessfulProcessingWithActivationCorrelationHidden() throws Exc
WebhookResult webhookResult = mock(WebhookResult.class);
when(webhookResult.request()).thenReturn(new MappedHttpRequest(Map.of(), Map.of(), Map.of()));
// default use-case, when result expression not set
when(webhookResult.responseBodyExpression()).thenReturn(webhookResultContext -> null);
when(webhookResult.response()).thenReturn(webhookResultContext -> null);
when(webhookConnectorExecutable.triggerWebhook(any(WebhookProcessingPayload.class)))
.thenReturn(webhookResult);

Expand Down Expand Up @@ -409,7 +410,7 @@ public void testFeelExpressionErrorDuringProcessing() throws Exception {
assertEquals("expression", responseEntity.getBody().expression());
}

interface MyVerifiableWebhook extends WebhookConnectorExecutable, VerifiableWebhook {}
interface MyVerifiableWebhook extends WebhookConnectorExecutable {}

@Test
@SuppressWarnings("unchecked")
Expand All @@ -418,14 +419,12 @@ public void testSuccessfulProcessingWithVerification() throws Exception {
WebhookResult webhookResult = mock(WebhookResult.class);
when(webhookResult.request()).thenReturn(new MappedHttpRequest(Map.of(), Map.of(), Map.of()));
when(webhookResult.response())
.thenReturn(new WebhookHttpResponse(Map.of("keyResponse", "valueResponse"), null));
.thenReturn((c) -> WebhookHttpResponse.ok(Map.of("keyResponse", "valueResponse")));
when(webhookConnectorExecutable.triggerWebhook(any(WebhookProcessingPayload.class)))
.thenReturn(webhookResult);

when(webhookConnectorExecutable.verify(any(WebhookProcessingPayload.class)))
.thenReturn(
new VerifiableWebhook.WebhookHttpVerificationResult(
Map.of("result", "GOOD"), Map.of(), 201));
.thenReturn(new WebhookHttpResponse(Map.of("result", "GOOD"), Map.of(), null));

var webhookDef = webhookDefinition("processA", 1, "myPath");
var webhookContext =
Expand Down Expand Up @@ -453,7 +452,7 @@ public void testSuccessfulProcessingWithVerification() throws Exception {
new HashMap<>(),
new MockHttpServletRequest());

assertEquals(201, responseEntity.getStatusCode().value());
assertEquals(200, responseEntity.getStatusCode().value());
assertNull(responseEntity.getBody().get("keyResponse"));
assertEquals("GOOD", responseEntity.getBody().get("result"));

Expand Down Expand Up @@ -481,7 +480,7 @@ public void testSuccessfulProcessingWithResponseBodyExpression() throws Exceptio

WebhookResult webhookResult = mock(WebhookResult.class);
when(webhookResult.request()).thenReturn(new MappedHttpRequest(Map.of(), Map.of(), Map.of()));
when(webhookResult.responseBodyExpression()).thenReturn((WebhookResultContext::correlation));
when(webhookResult.response()).thenReturn((c) -> WebhookHttpResponse.ok(c.correlation()));
when(webhookConnectorExecutable.triggerWebhook(any(WebhookProcessingPayload.class)))
.thenReturn(webhookResult);

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,20 @@
* supports HTTP webhooks uses this interface to control the execution of inbound webhook
* Connectors.
*/
public interface WebhookConnectorExecutable extends InboundConnectorExecutable {
public interface WebhookConnectorExecutable
extends InboundConnectorExecutable<InboundConnectorContext> {

/**
* Entry point for custom verification logic. The purpose of the method is to perform a
* verification for Webhook implementation that use an initial request to verify the existence and
* correctness of the webhook. Can also be used for one time validation purposes.
*
* @param payload
* @return The response returned by the webhook call.
*/
default WebhookHttpResponse verify(WebhookProcessingPayload payload) {
return null;
}

/**
* Entry-point method whenever webhook was triggered. The purpose of the method is to perform
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,9 @@

import java.util.Map;

public record WebhookHttpResponse(Object body, Map<String, String> headers) {}
public record WebhookHttpResponse(Object body, Map<String, String> headers, Integer statusCode) {

public static WebhookHttpResponse ok(Object body) {
return new WebhookHttpResponse(body, null, 200);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,11 @@ public interface WebhookResult {
MappedHttpRequest request();

/**
* @return strict response from the connector. May be useful to handle challenges, or special
* response cases.
* Returns a function that can be used to generate a response to the webhook request.
*
* @return
*/
default WebhookHttpResponse response() {
default Function<WebhookResultContext, WebhookHttpResponse> response() {
return null;
}

Expand All @@ -49,8 +50,4 @@ default WebhookHttpResponse response() {
default Map<String, Object> connectorData() {
return Collections.emptyMap();
}

default Function<WebhookResultContext, Object> responseBodyExpression() {
return response -> null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
import io.camunda.connector.api.annotation.InboundConnector;
import io.camunda.connector.api.inbound.InboundConnectorContext;
import io.camunda.connector.api.inbound.webhook.MappedHttpRequest;
import io.camunda.connector.api.inbound.webhook.VerifiableWebhook;
import io.camunda.connector.api.inbound.webhook.WebhookConnectorExecutable;
import io.camunda.connector.api.inbound.webhook.WebhookHttpResponse;
import io.camunda.connector.api.inbound.webhook.WebhookProcessingPayload;
Expand Down Expand Up @@ -69,8 +68,7 @@
templateIdOverride = "io.camunda.connectors.inbound.Slack.BoundaryEvent.v1",
templateNameOverride = "Slack Webhook Boundary Event Connector")
})
public class SlackInboundWebhookExecutable
implements WebhookConnectorExecutable, VerifiableWebhook {
public class SlackInboundWebhookExecutable implements WebhookConnectorExecutable {

protected static final String HEADER_SLACK_REQUEST_TIMESTAMP = "x-slack-request-timestamp";
protected static final String HEADER_SLACK_SIGNATURE = "x-slack-signature";
Expand Down Expand Up @@ -106,14 +104,14 @@ public WebhookResult triggerWebhook(WebhookProcessingPayload webhookProcessingPa
new MappedHttpRequest(
bodyAsMap, Map.of(HttpHeaders.CONTENT_TYPE, MediaType.JSON_UTF_8.toString()), null),
bodyAsMap,
new WebhookHttpResponse(defaultCommandResponse(), null));
new WebhookHttpResponse(defaultCommandResponse(), null, 200));
}

// Other requests, e.g. events
return new SlackWebhookProcessingResult(
new MappedHttpRequest(bodyAsMap, webhookProcessingPayload.headers(), null),
null,
new WebhookHttpResponse(bodyAsMap, null));
new WebhookHttpResponse(bodyAsMap, null, 200));
}

@Override
Expand All @@ -123,7 +121,7 @@ public void activate(InboundConnectorContext context) {
}

@Override
public WebhookHttpVerificationResult verify(WebhookProcessingPayload payload) {
public WebhookHttpResponse verify(WebhookProcessingPayload payload) {
verifySlackRequestAuthentic(payload);
return Optional.ofNullable(props.verificationExpression())
.orElse(stringObjectMap -> null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@
import io.camunda.connector.api.inbound.webhook.MappedHttpRequest;
import io.camunda.connector.api.inbound.webhook.WebhookHttpResponse;
import io.camunda.connector.api.inbound.webhook.WebhookResult;
import io.camunda.connector.api.inbound.webhook.WebhookResultContext;
import java.util.Map;
import java.util.function.Function;

public class SlackWebhookProcessingResult implements WebhookResult {

private MappedHttpRequest request;
private final MappedHttpRequest request;
private final Map<String, Object> connectorData;

private WebhookHttpResponse response;
private final WebhookHttpResponse response;

public SlackWebhookProcessingResult(
MappedHttpRequest request, Map<String, Object> connectorData, WebhookHttpResponse response) {
Expand All @@ -36,7 +37,11 @@ public Map<String, Object> connectorData() {
}

@Override
public WebhookHttpResponse response() {
public Function<WebhookResultContext, WebhookHttpResponse> response() {
return (c) -> response;
}

public WebhookHttpResponse getResponse() {
return response;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
package io.camunda.connector.slack.inbound.model;

import com.slack.api.app_backend.SlackSignature;
import io.camunda.connector.api.inbound.webhook.VerifiableWebhook;
import io.camunda.connector.api.inbound.webhook.WebhookHttpResponse;
import io.camunda.connector.generator.dsl.Property;
import io.camunda.connector.generator.java.annotation.TemplateProperty;
import io.camunda.connector.generator.java.annotation.TemplateProperty.PropertyType;
Expand Down Expand Up @@ -41,8 +41,7 @@ public record SlackWebhookProperties(
optional = true,
defaultValue =
"=if (body.type != null and body.type = \"url_verification\") then {body:{\"challenge\":body.challenge}, statusCode: 200} else null")
Function<Map<String, Object>, VerifiableWebhook.WebhookHttpVerificationResult>
verificationExpression) {
Function<Map<String, Object>, WebhookHttpResponse> verificationExpression) {
public SlackWebhookProperties(SlackConnectorPropertiesWrapper wrapper) {
this(
wrapper.inbound.context,
Expand Down

0 comments on commit bd786e5

Please sign in to comment.