Skip to content

Commit 41ac078

Browse files
authored
Merge pull request #211 from devondragon/issue-210-Audit-logging-throws-NPE-on-first-time-OAuth-registration-when-user-ID-is-null
Fix NPE in audit logging when user ID is null
2 parents f4225a6 + cec8d4b commit 41ac078

File tree

5 files changed

+514
-8
lines changed

5 files changed

+514
-8
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,14 +96,14 @@ Check out the [Spring User Framework Demo Application](https://github.com/devond
9696
<dependency>
9797
<groupId>com.digitalsanctuary</groupId>
9898
<artifactId>ds-spring-user-framework</artifactId>
99-
<version>3.4.0</version>
99+
<version>3.4.1</version>
100100
</dependency>
101101
```
102102

103103
### Gradle
104104

105105
```groovy
106-
implementation 'com.digitalsanctuary:ds-spring-user-framework:3.4.0'
106+
implementation 'com.digitalsanctuary:ds-spring-user-framework:3.4.1'
107107
```
108108

109109
## Quick Start

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

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,15 @@ public class AuditEventListener {
3131
*/
3232
@EventListener
3333
public void onApplicationEvent(AuditEvent event) {
34-
log.debug("AuditEventListener.onApplicationEvent: called with event: {}", event);
35-
if (auditConfig.isLogEvents() && event != null) {
36-
log.debug("AuditEventListener.onApplicationEvent: logging event...");
37-
auditLogWriter.writeLog(event);
34+
try {
35+
log.debug("AuditEventListener.onApplicationEvent: called with event: {}", event);
36+
if (auditConfig.isLogEvents() && event != null) {
37+
log.debug("AuditEventListener.onApplicationEvent: logging event...");
38+
auditLogWriter.writeLog(event);
39+
}
40+
} catch (Exception e) {
41+
// Never let audit failures impact application flow
42+
log.error("AuditEventListener.onApplicationEvent: Failed to process audit event (suppressed): {}", e.getMessage(), e);
3843
}
3944
}
4045
}

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

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,25 @@ public synchronized void writeLog(AuditEvent event) {
6565
return;
6666
}
6767
try {
68-
String userId = event.getUser() != null ? event.getUser().getId().toString() : null;
69-
String userEmail = event.getUser() != null ? event.getUser().getEmail() : null;
68+
// Null-safe extraction of user ID and email
69+
String userId = null;
70+
String userEmail = null;
71+
72+
if (event.getUser() != null) {
73+
Long id = event.getUser().getId();
74+
userId = (id != null) ? id.toString() : null;
75+
userEmail = event.getUser().getEmail();
76+
}
77+
78+
// If no user ID available, use email or "unknown" for the userId field
79+
if (userId == null) {
80+
if (userEmail != null) {
81+
userId = userEmail; // Use email as identifier when ID is null
82+
} else {
83+
userId = "unknown"; // Fallback when both are null
84+
}
85+
}
86+
7087
String output = MessageFormat.format("{0}|{1}|{2}|{3}|{4}|{5}|{6}|{7}|{8}|{9}", event.getDate(), event.getAction(),
7188
event.getActionStatus(), userId, userEmail, event.getIpAddress(), event.getSessionId(), event.getMessage(), event.getUserAgent(),
7289
event.getExtraData());
@@ -77,6 +94,9 @@ public synchronized void writeLog(AuditEvent event) {
7794
}
7895
} catch (IOException e) {
7996
log.error("FileAuditLogWriter.writeLog: IOException writing to log file: {}", auditConfig.getLogFilePath(), e);
97+
} catch (Exception e) {
98+
// Never let audit failures impact app flow
99+
log.error("FileAuditLogWriter.writeLog: Failed to write audit log (suppressed): {}", e.getMessage(), e);
80100
}
81101
}
82102

src/test/java/com/digitalsanctuary/spring/user/audit/AuditEventListenerTest.java

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.digitalsanctuary.spring.user.audit;
22

33
import static org.mockito.Mockito.*;
4+
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
45

56
import com.digitalsanctuary.spring.user.persistence.model.User;
67
import com.digitalsanctuary.spring.user.test.builders.UserTestDataBuilder;
@@ -168,6 +169,32 @@ void logs_failedLoginEvent() {
168169
verify(auditLogWriter).writeLog(auditEvent);
169170
}
170171

172+
@Test
173+
@DisplayName("Handles event with user having null ID gracefully")
174+
void handles_eventWithUserHavingNullId() {
175+
// Given
176+
User userWithNullId = UserTestDataBuilder.aUser()
177+
.withId(null) // User exists but ID is null (e.g., during registration)
178+
.withEmail("[email protected]")
179+
.withFirstName("New")
180+
.withLastName("User")
181+
.build();
182+
183+
auditEvent = AuditEvent.builder()
184+
.source(this)
185+
.user(userWithNullId)
186+
.action("Registration")
187+
.actionStatus("In Progress")
188+
.message("User registration started")
189+
.build();
190+
191+
// When
192+
auditEventListener.onApplicationEvent(auditEvent);
193+
194+
// Then
195+
verify(auditLogWriter).writeLog(auditEvent);
196+
}
197+
171198
@Test
172199
@DisplayName("Logs account deletion event")
173200
void logs_accountDeletionEvent() {
@@ -305,4 +332,55 @@ void logs_eventWithCompleteInfo() {
305332
verify(auditLogWriter).writeLog(auditEvent);
306333
}
307334
}
335+
336+
@Nested
337+
@DisplayName("Exception Handling Tests")
338+
class ExceptionHandlingTests {
339+
340+
@BeforeEach
341+
void setUp() {
342+
when(auditConfig.isLogEvents()).thenReturn(true);
343+
}
344+
345+
@Test
346+
@DisplayName("Does not propagate exceptions from writer")
347+
void doesNotPropagateExceptionsFromWriter() {
348+
// Given
349+
auditEvent = AuditEvent.builder()
350+
.source(this)
351+
.user(testUser)
352+
.action("Test Action")
353+
.actionStatus("Success")
354+
.build();
355+
356+
// Simulate an exception in the writer
357+
doThrow(new RuntimeException("Writer failed")).when(auditLogWriter).writeLog(any());
358+
359+
// When & Then - should not throw
360+
assertDoesNotThrow(() -> auditEventListener.onApplicationEvent(auditEvent));
361+
362+
// Verify the writer was still called
363+
verify(auditLogWriter).writeLog(auditEvent);
364+
}
365+
366+
@Test
367+
@DisplayName("Handles exceptions when checking config")
368+
void handlesExceptionsWhenCheckingConfig() {
369+
// Given
370+
auditEvent = AuditEvent.builder()
371+
.source(this)
372+
.action("Test")
373+
.actionStatus("Success")
374+
.build();
375+
376+
// Simulate an exception when checking config
377+
when(auditConfig.isLogEvents()).thenThrow(new RuntimeException("Config error"));
378+
379+
// When & Then - should not throw
380+
assertDoesNotThrow(() -> auditEventListener.onApplicationEvent(auditEvent));
381+
382+
// Verify writer was never called since config check failed
383+
verify(auditLogWriter, never()).writeLog(any());
384+
}
385+
}
308386
}

0 commit comments

Comments
 (0)