diff --git a/.gitignore b/.gitignore
index 415b3bf64..426b9eddd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,7 +18,6 @@ dist/
nbdist/
nbactions.xml
nb-configuration.xml
-META-INF/
datavault-broker/src/main/webapp/WEB-INF/glassfish-web.xml
datavault-webapp/src/main/webapp/WEB-INF/glassfish-web.xml
## Ignore all *.properties file in root folder, EXCEPT datavault.properties (the default)
@@ -43,4 +42,7 @@ datavault-webapp/pids
# ignore intellij run files
.run/
TEMPLATES/*
-dv5/local-db/docker/backup.D.SPEED.sql
\ No newline at end of file
+dv5/local-db/docker/backup.D.SPEED.sql
+# this can set the java version for the Intellij IDE and will set java versions for terminals too if 'sdk config set sdkman_auto_env true'
+.sdkmanrc
+SWAGGER_OPENAPI/*.zip
\ No newline at end of file
diff --git a/datavault-broker/pom.xml b/datavault-broker/pom.xml
index 7f6e6bcc3..cc0b722cc 100644
--- a/datavault-broker/pom.xml
+++ b/datavault-broker/pom.xml
@@ -149,6 +149,15 @@
com.fasterxml.jackson.core
jackson-databind
+
+ io.micrometer
+ micrometer-tracing-bridge-otel
+
+
+ io.micrometer
+ micrometer-tracing-test
+ test
+
diff --git a/datavault-broker/src/main/java/org/datavaultplatform/broker/config/ActuatorConfig.java b/datavault-broker/src/main/java/org/datavaultplatform/broker/config/ActuatorConfig.java
index aadcab55f..64b5050d2 100644
--- a/datavault-broker/src/main/java/org/datavaultplatform/broker/config/ActuatorConfig.java
+++ b/datavault-broker/src/main/java/org/datavaultplatform/broker/config/ActuatorConfig.java
@@ -4,14 +4,15 @@
import java.util.List;
import java.util.function.Function;
-import io.swagger.v3.oas.models.OpenAPI;
-import io.swagger.v3.oas.models.info.Info;
import org.datavaultplatform.broker.actuator.CurrentTimeEndpoint;
import org.datavaultplatform.broker.actuator.LocalFileStoreEndpoint;
import org.datavaultplatform.broker.actuator.MemoryInfoEndpoint;
import org.datavaultplatform.broker.actuator.SftpFileStoreEndpoint;
import org.datavaultplatform.broker.services.ArchiveStoreService;
import org.datavaultplatform.broker.services.FileStoreService;
+import org.datavaultplatform.common.actuator.ActuatorHealthSecurityAdvice;
+import org.datavaultplatform.common.actuator.ActuatorInfoSecurityAdvice;
+import org.datavaultplatform.common.actuator.ActuatorSecurityAdvice;
import org.datavaultplatform.common.util.StorageClassNameResolver;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
@@ -21,6 +22,21 @@
public class ActuatorConfig {
+ @Bean
+ ActuatorInfoSecurityAdvice actuatorInfoSecurityAdvice() {
+ return new ActuatorInfoSecurityAdvice();
+ }
+
+ @Bean
+ ActuatorHealthSecurityAdvice actuatorHealthSecurityAdvice() {
+ return new ActuatorHealthSecurityAdvice();
+ }
+
+ @Bean
+ ActuatorSecurityAdvice actuatorSecurityAdvice() {
+ return new ActuatorSecurityAdvice();
+ }
+
@Bean
Clock clock() {
return Clock.systemDefaultZone();
@@ -56,11 +72,4 @@ public SftpFileStoreEndpoint sftpFileStoreEndpoint(@Autowired FileStoreService
public LocalFileStoreEndpoint localFileStoreEndpoint(@Autowired ArchiveStoreService archiveStoreService) {
return new LocalFileStoreEndpoint(archiveStoreService);
}
-
- @Bean
- public OpenAPI openAPI() {
- return new OpenAPI().info(new Info().title("DataVault Broker")
- .description("broker application")
- .version("v0.0.1"));
- }
}
diff --git a/datavault-broker/src/main/java/org/datavaultplatform/broker/config/SecurityActuatorConfig.java b/datavault-broker/src/main/java/org/datavaultplatform/broker/config/SecurityActuatorConfig.java
index ea4ad855f..3bee1a028 100644
--- a/datavault-broker/src/main/java/org/datavaultplatform/broker/config/SecurityActuatorConfig.java
+++ b/datavault-broker/src/main/java/org/datavaultplatform/broker/config/SecurityActuatorConfig.java
@@ -6,6 +6,7 @@
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Profile;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
@@ -19,6 +20,8 @@
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
+import static org.springframework.security.config.Customizer.withDefaults;
+
@ConditionalOnExpression("${broker.security.enabled:true}")
@Configuration
@Slf4j
@@ -45,26 +48,41 @@ public DaoAuthenticationProvider actuatorAuthenticationProvider(@Qualifier("actu
return provider;
}
+ @Bean
+ @Order(0)
+ @Profile("database")
+ public SecurityFilterChain traceApiFilterChain(HttpSecurity http, AuthenticationProvider actuatorAuthenticationProvider) throws Exception {
+ return http
+ .securityMatcher("/trace/**")
+ .csrf(AbstractHttpConfigurer::disable)
+ .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
+ .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
+ .authenticationProvider(actuatorAuthenticationProvider)
+ .httpBasic(withDefaults())
+ .build();
+ }
+
@Bean
@Order(1)
public SecurityFilterChain actuatorSecurityFilterChain(HttpSecurity http,
@Qualifier("actuatorAuthenticationProvider") AuthenticationProvider authenticationProvider) throws Exception {
- http.securityMatcher("/actuator/**","/v3/**","/swagger-ui/**")
- .authenticationProvider( authenticationProvider )
+ http.securityMatcher("/actuator/**")
+ .authenticationProvider(authenticationProvider)
.csrf(AbstractHttpConfigurer::disable)
.httpBasic(Customizer.withDefaults())
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
- .authorizeHttpRequests( authz -> {
- authz.requestMatchers(
- "/v3/**",
- "/swagger-ui/**",
- "/actuator/info",
- "/actuator/health",
- "/actuator/metrics",
- "/actuator/mappings",
- "/actuator/memoryinfo").permitAll();
- authz.anyRequest().fullyAuthenticated();
- });
+ .authorizeHttpRequests(authz -> authz
+ // 1. Allow these specific endpoints without login
+ .requestMatchers(
+ "/actuator",
+ "/actuator/info",
+ "/actuator/health"
+ ).permitAll()
+
+ // 2. Require authentication for everything else covered by the securityMatcher
+ // (This includes Swagger, V3 docs, and the rest of the actuator endpoints)
+ .anyRequest().authenticated()
+ );
return http.build();
}
diff --git a/datavault-broker/src/main/java/org/datavaultplatform/broker/config/SecurityConfig.java b/datavault-broker/src/main/java/org/datavaultplatform/broker/config/SecurityConfig.java
index 5e9b3ef92..245a7836c 100644
--- a/datavault-broker/src/main/java/org/datavaultplatform/broker/config/SecurityConfig.java
+++ b/datavault-broker/src/main/java/org/datavaultplatform/broker/config/SecurityConfig.java
@@ -1,22 +1,16 @@
package org.datavaultplatform.broker.config;
-import static org.datavaultplatform.common.util.Constants.HEADER_USER_ID;
-
-import java.io.IOException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import lombok.extern.slf4j.Slf4j;
-import org.datavaultplatform.broker.authentication.RestAuthenticationFailureHandler;
-import org.datavaultplatform.broker.authentication.RestAuthenticationFilter;
-import org.datavaultplatform.broker.authentication.RestAuthenticationProvider;
-import org.datavaultplatform.broker.authentication.RestAuthenticationSuccessHandler;
-import org.datavaultplatform.broker.authentication.RestWebAuthenticationDetailsSource;
+import org.datavaultplatform.broker.authentication.*;
import org.datavaultplatform.broker.services.AdminService;
import org.datavaultplatform.broker.services.ClientsService;
import org.datavaultplatform.broker.services.RolesAndPermissionsService;
import org.datavaultplatform.broker.services.UsersService;
+import org.datavaultplatform.common.config.SecurityMethod;
import org.datavaultplatform.common.util.Constants;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
@@ -28,6 +22,7 @@
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.ProviderManager;
+import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
@@ -45,6 +40,12 @@
import org.springframework.web.filter.CommonsRequestLoggingFilter;
import org.springframework.web.filter.GenericFilterBean;
+import java.io.IOException;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import static org.datavaultplatform.common.util.Constants.HEADER_USER_ID;
+
@SuppressWarnings("DefaultAnnotationParam")
@ConditionalOnExpression("${broker.security.enabled:true}")
@Configuration
@@ -60,7 +61,6 @@ public class SecurityConfig {
WebSecurityCustomizer webSecurityCustomizer() {
return web -> {
web.debug(securityDebug);
- web.ignoring().requestMatchers("/retrieve/**");
};
}
@@ -78,24 +78,66 @@ public SecurityFilterChain securityFilterChain(
.addFilterAt(restFilter(authenticationManager), AbstractPreAuthenticatedProcessingFilter.class)
.sessionManagement(cust -> cust.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.exceptionHandling(ex -> ex.authenticationEntryPoint(http403EntryPoint()))
- .authorizeHttpRequests(authz -> authz
- .requestMatchers("/admin/users/**").hasAuthority("ROLE_ADMIN")
- .requestMatchers("/admin/archivestores/**").hasAuthority("ROLE_ADMIN_ARCHIVESTORES")
- .requestMatchers("/admin/deposits/**").hasAuthority("ROLE_ADMIN_DEPOSITS")
- .requestMatchers("/admin/retrieves/**").hasAuthority("ROLE_ADMIN_RETRIEVES")
- .requestMatchers("/admin/vaults/**").hasAuthority("ROLE_ADMIN_VAULTS")
- .requestMatchers("/admin/pendingVaults/**").hasAuthority("ROLE_ADMIN_PENDING_VAULTS")
- .requestMatchers("/admin/events/**").hasAuthority("ROLE_ADMIN_EVENTS")
- .requestMatchers("/admin/billing/**").hasAuthority("ROLE_ADMIN_BILLING")
- /* TODO : DavidHay : no controller mapped to /admin/reviews ! */
- .requestMatchers("/admin/reviews/**").hasAuthority("ROLE_ADMIN_REVIEWS")
- .requestMatchers("/admin/paused/deposit/toggle/**").hasAuthority("ROLE_ADMIN")
- .requestMatchers("/admin/paused/retrieve/toggle/**").hasAuthority("ROLE_ADMIN")
- .anyRequest().authenticated());
+ .authorizeHttpRequests(getAuthZCustomizer());
return http.build();
}
+ public static final Map SECURITY_PATH_MAP = new LinkedHashMap<>();
+
+ static {
+ SECURITY_PATH_MAP.put("/admin/users/**", "hasAuthority('ROLE_ADMIN')");
+ SECURITY_PATH_MAP.put("/admin/archivestores/**", "hasAuthority('ROLE_ADMIN_ARCHIVESTORES')");
+ SECURITY_PATH_MAP.put("/admin/deposits/**", "hasAuthority('ROLE_ADMIN_DEPOSITS')");
+ SECURITY_PATH_MAP.put("/admin/retrieves/**", "hasAuthority('ROLE_ADMIN_RETRIEVES')");
+ SECURITY_PATH_MAP.put("/admin/vaults/**", "hasAuthority('ROLE_ADMIN_VAULTS')");
+ SECURITY_PATH_MAP.put("/admin/pendingVaults/**", "hasAuthority('ROLE_ADMIN_PENDING_VAULTS')");
+ SECURITY_PATH_MAP.put("/admin/events/**", "hasAuthority('ROLE_ADMIN_EVENTS')");
+ SECURITY_PATH_MAP.put("/admin/billing/**", "hasAuthority('ROLE_ADMIN_BILLING')");
+ /* TODO : DavidHay : no controller mapped to /admin/reviews ! */
+ SECURITY_PATH_MAP.put("/admin/reviews/**", "hasAuthority('ROLE_ADMIN_REVIEWS')");
+ SECURITY_PATH_MAP.put("/admin/paused/deposit/toggle/**", "hasAuthority('ROLE_ADMIN')");
+ SECURITY_PATH_MAP.put("/admin/paused/retrieve/toggle/**", "hasAuthority('ROLE_ADMIN')");
+ }
+
+ public Customizer.AuthorizationManagerRequestMatcherRegistry> getAuthZCustomizer() {
+ return authz -> {
+
+ for(Map.Entry entry : SECURITY_PATH_MAP.entrySet()) {
+ var matchers = authz.requestMatchers(entry.getKey());
+ SecurityMethod sm = SecurityMethod.from(entry.getValue());
+ if (sm.isPermitAll()) {
+ matchers.permitAll();
+
+ } else if (sm.isHasRole()) {
+ matchers.hasRole(sm.arg());
+
+ } else if (sm.isHasAuthority()) {
+ matchers.hasAuthority(sm.arg());
+
+ } else {
+ throw new RuntimeException("Unknown security method: " + sm.method());
+ }
+ }
+ authz.anyRequest().authenticated();
+
+// authz
+// .requestMatchers("/admin/users/**").hasAuthority("ROLE_ADMIN")
+// .requestMatchers("/admin/archivestores/**").hasAuthority("ROLE_ADMIN_ARCHIVESTORES")
+// .requestMatchers("/admin/deposits/**").hasAuthority("ROLE_ADMIN_DEPOSITS")
+// .requestMatchers("/admin/retrieves/**").hasAuthority("ROLE_ADMIN_RETRIEVES")
+// .requestMatchers("/admin/vaults/**").hasAuthority("ROLE_ADMIN_VAULTS")
+// .requestMatchers("/admin/pendingVaults/**").hasAuthority("ROLE_ADMIN_PENDING_VAULTS")
+// .requestMatchers("/admin/events/**").hasAuthority("ROLE_ADMIN_EVENTS")
+// .requestMatchers("/admin/billing/**").hasAuthority("ROLE_ADMIN_BILLING")
+// /* TODO : DavidHay : no controller mapped to /admin/reviews ! */
+// .requestMatchers("/admin/reviews/**").hasAuthority("ROLE_ADMIN_REVIEWS")
+// .requestMatchers("/admin/paused/deposit/toggle/**").hasAuthority("ROLE_ADMIN")
+// .requestMatchers("/admin/paused/retrieve/toggle/**").hasAuthority("ROLE_ADMIN")
+// .anyRequest().authenticated();
+ };
+ }
+
/**
diff --git a/datavault-broker/src/main/java/org/datavaultplatform/broker/config/TracingConfig.java b/datavault-broker/src/main/java/org/datavaultplatform/broker/config/TracingConfig.java
new file mode 100644
index 000000000..9532ce1d7
--- /dev/null
+++ b/datavault-broker/src/main/java/org/datavaultplatform/broker/config/TracingConfig.java
@@ -0,0 +1,17 @@
+package org.datavaultplatform.broker.config;
+
+import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator;
+import io.opentelemetry.context.propagation.ContextPropagators;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+
+@Configuration
+@Import(UserMdcFilter.class)
+public class TracingConfig {
+
+ @Bean
+ ContextPropagators otelContextPropagators() {
+ return ContextPropagators.create(W3CTraceContextPropagator.getInstance());
+ }
+}
diff --git a/datavault-broker/src/main/java/org/datavaultplatform/broker/config/UserMdcFilter.java b/datavault-broker/src/main/java/org/datavaultplatform/broker/config/UserMdcFilter.java
new file mode 100644
index 000000000..42345a6a9
--- /dev/null
+++ b/datavault-broker/src/main/java/org/datavaultplatform/broker/config/UserMdcFilter.java
@@ -0,0 +1,43 @@
+package org.datavaultplatform.broker.config;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.datavaultplatform.common.util.MdcUtils;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.core.annotation.Order;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+
+@Component
+@ConditionalOnClass(Authentication.class)
+@Order(101) // Ensure it runs after Spring Security Filter (default 100)
+public class UserMdcFilter extends OncePerRequestFilter {
+
+ @Override
+ protected void doFilterInternal(HttpServletRequest request,
+ HttpServletResponse response,
+ FilterChain filterChain)
+ throws ServletException, IOException {
+
+ Authentication auth = SecurityContextHolder.getContext().getAuthentication();
+
+ String username = null;
+ if (auth != null && auth.isAuthenticated()) {
+ username = auth.getName();
+ }
+ username = MdcUtils.getMdcUserName(username);
+ MdcUtils.addUserNameToMdc(username);
+
+ try {
+ filterChain.doFilter(request, response);
+ } finally {
+ MdcUtils.removeMdcUserName();
+ }
+ }
+}
diff --git a/datavault-broker/src/main/java/org/datavaultplatform/broker/config/WebConfig.java b/datavault-broker/src/main/java/org/datavaultplatform/broker/config/WebConfig.java
index efea99613..6f596d6f3 100644
--- a/datavault-broker/src/main/java/org/datavaultplatform/broker/config/WebConfig.java
+++ b/datavault-broker/src/main/java/org/datavaultplatform/broker/config/WebConfig.java
@@ -1,10 +1,12 @@
package org.datavaultplatform.broker.config;
import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
+@Import(TracingConfig.class)
public class WebConfig implements WebMvcConfigurer {
@Override
diff --git a/datavault-broker/src/main/java/org/datavaultplatform/broker/controllers/TraceController.java b/datavault-broker/src/main/java/org/datavaultplatform/broker/controllers/TraceController.java
new file mode 100644
index 000000000..3c54fea56
--- /dev/null
+++ b/datavault-broker/src/main/java/org/datavaultplatform/broker/controllers/TraceController.java
@@ -0,0 +1,82 @@
+package org.datavaultplatform.broker.controllers;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.micrometer.tracing.Tracer;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import org.datavaultplatform.broker.queue.Sender;
+import org.datavaultplatform.common.event.Event;
+import org.datavaultplatform.common.model.ArchiveStore;
+import org.datavaultplatform.common.model.Job;
+import org.datavaultplatform.common.task.Task;
+import org.datavaultplatform.common.util.TraceInfo;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Profile;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@Profile("database")
+@RequestMapping("/trace")
+@Slf4j
+public class TraceController {
+
+ private final Tracer tracer;
+ private final Sender sender;
+ private final ObjectMapper mapper;
+ @Autowired
+ public TraceController(Tracer tracer, Sender sender, ObjectMapper mapper) {
+ this.tracer = tracer;
+ this.sender = sender;
+ this.mapper = mapper;
+ }
+
+ @GetMapping("/info")
+ public TraceInfo getTraceInfo() {
+ String traceId = tracer.currentSpan().context().traceId();
+ log.info("TraceId: {}", traceId);
+ return new TraceInfo(traceId);
+ }
+
+ @SneakyThrows
+ @GetMapping("/worker")
+ public TraceInfo sendTraceTaskToWorker() {
+ TraceInfo traceInfo = getTraceInfo();
+ Map properties = new HashMap<>();
+ properties.put("brokerTraceId", traceInfo.traceId());
+ List archiveStores = List.of();
+ Map> userFileStoreProperties = Map.of();
+ Map userFileStoreClasses = Map.of();
+ Map chunkFilesDigest = Map.of();
+ byte[] tarIVs = new byte[0];
+ Map chunksIVs = Map.of();
+ String encTarDigest = "";
+ Map encChunksDigests = Map.of();
+ Event lastEvent = null;
+ Job job = new Job(){
+ @Override
+ public String getID() {
+ return "1234567890";
+ }
+ };
+ job.setState(0);
+ job.setTaskClass("org.datavaultplatform.worker.tasks.Trace");
+ Task retrieveTask = new Task(
+ job, properties, archiveStores,
+ userFileStoreProperties, userFileStoreClasses,
+ null, null,
+ chunkFilesDigest,
+ tarIVs, chunksIVs,
+ encTarDigest, encChunksDigests, lastEvent);
+ String jsonRetrieve = mapper.writeValueAsString(retrieveTask);
+
+ boolean isRestart = false;
+ sender.send(jsonRetrieve, isRestart);
+ return traceInfo;
+ }
+}
diff --git a/datavault-broker/src/main/java/org/datavaultplatform/broker/queue/RabbitUtils.java b/datavault-broker/src/main/java/org/datavaultplatform/broker/queue/RabbitUtils.java
index f93c97b56..0c0073877 100644
--- a/datavault-broker/src/main/java/org/datavaultplatform/broker/queue/RabbitUtils.java
+++ b/datavault-broker/src/main/java/org/datavaultplatform/broker/queue/RabbitUtils.java
@@ -2,6 +2,10 @@
import java.nio.charset.StandardCharsets;
import java.util.UUID;
+
+import io.micrometer.tracing.Span;
+import io.micrometer.tracing.Tracer;
+import io.micrometer.tracing.propagation.Propagator;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageProperties;
@@ -12,22 +16,31 @@ public abstract class RabbitUtils {
public static final String DEFAULT_EXCHANGE = "";
+ private RabbitUtils() {
+ }
public static String sendDirectToQueue(RabbitTemplate template, String queueName,
- String messageText) {
- return sendToRoutingKey(template, DEFAULT_EXCHANGE, queueName, messageText);
+ String messageText, Tracer tracer, Propagator propagator) {
+ return sendToRoutingKey(template, DEFAULT_EXCHANGE, queueName, messageText, tracer, propagator);
}
public static String sendToExchange(RabbitTemplate template, String exchangeName,
- String messageText) {
- return sendToRoutingKey(template, exchangeName, null, messageText);
+ String messageText, Tracer tracer, Propagator propagator) {
+ return sendToRoutingKey(template, exchangeName, null, messageText, tracer, propagator);
}
public static String sendToRoutingKey(RabbitTemplate template, String exchange, String routingKey,
- String messageText) {
+ String messageText, Tracer tracer, Propagator propagator) {
MessageProperties props = new MessageProperties();
props.setContentType(MessageProperties.CONTENT_TYPE_TEXT_PLAIN);
String messageId = UUID.randomUUID().toString();
props.setMessageId(messageId);
+ // propagate w3c tracing headers
+ Span currentSpan = tracer.currentSpan();
+ if (currentSpan != null) {
+ propagator.inject(currentSpan.context(), props, (carrier, key, value) -> {
+ carrier.setHeader(key, value);
+ });
+ }
Message message = new Message(messageText.getBytes(StandardCharsets.UTF_8), props);
template.send(exchange, routingKey, message);
log.info("Sent [{}] to [{}/{}]", message, exchange, routingKey);
diff --git a/datavault-broker/src/main/java/org/datavaultplatform/broker/queue/Sender.java b/datavault-broker/src/main/java/org/datavaultplatform/broker/queue/Sender.java
index acd93359d..95d2c1537 100644
--- a/datavault-broker/src/main/java/org/datavaultplatform/broker/queue/Sender.java
+++ b/datavault-broker/src/main/java/org/datavaultplatform/broker/queue/Sender.java
@@ -1,5 +1,7 @@
package org.datavaultplatform.broker.queue;
+import io.micrometer.tracing.Tracer;
+import io.micrometer.tracing.propagation.Propagator;
import lombok.extern.slf4j.Slf4j;
import org.datavaultplatform.common.config.BaseQueueConfig;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
@@ -14,21 +16,25 @@ public class Sender {
private final RabbitTemplate template;
private final String workerQueueName;
private final String restartExchangeName;
-
+ private final Tracer tracer;
+ private final Propagator propagator;
+
@Autowired
public Sender(@Value(BaseQueueConfig.WORKER_QUEUE_NAME) String workerQueueName,
@Value(BaseQueueConfig.RESTART_EXCHANGE_NAME) String restartExchangeName,
- RabbitTemplate template) {
+ RabbitTemplate template, Tracer tracer, Propagator propagator) {
this.template = template;
this.workerQueueName = workerQueueName;
this.restartExchangeName = restartExchangeName;
+ this.tracer = tracer;
+ this.propagator = propagator;
}
public String send(String messageText, boolean restart) {
if (restart) {
- return RabbitUtils.sendToExchange(template, restartExchangeName, messageText);
+ return RabbitUtils.sendToExchange(template, restartExchangeName, messageText, tracer, propagator);
} else {
- return RabbitUtils.sendDirectToQueue(template, workerQueueName, messageText);
+ return RabbitUtils.sendDirectToQueue(template, workerQueueName, messageText, tracer, propagator);
}
}
diff --git a/datavault-broker/src/test/java/org/datavaultplatform/broker/actuator/ActuatorTest.java b/datavault-broker/src/test/java/org/datavaultplatform/broker/actuator/ActuatorTest.java
index 22a46c39b..93ed8fee4 100644
--- a/datavault-broker/src/test/java/org/datavaultplatform/broker/actuator/ActuatorTest.java
+++ b/datavault-broker/src/test/java/org/datavaultplatform/broker/actuator/ActuatorTest.java
@@ -9,6 +9,7 @@
import org.datavaultplatform.broker.test.AddTestProperties;
import org.datavaultplatform.broker.test.BaseDatabaseTest;
import org.datavaultplatform.broker.test.TestClockConfig;
+import org.datavaultplatform.common.actuator.WithMockActuatorUser;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
@@ -65,23 +66,27 @@ void setup() {
}
@ParameterizedTest
- @ValueSource(strings = {"/actuator/info", "/actuator/health",
- "/actuator/metrics", "/actuator/memoryinfo", "/actuator/mappings"})
+ @ValueSource(strings = {"/actuator", "/actuator/", "/actuator/info", "/actuator/health"})
@SneakyThrows
void testActuatorPublicAccess(String url) {
checkPublic(url);
}
@ParameterizedTest
- @ValueSource(strings={"/actuator", "/actuator/", "/actuator/env", "/users", "/actuator/loggers"})
+ @ValueSource(strings = {"/actuator/env", "/actuator/customtime",
+ "/actuator/sftpfilestores", "/actuator/localfilestores",
+ "/actuator/env", "/actuator/loggers",
+ "/actuator/metrics", "/actuator/memoryinfo", "/actuator/mappings"})
@SneakyThrows
void testActuatorUnauthorized(String url) {
checkUnauthorized(url);
}
@ParameterizedTest
- @ValueSource(strings = {"/actuator", "/actuator/", "/actuator/env", "/actuator/customtime",
- "/actuator/sftpfilestores", "/actuator/localfilestores"})
+ @ValueSource(strings = {"/actuator/env", "/actuator/customtime",
+ "/actuator/sftpfilestores", "/actuator/localfilestores",
+ "/actuator/env", "/actuator/loggers",
+ "/actuator/metrics", "/actuator/memoryinfo", "/actuator/mappings"})
@SneakyThrows
void testActuatorAuthorized(String url) {
checkAuthorized(url, "bactor", "bactorpass");
@@ -123,6 +128,7 @@ void testCurrentTime() throws Exception {
}
@Test
+ @WithMockActuatorUser
void testMemoryInfo() throws Exception {
MvcResult mvcResult = mvc.perform(
get("/actuator/memoryinfo"))
diff --git a/datavault-broker/src/test/java/org/datavaultplatform/broker/actuator/OpenApiBrokerTest.java b/datavault-broker/src/test/java/org/datavaultplatform/broker/actuator/OpenApiBrokerTest.java
deleted file mode 100644
index d98f5c73f..000000000
--- a/datavault-broker/src/test/java/org/datavaultplatform/broker/actuator/OpenApiBrokerTest.java
+++ /dev/null
@@ -1,85 +0,0 @@
-package org.datavaultplatform.broker.actuator;
-
-import io.swagger.v3.oas.models.OpenAPI;
-import lombok.extern.slf4j.Slf4j;
-import org.datavaultplatform.broker.app.DataVaultBrokerApp;
-import org.datavaultplatform.broker.queue.Sender;
-import org.datavaultplatform.broker.services.FileStoreService;
-import org.datavaultplatform.broker.test.AddTestProperties;
-import org.datavaultplatform.broker.test.BaseDatabaseTest;
-import org.datavaultplatform.broker.test.TestClockConfig;
-import org.junit.jupiter.api.Test;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.boot.test.mock.mockito.MockBean;
-import org.springframework.context.annotation.Import;
-import org.springframework.test.context.TestPropertySource;
-import org.springframework.test.web.servlet.MockMvc;
-import org.springframework.test.web.servlet.MvcResult;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
-import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
-import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
-
-@SpringBootTest(classes = DataVaultBrokerApp.class)
-@Import(TestClockConfig.class)
-@AddTestProperties
-@Slf4j
-@TestPropertySource(properties = {
- "broker.email.enabled=true",
- "broker.controllers.enabled=true",
- "broker.initialise.enabled=true",
- "broker.rabbit.enabled=false",
- "broker.scheduled.enabled=false",
- "management.endpoints.web.exposure.include=*",
- "management.health.rabbit.enabled=false"})
-@AutoConfigureMockMvc
-public class OpenApiBrokerTest extends BaseDatabaseTest {
-
- @Autowired
- MockMvc mvc;
-
- @MockBean
- Sender sender;
-
- @MockBean
- FileStoreService mFileStoreService;
-
- @Autowired
- OpenAPI openApi;
-
- @Test
- void testOpenApi() {
- assertThat(openApi.getInfo().getTitle()).isEqualTo("DataVault Broker");
- assertThat(openApi.getInfo().getDescription()).isEqualTo("broker application");
- }
-
- @Test
- void testOpenApiAsJson() throws Exception {
- MvcResult mvcResult = mvc.perform(
- get("http://localhost:8080/v3/api-docs"))
- .andExpect(content().contentTypeCompatibleWith("application/json"))
- .andExpect(status().is2xxSuccessful())
- .andExpect(jsonPath("$.openapi").value("3.1.0"))
- .andExpect(jsonPath("$.info.title").value("DataVault Broker"))
- .andExpect(jsonPath("$.info.description").value("broker application"))
- .andExpect(jsonPath("$.info.version").value("v0.0.1"))
- .andExpect(jsonPath("$.paths['/permissions/role']").exists())
- .andDo(print())
- .andReturn();
- }
-
- @Test
- void testOpenApiAsSwaggerUI() throws Exception {
- MvcResult mvcResult = mvc.perform(
- get("http://localhost:8080/swagger-ui/index.html"))
- .andExpect(content().contentTypeCompatibleWith("text/html"))
- .andExpect(status().is2xxSuccessful())
- .andDo(print())
- .andReturn();
- }
-
-
-}
diff --git a/datavault-broker/src/test/java/org/datavaultplatform/broker/authentication/DepositsControllerRetrieveNoAuthTest.java b/datavault-broker/src/test/java/org/datavaultplatform/broker/authentication/DepositsControllerRetrieveNoAuthTest.java
deleted file mode 100644
index a08f9e73c..000000000
--- a/datavault-broker/src/test/java/org/datavaultplatform/broker/authentication/DepositsControllerRetrieveNoAuthTest.java
+++ /dev/null
@@ -1,33 +0,0 @@
-package org.datavaultplatform.broker.authentication;
-
-import org.datavaultplatform.broker.controllers.DepositsController;
-import org.junit.jupiter.api.Test;
-import org.springframework.boot.test.mock.mockito.MockBean;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
-import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
-import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
-import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
-
-public class DepositsControllerRetrieveNoAuthTest extends BaseControllerAuthTest {
-
- @MockBean
- DepositsController controller;
-
- @Test
- void testRestartRetrieveNoSecurity() throws Exception {
- when(controller.retrieveRestart("retrieve-id-1")).thenReturn(true);
-
- mvc.perform(
- post("/retrieve/{retrieveId}/restart", "retrieve-id-1")).andDo(print())
- .andExpect(status().isOk())
- .andExpect(content().contentTypeCompatibleWith("application/json"))
- .andExpect(content().string("true"));
-
- verify(controller).retrieveRestart("retrieve-id-1");
- }
-}
diff --git a/datavault-broker/src/test/java/org/datavaultplatform/broker/config/SecurityConfigTest.java b/datavault-broker/src/test/java/org/datavaultplatform/broker/config/SecurityConfigTest.java
new file mode 100644
index 000000000..0b98bf783
--- /dev/null
+++ b/datavault-broker/src/test/java/org/datavaultplatform/broker/config/SecurityConfigTest.java
@@ -0,0 +1,88 @@
+package org.datavaultplatform.broker.config;
+
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer;
+
+import java.util.Arrays;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.mockito.Mockito.*;
+
+class SecurityConfigTest {
+
+ public static final Logger LOG = LoggerFactory.getLogger(SecurityConfigTest.class);
+
+ @Test
+ void testCustomUserDetailsService(){
+
+ SecurityConfig securityConfig = new SecurityConfig();
+ var customizer = securityConfig.getAuthZCustomizer();
+
+ var mAuthz = Mockito.mock(AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry.class);
+
+ AuthorizeHttpRequestsConfigurer.AuthorizedUrl mAuthURL = mock(AuthorizeHttpRequestsConfigurer.AuthorizedUrl.class);
+ AtomicInteger counter = new AtomicInteger(0);
+ doAnswer(invocation -> {
+ LOG.info("requestMatchers {} {}", counter.incrementAndGet(), Arrays.toString(invocation.getArguments()));
+ return mAuthURL;
+ }).when(mAuthz).requestMatchers(any(String[].class));
+
+ when(mAuthz.anyRequest()).thenReturn(mAuthURL);
+ customizer.customize(mAuthz);
+
+ var inOrder = inOrder(mAuthz, mAuthURL);
+
+ //1
+ inOrder.verify(mAuthz).requestMatchers("/admin/users/**");
+ inOrder.verify(mAuthURL).hasAuthority("ROLE_ADMIN");
+
+ //2
+ inOrder.verify(mAuthz).requestMatchers("/admin/archivestores/**");
+ inOrder.verify(mAuthURL).hasAuthority("ROLE_ADMIN_ARCHIVESTORES");
+
+ //3
+ inOrder.verify(mAuthz).requestMatchers("/admin/deposits/**");
+ inOrder.verify(mAuthURL).hasAuthority("ROLE_ADMIN_DEPOSITS");
+
+ //4
+ inOrder.verify(mAuthz).requestMatchers("/admin/retrieves/**");
+ inOrder.verify(mAuthURL).hasAuthority("ROLE_ADMIN_RETRIEVES");
+
+ //5
+ inOrder.verify(mAuthz).requestMatchers("/admin/vaults/**");
+ inOrder.verify(mAuthURL).hasAuthority("ROLE_ADMIN_VAULTS");
+
+ //6
+ inOrder.verify(mAuthz).requestMatchers("/admin/pendingVaults/**");
+ inOrder.verify(mAuthURL).hasAuthority("ROLE_ADMIN_PENDING_VAULTS");
+
+ //7
+ inOrder.verify(mAuthz).requestMatchers("/admin/events/**");
+ inOrder.verify(mAuthURL).hasAuthority("ROLE_ADMIN_EVENTS");
+
+ //8
+ inOrder.verify(mAuthz).requestMatchers("/admin/billing/**");
+ inOrder.verify(mAuthURL).hasAuthority("ROLE_ADMIN_BILLING");
+
+ //9
+ inOrder.verify(mAuthz).requestMatchers("/admin/reviews/**");
+ inOrder.verify(mAuthURL).hasAuthority("ROLE_ADMIN_REVIEWS");
+
+ //10
+ inOrder.verify(mAuthz).requestMatchers("/admin/paused/deposit/toggle/**");
+ inOrder.verify(mAuthURL).hasAuthority("ROLE_ADMIN");
+
+ //11
+ inOrder.verify(mAuthz).requestMatchers("/admin/paused/retrieve/toggle/**");
+ inOrder.verify(mAuthURL).hasAuthority("ROLE_ADMIN");
+
+ //12
+ inOrder.verify(mAuthz).anyRequest();
+ inOrder.verify(mAuthURL).authenticated();
+
+ inOrder.verifyNoMoreInteractions();
+ }
+}
\ No newline at end of file
diff --git a/datavault-broker/src/test/java/org/datavaultplatform/broker/controllers/TraceControllerMvcIT.java b/datavault-broker/src/test/java/org/datavaultplatform/broker/controllers/TraceControllerMvcIT.java
new file mode 100644
index 000000000..6b3f432f7
--- /dev/null
+++ b/datavault-broker/src/test/java/org/datavaultplatform/broker/controllers/TraceControllerMvcIT.java
@@ -0,0 +1,166 @@
+package org.datavaultplatform.broker.controllers;
+
+import io.micrometer.tracing.propagation.Propagator;
+import io.opentelemetry.api.trace.TraceId;
+import io.opentelemetry.api.trace.Tracer;
+import lombok.extern.slf4j.Slf4j;
+import org.datavaultplatform.broker.app.DataVaultBrokerApp;
+import org.datavaultplatform.broker.config.MockRabbitConfig;
+import org.datavaultplatform.broker.config.MockServicesConfig;
+import org.datavaultplatform.broker.test.AddTestProperties;
+import org.datavaultplatform.broker.test.BaseDatabaseTest;
+import org.datavaultplatform.common.util.TraceInfo;
+import org.datavaultplatform.common.util.TraceUtils;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.actuate.observability.AutoConfigureObservability;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.web.server.LocalServerPort;
+import org.springframework.boot.web.client.RestTemplateBuilder;
+import org.springframework.context.annotation.Import;
+import org.springframework.http.*;
+import org.springframework.http.client.ClientHttpRequestExecution;
+import org.springframework.http.client.ClientHttpRequestInterceptor;
+import org.springframework.http.client.ClientHttpResponse;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.TestPropertySource;
+import org.springframework.web.client.RestTemplate;
+
+import java.io.IOException;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@SpringBootTest(classes = DataVaultBrokerApp.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+@AddTestProperties
+@Slf4j
+@TestPropertySource(properties = {
+ "logging.level.org.springframework.security=TRACE",
+ "broker.security.enabled=true",
+ "broker.scheduled.enabled=false",
+ "broker.controllers.enabled=true",
+ "broker.services.enabled=true",
+ "broker.rabbit.enabled=false",
+ "broker.ldap.enabled=false",
+ "broker.initialise.enabled=false",
+ "broker.email.enabled=false",
+ "broker.database.enabled=true",
+ "logging.level.org.springframework.security=TRACE",
+ "logging.level.org.springframework.web.filter=DEBUG",
+ "logging.level.io.micrometer.tracing=DEBUG",
+ "management.tracing.sampling.probability=1.0",
+ "management.tracing.propagation.type=w3c"})
+@Import({MockServicesConfig.class, MockRabbitConfig.class}) //cos spring security requires some services so we have to mock them
+@AutoConfigureMockMvc
+@AutoConfigureObservability
+@ActiveProfiles("database")
+class TraceControllerMvcIT extends BaseDatabaseTest {
+
+ public static final String TRACE_ID = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
+ public static final String SPAN_ID = "bbbbbbbbbbbbbbbb";
+ public static final String TRACE_PARENT_VALUE = "00-" + TRACE_ID + "-" + SPAN_ID + "-01";
+
+ @Autowired
+ Tracer tracer;
+
+ @Autowired
+ Propagator propagator;
+
+ RestTemplate restTemplate;
+
+ @Autowired
+ AuthenticationManager authManager;
+
+ @LocalServerPort
+ int serverPort;
+
+ @BeforeEach
+ void setup() {
+ assertThat(tracer).isNotNull();
+ assertThat(propagator).isNotNull();
+ restTemplate = new RestTemplateBuilder()
+ .rootUri("http://localhost:" + serverPort)
+ .basicAuthentication("bactor", "bactorpass")
+ .build();
+ restTemplate.setInterceptors(List.of(new RequestLoggingInterceptor()));
+ }
+
+ @Test
+ void testHardcodedLogin() {
+ log.info("authManager class : {}", authManager.getClass().getName());
+ // 1. Create the "Unauthenticated" token
+ UsernamePasswordAuthenticationToken authRequest =
+ new UsernamePasswordAuthenticationToken("bactor", "bactorpass");
+
+ // 2. Pass it to the Manager
+ Authentication result = authManager.authenticate(authRequest);
+
+ // 3. Assert the result
+ assertThat(result).isNotNull();
+ assertThat(result.isAuthenticated()).isTrue();
+ assertThat(result.getAuthorities().stream().map(GrantedAuthority::getAuthority).toList()).containsExactlyInAnyOrder("ROLE_ACTUATOR");
+ }
+
+ private ResponseEntity getTraceInfo(boolean addTraceParentHeader) {
+ HttpHeaders headers = new HttpHeaders();
+ if (addTraceParentHeader) {
+ headers.add(TraceUtils.TRACE_PARENT, TRACE_PARENT_VALUE);
+ }
+ // Perform the GET request
+ ResponseEntity response = restTemplate.exchange(
+ "/trace/info",
+ HttpMethod.GET,
+ new HttpEntity<>(headers),
+ TraceInfo.class
+ );
+ return response;
+ }
+
+ @Test
+ void testTimeControllerNoTraceIdSupplied() {
+
+ ResponseEntity response = getTraceInfo(false);
+
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
+ assertThat(response.getBody()).isNotNull();
+
+ TraceInfo traceInfo = response.getBody();
+ assertThat(traceInfo.traceId()).isNotNull();
+ assertThat(traceInfo.traceId()).isNotEqualTo(TRACE_ID);
+ assertThat(TraceId.isValid(traceInfo.traceId())).isTrue();
+ }
+
+ @Test
+ void testTimeControllerWithTraceIdSupplied() {
+
+ ResponseEntity response = getTraceInfo(true);
+
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
+ assertThat(response.getBody()).isNotNull();
+
+ TraceInfo traceInfo = response.getBody();
+ assertThat(traceInfo.traceId()).isNotNull();
+ assertThat(traceInfo.traceId()).isEqualTo(TRACE_ID);
+ assertThat(TraceId.isValid(traceInfo.traceId())).isTrue();
+ }
+
+ public static class RequestLoggingInterceptor implements ClientHttpRequestInterceptor {
+
+ @Override
+ public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
+ log.info("=== Request Start ===");
+ log.info("URI : {}", request.getURI());
+ log.info("Method : {}", request.getMethod());
+ log.info("Headers: {}", request.getHeaders());
+ log.info("=== Request End ===");
+
+ return execution.execute(request, body);
+ }
+ }
+}
diff --git a/datavault-broker/src/test/java/org/datavaultplatform/broker/controllers/TraceControllerTest.java b/datavault-broker/src/test/java/org/datavaultplatform/broker/controllers/TraceControllerTest.java
new file mode 100644
index 000000000..164978cf2
--- /dev/null
+++ b/datavault-broker/src/test/java/org/datavaultplatform/broker/controllers/TraceControllerTest.java
@@ -0,0 +1,52 @@
+package org.datavaultplatform.broker.controllers;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.micrometer.tracing.Tracer;
+import lombok.extern.slf4j.Slf4j;
+import org.datavaultplatform.broker.queue.Sender;
+import org.datavaultplatform.common.util.TraceInfo;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.*;
+
+@Slf4j
+@ExtendWith(MockitoExtension.class)
+class TraceControllerTest {
+
+ TraceController controller;
+
+ @Mock
+ Sender mSender;
+
+ @Mock
+ Tracer mTracer;
+
+ final ObjectMapper mapper = new ObjectMapper();
+
+ @BeforeEach
+ void setup() {
+ controller = spy(new TraceController(mTracer, mSender, mapper));
+ }
+
+
+ @Captor
+ ArgumentCaptor argMessage;
+
+ @Test
+ void testBroker() {
+ TraceInfo traceInfo = new TraceInfo("1234");
+ doReturn(traceInfo).when(controller).getTraceInfo();
+ TraceInfo result = controller.sendTraceTaskToWorker();
+ verify(mSender).send(argMessage.capture(), eq(false));
+ assertThat(result).isEqualTo(traceInfo);
+
+ log.info(argMessage.getValue());
+ }
+}
\ No newline at end of file
diff --git a/datavault-broker/src/test/java/org/datavaultplatform/broker/queue/RabbitEventListenerIT.java b/datavault-broker/src/test/java/org/datavaultplatform/broker/queue/RabbitEventListenerIT.java
index 94daed5f4..a530fc31d 100644
--- a/datavault-broker/src/test/java/org/datavaultplatform/broker/queue/RabbitEventListenerIT.java
+++ b/datavault-broker/src/test/java/org/datavaultplatform/broker/queue/RabbitEventListenerIT.java
@@ -8,6 +8,8 @@
import java.time.Duration;
import java.util.UUID;
+import io.micrometer.tracing.Tracer;
+import io.micrometer.tracing.propagation.Propagator;
import org.datavaultplatform.common.util.TestUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -23,7 +25,7 @@
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.mock.mockito.SpyBean;
-public class RabbitEventListenerIT extends BaseRabbitTCIT {
+class RabbitEventListenerIT extends BaseRabbitTCIT {
@SpyBean
EventListener eventListener;
@@ -44,6 +46,12 @@ public class RabbitEventListenerIT extends BaseRabbitTCIT {
@Autowired
RabbitAdmin admin;
+ @Autowired
+ Tracer tracer;
+
+ @Autowired
+ Propagator propagator;
+
@BeforeEach
void checkRecvQueueIsEmptyBeforeTest() {
QueueInformation info = admin.getQueueInfo(expectedQueueName);
@@ -57,7 +65,7 @@ void testRecvFromWorker() {
String rand = UUID.randomUUID().toString();
//send message direct to 'events queue' and check that we can receive it via Listener
- RabbitUtils.sendDirectToQueue(template, eventQueue.getActualName(), rand);
+ RabbitUtils.sendDirectToQueue(template, eventQueue.getActualName(), rand, tracer, propagator);
TestUtils.waitUntil(
Duration.ofSeconds(10),
diff --git a/datavault-broker/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension b/datavault-broker/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension
new file mode 100644
index 000000000..16c8c8ce0
--- /dev/null
+++ b/datavault-broker/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension
@@ -0,0 +1 @@
+org.datavaultplatform.common.util.TempFileCleanerExtension
diff --git a/datavault-broker/src/test/resources/application-test.properties b/datavault-broker/src/test/resources/application-test.properties
index 486801f13..6e982af6c 100644
--- a/datavault-broker/src/test/resources/application-test.properties
+++ b/datavault-broker/src/test/resources/application-test.properties
@@ -39,3 +39,10 @@ keystore.path=test-keystore-path
rabbitmq.define.queue.worker=true
rabbitmq.define.queue.broker=true
rabbitmq.define.queue.restarts=true
+
+# see org.datavaultplatform.broker.queue.TaskSender
+workers.executor.proper.shutdown.enabled=true
+workers.executor.pre.shutdown.now.duration=5m
+workers.process.max.duration=1h
+workers.process.sigterm.timeout.duration=30s
+workers.process.post.sigkill.timeout.duration=5s
diff --git a/datavault-broker/src/test/resources/junit-platform.properties b/datavault-broker/src/test/resources/junit-platform.properties
new file mode 100644
index 000000000..3c550cd32
--- /dev/null
+++ b/datavault-broker/src/test/resources/junit-platform.properties
@@ -0,0 +1 @@
+junit.jupiter.extensions.autodBaseetection.enabled = true
\ No newline at end of file
diff --git a/datavault-common/pom.xml b/datavault-common/pom.xml
index 273192685..de2e2f4ca 100644
--- a/datavault-common/pom.xml
+++ b/datavault-common/pom.xml
@@ -12,7 +12,6 @@
datavault-common
datavault-common
- 0.0.1-SNAPSHOT
@@ -66,6 +65,11 @@
jackson-annotations
+
+ com.google.guava
+ guava
+
+
org.jsondoc
jsondoc-core
@@ -167,8 +171,38 @@
net.i2p.crypto
eddsa
-->
+
+ io.micrometer
+ micrometer-tracing-bridge-otel
+
+
+ io.micrometer
+ micrometer-tracing-test
+ test
+
+
+ org.springframework.boot
+ spring-boot-actuator
+
+
+ org.springframework.security
+ spring-security-core
+
+
+ org.springframework.security
+ spring-security-test
+ test
+
+
+ jakarta.servlet
+ jakarta.servlet-api
+
+
+ org.springframework
+ spring-webmvc
+ provided
+
-
@@ -192,6 +226,10 @@
**/EventTest*
**/TestUtils*
**/UsesTestContainers*
+ **/TempFileCleaner*
+ **/TempFileCleanerExtension*
+ **/TraceIdWrapper*
+ **/WithMockActuatorUser*
@@ -200,6 +238,7 @@
org.jacoco
jacoco-maven-plugin
+ ${jacoco.plugin.version}
prepare-agent-integration
diff --git a/datavault-common/src/main/java/org/datavaultplatform/common/actuator/ActuatorHealthSecurityAdvice.java b/datavault-common/src/main/java/org/datavaultplatform/common/actuator/ActuatorHealthSecurityAdvice.java
new file mode 100644
index 000000000..95e0c86e0
--- /dev/null
+++ b/datavault-common/src/main/java/org/datavaultplatform/common/actuator/ActuatorHealthSecurityAdvice.java
@@ -0,0 +1,34 @@
+package org.datavaultplatform.common.actuator;
+
+import jakarta.servlet.http.HttpServletRequest;
+import org.springframework.boot.actuate.health.SystemHealth;
+import org.springframework.web.bind.annotation.ControllerAdvice;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Predicate;
+
+@ControllerAdvice
+public class ActuatorHealthSecurityAdvice extends BaseActuatorSecurityAdvice {
+
+ @Override
+ public Predicate getAcuatorUrlMatcher() {
+ return req -> req.getRequestURI().startsWith("/actuator/health");
+ }
+
+ @Override
+ public List getKeysToKeep(){
+ return List.of("status");
+ }
+
+ @Override
+ public Object filter(Object fullInfo) {
+ if( !(fullInfo instanceof SystemHealth systemHealth)){
+ return fullInfo;
+ }
+ Map result = new HashMap<>();
+ result.put("status", systemHealth.getStatus().getCode());
+ return result;
+ }
+}
\ No newline at end of file
diff --git a/datavault-common/src/main/java/org/datavaultplatform/common/actuator/ActuatorInfoSecurityAdvice.java b/datavault-common/src/main/java/org/datavaultplatform/common/actuator/ActuatorInfoSecurityAdvice.java
new file mode 100644
index 000000000..1ebbd7c76
--- /dev/null
+++ b/datavault-common/src/main/java/org/datavaultplatform/common/actuator/ActuatorInfoSecurityAdvice.java
@@ -0,0 +1,36 @@
+package org.datavaultplatform.common.actuator;
+
+import jakarta.servlet.http.HttpServletRequest;
+import org.springframework.web.bind.annotation.ControllerAdvice;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Predicate;
+
+@ControllerAdvice
+public class ActuatorInfoSecurityAdvice extends BaseActuatorSecurityAdvice {
+
+ @Override
+ public Predicate getAcuatorUrlMatcher() {
+ return req -> req.getRequestURI().startsWith("/actuator/info");
+ }
+
+ @Override
+ public List getKeysToKeep(){
+ return List.of("app");
+ }
+
+ @Override
+ public Object filter(Object fullInfo) {
+ if (!(fullInfo instanceof Map, ?> fullInfoAsMap)) {
+ return fullInfo;
+ }
+ // Create a "Safe" copy with only the bare minimum
+ Map filteredInfo = new HashMap<>();
+ for (String key : getKeysToKeep()) {
+ filteredInfo.put(key, fullInfoAsMap.get(key));
+ }
+ return filteredInfo;
+ }
+}
\ No newline at end of file
diff --git a/datavault-common/src/main/java/org/datavaultplatform/common/actuator/ActuatorSecurityAdvice.java b/datavault-common/src/main/java/org/datavaultplatform/common/actuator/ActuatorSecurityAdvice.java
new file mode 100644
index 000000000..b1aed5615
--- /dev/null
+++ b/datavault-common/src/main/java/org/datavaultplatform/common/actuator/ActuatorSecurityAdvice.java
@@ -0,0 +1,52 @@
+package org.datavaultplatform.common.actuator;
+
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.actuate.endpoint.web.Link;
+import org.springframework.web.bind.annotation.ControllerAdvice;
+
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Predicate;
+
+@Slf4j
+@ControllerAdvice
+public class ActuatorSecurityAdvice extends BaseActuatorSecurityAdvice {
+
+ public static final List ALLOWED = List.of("/actuator", "/actuator/");
+ public static final String LINKS = "_links";
+
+ @Override
+ public Predicate getAcuatorUrlMatcher() {
+ return request -> {
+ boolean allowed = ALLOWED.contains(request.getRequestURI());
+ return allowed;
+ };
+ }
+
+ @Override
+ public List getKeysToKeep(){
+ return List.of("self", "heath", "info");
+ }
+
+ @Override
+ public Object filter(Object fullInfo) {
+ if (!(fullInfo instanceof Map, ?> fullInfoAsMap)) {
+ return fullInfo;
+ }
+ Map links = (Map) fullInfoAsMap.get(LINKS);
+
+ LinkedHashMap filteredLinks = new LinkedHashMap<>();
+ for (String key : getKeysToKeep()) {
+ Link link = links.get(key);
+ if (link != null) {
+ filteredLinks.put(key, link);
+ }
+ }
+ Map filteredInfo = new HashMap<>();
+ filteredInfo.put(LINKS, filteredLinks);
+ return filteredInfo;
+ }
+}
\ No newline at end of file
diff --git a/datavault-common/src/main/java/org/datavaultplatform/common/actuator/BaseActuatorSecurityAdvice.java b/datavault-common/src/main/java/org/datavaultplatform/common/actuator/BaseActuatorSecurityAdvice.java
new file mode 100644
index 000000000..6afdb2f28
--- /dev/null
+++ b/datavault-common/src/main/java/org/datavaultplatform/common/actuator/BaseActuatorSecurityAdvice.java
@@ -0,0 +1,55 @@
+package org.datavaultplatform.common.actuator;
+
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.core.MethodParameter;
+import org.springframework.http.MediaType;
+import org.springframework.http.server.ServerHttpRequest;
+import org.springframework.http.server.ServerHttpResponse;
+import org.springframework.http.server.ServletServerHttpRequest;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.web.bind.annotation.ControllerAdvice;
+import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
+
+import java.util.List;
+import java.util.function.Predicate;
+
+@ControllerAdvice
+@Slf4j
+public abstract class BaseActuatorSecurityAdvice implements ResponseBodyAdvice