Skip to content

Commit c532550

Browse files
authored
feat(gdpr): add GDPR compliance features (#252)
* feat(audit): add audit log query infrastructure for GDPR export Add the ability to query audit logs, which is essential for GDPR data export functionality. The implementation uses an interface-based design allowing different backends (file, database, Elasticsearch). - AuditEventDTO: DTO for returning audit event data - AuditLogQueryService: Interface for querying audit events by user - FileAuditLogQueryService: Default implementation parsing pipe-delimited logs The file-based implementation parses the existing audit log format and supports filtering by user, timestamp, and action type. Closes #250 * feat(event): add GDPR-related application events Add events for GDPR lifecycle operations following the existing event pattern used by UserPreDeleteEvent and OnRegistrationCompleteEvent. - UserDataExportedEvent: Published after successful data export - UserDeletedEvent: Published after user deletion (post-transaction) - ConsentChangedEvent: Published when consent is granted or withdrawn These events enable consuming applications to react to GDPR operations, such as updating external systems or triggering notifications. * feat(gdpr): add GDPR compliance services and types Implement core GDPR functionality including data export, deletion orchestration, and consent tracking services. Core Types: - ConsentType: Enum of standard consent types (PRIVACY_POLICY, etc.) - ConsentRecord: DTO for consent grant/withdrawal data - GdprDataContributor: Interface for apps to contribute export data - GdprConfig: Configuration properties with user.gdpr.* prefix - GdprExportDTO: Complete data export response structure Services: - GdprExportService: Aggregates user data for GDPR Article 15 export - GdprDeletionService: Orchestrates GDPR Article 17 deletion with hooks - ConsentAuditService: Tracks consent changes via audit system The GdprDataContributor interface allows consuming applications to include their domain-specific data in exports and clean up during deletion, making the framework extensible while handling core data. * feat(api): add GDPR REST API endpoints Add REST controller with endpoints for GDPR operations at /user/gdpr/* following the existing UserAPI pattern. Endpoints: - GET /user/gdpr/export - Export current user's data as JSON - POST /user/gdpr/delete - Request account deletion - POST /user/gdpr/consent - Record consent grant/withdrawal - GET /user/gdpr/consent - Get current consent status All endpoints require authentication and return standard JSONResponse format. When GDPR features are disabled via configuration, endpoints return 404 Not Found. * chore: add GDPR configuration and UserDeletedEvent publication Update existing files to support GDPR functionality: Configuration (dsspringuserconfig.properties): - user.gdpr.enabled: Master toggle for GDPR features - user.gdpr.exportBeforeDeletion: Auto-export before hard delete - user.gdpr.consentTracking: Enable consent audit events UserService: - Publish UserDeletedEvent after successful user deletion - Captures userId and email before deletion for the event * fix(security): replace manual JSON parsing with Jackson serialization Address critical code review finding: fragile string parsing had injection risk. Now using Jackson ObjectMapper for safe JSON handling. - Add ConsentExtraData DTO for consent extra data serialization - Replace buildExtraData() with Jackson writeValueAsString() - Replace extractConsentType()/extractValue() with Jackson readValue() - Remove vulnerable escapeJson() helper method - Optimize consent status/export to process events in single pass * fix(security): remove PII from log statements Address critical code review finding: user emails were being logged, exposing PII. Now using user IDs instead. - Replace user.getEmail() with user.getId() in all GDPR log statements - Affected files: GdprAPI, GdprDeletionService - ConsentAuditService/GdprExportService already updated in prior commit * fix(security): add input validation to consent customType field Address critical code review finding: missing validation on customType could allow injection attacks. - Add @SiZe(max = 100) to limit custom type length - Add @pattern for alphanumeric, underscore, hyphen only - Prevents injection of special characters in consent type names * perf(audit): use streaming for audit log file queries Address high-severity code review finding: unbounded memory usage when reading large audit log files. - Replace BufferedReader iteration with Files.lines() stream - Process lines lazily to reduce memory footprint - Maintain same filtering and sorting behavior * test(api): add GdprAPI unit tests Add comprehensive unit tests for GDPR REST API endpoints using standalone MockMvc with mocked services. Test cases cover: - Data export: authenticated, unauthenticated, GDPR disabled - Account deletion: authenticated, unauthenticated - Consent recording: valid, custom type, validation errors - Consent status: authenticated, unauthenticated, tracking disabled * fix(security): invalidate all user sessions on GDPR account deletion Use SessionInvalidationService to invalidate ALL user sessions across all devices/browsers before account deletion, not just the current session. This prevents: - Security vulnerabilities from orphaned sessions with deleted user refs - GDPR non-compliance from user data remaining accessible via active sessions - Application errors when code tries to access the deleted user entity Changes: - Add SessionInvalidationService dependency to GdprAPI - Update logoutUser() to accept User parameter and call invalidateUserSessions() - Update GdprAPITest with SessionInvalidationService mock - Update test to use Jackson 3.x (tools.jackson) for consistency with main code * fix(gdpr): address PR review feedback for validation and documentation Address code review feedback from PR #252: - ConsentRequestDto: Change regex pattern from * to + to prevent empty strings from passing validation for custom consent types - GdprDeletionService: Capture user email at start of executeUserDeletion() before entity deletion to avoid potential entity lifecycle issues when publishing UserDeletedEvent - FileAuditLogQueryService: Enhance JavaDoc with detailed performance warning about file-scanning limitations (<50MB, <100K events) and recommendations for high-volume production deployments * fix(gdpr): address additional PR review feedback Apply fixes from second round of code review: Security/Compliance: - Sanitize custom consent type names in logs to prevent PII exposure (log "CUSTOM" instead of actual custom type name) Performance/Stability: - Add configurable maxQueryResults limit to AuditConfig (default 10000) to prevent unbounded memory usage when querying large audit logs - Apply limit in FileAuditLogQueryService.findByUser() stream Code Quality: - Inject Spring-configured ObjectMapper via constructor in GdprExportService and ConsentAuditService instead of creating new instances (respects application's Jackson configuration) - Update tests with @SPY ObjectMapper for proper injection Configuration: - Add user.audit.maxQueryResults property to dsspringuserconfig.properties * fix(gdpr): resolve Codex-identified bugs in audit and consent handling Address 3 bugs identified by Codex code review: [P1] Fix audit timestamp parsing for zone-less dates - FileAuditLogQueryService.parseTimestamp() now tries LocalDateTime.parse() after ZonedDateTime.parse() fails, handling MessageFormat output like "Jan 16, 2025, 2:03:04 PM" that lacks timezone info - Converts LocalDateTime to Instant using system default zone - Fixes null timestamps in all audit events [P2] Clear withdrawnAt when consent is re-granted - GdprExportService.exportConsents() now sets withdrawnAt=null when updating existing consent record with a grant - Fixes bug where re-granted consents appeared inactive [P3] Preserve user identity in GDPR deletion audit event - GdprAPI.deleteAccount() now passes user object to logAuditEvent() instead of null, preserving user ID/email in audit trail - User is still in memory at this point before logout * fix(gdpr): address final PR review feedback - Add JavaDoc warning in GdprDeletionService about contributor transaction safety (external API calls could cause partial deletion) - Consolidate ConsentAuditService to use shared UserUtils.getClientIP() - Fix session creation in GdprAPI audit logging (use getSession(false)) - Add rate limiting documentation to export and delete endpoints - Add defensive parsing in FileAuditLogQueryService for unescaped pipes * docs(gdpr): add GDPR documentation and disable by default - Change user.gdpr.enabled default to false for backward compatibility - Add GDPR features to README.md Features list - Add comprehensive GDPR Compliance section to README.md with: - Configuration instructions - Data export (Right of Access) documentation - Account deletion (Right to be Forgotten) documentation - Consent management API documentation - GdprDataContributor extensibility guide - GDPR events reference - Add GDPR configuration section to CONFIG.md - Add user.audit.maxQueryResults to CONFIG.md
1 parent 652ec69 commit c532550

29 files changed

Lines changed: 4600 additions & 0 deletions

CONFIG.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,26 @@ Welcome to the User Framework SpringBoot Configuration Guide! This document outl
3838

3939
- **Log File Path (`user.audit.logFilePath`)**: The path to the audit log file.
4040
- **Flush on Write (`user.audit.flushOnWrite`)**: Set to `true` for immediate log flushing. Defaults to `false` for performance.
41+
- **Max Query Results (`user.audit.maxQueryResults`)**: Maximum number of audit events returned from queries. Prevents memory issues with large logs. Defaults to `10000`.
42+
43+
## GDPR Compliance
44+
45+
GDPR features are disabled by default and must be explicitly enabled.
46+
47+
- **Enable GDPR (`user.gdpr.enabled`)**: Master toggle for all GDPR features. When `false`, all GDPR endpoints return 404. Defaults to `false`.
48+
- **Export Before Deletion (`user.gdpr.exportBeforeDeletion`)**: When `true`, user data is automatically exported and included in the deletion response. Defaults to `true`.
49+
- **Consent Tracking (`user.gdpr.consentTracking`)**: Enable consent grant/withdrawal tracking via the audit system. Defaults to `true`.
50+
51+
**Example configuration:**
52+
```yaml
53+
user:
54+
gdpr:
55+
enabled: true
56+
exportBeforeDeletion: true
57+
consentTracking: true
58+
```
59+
60+
**Note**: When GDPR is enabled, ensure you have a `UserPreDeleteEvent` listener configured to clean up application-specific user data before deletion. See the README for details.
4161

4262
## Security Settings
4363

README.md

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,13 @@ Check out the [Spring User Framework Demo Application](https://github.com/devond
5151
- [Handling User Account Deletion and Profile Cleanup](#handling-user-account-deletion-and-profile-cleanup)
5252
- [Enabling Actual Deletion](#enabling-actual-deletion)
5353
- [SSO OAuth2 with Google and Facebook](#sso-oauth2-with-google-and-facebook)
54+
- [GDPR Compliance](#gdpr-compliance)
55+
- [Enabling GDPR Features](#enabling-gdpr-features)
56+
- [Data Export (Right of Access)](#data-export-right-of-access)
57+
- [Account Deletion (Right to be Forgotten)](#account-deletion-right-to-be-forgotten)
58+
- [Consent Management](#consent-management)
59+
- [Extending GDPR Exports](#extending-gdpr-exports)
60+
- [GDPR Events](#gdpr-events)
5461
- [Examples](#examples)
5562
- [Contributing](#contributing)
5663
- [Reference Documentation](#reference-documentation)
@@ -92,6 +99,13 @@ Check out the [Spring User Framework Demo Application](https://github.com/devond
9299
- Comprehensive documentation
93100
- Demo application for reference
94101

102+
- **GDPR Compliance** (opt-in)
103+
- Data export (Right of Access - Article 15)
104+
- Account deletion (Right to be Forgotten - Article 17)
105+
- Consent tracking and management (Article 7)
106+
- Extensible data contributor system for custom data
107+
- Audit trail for all GDPR operations
108+
95109
## Installation
96110

97111
Choose the version that matches your Spring Boot version:
@@ -747,6 +761,182 @@ By implementing such a listener, your application ensures data integrity when th
747761
The framework supports SSO OAuth2 with Google, Facebook and Keycloak. To enable this you need to configure the client id and secret for each provider. This is done in the application.yml (or application.properties) file using the [Spring Security OAuth2 properties](https://docs.spring.io/spring-security/reference/servlet/oauth2/login/core.html). You can see the example configuration in the Demo Project's `application.yml` file.
748762

749763

764+
## GDPR Compliance
765+
766+
The framework provides opt-in GDPR compliance features to help your application meet European data protection requirements. These features are **disabled by default** and must be explicitly enabled.
767+
768+
### Enabling GDPR Features
769+
770+
Add the following to your `application.yml`:
771+
772+
```yaml
773+
user:
774+
gdpr:
775+
enabled: true # Master toggle for all GDPR features
776+
exportBeforeDeletion: true # Automatically export data before deletion
777+
consentTracking: true # Enable consent grant/withdrawal tracking
778+
```
779+
780+
When enabled, the following REST endpoints become available (all require authentication):
781+
782+
| Endpoint | Method | Description |
783+
|----------|--------|-------------|
784+
| `/user/gdpr/export` | GET | Export all user data as JSON |
785+
| `/user/gdpr/delete` | POST | Request account deletion |
786+
| `/user/gdpr/consent` | POST | Record consent grant or withdrawal |
787+
| `/user/gdpr/consent/status` | GET | Get current consent status |
788+
789+
### Data Export (Right of Access)
790+
791+
Users can request a complete export of their data via the `/user/gdpr/export` endpoint. The export includes:
792+
793+
- **User account data**: Name, email, registration date, roles
794+
- **Audit history**: Login events, password changes, profile updates
795+
- **Consent records**: All consent grants and withdrawals with timestamps
796+
- **Token metadata**: Verification and password reset token expiry (not actual tokens)
797+
- **Custom data**: Any data contributed by registered `GdprDataContributor` beans
798+
799+
**Example Response:**
800+
```json
801+
{
802+
"success": true,
803+
"data": {
804+
"exportedAt": "2024-01-15T10:30:00Z",
805+
"user": {
806+
"id": 123,
807+
"email": "user@example.com",
808+
"firstName": "John",
809+
"lastName": "Doe",
810+
"createdDate": "2023-06-01T08:00:00Z",
811+
"roles": ["ROLE_USER"]
812+
},
813+
"auditHistory": [...],
814+
"consents": [...],
815+
"customData": {}
816+
}
817+
}
818+
```
819+
820+
### Account Deletion (Right to be Forgotten)
821+
822+
Users can request complete deletion of their account via the `/user/gdpr/delete` endpoint. The deletion process:
823+
824+
1. **Exports data** (if `exportBeforeDeletion=true`) and includes it in the response
825+
2. **Notifies contributors** via `GdprDataContributor.prepareForDeletion()`
826+
3. **Publishes `UserPreDeleteEvent`** for custom cleanup listeners
827+
4. **Deletes framework data**: Verification tokens, password reset tokens, password history
828+
5. **Deletes user entity** from database
829+
6. **Publishes `UserDeletedEvent`** for post-deletion processing
830+
7. **Invalidates all sessions** across all devices
831+
8. **Logs out** the current session
832+
833+
**Important**: This performs a hard delete. Ensure you have the `UserPreDeleteEvent` listener configured (see [Handling User Account Deletion](#handling-user-account-deletion-and-profile-cleanup)) to clean up related data.
834+
835+
### Consent Management
836+
837+
Track user consent for various purposes (marketing, analytics, data processing, etc.):
838+
839+
**Recording Consent:**
840+
```bash
841+
# Grant consent
842+
curl -X POST /user/gdpr/consent \
843+
-H "Content-Type: application/json" \
844+
-d '{"consentType": "MARKETING", "granted": true}'
845+
846+
# Withdraw consent
847+
curl -X POST /user/gdpr/consent \
848+
-H "Content-Type: application/json" \
849+
-d '{"consentType": "MARKETING", "granted": false}'
850+
851+
# Custom consent type
852+
curl -X POST /user/gdpr/consent \
853+
-H "Content-Type: application/json" \
854+
-d '{"consentType": "CUSTOM", "customType": "newsletter_weekly", "granted": true}'
855+
```
856+
857+
**Built-in Consent Types:**
858+
- `MARKETING` - Marketing communications
859+
- `ANALYTICS` - Analytics and tracking
860+
- `THIRD_PARTY` - Third-party data sharing
861+
- `PROFILING` - User profiling
862+
- `CUSTOM` - Application-specific (requires `customType` field)
863+
864+
**Checking Consent Status:**
865+
```bash
866+
curl /user/gdpr/consent/status?type=MARKETING
867+
```
868+
869+
All consent changes are recorded in the audit log with timestamps, IP addresses, and user agent information.
870+
871+
### Extending GDPR Exports
872+
873+
To include your application's custom data in GDPR exports, implement the `GdprDataContributor` interface:
874+
875+
```java
876+
@Component
877+
public class OrderDataContributor implements GdprDataContributor {
878+
879+
private final OrderRepository orderRepository;
880+
881+
public OrderDataContributor(OrderRepository orderRepository) {
882+
this.orderRepository = orderRepository;
883+
}
884+
885+
@Override
886+
public String getDataKey() {
887+
return "orders"; // Key in the export JSON
888+
}
889+
890+
@Override
891+
public Object contributeData(User user) {
892+
// Return data to include in export (will be serialized to JSON)
893+
return orderRepository.findByUserId(user.getId())
894+
.stream()
895+
.map(this::toExportDto)
896+
.toList();
897+
}
898+
899+
@Override
900+
public void prepareForDeletion(User user) {
901+
// Clean up data before user deletion (runs within transaction)
902+
// WARNING: Only delete LOCAL database data here, not external APIs
903+
orderRepository.deleteByUserId(user.getId());
904+
}
905+
906+
private OrderExportDto toExportDto(Order order) {
907+
// Map to DTO for export
908+
}
909+
}
910+
```
911+
912+
**Important**: The `prepareForDeletion()` method runs within the same database transaction as user deletion. Only perform local database operations here. For external API cleanup, use a `UserDeletedEvent` listener instead.
913+
914+
### GDPR Events
915+
916+
The framework publishes Spring events for GDPR operations:
917+
918+
| Event | When Published | Use Case |
919+
|-------|----------------|----------|
920+
| `UserPreDeleteEvent` | Before user deletion (in transaction) | Clean up related database records |
921+
| `UserDeletedEvent` | After successful deletion | External API cleanup, notifications |
922+
| `UserDataExportedEvent` | After data export | Audit logging, analytics |
923+
| `ConsentChangedEvent` | After consent grant/withdrawal | Trigger consent-dependent workflows |
924+
925+
**Example: External Cleanup After Deletion**
926+
```java
927+
@Component
928+
public class ExternalCleanupListener {
929+
930+
@EventListener
931+
@Async // Run asynchronously after transaction commits
932+
public void onUserDeleted(UserDeletedEvent event) {
933+
// Safe to call external APIs here
934+
externalCrmService.deleteCustomer(event.getUserEmail());
935+
analyticsService.anonymizeUser(event.getUserId());
936+
}
937+
}
938+
```
939+
750940
## Examples
751941

752942
For complete working examples, check out the [Spring User Framework Demo Application](https://github.com/devondragon/SpringUserFrameworkDemoApp).

0 commit comments

Comments
 (0)