From c8321fdb44aa300795e05b46fbf5ee7f2490d6b9 Mon Sep 17 00:00:00 2001 From: Avgustin Marinov Date: Fri, 9 Aug 2024 14:27:07 +0300 Subject: [PATCH] Feature/add tenant and user into mdc (#1806) * Add MDC * Add tenant/user into MDC in order to be possible to be used in logging Enabled by default. Could be disabled via hawkbit.logging.mdchandler.enable=false Signed-off-by: Marinov Avgustin --------- Signed-off-by: Marinov Avgustin --- .../OidcUserManagementAutoConfiguration.java | 5 +- .../security/SecurityAutoConfiguration.java | 19 +-- .../SecurityManagedConfiguration.java | 11 +- .../eclipse/hawkbit/security/MDCHandler.java | 160 ++++++++++++++++++ .../security/SystemSecurityContext.java | 27 ++- 5 files changed, 189 insertions(+), 33 deletions(-) create mode 100644 hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/MDCHandler.java diff --git a/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/security/OidcUserManagementAutoConfiguration.java b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/security/OidcUserManagementAutoConfiguration.java index 0edae78db..f7f374391 100644 --- a/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/security/OidcUserManagementAutoConfiguration.java +++ b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/security/OidcUserManagementAutoConfiguration.java @@ -178,8 +178,7 @@ public class OidcUserManagementAutoConfiguration { } /** - * Utility class to extract authorities out of the jwt. It interprets the user's - * role as their authorities. + * Utility class to extract authorities out of the jwt. It interprets the user's role as their authorities. */ private record DefaultJwtAuthoritiesExtractor(GrantedAuthoritiesMapper authoritiesMapper) implements JwtAuthoritiesExtractor { @@ -220,4 +219,4 @@ public class OidcUserManagementAutoConfiguration { return new LinkedHashSet<>(authorities); } } -} +} \ No newline at end of file diff --git a/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/security/SecurityAutoConfiguration.java b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/security/SecurityAutoConfiguration.java index 5c3611f10..3c2b52675 100644 --- a/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/security/SecurityAutoConfiguration.java +++ b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/security/SecurityAutoConfiguration.java @@ -21,6 +21,7 @@ import org.eclipse.hawkbit.im.authentication.TenantAwareUserProperties.User; import org.eclipse.hawkbit.security.DdiSecurityProperties; import org.eclipse.hawkbit.security.InMemoryUserAuthoritiesResolver; import org.eclipse.hawkbit.security.HawkbitSecurityProperties; +import org.eclipse.hawkbit.security.MDCHandler; import org.eclipse.hawkbit.security.SecurityContextSerializer; import org.eclipse.hawkbit.security.SecurityContextTenantAware; import org.eclipse.hawkbit.security.SecurityTokenGenerator; @@ -121,36 +122,30 @@ public class SecurityAutoConfiguration { return new SystemSecurityContext(tenantAware, roleHierarchy); } - /** - * @return {@link SecurityTokenGenerator} bean - */ + @Bean + @ConditionalOnMissingBean + public MDCHandler mdcHandler() { + return MDCHandler.getInstance(); + } + @Bean @ConditionalOnMissingBean public SecurityTokenGenerator securityTokenGenerator() { return new SecurityTokenGenerator(); } - /** - * @return {@link AuthenticationSuccessHandler} bean - */ @Bean @ConditionalOnMissingBean public AuthenticationSuccessHandler authenticationSuccessHandler() { return new SimpleUrlAuthenticationSuccessHandler(); } - /** - * @return {@link LogoutHandler} bean - */ @Bean @ConditionalOnMissingBean public LogoutHandler logoutHandler() { return new SecurityContextLogoutHandler(); } - /** - * @return {@link LogoutSuccessHandler} bean - */ @Bean @ConditionalOnMissingBean public LogoutSuccessHandler logoutSuccessHandler() { diff --git a/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/security/SecurityManagedConfiguration.java b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/security/SecurityManagedConfiguration.java index ad74a461b..a15f5843c 100644 --- a/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/security/SecurityManagedConfiguration.java +++ b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/security/SecurityManagedConfiguration.java @@ -35,6 +35,7 @@ import org.eclipse.hawkbit.security.HttpControllerPreAuthenticateSecurityTokenFi import org.eclipse.hawkbit.security.HttpControllerPreAuthenticatedGatewaySecurityTokenFilter; import org.eclipse.hawkbit.security.HttpControllerPreAuthenticatedSecurityHeaderFilter; import org.eclipse.hawkbit.security.HttpDownloadAuthenticationFilter; +import org.eclipse.hawkbit.security.MDCHandler; import org.eclipse.hawkbit.security.PreAuthTokenSourceTrustAuthenticationProvider; import org.eclipse.hawkbit.security.SystemSecurityContext; import org.eclipse.hawkbit.tenancy.TenantAware; @@ -57,7 +58,6 @@ import org.springframework.security.config.annotation.method.configuration.Enabl import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; -import org.springframework.security.config.annotation.web.configurers.HttpBasicConfigurer; import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.core.Authentication; @@ -206,6 +206,8 @@ public class SecurityManagedConfiguration { .sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); } + MDCHandler.getInstance().addLoggingFilter(http); + return http.build(); } } @@ -321,6 +323,8 @@ public class SecurityManagedConfiguration { .sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); } + MDCHandler.getInstance().addLoggingFilter(http); + return http.build(); } } @@ -383,6 +387,8 @@ public class SecurityManagedConfiguration { .addFilterBefore(downloadIdAuthenticationFilter, AuthorizationFilter.class) .sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); + MDCHandler.getInstance().addLoggingFilter(http); + return http.build(); } } @@ -485,6 +491,8 @@ public class SecurityManagedConfiguration { httpSecurityCustomizer.customize(http); } + MDCHandler.getInstance().addLoggingFilter(http); + return http.build(); } @@ -521,7 +529,6 @@ public class SecurityManagedConfiguration { return firewall; } - private static class IgnorePathsStrictHttpFirewall extends StrictHttpFirewall { private final Collection pathsToIgnore; diff --git a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/MDCHandler.java b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/MDCHandler.java new file mode 100644 index 000000000..4bdfc93ef --- /dev/null +++ b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/MDCHandler.java @@ -0,0 +1,160 @@ +/** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.hawkbit.security; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.NoArgsConstructor; +import org.eclipse.hawkbit.im.authentication.TenantAwareAuthenticationDetails; +import org.slf4j.MDC; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.access.intercept.AuthorizationFilter; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.Objects; +import java.util.concurrent.Callable; + +@NoArgsConstructor +public class MDCHandler { + + public static String MDC_KEY_TENANT = "tenant"; + public static String MDC_KEY_USER = "user"; + + private static final MDCHandler SINGLETON = new MDCHandler(); + + @Value("${hawkbit.logging.mdchandler.enable:true}") + private boolean mdcEnabled; + @Autowired + private SpringSecurityAuditorAware springSecurityAuditorAware; + @Autowired + private SystemSecurityContext securityContext; + + /** + * @return The holder singleton instance. + */ + public static MDCHandler getInstance() { + return SINGLETON; + } + + public T withLogging(final Callable callable) throws Exception { + if (!mdcEnabled) { + return callable.call(); + } + + final Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + final String tenant; + if (authentication.getDetails() instanceof TenantAwareAuthenticationDetails tenantAwareAuthenticationDetails) { + tenant = tenantAwareAuthenticationDetails.getTenant(); + } else { + tenant = null; + } + + final String currentTenant = MDC.get(MDC_KEY_TENANT); + if (Objects.equals(currentTenant, tenant)) { + return putUser(callable); + } else { + set(MDC_KEY_TENANT, tenant); + try { + return putUser(callable); + } finally { + set(MDC_KEY_TENANT, currentTenant); + } + } + } + + public void addLoggingFilter(final HttpSecurity httpSecurity) { + if (!mdcEnabled) { + return; + } + + httpSecurity.addFilterBefore(new OncePerRequestFilter() { + @Override + protected void doFilterInternal( + final HttpServletRequest request, final HttpServletResponse response, final FilterChain filterChain) + throws ServletException, IOException { + try { + withLogging(() -> { + filterChain.doFilter(request, response); + return null; + }); + } catch (final RuntimeException re) { + throw re; + } catch (final WrappedException we) { + final Throwable cause = we.getCause(); + if (cause instanceof ServletException se) { + throw se; + } else if (cause instanceof IOException ioe) { + throw ioe; + } else { + throw we.toRuntimeException(); + } + } catch (final Exception e) { + // should never be here - if mdc is handler is enabled non runtime exceptions are + // always wrapped + throw new RuntimeException(e); + } + } + }, AuthorizationFilter.class); + } + + private T putUser(final Callable callable) throws WrappedException { + final String user = springSecurityAuditorAware + .getCurrentAuditor() + .map(username -> (securityContext.isCurrentThreadSystemCode() ? "as " : "") + username) + .orElse(null); + + final String currentUser = MDC.get(MDC_KEY_USER); + try { + if (Objects.equals(currentUser, user)) { + return callable.call(); + } else { + set(MDC_KEY_USER, user); + try { + return callable.call(); + } finally { + set(MDC_KEY_USER, currentUser); + } + } + } catch (final Exception e) { + if (e instanceof RuntimeException) { + throw (RuntimeException) e; + } else { + throw new WrappedException(e); + } + } + } + + private static void set(final String key, final String value) { + if (value == null) { + MDC.remove(key); + } else { + MDC.put(key, value); + } + } + + // Wraps catchable exceptions to rethrow + public static class WrappedException extends Exception { + + public WrappedException(final Throwable cause) { + super(cause); + } + + public RuntimeException toRuntimeException() { + return new RuntimeException(getCause() == null ? this : getCause()); + } + } +} diff --git a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/SystemSecurityContext.java b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/SystemSecurityContext.java index bdabd17a3..ca355332f 100644 --- a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/SystemSecurityContext.java +++ b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/SystemSecurityContext.java @@ -115,15 +115,13 @@ public class SystemSecurityContext { return tenantAware.runAsTenant(tenant, () -> { try { setSystemContext(SecurityContextHolder.getContext()); - return callable.call(); - + return MDCHandler.getInstance().withLogging(callable); } catch (final RuntimeException e) { throw e; } catch (final Exception e) { throw new RuntimeException(e); } }); - } finally { SecurityContextHolder.setContext(oldContext); log.debug("leaving system code execution"); @@ -154,21 +152,18 @@ public class SystemSecurityContext { return tenantAware.runAsTenant(tenant, () -> { try { setCustomSecurityContext(tenant, oldContext.getAuthentication().getPrincipal(), authorities); - return callable.call(); - + return MDCHandler.getInstance().withLogging(callable); } catch (final Exception e) { throw new RuntimeException(e); } }); - } finally { SecurityContextHolder.setContext(oldContext); } } /** - * @return {@code true} if the current running code is running as system - * code block. + * @return {@code true} if the current running code is running as system code block. */ public boolean isCurrentThreadSystemCode() { return SecurityContextHolder.getContext().getAuthentication() instanceof SystemCodeAuthentication; @@ -195,6 +190,13 @@ public class SystemSecurityContext { return false; } + static void setSystemContext(final SecurityContext oldContext) { + final Authentication oldAuthentication = oldContext.getAuthentication(); + final SecurityContextImpl securityContextImpl = new SecurityContextImpl(); + securityContextImpl.setAuthentication(new SystemCodeAuthentication(oldAuthentication)); + SecurityContextHolder.setContext(securityContextImpl); + } + private void setCustomSecurityContext(final String tenantId, final Object principal, final Collection authorities) { final AnonymousAuthenticationToken authenticationToken = new AnonymousAuthenticationToken( @@ -205,13 +207,6 @@ public class SystemSecurityContext { SecurityContextHolder.setContext(securityContextImpl); } - static void setSystemContext(final SecurityContext oldContext) { - final Authentication oldAuthentication = oldContext.getAuthentication(); - final SecurityContextImpl securityContextImpl = new SecurityContextImpl(); - securityContextImpl.setAuthentication(new SystemCodeAuthentication(oldAuthentication)); - SecurityContextHolder.setContext(securityContextImpl); - } - /** * An implementation of the Spring's {@link Authentication} object which is * used within a system security code block and wraps the original @@ -265,4 +260,4 @@ public class SystemSecurityContext { // not needed } } -} +} \ No newline at end of file