ICS reading auth token from file 51/7951/3
authorPatrikBuhr <patrik.buhr@est.tech>
Fri, 4 Mar 2022 09:37:00 +0000 (10:37 +0100)
committerPatrikBuhr <patrik.buhr@est.tech>
Wed, 16 Mar 2022 08:23:44 +0000 (09:23 +0100)
Signed-off-by: PatrikBuhr <patrik.buhr@est.tech>
Issue-ID: NONRTRIC-735
Change-Id: Ib7bf0f4f10f441f9a29366a4f32c953ecdea01a8

12 files changed:
information-coordinator-service/config/application.yaml
information-coordinator-service/src/main/java/org/oransc/ics/BeanFactory.java
information-coordinator-service/src/main/java/org/oransc/ics/SwaggerConfig.java
information-coordinator-service/src/main/java/org/oransc/ics/clients/AsyncRestClient.java
information-coordinator-service/src/main/java/org/oransc/ics/clients/AsyncRestClientFactory.java
information-coordinator-service/src/main/java/org/oransc/ics/clients/SecurityContext.java [new file with mode: 0644]
information-coordinator-service/src/main/java/org/oransc/ics/controllers/a1e/A1eCallbacks.java
information-coordinator-service/src/main/java/org/oransc/ics/controllers/r1consumer/ConsumerCallbacks.java
information-coordinator-service/src/main/java/org/oransc/ics/controllers/r1producer/ProducerCallbacks.java
information-coordinator-service/src/test/java/org/oransc/ics/ApplicationTest.java
information-coordinator-service/src/test/java/org/oransc/ics/clients/AsyncRestClientTest.java
information-coordinator-service/src/test/java/org/oransc/ics/controller/ProducerSimulatorController.java

index 72bb907..372b61c 100644 (file)
@@ -6,7 +6,7 @@ spring:
   aop:
     auto: false
 springdoc:
-  show-actuator: true 
+  show-actuator: true
 management:
   endpoints:
     web:
@@ -48,4 +48,5 @@ app:
     http.proxy-host:
     http.proxy-port: 0
   vardata-directory: /var/information-coordinator-service
-
+  # If the file name is empty, no authorzation token is sent
+  auth-token-file:
\ No newline at end of file
index 6adf8ad..3847cc3 100644 (file)
@@ -25,6 +25,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 import java.lang.invoke.MethodHandles;
 
 import org.apache.catalina.connector.Connector;
+import org.oransc.ics.clients.SecurityContext;
 import org.oransc.ics.configuration.ApplicationConfig;
 import org.oransc.ics.controllers.r1producer.ProducerCallbacks;
 import org.oransc.ics.repository.InfoJobs;
@@ -65,9 +66,9 @@ class BeanFactory {
     }
 
     @Bean
