Fix to many request filter usage in DDI (#526)

* Fix DOS filter usage in DDI

Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com>

* Add optional CSP definition.

Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com>

* Fix for empty case.

Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com>

* readability and ensure that manual enforcement is also possible in
timeforced active but no hit yet.

Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com>

* Class order to bean order.

Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com>

* Fix exception propagation.

Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com>
This commit is contained in:
Kai Zimmermann
2017-06-07 16:32:52 +02:00
committed by GitHub
parent fa23aa05a8
commit 4709f4374c
8 changed files with 124 additions and 77 deletions

View File

@@ -10,6 +10,8 @@ package org.eclipse.hawkbit.autoconfigure.security;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
import javax.servlet.Filter; import javax.servlet.Filter;
@@ -80,6 +82,7 @@ import org.springframework.security.web.authentication.www.BasicAuthenticationEn
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.session.HttpSessionEventPublisher; import org.springframework.security.web.session.HttpSessionEventPublisher;
import org.springframework.security.web.session.SessionManagementFilter; import org.springframework.security.web.session.SessionManagementFilter;
import org.springframework.util.StringUtils;
import org.vaadin.spring.security.VaadinSecurityContext; import org.vaadin.spring.security.VaadinSecurityContext;
import org.vaadin.spring.security.annotation.EnableVaadinSecurity; import org.vaadin.spring.security.annotation.EnableVaadinSecurity;
import org.vaadin.spring.security.web.VaadinDefaultRedirectStrategy; import org.vaadin.spring.security.web.VaadinDefaultRedirectStrategy;
@@ -99,7 +102,7 @@ public class SecurityManagedConfiguration {
private static final Logger LOG = LoggerFactory.getLogger(SecurityManagedConfiguration.class); private static final Logger LOG = LoggerFactory.getLogger(SecurityManagedConfiguration.class);
private static final int DOS_FILTER_ORDER = 1; private static final int DOS_FILTER_ORDER = -200;
@Autowired @Autowired
private AuthenticationConfiguration configuration; private AuthenticationConfiguration configuration;
@@ -136,6 +139,8 @@ public class SecurityManagedConfiguration {
@ConditionalOnClass(DdiApiConfiguration.class) @ConditionalOnClass(DdiApiConfiguration.class)
static class ControllerSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter { static class ControllerSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
private static final String DDI_ANT_MATCHER = "/{tenant}/controller/**";
@Autowired @Autowired
private ControllerManagement controllerManagement; private ControllerManagement controllerManagement;
@@ -165,12 +170,10 @@ public class SecurityManagedConfiguration {
* of service protection filter in the filter chain * of service protection filter in the filter chain
*/ */
@Bean @Bean
@ConditionalOnClass(DdiApiConfiguration.class)
public FilterRegistrationBean dosDDiFilter(final HawkbitSecurityProperties securityProperties) { public FilterRegistrationBean dosDDiFilter(final HawkbitSecurityProperties securityProperties) {
final FilterRegistrationBean filterRegBean = dosFilter(securityProperties.getDos().getFilter(), final FilterRegistrationBean filterRegBean = dosFilter(Arrays.asList(DDI_ANT_MATCHER),
securityProperties.getClients()); securityProperties.getDos().getFilter(), securityProperties.getClients());
filterRegBean.addUrlPatterns("/{tenant}/controller/v1/*");
filterRegBean.setOrder(DOS_FILTER_ORDER); filterRegBean.setOrder(DOS_FILTER_ORDER);
filterRegBean.setName("dosDDiFilter"); filterRegBean.setName("dosDDiFilter");
@@ -224,13 +227,13 @@ public class SecurityManagedConfiguration {
Arrays.asList(new SimpleGrantedAuthority(SpringEvalExpressions.CONTROLLER_ROLE_ANONYMOUS), Arrays.asList(new SimpleGrantedAuthority(SpringEvalExpressions.CONTROLLER_ROLE_ANONYMOUS),
new SimpleGrantedAuthority(SpringEvalExpressions.CONTROLLER_DOWNLOAD_ROLE))); new SimpleGrantedAuthority(SpringEvalExpressions.CONTROLLER_DOWNLOAD_ROLE)));
anoymousFilter.setAuthenticationDetailsSource(authenticationDetailsSource); anoymousFilter.setAuthenticationDetailsSource(authenticationDetailsSource);
httpSec.requestMatchers().antMatchers("/*/controller/v1/**", "/*/controller/artifacts/v1/**").and() httpSec.requestMatchers().antMatchers(DDI_ANT_MATCHER).and().securityContext().disable().anonymous()
.securityContext().disable().anonymous().authenticationFilter(anoymousFilter); .authenticationFilter(anoymousFilter);
} else { } else {
httpSec.addFilter(securityHeaderFilter).addFilter(securityTokenFilter) httpSec.addFilter(securityHeaderFilter).addFilter(securityTokenFilter)
.addFilter(gatewaySecurityTokenFilter).addFilter(controllerAnonymousDownloadFilter) .addFilter(gatewaySecurityTokenFilter).addFilter(controllerAnonymousDownloadFilter)
.antMatcher("/*/controller/**").anonymous().disable().authorizeRequests().anyRequest() .antMatcher(DDI_ANT_MATCHER).anonymous().disable().authorizeRequests().anyRequest()
.authenticated().and().exceptionHandling() .authenticated().and().exceptionHandling()
.authenticationEntryPoint((request, response, authException) -> response .authenticationEntryPoint((request, response, authException) -> response
.setStatus(HttpStatus.UNAUTHORIZED.value())) .setStatus(HttpStatus.UNAUTHORIZED.value()))
@@ -259,8 +262,8 @@ public class SecurityManagedConfiguration {
@Bean @Bean
public FilterRegistrationBean dosSystemFilter(final HawkbitSecurityProperties securityProperties) { public FilterRegistrationBean dosSystemFilter(final HawkbitSecurityProperties securityProperties) {
final FilterRegistrationBean filterRegBean = dosFilter(securityProperties.getDos().getFilter(), final FilterRegistrationBean filterRegBean = dosFilter(Collections.emptyList(),
securityProperties.getClients()); securityProperties.getDos().getFilter(), securityProperties.getClients());
filterRegBean.setUrlPatterns(Arrays.asList("/system/*")); filterRegBean.setUrlPatterns(Arrays.asList("/system/*"));
filterRegBean.setOrder(DOS_FILTER_ORDER); filterRegBean.setOrder(DOS_FILTER_ORDER);
filterRegBean.setName("dosSystemFilter"); filterRegBean.setName("dosSystemFilter");
@@ -268,39 +271,54 @@ public class SecurityManagedConfiguration {
return filterRegBean; return filterRegBean;
} }
private static FilterRegistrationBean dosFilter(final HawkbitSecurityProperties.Dos.Filter filterProperties, private static FilterRegistrationBean dosFilter(final Collection<String> includeAntPaths,
final HawkbitSecurityProperties.Dos.Filter filterProperties,
final HawkbitSecurityProperties.Clients clientProperties) { final HawkbitSecurityProperties.Clients clientProperties) {
final FilterRegistrationBean filterRegBean = new FilterRegistrationBean(); final FilterRegistrationBean filterRegBean = new FilterRegistrationBean();
filterRegBean.setFilter(new DosFilter(filterProperties.getMaxRead(), filterProperties.getMaxWrite(), filterRegBean.setFilter(new DosFilter(includeAntPaths, filterProperties.getMaxRead(),
filterProperties.getWhitelist(), clientProperties.getBlacklist(), filterProperties.getMaxWrite(), filterProperties.getWhitelist(), clientProperties.getBlacklist(),
clientProperties.getRemoteIpHeader())); clientProperties.getRemoteIpHeader()));
return filterRegBean; return filterRegBean;
} }
/** /**
* Filter registration bean for spring etag filter. * A Websecruity config to handle and filter the download ids.
*
* @return the spring filter registration bean for registering an etag
* filter in the filter chain
*/ */
@Bean @Configuration
@Order(100) @EnableWebSecurity
public FilterRegistrationBean eTagFilter() { @Order(320)
@ConditionalOnClass(MgmtApiConfiguration.class)
public static class IdRestSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
final FilterRegistrationBean filterRegBean = new FilterRegistrationBean(); @Autowired
// Exclude the URLs for downloading artifacts, so no eTag is generated private DdiSecurityProperties ddiSecurityConfiguration;
// in the ShallowEtagHeaderFilter, just using the SH1 hash of the
// artifact itself as 'ETag', because otherwise the file will be copied
// in memory!
filterRegBean.setFilter(
new ExcludePathAwareShallowETagFilter("/rest/v1/softwaremodules/{smId}/artifacts/{artId}/download",
"/{tenant}/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/**",
"/api/v1/downloadserver/**"));
return filterRegBean; @Autowired
private DownloadIdCache downloadIdCache;
@Override
protected void configure(final HttpSecurity http) throws Exception {
final HttpDownloadAuthenticationFilter downloadIdAuthenticationFilter = new HttpDownloadAuthenticationFilter(
downloadIdCache);
downloadIdAuthenticationFilter.setAuthenticationManager(authenticationManager());
http.csrf().disable();
http.anonymous().disable();
http.regexMatcher(HttpDownloadAuthenticationFilter.REQUEST_ID_REGEX_PATTERN)
.addFilterBefore(downloadIdAuthenticationFilter, FilterSecurityInterceptor.class);
http.authorizeRequests().anyRequest().authenticated();
}
@Override
protected void configure(final AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(new PreAuthTokenSourceTrustAuthenticationProvider(
ddiSecurityConfiguration.getRp().getTrustedIPs()));
}
} }
/** /**
@@ -336,7 +354,7 @@ public class SecurityManagedConfiguration {
@Bean @Bean
public FilterRegistrationBean dosMgmtFilter(final HawkbitSecurityProperties securityProperties) { public FilterRegistrationBean dosMgmtFilter(final HawkbitSecurityProperties securityProperties) {
final FilterRegistrationBean filterRegBean = dosFilter(securityProperties.getDos().getFilter(), final FilterRegistrationBean filterRegBean = dosFilter(null, securityProperties.getDos().getFilter(),
securityProperties.getClients()); securityProperties.getClients());
filterRegBean.setUrlPatterns(Arrays.asList("/rest/*", "/api/*")); filterRegBean.setUrlPatterns(Arrays.asList("/rest/*", "/api/*"));
filterRegBean.setOrder(DOS_FILTER_ORDER); filterRegBean.setOrder(DOS_FILTER_ORDER);
@@ -384,6 +402,29 @@ public class SecurityManagedConfiguration {
} }
} }
/**
* Filter registration bean for spring etag filter.
*
* @return the spring filter registration bean for registering an etag
* filter in the filter chain
*/
@Bean
@Order(380)
public FilterRegistrationBean eTagFilter() {
final FilterRegistrationBean filterRegBean = new FilterRegistrationBean();
// Exclude the URLs for downloading artifacts, so no eTag is generated
// in the ShallowEtagHeaderFilter, just using the SH1 hash of the
// artifact itself as 'ETag', because otherwise the file will be copied
// in memory!
filterRegBean.setFilter(new ExcludePathAwareShallowETagFilter("/UI/**",
"/rest/v1/softwaremodules/{smId}/artifacts/{artId}/download",
"/{tenant}/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/**",
"/api/v1/downloadserver/**"));
return filterRegBean;
}
/** /**
* {@link WebSecurityConfigurer} for external (management) access. * {@link WebSecurityConfigurer} for external (management) access.
*/ */
@@ -399,6 +440,9 @@ public class SecurityManagedConfiguration {
@Autowired @Autowired
private SecurityProperties springSecurityProperties; private SecurityProperties springSecurityProperties;
@Autowired
private HawkbitSecurityProperties hawkbitSecurityProperties;
/** /**
* Filter to protect the hawkBit management UI against to many requests. * Filter to protect the hawkBit management UI against to many requests.
* *
@@ -411,7 +455,7 @@ public class SecurityManagedConfiguration {
@Bean @Bean
public FilterRegistrationBean dosMgmtUiFilter(final HawkbitSecurityProperties securityProperties) { public FilterRegistrationBean dosMgmtUiFilter(final HawkbitSecurityProperties securityProperties) {
final FilterRegistrationBean filterRegBean = dosFilter(securityProperties.getDos().getUiFilter(), final FilterRegistrationBean filterRegBean = dosFilter(null, securityProperties.getDos().getUiFilter(),
securityProperties.getClients()); securityProperties.getClients());
// All URLs that can be called anonymous // All URLs that can be called anonymous
filterRegBean.setUrlPatterns(Arrays.asList("/UI/login", "/UI/login/*", "/UI/logout", "/UI/logout/*")); filterRegBean.setUrlPatterns(Arrays.asList("/UI/login", "/UI/login/*", "/UI/logout", "/UI/logout/*"));
@@ -490,6 +534,10 @@ public class SecurityManagedConfiguration {
"\"******************\\n** Requires HTTPS Security has been disabled for UI, should only be used for developing purposes **\\n******************\""); "\"******************\\n** Requires HTTPS Security has been disabled for UI, should only be used for developing purposes **\\n******************\"");
} }
if (!StringUtils.isEmpty(hawkbitSecurityProperties.getContentSecurityPolicy())) {
httpSec.headers().contentSecurityPolicy(hawkbitSecurityProperties.getContentSecurityPolicy());
}
httpSec httpSec
// UI // UI
.authorizeRequests().antMatchers("/UI/login/**").permitAll().antMatchers("/UI/UIDL/**").permitAll() .authorizeRequests().antMatchers("/UI/login/**").permitAll().antMatchers("/UI/UIDL/**").permitAll()
@@ -501,46 +549,11 @@ public class SecurityManagedConfiguration {
@Override @Override
public void configure(final WebSecurity webSecurity) throws Exception { public void configure(final WebSecurity webSecurity) throws Exception {
// Not security for static content
webSecurity.ignoring().antMatchers("/documentation/**", "/VAADIN/**", "/*.*", "/docs/**"); webSecurity.ignoring().antMatchers("/documentation/**", "/VAADIN/**", "/*.*", "/docs/**");
} }
} }
/**
* A Websecruity config to handle and filter the download ids.
*/
@Configuration
@EnableWebSecurity
@Order(200)
@ConditionalOnClass(DdiApiConfiguration.class)
public static class IdRestSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
@Autowired
private DdiSecurityProperties ddiSecurityConfiguration;
@Autowired
private DownloadIdCache downloadIdCache;
@Override
protected void configure(final HttpSecurity http) throws Exception {
final HttpDownloadAuthenticationFilter downloadIdAuthenticationFilter = new HttpDownloadAuthenticationFilter(
downloadIdCache);
downloadIdAuthenticationFilter.setAuthenticationManager(authenticationManager());
http.csrf().disable();
http.anonymous().disable();
http.regexMatcher(HttpDownloadAuthenticationFilter.REQUEST_ID_REGEX_PATTERN)
.addFilterBefore(downloadIdAuthenticationFilter, FilterSecurityInterceptor.class);
http.authorizeRequests().anyRequest().authenticated();
}
@Override
protected void configure(final AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(new PreAuthTokenSourceTrustAuthenticationProvider(
ddiSecurityConfiguration.getRp().getTrustedIPs()));
}
}
} }
/** /**

View File

@@ -15,9 +15,6 @@ security.basic.realm=HawkBit
security.user.name=admin security.user.name=admin
security.user.password=admin security.user.password=admin
# Ensure that DosFilter runs before Spring Security
security.filter-order=5
# Spring cloud bus and stream # Spring cloud bus and stream
spring.cloud.bus.enabled=false spring.cloud.bus.enabled=false
# Disable Cloud Bus default events # Disable Cloud Bus default events

View File

@@ -614,7 +614,7 @@ public class JpaDeploymentManagement implements DeploymentManagement {
final JpaAction action = actionRepository.findById(actionId) final JpaAction action = actionRepository.findById(actionId)
.orElseThrow(() -> new EntityNotFoundException(Action.class, actionId)); .orElseThrow(() -> new EntityNotFoundException(Action.class, actionId));
if (!action.isForced()) { if (!action.isForce()) {
action.setActionType(ActionType.FORCED); action.setActionType(ActionType.FORCED);
return actionRepository.save(action); return actionRepository.save(action);
} }

View File

@@ -313,7 +313,7 @@ public abstract class AbstractIntegrationTest implements EnvironmentAware {
protected DefaultMockMvcBuilder createMvcWebAppContext() { protected DefaultMockMvcBuilder createMvcWebAppContext() {
return MockMvcBuilders.webAppContextSetup(context) return MockMvcBuilders.webAppContextSetup(context)
.addFilter(new DosFilter(100, 10, "127\\.0\\.0\\.1|\\[0:0:0:0:0:0:0:1\\]", "(^192\\.168\\.)", .addFilter(new DosFilter(null, 100, 10, "127\\.0\\.0\\.1|\\[0:0:0:0:0:0:0:1\\]", "(^192\\.168\\.)",
"X-Forwarded-For")) "X-Forwarded-For"))
.addFilter(new ExcludePathAwareShallowETagFilter( .addFilter(new ExcludePathAwareShallowETagFilter(
"/rest/v1/softwaremodules/{smId}/artifacts/{artId}/download", "/rest/v1/softwaremodules/{smId}/artifacts/{artId}/download",

View File

@@ -9,6 +9,7 @@
package org.eclipse.hawkbit.security; package org.eclipse.hawkbit.security;
import java.io.IOException; import java.io.IOException;
import java.util.Collection;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@@ -23,6 +24,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.filter.OncePerRequestFilter;
import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Cache;
@@ -40,6 +42,9 @@ public class DosFilter extends OncePerRequestFilter {
private static final Logger LOG_BLACKLIST = LoggerFactory private static final Logger LOG_BLACKLIST = LoggerFactory
.getLogger(SecurityConstants.SECURITY_LOG_PREFIX + ".blacklist"); .getLogger(SecurityConstants.SECURITY_LOG_PREFIX + ".blacklist");
private final AntPathMatcher antMatcher = new AntPathMatcher();
private final Collection<String> includeAntPaths;
private final Pattern ipAdressBlacklist; private final Pattern ipAdressBlacklist;
private final Cache<String, AtomicInteger> readCountCache = Caffeine.newBuilder() private final Cache<String, AtomicInteger> readCountCache = Caffeine.newBuilder()
@@ -58,6 +63,9 @@ public class DosFilter extends OncePerRequestFilter {
/** /**
* Filter constructor including configuration. * Filter constructor including configuration.
* *
* @param includeAntPaths
* paths where filter should hit
*
* @param maxRead * @param maxRead
* Maximum number of allowed REST read/GET requests per second * Maximum number of allowed REST read/GET requests per second
* per client * per client
@@ -73,9 +81,10 @@ public class DosFilter extends OncePerRequestFilter {
* the header containing the forwarded IP address e.g. * the header containing the forwarded IP address e.g.
* {@code x-forwarded-for} * {@code x-forwarded-for}
*/ */
public DosFilter(final int maxRead, final int maxWrite, final String ipDosWhiteListPattern, public DosFilter(final Collection<String> includeAntPaths, final int maxRead, final int maxWrite,
final String ipBlackListPattern, final String forwardHeader) { final String ipDosWhiteListPattern, final String ipBlackListPattern, final String forwardHeader) {
this.includeAntPaths = includeAntPaths;
this.maxRead = maxRead; this.maxRead = maxRead;
this.maxWrite = maxWrite; this.maxWrite = maxWrite;
this.forwardHeader = forwardHeader; this.forwardHeader = forwardHeader;
@@ -93,10 +102,24 @@ public class DosFilter extends OncePerRequestFilter {
} }
} }
private boolean shouldInclude(final HttpServletRequest request) {
if (includeAntPaths == null || includeAntPaths.isEmpty()) {
return true;
}
return includeAntPaths.stream()
.anyMatch(pattern -> antMatcher.match(request.getContextPath() + pattern, request.getRequestURI()));
}
@Override @Override
protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response,
final FilterChain filterChain) throws ServletException, IOException { final FilterChain filterChain) throws ServletException, IOException {
if (!shouldInclude(request)) {
filterChain.doFilter(request, response);
return;
}
boolean processChain; boolean processChain;
final String ip = IpUtil.getClientIpFromRequest(request, forwardHeader).getHost(); final String ip = IpUtil.getClientIpFromRequest(request, forwardHeader).getHost();

View File

@@ -27,14 +27,13 @@ import org.springframework.web.filter.ShallowEtagHeaderFilter;
public class ExcludePathAwareShallowETagFilter extends ShallowEtagHeaderFilter { public class ExcludePathAwareShallowETagFilter extends ShallowEtagHeaderFilter {
private final String[] excludeAntPaths; private final String[] excludeAntPaths;
private final AntPathMatcher antMatcher; private final AntPathMatcher antMatcher = new AntPathMatcher();
/** /**
* @param excludeAntPaths * @param excludeAntPaths
*/ */
public ExcludePathAwareShallowETagFilter(final String... excludeAntPaths) { public ExcludePathAwareShallowETagFilter(final String... excludeAntPaths) {
this.excludeAntPaths = excludeAntPaths; this.excludeAntPaths = excludeAntPaths;
this.antMatcher = new AntPathMatcher();
} }
@Override @Override

View File

@@ -20,6 +20,19 @@ public class HawkbitSecurityProperties {
private final Clients clients = new Clients(); private final Clients clients = new Clients();
private final Dos dos = new Dos(); private final Dos dos = new Dos();
/**
* Content Security policy Header for Manager UI.
*/
private String contentSecurityPolicy;
public String getContentSecurityPolicy() {
return contentSecurityPolicy;
}
public void setContentSecurityPolicy(final String contentSecurityPolicy) {
this.contentSecurityPolicy = contentSecurityPolicy;
}
public Dos getDos() { public Dos getDos() {
return dos; return dos;
} }

View File

@@ -98,6 +98,8 @@ public class SystemSecurityContext {
setSystemContext(SecurityContextHolder.getContext()); setSystemContext(SecurityContextHolder.getContext());
return callable.call(); return callable.call();
} catch (final RuntimeException e) {
throw e;
} catch (final Exception e) { } catch (final Exception e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }