package com.cloudhandson.vpdbackoffice.config; import java.util.ArrayList; import java.util.List; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.factory.PasswordEncoderFactories; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.web.SecurityFilterChain; @Configuration public class SecurityConfig { @Bean SecurityFilterChain securityFilterChain( HttpSecurity http, BackofficeProperties properties, UserDetailsService userDetailsService ) throws Exception { if (properties.security().requireHttps()) { http.requiresChannel(channel -> channel.anyRequest().requiresSecure()); } var security = properties.security(); if (security.rememberMeConfigured()) { http.rememberMe(rememberMe -> rememberMe .key(security.rememberMeKey()) .userDetailsService(userDetailsService) .rememberMeParameter("remember-me") .rememberMeCookieName("VPD_REMEMBER_ME") .tokenValiditySeconds(security.rememberMeValiditySeconds()) .useSecureCookie(true) .alwaysRemember(false)); } return http .csrf(csrf -> csrf.ignoringRequestMatchers( "/mcp", "/mcp/messages", "/mcp/*/messages")) .headers(headers -> headers.httpStrictTransportSecurity(hsts -> hsts .includeSubDomains(true) .maxAgeInSeconds(31_536_000))) .authorizeHttpRequests(auth -> auth .requestMatchers( "/css/**", "/js/**", "/webjars/**", "/mcp", "/mcp/sse", "/mcp/*/sse", "/mcp/messages", "/mcp/*/messages") .permitAll() .requestMatchers(HttpMethod.POST, "/login", "/logout").permitAll() .requestMatchers(HttpMethod.POST, "/probe", "/vector-knowledge/search", "/security-sql-scripts/explanation", "/mcp-chatbot", "/mcp-client-demo", "/mcp-reasoning") .authenticated() .requestMatchers(HttpMethod.PUT, "/**").hasRole("ADMIN") .requestMatchers(HttpMethod.PATCH, "/**").hasRole("ADMIN") .requestMatchers(HttpMethod.POST, "/**").hasRole("ADMIN") .requestMatchers(HttpMethod.DELETE, "/**").hasRole("ADMIN") .anyRequest().authenticated()) .httpBasic(basic -> { }) .formLogin(login -> login .loginPage("/login") .permitAll()) .logout(logout -> logout.logoutSuccessUrl("/login?logout")) .build(); } @Bean UserDetailsService userDetailsService( BackofficeProperties properties, PasswordEncoder passwordEncoder ) { var security = properties.security(); String encodedPassword = security.adminPasswordHash() == null || security.adminPasswordHash().isBlank() ? passwordEncoder.encode(security.adminPassword()) : security.adminPasswordHash(); List users = new ArrayList<>(); users.add(User.withUsername(security.adminUser()) // BCrypt encoding includes a random salt. Re-encoding the configured // password on every startup would invalidate remember-me signatures. .password(encodedPassword) .roles("ADMIN") .build()); if (security.guestConfigured()) { String encodedGuestPassword = security.guestPasswordHash() == null || security.guestPasswordHash().isBlank() ? passwordEncoder.encode(security.guestPassword()) : security.guestPasswordHash(); users.add(User.withUsername(security.guestUser()) .password(encodedGuestPassword) .roles("VIEWER") .build()); } return new InMemoryUserDetailsManager(users); } @Bean PasswordEncoder passwordEncoder() { return PasswordEncoderFactories.createDelegatingPasswordEncoder(); } }