/**
* This file is part of alf.io.
*
* alf.io is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* alf.io is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with alf.io. If not, see <http://www.gnu.org/licenses/>.
*/
package alfio.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.csrf.CsrfTokenRepository;
import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository;
import org.springframework.security.web.util.matcher.NegatedRequestMatcher;
import org.springframework.security.web.util.matcher.RequestHeaderRequestMatcher;
import javax.servlet.http.HttpServletResponse;
import javax.sql.DataSource;
import java.util.regex.Pattern;
@Configuration
@EnableWebSecurity
public class WebSecurityConfig {
public static final String ADMIN_API = "/admin/api";
public static final String CSRF_SESSION_ATTRIBUTE = "CSRF_SESSION_ATTRIBUTE";
public static final String CSRF_PARAM_NAME = "_csrf";
public static final String OPERATOR = "OPERATOR";
private static final String SUPERVISOR = "SUPERVISOR";
public static final String SPONSOR = "SPONSOR";
private static final String ADMIN = "ADMIN";
private static final String OWNER = "OWNER";
private static class BaseWebSecurity extends WebSecurityConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.jdbcAuthentication().dataSource(dataSource)
.usersByUsernameQuery("select username, password, enabled from ba_user where username = ?")
.authoritiesByUsernameQuery("select username, role from authority where username = ?")
.passwordEncoder(passwordEncoder);
}
}
/**
* Basic auth configuration for Mobile App.
* The rules are only valid if the header Authorization is present, otherwise it fallback to the
* FormBasedWebSecurity rules.
*/
@Configuration
@Order(1)
public static class BasicAuthWebSecurity extends BaseWebSecurity {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.requestMatcher((request) -> request.getHeader("Authorization") != null).sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().csrf().disable()
.authorizeRequests()
.antMatchers(ADMIN_API + "/check-in/**").hasAnyRole(OPERATOR, SUPERVISOR)
.antMatchers(HttpMethod.GET, ADMIN_API + "/events").hasAnyRole(OPERATOR, SUPERVISOR, SPONSOR)
.antMatchers(ADMIN_API + "/user-type").hasAnyRole(OPERATOR, SUPERVISOR, SPONSOR)
.antMatchers(ADMIN_API + "/**").denyAll()
.antMatchers(HttpMethod.POST, "/api/attendees/sponsor-scan").hasRole(SPONSOR)
.antMatchers("/**").authenticated()
.and().httpBasic();
}
}
/**
* Default form based configuration.
*/
@Configuration
@Order(2)
public static class FormBasedWebSecurity extends BaseWebSecurity {
@Autowired
private Environment environment;
@Bean
public CsrfTokenRepository getCsrfTokenRepository() {
HttpSessionCsrfTokenRepository repository = new HttpSessionCsrfTokenRepository();
repository.setSessionAttributeName(CSRF_SESSION_ATTRIBUTE);
repository.setParameterName(CSRF_PARAM_NAME);
return repository;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
if(environment.acceptsProfiles("!"+Initializer.PROFILE_DEV)) {
http.requiresChannel().anyRequest().requiresSecure();
}
CsrfConfigurer<HttpSecurity> configurer =
http.exceptionHandling()
.accessDeniedPage("/session-expired")
.defaultAuthenticationEntryPointFor((request, response, ex) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED), new RequestHeaderRequestMatcher("X-Requested-With", "XMLHttpRequest"))
.and()
.headers().cacheControl().disable()
.and()
.csrf();
if(environment.acceptsProfiles(Initializer.PROFILE_DEBUG_CSP)) {
Pattern whiteList = Pattern.compile("^(GET|HEAD|TRACE|OPTIONS)$");
configurer.requireCsrfProtectionMatcher(new NegatedRequestMatcher((r) -> whiteList.matcher(r.getMethod()).matches() || r.getRequestURI().equals("/report-csp-violation")));
}
configurer.csrfTokenRepository(getCsrfTokenRepository())
.and()
.authorizeRequests()
.antMatchers(ADMIN_API + "/configuration/**", ADMIN_API + "/users/**").hasAnyRole(ADMIN, OWNER)
.antMatchers(ADMIN_API + "/organizations/new").hasRole(ADMIN)
.antMatchers(ADMIN_API + "/check-in/**").hasAnyRole(ADMIN, OWNER, SUPERVISOR)
.antMatchers(HttpMethod.GET, ADMIN_API + "/**").hasAnyRole(ADMIN, OWNER, SUPERVISOR)
.antMatchers(ADMIN_API + "/**").hasAnyRole(ADMIN, OWNER)
.antMatchers("/admin/**/export/**").hasAnyRole(ADMIN, OWNER)
.antMatchers("/admin/**").hasAnyRole(ADMIN, OWNER, SUPERVISOR)
.antMatchers("/api/attendees/**").denyAll()
.antMatchers("/**").permitAll()
.and()
.formLogin()
.loginPage("/authentication")
.loginProcessingUrl("/authenticate")
.failureUrl("/authentication?failed")
.and().logout().permitAll();
}
}
}