package org.molgenis.security;
import com.google.api.client.googleapis.auth.oauth2.GooglePublicKeysManager;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.jackson2.JacksonFactory;
import org.molgenis.auth.GroupMemberFactory;
import org.molgenis.auth.TokenFactory;
import org.molgenis.auth.UserFactory;
import org.molgenis.data.DataService;
import org.molgenis.data.settings.AppSettings;
import org.molgenis.security.account.AccountController;
import org.molgenis.security.core.MolgenisPasswordEncoder;
import org.molgenis.security.core.MolgenisPermissionService;
import org.molgenis.security.core.token.TokenService;
import org.molgenis.security.core.utils.SecurityUtils;
import org.molgenis.security.google.GoogleAuthenticationProcessingFilter;
import org.molgenis.security.permission.MolgenisPermissionServiceImpl;
import org.molgenis.security.session.ApiSessionExpirationFilter;
import org.molgenis.security.token.DataServiceTokenService;
import org.molgenis.security.token.TokenAuthenticationFilter;
import org.molgenis.security.token.TokenAuthenticationProvider;
import org.molgenis.security.token.TokenGenerator;
import org.molgenis.security.user.MolgenisUserDetailsChecker;
import org.molgenis.security.user.UserDetailsService;
import org.molgenis.security.user.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.access.hierarchicalroles.RoleHierarchyAuthoritiesMapper;
import org.springframework.security.access.intercept.RunAsImplAuthenticationProvider;
import org.springframework.security.access.vote.RoleHierarchyVoter;
import org.springframework.security.access.vote.RoleVoter;
import org.springframework.security.authentication.AnonymousAuthenticationProvider;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.core.userdetails.UserDetailsChecker;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
import org.springframework.security.web.authentication.switchuser.SwitchUserFilter;
import org.springframework.security.web.header.writers.CacheControlHeadersWriter;
import org.springframework.security.web.header.writers.DelegatingRequestMatcherHeaderWriter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.NegatedRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import javax.servlet.Filter;
import java.util.List;
import static org.molgenis.framework.ui.ResourcePathPatterns.*;
import static org.molgenis.security.google.GoogleAuthenticationProcessingFilter.GOOGLE_AUTHENTICATION_URL;
public abstract class MolgenisWebAppSecurityConfig extends WebSecurityConfigurerAdapter
{
private static final String ANONYMOUS_AUTHENTICATION_KEY = "anonymousAuthenticationKey";
@Autowired
private DataService dataService;
@Autowired
private UserService userService;
@Autowired
private AppSettings appSettings;
@Autowired
private TokenFactory tokenFactory;
@Autowired
private UserFactory userFactory;
@Autowired
private GroupMemberFactory groupMemberFactory;
@Override
protected void configure(HttpSecurity http) throws Exception
{
// do not write cache control headers for static resources
RequestMatcher matcher = new NegatedRequestMatcher(
new OrRequestMatcher(new AntPathRequestMatcher(PATTERN_CSS), new AntPathRequestMatcher(PATTERN_JS),
new AntPathRequestMatcher(PATTERN_IMG), new AntPathRequestMatcher(PATTERN_FONTS)));
DelegatingRequestMatcherHeaderWriter cacheControlHeaderWriter = new DelegatingRequestMatcherHeaderWriter(
matcher, new CacheControlHeadersWriter());
// add default header options but use custom cache control header writer
http.headers().contentTypeOptions().and().xssProtection().and().httpStrictTransportSecurity().and()
.frameOptions().and().addHeaderWriter(cacheControlHeaderWriter);
http.addFilterBefore(anonymousAuthFilter(), AnonymousAuthenticationFilter.class);
http.authenticationProvider(anonymousAuthenticationProvider());
http.addFilterBefore(apiSessionExpirationFilter(), MolgenisAnonymousAuthenticationFilter.class);
http.authenticationProvider(tokenAuthenticationProvider());
http.authenticationProvider(runAsAuthenticationProvider());
http.addFilterBefore(tokenAuthenticationFilter(), ApiSessionExpirationFilter.class);
http.addFilterBefore(googleAuthenticationProcessingFilter(), TokenAuthenticationFilter.class);
http.addFilterAfter(changePasswordFilter(), SwitchUserFilter.class);
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry expressionInterceptUrlRegistry = http
.authorizeRequests();
configureUrlAuthorization(expressionInterceptUrlRegistry);
expressionInterceptUrlRegistry
.antMatchers("/login").permitAll()
.antMatchers(GOOGLE_AUTHENTICATION_URL).permitAll()
.antMatchers("/logo/**").permitAll()
.antMatchers("/molgenis.py").permitAll()
.antMatchers("/molgenis.R").permitAll()
.antMatchers(AccountController.CHANGE_PASSWORD_URI).authenticated()
.antMatchers("/account/**").permitAll()
.antMatchers(PATTERN_CSS).permitAll()
.antMatchers(PATTERN_IMG).permitAll()
.antMatchers(PATTERN_JS).permitAll()
.antMatchers(PATTERN_FONTS).permitAll()
.antMatchers("/html/**").permitAll()
.antMatchers("/plugin/void/**").permitAll()
.antMatchers("/api/**").permitAll()
.antMatchers("/search").permitAll()
.antMatchers("/captcha").permitAll()
.antMatchers("/dataindexerstatus").authenticated()
.antMatchers("/permission/**/read/**").permitAll()
.antMatchers("/permission/**/write/**").permitAll()
.antMatchers("/scripts/**/run").authenticated()
.antMatchers("/files/**").permitAll()
.anyRequest().denyAll().and()
.httpBasic().authenticationEntryPoint(authenticationEntryPoint()).and()
.formLogin().loginPage("/login").failureUrl("/login?error").and()
.logout().deleteCookies("JSESSIONID").addLogoutHandler((req, res, auth) ->
{
if (req.getSession(false) != null
&& req.getSession().getAttribute("continueWithUnsupportedBrowser") != null)
{
req.setAttribute("continueWithUnsupportedBrowser", true);
}
}).logoutSuccessHandler((req, res, auth) ->
{
StringBuilder logoutSuccessUrl = new StringBuilder("/");
if (req.getAttribute("continueWithUnsupportedBrowser") != null)
{
logoutSuccessUrl.append("?continueWithUnsupportedBrowser=true");
}
SimpleUrlLogoutSuccessHandler logoutSuccessHandler = new SimpleUrlLogoutSuccessHandler();
logoutSuccessHandler.setDefaultTargetUrl(logoutSuccessUrl.toString());
logoutSuccessHandler.onLogoutSuccess(req, res, auth);
})
.and()
.csrf().disable();
}
@Bean
public AuthenticationProvider runAsAuthenticationProvider()
{
RunAsImplAuthenticationProvider provider = new RunAsImplAuthenticationProvider();
provider.setKey("Job Execution");
return provider;
}
protected abstract void configureUrlAuthorization(
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry expressionInterceptUrlRegistry);
protected abstract RoleHierarchy roleHierarchy();
@Bean
public MolgenisAnonymousAuthenticationFilter anonymousAuthFilter()
{
return new MolgenisAnonymousAuthenticationFilter(ANONYMOUS_AUTHENTICATION_KEY, SecurityUtils.ANONYMOUS_USERNAME,
userDetailsService());
}
protected abstract List<GrantedAuthority> createAnonymousUserAuthorities();
@Bean
public AnonymousAuthenticationProvider anonymousAuthenticationProvider()
{
return new AnonymousAuthenticationProvider(ANONYMOUS_AUTHENTICATION_KEY);
}
@Bean
public TokenService tokenService()
{
return new DataServiceTokenService(new TokenGenerator(), dataService, userDetailsService(), tokenFactory);
}
@Bean
public AuthenticationProvider tokenAuthenticationProvider()
{
return new TokenAuthenticationProvider(tokenService());
}
@Bean
public Filter tokenAuthenticationFilter()
{
return new TokenAuthenticationFilter(tokenAuthenticationProvider());
}
@Bean
public GooglePublicKeysManager googlePublicKeysManager()
{
HttpTransport transport = new NetHttpTransport();
JsonFactory jsonFactory = new JacksonFactory();
return new GooglePublicKeysManager(transport, jsonFactory);
}
@Bean
public Filter googleAuthenticationProcessingFilter() throws Exception
{
GoogleAuthenticationProcessingFilter googleAuthenticationProcessingFilter = new GoogleAuthenticationProcessingFilter(
googlePublicKeysManager(), dataService, (UserDetailsService) userDetailsService(), appSettings,
userFactory, groupMemberFactory);
googleAuthenticationProcessingFilter.setAuthenticationManager(authenticationManagerBean());
return googleAuthenticationProcessingFilter;
}
@Bean
public Filter changePasswordFilter()
{
return new MolgenisChangePasswordFilter(userService, redirectStrategy());
}
@Bean
public RedirectStrategy redirectStrategy()
{
return new DefaultRedirectStrategy();
}
@Bean
public RoleHierarchy roleHierarchyBean()
{
return roleHierarchy();
}
@Bean
public RoleVoter roleVoter()
{
return new RoleHierarchyVoter(roleHierarchy());
}
@Bean
public GrantedAuthoritiesMapper roleHierarchyAuthoritiesMapper()
{
return new RoleHierarchyAuthoritiesMapper(roleHierarchy());
}
@Bean
public PasswordEncoder passwordEncoder()
{
return new MolgenisPasswordEncoder(new BCryptPasswordEncoder());
}
@Override
protected org.springframework.security.core.userdetails.UserDetailsService userDetailsService()
{
return new UserDetailsService(dataService, roleHierarchyAuthoritiesMapper());
}
@Override
@Bean
public org.springframework.security.core.userdetails.UserDetailsService userDetailsServiceBean() throws Exception
{
return userDetailsService();
}
@Bean
public UserDetailsChecker userDetailsChecker()
{
return new MolgenisUserDetailsChecker();
}
@Override
protected void configure(AuthenticationManagerBuilder auth)
{
try
{
auth.userDetailsService(userDetailsServiceBean());
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
daoAuthenticationProvider.setUserDetailsService(userDetailsServiceBean());
daoAuthenticationProvider.setPreAuthenticationChecks(userDetailsChecker());
auth.authenticationProvider(daoAuthenticationProvider);
}
catch (Exception e)
{
throw new RuntimeException(e);
}
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception
{
return super.authenticationManagerBean();
}
@Bean
public MolgenisPermissionService molgenisPermissionService()
{
return new MolgenisPermissionServiceImpl();
}
@Bean
public LoginUrlAuthenticationEntryPoint authenticationEntryPoint()
{
return new AjaxAwareLoginUrlAuthenticationEntryPoint("/login");
}
@Bean
public ApiSessionExpirationFilter apiSessionExpirationFilter()
{
return new ApiSessionExpirationFilter();
}
}