Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions Middleware/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,26 @@
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>5.2.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> 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<Map<String, String>> getPublicDataset() {
return fetchDataset(DatasetType.PUBLIC);
}

@GetMapping("/protected")
public ResponseEntity<Map<String, String>> getProtectedDataset() {
return fetchDataset(DatasetType.PROTECTED);
}

@GetMapping("/private")
public ResponseEntity<Map<String, String>> getPrivateDataset() {
return fetchDataset(DatasetType.PRIVATE);
}

@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<Map<String, String>> handleAccessDenied(AccessDeniedException ex) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(Map.of("error", ex.getMessage()));
}

@ExceptionHandler(MetadataException.class)
public ResponseEntity<Map<String, String>> handleMetadataError(MetadataException ex) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(Map.of("error", ex.getMessage()));
}

private ResponseEntity<Map<String, String>> fetchDataset(DatasetType type) {
String content = datasetAccessService.getDatasetContent(type);
return ResponseEntity.ok(Map.of("content", content));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package ca.concordia.encs.citydata.core.exceptions;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

/**
* Thrown when an authenticated user attempts to access a dataset they are
* not listed in the corresponding metadata file.
*
* @author Sikandar Ejaz
* @since 2026-06-01
*/

@ResponseStatus(HttpStatus.FORBIDDEN)
public class AccessDeniedException extends RuntimeException {

public AccessDeniedException(String username, String datasetType) {
super(String.format("User '%s' is not authorised to access the %s dataset.", username, datasetType));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package ca.concordia.encs.citydata.core.exceptions;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

/**
* Thrown when a metadata file cannot be found on the classpath, is empty,
* or has a first line that does not match the expected dataset type label.
*
* @author Sikandar Ejaz
* @since 2026-06-01
*/

@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public class MetadataException extends RuntimeException {

public MetadataException(String message) {
super(message);
}

public MetadataException(String message, Throwable cause) {
super(message, cause);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package ca.concordia.encs.citydata.core.model;

/**
* Represents the three tiers of dataset access control.
* Each type maps to a corresponding metadata file and dataset CSV.
*
* @author Sikandar Ejaz
* @since 2026-06-01
*/

public enum DatasetType {

PUBLIC("Public", "public_metadata.txt", "public_dataset.csv"),
PROTECTED("Protected", "protected_metadata.txt", "protected_dataset.csv"),
PRIVATE("Private", "private_metadata.txt", "private_dataset.csv");

/** The exact label expected on the first line of the metadata file. */
private final String metadataLabel;

/** Classpath-relative path to the metadata file (under src/test/resources). */
private final String metadataPath;

/** Classpath-relative path to the dataset CSV (under src/test/resources). */
private final String datasetPath;

DatasetType(String metadataLabel, String metadataPath, String datasetPath) {
this.metadataLabel = metadataLabel;
this.metadataPath = metadataPath;
this.datasetPath = datasetPath;
}

public String getMetadataLabel() {
return metadataLabel;
}

public String getMetadataPath() {
return metadataPath;
}

public String getDatasetPath() {
return datasetPath;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;

import org.springframework.security.core.context.SecurityContextHolder;

import ca.concordia.encs.citydata.core.exceptions.AccessDeniedException;
import ca.concordia.encs.citydata.core.exceptions.MiddlewareException;
import ca.concordia.encs.citydata.core.implementations.CSVProducer;
import ca.concordia.encs.citydata.core.utils.RequestOptions;

import ca.concordia.encs.citydata.services.DatasetAccessService;

/**
* This producer reads Flow data from a CSV source, processes it line by line, and produces a result set for the data
Expand All @@ -18,24 +21,33 @@
* @author: Minette Z. Fixed the producer by adding the required constructor from CSVProducer to properly initialize the inherited base producer (CSVProducer)
* @date: 2026-05-29
*/
public class BTUProducer extends CSVProducer{

public class BTUProducer extends CSVProducer {

private String metadataPath;

public BTUProducer(String filePath) {
super(filePath);
}

public BTUProducer(final String filePath, final RequestOptions fileOptions) {
super(filePath, fileOptions);
}


public void setMetadataPath(String metadataPath) {
this.metadataPath = metadataPath;
}

@Override
public void fetch() {
try {
if (metadataPath != null) {
String username = SecurityContextHolder.getContext().getAuthentication().getName();
new DatasetAccessService().checkAuthorisationForPath(username, metadataPath);
}

ByteArrayOutputStream outputStream = (ByteArrayOutputStream) this.fetchFromPath();

String csvString = outputStream.toString(StandardCharsets.UTF_8);

String[] lines = csvString.split("\\R");

ArrayList<String> csvLines = new ArrayList<>();
Expand All @@ -49,10 +61,10 @@ public void fetch() {
this.setResult(csvLines);
this.applyOperation();


} catch (AccessDeniedException e) {
throw e;
} catch (Exception e) {
throw new MiddlewareException.DatasetNotFound("Error processing BTU CSV data: " + e.getMessage());
}
}

}
}
Loading
Loading