From 522720302a26754fd318f2044e98ecc221a7572d Mon Sep 17 00:00:00 2001 From: Fabien Date: Wed, 28 Feb 2024 16:07:59 +0100 Subject: [PATCH] fix: manage partners endpoints authorizations --- .../config/GeneralConfig.java | 4 +-- .../front/config/ResourceServerConfig.java | 32 +++++++++-------- .../config/filter/RateLimitingFilter.java | 1 - .../filter/RateLimitingSupportMailFilter.java | 1 - .../register/form/tenant/AccountForm.java | 1 - .../form/tenant/GuarantorTypeForm.java | 2 -- .../front/register/form/tenant/NamesForm.java | 2 -- .../CustomWebSecurityExpressionHandler.java | 28 --------------- .../CustomWebSecurityExpressionRoot.java | 23 ------------- .../security/PartnerAuthorizationManager.java | 34 +++++++++++++++++++ .../v2/DeniedJoinTenantValidator.java | 4 --- .../oauth2/CustomOAuth2UserService.java | 4 +-- 12 files changed, 56 insertions(+), 80 deletions(-) delete mode 100644 dossierfacile-api-tenant/src/main/java/fr/dossierfacile/api/front/security/CustomWebSecurityExpressionHandler.java delete mode 100644 dossierfacile-api-tenant/src/main/java/fr/dossierfacile/api/front/security/CustomWebSecurityExpressionRoot.java create mode 100644 dossierfacile-api-tenant/src/main/java/fr/dossierfacile/api/front/security/PartnerAuthorizationManager.java diff --git a/dossierfacile-api-owner/src/main/java/fr/dossierfacile/api/dossierfacileapiowner/config/GeneralConfig.java b/dossierfacile-api-owner/src/main/java/fr/dossierfacile/api/dossierfacileapiowner/config/GeneralConfig.java index 0251c23ef..53e081c19 100644 --- a/dossierfacile-api-owner/src/main/java/fr/dossierfacile/api/dossierfacileapiowner/config/GeneralConfig.java +++ b/dossierfacile-api-owner/src/main/java/fr/dossierfacile/api/dossierfacileapiowner/config/GeneralConfig.java @@ -1,5 +1,6 @@ package fr.dossierfacile.api.dossierfacileapiowner.config; +import org.apache.commons.lang3.StringUtils; import org.springframework.boot.actuate.autoconfigure.endpoint.web.CorsEndpointProperties; import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties; import org.springframework.boot.actuate.autoconfigure.web.server.ManagementPortType; @@ -15,7 +16,6 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; -import org.springframework.util.StringUtils; import org.springframework.web.client.RestTemplate; import java.util.ArrayList; @@ -45,6 +45,6 @@ public WebMvcEndpointHandlerMapping webEndpointServletHandlerMapping(WebEndpoint private boolean shouldRegisterLinksMapping(WebEndpointProperties webEndpointProperties, Environment environment, String basePath) { - return webEndpointProperties.getDiscovery().isEnabled() && (StringUtils.hasText(basePath) || ManagementPortType.get(environment).equals(ManagementPortType.DIFFERENT)); + return webEndpointProperties.getDiscovery().isEnabled() && (StringUtils.isNotBlank(basePath) || ManagementPortType.get(environment).equals(ManagementPortType.DIFFERENT)); } } diff --git a/dossierfacile-api-tenant/src/main/java/fr/dossierfacile/api/front/config/ResourceServerConfig.java b/dossierfacile-api-tenant/src/main/java/fr/dossierfacile/api/front/config/ResourceServerConfig.java index 101311a6b..e45e7f088 100644 --- a/dossierfacile-api-tenant/src/main/java/fr/dossierfacile/api/front/config/ResourceServerConfig.java +++ b/dossierfacile-api-tenant/src/main/java/fr/dossierfacile/api/front/config/ResourceServerConfig.java @@ -1,22 +1,21 @@ package fr.dossierfacile.api.front.config; import fr.dossierfacile.api.front.config.filter.ConnectionContextFilter; -import fr.dossierfacile.api.front.security.CustomWebSecurityExpressionHandler; +import fr.dossierfacile.api.front.security.PartnerAuthorizationManager; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.security.access.expression.SecurityExpressionHandler; +import org.springframework.security.authorization.AuthorizationManager; 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.HeadersConfigurer.FrameOptionsConfig; +import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.web.FilterInvocation; import org.springframework.security.web.SecurityFilterChain; -import org.springframework.security.web.access.expression.WebExpressionAuthorizationManager; import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; +import org.springframework.security.web.access.intercept.RequestAuthorizationContext; import org.springframework.security.web.header.writers.StaticHeadersWriter; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; @@ -43,7 +42,7 @@ SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { .cacheControl(withDefaults()) .httpStrictTransportSecurity(transport -> transport.maxAgeInSeconds(63072000).includeSubDomains(true)) .contentSecurityPolicy(csp -> csp.policyDirectives("frame-ancestors 'none'; frame-src 'none'; child-src 'none'; upgrade-insecure-requests; default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; object-src 'none'; img-src 'self' data:; font-src 'self'; connect-src *.dossierfacile.fr *.dossierfacile.fr:*; base-uri 'self'; form-action 'none'; media-src 'none'; worker-src 'none'; manifest-src 'none'; prefetch-src 'none';")) - .frameOptions(FrameOptionsConfig::sameOrigin) + .frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin) ) .formLogin(AbstractHttpConfigurer::disable) .httpBasic(AbstractHttpConfigurer::disable) @@ -59,10 +58,10 @@ SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { "/api/support/email", "/api/stats/**", "/api/onetimesecret/**", - "/actuator/health").permitAll() - .requestMatchers("/api-partner/**").access(new WebExpressionAuthorizationManager("hasAuthority(\"SCOPE_api-partner\") && isClient()")) + "/actuator/health").permitAll() + .requestMatchers("/api-partner/**").access(apiPartnerAuthorizationManager()) + .requestMatchers("/dfc/api/**").access(dfcPartnerServiceAuthorizationManager()) .requestMatchers("/dfc/**").hasAuthority("SCOPE_dfc") - .requestMatchers("/dfc/api/**").access(new WebExpressionAuthorizationManager("isClient()")) .anyRequest().hasAuthority("SCOPE_dossier") ) .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt); @@ -71,18 +70,23 @@ SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { } @Bean - public SecurityExpressionHandler customWebSecurityExpressionHandler() { - return new CustomWebSecurityExpressionHandler(); + AuthorizationManager apiPartnerAuthorizationManager() { + return new PartnerAuthorizationManager("api-partner"); + } + + @Bean + AuthorizationManager dfcPartnerServiceAuthorizationManager() { + return new PartnerAuthorizationManager("dfc"); } @Bean CorsConfigurationSource corsConfigurationSource() { - var configuration = new CorsConfiguration(); + CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedOriginPatterns(Collections.singletonList("*")); configuration.setAllowCredentials(true); configuration.setAllowedHeaders(Arrays.asList("Access-Control-Allow-Headers", "Access-Control-Allow-Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers", "Origin", "Cache-Control", "Content-Type", "Authorization", "Baggage", "Sentry-trace")); configuration.setAllowedMethods(Arrays.asList("DELETE", "GET", "POST", "PATCH", "PUT")); - var source = new UrlBasedCorsConfigurationSource(); + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); return source; } @@ -91,4 +95,4 @@ CorsConfigurationSource corsConfigurationSource() { public BCryptPasswordEncoder bCryptPasswordEncoder() { return new BCryptPasswordEncoder(); } -} \ No newline at end of file +} diff --git a/dossierfacile-api-tenant/src/main/java/fr/dossierfacile/api/front/config/filter/RateLimitingFilter.java b/dossierfacile-api-tenant/src/main/java/fr/dossierfacile/api/front/config/filter/RateLimitingFilter.java index e6d230f6f..f755ecffc 100644 --- a/dossierfacile-api-tenant/src/main/java/fr/dossierfacile/api/front/config/filter/RateLimitingFilter.java +++ b/dossierfacile-api-tenant/src/main/java/fr/dossierfacile/api/front/config/filter/RateLimitingFilter.java @@ -1,6 +1,5 @@ package fr.dossierfacile.api.front.config.filter; -import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; diff --git a/dossierfacile-api-tenant/src/main/java/fr/dossierfacile/api/front/config/filter/RateLimitingSupportMailFilter.java b/dossierfacile-api-tenant/src/main/java/fr/dossierfacile/api/front/config/filter/RateLimitingSupportMailFilter.java index ccb685e45..d351d26db 100644 --- a/dossierfacile-api-tenant/src/main/java/fr/dossierfacile/api/front/config/filter/RateLimitingSupportMailFilter.java +++ b/dossierfacile-api-tenant/src/main/java/fr/dossierfacile/api/front/config/filter/RateLimitingSupportMailFilter.java @@ -1,6 +1,5 @@ package fr.dossierfacile.api.front.config.filter; -import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; diff --git a/dossierfacile-api-tenant/src/main/java/fr/dossierfacile/api/front/register/form/tenant/AccountForm.java b/dossierfacile-api-tenant/src/main/java/fr/dossierfacile/api/front/register/form/tenant/AccountForm.java index e286eabd1..6f9ec98b5 100644 --- a/dossierfacile-api-tenant/src/main/java/fr/dossierfacile/api/front/register/form/tenant/AccountForm.java +++ b/dossierfacile-api-tenant/src/main/java/fr/dossierfacile/api/front/register/form/tenant/AccountForm.java @@ -9,7 +9,6 @@ import javax.validation.constraints.Email; import javax.validation.constraints.NotBlank; -import javax.validation.constraints.NotEmpty; @Data @AllArgsConstructor diff --git a/dossierfacile-api-tenant/src/main/java/fr/dossierfacile/api/front/register/form/tenant/GuarantorTypeForm.java b/dossierfacile-api-tenant/src/main/java/fr/dossierfacile/api/front/register/form/tenant/GuarantorTypeForm.java index 1fee04e9d..8bf729320 100644 --- a/dossierfacile-api-tenant/src/main/java/fr/dossierfacile/api/front/register/form/tenant/GuarantorTypeForm.java +++ b/dossierfacile-api-tenant/src/main/java/fr/dossierfacile/api/front/register/form/tenant/GuarantorTypeForm.java @@ -3,14 +3,12 @@ import fr.dossierfacile.api.front.form.interfaces.FormWithTenantId; import fr.dossierfacile.api.front.validator.anotation.tenant.type_guarantor.MaxGuarantor; import fr.dossierfacile.api.front.validator.group.ApiPartner; -import fr.dossierfacile.api.front.validator.group.Dossier; import fr.dossierfacile.common.enums.TypeGuarantor; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import javax.validation.constraints.NotNull; -import javax.validation.constraints.Null; @Data @AllArgsConstructor diff --git a/dossierfacile-api-tenant/src/main/java/fr/dossierfacile/api/front/register/form/tenant/NamesForm.java b/dossierfacile-api-tenant/src/main/java/fr/dossierfacile/api/front/register/form/tenant/NamesForm.java index be991850e..4c3167f66 100644 --- a/dossierfacile-api-tenant/src/main/java/fr/dossierfacile/api/front/register/form/tenant/NamesForm.java +++ b/dossierfacile-api-tenant/src/main/java/fr/dossierfacile/api/front/register/form/tenant/NamesForm.java @@ -2,7 +2,6 @@ import fr.dossierfacile.api.front.form.interfaces.FormWithTenantId; import fr.dossierfacile.api.front.validator.anotation.tenant.name.CheckFranceConnect; -import fr.dossierfacile.api.front.validator.group.ApiPartner; import fr.dossierfacile.api.front.validator.group.Dossier; import lombok.AllArgsConstructor; import lombok.Data; @@ -10,7 +9,6 @@ import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; -import javax.validation.constraints.Null; @Data @AllArgsConstructor diff --git a/dossierfacile-api-tenant/src/main/java/fr/dossierfacile/api/front/security/CustomWebSecurityExpressionHandler.java b/dossierfacile-api-tenant/src/main/java/fr/dossierfacile/api/front/security/CustomWebSecurityExpressionHandler.java deleted file mode 100644 index fd5ec7031..000000000 --- a/dossierfacile-api-tenant/src/main/java/fr/dossierfacile/api/front/security/CustomWebSecurityExpressionHandler.java +++ /dev/null @@ -1,28 +0,0 @@ -package fr.dossierfacile.api.front.security; - -import org.springframework.context.annotation.Configuration; -import org.springframework.security.access.expression.SecurityExpressionOperations; -import org.springframework.security.authentication.AuthenticationTrustResolver; -import org.springframework.security.authentication.AuthenticationTrustResolverImpl; -import org.springframework.security.core.Authentication; -import org.springframework.security.web.FilterInvocation; -import org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler; -import org.springframework.security.web.access.expression.WebSecurityExpressionRoot; -import org.springframework.stereotype.Component; - -@Configuration -@Component -public class CustomWebSecurityExpressionHandler extends DefaultWebSecurityExpressionHandler { - private final AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl(); - private final String defaultRolePrefix = "ROLE_"; - - @Override - protected SecurityExpressionOperations createSecurityExpressionRoot(Authentication authentication, FilterInvocation fi) { - WebSecurityExpressionRoot root = new CustomWebSecurityExpressionRoot(authentication, fi); - root.setPermissionEvaluator(this.getPermissionEvaluator()); - root.setTrustResolver(this.trustResolver); - root.setRoleHierarchy(this.getRoleHierarchy()); - root.setDefaultRolePrefix(this.defaultRolePrefix); - return root; - } -} \ No newline at end of file diff --git a/dossierfacile-api-tenant/src/main/java/fr/dossierfacile/api/front/security/CustomWebSecurityExpressionRoot.java b/dossierfacile-api-tenant/src/main/java/fr/dossierfacile/api/front/security/CustomWebSecurityExpressionRoot.java deleted file mode 100644 index 90c6f52ab..000000000 --- a/dossierfacile-api-tenant/src/main/java/fr/dossierfacile/api/front/security/CustomWebSecurityExpressionRoot.java +++ /dev/null @@ -1,23 +0,0 @@ -package fr.dossierfacile.api.front.security; - -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.oauth2.jwt.Jwt; -import org.springframework.security.web.FilterInvocation; -import org.springframework.security.web.access.expression.WebSecurityExpressionRoot; - -public class CustomWebSecurityExpressionRoot extends WebSecurityExpressionRoot { - - public CustomWebSecurityExpressionRoot(Authentication a, FilterInvocation fi) { - super(a, fi); - } - - public boolean isClient() { - try { - return ((Jwt) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getClaimAsString("clientId") != null - || ((Jwt) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getClaimAsString("client_id") != null; - } catch (Throwable t) { - return false; - } - } -} \ No newline at end of file diff --git a/dossierfacile-api-tenant/src/main/java/fr/dossierfacile/api/front/security/PartnerAuthorizationManager.java b/dossierfacile-api-tenant/src/main/java/fr/dossierfacile/api/front/security/PartnerAuthorizationManager.java new file mode 100644 index 000000000..a43003db9 --- /dev/null +++ b/dossierfacile-api-tenant/src/main/java/fr/dossierfacile/api/front/security/PartnerAuthorizationManager.java @@ -0,0 +1,34 @@ +package fr.dossierfacile.api.front.security; + +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.web.access.intercept.RequestAuthorizationContext; + +import java.util.function.Supplier; + +public class PartnerAuthorizationManager implements AuthorizationManager { + private final String authScope; + + public PartnerAuthorizationManager(String scope) { + this.authScope = "SCOPE_" + scope; + } + + private boolean hasScope(Authentication authentication) { + return authentication.getAuthorities().stream().anyMatch(a -> authScope.equals(a.getAuthority())); + } + + private boolean isClient(Authentication authentication) { + try { + return ((Jwt) authentication.getPrincipal()).getClaimAsString("client_id") != null; + } catch (Throwable t) { + return false; + } + } + + @Override + public AuthorizationDecision check(Supplier authentication, RequestAuthorizationContext object) { + return new AuthorizationDecision(isClient(authentication.get()) && hasScope(authentication.get())); + } +} \ No newline at end of file diff --git a/dossierfacile-api-tenant/src/main/java/fr/dossierfacile/api/front/validator/tenant/application/v2/DeniedJoinTenantValidator.java b/dossierfacile-api-tenant/src/main/java/fr/dossierfacile/api/front/validator/tenant/application/v2/DeniedJoinTenantValidator.java index c54784e3e..e51c99fe4 100644 --- a/dossierfacile-api-tenant/src/main/java/fr/dossierfacile/api/front/validator/tenant/application/v2/DeniedJoinTenantValidator.java +++ b/dossierfacile-api-tenant/src/main/java/fr/dossierfacile/api/front/validator/tenant/application/v2/DeniedJoinTenantValidator.java @@ -2,16 +2,12 @@ import fr.dossierfacile.api.front.register.form.tenant.ApplicationFormV2; -import fr.dossierfacile.api.front.security.interfaces.AuthenticationFacade; -import fr.dossierfacile.api.front.service.interfaces.TenantService; import fr.dossierfacile.api.front.validator.TenantConstraintValidator; import fr.dossierfacile.api.front.validator.anotation.tenant.application.v2.DeniedJoinTenant; -import fr.dossierfacile.common.entity.Tenant; import fr.dossierfacile.common.enums.TenantType; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; -import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; @Component diff --git a/dossierfacile-bo/src/main/java/fr/gouv/bo/security/oauth2/CustomOAuth2UserService.java b/dossierfacile-bo/src/main/java/fr/gouv/bo/security/oauth2/CustomOAuth2UserService.java index 2e7528508..3d9d4cac1 100644 --- a/dossierfacile-bo/src/main/java/fr/gouv/bo/security/oauth2/CustomOAuth2UserService.java +++ b/dossierfacile-bo/src/main/java/fr/gouv/bo/security/oauth2/CustomOAuth2UserService.java @@ -9,6 +9,7 @@ import fr.gouv.bo.security.UserPrincipal; import fr.gouv.bo.security.oauth2.user.OAuth2UserInfo; import fr.gouv.bo.security.oauth2.user.OAuth2UserInfoFactory; +import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.authentication.InternalAuthenticationServiceException; @@ -19,7 +20,6 @@ import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.stereotype.Service; -import org.springframework.util.StringUtils; import java.util.HashSet; import java.util.List; @@ -55,7 +55,7 @@ public OAuth2User loadUser(OAuth2UserRequest oAuth2UserRequest) { public OAuth2User processOAuth2User(OAuth2UserRequest oAuth2UserRequest, OAuth2User oAuth2User) { OAuth2UserInfo oAuth2UserInfo = OAuth2UserInfoFactory.getOAuth2UserInfo(oAuth2UserRequest.getClientRegistration().getRegistrationId(), oAuth2User.getAttributes()); - if (!StringUtils.hasLength(oAuth2UserInfo.getEmail())) { + if (!StringUtils.isNotBlank(oAuth2UserInfo.getEmail())) { throw new OAuth2AuthenticationProcessingException("Email not found from OAuth2 provider"); } if (!Objects.requireNonNull(StringUtils.split(oAuth2UserInfo.getEmail(), "@"))[1].contains(authorizeDomainBo)) {