Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

control-service: enable WebHook authentication #2551

Merged
merged 8 commits into from
Aug 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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=versatiledatakit@groups.vmware.com
Expand Down
Loading