Split SecurityManagedConfiguration to mgmt and ddi starters (#2014)

* SecurityManagedConfiguration is moved to hawkbit-rest-core with commons for mgmt and ddi only
* Configurations for DDI and Management API are moved to respective starters
* hawkbit-http-security is removed - DosFilter (as common) is moved in hawkbit-rest-security, rest to the ddi starter as used only there
* some classes are moved into different packages - it is a bad practice to have same packet into multiple artifacts

_release_notes_

Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
This commit is contained in:
Avgustin Marinov
2024-11-12 12:06:56 +02:00
committed by GitHub
parent 1e6c9d5efe
commit c85518be3c
34 changed files with 689 additions and 676 deletions

View File

@@ -0,0 +1,164 @@
/**
* Copyright (c) 2024 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.autoconfigure.ddi;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.autoconfigure.ddi.security.ControllerTenantAwareAuthenticationDetailsSource;
import org.eclipse.hawkbit.autoconfigure.ddi.security.HttpControllerPreAuthenticateAnonymousDownloadFilter;
import org.eclipse.hawkbit.autoconfigure.ddi.security.HttpControllerPreAuthenticateSecurityTokenFilter;
import org.eclipse.hawkbit.autoconfigure.ddi.security.HttpControllerPreAuthenticatedGatewaySecurityTokenFilter;
import org.eclipse.hawkbit.autoconfigure.ddi.security.HttpControllerPreAuthenticatedSecurityHeaderFilter;
import org.eclipse.hawkbit.ddi.rest.api.DdiRestConstants;
import org.eclipse.hawkbit.im.authentication.SpPermission;
import org.eclipse.hawkbit.repository.ControllerManagement;
import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
import org.eclipse.hawkbit.rest.SecurityManagedConfiguration;
import org.eclipse.hawkbit.rest.security.DosFilter;
import org.eclipse.hawkbit.security.DdiSecurityProperties;
import org.eclipse.hawkbit.security.HawkbitSecurityProperties;
import org.eclipse.hawkbit.security.MdcHandler;
import org.eclipse.hawkbit.security.SystemSecurityContext;
import org.eclipse.hawkbit.tenancy.TenantAware;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
/**
* Security configuration for the hawkBit server DDI download interface.
*/
@Slf4j
@Configuration
class ControllerDownloadSecurityConfiguration {
private static final String DDI_DL_ANT_MATCHER = DdiRestConstants.BASE_V1_REQUEST_MAPPING +
"/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/*";
private final ControllerManagement controllerManagement;
private final TenantConfigurationManagement tenantConfigurationManagement;
private final TenantAware tenantAware;
private final DdiSecurityProperties ddiSecurityConfiguration;
private final HawkbitSecurityProperties securityProperties;
private final SystemSecurityContext systemSecurityContext;
@Autowired
ControllerDownloadSecurityConfiguration(final ControllerManagement controllerManagement,
final TenantConfigurationManagement tenantConfigurationManagement, final TenantAware tenantAware,
final DdiSecurityProperties ddiSecurityConfiguration,
final HawkbitSecurityProperties securityProperties, final SystemSecurityContext systemSecurityContext) {
this.controllerManagement = controllerManagement;
this.tenantConfigurationManagement = tenantConfigurationManagement;
this.tenantAware = tenantAware;
this.ddiSecurityConfiguration = ddiSecurityConfiguration;
this.securityProperties = securityProperties;
this.systemSecurityContext = systemSecurityContext;
}
/**
* Filter to protect the hawkBit server DDI download interface against too many requests.
*
* @param securityProperties for filter configuration
* @return the spring filter registration bean for registering a denial of service protection filter in the filter chain
*/
@Bean
@ConditionalOnProperty(prefix = "hawkbit.server.security.dos.filter", name = "enabled", matchIfMissing = true)
public FilterRegistrationBean<DosFilter> dosFilterDDIDL(final HawkbitSecurityProperties securityProperties) {
final FilterRegistrationBean<DosFilter> filterRegBean = SecurityManagedConfiguration.dosFilter(List.of(DDI_DL_ANT_MATCHER),
securityProperties.getDos().getFilter(), securityProperties.getClients());
filterRegBean.setOrder(SecurityManagedConfiguration.DOS_FILTER_ORDER);
filterRegBean.setName("dosDDiDlFilter");
return filterRegBean;
}
@Bean
@Order(301)
protected SecurityFilterChain filterChainDDIDL(final HttpSecurity http) throws Exception {
final AuthenticationManager authenticationManager = ControllerSecurityConfiguration.setAuthenticationManager(
http, ddiSecurityConfiguration);
http
.securityMatcher(DDI_DL_ANT_MATCHER)
.csrf(AbstractHttpConfigurer::disable);
if (securityProperties.isRequireSsl()) {
http.requiresChannel(crmRegistry -> crmRegistry.anyRequest().requiresSecure());
}
final ControllerTenantAwareAuthenticationDetailsSource authenticationDetailsSource = new ControllerTenantAwareAuthenticationDetailsSource();
if (ddiSecurityConfiguration.getAuthentication().getAnonymous().isEnabled()) {
log.warn(
SecurityManagedConfiguration.ANONYMOUS_CONTROLLER_SECURITY_ENABLED_SHOULD_ONLY_BE_USED_FOR_DEVELOPMENT_PURPOSES);
final AnonymousAuthenticationFilter anonymousFilter = new AnonymousAuthenticationFilter(
"controllerAnonymousFilter", "anonymous",
List.of(new SimpleGrantedAuthority(SpPermission.SpringEvalExpressions.CONTROLLER_ROLE_ANONYMOUS)));
anonymousFilter.setAuthenticationDetailsSource(authenticationDetailsSource);
http
.securityContext(AbstractHttpConfigurer::disable)
.anonymous(configurer -> configurer.authenticationFilter(anonymousFilter));
} else {
final HttpControllerPreAuthenticatedSecurityHeaderFilter securityHeaderFilter = new HttpControllerPreAuthenticatedSecurityHeaderFilter(
ddiSecurityConfiguration.getRp().getCnHeader(),
ddiSecurityConfiguration.getRp().getSslIssuerHashHeader(), tenantConfigurationManagement,
tenantAware, systemSecurityContext);
securityHeaderFilter.setAuthenticationManager(authenticationManager);
securityHeaderFilter.setCheckForPrincipalChanges(true);
securityHeaderFilter.setAuthenticationDetailsSource(authenticationDetailsSource);
final HttpControllerPreAuthenticateSecurityTokenFilter securityTokenFilter = new HttpControllerPreAuthenticateSecurityTokenFilter(
tenantConfigurationManagement, tenantAware, controllerManagement, systemSecurityContext);
securityTokenFilter.setAuthenticationManager(authenticationManager);
securityTokenFilter.setCheckForPrincipalChanges(true);
securityTokenFilter.setAuthenticationDetailsSource(authenticationDetailsSource);
final HttpControllerPreAuthenticatedGatewaySecurityTokenFilter gatewaySecurityTokenFilter = new HttpControllerPreAuthenticatedGatewaySecurityTokenFilter(
tenantConfigurationManagement, tenantAware, systemSecurityContext);
gatewaySecurityTokenFilter.setAuthenticationManager(authenticationManager);
gatewaySecurityTokenFilter.setCheckForPrincipalChanges(true);
gatewaySecurityTokenFilter.setAuthenticationDetailsSource(authenticationDetailsSource);
final HttpControllerPreAuthenticateAnonymousDownloadFilter controllerAnonymousDownloadFilter = new HttpControllerPreAuthenticateAnonymousDownloadFilter(
tenantConfigurationManagement, tenantAware, systemSecurityContext);
controllerAnonymousDownloadFilter.setAuthenticationManager(authenticationManager);
controllerAnonymousDownloadFilter.setCheckForPrincipalChanges(true);
controllerAnonymousDownloadFilter.setAuthenticationDetailsSource(authenticationDetailsSource);
http
.authorizeHttpRequests(amrmRegistry -> amrmRegistry.anyRequest().authenticated())
.anonymous(AbstractHttpConfigurer::disable)
.addFilter(securityHeaderFilter)
.addFilter(securityTokenFilter)
.addFilter(gatewaySecurityTokenFilter)
.addFilter(controllerAnonymousDownloadFilter)
.exceptionHandling(configurer -> configurer.authenticationEntryPoint(
(request, response, authException) -> response.setStatus(HttpStatus.UNAUTHORIZED.value())))
.sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
}
MdcHandler.Filter.addMdcFilter(http);
return http.build();
}
}

View File

@@ -0,0 +1,178 @@
/**
* Copyright (c) 2024 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.autoconfigure.ddi;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.autoconfigure.ddi.security.ControllerTenantAwareAuthenticationDetailsSource;
import org.eclipse.hawkbit.autoconfigure.ddi.security.HttpControllerPreAuthenticateSecurityTokenFilter;
import org.eclipse.hawkbit.autoconfigure.ddi.security.HttpControllerPreAuthenticatedGatewaySecurityTokenFilter;
import org.eclipse.hawkbit.autoconfigure.ddi.security.HttpControllerPreAuthenticatedSecurityHeaderFilter;
import org.eclipse.hawkbit.ddi.rest.api.DdiRestConstants;
import org.eclipse.hawkbit.im.authentication.SpPermission;
import org.eclipse.hawkbit.repository.ControllerManagement;
import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
import org.eclipse.hawkbit.rest.SecurityManagedConfiguration;
import org.eclipse.hawkbit.rest.security.DosFilter;
import org.eclipse.hawkbit.security.DdiSecurityProperties;
import org.eclipse.hawkbit.security.HawkbitSecurityProperties;
import org.eclipse.hawkbit.security.MdcHandler;
import org.eclipse.hawkbit.security.PreAuthTokenSourceTrustAuthenticationProvider;
import org.eclipse.hawkbit.security.SystemSecurityContext;
import org.eclipse.hawkbit.tenancy.TenantAware;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.AuthenticationManager;
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.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
/**
* Security configuration for the hawkBit server DDI interface.
*/
@Slf4j
@Configuration
@EnableWebSecurity
class ControllerSecurityConfiguration {
private static final String[] DDI_ANT_MATCHERS = {
DdiRestConstants.BASE_V1_REQUEST_MAPPING + "/{controllerId}",
DdiRestConstants.BASE_V1_REQUEST_MAPPING + "/{controllerId}/confirmationBase/**",
DdiRestConstants.BASE_V1_REQUEST_MAPPING + "/{controllerId}/deploymentBase/**",
DdiRestConstants.BASE_V1_REQUEST_MAPPING + "/{controllerId}/installedBase/**",
DdiRestConstants.BASE_V1_REQUEST_MAPPING + "/{controllerId}/cancelAction/**",
DdiRestConstants.BASE_V1_REQUEST_MAPPING + "/{controllerId}/configData",
DdiRestConstants.BASE_V1_REQUEST_MAPPING + "/{controllerId}/softwaremodules/{softwareModuleId}/artifacts" };
private final ControllerManagement controllerManagement;
private final TenantConfigurationManagement tenantConfigurationManagement;
private final TenantAware tenantAware;
private final DdiSecurityProperties ddiSecurityConfiguration;
private final HawkbitSecurityProperties securityProperties;
private final SystemSecurityContext systemSecurityContext;
@Autowired
ControllerSecurityConfiguration(final ControllerManagement controllerManagement,
final TenantConfigurationManagement tenantConfigurationManagement, final TenantAware tenantAware,
final DdiSecurityProperties ddiSecurityConfiguration,
final HawkbitSecurityProperties securityProperties, final SystemSecurityContext systemSecurityContext) {
this.controllerManagement = controllerManagement;
this.tenantConfigurationManagement = tenantConfigurationManagement;
this.tenantAware = tenantAware;
this.ddiSecurityConfiguration = ddiSecurityConfiguration;
this.securityProperties = securityProperties;
this.systemSecurityContext = systemSecurityContext;
}
/**
* Filter to protect the hawkBit server DDI interface against too many requests.
*
* @param securityProperties for filter configuration
* @return the spring filter registration bean for registering a denial of service protection filter in the filter chain
*/
@Bean
@ConditionalOnProperty(prefix = "hawkbit.server.security.dos.filter", name = "enabled", matchIfMissing = true)
protected FilterRegistrationBean<DosFilter> dosFilterDDI(final HawkbitSecurityProperties securityProperties) {
final FilterRegistrationBean<DosFilter> filterRegBean =
SecurityManagedConfiguration.dosFilter(List.of(DDI_ANT_MATCHERS),
securityProperties.getDos().getFilter(), securityProperties.getClients());
filterRegBean.setOrder(SecurityManagedConfiguration.DOS_FILTER_ORDER);
filterRegBean.setName("dosDDiFilter");
return filterRegBean;
}
@Bean
@Order(300)
protected SecurityFilterChain filterChainDDI(final HttpSecurity http) throws Exception {
final AuthenticationManager authenticationManager = setAuthenticationManager(http, ddiSecurityConfiguration);
http
.securityMatcher(DDI_ANT_MATCHERS)
.csrf(AbstractHttpConfigurer::disable);
if (securityProperties.isRequireSsl()) {
http.requiresChannel(crmRegistry -> crmRegistry.anyRequest().requiresSecure());
}
final ControllerTenantAwareAuthenticationDetailsSource authenticationDetailsSource = new ControllerTenantAwareAuthenticationDetailsSource();
if (ddiSecurityConfiguration.getAuthentication().getAnonymous().isEnabled()) {
log.warn(SecurityManagedConfiguration.ANONYMOUS_CONTROLLER_SECURITY_ENABLED_SHOULD_ONLY_BE_USED_FOR_DEVELOPMENT_PURPOSES);
final AnonymousAuthenticationFilter anonymousFilter = new AnonymousAuthenticationFilter(
"controllerAnonymousFilter", "anonymous",
List.of(new SimpleGrantedAuthority(SpPermission.SpringEvalExpressions.CONTROLLER_ROLE_ANONYMOUS)));
anonymousFilter.setAuthenticationDetailsSource(authenticationDetailsSource);
http
.securityContext(AbstractHttpConfigurer::disable)
.anonymous(configurer -> configurer.authenticationFilter(anonymousFilter));
} else {
final HttpControllerPreAuthenticatedSecurityHeaderFilter securityHeaderFilter = new HttpControllerPreAuthenticatedSecurityHeaderFilter(
ddiSecurityConfiguration.getRp().getCnHeader(),
ddiSecurityConfiguration.getRp().getSslIssuerHashHeader(), tenantConfigurationManagement,
tenantAware, systemSecurityContext);
securityHeaderFilter.setAuthenticationManager(authenticationManager);
securityHeaderFilter.setCheckForPrincipalChanges(true);
securityHeaderFilter.setAuthenticationDetailsSource(authenticationDetailsSource);
final HttpControllerPreAuthenticateSecurityTokenFilter securityTokenFilter = new HttpControllerPreAuthenticateSecurityTokenFilter(
tenantConfigurationManagement, tenantAware, controllerManagement, systemSecurityContext);
securityTokenFilter.setAuthenticationManager(authenticationManager);
securityTokenFilter.setCheckForPrincipalChanges(true);
securityTokenFilter.setAuthenticationDetailsSource(authenticationDetailsSource);
final HttpControllerPreAuthenticatedGatewaySecurityTokenFilter gatewaySecurityTokenFilter = new HttpControllerPreAuthenticatedGatewaySecurityTokenFilter(
tenantConfigurationManagement, tenantAware, systemSecurityContext);
gatewaySecurityTokenFilter.setAuthenticationManager(authenticationManager);
gatewaySecurityTokenFilter.setCheckForPrincipalChanges(true);
gatewaySecurityTokenFilter.setAuthenticationDetailsSource(authenticationDetailsSource);
http
.authorizeHttpRequests(amrmRegistry ->
amrmRegistry.anyRequest().authenticated())
.anonymous(AbstractHttpConfigurer::disable)
.addFilter(securityHeaderFilter)
.addFilter(securityTokenFilter)
.addFilter(gatewaySecurityTokenFilter)
.exceptionHandling(configurer -> configurer.authenticationEntryPoint(
(request, response, authException) ->
response.setStatus(HttpStatus.UNAUTHORIZED.value())))
.sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
}
MdcHandler.Filter.addMdcFilter(http);
return http.build();
}
static AuthenticationManager setAuthenticationManager(final HttpSecurity http, final DdiSecurityProperties ddiSecurityConfiguration)
throws Exception {
// configure authentication manager
final AuthenticationManager authenticationManager =
http
.getSharedObject(AuthenticationManagerBuilder.class)
.authenticationProvider(
new PreAuthTokenSourceTrustAuthenticationProvider(ddiSecurityConfiguration.getRp().getTrustedIPs()))
.build();
http.authenticationManager(authenticationManager);
return authenticationManager;
}
}

View File

@@ -20,6 +20,6 @@ import org.springframework.context.annotation.Import;
@Configuration
@ConditionalOnClass(DdiApiConfiguration.class)
@Import(DdiApiConfiguration.class)
public class DDiApiAutoConfiguration {
public class DdiApiAutoConfiguration {
}

View File

@@ -0,0 +1,186 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.autoconfigure.ddi.security;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
import org.eclipse.hawkbit.security.DmfTenantSecurityToken;
import org.eclipse.hawkbit.security.PreAuthenticationFilter;
import org.eclipse.hawkbit.security.SystemSecurityContext;
import org.eclipse.hawkbit.tenancy.TenantAware;
import org.eclipse.hawkbit.util.UrlUtils;
import org.slf4j.Logger;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
import org.springframework.util.AntPathMatcher;
/**
* An abstraction for all controller based security to parse the e.g. the tenant
* name from the URL and the controller ID from the URL to do security checks
* based on this information.
*/
public abstract class AbstractHttpControllerAuthenticationFilter extends AbstractPreAuthenticatedProcessingFilter {
protected TenantConfigurationManagement tenantConfigurationManagement;
protected TenantAware tenantAware;
protected SystemSecurityContext systemSecurityContext;
private static final String TENANT_PLACE_HOLDER = "tenant";
private static final String CONTROLLER_ID_PLACE_HOLDER = "controllerId";
/**
* requestURIPathPattern the request URI path pattern in ANT style
* containing the placeholder key for retrieving the principal from the URI
* request. e.g."/{tenant}/controller/v1/{controllerId}
*/
private static final String CONTROLLER_REQUEST_ANT_PATTERN = "/{" + TENANT_PLACE_HOLDER + "}/controller/v1" + "/{"
+ CONTROLLER_ID_PLACE_HOLDER + "}/**";
private static final String CONTROLLER_DL_REQUEST_ANT_PATTERN = "/{" + TENANT_PLACE_HOLDER
+ "}/controller/artifacts/v1/**";
private final AntPathMatcher pathExtractor;
private PreAuthenticationFilter abstractControllerAuthenticationFilter;
/**
* Constructor for subclasses.
*
* @param tenantConfigurationManagement the tenant configuration service
* @param tenantAware the tenant aware service
* @param systemSecurityContext the system security context
*/
protected AbstractHttpControllerAuthenticationFilter(
final TenantConfigurationManagement tenantConfigurationManagement, final TenantAware tenantAware,
final SystemSecurityContext systemSecurityContext) {
this.tenantConfigurationManagement = tenantConfigurationManagement;
this.tenantAware = tenantAware;
this.systemSecurityContext = systemSecurityContext;
pathExtractor = new AntPathMatcher();
}
@Override
public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain)
throws IOException, ServletException {
if (SecurityContextHolder.getContext().getAuthentication() != null) {
log().trace("Request is already authenticated. Skip filter");
chain.doFilter(request, response);
return;
}
if (!(request instanceof HttpServletRequest)) {
chain.doFilter(request, response);
return;
}
final DmfTenantSecurityToken securityToken = createTenantSecurityTokenVariables((HttpServletRequest) request);
if (securityToken == null) {
chain.doFilter(request, response);
return;
}
abstractControllerAuthenticationFilter = createControllerAuthenticationFilter();
if (abstractControllerAuthenticationFilter.isEnable(securityToken)) {
super.doFilter(request, response, chain);
} else {
log().debug("Filter is disabled for the tenant {}", securityToken.getTenant());
chain.doFilter(request, response);
}
}
@Override
protected void successfulAuthentication(final HttpServletRequest request, final HttpServletResponse response,
final Authentication authResult) throws IOException, ServletException {
final Collection<GrantedAuthority> authorities = new ArrayList<>();
authorities.addAll(authResult.getAuthorities());
authorities.addAll(abstractControllerAuthenticationFilter.getSuccessfulAuthenticationAuthorities());
final PreAuthenticatedAuthenticationToken authTokenWithGrantedAuthorities = new PreAuthenticatedAuthenticationToken(
authResult.getPrincipal(), authResult.getCredentials(), authorities);
authTokenWithGrantedAuthorities.setDetails(authResult.getDetails());
super.successfulAuthentication(request, response, authTokenWithGrantedAuthorities);
}
@Override
protected Object getPreAuthenticatedPrincipal(final HttpServletRequest request) {
final DmfTenantSecurityToken securityToken = createTenantSecurityTokenVariables(request);
if (securityToken == null) {
return null;
}
return abstractControllerAuthenticationFilter.getPreAuthenticatedPrincipal(securityToken);
}
@Override
protected Object getPreAuthenticatedCredentials(final HttpServletRequest request) {
final DmfTenantSecurityToken securityToken = createTenantSecurityTokenVariables(request);
if (securityToken == null) {
return null;
}
return abstractControllerAuthenticationFilter.getPreAuthenticatedCredentials(securityToken);
}
protected abstract PreAuthenticationFilter createControllerAuthenticationFilter();
protected abstract Logger log();
/**
* Extracts tenant and controllerId from the request URI as path variables.
*
* @param request the Http request to extract the path variables.
* @return the extracted {@link DmfTenantSecurityToken} or {@code null} if the
* request does not match the pattern and no variables could be
* extracted
*/
protected DmfTenantSecurityToken createTenantSecurityTokenVariables(final HttpServletRequest request) {
final String requestURI = request.getRequestURI();
if (pathExtractor.match(request.getContextPath() + CONTROLLER_REQUEST_ANT_PATTERN, requestURI)) {
log().debug("retrieving principal from URI request {}", requestURI);
final Map<String, String> extractUriTemplateVariables = pathExtractor
.extractUriTemplateVariables(request.getContextPath() + CONTROLLER_REQUEST_ANT_PATTERN, requestURI);
final String controllerId = UrlUtils.decodeUriValue(extractUriTemplateVariables.get(CONTROLLER_ID_PLACE_HOLDER));
final String tenant = UrlUtils.decodeUriValue(extractUriTemplateVariables.get(TENANT_PLACE_HOLDER));
log().trace("Parsed tenant {} and controllerId {} from path request {}", tenant, controllerId, requestURI);
return createTenantSecurityTokenVariables(request, tenant, controllerId);
} else if (pathExtractor.match(request.getContextPath() + CONTROLLER_DL_REQUEST_ANT_PATTERN, requestURI)) {
log().debug("retrieving path variables from URI request {}", requestURI);
final Map<String, String> extractUriTemplateVariables = pathExtractor.extractUriTemplateVariables(
request.getContextPath() + CONTROLLER_DL_REQUEST_ANT_PATTERN, requestURI);
final String tenant = UrlUtils.decodeUriValue(extractUriTemplateVariables.get(TENANT_PLACE_HOLDER));
log().trace("Parsed tenant {} from path request {}", tenant, requestURI);
return createTenantSecurityTokenVariables(request, tenant, "anonymous");
} else {
log().trace("request {} does not match the path pattern {}, request gets ignored", requestURI,
CONTROLLER_REQUEST_ANT_PATTERN);
return null;
}
}
private DmfTenantSecurityToken createTenantSecurityTokenVariables(final HttpServletRequest request,
final String tenant, final String controllerId) {
final DmfTenantSecurityToken securityToken = new DmfTenantSecurityToken(tenant, null, controllerId, null);
Collections.list(request.getHeaderNames())
.forEach(header -> securityToken.putHeader(header, request.getHeader(header)));
return securityToken;
}
}

View File

@@ -0,0 +1,65 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.autoconfigure.ddi.security;
import java.util.Map;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.security.TenantAwareWebAuthenticationDetails;
import org.eclipse.hawkbit.tenancy.TenantAwareAuthenticationDetails;
import org.eclipse.hawkbit.util.UrlUtils;
import org.springframework.security.authentication.AuthenticationDetailsSource;
import org.springframework.util.AntPathMatcher;
/**
* An {@link AuthenticationDetailsSource} implementation which retrieves the
* tenant from a request pattern {@link #TENANT_AWARE_CONTROLLER_PATTERN} and
* stores the retrieved tenant in the {@link TenantAwareAuthenticationDetails}.
*/
@Slf4j
public class ControllerTenantAwareAuthenticationDetailsSource
implements AuthenticationDetailsSource<HttpServletRequest, TenantAwareAuthenticationDetails> {
private static final String TENANT_AWARE_CONTROLLER_PATTERN = "/{tenant}/controller/**";
private static final String TENANT_PLACE_HOLDER = "tenant";
private final AntPathMatcher pathExtractor;
/**
* Constructor.
*/
public ControllerTenantAwareAuthenticationDetailsSource() {
pathExtractor = new AntPathMatcher();
}
@Override
public TenantAwareAuthenticationDetails buildDetails(final HttpServletRequest request) {
return new TenantAwareWebAuthenticationDetails(getTenantFromRequestUri(request), request.getRemoteAddr(), true);
}
private String getTenantFromRequestUri(final HttpServletRequest request) {
final String requestURI = request.getRequestURI();
log.debug("retrieving tenant from URI request {}", requestURI);
final String requestPathPattern = request.getContextPath() + TENANT_AWARE_CONTROLLER_PATTERN;
if (!pathExtractor.match(requestPathPattern, requestURI)) {
log.info("Controller request not matching tenant aware request pattern requestpath: {}, pattern {}",
requestURI, TENANT_AWARE_CONTROLLER_PATTERN);
return null;
}
final Map<String, String> extractUriTemplateVariables = pathExtractor
.extractUriTemplateVariables(requestPathPattern, requestURI);
if (log.isTraceEnabled()) {
log.trace("Parsed path variables {} using tenant {}", extractUriTemplateVariables,
extractUriTemplateVariables.get(TENANT_PLACE_HOLDER));
}
return UrlUtils.decodeUriValue(extractUriTemplateVariables.get(TENANT_PLACE_HOLDER));
}
}

View File

@@ -0,0 +1,52 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.autoconfigure.ddi.security;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions;
import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
import org.eclipse.hawkbit.security.ControllerPreAuthenticatedAnonymousDownload;
import org.eclipse.hawkbit.security.PreAuthenticationFilter;
import org.eclipse.hawkbit.security.SystemSecurityContext;
import org.eclipse.hawkbit.tenancy.TenantAware;
import org.slf4j.Logger;
/**
* An pre-authenticated processing filter which add the
* {@link SpringEvalExpressions#CONTROLLER_DOWNLOAD_ROLE_ANONYMOUS} to the
* security context in case the anonymous download is allowed through
* configuration.
*/
@Slf4j
public class HttpControllerPreAuthenticateAnonymousDownloadFilter extends AbstractHttpControllerAuthenticationFilter {
/**
* Constructor.
*
* @param tenantConfigurationManagement the system management service to retrieve configuration properties
* @param tenantAware the tenant aware service to get configuration for the specific tenant
* @param systemSecurityContext the system security context
*/
public HttpControllerPreAuthenticateAnonymousDownloadFilter(
final TenantConfigurationManagement tenantConfigurationManagement, final TenantAware tenantAware,
final SystemSecurityContext systemSecurityContext) {
super(tenantConfigurationManagement, tenantAware, systemSecurityContext);
}
@Override
protected PreAuthenticationFilter createControllerAuthenticationFilter() {
return new ControllerPreAuthenticatedAnonymousDownload(tenantConfigurationManagement, tenantAware, systemSecurityContext);
}
@Override
protected Logger log() {
return log;
}
}

View File

@@ -0,0 +1,67 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.autoconfigure.ddi.security;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.repository.ControllerManagement;
import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
import org.eclipse.hawkbit.security.ControllerPreAuthenticateSecurityTokenFilter;
import org.eclipse.hawkbit.security.PreAuthenticationFilter;
import org.eclipse.hawkbit.security.SystemSecurityContext;
import org.eclipse.hawkbit.tenancy.TenantAware;
import org.slf4j.Logger;
/**
* An pre-authenticated processing filter which extracts (if enabled through
* configuration) the possibility to authenticate a target based on its target
* security-token with the {@code Authorization} HTTP header.
* {@code Example Header: Authorization: TargetToken
* 5d8fSD54fdsFG98DDsa.}
*
* The {@code Authorization} header is a HTTP standard and reverse proxy or
* other proxies will keep the Authorization headers untouched instead of maybe
* custom headers which have then weird side-effects. Furthermore frameworks are
* aware of the sensitivity of the Authorization header and do not log it and
* store it somewhere.
*/
@Slf4j
public class HttpControllerPreAuthenticateSecurityTokenFilter extends AbstractHttpControllerAuthenticationFilter {
private final ControllerManagement controllerManagement;
/**
* Constructor.
*
* @param tenantConfigurationManagement the system management service to retrieve configuration
* properties
* @param tenantAware the tenant aware service to get configuration for the specific
* tenant
* @param controllerManagement the controller management to retrieve the specific target
* security token to verify
* @param systemSecurityContext the system security context
*/
public HttpControllerPreAuthenticateSecurityTokenFilter(
final TenantConfigurationManagement tenantConfigurationManagement, final TenantAware tenantAware,
final ControllerManagement controllerManagement, final SystemSecurityContext systemSecurityContext) {
super(tenantConfigurationManagement, tenantAware, systemSecurityContext);
this.controllerManagement = controllerManagement;
}
@Override
protected PreAuthenticationFilter createControllerAuthenticationFilter() {
return new ControllerPreAuthenticateSecurityTokenFilter(tenantConfigurationManagement, controllerManagement,
tenantAware, systemSecurityContext);
}
@Override
protected Logger log() {
return log;
}
}

View File

@@ -0,0 +1,56 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.autoconfigure.ddi.security;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
import org.eclipse.hawkbit.security.ControllerPreAuthenticatedGatewaySecurityTokenFilter;
import org.eclipse.hawkbit.security.PreAuthenticationFilter;
import org.eclipse.hawkbit.security.SystemSecurityContext;
import org.eclipse.hawkbit.tenancy.TenantAware;
import org.slf4j.Logger;
/**
* Extract the {@code Authorization} header is a HTTP standard and reverse proxy
* or other proxies will keep the Authorization headers untouched instead of
* maybe custom headers which have then weird side-effects. Furthermore
* frameworks are aware of the sensitivity of the Authorization header and do
* not log it and store it somewhere.
*/
@Slf4j
public class HttpControllerPreAuthenticatedGatewaySecurityTokenFilter
extends AbstractHttpControllerAuthenticationFilter {
/**
* Constructor.
*
* @param tenantConfigurationManagement the system management service to retrieve configuration
* properties
* @param tenantAware the tenant aware service to get configuration for the specific
* tenant
* @param systemSecurityContext the system security context
*/
public HttpControllerPreAuthenticatedGatewaySecurityTokenFilter(
final TenantConfigurationManagement tenantConfigurationManagement, final TenantAware tenantAware,
final SystemSecurityContext systemSecurityContext) {
super(tenantConfigurationManagement, tenantAware, systemSecurityContext);
}
@Override
protected PreAuthenticationFilter createControllerAuthenticationFilter() {
return new ControllerPreAuthenticatedGatewaySecurityTokenFilter(tenantConfigurationManagement, tenantAware,
systemSecurityContext);
}
@Override
protected Logger log() {
return log;
}
}

View File

@@ -0,0 +1,66 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.autoconfigure.ddi.security;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
import org.eclipse.hawkbit.security.ControllerPreAuthenticatedSecurityHeaderFilter;
import org.eclipse.hawkbit.security.PreAuthenticationFilter;
import org.eclipse.hawkbit.security.SystemSecurityContext;
import org.eclipse.hawkbit.tenancy.TenantAware;
import org.slf4j.Logger;
/**
* An pre-authenticated processing filter which extracts the principal from a
* request URI and the credential from a request header.
*/
@Slf4j
public class HttpControllerPreAuthenticatedSecurityHeaderFilter extends AbstractHttpControllerAuthenticationFilter {
private final String caCommonNameHeader;
private final String caAuthorityNameHeader;
/**
* Creates a new {@link org.eclipse.hawkbit.security.ControllerPreAuthenticatedSecurityHeaderFilter}, in
* case the HTTP request matches the given pattern the principal is parsed
* from the HTTP request with the given URI pattern, in case the URI pattern
* does not match the current request then only the existence of the
* configured header field is checked.
*
* @param caCommonNameHeader the http-header which holds the common-name of the certificate
* @param caAuthorityNameHeader the http-header which holds the ca-authority name of the
* certificate
* @param tenantConfigurationManagement the tenant configuration management service to retrieve
* configuration properties to check if the header authentication
* is enabled for this tenant
* @param tenantAware the tenant aware service to get configuration for the specific
* tenant
* @param systemSecurityContext the system security context
*/
public HttpControllerPreAuthenticatedSecurityHeaderFilter(final String caCommonNameHeader,
final String caAuthorityNameHeader, final TenantConfigurationManagement tenantConfigurationManagement,
final TenantAware tenantAware, final SystemSecurityContext systemSecurityContext) {
super(tenantConfigurationManagement, tenantAware, systemSecurityContext);
this.caCommonNameHeader = caCommonNameHeader;
this.caAuthorityNameHeader = caAuthorityNameHeader;
}
@Override
protected PreAuthenticationFilter createControllerAuthenticationFilter() {
return new ControllerPreAuthenticatedSecurityHeaderFilter(
caCommonNameHeader, caAuthorityNameHeader,
tenantConfigurationManagement, tenantAware, systemSecurityContext);
}
@Override
protected Logger log() {
return log;
}
}

View File

@@ -1 +1,4 @@
org.eclipse.hawkbit.autoconfigure.ddi.DDiApiAutoConfiguration
org.eclipse.hawkbit.autoconfigure.ddi.DdiApiAutoConfiguration
org.eclipse.hawkbit.rest.SecurityManagedConfiguration
org.eclipse.hawkbit.autoconfigure.ddi.ControllerSecurityConfiguration
org.eclipse.hawkbit.autoconfigure.ddi.ControllerDownloadSecurityConfiguration

View File

@@ -0,0 +1,144 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.autoconfigure.ddi.security;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.Mockito.when;
import java.util.Collections;
import io.qameta.allure.Description;
import io.qameta.allure.Feature;
import io.qameta.allure.Story;
import org.eclipse.hawkbit.security.PreAuthTokenSourceTrustAuthenticationProvider;
import org.eclipse.hawkbit.security.TenantAwareWebAuthenticationDetails;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
@Feature("Unit Tests - Security")
@Story("PreAuthToken Source TrustAuthentication Provider Test")
@ExtendWith(MockitoExtension.class)
public class PreAuthTokenSourceTrustAuthenticationProviderTest {
private static final String REQUEST_SOURCE_IP = "127.0.0.1";
private final PreAuthTokenSourceTrustAuthenticationProvider underTestWithoutSourceIpCheck =
new PreAuthTokenSourceTrustAuthenticationProvider();
private final PreAuthTokenSourceTrustAuthenticationProvider underTestWithSourceIpCheck =
new PreAuthTokenSourceTrustAuthenticationProvider(REQUEST_SOURCE_IP);
@Mock
private TenantAwareWebAuthenticationDetails webAuthenticationDetailsMock;
@Test
@Description("Testing in case the containing controllerId in the URI request path does not accord with the controllerId in the request header.")
public void principalAndCredentialsNotTheSameThrowsAuthenticationException() {
final String principal = "controllerIdURL";
final String credentials = "controllerIdHeader";
final PreAuthenticatedAuthenticationToken token = new PreAuthenticatedAuthenticationToken(principal,
Collections.singletonList(credentials));
token.setDetails(webAuthenticationDetailsMock);
assertThatExceptionOfType(BadCredentialsException.class).as("Should not work with wrong credentials")
.isThrownBy(() -> underTestWithoutSourceIpCheck.authenticate(token));
}
@Test
@Description("Testing that the controllerId within the URI request path is the same with the controllerId within the request header and no source IP check is in place.")
public void principalAndCredentialsAreTheSameWithNoSourceIpCheckIsSuccessful() {
final String principal = "controllerId";
final String credentials = "controllerId";
final PreAuthenticatedAuthenticationToken token = new PreAuthenticatedAuthenticationToken(principal,
Collections.singletonList(credentials));
token.setDetails(webAuthenticationDetailsMock);
final Authentication authenticate = underTestWithoutSourceIpCheck.authenticate(token);
assertThat(authenticate.isAuthenticated()).isTrue();
}
@Test
@Description("Testing that the controllerId in the URI request match with the controllerId in the request header but the request are not coming from a trustful source.")
public void principalAndCredentialsAreTheSameButSourceIpRequestNotMatching2() {
final String remoteAddress = "192.168.1.1";
final String principal = "controllerId";
final String credentials = "controllerId";
final PreAuthenticatedAuthenticationToken token = new PreAuthenticatedAuthenticationToken(principal,
Collections.singletonList(credentials));
token.setDetails(webAuthenticationDetailsMock);
when(webAuthenticationDetailsMock.getRemoteAddress()).thenReturn(remoteAddress);
assertThatExceptionOfType(InsufficientAuthenticationException.class).as("as source is not trusted.")
.isThrownBy(() -> underTestWithSourceIpCheck.authenticate(token));
}
@Test
@Description("Testing that the controllerId in the URI request match with the controllerId in the request header and the source IP is matching the allowed remote IP address.")
public void principalAndCredentialsAreTheSameAndSourceIpIsTrusted() {
final String principal = "controllerId";
final String credentials = "controllerId";
final PreAuthenticatedAuthenticationToken token = new PreAuthenticatedAuthenticationToken(principal,
Collections.singletonList(credentials));
token.setDetails(webAuthenticationDetailsMock);
when(webAuthenticationDetailsMock.getRemoteAddress()).thenReturn(REQUEST_SOURCE_IP);
// test, should throw authentication exception
final Authentication authenticate = underTestWithSourceIpCheck.authenticate(token);
assertThat(authenticate.isAuthenticated()).isTrue();
}
@Test
@Description("Testing that the controllerId in the URI request match with the controllerId in the request header and the source IP matches one of the allowed remote IP addresses.")
public void principalAndCredentialsAreTheSameAndSourceIpIsWithinList() {
final String[] trustedIPAddresses = new String[] { "192.168.1.1", "192.168.1.2", REQUEST_SOURCE_IP,
"192.168.1.3" };
final String principal = "controllerId";
final String credentials = "controllerId";
final PreAuthenticatedAuthenticationToken token = new PreAuthenticatedAuthenticationToken(principal,
Collections.singletonList(credentials));
token.setDetails(webAuthenticationDetailsMock);
when(webAuthenticationDetailsMock.getRemoteAddress()).thenReturn(REQUEST_SOURCE_IP);
final PreAuthTokenSourceTrustAuthenticationProvider underTestWithList = new PreAuthTokenSourceTrustAuthenticationProvider(
trustedIPAddresses);
// test, should throw authentication exception
final Authentication authenticate = underTestWithList.authenticate(token);
assertThat(authenticate.isAuthenticated()).isTrue();
}
@Test
@Description("Testing that the controllerId in the URI request match with the controllerId in the request header and the source IP does not match any of the allowed remote IP addresses.")
public void principalAndCredentialsAreTheSameSourceIpListNotMatches() {
final String[] trustedIPAddresses = new String[] { "192.168.1.1", "192.168.1.2", "192.168.1.3" };
final String principal = "controllerId";
final String credentials = "controllerId";
final PreAuthenticatedAuthenticationToken token = new PreAuthenticatedAuthenticationToken(principal,
Collections.singletonList(credentials));
token.setDetails(webAuthenticationDetailsMock);
when(webAuthenticationDetailsMock.getRemoteAddress()).thenReturn(REQUEST_SOURCE_IP);
final PreAuthTokenSourceTrustAuthenticationProvider underTestWithList = new PreAuthTokenSourceTrustAuthenticationProvider(
trustedIPAddresses);
assertThatExceptionOfType(InsufficientAuthenticationException.class)
.isThrownBy(() -> underTestWithList.authenticate(token));
}
}