diff --git a/Middleware/pom.xml b/Middleware/pom.xml
index 94e3ca96..22a90b24 100644
--- a/Middleware/pom.xml
+++ b/Middleware/pom.xml
@@ -99,6 +99,26 @@
org.springframework.security
spring-security-test
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+ org.mockito
+ mockito-inline
+ 5.2.0
+ test
+
+
+ org.assertj
+ assertj-core
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ test
+
diff --git a/Middleware/src/main/java/ca/concordia/encs/citydata/core/Application.java b/Middleware/src/main/java/ca/concordia/encs/citydata/core/Application.java
index f2e1c503..b1e18e7d 100644
--- a/Middleware/src/main/java/ca/concordia/encs/citydata/core/Application.java
+++ b/Middleware/src/main/java/ca/concordia/encs/citydata/core/Application.java
@@ -5,11 +5,13 @@
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.ComponentScan;
+import org.springframework.security.core.context.SecurityContextHolder;
import ca.concordia.encs.citydata.core.configs.RsaKeyProperties;
import ca.concordia.encs.citydata.datastores.DiskDatastore;
import ca.concordia.encs.citydata.datastores.InMemoryDataStore;
import ca.concordia.encs.citydata.datastores.MongoDataStore;
+import jakarta.annotation.PostConstruct;
/**
* This is the Spring Boot application entry point.
@@ -31,6 +33,11 @@ public class Application {
final DiskDatastore diskStore = DiskDatastore.getInstance();
final MongoDataStore mongoDataStore = MongoDataStore.getInstance();
+ @PostConstruct
+ public void init() {
+ SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
+ }
+
public static void main(String[] args) {
final ApplicationContext context = SpringApplication.run(Application.class, args);
final MongoDataStore mongoDataStore = context.getBean(MongoDataStore.class);
diff --git a/Middleware/src/main/java/ca/concordia/encs/citydata/core/configs/SecurityConfig.java b/Middleware/src/main/java/ca/concordia/encs/citydata/core/configs/SecurityConfig.java
index 411e5786..9d2e13a0 100644
--- a/Middleware/src/main/java/ca/concordia/encs/citydata/core/configs/SecurityConfig.java
+++ b/Middleware/src/main/java/ca/concordia/encs/citydata/core/configs/SecurityConfig.java
@@ -206,10 +206,9 @@ public AuthenticationManager authManager(UserDetailsService userDetailsService,
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http.cors(Customizer.withDefaults()).csrf(AbstractHttpConfigurer::disable)
- .authorizeHttpRequests(auth -> auth
- .requestMatchers("/authenticate", "/home", "/health/ping", "/producers/list",
- "/operations/list", "/routes/list", "/error", "/apply/sync", "/apply/async")
- .permitAll().anyRequest().authenticated())
+ .authorizeHttpRequests(auth -> auth.requestMatchers("/authenticate", "/home", "/health/ping",
+ "/producers/list", "/operations/list", "/routes/list", "/error", "/apply/sync", "/apply/async",
+ "/api/datasets/list").permitAll().anyRequest().authenticated())
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)).build();
}
diff --git a/Middleware/src/main/java/ca/concordia/encs/citydata/core/controllers/DatasetController.java b/Middleware/src/main/java/ca/concordia/encs/citydata/core/controllers/DatasetController.java
new file mode 100644
index 00000000..c32e73bc
--- /dev/null
+++ b/Middleware/src/main/java/ca/concordia/encs/citydata/core/controllers/DatasetController.java
@@ -0,0 +1,89 @@
+package ca.concordia.encs.citydata.core.controllers;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Map;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import ca.concordia.encs.citydata.core.exceptions.AccessDeniedException;
+import ca.concordia.encs.citydata.core.exceptions.MetadataException;
+import ca.concordia.encs.citydata.core.model.DatasetType;
+import ca.concordia.encs.citydata.services.DatasetAccessService;
+
+/**
+ * REST controller providing access to the three dataset tiers.
+ *
+ * @author Sikandar Ejaz
+ * @since 2026-06-01
+ */
+
+@RestController
+@RequestMapping("/api/datasets")
+public class DatasetController {
+
+ private final DatasetAccessService datasetAccessService;
+
+ public DatasetController(DatasetAccessService datasetAccessService) {
+ this.datasetAccessService = datasetAccessService;
+ }
+
+ @GetMapping("/list")
+ public ResponseEntity listDatasets() {
+ try {
+ Path filePath = Paths.get("DATA_SOURCES.md");
+ String content = Files.readString(filePath, StandardCharsets.UTF_8);
+ return ResponseEntity.ok().contentType(MediaType.TEXT_PLAIN).body(content);
+ } catch (IOException e) {
+ // Fallback: try relative to project root
+ try {
+ Path filePath = Paths.get("Middleware/DATA_SOURCES.md");
+ String content = Files.readString(filePath, StandardCharsets.UTF_8);
+ return ResponseEntity.ok().contentType(MediaType.TEXT_PLAIN).body(content);
+ } catch (IOException e2) {
+ return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
+ .body("Could not load dataset list. Tried ./DATA_SOURCES.md and Middleware/DATA_SOURCES.md. "
+ + "Working directory: " + Paths.get("").toAbsolutePath());
+ }
+ }
+ }
+
+ @GetMapping("/public")
+ public ResponseEntity