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
283 changes: 204 additions & 79 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,92 +1,187 @@
# The ALFS Whistleblower Ticket System
# ALFS Whistleblower Ticket System

ALFS is a secure case management system for handling whistleblower reports, built with Spring Boot.

## Features
- **Anonymous & Authenticated Reporting**: Submit reports without an account using secure tokens or as a registered user.
- **Ticket Lifecycle Management**: Full workflow support including assignment, status transitions, and public/internal comments.
- **Audit Logging**: Comprehensive trail of all security-sensitive actions and ticket modifications.
- **Server-Side Rendering**: Fast, secure UI built with JTE templates.
- **Secure Storage**: File attachments managed via MinIO/S3-compatible storage.
- **Secure Access Model**: RBAC, ownership validation, and token-based access control.

## Security
- **RBAC**: Multi-layered permissions for Reporters, Investigators, and Admins.
- **Owner-Based Access**: Strict isolation of ticket data based on reporter and assigned investigator.
- **Secure Token Access**: Unique cryptographic tokens for anonymous ticket follow-ups.
- **Data Protection**: Secure password hashing with BCrypt.
- **XSS Prevention**: Automatic HTML escaping via server-side JTE rendering.
- **JWT Authentication**: Supports both browser cookies and API headers with database-backed role verification.

## Feature Details
### Anonymous Ticket Submission
Anonymous (unauthenticated) users can submit tickets without creating an account. A unique secure token is generated on submission — anyone with the token can view and comment on that specific ticket without logging in.

### Ticket Lifecycle Management
Tickets follow a structured lifecycle to ensure proper handling from submission to resolution.

#### Statuses
- **OPEN**: Default state for new submissions.
- **IN_PROGRESS**: The ticket is currently being handled by an investigator.
- **RESOLVED**: The investigation is complete, and a resolution has been reached.
- **CLOSED**: The case is finalized and no further actions are expected.

#### Assignment Flow
1. **Submission**: A ticket is created in the **OPEN** state.
2. **Triage**: An **ADMIN** reviews the ticket and assigns it to an **INVESTIGATOR**.
3. **Investigation**: The assigned investigator updates the status to **IN_PROGRESS** and interacts with the reporter.
4. **Resolution**: Once finished, the investigator or admin moves the ticket to **RESOLVED** or **CLOSED**.

#### Communication
The system supports two types of comments:
- **Public Comments**: Visible to everyone with access to the ticket (Reporters, Investigators, Admins). Used for follow-up questions and providing updates.
- **Internal Notes**: Visible only to **INVESTIGATORS** and **ADMINS**. These are kept separate from public comments to ensure internal coordination stays private, making it easier to discuss case handling and assignments without exposing sensitive details.

### Audit Logging
The application maintains a comprehensive audit trail of security-sensitive actions and ticket modifications.

**What is logged:**
- Ticket creation and status changes.
- Investigator assignments and reassignments.
- New public comments and internal notes.
- File attachment uploads.

Each log entry records the action type, affected field, previous and new values, the acting user, and a precise timestamp. This provides accountability and helps meet compliance requirements for whistleblower systems.

## Tech Stack
- Java 25
- Spring Boot
- Spring MVC
- Spring Data JPA
- Spring Security
- Jakarta EE APIs
- Lombok
- JTE templates
- H2 for local development
- MinIO for file storage
- JUnit 5, Mockito, and Spring test support

## Quick start
1. Make sure you have Java 25 and Maven installed.
2. Enable demo data by adding the following to your local `src/main/resources/application.properties`:

## A secure case management system built with Spring Boot for handling whistleblower reports.
The system allows anonymous reporting, secure file uploads, role-based access control, and full audit logging.

### CI/CD
```text
This project uses GitHub Actions for CI/CD.
CI runs tests and validates code on push and pull requests.
CD builds and uploads a JAR artifact when code is merged to main.
```properties
spring.profiles.active=demo
```

*Alternatively, run with the profile directly via command line (see step 3).*

> **Note:** Read more about [demo data](#demo-data) below.

3. Start the application:
```bash
# Using the property file
./mvnw spring-boot:run

