Skip to content

Commit fd6dda4

Browse files
committed
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
1 parent a071045 commit fd6dda4

4 files changed

Lines changed: 42 additions & 10 deletions

File tree

src/main/java/com/digitalsanctuary/spring/user/api/GdprAPI.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,11 @@ public class GdprAPI {
7777
* <li>Data from registered GdprDataContributors</li>
7878
* </ul>
7979
*
80+
* <p><b>Rate Limiting:</b> This endpoint performs resource-intensive operations
81+
* (file I/O, JSON parsing, data aggregation). Consider implementing rate limiting
82+
* at the infrastructure level (e.g., API gateway, Spring Security rate limiter,
83+
* or reverse proxy) to prevent abuse.
84+
*
8085
* @param userDetails the authenticated user
8186
* @param request the HTTP request
8287
* @return the complete data export as JSON
@@ -117,6 +122,10 @@ public ResponseEntity<JSONResponse> exportUserData(@AuthenticationPrincipal DSUs
117122
* <p>If configured, exports user data before deletion and includes
118123
* it in the response. After deletion, the user is logged out.
119124
*
125+
* <p><b>Rate Limiting:</b> This endpoint performs destructive, resource-intensive
126+
* operations. Consider implementing rate limiting at the infrastructure level
127+
* to prevent abuse or accidental repeated deletion attempts.
128+
*
120129
* @param userDetails the authenticated user
121130
* @param request the HTTP request
122131
* @return deletion result, optionally including exported data
@@ -306,7 +315,7 @@ private void logAuditEvent(String action, String status, String message, User us
306315
AuditEvent event = AuditEvent.builder()
307316
.source(this)
308317
.user(user)
309-
.sessionId(request.getSession().getId())
318+
.sessionId(request.getSession(false) != null ? request.getSession(false).getId() : null)
310319
.ipAddress(UserUtils.getClientIP(request))
311320
.userAgent(request.getHeader("User-Agent"))
312321
.action(action)

src/main/java/com/digitalsanctuary/spring/user/audit/FileAuditLogQueryService.java

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,11 @@ private Path getLogFilePath() {
159159
/**
160160
* Parses a single line from the audit log file.
161161
*
162+
* <p><b>Note:</b> This parser assumes the audit log writer properly escapes
163+
* pipe characters in message content. If audit messages contain unescaped pipes,
164+
* parsing may be corrupted. Consider migrating to a structured format (JSON lines)
165+
* for production deployments with untrusted input.
166+
*
162167
* @param line the line to parse
163168
* @return the parsed AuditEventDTO, or null if parsing fails
164169
*/
@@ -169,10 +174,27 @@ private AuditEventDTO parseLine(String line) {
169174

170175
String[] parts = line.split("\\|", -1); // -1 to keep trailing empty strings
171176
if (parts.length < 10) {
172-
log.debug("FileAuditLogQueryService.parseLine: Invalid line format, expected 10 fields: {}", line);
177+
log.debug("FileAuditLogQueryService.parseLine: Invalid line format, expected 10 fields but got {}: {}",
178+
parts.length, line);
173179
return null;
174180
}
175181

182+
// Defensive: If more than 10 fields exist due to unescaped pipes in message,
183+
// join the extra parts back into the message field
184+
if (parts.length > 10) {
185+
log.debug("FileAuditLogQueryService.parseLine: Line has {} fields (expected 10), " +
186+
"likely due to unescaped pipes in message content", parts.length);
187+
// Join parts[7] through parts[parts.length-3] as the message
188+
StringBuilder messageBuilder = new StringBuilder(parts[7]);
189+
for (int i = 8; i < parts.length - 2; i++) {
190+
messageBuilder.append("|").append(parts[i]);
191+
}
192+
parts[7] = messageBuilder.toString();
193+
// Shift the last two fields (userAgent and extraData) to their expected positions
194+
parts[8] = parts[parts.length - 2];
195+
parts[9] = parts[parts.length - 1];
196+
}
197+
176198
try {
177199
return AuditEventDTO.builder()
178200
.timestamp(parseTimestamp(parts[0]))

src/main/java/com/digitalsanctuary/spring/user/gdpr/ConsentAuditService.java

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import com.digitalsanctuary.spring.user.audit.AuditLogQueryService;
1414
import com.digitalsanctuary.spring.user.event.ConsentChangedEvent;
1515
import com.digitalsanctuary.spring.user.persistence.model.User;
16+
import com.digitalsanctuary.spring.user.util.UserUtils;
1617
import jakarta.servlet.http.HttpServletRequest;
1718
import lombok.RequiredArgsConstructor;
1819
import lombok.extern.slf4j.Slf4j;
@@ -393,14 +394,7 @@ private ConsentRecord parseConsentRecord(AuditEventDTO event) {
393394
* Gets the client IP address from the request.
394395
*/
395396
private String getClientIP(HttpServletRequest request) {
396-
if (request == null) {
397-
return null;
398-
}
399-
String xForwardedFor = request.getHeader("X-Forwarded-For");
400-
if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
401-
return xForwardedFor.split(",")[0].trim();
402-
}
403-
return request.getRemoteAddr();
397+
return UserUtils.getClientIP(request);
404398
}
405399

406400
/**

src/main/java/com/digitalsanctuary/spring/user/gdpr/GdprDeletionService.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,13 @@ protected DeletionResult executeUserDeletion(User user, GdprExportDTO exportedDa
178178

179179
/**
180180
* Notifies all GdprDataContributors to prepare for deletion.
181+
* <p>
182+
* <b>IMPORTANT:</b> Contributors MUST only delete data within the same transactional context
183+
* (i.e., database records in the same database). Avoid calling external APIs or services
184+
* that could succeed while the main transaction fails, leading to partial deletion.
185+
* <p>
186+
* If external data needs cleanup, consider implementing async event listeners for
187+
* {@link com.digitalsanctuary.spring.user.event.UserDeletedEvent} instead.
181188
*/
182189
private void prepareContributorsForDeletion(User user) {
183190
if (dataContributors == null || dataContributors.isEmpty()) {

0 commit comments

Comments
 (0)