Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
@Slf4j
public class AccessControlAuditLogAspect {

private final AccessControlAuditLogger accessControlAuditLogger;
private final AuditLogger auditLogger;

private final ObjectMapper objectMapper;
private final ExpressionParser parser = new SpelExpressionParser();
Expand All @@ -57,9 +57,7 @@ public Object auditAround(ProceedingJoinPoint joinPoint, AccessControl accessCon

try {
action = accessControl.actionPerformed();
isActive =
accessControlAuditLogger.isAuditLoggingEnabled()
&& accessControlAuditLogger.isAuditLoggingValid(action);
isActive = auditLogger.isAuditLoggingEnabled() && auditLogger.isAuditLoggingValid(action);
} catch (Exception ex) {
log.warn("Error during audit logging", ex);
}
Expand Down Expand Up @@ -113,7 +111,7 @@ public Object auditAround(ProceedingJoinPoint joinPoint, AccessControl accessCon
JsonNode resultNode = getOutputNode(result);
JsonNode errorNode = buildErrorNode(resultNode, ex);

accessControlAuditLogger
auditLogger
.logAccessControlEvent(
eventScope,
"error",
Expand All @@ -134,7 +132,7 @@ public Object auditAround(ProceedingJoinPoint joinPoint, AccessControl accessCon
try {
JsonNode resultNode = getOutputNode(result);

accessControlAuditLogger
auditLogger
.logAccessControlEvent(
eventScope,
"success",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.fasterxml.jackson.databind.JsonNode;
import io.openaev.aop.AccessControl;
import io.openaev.aop.AccessControlAspect;
import io.openaev.config.ThreadPoolTaskLoggerConfig;
import io.openaev.database.model.Action;
import io.openaev.database.model.ResourceType;
import io.openaev.service.LogService;
Expand All @@ -27,7 +28,7 @@
@ConditionalOnProperty(name = "openaev.audit-logs.service.enabled", havingValue = "true")
@RequiredArgsConstructor
@Slf4j
public class AccessControlAuditLogger {
public class AuditLogger {

private final AuditRequestValidator auditRequestValidator;

Expand All @@ -42,7 +43,24 @@ public boolean isAuditLoggingValid(Action action) {
}

/** Wraps the audit service call in try/catch — audit must never break the business flow. */
@Async("accessControlAuditLoggerExecutor")
@Async("taskLoggerExecutor")
public CompletableFuture<Boolean> logAuthEventWithRequestContext(
ThreadPoolTaskLoggerConfig.ThreadRequestContextHolder.RequestContextData rcd,
String eventScope,
String eventStatus,
String provider,
String reason,
String logUUID) {

if (rcd != null) {
ThreadPoolTaskLoggerConfig.ThreadRequestContextHolder.setRequestContextData(rcd);
}

return logAuthEvent(eventScope, eventStatus, provider, reason, logUUID);
}

/** Wraps the audit service call in try/catch — audit must never break the business flow. */
@Async("taskLoggerExecutor")
public CompletableFuture<Boolean> logAuthEvent(
String eventScope, String eventStatus, String provider, String reason, String logUUID) {
boolean status = false;
Expand All @@ -64,7 +82,7 @@ public CompletableFuture<Boolean> logAuthEvent(
return CompletableFuture.completedFuture(status);
}

@Async("accessControlAuditLoggerExecutor")
@Async("taskLoggerExecutor")
public CompletableFuture<Boolean> logAccessControlEvent(
String eventScope,
String eventStatus,
Expand Down
32 changes: 30 additions & 2 deletions openaev-api/src/main/java/io/openaev/config/AppSecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import static org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI;

import com.fasterxml.jackson.databind.ObjectMapper;
import io.openaev.aop.audit_log.AuditLogger;
import io.openaev.config.security.OpenSamlConfig;
import io.openaev.config.security.SecurityService;
import io.openaev.database.model.User;
Expand All @@ -18,9 +19,11 @@
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.security.config.Customizer;
Expand Down Expand Up @@ -64,6 +67,8 @@ public class AppSecurityConfig {
private final UserEventService userEventService;
private final UserMappingService userMappingService;

@Autowired @Lazy private AuditLogger auditLogger;

@Resource protected ObjectMapper mapper;

@Bean
Expand Down Expand Up @@ -117,6 +122,28 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.logout(
logout ->
logout
// Audit Log: audit handler fires first, then Spring Security's built-in
// SecurityContextLogoutHandler
// invalidates the session and clears cookies
.addLogoutHandler(
(request, response, authentication) -> {
ThreadPoolTaskLoggerConfig.ThreadRequestContextHolder.RequestContextData
rcd = null;
try {
rcd =
ThreadPoolTaskLoggerConfig.buildThreadRequestContextHolder(
request, authentication);
} catch (Exception e) {
// Never block the logout flow
log.error(
"Failed to prepare request context on the logout callback handler: {}",
e.getMessage(),
e);
}

auditLogger.logAuthEventWithRequestContext(
rcd, "logout", "success", null, null, null);
})
Comment on lines 122 to +146
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID", openAEVConfig.getCookieName())
.logoutSuccessUrl(
Expand All @@ -131,9 +158,10 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
auth.authorizationRequestResolver(
authorizationRequestResolver(
http.getSharedObject(ClientRegistrationRepository.class))))
.successHandler(new SsoRefererAuthenticationSuccessHandler())
.successHandler(new SsoRefererAuthenticationSuccessHandler(this.auditLogger))
.failureHandler(
new SsoRefererAuthenticationFailureHandler(this.userEventService)));
new SsoRefererAuthenticationFailureHandler(
this.userEventService, this.auditLogger)));
}

if (openAEVConfig.isAuthSaml2Enable()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import org.springframework.context.annotation.Configuration;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.context.request.RequestContextHolder;
Expand All @@ -21,49 +22,36 @@ public class ThreadPoolTaskLoggerConfig {
private ThreadPoolTaskExecutor createBaseExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();

// TODO: find a better way to configure this variables dynamically - maybe through properties
// file.
// TODO AUDIT: find a better way to configure this variables dynamically - maybe through
// properties file.

executor.setCorePoolSize(10);
executor.setMaxPoolSize(50);
executor.setQueueCapacity(1000);
executor.setThreadNamePrefix("AuditLogger-");
executor.setThreadNamePrefix("TaskLogger-");

return executor;
}

@Bean(name = "accessControlAuditLoggerExecutor")
@Bean(name = "taskLoggerExecutor")
public Executor contextAwareExecutor() {
ThreadPoolTaskExecutor executor = createBaseExecutor();

executor.setTaskDecorator(
runnable -> {
// CAPTURE REQUEST HEADERS AND IP, REQUEST URI and BODY (PARENT THREAD)
// CAPTURE REQUEST (PARENT THREAD)
var requestAttributes = RequestContextHolder.getRequestAttributes();
HttpServletRequest request =
requestAttributes instanceof ServletRequestAttributes attrs
? attrs.getRequest()
: null;
Map<String, String> headers;
String remoteAddress, method, url;

if (request != null) {
headers = HttpReqRespUtils.extractHeaders(request);
remoteAddress = request.getRemoteAddr();
method = request.getMethod();

url = request.getRequestURL().toString();
} else {
headers = null;
remoteAddress = method = url = null;
}

// CAPTURE LOGs CONTEXT
var mdcContext = MDC.getCopyOfContextMap();

// CAPTURE AUTHENTICATION CONTEXT (PARENT THREAD)
var originalSecurityContext = SecurityContextHolder.getContext();
var authentication = originalSecurityContext.getAuthentication();
Authentication authentication = originalSecurityContext.getAuthentication();

SecurityContext securityContextCopy =
SecurityContextHolder.createEmptyContext(); // SAFE COPY (IMPORTANT)
Expand All @@ -75,12 +63,14 @@ public Executor contextAwareExecutor() {
// CAPTURE LOCALE CONTEXT (PARENT THREAD)
var localeContext = LocaleContextHolder.getLocaleContext();

// CREATE REQUEST CONTEXT HOLDER DATA WITH REQUEST HEADERS AND IP, REQUEST URI...
ThreadRequestContextHolder.RequestContextData rcd =
buildThreadRequestContextHolder(request, authentication);

return () -> {
try {
// STORE HEADERS AND REMOTE ADDRESS
ThreadRequestContextHolder.setRequestContextData(
new ThreadRequestContextHolder.RequestContextData(
headers, remoteAddress, method, url));
ThreadRequestContextHolder.setRequestContextData(rcd);

// RESTORE MDC
if (mdcContext != null) {
Expand Down Expand Up @@ -112,10 +102,36 @@ public Executor contextAwareExecutor() {
return executor;
}

public static ThreadRequestContextHolder.RequestContextData buildThreadRequestContextHolder(
HttpServletRequest request, Authentication authentication) {
// GET REQUEST HEADERS AND IP, REQUEST URI and BODY (PARENT THREAD)
Map<String, String> headers;
String remoteAddress, method, url;

if (request != null) {
headers = HttpReqRespUtils.extractHeaders(request);
remoteAddress = request.getRemoteAddr();
method = request.getMethod();

url = request.getRequestURL().toString();
} else {
headers = null;
remoteAddress = method = url = null;
}

// STORE HEADERS AND REMOTE ADDRESS
return new ThreadRequestContextHolder.RequestContextData(
headers, remoteAddress, method, url, authentication);
}

public static class ThreadRequestContextHolder {

public record RequestContextData(
Map<String, String> headers, String remoteAddress, String method, String url) {}
Map<String, String> headers,
String remoteAddress,
String method,
String url,
Authentication authentication) {}

private static final ThreadLocal<Map<String, Object>> CONTEXT = new ThreadLocal<>();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import static io.openaev.database.model.User.ROLE_USER;
import static org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider.createDefaultResponseAuthenticationConverter;

import io.openaev.aop.audit_log.AuditLogger;
import io.openaev.config.OpenAEVSaml2User;
import io.openaev.database.model.User;
import io.openaev.security.SsoRefererAuthenticationSuccessHandler;
Expand All @@ -17,6 +18,7 @@
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.env.Environment;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
Expand Down Expand Up @@ -49,6 +51,8 @@ public class OpenSamlConfig {

private final UserEventService userEventService;

@Autowired @Lazy private AuditLogger auditLogger;

public void addOpenSamlConfig(@NotNull final HttpSecurity http) throws Exception {
if (this.relyingPartyRegistrationRepository == null) {
log.warn("No RelyingPartyRegistrationRepository found, skipping SAML2 configuration.");
Expand All @@ -66,7 +70,7 @@ public void addOpenSamlConfig(@NotNull final HttpSecurity http) throws Exception
saml2Login ->
saml2Login
.authenticationManager(new ProviderManager(authenticationProvider))
.successHandler(new SsoRefererAuthenticationSuccessHandler()));
.successHandler(new SsoRefererAuthenticationSuccessHandler(this.auditLogger)));
}

// -- PRIVATE --
Expand Down
5 changes: 5 additions & 0 deletions openaev-api/src/main/java/io/openaev/rest/user/UserApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import io.openaev.aop.AccessControl;
import io.openaev.aop.UserRoleDescription;
import io.openaev.aop.audit_log.AuditLogger;
import io.openaev.config.SessionManager;
import io.openaev.database.model.Action;
import io.openaev.database.model.ResourceType;
Expand Down Expand Up @@ -51,6 +52,7 @@ public class UserApi extends RestBehavior {
private final UserRepository userRepository;
private final UserService userService;
private final UserEventService userEventService;
private final AuditLogger auditLogger;

@Operation(description = "Endpoint to login", summary = "Endpoint to login")
@ApiResponses(
Expand All @@ -70,11 +72,14 @@ public User login(@Valid @RequestBody LoginUserInput input) {
if (userService.isUserPasswordValid(user, input.getPassword())) {
userService.createUserSession(user);
userEventService.createLoginSuccessEvent(user);
auditLogger.logAuthEvent("login", "success", "local", null, null);
return user;
}
}
userEventService.createLoginFailedEvent(
"local login", BadCredentialsException.class.getSimpleName());
auditLogger.logAuthEvent(
"login", "error", "local", BadCredentialsException.class.getSimpleName(), null);
throw new BadCredentialsException("Invalid credential.");
Comment thread
a19836 marked this conversation as resolved.
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import static org.springframework.http.HttpHeaders.REFERER;

import io.openaev.aop.audit_log.AuditLogger;
import io.openaev.service.user_events.UserEventService;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
Expand All @@ -18,9 +19,12 @@ public class SsoRefererAuthenticationFailureHandler extends SimpleUrlAuthenticat

private RequestCache requestCache = new HttpSessionRequestCache();
private final UserEventService userEventService;
private final AuditLogger auditLogger;

public SsoRefererAuthenticationFailureHandler(UserEventService userEventService) {
public SsoRefererAuthenticationFailureHandler(
UserEventService userEventService, AuditLogger auditLogger) {
this.userEventService = userEventService;
this.auditLogger = auditLogger;
}

@Override
Expand All @@ -29,6 +33,8 @@ public void onAuthenticationFailure(
throws ServletException, IOException {
userEventService.createLoginFailedEvent(
request.getRequestURI(), exception.getClass().getSimpleName());
auditLogger.logAuthEvent(
"login", "error", request.getRequestURI(), exception.getClass().getSimpleName(), null);
Comment on lines 34 to +37

Comment on lines 34 to +38
this.saveException(request, exception);
SavedRequest savedRequest = this.requestCache.getRequest(request, response);
Expand Down
Loading
Loading