From a289cce805a0a7fd7bcdf18842da78bb9525aec0 Mon Sep 17 00:00:00 2001 From: Jan Date: Thu, 30 Oct 2025 15:05:12 +0100 Subject: [PATCH] Enhanced CORS configuration for OAuth2 and role-based tab visibility in frontend: - **Backend**: Added separate CORS settings for `/oauth2/token` endpoint with enhanced origin handling based on new `lcc.allowed_oauth_token_cors` property. - **Frontend**: Updated `Config.vue` to conditionally display `nodesTab` and `bulkOperationsTab` based on user roles. --- src/frontend/src/pages/Config.vue | 8 +- .../de/avatic/lcc/config/SecurityConfig.java | 114 +++++++++++------- src/main/resources/application.properties | 3 +- 3 files changed, 77 insertions(+), 48 deletions(-) diff --git a/src/frontend/src/pages/Config.vue b/src/frontend/src/pages/Config.vue index 8989796..9b0c42e 100644 --- a/src/frontend/src/pages/Config.vue +++ b/src/frontend/src/pages/Config.vue @@ -90,12 +90,16 @@ export default { tabs.push(this.materialsTab); } - tabs.push(this.nodesTab); + if (this.activeUserStore.isSuper || this.activeUserStore.isMaterial || this.activeUserStore.isPackaging || this.activeUserStore.isFreight) { + tabs.push(this.nodesTab); + } if (this.activeUserStore.allowRates) tabs.push(this.ratesTab); - tabs.push(this.bulkOperationsTab); + if (this.activeUserStore.isSuper || this.activeUserStore.isMaterial || this.activeUserStore.isPackaging || this.activeUserStore.isFreight) { + tabs.push(this.bulkOperationsTab); + } return tabs; diff --git a/src/main/java/de/avatic/lcc/config/SecurityConfig.java b/src/main/java/de/avatic/lcc/config/SecurityConfig.java index bf1bb7f..f53200a 100644 --- a/src/main/java/de/avatic/lcc/config/SecurityConfig.java +++ b/src/main/java/de/avatic/lcc/config/SecurityConfig.java @@ -57,9 +57,12 @@ public class SecurityConfig { @Value("${lcc.allowed_cors}") private String allowedCors; + @Value("${lcc.allowed_oauth_token_cors:*}") // Default: alle Origins + private String oauthTokenCors; + @Bean @Profile("!dev & !test") // Only active when NOT in dev profile - public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtTokenService jwtTokenService) throws Exception { + public SecurityFilterChain prodSecurityFilterChain(HttpSecurity http, JwtTokenService jwtTokenService) throws Exception { http .cors(cors -> cors.configurationSource(prodCorsConfigurationSource())) // Production CORS .authorizeHttpRequests(auth -> auth @@ -98,39 +101,42 @@ public class SecurityConfig { return http.build(); } - @Bean - @Profile("dev | test") - public CorsConfigurationSource devCorsConfigurationSource() { - CorsConfiguration configuration = new CorsConfiguration(); - configuration.setAllowedOriginPatterns(List.of("http://localhost:*")); - configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); - configuration.setAllowedHeaders(List.of("*")); - configuration.setAllowCredentials(true); - configuration.setMaxAge(3600L); - - configuration.setExposedHeaders(Arrays.asList("X-Total-Count", "X-Page-Count", "X-Current-Page")); - - - UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); - source.registerCorsConfiguration("/**", configuration); - return source; - } - // Production CORS Configuration @Bean @Profile("!dev & !test") public CorsConfigurationSource prodCorsConfigurationSource() { + + + // CORS for /oauth2/token + CorsConfiguration tokenConfiguration = new CorsConfiguration(); + if ("*".equals(oauthTokenCors)) { + tokenConfiguration.setAllowedOriginPatterns(List.of("*")); + } else { + String[] tokenOrigins = oauthTokenCors.split(","); + for (int i = 0; i < tokenOrigins.length; i++) { + tokenOrigins[i] = tokenOrigins[i].trim(); + } + if (tokenOrigins.length != 0) { + tokenConfiguration.setAllowedOrigins(Arrays.asList(tokenOrigins)); + } + } + CorsConfiguration configuration = new CorsConfiguration(); - // Parse comma-separated origins from property - String[] origins = allowedCors.split(","); - for (int i = 0; i < origins.length; i++) { - origins[i] = origins[i].trim(); + if ("*".equals(allowedCors)) { + configuration.setAllowedOriginPatterns(List.of("*")); + } else { + // Parse comma-separated origins from property + String[] origins = allowedCors.split(","); + for (int i = 0; i < origins.length; i++) { + origins[i] = origins[i].trim(); + } + + if (origins.length != 0) { + configuration.setAllowedOrigins(Arrays.asList(origins)); + } } - if (origins.length != 0) { - configuration.setAllowedOrigins(Arrays.asList(origins)); - } configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); configuration.setAllowedHeaders(List.of("*")); configuration.setAllowCredentials(true); @@ -138,30 +144,12 @@ public class SecurityConfig { configuration.setExposedHeaders(Arrays.asList("X-Total-Count", "X-Page-Count", "X-Current-Page")); - UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); + source.registerCorsConfiguration("/oauth2/token", tokenConfiguration); return source; } - - @Bean - public JwtAuthenticationConverter jwtAuthenticationConverter() { - // Für Entra ID Tokens - JwtGrantedAuthoritiesConverter converter = new JwtGrantedAuthoritiesConverter(); - converter.setAuthoritiesClaimName("scp"); - converter.setAuthorityPrefix("SCOPE_"); - - JwtAuthenticationConverter jwtConverter = new JwtAuthenticationConverter(); - jwtConverter.setJwtGrantedAuthoritiesConverter(converter); - return jwtConverter; - } - - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } - @Bean @Profile("dev | test") public SecurityFilterChain devSecurityFilterChain(HttpSecurity http, UserRepository userRepository, JwtTokenService jwtTokenService) throws Exception { @@ -188,6 +176,42 @@ public class SecurityConfig { .build(); } + @Bean + @Profile("dev | test") + public CorsConfigurationSource devCorsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOriginPatterns(List.of("http://localhost:*")); + configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); + configuration.setAllowedHeaders(List.of("*")); + configuration.setAllowCredentials(true); + configuration.setMaxAge(3600L); + + configuration.setExposedHeaders(Arrays.asList("X-Total-Count", "X-Page-Count", "X-Current-Page")); + + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } + + @Bean + public JwtAuthenticationConverter jwtAuthenticationConverter() { + // Für Entra ID Tokens + JwtGrantedAuthoritiesConverter converter = new JwtGrantedAuthoritiesConverter(); + converter.setAuthoritiesClaimName("scp"); + converter.setAuthorityPrefix("SCOPE_"); + + JwtAuthenticationConverter jwtConverter = new JwtAuthenticationConverter(); + jwtConverter.setJwtGrantedAuthoritiesConverter(converter); + return jwtConverter; + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean @Profile("!dev & !test") public OAuth2UserService oidcUserService(UserRepository userRepository, GroupRepository groupRepository) { diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 7399574..7322fef 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -22,4 +22,5 @@ spring.flyway.locations=classpath:db/migration spring.flyway.baseline-on-migrate=true spring.sql.init.mode=never -lcc.allowed_cors= \ No newline at end of file +lcc.allowed_cors= +lcc.allowed_oauth_token_cors=* \ No newline at end of file