Skip to content

Commit

Permalink
Add possibility of authorization to AsyncHttpRegistryClient; implemen…
Browse files Browse the repository at this point in the history
…t OAuth2TokenProvider; authorization can be configured with properties under edison.serviceregistry.security.oauth2

Co-authored-by: Jan Schawo <[email protected]>
Co-authored-by: Yannik Rudolph <[email protected]>
  • Loading branch information
YannikRudolph0108 and janschawo1111 committed Oct 4, 2023
1 parent 3cf805a commit fd5deea
Show file tree
Hide file tree
Showing 13 changed files with 451 additions and 43 deletions.
4 changes: 4 additions & 0 deletions edison-core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ dependencies {

testImplementation project(":edison-testsupport")
testImplementation libraries.aws_sdk_ssm
testImplementation test_libraries.mockserver_netty
testImplementation test_libraries.mockserver_client_java
testImplementation test_libraries.mockserver_junit_jupiter
testImplementation test_libraries.awaitility
}

artifacts {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import de.otto.edison.annotations.Beta;
import de.otto.edison.configuration.EdisonApplicationProperties;
import de.otto.edison.registry.configuration.ServiceRegistryProperties;
import de.otto.edison.registry.security.OAuth2TokenProvider;
import de.otto.edison.registry.security.OAuth2TokenProviderFactory;
import de.otto.edison.status.domain.ApplicationInfo;
import jakarta.annotation.PostConstruct;
import org.slf4j.Logger;
Expand All @@ -20,6 +22,7 @@
import static java.util.Arrays.stream;
import static java.util.concurrent.Executors.newSingleThreadScheduledExecutor;
import static java.util.concurrent.TimeUnit.MINUTES;
import static org.springframework.http.HttpHeaders.*;
import static org.springframework.util.ObjectUtils.isEmpty;

/**
Expand All @@ -34,22 +37,24 @@
public class AsyncHttpRegistryClient implements RegistryClient {

private static final Logger LOG = LoggerFactory.getLogger(AsyncHttpRegistryClient.class);

private final ApplicationInfo applicationInfo;
private final HttpClient httpClient;
private final ServiceRegistryProperties serviceRegistryProperties;
private final EdisonApplicationProperties edisonApplicationProperties;
private final OAuth2TokenProvider oAuth2TokenProvider;
private final ScheduledExecutorService scheduledExecutorService = newSingleThreadScheduledExecutor();
private boolean isRunning = false;

@Autowired
public AsyncHttpRegistryClient(final ApplicationInfo applicationInfo,
final ServiceRegistryProperties serviceRegistryProperties,
final EdisonApplicationProperties edisonApplicationProperties) {
final EdisonApplicationProperties edisonApplicationProperties,
final OAuth2TokenProviderFactory oAuth2TokenProviderFactory) {
this.applicationInfo = applicationInfo;
this.httpClient = HttpClient.newBuilder().build();
this.serviceRegistryProperties = serviceRegistryProperties;
this.edisonApplicationProperties = edisonApplicationProperties;
this.oAuth2TokenProvider = oAuth2TokenProviderFactory.isEnabled() ? oAuth2TokenProviderFactory.create() : null;
}

@PostConstruct
Expand Down Expand Up @@ -77,32 +82,37 @@ public void registerService() {
.filter(server -> !isEmpty(server))
.forEach(discoveryServer -> {
try {
final HttpRequest.Builder requestBuilder = HttpRequest.newBuilder()
.PUT(HttpRequest.BodyPublishers.ofString(
"{\n" +
" \"groups\":[\"" + edisonApplicationProperties.getGroup() + "\"],\n" +
" \"expire\":" + serviceRegistryProperties.getExpireAfter() + ",\n" +
" \"links\":[{\n" +
" \"rel\":\"https://github.com/otto-de/edison/link-relations/microservice\",\n" +
" \"href\" : \"" + serviceRegistryProperties.getService() + "\",\n" +
" \"title\":\"" + applicationInfo.title + "\"\n" +
" }] \n" +
"}"
))
.uri(URI.create(discoveryServer + "/environments/" + edisonApplicationProperties.getEnvironment() + "/" + applicationInfo.name))
.header(CONTENT_TYPE, "application/vnd.otto.edison.links+json")
.header(ACCEPT, "application/vnd.otto.edison.links+json");

if (oAuth2TokenProvider != null) {
requestBuilder.header(AUTHORIZATION, "Bearer " + oAuth2TokenProvider.getAccessToken());
}

LOG.debug("Updating registration of service at '{}'", discoveryServer);
httpClient
.sendAsync(HttpRequest.newBuilder()
.PUT(HttpRequest.BodyPublishers.ofString(
"{\n" +
" \"groups\":[\"" + edisonApplicationProperties.getGroup() + "\"],\n" +
" \"expire\":" + serviceRegistryProperties.getExpireAfter() + ",\n" +
" \"links\":[{\n" +
" \"rel\":\"https://github.com/otto-de/edison/link-relations/microservice\",\n" +
" \"href\" : \"" + serviceRegistryProperties.getService() + "\",\n" +
" \"title\":\"" + applicationInfo.title + "\"\n" +
" }] \n" +
"}"
))
.uri(URI.create(discoveryServer + "/environments/" + edisonApplicationProperties.getEnvironment() + "/" + applicationInfo.name))
.header("Content-Type", "application/vnd.otto.edison.links+json")
.header("Accept", "application/vnd.otto.edison.links+json")
.build(), HttpResponse.BodyHandlers.ofString())
.thenApply(response -> {
if (response.statusCode() < 300) {
LOG.info("Successfully updated registration at " + discoveryServer);
} else {
LOG.warn("Failed to update registration at '{}': Status='{}'", discoveryServer, response.statusCode());
}
return response.statusCode();
});
.sendAsync(requestBuilder.build(), HttpResponse.BodyHandlers.ofString())
.thenApply(response -> {
if (response.statusCode() < 300) {
LOG.info("Successfully updated registration at " + discoveryServer);
} else {
LOG.warn("Failed to update registration at '{}': Status='{}'", discoveryServer, response.statusCode());
}
return response.statusCode();
});
} catch (final Exception e) {
LOG.error("Error updating registration", e);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package de.otto.edison.registry.configuration;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.DefaultValue;

@ConfigurationProperties(prefix = "edison.serviceregistry.security.oauth2")
public record ServiceRegistrySecurityOAuthProperties(@DefaultValue("false") boolean enabled,
String tokenEndpoint,
String clientId,
String clientSecret,
@DefaultValue("10") int timeoutSeconds) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package de.otto.edison.registry.security;

public class OAuth2TokenException extends Exception {

public OAuth2TokenException() {
super("Error while fetching access token");
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package de.otto.edison.registry.security;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.concurrent.ExecutionException;

public class OAuth2TokenProvider {

private static final Logger LOG = LoggerFactory.getLogger(OAuth2TokenProvider.class);
private final HttpClient httpClient;
private final ObjectMapper objectMapper;
private final String clientId;
private final String clientSecret;
private final String tokenEndpoint;
private final int timeoutSeconds;

public OAuth2TokenProvider(final String clientId,
final String clientSecret,
final String tokenEndpoint,
final int timeoutSeconds) {
this.httpClient = HttpClient.newBuilder().build();
this.clientId = clientId;
this.clientSecret = clientSecret;
this.tokenEndpoint = tokenEndpoint;
this.timeoutSeconds = timeoutSeconds;
this.objectMapper = new ObjectMapper();
}

public String getAccessToken() throws ExecutionException, InterruptedException, JsonProcessingException, OAuth2TokenException {
try {
final HttpResponse<String> response = httpClient
.sendAsync(HttpRequest.newBuilder()
.timeout(Duration.ofSeconds(timeoutSeconds))
.POST(HttpRequest.BodyPublishers.ofString("grant_type=client_credentials&client_id=" + clientId + "&client_secret=" + clientSecret))
.uri(URI.create(tokenEndpoint))
.header("Accept", "application/json")
.header("Content-Type", "application/x-www-form-urlencoded")
.build(), HttpResponse.BodyHandlers.ofString())
.get();
if (response.statusCode() != HttpStatus.OK.value()) {
throw new OAuth2TokenException();
}
return objectMapper.readValue(response.body(), OAuth2TokenResponse.class).accessToken();
} catch (final Exception e) {
LOG.error("Error fetching access token", e);
throw e;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package de.otto.edison.registry.security;

import de.otto.edison.registry.configuration.ServiceRegistrySecurityOAuthProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@EnableConfigurationProperties(ServiceRegistrySecurityOAuthProperties.class)
public class OAuth2TokenProviderFactory {

private final ServiceRegistrySecurityOAuthProperties properties;

@Autowired
public OAuth2TokenProviderFactory(final ServiceRegistrySecurityOAuthProperties properties) {
this.properties = properties;
}

public OAuth2TokenProvider create() {
return new OAuth2TokenProvider(properties.clientId(), properties.clientSecret(), properties.tokenEndpoint(), properties.timeoutSeconds());
}

public boolean isEnabled() {
return properties.enabled();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package de.otto.edison.registry.security;

import com.fasterxml.jackson.annotation.JsonProperty;

public record OAuth2TokenResponse(@JsonProperty("access_token") String accessToken) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package de.otto.edison.acceptance;

import org.springframework.context.annotation.ComponentScan;

import de.otto.edison.testsupport.applicationdriver.SpringTestBase;
@ComponentScan(basePackages = {"de.otto.edison"})
public class SpringTestBaseWithComponentScan extends SpringTestBase {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package de.otto.edison.acceptance.registry;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import de.otto.edison.acceptance.SpringTestBaseWithComponentScan;
import de.otto.edison.registry.client.AsyncHttpRegistryClient;
import de.otto.edison.registry.configuration.ServiceRegistrySecurityOAuthProperties;

@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = {SpringTestBaseWithComponentScan.class})
@ActiveProfiles("test")
public class OAuth2TokenProviderAcceptanceTest {
@Autowired
private ServiceRegistrySecurityOAuthProperties serviceRegistrySecurityOAuthProperties;

@Autowired
private AsyncHttpRegistryClient asyncHttpRegistryClient;

@Test
public void shouldInstantiateRegistryClientWithoutExistingProperties() {
// then
assertNotNull(asyncHttpRegistryClient);
assertFalse(serviceRegistrySecurityOAuthProperties.enabled());
assertEquals(serviceRegistrySecurityOAuthProperties.timeoutSeconds(), 10);
}
}
Loading

0 comments on commit fd5deea

Please sign in to comment.