-    public InfoJobs infoJobs() {
+    public InfoJobs infoJobs(SecurityContext securityContext) {
         if (infoJobs == null) {
-            infoJobs = new InfoJobs(getApplicationConfig(), producerCallbacks());
+            infoJobs = new InfoJobs(getApplicationConfig(), producerCallbacks(securityContext));
             try {
                 infoJobs.restoreJobsFromDatabase();
             } catch (Exception e) {
@@ -91,9 +92,9 @@ class BeanFactory {
     }
 
     @Bean
-    public ProducerCallbacks producerCallbacks() {
+    public ProducerCallbacks producerCallbacks(SecurityContext securityContext) {
         if (this.producerCallbacks == null) {
-            producerCallbacks = new ProducerCallbacks(getApplicationConfig());
+            producerCallbacks = new ProducerCallbacks(getApplicationConfig(), securityContext);
         }
         return this.producerCallbacks;
     }
index 4b22564..30c0d4a 100644 (file)
@@ -24,7 +24,6 @@ import io.swagger.v3.oas.annotations.OpenAPIDefinition;
 import io.swagger.v3.oas.annotations.info.Info;
 import io.swagger.v3.oas.annotations.info.License;
 
-
 /**
  * Swagger configuration class that uses swagger documentation type and scans
  * all the controllers. To access the swagger gui go to
index 1de7437..2879092 100644 (file)
@@ -35,10 +35,10 @@ import org.springframework.http.MediaType;
 import org.springframework.http.ResponseEntity;
 import org.springframework.http.client.reactive.ReactorClientHttpConnector;
 import org.springframework.lang.Nullable;
+import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
 import org.springframework.web.reactive.function.client.ExchangeStrategies;
 import org.springframework.web.reactive.function.client.WebClient;
 import org.springframework.web.reactive.function.client.WebClient.RequestHeadersSpec;
-import org.springframework.web.reactive.function.client.WebClientResponseException;
 
 import reactor.core.publisher.Mono;
 import reactor.netty.http.client.HttpClient;
@@ -55,17 +55,17 @@ public class AsyncRestClient {
     private static final AtomicInteger sequenceNumber = new AtomicInteger();
     private final SslContext sslContext;
     private final HttpProxyConfig httpProxyConfig;
+    private final SecurityContext securityContext;
 
-    public AsyncRestClient(String baseUrl, @Nullable SslContext sslContext, @Nullable HttpProxyConfig httpProxyConfig) {
+    public AsyncRestClient(String baseUrl, @Nullable SslContext sslContext, @Nullable HttpProxyConfig httpProxyConfig,
+        SecurityContext securityContext) {
         this.baseUrl = baseUrl;
         this.sslContext = sslContext;
         this.httpProxyConfig = httpProxyConfig;
+        this.securityContext = securityContext;
     }
 
     public Mono<ResponseEntity<String>> postForEntity(String uri, @Nullable String body) {
-        Object traceTag = createTraceTag();
-        logger.debug("{} POST uri = '{}{}''", traceTag, baseUrl, uri);
-        logger.trace("{} POST body: {}", traceTag, body);
         Mono<String> bodyProducer = body != null ? Mono.just(body) : Mono.empty();
 
         RequestHeadersSpec<?> request = getWebClient() //
@@ -73,7 +73,7 @@ public class AsyncRestClient {
             .uri(uri) //
             .contentType(MediaType.APPLICATION_JSON) //
             .body(bodyProducer, String.class);
-        return retrieve(traceTag, request);
+        return retrieve(request);
     }
 
     public Mono<String> post(String uri, @Nullable String body) {
@@ -82,41 +82,30 @@ public class AsyncRestClient {
     }
 
     public Mono<String> postWithAuthHeader(String uri, String body, String username, String password) {
-        Object traceTag = createTraceTag();
-        logger.debug("{} POST (auth) uri = '{}{}''", traceTag, baseUrl, uri);
-        logger.trace("{} POST body: {}", traceTag, body);
-
         RequestHeadersSpec<?> request = getWebClient() //
             .post() //
             .uri(uri) //
             .headers(headers -> headers.setBasicAuth(username, password)) //
             .contentType(MediaType.APPLICATION_JSON) //
             .bodyValue(body);
-        return retrieve(traceTag, request) //
+        return retrieve(request) //
             .map(this::toBody);
     }
 
     public Mono<ResponseEntity<String>> putForEntity(String uri, String body) {
-        Object traceTag = createTraceTag();
-        logger.debug("{} PUT uri = '{}{}''", traceTag, baseUrl, uri);
-        logger.trace("{} PUT body: {}", traceTag, body);
-
         RequestHeadersSpec<?> request = getWebClient() //
             .put() //
             .uri(uri) //
             .contentType(MediaType.APPLICATION_JSON) //
             .bodyValue(body);
-        return retrieve(traceTag, request);
+        return retrieve(request);
     }
 
     public Mono<ResponseEntity<String>> putForEntity(String uri) {
-        Object traceTag = createTraceTag();
-        logger.debug("{} PUT uri = '{}{}''", traceTag, baseUrl, uri);
-        logger.trace("{} PUT body: <empty>", traceTag);
         RequestHeadersSpec<?> request = getWebClient() //
             .put() //
             .uri(uri);
-        return retrieve(traceTag, request);
+        return retrieve(request);
     }
 
     public Mono<String> put(String uri, String body) {
@@ -125,10 +114,8 @@ public class AsyncRestClient {
     }
 
     public Mono<ResponseEntity<String>> getForEntity(String uri) {
-        Object traceTag = createTraceTag();
-        logger.debug("{} GET uri = '{}{}''", traceTag, baseUrl, uri);
         RequestHeadersSpec<?> request = getWebClient().get().uri(uri);
-        return retrieve(traceTag, request);
+        return retrieve(request);
     }
 
     public Mono<String> get(String uri) {
@@ -137,10 +124,8 @@ public class AsyncRestClient {
     }
 
     public Mono<ResponseEntity<String>> deleteForEntity(String uri) {
-        Object traceTag = createTraceTag();
-        logger.debug("{} DELETE uri = '{}{}''", traceTag, baseUrl, uri);
         RequestHeadersSpec<?> request = getWebClient().delete().uri(uri);
-        return retrieve(traceTag, request);
+        return retrieve(request);
     }
 
     public Mono<String> delete(String uri) {
@@ -148,32 +133,18 @@ public class AsyncRestClient {
             .map(this::toBody);
     }
 
-    private Mono<ResponseEntity<String>> retrieve(Object traceTag, RequestHeadersSpec<?> request) {
-        final Class<String> clazz = String.class;
+    private Mono<ResponseEntity<String>> retrieve(RequestHeadersSpec<?> request) {
+        if (securityContext.isConfigured()) {
+            request.headers(h -> h.setBearerAuth(securityContext.getBearerAuthToken()));
+        }
         return request.retrieve() //
-            .toEntity(clazz) //
-            .doOnNext(entity -> logReceivedData(traceTag, entity)) //
-            .doOnError(throwable -> onHttpError(traceTag, throwable));
-    }
-
-    private void logReceivedData(Object traceTag, ResponseEntity<String> entity) {
-        logger.trace("{} Received: {} {}", traceTag, entity.getBody(), entity.getHeaders().getContentType());
+            .toEntity(String.class);
     }
 
     private static Object createTraceTag() {
         return sequenceNumber.incrementAndGet();
     }
 
-    private void onHttpError(Object traceTag, Throwable t) {
-        if (t instanceof WebClientResponseException) {
-            WebClientResponseException exception = (WebClientResponseException) t;
-            logger.debug("{} HTTP error status = '{}', body '{}'", traceTag, exception.getStatusCode(),
-                exception.getResponseBodyAsString());
-        } else {
-            logger.debug("{} HTTP error {}", traceTag, t.getMessage());
-        }
-    }
-
     private String toBody(ResponseEntity<String> entity) {
         if (entity.getBody() == null) {
             return "";
@@ -206,15 +177,30 @@ public class AsyncRestClient {
         return httpClient;
     }
 
-    private WebClient buildWebClient(String baseUrl) {
+    public WebClient buildWebClient(String baseUrl) {
+        Object traceTag = createTraceTag();
+
         final HttpClient httpClient = buildHttpClient();
         ExchangeStrategies exchangeStrategies = ExchangeStrategies.builder() //
             .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(-1)) //
             .build();
+
+        ExchangeFilterFunction reqLogger = ExchangeFilterFunction.ofRequestProcessor(req -> {
+            logger.debug("{} {} uri = '{}''", traceTag, req.method(), req.url());
+            return Mono.just(req);
+        });
+
+        ExchangeFilterFunction respLogger = ExchangeFilterFunction.ofResponseProcessor(resp -> {
+            logger.debug("{} resp: {}", traceTag, resp.statusCode());
+            return Mono.just(resp);
+        });
+
         return WebClient.builder() //
             .clientConnector(new ReactorClientHttpConnector(httpClient)) //
             .baseUrl(baseUrl) //
             .exchangeStrategies(exchangeStrategies) //
+            .filter(reqLogger) //
+            .filter(respLogger) //
             .build();
     }
 
index 0b47733..9a6c4f7 100644 (file)
@@ -54,8 +54,9 @@ public class AsyncRestClientFactory {
 
     private final SslContextFactory sslContextFactory;
     private final HttpProxyConfig httpProxyConfig;
+    private final SecurityContext securityContext;
 
-    public AsyncRestClientFactory(WebClientConfig clientConfig) {
+    public AsyncRestClientFactory(WebClientConfig clientConfig, SecurityContext securityContext) {
         if (clientConfig != null) {
             this.sslContextFactory = new CachingSslContextFactory(clientConfig);
             this.httpProxyConfig = clientConfig.httpProxyConfig();
@@ -64,6 +65,7 @@ public class AsyncRestClientFactory {
             this.sslContextFactory = null;
             this.httpProxyConfig = null;
         }
+        this.securityContext = securityContext;
     }
 
     public AsyncRestClient createRestClientNoHttpProxy(String baseUrl) {
@@ -78,13 +80,13 @@ public class AsyncRestClientFactory {
         if (this.sslContextFactory != null) {
             try {
                 return new AsyncRestClient(baseUrl, this.sslContextFactory.createSslContext(),
-                    useHttpProxy ? httpProxyConfig : null);
+                    useHttpProxy ? httpProxyConfig : null, this.securityContext);
             } catch (Exception e) {
                 String exceptionString = e.toString();
                 logger.error("Could not init SSL context, reason: {}", exceptionString);
             }
         }
-        return new AsyncRestClient(baseUrl, null, httpProxyConfig);
+        return new AsyncRestClient(baseUrl, null, httpProxyConfig, this.securityContext);
     }
 
     private class SslContextFactory {
diff --git a/information-coordinator-service/src/main/java/org/oransc/ics/clients/SecurityContext.java b/information-coordinator-service/src/main/java/org/oransc/ics/clients/SecurityContext.java
new file mode 100644 (file)
index 0000000..aadc1bf
--- /dev/null
@@ -0,0 +1,76 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2022 Nordix Foundation
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================LICENSE_END===================================
+ */
+
+package org.oransc.ics.clients;
+
+import java.lang.invoke.MethodHandles;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import lombok.Setter;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+@EnableConfigurationProperties
+@ConfigurationProperties()
+@Component
+public class SecurityContext {
+
+    private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+    private long tokenTimestamp = 0;
+
+    private String authToken = "";
+
+    @Setter
+    private Path authTokenFilePath;
+
+    public SecurityContext(@Value("${app.auth-token-file:\"\"}") String authTokenFilename) {
+        if (!authTokenFilename.isEmpty()) {
+            this.authTokenFilePath = Path.of(authTokenFilename);
+        }
+    }
+
+    public boolean isConfigured() {
+        return authTokenFilePath != null;
+    }
+
+    public synchronized String getBearerAuthToken() {
+        if (!isConfigured()) {
+            return "";
+        }
+        try {
+            long lastModified = authTokenFilePath.toFile().lastModified();
+            if (lastModified != this.tokenTimestamp) {
+                this.authToken = Files.readString(authTokenFilePath);
+                this.tokenTimestamp = lastModified;
+            }
+        } catch (Exception e) {
+            logger.warn("Could not read auth token file: {}, reason: {}", authTokenFilePath, e.getMessage());
+        }
+        return this.authToken;
+    }
+
+}
index b369261..15394ab 100644 (file)
@@ -28,6 +28,7 @@ import java.util.Collection;
 
 import org.oransc.ics.clients.AsyncRestClient;
 import org.oransc.ics.clients.AsyncRestClientFactory;
+import org.oransc.ics.clients.SecurityContext;
 import org.oransc.ics.configuration.ApplicationConfig;
 import org.oransc.ics.repository.InfoJob;
 import org.oransc.ics.repository.InfoJobs;
@@ -55,8 +56,9 @@ public class A1eCallbacks {
     private final InfoJobs eiJobs;
 
     @Autowired
-    public A1eCallbacks(ApplicationConfig config, InfoJobs eiJobs) {
-        AsyncRestClientFactory restClientFactory = new AsyncRestClientFactory(config.getWebClientConfig());
+    public A1eCallbacks(ApplicationConfig config, InfoJobs eiJobs, SecurityContext securityContext) {
+        AsyncRestClientFactory restClientFactory =
+            new AsyncRestClientFactory(config.getWebClientConfig(), securityContext);
         this.restClient = restClientFactory.createRestClientUseHttpProxy("");
         this.eiJobs = eiJobs;
     }
index b96c160..3db904d 100644 (file)
@@ -25,6 +25,7 @@ import com.google.gson.GsonBuilder;
 
 import org.oransc.ics.clients.AsyncRestClient;
 import org.oransc.ics.clients.AsyncRestClientFactory;
+import org.oransc.ics.clients.SecurityContext;
 import org.oransc.ics.configuration.ApplicationConfig;
 import org.oransc.ics.repository.InfoType;
 import org.oransc.ics.repository.InfoTypeSubscriptions;
@@ -45,9 +46,11 @@ public class ConsumerCallbacks implements InfoTypeSubscriptions.ConsumerCallback
 
     public static final String API_VERSION = "version_1";
 
-    public ConsumerCallbacks(@Autowired ApplicationConfig config,
-        @Autowired InfoTypeSubscriptions infoTypeSubscriptions) {
-        AsyncRestClientFactory restClientFactory = new AsyncRestClientFactory(config.getWebClientConfig());
+    @Autowired
+    public ConsumerCallbacks(ApplicationConfig config, InfoTypeSubscriptions infoTypeSubscriptions,
+        SecurityContext securityContext) {
+        AsyncRestClientFactory restClientFactory =
+            new AsyncRestClientFactory(config.getWebClientConfig(), securityContext);
         this.restClient = restClientFactory.createRestClientNoHttpProxy("");
         infoTypeSubscriptions.registerCallbackhandler(this, API_VERSION);
     }
index c861d20..0881cd7 100644 (file)
@@ -29,6 +29,7 @@ import java.util.Collection;
 
 import org.oransc.ics.clients.AsyncRestClient;
 import org.oransc.ics.clients.AsyncRestClientFactory;
+import org.oransc.ics.clients.SecurityContext;
 import org.oransc.ics.configuration.ApplicationConfig;
 import org.oransc.ics.repository.InfoJob;
 import org.oransc.ics.repository.InfoJobs;
@@ -52,8 +53,9 @@ public class ProducerCallbacks {
 
     private final AsyncRestClient restClient;
 
-    public ProducerCallbacks(ApplicationConfig config) {
-        AsyncRestClientFactory restClientFactory = new AsyncRestClientFactory(config.getWebClientConfig());
+    public ProducerCallbacks(ApplicationConfig config, SecurityContext securityContext) {
+        AsyncRestClientFactory restClientFactory =
+            new AsyncRestClientFactory(config.getWebClientConfig(), securityContext);
         this.restClient = restClientFactory.createRestClientNoHttpProxy("");
     }
 
index cc7e16f..975bf81 100644 (file)
@@ -34,7 +34,10 @@ import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
 import java.io.PrintStream;
 import java.lang.invoke.MethodHandles;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.Arrays;
+import java.util.Map;
 
 import org.json.JSONObject;
 import org.junit.jupiter.api.AfterEach;
@@ -43,6 +46,7 @@ import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.oransc.ics.clients.AsyncRestClient;
 import org.oransc.ics.clients.AsyncRestClientFactory;
+import org.oransc.ics.clients.SecurityContext;
 import org.oransc.ics.configuration.ApplicationConfig;
 import org.oransc.ics.configuration.ImmutableHttpProxyConfig;
 import org.oransc.ics.configuration.ImmutableWebClientConfig;
@@ -145,6 +149,9 @@ class ApplicationTest {
     @Autowired
     InfoTypeSubscriptions infoTypeSubscriptions;
 
+    @Autowired
+    SecurityContext securityContext;
+
     private static Gson gson = new GsonBuilder().create();
 
     /**
@@ -170,6 +177,7 @@ class ApplicationTest {
         this.producerSimulator.getTestResults().reset();
         this.consumerSimulator.getTestResults().reset();
         this.a1eCallbacksSimulator.getTestResults().reset();
+        this.securityContext.setAuthTokenFilePath(null);
     }
 
     @AfterEach
@@ -386,6 +394,7 @@ class ApplicationTest {
         ProducerSimulatorController.TestResults simulatorResults = this.producerSimulator.getTestResults();
         await().untilAsserted(() -> assertThat(simulatorResults.jobsStopped).hasSize(1));
         assertThat(simulatorResults.jobsStopped.get(0)).isEqualTo("jobId");
+
     }
 
     @Test
@@ -1053,6 +1062,30 @@ class ApplicationTest {
             "Could not find Information subscription: junk");
     }
 
+    @Test
+    void testAuthHeader() throws Exception {
+        final String AUTH_TOKEN = "testToken";
+        Path authFile = Files.createTempFile("icsTestAuthToken", ".txt");
+        Files.write(authFile, AUTH_TOKEN.getBytes());
+        this.securityContext.setAuthTokenFilePath(authFile);
+        putInfoProducerWithOneType(PRODUCER_ID, TYPE_ID);
+        putInfoJob(TYPE_ID, "jobId");
+
+        // Test that authorization header is sent to the producer.
+        await().untilAsserted(() -> assertThat(this.producerSimulator.getTestResults().receivedHeaders).hasSize(1));
+        Map<String, String> headers = this.producerSimulator.getTestResults().receivedHeaders.get(0);
+        assertThat(headers).containsEntry("authorization", "Bearer " + AUTH_TOKEN);
+
+        Files.delete(authFile);
+
+        // Test that it works. The cached header is used
+        putInfoJob(TYPE_ID, "jobId2");
+        await().untilAsserted(() -> assertThat(this.infoJobs.size()).isEqualByComparingTo(2));
+        headers = this.producerSimulator.getTestResults().receivedHeaders.get(1);
+        assertThat(headers).containsEntry("authorization", "Bearer " + AUTH_TOKEN);
+
+    }
+
     private String typeSubscriptionUrl() {
         return ConsumerConsts.API_ROOT + "/info-type-subscription";
     }
@@ -1215,7 +1248,7 @@ class ApplicationTest {
             .trustStorePassword(config.trustStorePassword()) //
             .httpProxyConfig(httpProxyConfig).build();
 
-        AsyncRestClientFactory restClientFactory = new AsyncRestClientFactory(config);
+        AsyncRestClientFactory restClientFactory = new AsyncRestClientFactory(config, securityContext);
         return restClientFactory.createRestClientNoHttpProxy(baseUrl());
     }
 
index 6735447..3f5f78d 100644 (file)
@@ -52,13 +52,15 @@ class AsyncRestClientTest {
 
     private static AsyncRestClient clientUnderTest;
 
+    private static final SecurityContext securityContext = new SecurityContext("");
+
     @BeforeAll
     static void init() {
         // skip a lot of unnecessary logs from MockWebServer
         InternalLoggerFactory.setDefaultFactory(JdkLoggerFactory.INSTANCE);
         Loggers.useJdkLoggers();
         mockWebServer = new MockWebServer();
-        clientUnderTest = new AsyncRestClient(mockWebServer.url(BASE_URL).toString(), null, null);
+        clientUnderTest = new AsyncRestClient(mockWebServer.url(BASE_URL).toString(), null, null, securityContext);
     }
 
     @AfterAll
index 9e38394..a5ddc49 100644 (file)
@@ -31,6 +31,7 @@ import java.lang.invoke.MethodHandles;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 
 import lombok.Getter;
 
@@ -49,6 +50,7 @@ import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestHeader;
 import org.springframework.web.bind.annotation.RestController;
 
 @RestController("ProducerSimulatorController")
@@ -67,6 +69,8 @@ public class ProducerSimulatorController {
 
         public List<ProducerJobInfo> jobsStarted = Collections.synchronizedList(new ArrayList<ProducerJobInfo>());
         public List<String> jobsStopped = Collections.synchronizedList(new ArrayList<String>());
+        public List<Map<String, String>> receivedHeaders =
+            Collections.synchronizedList(new ArrayList<Map<String, String>>());
         public int noOfRejectedCreate = 0;
         public int noOfRejectedDelete = 0;
         public boolean errorFound = false;
@@ -77,6 +81,7 @@ public class ProducerSimulatorController {
         public void reset() {
             jobsStarted.clear();
             jobsStopped.clear();
+            receivedHeaders.clear();
             this.errorFound = false;
             this.noOfRejectedCreate = 0;
             this.noOfRejectedDelete = 0;
@@ -98,9 +103,12 @@ public class ProducerSimulatorController {
                 content = @Content(schema = @Schema(implementation = VoidResponse.class))) //
         })
     public ResponseEntity<Object> jobCreatedCallback( //
+        @RequestHeader Map<String, String> headers, //
         @RequestBody ProducerJobInfo request) {
         try {
+            logHeaders(headers);
             this.testResults.jobsStarted.add(request);
+            this.testResults.receivedHeaders.add(headers);
             logger.info("Job started callback {}", request.id);
             if (request.id == null) {
                 throw new NullPointerException("Illegal argument");
@@ -124,10 +132,12 @@ public class ProducerSimulatorController {
                 content = @Content(schema = @Schema(implementation = VoidResponse.class))) //
         })
     public ResponseEntity<Object> jobDeletedCallback( //
-        @PathVariable(ConsumerConsts.INFO_JOB_ID_PATH) String infoJobId) {
+        @RequestHeader Map<String, String> headers, @PathVariable(ConsumerConsts.INFO_JOB_ID_PATH) String infoJobId) {
         try {
+            logHeaders(headers);
             logger.info("Job deleted callback {}", infoJobId);
             this.testResults.jobsStopped.add(infoJobId);
+            this.testResults.receivedHeaders.add(headers);
             return new ResponseEntity<>(HttpStatus.OK);
         } catch (Exception e) {
             return ErrorResponse.create(e, HttpStatus.NOT_FOUND);
@@ -179,7 +189,7 @@ public class ProducerSimulatorController {
         })
     public ResponseEntity<Object> producerSupervision() {
         logger.info("Producer supervision");
-        return new ResponseEntity<>(HttpStatus.OK);
+        return new ResponseEntity<>("Hunky dory", HttpStatus.OK);
     }
 
     @GetMapping(path = SUPERVISION_ERROR_URL, produces = MediaType.APPLICATION_JSON_VALUE)
@@ -196,4 +206,10 @@ public class ProducerSimulatorController {
         return new ResponseEntity<>(HttpStatus.NOT_FOUND);
     }
 
+    private void logHeaders(Map<String, String> headers) {
+        logger.debug("Header begin");
+        headers.forEach((key, value) -> logger.debug("  key: {}, value: {}", key, value));
+        logger.debug("Header end");
+    }
+
 }