# OR: directly enabling the demo profile via command line
./mvnw spring-boot:run -Dspring-boot.run.profiles=demo
```
4. Open the app in your browser at:
```text
http://localhost:8080
```
*The landing page provides quick links to submit a report or log in.*

5. Log in at `http://localhost:8080/login` (or via the UI) using the default admin credentials:
```text
username: admin
password: admin
```

> **Note:** To test file uploads and downloads, you'll need a local MinIO instance running. See the [Local MinIO setup](#local-minio-setup) section below.

## Typical User Flow
1. **As a Whistleblower (Anonymous)**: Visit `/tickets/create` to submit a report. Save the provided token to follow up later.
2. **As an Authenticated Reporter**: Login at `/login`, then go to `/tickets/my` to see and track your submitted reports.
3. **As an Admin**: Login at `/login` (user: `admin`), then go to `/admin/tickets` to see all reports and assign them to investigators.
4. **As an Investigator**: Login at `/login`, then check `/tickets/assigned` for cases assigned to you.

## Design Decisions

- Server-side rendering (JTE) was chosen over an SPA approach to reduce complexity
and minimize client-side security concerns (e.g., XSS, token handling)
- Token-based access allows anonymous reporting without account creation
- MinIO enables S3-compatible storage without requiring a cloud provider

## Project Structure
- `controllers` — web endpoints (UI and API)
- `services` — business logic
- `repositories` — database access
- `entities` — JPA models
- `dto` and `mapper` — request/response mapping
- `security` — Spring Security configuration, authentication, and access control

## Architecture

The application follows a layered architecture with server-side rendering:

### Logs should look like this:
```text
action = HANDLER_ASSIGNED
fieldName = assignedHandler
oldValue = null
newValue = userId:5
createdAt = 2026-03-27
Browser (JTE) → Controller → Service → Repository → Database
```
This layered architecture separates concerns:
- **Controllers**: Handle HTTP requests, input validation, and view redirects.
- **Services**: Orchestrate business logic, enforce security rules, and trigger audit logging.
- **Repositories**: Abstract database persistence using Spring Data JPA.
- **Storage Service**: Encapsulates interaction with MinIO/S3 for attachment handling.
- **Audit Service**: Records a detailed trail of modifications for compliance and tracking.

Security is enforced at both controller (URL-based) and service levels (method-based) to ensure defense-in-depth.

## Local MinIO setup (dev) and testing file upload/download
## CI/CD
The project uses GitHub Actions for:
- CI on push and pull requests
- CD builds that publish a JAR artifact on `main`

1) Start MinIO locally (Docker)
- Windows PowerShell example:
- Create a data folder (optional): `mkdir C:\minio\data`
- Run MinIO:
```powershell
docker run --name minio -p 9000:9000 -p 9001:9001 `
-e MINIO_ROOT_USER=minioadmin -e MINIO_ROOT_PASSWORD=minioadmin `
-v C:\minio\data:/data `
-d quay.io/minio/minio server /data --console-address ":9001"
```
## Role-Based Access Control (RBAC)

2) Create the bucket
- Open MinIO Console: http://localhost:9001 (user: `minioadmin`, pass: `minioadmin`).
- Go to Buckets → Create bucket → name it: `alfs-attachments`.
The system implements a multi-layered security model to protect whistleblower reports and ensure that only authorized personnel can access sensitive information.

3) Verify application configuration (already set for local dev)
- See `src/main/resources/application.properties`:
- `storage.s3.endpoint=http://localhost:9000`
- `storage.s3.accessKey=minioadmin`
- `storage.s3.secretKey=minioadmin`
- `storage.s3.bucket=alfs-attachments`
- `spring.servlet.multipart.max-file-size=50MB`
### Roles and Permissions

4) Test file upload (POST)
- Start the Spring Boot app (default port 8080).
- Use Postman or curl:
```bash
curl -X POST "http://localhost:8080/api/files/upload" \
-F ticketId=1 \
-F file=@"C:/path/to/your/test.pdf"
```
- Expected JSON response example:
```json
{
"id": 42,
"ticketId": 1,
"fileName": "test.pdf",
"s3Key": "<generated-uuid>/test.pdf",
"uploadedAt": "2026-04-02T12:34:56"
}
```
- Check MinIO Console → your bucket → object is present.
| Role | Purpose | Permissions |
| --- | --- | --- |
| **REPORTER** | Standard whistleblower user. | Create tickets, view their own tickets, and add comments. |
| **INVESTIGATOR**| Internal staff handling cases. | View assigned tickets, update ticket status, and add comments (public & internal). |
| **ADMIN** | System administrator. | Full access to all tickets, assign investigators to tickets, and system management. |

