FINERACT-1724: EclipseLink upgrade fixes + Spring Security chain fixes

This commit is contained in:
Arnold Galovics
2023-06-06 11:45:08 +02:00
committed by Arnold Gálovics
parent abb5d4abb5
commit a7d5f834d5
29 changed files with 404 additions and 257 deletions
+1 -1
View File
@@ -55,7 +55,7 @@ buildscript {
dependencies {
classpath 'com.bmuschko:gradle-cargo-plugin:2.9.0'
classpath 'org.eclipse.persistence:eclipselink:4.0.1'
classpath 'org.eclipse.persistence:eclipselink:4.0.0'
classpath 'jakarta.ws.rs:jakarta.ws.rs-api:3.1.0'
classpath 'com.google.cloud.tools:jib-layer-filter-extension-gradle:0.3.0'
classpath 'org.apache.commons:commons-lang3:3.12.0'
@@ -50,7 +50,7 @@ dependencyManagement {
dependency 'org.codehaus.janino:janino:3.1.9'
dependency 'org.eclipse.persistence:org.eclipse.persistence.jpa:4.0.1'
dependency 'org.eclipse.persistence:org.eclipse.persistence.jpa:4.0.0'
dependency 'com.google.guava:guava:31.1-jre'
dependency 'com.google.code.gson:gson:2.10.1'
dependency 'com.google.truth:truth:1.1.3'
@@ -64,6 +64,7 @@ dependencyManagement {
dependency 'com.github.librepdf:openpdf:1.3.30'
dependency ('org.mnode.ical4j:ical4j:3.2.11') {
exclude 'com.sun.mail:javax.mail'
exclude 'org.codehaus.groovy:groovy'
}
dependency 'org.apache.commons:commons-csv:1.10.0'
dependency 'org.quartz-scheduler:quartz:2.3.2'
@@ -233,6 +234,8 @@ dependencyManagement {
entry 'json-path'
entry 'xml-path'
}
dependency 'org.apache.groovy:groovy-xml:4.0.6'
dependency 'org.apache.groovy:groovy-json:4.0.6'
dependency 'org.mapstruct:mapstruct:1.5.4.Final'
dependency 'org.mapstruct:mapstruct-processor:1.5.4.Final'
@@ -1,2 +0,0 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.acme.fineract.event.starter.AcmeEventAutoConfiguration
@@ -0,0 +1 @@
com.acme.fineract.event.starter.AcmeEventAutoConfiguration
@@ -1,2 +0,0 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.acme.fineract.loan.starter.AcmeLoanAutoConfiguration
@@ -0,0 +1 @@
com.acme.fineract.loan.starter.AcmeLoanAutoConfiguration
@@ -1,2 +1 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.acme.fineract.portfolio.note.starter.AcmeNoteAutoConfiguration
@@ -21,6 +21,7 @@ package org.apache.fineract.infrastructure.core.boot;
public final class FineractProfiles {
public static final String LIQUIBASE_ONLY = "liquibase-only";
public static final String DIAGNOSTICS = "diagnostics";
private FineractProfiles() {}
}
@@ -57,6 +57,7 @@ public class FineractProperties {
private FineractQueryProperties query;
private FineractApiProperties api;
private FineractSecurityProperties security;
private FineractNotificationProperties notification;
@@ -352,6 +353,41 @@ public class FineractProperties {
private boolean errorNotFoundFail;
}
@Getter
@Setter
public static class FineractSecurityProperties {
private FineractSecurityBasicAuth basicauth;
private FineractSecurityTwoFactorAuth twoFactor;
private FineractSecurityOAuth oauth;
public void set2fa(FineractSecurityTwoFactorAuth twoFactor) {
this.twoFactor = twoFactor;
}
}
@Getter
@Setter
public static class FineractSecurityBasicAuth {
private boolean enabled;
}
@Getter
@Setter
public static class FineractSecurityTwoFactorAuth {
private boolean enabled;
}
@Getter
@Setter
public static class FineractSecurityOAuth {
private boolean enabled;
}
@Getter
@Setter
public static class FineractTransactionProcessorItemProperties {
@@ -25,15 +25,21 @@ import static org.springframework.security.authorization.AuthorityAuthorizationM
import static org.springframework.security.authorization.AuthorizationManagers.allOf;
import java.util.Collection;
import org.apache.fineract.infrastructure.businessdate.service.BusinessDateReadPlatformService;
import org.apache.fineract.infrastructure.cache.service.CacheWritePlatformService;
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
import org.apache.fineract.infrastructure.core.exceptionmapper.OAuth2ExceptionEntryPoint;
import org.apache.fineract.infrastructure.core.serialization.ToApiJsonSerializer;
import org.apache.fineract.infrastructure.security.data.FineractJwtAuthenticationToken;
import org.apache.fineract.infrastructure.security.data.PlatformRequestLog;
import org.apache.fineract.infrastructure.security.filter.TenantAwareTenantIdentifierFilter;
import org.apache.fineract.infrastructure.security.filter.TwoFactorAuthenticationFilter;
import org.apache.fineract.infrastructure.security.service.BasicAuthTenantDetailsService;
import org.apache.fineract.infrastructure.security.service.TenantAwareJpaPlatformUserDetailsService;
import org.apache.fineract.infrastructure.security.service.TwoFactorService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
@@ -55,25 +61,36 @@ import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.context.SecurityContextHolderFilter;
@Configuration
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty("fineract.security.oauth.enabled")
@EnableMethodSecurity
public class OAuth2SecurityConfig {
@Autowired
private TwoFactorAuthenticationFilter twoFactorAuthenticationFilter;
@Autowired
private TenantAwareTenantIdentifierFilter tenantAwareTenantIdentifierFilter;
@Autowired
private TenantAwareJpaPlatformUserDetailsService userDetailsService;
@Autowired
private ServerProperties serverProperties;
private static final JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
@Autowired
private TwoFactorService twoFactorService;
@Autowired
private BasicAuthTenantDetailsService basicAuthTenantDetailsService;
@Autowired
private ToApiJsonSerializer<PlatformRequestLog> toApiJsonSerializer;
@Autowired
private ConfigurationDomainService configurationDomainService;
@Autowired
private CacheWritePlatformService cacheWritePlatformService;
@Autowired
private BusinessDateReadPlatformService businessDateReadPlatformService;
private static final JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
@Bean
public SecurityFilterChain authorizationFilterChain(HttpSecurity http) throws Exception {
@@ -90,26 +107,40 @@ public class OAuth2SecurityConfig {
.requestMatchers("/api/*/twofactor").fullyAuthenticated() //
.requestMatchers("/api/**").access(allOf(fullyAuthenticated(), hasAuthority("TWOFACTOR_AUTHENTICATED"), selfServiceUserAuthManager())); //
});
if (serverProperties.getSsl().isEnabled()) {
http.requiresChannel(channel -> channel.requestMatchers("/api/**").requiresSecure());
}
return http.build();
}
@Bean
public SecurityFilterChain FilterChain(HttpSecurity http) throws Exception {
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http //
.csrf((csrf) -> csrf.disable()) // NOSONAR only creating a service that is used by non-browser clients
.exceptionHandling((ehc) -> ehc.authenticationEntryPoint(new OAuth2ExceptionEntryPoint()))
.oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt -> jwt.jwtAuthenticationConverter(authenticationConverter()))
.authenticationEntryPoint(new OAuth2ExceptionEntryPoint())) //
.sessionManagement((smc) -> smc.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) //
.addFilterAfter(tenantAwareTenantIdentifierFilter, SecurityContextHolderFilter.class) //
.addFilterAfter(twoFactorAuthenticationFilter, BasicAuthenticationFilter.class); //
.addFilterAfter(tenantAwareTenantIdentifierFilter(), SecurityContextHolderFilter.class) //
.addFilterAfter(twoFactorAuthenticationFilter(), BasicAuthenticationFilter.class); //
if (serverProperties.getSsl().isEnabled()) {
http.requiresChannel(channel -> channel.requestMatchers("/api/**").requiresSecure());
}
return http.build();
}
public TenantAwareTenantIdentifierFilter tenantAwareTenantIdentifierFilter() {
return new TenantAwareTenantIdentifierFilter(basicAuthTenantDetailsService, toApiJsonSerializer, configurationDomainService, cacheWritePlatformService, businessDateReadPlatformService);
}
public TwoFactorAuthenticationFilter twoFactorAuthenticationFilter() {
return new TwoFactorAuthenticationFilter(twoFactorService);
}
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
@@ -127,20 +158,4 @@ public class OAuth2SecurityConfig {
}
};
}
@Bean
public FilterRegistrationBean<TenantAwareTenantIdentifierFilter> tenantAwareTenantIdentifierFilterRegistration() throws Exception {
FilterRegistrationBean<TenantAwareTenantIdentifierFilter> registration = new FilterRegistrationBean<TenantAwareTenantIdentifierFilter>(
tenantAwareTenantIdentifierFilter);
registration.setEnabled(false);
return registration;
}
@Bean
public FilterRegistrationBean<TwoFactorAuthenticationFilter> twoFactorAuthenticationFilterRegistration() {
FilterRegistrationBean<TwoFactorAuthenticationFilter> registration = new FilterRegistrationBean<TwoFactorAuthenticationFilter>(
twoFactorAuthenticationFilter);
registration.setEnabled(false);
return registration;
}
}
@@ -23,17 +23,38 @@ import static org.springframework.security.authorization.AuthenticatedAuthorizat
import static org.springframework.security.authorization.AuthorityAuthorizationManager.hasAuthority;
import static org.springframework.security.authorization.AuthorizationManagers.allOf;
import org.apache.fineract.cob.service.InlineLoanCOBExecutorServiceImpl;
import org.apache.fineract.cob.service.LoanAccountLockService;
import org.apache.fineract.commands.domain.CommandSourceRepository;
import org.apache.fineract.commands.service.CommandSourceService;
import org.apache.fineract.infrastructure.businessdate.service.BusinessDateReadPlatformService;
import org.apache.fineract.infrastructure.cache.service.CacheWritePlatformService;
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
import org.apache.fineract.infrastructure.core.domain.FineractRequestContextHolder;
import org.apache.fineract.infrastructure.core.filters.CorrelationHeaderFilter;
import org.apache.fineract.infrastructure.core.filters.IdempotencyStoreFilter;
import org.apache.fineract.infrastructure.core.filters.RequestResponseFilter;
import org.apache.fineract.infrastructure.core.filters.ResponseCorsFilter;
import org.apache.fineract.infrastructure.core.serialization.ToApiJsonSerializer;
import org.apache.fineract.infrastructure.core.service.MDCWrapper;
import org.apache.fineract.infrastructure.instancemode.filter.FineractInstanceModeApiFilter;
import org.apache.fineract.infrastructure.jobs.filter.LoanCOBApiFilter;
import org.apache.fineract.infrastructure.security.data.PlatformRequestLog;
import org.apache.fineract.infrastructure.security.filter.InsecureTwoFactorAuthenticationFilter;
import org.apache.fineract.infrastructure.security.filter.TenantAwareBasicAuthenticationFilter;
import org.apache.fineract.infrastructure.security.filter.TwoFactorAuthenticationFilter;
import org.apache.fineract.infrastructure.security.service.BasicAuthTenantDetailsService;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.infrastructure.security.service.TenantAwareJpaPlatformUserDetailsService;
import org.apache.fineract.infrastructure.security.service.TwoFactorService;
import org.apache.fineract.notification.service.UserNotificationService;
import org.apache.fineract.portfolio.loanaccount.domain.GLIMAccountInfoRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
import org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanRescheduleRequestRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
@@ -48,25 +69,19 @@ import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.ExceptionTranslationFilter;
import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.context.SecurityContextHolderFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.transaction.PlatformTransactionManager;
@Configuration
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty("fineract.security.basicauth.enabled")
@EnableMethodSecurity
public class SecurityConfig {
@Autowired
private ApplicationContext applicationContext;
@Autowired
private TenantAwareJpaPlatformUserDetailsService userDetailsService;
@Autowired
private TwoFactorAuthenticationFilter twoFactorAuthenticationFilter;
@Autowired
private FineractInstanceModeApiFilter fineractInstanceModeApiFilter;
@Autowired
private LoanCOBApiFilter loanCOBApiFilter;
@Autowired
private FineractProperties fineractProperties;
@@ -74,8 +89,40 @@ public class SecurityConfig {
private ServerProperties serverProperties;
@Autowired
private IdempotencyStoreFilter idempotencyStoreFilter;
private ToApiJsonSerializer<PlatformRequestLog> toApiJsonSerializer;
@Autowired
private ConfigurationDomainService configurationDomainService;
@Autowired
private CacheWritePlatformService cacheWritePlatformService;
@Autowired
private UserNotificationService userNotificationService;
@Autowired
private BasicAuthTenantDetailsService basicAuthTenantDetailsService;
@Autowired
private BusinessDateReadPlatformService businessDateReadPlatformService;
@Autowired
private MDCWrapper mdcWrapper;
@Autowired
private CommandSourceRepository commandSourceRepository;
@Autowired
private CommandSourceService commandSourceService;
@Autowired
private FineractRequestContextHolder fineractRequestContextHolder;
@Autowired
private GLIMAccountInfoRepository glimAccountInfoRepository;
@Autowired
private LoanAccountLockService loanAccountLockService;
@Autowired
private PlatformSecurityContext context;
@Autowired
private InlineLoanCOBExecutorServiceImpl inlineLoanCOBExecutorService;
@Autowired
private LoanRepository loanRepository;
@Autowired
private LoanRescheduleRequestRepository loanRescheduleRequestRepository;
@Autowired
private PlatformTransactionManager transactionManager;
@Bean
public SecurityFilterChain authorizationFilterChain(HttpSecurity http) throws Exception {
@@ -93,20 +140,7 @@ public class SecurityConfig {
.requestMatchers("/api/*/twofactor").fullyAuthenticated() //
.requestMatchers("/api/**").access(allOf(fullyAuthenticated(), hasAuthority("TWOFACTOR_AUTHENTICATED"))); //
}) //
.httpBasic((httpBasic) -> httpBasic.authenticationEntryPoint(basicAuthenticationEntryPoint())); //
return http.build();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http //
.csrf((csrf) -> csrf.disable()) // NOSONAR only creating a service that is used by non-browser clients
.sessionManagement((smc) -> smc.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) //
.addFilterAfter(fineractInstanceModeApiFilter, SecurityContextHolderFilter.class) //
.addFilterAfter(tenantAwareBasicAuthenticationFilter(), FineractInstanceModeApiFilter.class) //
.addFilterAfter(twoFactorAuthenticationFilter, BasicAuthenticationFilter.class) //
.addFilterAfter(loanCOBApiFilter, InsecureTwoFactorAuthenticationFilter.class) //
.addFilterBefore(idempotencyStoreFilter, ExceptionTranslationFilter.class); //
.httpBasic((httpBasic) -> httpBasic.authenticationEntryPoint(basicAuthenticationEntryPoint()));
if (serverProperties.getSsl().isEnabled()) {
http.requiresChannel(channel -> channel.requestMatchers("/api/**").requiresSecure());
@@ -115,8 +149,68 @@ public class SecurityConfig {
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http //
.csrf((csrf) -> csrf.disable()) // NOSONAR only creating a service that is used by non-browser clients
.securityContext((securityContext) -> securityContext.requireExplicitSave(false))
.sessionManagement((smc) -> smc.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) //
.addFilterAfter(requestResponseFilter(), ExceptionTranslationFilter.class)
.addFilterAfter(correlationHeaderFilter(), RequestResponseFilter.class)
.addFilterAfter(responseCorsFilter(), CorrelationHeaderFilter.class) //
.addFilterAfter(fineractInstanceModeApiFilter(), ResponseCorsFilter.class) //
.addFilterAfter(tenantAwareBasicAuthenticationFilter(), FineractInstanceModeApiFilter.class) //
.addFilterAfter(loanCOBApiFilter(), TenantAwareBasicAuthenticationFilter.class) //
.addFilterAfter(idempotencyStoreFilter(), LoanCOBApiFilter.class); //
if (fineractProperties.getSecurity().getTwoFactor().isEnabled()) {
http.addFilterAfter(twoFactorAuthenticationFilter(), FineractInstanceModeApiFilter.class);
} else {
http.addFilterAfter(insecureTwoFactorAuthenticationFilter(), FineractInstanceModeApiFilter.class);
}
return http.build();
}
public RequestResponseFilter requestResponseFilter() {
return new RequestResponseFilter();
}
public LoanCOBApiFilter loanCOBApiFilter() {
return new LoanCOBApiFilter(glimAccountInfoRepository, loanAccountLockService, context, inlineLoanCOBExecutorService, loanRepository, fineractProperties, loanRescheduleRequestRepository, transactionManager);
}
public TwoFactorAuthenticationFilter twoFactorAuthenticationFilter() {
TwoFactorService twoFactorService = applicationContext.getBean(TwoFactorService.class);
return new TwoFactorAuthenticationFilter(twoFactorService);
}
public InsecureTwoFactorAuthenticationFilter insecureTwoFactorAuthenticationFilter() {
return new InsecureTwoFactorAuthenticationFilter();
}
public FineractInstanceModeApiFilter fineractInstanceModeApiFilter() {
return new FineractInstanceModeApiFilter(fineractProperties);
}
public IdempotencyStoreFilter idempotencyStoreFilter() {
return new IdempotencyStoreFilter(commandSourceRepository, commandSourceService, fineractProperties, fineractRequestContextHolder);
}
public CorrelationHeaderFilter correlationHeaderFilter() {
return new CorrelationHeaderFilter(fineractProperties, mdcWrapper);
}
public ResponseCorsFilter responseCorsFilter() {
return new ResponseCorsFilter();
}
public TenantAwareBasicAuthenticationFilter tenantAwareBasicAuthenticationFilter() throws Exception {
return new TenantAwareBasicAuthenticationFilter(authenticationManagerBean(), basicAuthenticationEntryPoint());
TenantAwareBasicAuthenticationFilter filter = new TenantAwareBasicAuthenticationFilter(authenticationManagerBean(), basicAuthenticationEntryPoint(),
toApiJsonSerializer, configurationDomainService, cacheWritePlatformService,
userNotificationService, basicAuthTenantDetailsService,
businessDateReadPlatformService);
filter.setRequestMatcher(AntPathRequestMatcher.antMatcher("/api/**"));
return filter;
}
@Bean
@@ -145,21 +239,4 @@ public class SecurityConfig {
providerManager.setEraseCredentialsAfterAuthentication(false);
return providerManager;
}
@Bean
public FilterRegistrationBean<TenantAwareBasicAuthenticationFilter> tenantAwareBasicAuthenticationFilterRegistration()
throws Exception {
FilterRegistrationBean<TenantAwareBasicAuthenticationFilter> registration = new FilterRegistrationBean<TenantAwareBasicAuthenticationFilter>(
tenantAwareBasicAuthenticationFilter());
registration.setEnabled(false);
return registration;
}
@Bean
public FilterRegistrationBean<TwoFactorAuthenticationFilter> twoFactorAuthenticationFilterRegistration() {
FilterRegistrationBean<TwoFactorAuthenticationFilter> registration = new FilterRegistrationBean<TwoFactorAuthenticationFilter>(
twoFactorAuthenticationFilter);
registration.setEnabled(false);
return registration;
}
}
@@ -1,36 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.core.config;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
import org.springframework.stereotype.Component;
@Component
@ConditionalOnClass(name = "org.apache.coyote.http11.Http11Nio2Protocol")
public class TomcatConnectorCustomizer implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> {
@Override
public void customize(ConfigurableServletWebServerFactory factory) {
((TomcatServletWebServerFactory) factory).setProtocol("org.apache.coyote.http11.Http11Nio2Protocol");
}
}
@@ -20,6 +20,7 @@ package org.apache.fineract.infrastructure.core.diagnostics.jpa;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.persistence.EntityManagerFactory;
import org.apache.fineract.infrastructure.core.boot.FineractProfiles;
import org.eclipse.persistence.internal.jpa.EntityManagerImpl;
import org.eclipse.persistence.sessions.changesets.UnitOfWorkChangeSet;
import org.springframework.beans.BeansException;
@@ -38,7 +39,7 @@ import org.springframework.stereotype.Component;
* <br>
* To enable this, run Fineract with the <b>diagnostics</b> profile.
*/
@Profile("diagnostics")
@Profile(FineractProfiles.DIAGNOSTICS)
@Component
public class DiagnosticsEntityManager implements ApplicationContextAware {
@@ -0,0 +1,81 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.core.diagnostics.security;
import static java.lang.System.lineSeparator;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.boot.FineractProfiles;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.annotation.Profile;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.stereotype.Component;
@Component
@Profile(FineractProfiles.DIAGNOSTICS)
@Slf4j
@RequiredArgsConstructor
public class SecurityFilterChainDiagnostics implements InitializingBean {
private final List<SecurityFilterChain> filterChains;
@Override
public void afterPropertiesSet() throws Exception {
filterChains.forEach(this::printFilterChain);
}
private void printFilterChain(SecurityFilterChain filterChain) {
if (filterChain instanceof DefaultSecurityFilterChain) {
printDefaultFilterChain((DefaultSecurityFilterChain) filterChain);
} else {
printUnknownFilterChain(filterChain);
}
}
private void printDefaultFilterChain(DefaultSecurityFilterChain filterChain) {
log.info(
"""
Filter chain matcher: %s
Filters in order:
%s
""".formatted(filterChain.getRequestMatcher(), getFormattedFilters(filterChain)));
}
private String getFormattedFilters(DefaultSecurityFilterChain filterChain) {
StringBuilder result = new StringBuilder();
filterChain.getFilters().forEach(f -> {
result.append("- ");
result.append(f.getClass().getName());
result.append(lineSeparator());
});
return result.toString();
}
private void printUnknownFilterChain(SecurityFilterChain filterChain) {
log.info(
"""
Filter chain:
%s
""".formatted(filterChain));
}
}
@@ -19,23 +19,21 @@
package org.apache.fineract.infrastructure.core.filters;
import java.io.IOException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.config.FineractProperties;
import org.apache.fineract.infrastructure.core.service.MDCWrapper;
import org.apache.fineract.infrastructure.security.utils.LogParameterEscapeUtil;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
@RequiredArgsConstructor
@Slf4j
@Component
public class CorrelationHeaderFilter extends OncePerRequestFilter {
public static final String CORRELATION_ID_KEY = "correlationId";
@@ -18,14 +18,14 @@
*/
package org.apache.fineract.infrastructure.core.filters;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.ws.rs.core.UriInfo;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.mutable.Mutable;
@@ -39,13 +39,11 @@ import org.apache.fineract.commands.service.SynchronousCommandProcessingService;
import org.apache.fineract.infrastructure.core.config.FineractProperties;
import org.apache.fineract.infrastructure.core.domain.FineractRequestContextHolder;
import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingResponseWrapper;
@RequiredArgsConstructor
@Slf4j
@Component
public class IdempotencyStoreFilter extends OncePerRequestFilter implements BatchFilter {
private final CommandSourceRepository commandSourceRepository;
@@ -19,19 +19,17 @@
package org.apache.fineract.infrastructure.core.filters;
import java.io.IOException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
@RequiredArgsConstructor
@Slf4j
@Component
public class RequestResponseFilter extends OncePerRequestFilter {
@Override
@@ -18,36 +18,31 @@
*/
package org.apache.fineract.infrastructure.core.filters;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerResponseContext;
import jakarta.ws.rs.container.ContainerResponseFilter;
import jakarta.ws.rs.ext.Provider;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
/**
* Filter that returns a response with headers that allows for Cross-Origin Requests (CORs) to be performed against the
* platform API.
*/
@Provider
@Component
@Scope("singleton")
public class ResponseCorsFilter implements ContainerResponseFilter {
public class ResponseCorsFilter extends OncePerRequestFilter {
@Override
public void filter(final ContainerRequestContext request, final ContainerResponseContext response) {
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
response.addHeader("Access-Control-Allow-Origin", "*");
response.addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
response.getHeaders().add("Access-Control-Allow-Origin", "*");
// .header("Access-Control-Expose-Headers",
// "Fineract-Platform-TenantId")
response.getHeaders().add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
final String reqHead = request.getHeader("Access-Control-Request-Headers");
final String reqHead = request.getHeaders().getFirst("Access-Control-Request-Headers");
if (null != reqHead && StringUtils.hasText(reqHead)) {
response.getHeaders().add("Access-Control-Allow-Headers", reqHead);
if (StringUtils.hasText(reqHead)) {
response.addHeader("Access-Control-Allow-Headers", reqHead);
}
filterChain.doFilter(request, response);
}
}
@@ -29,7 +29,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Configuration;
@Configuration
@Configuration(proxyBeanMethods = false)
@ApplicationPath("/api")
public class JerseyConfig extends ResourceConfig {
@@ -21,23 +21,21 @@ package org.apache.fineract.infrastructure.instancemode.filter;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.fineract.infrastructure.instancemode.filter.FineractInstanceModeApiFilter.ExceptionListItem.item;
import java.io.IOException;
import java.util.List;
import java.util.function.Function;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.ws.rs.HttpMethod;
import java.io.IOException;
import java.util.List;
import java.util.function.Function;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.config.FineractProperties;
import org.apache.fineract.infrastructure.core.data.ApiGlobalErrorResponse;
import org.apache.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
@Component
@RequiredArgsConstructor
public class FineractInstanceModeApiFilter extends OncePerRequestFilter {
@@ -22,6 +22,10 @@ import static org.apache.fineract.batch.command.CommandStrategyUtils.isRelativeU
import com.google.common.collect.Lists;
import io.github.resilience4j.core.functions.Either;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.ArrayList;
@@ -29,10 +33,6 @@ import java.util.Collections;
import java.util.List;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
@@ -57,13 +57,11 @@ import org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanResch
import org.apache.fineract.useradministration.exception.UnAuthenticatedUserException;
import org.apache.http.HttpStatus;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.web.filter.OncePerRequestFilter;
@Component
@RequiredArgsConstructor
public class LoanCOBApiFilter extends OncePerRequestFilter implements BatchRequestPreprocessor {
@@ -18,16 +18,15 @@
*/
package org.apache.fineract.infrastructure.security.filter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.apache.fineract.infrastructure.security.data.FineractJwtAuthenticationToken;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
@@ -35,15 +34,12 @@ import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
/**
* A dummy {@link TwoFactorAuthenticationFilter} filter used when 'twofactor' environment profile is not active.
*
* This filter adds 'TWOFACTOR_AUTHENTICATED' authority to every authenticated platform user.
*/
@Service
@ConditionalOnProperty(name = "fineract.security.2fa.enabled", havingValue = "false")
public class InsecureTwoFactorAuthenticationFilter extends TwoFactorAuthenticationFilter {
public InsecureTwoFactorAuthenticationFilter() {
@@ -18,13 +18,15 @@
*/
package org.apache.fineract.infrastructure.security.filter;
import java.io.IOException;
import java.time.LocalDate;
import java.util.HashMap;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.time.LocalDate;
import java.util.HashMap;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.time.StopWatch;
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
import org.apache.fineract.infrastructure.businessdate.service.BusinessDateReadPlatformService;
@@ -39,16 +41,14 @@ import org.apache.fineract.infrastructure.security.exception.InvalidTenantIdenti
import org.apache.fineract.infrastructure.security.service.BasicAuthTenantDetailsService;
import org.apache.fineract.notification.service.UserNotificationService;
import org.apache.fineract.useradministration.domain.AppUser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.util.matcher.AnyRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
/**
* A customised version of spring security's {@link BasicAuthenticationFilter}.
@@ -63,36 +63,32 @@ import org.springframework.security.web.authentication.www.BasicAuthenticationFi
* If multi-tenant and basic auth credentials are invalid, a http error response is returned.
*/
@ConditionalOnProperty("fineract.security.basicauth.enabled")
@Slf4j
public class TenantAwareBasicAuthenticationFilter extends BasicAuthenticationFilter {
private static boolean firstRequestProcessed = false;
private static final Logger LOG = LoggerFactory.getLogger(TenantAwareBasicAuthenticationFilter.class);
@Autowired
private ToApiJsonSerializer<PlatformRequestLog> toApiJsonSerializer;
@Autowired
private ConfigurationDomainService configurationDomainService;
@Autowired
private CacheWritePlatformService cacheWritePlatformService;
@Autowired
private UserNotificationService userNotificationService;
@Autowired
private BasicAuthTenantDetailsService basicAuthTenantDetailsService;
@Autowired
private BusinessDateReadPlatformService businessDateReadPlatformService;
private final ToApiJsonSerializer<PlatformRequestLog> toApiJsonSerializer;
private final ConfigurationDomainService configurationDomainService;
private final CacheWritePlatformService cacheWritePlatformService;
private final UserNotificationService userNotificationService;
private final BasicAuthTenantDetailsService basicAuthTenantDetailsService;
private final BusinessDateReadPlatformService businessDateReadPlatformService;
private final String tenantRequestHeader = "Fineract-Platform-TenantId";
private final boolean exceptionIfHeaderMissing = true;
@Setter
private RequestMatcher requestMatcher = AnyRequestMatcher.INSTANCE;
public TenantAwareBasicAuthenticationFilter(final AuthenticationManager authenticationManager,
final AuthenticationEntryPoint authenticationEntryPoint) {
final AuthenticationEntryPoint authenticationEntryPoint, ToApiJsonSerializer<PlatformRequestLog> toApiJsonSerializer, ConfigurationDomainService configurationDomainService, CacheWritePlatformService cacheWritePlatformService, UserNotificationService userNotificationService, BasicAuthTenantDetailsService basicAuthTenantDetailsService, BusinessDateReadPlatformService businessDateReadPlatformService) {
super(authenticationManager, authenticationEntryPoint);
this.toApiJsonSerializer = toApiJsonSerializer;
this.configurationDomainService = configurationDomainService;
this.cacheWritePlatformService = cacheWritePlatformService;
this.userNotificationService = userNotificationService;
this.basicAuthTenantDetailsService = basicAuthTenantDetailsService;
this.businessDateReadPlatformService = businessDateReadPlatformService;
}
@Override
@@ -108,48 +104,49 @@ public class TenantAwareBasicAuthenticationFilter extends BasicAuthenticationFil
// ignore to allow 'preflight' requests from AJAX applications
// in different origin (domain name)
} else {
if (requestMatcher.matches(request)) {
String tenantIdentifier = request.getHeader(this.tenantRequestHeader);
String tenantIdentifier = request.getHeader(this.tenantRequestHeader);
if (org.apache.commons.lang3.StringUtils.isBlank(tenantIdentifier)) {
tenantIdentifier = request.getParameter("tenantIdentifier");
}
if (tenantIdentifier == null && this.exceptionIfHeaderMissing) {
throw new InvalidTenantIdentifierException("No tenant identifier found: Add request header of '"
+ this.tenantRequestHeader + "' or add the parameter 'tenantIdentifier' to query string of request URL.");
}
String pathInfo = request.getRequestURI();
boolean isReportRequest = false;
if (pathInfo != null && pathInfo.contains("report")) {
isReportRequest = true;
}
final FineractPlatformTenant tenant = this.basicAuthTenantDetailsService.loadTenantById(tenantIdentifier, isReportRequest);
ThreadLocalContextUtil.setTenant(tenant);
HashMap<BusinessDateType, LocalDate> businessDates = this.businessDateReadPlatformService.getBusinessDates();
ThreadLocalContextUtil.setBusinessDates(businessDates);
String authToken = request.getHeader("Authorization");
if (authToken != null && authToken.startsWith("Basic ")) {
ThreadLocalContextUtil.setAuthToken(authToken.replaceFirst("Basic ", ""));
}
if (!firstRequestProcessed) {
final String baseUrl = request.getRequestURL().toString().replace(request.getPathInfo(), "/");
System.setProperty("baseUrl", baseUrl);
final boolean ehcacheEnabled = this.configurationDomainService.isEhcacheEnabled();
if (ehcacheEnabled) {
this.cacheWritePlatformService.switchToCache(CacheType.SINGLE_NODE);
} else {
this.cacheWritePlatformService.switchToCache(CacheType.NO_CACHE);
if (org.apache.commons.lang3.StringUtils.isBlank(tenantIdentifier)) {
tenantIdentifier = request.getParameter("tenantIdentifier");
}
TenantAwareBasicAuthenticationFilter.firstRequestProcessed = true;
}
}
super.doFilterInternal(request, response, filterChain);
if (tenantIdentifier == null && this.exceptionIfHeaderMissing) {
throw new InvalidTenantIdentifierException("No tenant identifier found: Add request header of '"
+ this.tenantRequestHeader + "' or add the parameter 'tenantIdentifier' to query string of request URL.");
}
String pathInfo = request.getRequestURI();
boolean isReportRequest = false;
if (pathInfo != null && pathInfo.contains("report")) {
isReportRequest = true;
}
final FineractPlatformTenant tenant = this.basicAuthTenantDetailsService.loadTenantById(tenantIdentifier, isReportRequest);
ThreadLocalContextUtil.setTenant(tenant);
HashMap<BusinessDateType, LocalDate> businessDates = this.businessDateReadPlatformService.getBusinessDates();
ThreadLocalContextUtil.setBusinessDates(businessDates);
String authToken = request.getHeader("Authorization");
if (authToken != null && authToken.startsWith("Basic ")) {
ThreadLocalContextUtil.setAuthToken(authToken.replaceFirst("Basic ", ""));
}
if (!firstRequestProcessed) {
final String baseUrl = request.getRequestURL().toString().replace(request.getPathInfo(), "/");
System.setProperty("baseUrl", baseUrl);
final boolean ehcacheEnabled = this.configurationDomainService.isEhcacheEnabled();
if (ehcacheEnabled) {
this.cacheWritePlatformService.switchToCache(CacheType.SINGLE_NODE);
} else {
this.cacheWritePlatformService.switchToCache(CacheType.NO_CACHE);
}
TenantAwareBasicAuthenticationFilter.firstRequestProcessed = true;
}
}
super.doFilterInternal(request, response, filterChain);
}
} catch (final InvalidTenantIdentifierException e) {
// deal with exception at low level
SecurityContextHolder.getContext().setAuthentication(null);
@@ -160,7 +157,7 @@ public class TenantAwareBasicAuthenticationFilter extends BasicAuthenticationFil
ThreadLocalContextUtil.reset();
task.stop();
final PlatformRequestLog log = PlatformRequestLog.from(task, request);
LOG.debug("{}", this.toApiJsonSerializer.serialize(log));
TenantAwareBasicAuthenticationFilter.log.debug("{}", this.toApiJsonSerializer.serialize(log));
}
}
@@ -18,23 +18,22 @@
*/
package org.apache.fineract.infrastructure.security.filter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.security.constants.TwoFactorConstants;
import org.apache.fineract.infrastructure.security.data.FineractJwtAuthenticationToken;
import org.apache.fineract.infrastructure.security.domain.TFAccessToken;
import org.apache.fineract.infrastructure.security.service.TwoFactorService;
import org.apache.fineract.useradministration.domain.AppUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
@@ -42,7 +41,6 @@ import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
import org.springframework.web.filter.GenericFilterBean;
/**
@@ -56,17 +54,11 @@ import org.springframework.web.filter.GenericFilterBean;
* An authenticated platform user with permission 'BYPASS_TWOFACTOR' will always be granted 'TWOFACTOR_AUTHENTICATED'
* authority regardless of the value of the 'Fineract-Platform-TFA-Token' header.
*/
@Service
@ConditionalOnProperty("fineract.security.2fa.enabled")
@RequiredArgsConstructor
public class TwoFactorAuthenticationFilter extends GenericFilterBean {
private final TwoFactorService twoFactorService;
@Autowired
public TwoFactorAuthenticationFilter(TwoFactorService twoFactorService) {
this.twoFactorService = twoFactorService;
}
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
@@ -21,18 +21,18 @@
-->
<entity-mappings xmlns="http://java.sun.com/xml/ns/persistence/orm"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm http://java.sun.com/xml/ns/persistence/orm_2_0.xsd"
version="2.0">
<entity-mappings xmlns="https://jakarta.ee/xml/ns/persistence/orm"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence/orm
https://jakarta.ee/xml/ns/persistence/orm/orm_3_0.xsd" version="3.0">
<persistence-unit-metadata>
<persistence-unit-defaults>
<delimited-identifiers/>
<!-- <entity-listeners>-->
<!-- <entity-listener-->
<!-- class="org.springframework.data.jpa.domain.support.AuditingEntityListener" />-->
<!-- </entity-listeners>-->
<entity-listeners>
<entity-listener
class="org.springframework.data.jpa.domain.support.AuditingEntityListener"/>
</entity-listeners>
</persistence-unit-defaults>
</persistence-unit-metadata>
+2
View File
@@ -42,6 +42,8 @@ dependencies {
exclude group: 'org.apache.sling'
exclude group: 'com.sun.xml.bind'
}
testImplementation 'org.apache.groovy:groovy-xml'
testImplementation 'org.apache.groovy:groovy-json'
testImplementation 'org.awaitility:awaitility'
testCompileOnly 'org.projectlombok:lombok'