Skip to content

Commit

Permalink
control-service: enable WebHook authentication (#2551)
Browse files Browse the repository at this point in the history
# Why
Currently, the Control Service lacks the capability to authenticate
against the WebHook API. A specific customer of ours requires
authentication for the WebHook API utilizing the same access token
passed by the client to the Control Service APIs.



# What
Incorporated logic to forward the oAuth2 access token to the WebHook
API.

# Testing Done
Unit and integration tests.

Signed-off-by: Miroslav Ivanov [email protected]

---------

Signed-off-by: Miroslav Ivanov [email protected]
Co-authored-by: github-actions <>
  • Loading branch information
mivanov1988 authored Aug 16, 2023
1 parent bcad522 commit 2276a54
Show file tree
Hide file tree
Showing 12 changed files with 285 additions and 128 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -389,12 +389,17 @@ webHooks:
## 4xx or 5xx the job is not created/deleted
## 4xx response content is shown to end user as an error message.
## 5xx responses will be retried (number of retries is based on configuration, defaults to 3)
##
## In case authenticationEnabled is set to true, the Control Service (CS) will transmit the oAuth2 access token
## to the WebHook API. This access token serves the purpose of authenticating the client against the CS.
postCreate:
webhookUri: ""
internalErrorsRetries: 3
authenticationEnabled: false
postDelete:
webhookUri: ""
internalErrorsRetries: 3
authenticationEnabled: false

### Security configuration

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,14 +80,15 @@ dependencies { // Implementation dependencies are found on compile classpath of
implementation versions.'com.amazonaws:aws-java-sdk-core'
implementation versions.'com.amazonaws:aws-java-sdk-sts'

implementation 'org.springframework.security:spring-security-oauth2-jose'

compileOnly versions.'org.hibernate:hibernate-jpamodelgen'
annotationProcessor versions.'org.hibernate:hibernate-jpamodelgen'

testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testImplementation versions.'com.mmnaseri.utils:spring-data-mock'
testImplementation versions.'org.mock-server:mockserver-netty'
testImplementation 'org.springframework.security:spring-security-oauth2-jose'
testImplementation versions.'org.mockito:mockito-core'
testImplementation versions.'net.bytebuddy:byte-buddy'
testImplementation versions.'org.testcontainers:testcontainers'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,16 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.net.URLDecoder;
import java.nio.charset.Charset;
import java.util.Optional;

/**
* AuthorizationProvider class used in {@link AuthorizationInterceptor} responsible for handling of
Expand Down Expand Up @@ -84,6 +87,21 @@ public String getUserId(Authentication authentication) {
}
}

public String getAccessToken() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String accessToken = null;

if (authentication instanceof JwtAuthenticationToken) {
JwtAuthenticationToken oauthToken = (JwtAuthenticationToken) authentication;
accessToken =
Optional.ofNullable(oauthToken.getToken())
.map(AbstractOAuth2Token::getTokenValue)
.orElse(null);
}

return accessToken;
}

String parsePropertyFromURI(String contextPath, String fullPath, int index) {
String uri = URLDecoder.decode(fullPath, Charset.defaultCharset());
if (!StringUtils.isBlank(contextPath)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
package com.vmware.taurus.authorization.webhook;

import com.vmware.taurus.authorization.AuthorizationInterceptor;
import com.vmware.taurus.authorization.provider.AuthorizationProvider;
import com.vmware.taurus.base.FeatureFlags;
import com.vmware.taurus.exception.AuthorizationError;
import com.vmware.taurus.exception.ExternalSystemError;
import com.vmware.taurus.service.webhook.WebHookRequestBody;
Expand All @@ -15,6 +17,7 @@
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

/**
* AuthorizationWebhookProvider class which delegates authorization request to a third party webhook
Expand All @@ -30,14 +33,25 @@ public class AuthorizationWebHookProvider extends WebHookService<AuthorizationBo

public AuthorizationWebHookProvider(
@Value("${datajobs.authorization.webhook.endpoint}") String webHookEndpoint,
@Value("${datajobs.authorization.webhook.internal.errors.retries:1}")
int retriesOn5xxErrors) {
super(webHookEndpoint, retriesOn5xxErrors, log);
@Value("${datajobs.authorization.webhook.internal.errors.retries:1}") int retriesOn5xxErrors,
@Value("${datajobs.authorization.webhook.authentication.enabled:false}")
boolean authenticationEnabled,
RestTemplate restTemplate,
FeatureFlags featureFlags,
AuthorizationProvider authorizationProvider) {
super(
webHookEndpoint,
retriesOn5xxErrors,
authenticationEnabled,
log,
restTemplate,
featureFlags,
authorizationProvider);
}

@Override
public void ensureConfigured() {
if (StringUtils.isBlank(getWebHookEndpoint())) {
if (StringUtils.isBlank(webHookEndpoint)) {
throw new AuthorizationError(
"Authorization webhook endpoint is not configured",
"Cannot determine whether a user is authorized to do this request",
Expand All @@ -48,7 +62,7 @@ public void ensureConfigured() {

@Override
protected String getWebHookRequestURL(AuthorizationBody webHookRequestBody) {
return getWebHookEndpoint();
return webHookEndpoint;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@

package com.vmware.taurus.datajobs.webhook;

import com.vmware.taurus.authorization.provider.AuthorizationProvider;
import com.vmware.taurus.base.FeatureFlags;
import com.vmware.taurus.service.webhook.WebHookRequestBody;
import com.vmware.taurus.service.webhook.WebHookService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

/**
* PostCreateWebHookProvider class which delegates custom post data job creation operations via a
Expand All @@ -24,7 +27,19 @@ public class PostCreateWebHookProvider extends WebHookService<WebHookRequestBody

public PostCreateWebHookProvider(
@Value("${datajobs.post.create.webhook.endpoint}") String webHookEndpoint,
@Value("${datajobs.post.create.webhook.internal.errors.retries:-1}") int retriesOn5xxErrors) {
super(webHookEndpoint, retriesOn5xxErrors, log);
@Value("${datajobs.post.create.webhook.internal.errors.retries:-1}") int retriesOn5xxErrors,
@Value("${datajobs.post.create.webhook.authentication.enabled:false}")
boolean authenticationEnabled,
RestTemplate restTemplate,
FeatureFlags featureFlags,
AuthorizationProvider authorizationProvider) {
super(
webHookEndpoint,
retriesOn5xxErrors,
authenticationEnabled,
log,
restTemplate,
featureFlags,
authorizationProvider);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@

package com.vmware.taurus.datajobs.webhook;

import com.vmware.taurus.authorization.provider.AuthorizationProvider;
import com.vmware.taurus.base.FeatureFlags;
import com.vmware.taurus.service.webhook.WebHookRequestBody;
import com.vmware.taurus.service.webhook.WebHookService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

/**
* PostDeleteWebHookProvider class which delegates custom post data job delete operations via a
Expand All @@ -21,9 +24,22 @@
@Service
@Slf4j
public class PostDeleteWebHookProvider extends WebHookService<WebHookRequestBody> {

public PostDeleteWebHookProvider(
@Value("${datajobs.post.delete.webhook.endpoint}") String webHookEndpoint,
@Value("${datajobs.post.delete.webhook.internal.errors.retries:-1}") int retriesOn5xxErrors) {
super(webHookEndpoint, retriesOn5xxErrors, log);
@Value("${datajobs.post.delete.webhook.internal.errors.retries:-1}") int retriesOn5xxErrors,
@Value("${datajobs.post.delete.webhook.authentication.enabled:false}")
boolean authenticationEnabled,
RestTemplate restTemplate,
FeatureFlags featureFlags,
AuthorizationProvider authorizationProvider) {
super(
webHookEndpoint,
retriesOn5xxErrors,
authenticationEnabled,
log,
restTemplate,
featureFlags,
authorizationProvider);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,14 @@

package com.vmware.taurus.service.webhook;

import com.vmware.taurus.authorization.provider.AuthorizationProvider;
import com.vmware.taurus.base.FeatureFlags;
import com.vmware.taurus.exception.ExternalSystemError;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.http.*;
import org.springframework.web.client.RestClientResponseException;
import org.springframework.web.client.RestTemplate;

Expand All @@ -33,44 +29,49 @@
* @see com.vmware.taurus.datajobs.webhook.PostCreateWebHookProvider
* @see com.vmware.taurus.authorization.webhook.AuthorizationWebHookProvider
*/
@Service
@RequiredArgsConstructor
public abstract class WebHookService<T extends WebHookRequestBody> implements InitializingBean {

@Getter private final String webHookEndpoint;
protected final String webHookEndpoint;

private final int retriesOn5xxErrors;

private final boolean authenticationEnabled;

private final Logger log;

@Autowired private RestTemplate restTemplate;
private final RestTemplate restTemplate;

private final FeatureFlags featureFlags;

private final AuthorizationProvider authorizationProvider;

public Optional<WebHookResult> invokeWebHook(T webHookRequestBody) {
ensureConfigured();

if (StringUtils.isBlank(getWebHookEndpoint())) {
if (StringUtils.isBlank(webHookEndpoint)) {
log.debug("The webHook Endpoint is not configured. Requests will not be send ...");
return Optional.empty();
}

ResponseEntity responseEntity = sendRequest(webHookRequestBody);

if (responseEntity.getStatusCode().is5xxServerError()) {
log.debug("The WebHook invocation {} returns 5xxServerError ...", getWebHookEndpoint());
log.debug("The WebHook invocation {} returns 5xxServerError ...", webHookEndpoint);
responseEntity = retry5xxWebHookRequest(webHookRequestBody, responseEntity);
}
if (responseEntity.getStatusCode().is4xxClientError()) {
log.debug("The WebHook invocation {} returns 4xxClientError ...", getWebHookEndpoint());
log.debug("The WebHook invocation {} returns 4xxClientError ...", webHookEndpoint);
return Optional.of(provideClientErrorMessage(responseEntity));
}
if (responseEntity.getStatusCode().is2xxSuccessful()) {
log.debug("The WebHook invocation {} is successful ...", getWebHookEndpoint());
log.debug("The WebHook invocation {} is successful ...", webHookEndpoint);
return Optional.of(provideSuccessMessage(responseEntity));
}

log.debug(
"The WebHook invocation {} returns unhandled status code: {}",
getWebHookEndpoint(),
webHookEndpoint,
responseEntity.getStatusCode().value());
ExternalSystemError.MainExternalSystem mainExternalSystem = getExternalSystemType();
throw new ExternalSystemError(
Expand Down Expand Up @@ -103,7 +104,8 @@ private ResponseEntity sendRequest(T webHookRequestBody) {
// necessary
log.info("WebHook body: {}", webHookRequestBody.toString());
try {
HttpEntity<WebHookRequestBody> request = new HttpEntity<>(webHookRequestBody);
HttpEntity<WebHookRequestBody> request = createHttpRequest(webHookRequestBody);

return restTemplate.exchange(
getWebHookRequestURL(webHookRequestBody), HttpMethod.POST, request, String.class);
} catch (RestClientResponseException responseException) {
Expand Down Expand Up @@ -133,7 +135,7 @@ protected void ensureConfigured() {}
* @return a valid URL
*/
protected String getWebHookRequestURL(T webHookRequestBody) {
return getWebHookEndpoint() + webHookRequestBody.getRequestedHttpPath();
return webHookEndpoint + webHookRequestBody.getRequestedHttpPath();
}

/**
Expand Down Expand Up @@ -175,4 +177,21 @@ protected WebHookResult provideSuccessMessage(ResponseEntity responseEntity) {
.success(true)
.build();
}

private HttpEntity<WebHookRequestBody> createHttpRequest(T webHookRequestBody) {
HttpEntity<WebHookRequestBody> request = null;

if (featureFlags.isSecurityEnabled() && authenticationEnabled) {
String accessToken = authorizationProvider.getAccessToken();

if (StringUtils.isNotEmpty(accessToken)) {
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setBearerAuth(accessToken);

request = new HttpEntity<>(webHookRequestBody, httpHeaders);
}
}

return request != null ? request : new HttpEntity<>(webHookRequestBody);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,10 @@ datajobs.authorization.jwt.claim.username=username
# Data Jobs post webhook settings (Create and Delete)
datajobs.post.create.webhook.endpoint=
datajobs.post.create.webhook.internal.errors.retries=3
datajobs.post.create.webhook.authentication.enabled=false
datajobs.post.delete.webhook.endpoint=
datajobs.post.delete.webhook.internal.errors.retries=3
datajobs.post.delete.webhook.authentication.enabled=false

# The owner name and email address that will be used to send all Versatile Data Kit related email notifications.
datajobs.notification.owner.email=[email protected]
Expand Down
Loading

0 comments on commit 2276a54

Please sign in to comment.