5) Test file download (GET)
- Take the `id` from the upload response above and request:
```bash
curl -v -o downloaded.pdf "http://localhost:8080/api/files/42/download"
```
- The file should be downloaded as `downloaded.pdf`.
### Access Control Mechanisms

1. **Endpoint Protection**: Configured in `SecurityConfig.java`, defining which URL patterns are public (e.g., login, signup, anonymous ticket creation) and which require authentication.
2. **Method Security**: Using `@PreAuthorize` annotations on controller methods to enforce role requirements (e.g., restricting status updates to Admins and Investigators).
3. **Owner-Based Access**: Tickets created by a `REPORTER` are only accessible to that specific user, the assigned `INVESTIGATOR`, and all `ADMIN` users.
4. **Token-Based Access**: For anonymous reports, a unique secure token is generated. Anyone with the token can view and comment on that specific ticket without needing an account.

Notes
- No authentication is enforced on these endpoints yet (Week 1 scope).
- Ensure a Ticket with the provided `ticketId` exists in the database before uploading.
## JWT Authentication

The application uses JSON Web Tokens (JWT) for stateless authentication, supporting both browser and API-based interactions.
Even though the UI is server-rendered, the system remains stateless by storing the JWT in a cookie.

# 🚀 API Documentation & Testing
## This project uses Swagger UI to provide a visual interface for exploring and testing the API endpoints.
### Key Features
- **Dual-Source Lookup**: The system identifies the user via the `JWT` cookie (for browser/UI) or the `Authorization: Bearer <token>` header (for API clients).
- **Database-Backed Roles**: Roles are reloaded from the database on every request. This ensures that permission changes (e.g., revoking admin rights) take effect immediately, even if the user has a long-lived token.
- **Security Secret**: Local development uses a secret defined in `application.properties`. In production, this must be provided via the `JWT_SECRET` environment variable.
- **API Endpoints**: RESTful authentication is available at `/auth/login` and `/auth/signup` for JSON-based programmatic access.

## API Documentation & Testing

This project uses Swagger UI to provide a visual interface for exploring and testing the API endpoints.

### Accessing Swagger
Once the application is running, you can access the interactive documentation at:

Swagger UI: http://localhost:8080/swagger-ui/index.html
- Swagger UI: http://localhost:8080/swagger-ui/index.html
- OpenAPI Spec (JSON): http://localhost:8080/v3/api-docs

OpenAPI Spec (JSON): http://localhost:8080/v3/api-docs
## Demo Data

The application includes a demo data seeder that populates the database with realistic test data on startup.
Expand Down Expand Up @@ -117,20 +212,50 @@ The seeder will only run if the admin user does not exist, so it is safe to leav
| reporter1 | reporter1 | REPORTER |
| reporter2 | reporter2 | REPORTER |

Once running, the H2 database console is available at `/h2-console` using the credentials in `application.properties`.
Once running, the H2 database console is available at `/h2-console` using the credentials in `application.properties` (JDBC URL: `jdbc:h2:mem:testdb`).

-------
## Local MinIO setup

## 🏗️ Architecture
1) Start MinIO locally (Docker)
- Windows PowerShell example:
- Create a data folder (optional): `mkdir C:\minio\data`
- Run MinIO:
```powershell
docker run --name minio -p 9000:9000 -p 9001:9001 `
-e MINIO_ROOT_USER=minioadmin -e MINIO_ROOT_PASSWORD=minioadmin `
-v C:\minio\data:/data `
-d quay.io/minio/minio server /data --console-address ":9001"
```

The application follows a layered architecture:
2) Create the bucket
- Open MinIO Console: http://localhost:9001 (user: `minioadmin`, pass: `minioadmin`).
- Go to Buckets → Create bucket → name it: `alfs-attachments`.

```text
Controller → Service → Repository → Database
```
3) Verify application configuration (already set for local dev)
- See `src/main/resources/application.properties`:
- `storage.s3.endpoint=http://localhost:9000`
- `storage.s3.accessKey=minioadmin`
- `storage.s3.secretKey=minioadmin`
- `storage.s3.bucket=alfs-attachments`
- `spring.servlet.multipart.max-file-size=50MB`

- Controllers handle HTTP requests and responses
- Services contain business logic
- Repositories handle data access
4) Test file upload (POST)
- Start the Spring Boot app (default port 8080).
- Use Postman or curl:
```bash
curl -X POST "http://localhost:8080/api/files/upload" \
-F ticketId=1 \
-F file=@"/path/to/your/test.pdf"
```
- Since `AttachmentController` is a `@Controller`, it will return a redirect (302) to the ticket view page.
- Check MinIO Console → your bucket → object is present.

5) Test file download (GET)
- Use the attachment ID (e.g., 1) to download the file:
```bash
curl -v -o downloaded.pdf "http://localhost:8080/api/files/1/download"
```
- The file should be downloaded as `downloaded.pdf`.

-------
### Notes
- A ticket with the provided ticketId must exist in the database before uploading.
4 changes: 2 additions & 2 deletions src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ spring.web.error.whitelabel.enabled=false

# Storage (MinIO/S3) ? default local dev values; override via env in andra milj�er
storage.s3.endpoint=http://localhost:9000
storage.s3.accessKey=minio
storage.s3.secretKey=minio123
storage.s3.accessKey=minioadmin
storage.s3.secretKey=minioadmin
storage.s3.bucket=alfs-attachments
storage.s3.region=us-east-1
storage.s3.secure=false
Expand Down
16 changes: 5 additions & 11 deletions src/test/java/org/example/alfs/integration/TicketControllerIT.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
import org.springframework.transaction.annotation.Transactional;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
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.MockMvcResultMatchers.*;
Expand Down Expand Up @@ -112,8 +111,7 @@ void anonymousReporter_invalidToken_redirectsToLogin() throws Exception {
void anonymousReporter_validPost_redirectsToTicketCreated() throws Exception {
mockMvc.perform(post("/tickets/create")
.param("title", "Test title")
.param("description", "Test description")
.with(csrf()))
.param("description", "Test description"))
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrlPattern("/tickets/ticket-created?token=*"));
}
Expand All @@ -123,8 +121,7 @@ void anonymousReporter_validPost_redirectsToTicketCreated() throws Exception {
void anonymousReporter_blankPost_returnsCreateForm() throws Exception {
mockMvc.perform(post("/tickets/create")
.param("title", "")
.param("description", "")
.with(csrf()))
.param("description", ""))
.andExpect(status().isOk())
.andExpect(view().name("create"))
.andExpect(model().attributeHasFieldErrors("ticket", "title", "description"));
Expand Down Expand Up @@ -184,8 +181,7 @@ void admin_canViewTicketById() throws Exception {
@DisplayName("Admin can assign an investigator to a ticket")
void admin_canAssignInvestigator() throws Exception {
mockMvc.perform(post("/tickets/{id}/assign", ticketId)
.param("investigatorId", investigator.getId().toString())
.with(csrf()))
.param("investigatorId", investigator.getId().toString()))
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("/tickets/" + ticketId));
}
Expand All @@ -197,8 +193,7 @@ void admin_canUpdateTicketStatus() throws Exception {
ticketService.assignInvestigator(ticketId, investigator.getId());

mockMvc.perform(post("/tickets/{id}/status", ticketId)
.param("status", "RESOLVED")
.with(csrf()))
.param("status", "RESOLVED"))
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("/tickets/" + ticketId));
}
Expand All @@ -211,8 +206,7 @@ void admin_cannotPerformInvalidStatusTransition() throws Exception {
assertEquals(org.example.alfs.enums.TicketStatus.OPEN, ticket.getStatus());

mockMvc.perform(post("/tickets/{id}/status", ticketId)
.param("status", "RESOLVED") // Transition OPEN -> RESOLVED is invalid
.with(csrf()))
.param("status", "RESOLVED")) // Transition OPEN -> RESOLVED is invalid
.andExpect(status().isBadRequest());
}
}
Expand Down
Loading