Cleanup and improve the controller authentication (#2287)

Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
This commit is contained in:
Avgustin Marinov
2025-02-18 15:10:16 +02:00
committed by GitHub
parent cace8bd20e
commit 76ce1cf052
51 changed files with 942 additions and 1517 deletions

View File

@@ -14,11 +14,11 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.eclipse.hawkbit</groupId>
<artifactId>hawkbit-security-parent</artifactId>
<artifactId>hawkbit-ddi-parent</artifactId>
<version>${revision}</version>
</parent>
<artifactId>hawkbit-security-controller</artifactId>
<artifactId>hawkbit-ddi-security</artifactId>
<name>hawkBit :: Security :: Controller</name>
<dependencies>

View File

@@ -0,0 +1,165 @@
/**
* Copyright (c) 2025 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.security.controller;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.eclipse.hawkbit.security.DdiSecurityProperties;
import org.eclipse.hawkbit.util.UrlUtils;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextHolderStrategy;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.filter.OncePerRequestFilter;
/**
* 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.
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class AuthenticationFilters {
public static class GatewayTokenAuthenticationFilter extends AbstractAuthenticationFilter {
public GatewayTokenAuthenticationFilter(final GatewayTokenAuthenticator authenticator, final DdiSecurityProperties ddiSecurityProperties) {
super(authenticator, ddiSecurityProperties);
}
}
public static class SecurityHeaderAuthenticationFilter extends AbstractAuthenticationFilter {
public SecurityHeaderAuthenticationFilter(final SecurityHeaderAuthenticator authenticator, final DdiSecurityProperties ddiSecurityProperties) {
super(authenticator, ddiSecurityProperties);
}
}
public static class SecurityTokenAuthenticationFilter extends AbstractAuthenticationFilter {
public SecurityTokenAuthenticationFilter(final SecurityTokenAuthenticator authenticator, final DdiSecurityProperties ddiSecurityProperties) {
super(authenticator, ddiSecurityProperties);
}
}
/**
* 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 static abstract class AbstractAuthenticationFilter extends OncePerRequestFilter {
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 SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder.getContextHolderStrategy();
private final AntPathMatcher pathExtractor = new AntPathMatcher();
private final Authenticator authenticator;
private final List<String> authorizedSourceIps;
protected AbstractAuthenticationFilter(final Authenticator authenticator, final DdiSecurityProperties ddiSecurityProperties) {
this.authenticator = authenticator;
authorizedSourceIps = ddiSecurityProperties.getRp().getTrustedIPs();
}
@Override
protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, final FilterChain chain)
throws IOException, ServletException {
if (acceptIPAddress(request)) {
final Authentication currentAuthentication = SecurityContextHolder.getContext().getAuthentication();
if (currentAuthentication == null || !currentAuthentication.isAuthenticated()) {
final ControllerSecurityToken securityToken = createTenantSecurityTokenVariables(request);
if (securityToken != null) {
final Authentication authentication = authenticator.authenticate(securityToken);
if (authentication != null) {
SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();
context.setAuthentication(authentication);
this.securityContextHolderStrategy.setContext(context);
}
}
} else {
authenticator.log().trace("Request is already authenticated. Skip filter");
}
}
chain.doFilter(request, response);
}
/**
* 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 ControllerSecurityToken} or {@code null} if the request does not match the pattern and no variables could be
* extracted
*/
private ControllerSecurityToken createTenantSecurityTokenVariables(final HttpServletRequest request) {
final String requestURI = request.getRequestURI();
if (pathExtractor.match(request.getContextPath() + CONTROLLER_REQUEST_ANT_PATTERN, requestURI)) {
authenticator.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));
authenticator.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)) {
authenticator.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));
authenticator.log().trace("Parsed tenant {} from path request {}", tenant, requestURI);
return createTenantSecurityTokenVariables(request, tenant, "anonymous");
} else {
authenticator.log().trace("request {} does not match the path pattern {}, request gets ignored", requestURI, CONTROLLER_REQUEST_ANT_PATTERN);
return null;
}
}
private ControllerSecurityToken createTenantSecurityTokenVariables(
final HttpServletRequest request, final String tenant, final String controllerId) {
final ControllerSecurityToken securityToken = new ControllerSecurityToken(tenant, null, controllerId, null);
Collections.list(request.getHeaderNames()).forEach(header -> securityToken.putHeader(header, request.getHeader(header)));
return securityToken;
}
private boolean acceptIPAddress(final HttpServletRequest request) {
if (authorizedSourceIps == null) {
// no trusted IP check, because no authorizedSourceIPs configuration
return true;
}
final String remoteAddress = request.getRemoteAddr();
if (authorizedSourceIps.contains(remoteAddress)) {
// source ip matches the given pattern -> authenticated
return true;
} else {
authenticator.log().debug(
"The remote source IP address {} is not in the list of trusted IP addresses {}", remoteAddress, authorizedSourceIps);
return false;
}
}
}
}

View File

@@ -0,0 +1,101 @@
/**
* Copyright (c) 2025 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.security.controller;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import lombok.EqualsAndHashCode;
import org.eclipse.hawkbit.im.authentication.SpPermission;
import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
import org.eclipse.hawkbit.security.SystemSecurityContext;
import org.eclipse.hawkbit.tenancy.TenantAware;
import org.eclipse.hawkbit.tenancy.TenantAwareAuthenticationDetails;
import org.slf4j.Logger;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
/**
* Interface for Authentication mechanism.
*/
public interface Authenticator {
/**
* If the authentication mechanism is not enabled for the tenant - it just returns null.
* If the authentication mechanism is supported, the filter extracts from the security token the related credentials,
* validate them (do authenticate the caller).
* If validation / authentication is successful returns an authenticated authentication object. Otherwise,
* throws BadCredentialsException.
*
* @param controllerSecurityToken the securityToken
* @return the extracted tenant and controller id
*/
Authentication authenticate(ControllerSecurityToken controllerSecurityToken);
Logger log();
abstract class AbstractAuthenticator implements Authenticator {
protected final TenantConfigurationManagement tenantConfigurationManagement;
protected final TenantAware tenantAware;
protected final SystemSecurityContext systemSecurityContext;
private final TenantAware.TenantRunner<Boolean> isEnabledTenantRunner;
protected AbstractAuthenticator(
final TenantConfigurationManagement tenantConfigurationManagement,
final TenantAware tenantAware, final SystemSecurityContext systemSecurityContext) {
this.tenantConfigurationManagement = tenantConfigurationManagement;
this.tenantAware = tenantAware;
this.systemSecurityContext = systemSecurityContext;
isEnabledTenantRunner = () -> systemSecurityContext.runAsSystem(
() -> tenantConfigurationManagement.getConfigurationValue(getTenantConfigurationKey(), Boolean.class).getValue());
}
protected boolean isEnabled(final ControllerSecurityToken securityToken) {
return tenantAware.runAsTenant(securityToken.getTenant(), isEnabledTenantRunner);
}
protected abstract String getTenantConfigurationKey();
protected Authentication authenticatedController(final String tenant, final String controllerId) {
Objects.requireNonNull(tenant, "tenant must not be null");
Objects.requireNonNull(controllerId, "controllerId must not be null");
return new AuthenticatedController(tenant, controllerId);
}
@EqualsAndHashCode(callSuper = true)
private static class AuthenticatedController extends AbstractAuthenticationToken {
private static final Collection<GrantedAuthority> CONTROLLER_AUTHORITY =
List.of(new SimpleGrantedAuthority(SpPermission.SpringEvalExpressions.CONTROLLER_ROLE));
private final String controllerId;
AuthenticatedController(final String tenant, final String controllerId) {
super(CONTROLLER_AUTHORITY);
super.setDetails(new TenantAwareAuthenticationDetails(tenant, true));
this.controllerId = controllerId;
setAuthenticated(true);
}
@Override
public Object getCredentials() {
return null;
}
@Override
public Object getPrincipal() {
return controllerId;
}
}
}
}

View File

@@ -0,0 +1,83 @@
/**
* Copyright (c) 2025 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.security.controller;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
import org.eclipse.hawkbit.security.SystemSecurityContext;
import org.eclipse.hawkbit.tenancy.TenantAware;
import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey;
import org.slf4j.Logger;
import org.springframework.security.core.Authentication;
/**
* An authenticator which extracts (if enabled through configuration) the possibility to authenticate a target based through
* a gateway security token. This is commonly used for targets connected indirectly via a gateway. This gateway controls multiple targets
* under the gateway security token which can be set via the {@code Authorization} header.
* <p>
* {@code Example Header: Authorization: GatewayToken 5d8fSD54fdsFG98DDsa.}
*/
@Slf4j
public class GatewayTokenAuthenticator extends Authenticator.AbstractAuthenticator {
public static final String GATEWAY_SECURITY_TOKEN_AUTH_SCHEME = "GatewayToken ";
private static final int OFFSET_GATEWAY_TOKEN = GATEWAY_SECURITY_TOKEN_AUTH_SCHEME.length();
private final TenantAware.TenantRunner<String> gatewaySecurityTokenKeyConfigRunner;
public GatewayTokenAuthenticator(
final TenantConfigurationManagement tenantConfigurationManagement, final TenantAware tenantAware,
final SystemSecurityContext systemSecurityContext) {
super(tenantConfigurationManagement, tenantAware, systemSecurityContext);
gatewaySecurityTokenKeyConfigRunner = () -> {
log.trace("retrieving configuration value for configuration key {}",
TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY);
return systemSecurityContext
.runAsSystem(() -> tenantConfigurationManagement
.getConfigurationValue(TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY, String.class)
.getValue());
};
}
@Override
public Authentication authenticate(final ControllerSecurityToken controllerSecurityToken) {
final String authHeader = controllerSecurityToken.getHeader(ControllerSecurityToken.AUTHORIZATION_HEADER);
if (authHeader == null) {
log.debug("The request doesn't contain the 'authorization' header");
return null;
} else if (!authHeader.startsWith(GATEWAY_SECURITY_TOKEN_AUTH_SCHEME)) {
log.debug("The request contains the 'authorization' header but it doesn't start with '{}'", GATEWAY_SECURITY_TOKEN_AUTH_SCHEME);
return null;
}
if (!isEnabled(controllerSecurityToken)) {
log.debug("The gateway token authentication is disabled");
return null;
}
log.debug("Found 'authorization' header starting with '{}'", GATEWAY_SECURITY_TOKEN_AUTH_SCHEME);
final String presentedToken = authHeader.substring(OFFSET_GATEWAY_TOKEN);
// validate if the presented token is the same as the gateway token
return presentedToken.equals(tenantAware.runAsTenant(controllerSecurityToken.getTenant(), gatewaySecurityTokenKeyConfigRunner))
? authenticatedController(controllerSecurityToken.getTenant(), controllerSecurityToken.getControllerId()) : null;
}
@Override
public Logger log() {
return log;
}
@Override
protected String getTenantConfigurationKey() {
return TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_ENABLED;
}
}

View File

@@ -0,0 +1,130 @@
/**
* Copyright (c) 2025 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.security.controller;
import java.util.Arrays;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
import org.eclipse.hawkbit.security.SystemSecurityContext;
import org.eclipse.hawkbit.tenancy.TenantAware;
import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
/**
* An authenticator which extracts the principal from a request URI and the credential from a request header in a the
* {@link ControllerSecurityToken}.
*/
@Slf4j
public class SecurityHeaderAuthenticator extends Authenticator.AbstractAuthenticator {
private static final Logger LOG_SECURITY_AUTH = LoggerFactory.getLogger("server-security.authentication");
// Example Headers with Cert Information
// Clientip: 217.24.201.180
// X-Forwarded-Proto: https
// X-Ssl-Client-Cn: my.name
// X-Ssl-Client-Dn: CN=my.name,CN=O,CN=R,CN=DE,CN=BOSCH,CN=pki,DC=bosch,DC=com
// X-Ssl-Client-Hash: 7f:87:cb:b5:9c:e0:c5:0a:1a:a6:57:69:0f:ca:0a:95
// X-Ssl-Client-Notafter: Dec 18 08:02:45 2017 GMT
// X-Ssl-Client-Notbefore: Dec 18 07:32:45 2014 GMT
// X-Ssl-Client-Verify: ok
// X-Ssl-Issuer: CN=Bosch-CA1-DE,CN=PKI,DC=Bosch,DC=com
// X-Ssl-Issuer-Dn-1: CN=Bosch-CA-DE,CN=PKI,DC=Bosch,DC=com
// X-Ssl-Issuer-Hash-1: ae:11:f5:6a:0a:e8:74:50:81:0e:0c:37:ec:c5:22:fc
private final String caCommonNameHeader;
// the X-Ssl-Issuer-Hash basic header: Contains the x509 fingerprint hash, this
// header exists multiple times in the request for all trusted chains.
private final String sslIssuerHashBasicHeader;
private final TenantAware.TenantRunner<String> sslIssuerNameConfigTenantRunner;
public SecurityHeaderAuthenticator(
final TenantConfigurationManagement tenantConfigurationManagement, final TenantAware tenantAware,
final SystemSecurityContext systemSecurityContext,
final String caCommonNameHeader, final String caAuthorityNameHeader) {
super(tenantConfigurationManagement, tenantAware, systemSecurityContext);
this.caCommonNameHeader = caCommonNameHeader;
this.sslIssuerHashBasicHeader = caAuthorityNameHeader;
sslIssuerNameConfigTenantRunner = () -> systemSecurityContext.runAsSystem(
() -> tenantConfigurationManagement.getConfigurationValue(
TenantConfigurationKey.AUTHENTICATION_MODE_HEADER_AUTHORITY_NAME, String.class).getValue());
}
@Override
public Authentication authenticate(final ControllerSecurityToken controllerSecurityToken) {
// retrieve the common name header and the authority name header from the http request and combine them together
final String commonNameValue = controllerSecurityToken.getHeader(caCommonNameHeader);
if (commonNameValue == null) {
log.debug("The request doesn't contain the 'common name' header");
return null;
}
if (!commonNameValue.equals(controllerSecurityToken.getControllerId())) {
log.debug("The request contains the 'common name' header but it doesn't match the controller id");
return null;
}
if (!isEnabled(controllerSecurityToken)) {
log.debug("The gateway header authentication is disabled");
return null;
}
final String sslIssuerHashValue = getIssuerHashHeader(
controllerSecurityToken,
tenantAware.runAsTenant(controllerSecurityToken.getTenant(), sslIssuerNameConfigTenantRunner));
if (sslIssuerHashValue == null) {
log.debug("The request contains the 'common name' header but trusted hash is not found");
return null;
}
if (log.isTraceEnabled()) {
log.debug("Found sslIssuerHash ****, using as credentials for tenant {}", controllerSecurityToken.getTenant());
}
return authenticatedController(controllerSecurityToken.getTenant(), commonNameValue);
}
@Override
protected String getTenantConfigurationKey() {
return TenantConfigurationKey.AUTHENTICATION_MODE_HEADER_ENABLED;
}
@Override
public Logger log() {
return log;
}
/**
* Iterates over the {@link #sslIssuerHashBasicHeader} basic header {@code X-Ssl-Issuer-Hash-%d} and try to find the same hash as known.
* It's ok if we find the hash in any the trusted CA chain to accept this request for this tenant.
*/
@SuppressWarnings("java:S2629") // check if debug is enabled is maybe heavier then evaluation
private String getIssuerHashHeader(final ControllerSecurityToken controllerSecurityToken, final String knownIssuerHashes) {
// there may be several knownIssuerHashes configured for the tenant
final List<String> knownHashes = Arrays.stream(knownIssuerHashes.split("[;,]")).map(String::toLowerCase).toList();
// iterate over the headers until we get a null header.
String foundHash;
for (int iHeader = 1; (foundHash = controllerSecurityToken.getHeader(String.format(sslIssuerHashBasicHeader, iHeader))) != null; iHeader++) {
if (knownHashes.contains(foundHash.toLowerCase())) {
if (log.isTraceEnabled()) {
log.trace("Found matching ssl issuer hash at position {}", iHeader);
}
return foundHash.toLowerCase();
}
}
LOG_SECURITY_AUTH.debug(
"Certificate request but no matching hash found in headers {} for common name {} in request",
sslIssuerHashBasicHeader, controllerSecurityToken.getHeader(caCommonNameHeader));
return null;
}
}

View File

@@ -0,0 +1,81 @@
/**
* Copyright (c) 2025 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.security.controller;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.repository.ControllerManagement;
import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
import org.eclipse.hawkbit.security.SystemSecurityContext;
import org.eclipse.hawkbit.tenancy.TenantAware;
import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey;
import org.slf4j.Logger;
import org.springframework.security.core.Authentication;
/**
* An authenticator which extracts (if enabled through configuration) the possibility to authenticate a target based on
* its target security-token with the {@code Authorization} HTTP header.
* <p>
* {@code Example Header: Authorization: TargetToken 5d8fSD54fdsFG98DDsa.}
*/
@Slf4j
public class SecurityTokenAuthenticator extends Authenticator.AbstractAuthenticator {
public static final String TARGET_SECURITY_TOKEN_AUTH_SCHEME = "TargetToken ";
private static final int OFFSET_TARGET_TOKEN = TARGET_SECURITY_TOKEN_AUTH_SCHEME.length();
private final ControllerManagement controllerManagement;
public SecurityTokenAuthenticator(
final TenantConfigurationManagement tenantConfigurationManagement, final TenantAware tenantAware,
final SystemSecurityContext systemSecurityContext,
final ControllerManagement controllerManagement) {
super(tenantConfigurationManagement, tenantAware, systemSecurityContext);
this.controllerManagement = controllerManagement;
}
@Override
public Authentication authenticate(final ControllerSecurityToken controllerSecurityToken) {
final String authHeader = controllerSecurityToken.getHeader(ControllerSecurityToken.AUTHORIZATION_HEADER);
if (authHeader == null) {
log.debug("The request doesn't contain the 'authorization' header");
return null;
} else if (!authHeader.startsWith(TARGET_SECURITY_TOKEN_AUTH_SCHEME)) {
log.debug("The request contains the 'authorization' header but it doesn't start with '{}'", TARGET_SECURITY_TOKEN_AUTH_SCHEME);
return null;
}
if (!isEnabled(controllerSecurityToken)) {
log.debug("The target security token authentication is disabled");
return null;
}
log.debug("Found 'authorization' header starting with '{}'", TARGET_SECURITY_TOKEN_AUTH_SCHEME);
final String presentedToken = authHeader.substring(OFFSET_TARGET_TOKEN);
return systemSecurityContext.runAsSystemAsTenant(() -> controllerSecurityToken.getTargetId() != null
? controllerManagement.get(controllerSecurityToken.getTargetId())
: controllerManagement.getByControllerId(controllerSecurityToken.getControllerId()),
controllerSecurityToken.getTenant())
// validate if the presented token is the same as the one set for the target
.filter(target -> presentedToken.equals(target.getSecurityToken()))
.map(target -> authenticatedController(controllerSecurityToken.getTenant(), target.getControllerId()))
.orElse(null);
}
@Override
public Logger log() {
return log;
}
@Override
protected String getTenantConfigurationKey() {
return TenantConfigurationKey.AUTHENTICATION_MODE_TARGET_SECURITY_TOKEN_ENABLED;
}
}

View File

@@ -0,0 +1,116 @@
/**
* 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.security.controller;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
import io.qameta.allure.Description;
import io.qameta.allure.Feature;
import io.qameta.allure.Story;
import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
import org.eclipse.hawkbit.repository.model.TenantConfigurationValue;
import org.eclipse.hawkbit.security.SecurityContextSerializer;
import org.eclipse.hawkbit.security.SecurityContextTenantAware;
import org.eclipse.hawkbit.security.SystemSecurityContext;
import org.eclipse.hawkbit.tenancy.UserAuthoritiesResolver;
import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@Feature("Unit Tests - Security")
@Story("Gateway token authentication")
@ExtendWith(MockitoExtension.class)
class GatewayTokenAuthenticatorTest {
private static final String CONTROLLER_ID = "controllerId_gwtoken";
private static final String GATEWAY_TOKEN = "test-gw-token";
private static final String UNKNOWN_TOKEN = "unknown";
private static final TenantConfigurationValue<String> CONFIG_VALUE_GW_TOKEN = TenantConfigurationValue
.<String> builder().value(GATEWAY_TOKEN).build();
private static final TenantConfigurationValue<Boolean> CONFIG_VALUE_ENABLED = TenantConfigurationValue
.<Boolean> builder().value(true).build();
private static final TenantConfigurationValue<Boolean> CONFIG_VALUE_DISABLED = TenantConfigurationValue
.<Boolean> builder().value(false).build();
private Authenticator authenticator;
@Mock
private TenantConfigurationManagement tenantConfigurationManagementMock;
@Mock
private UserAuthoritiesResolver authoritiesResolver;
@Mock
private SecurityContextSerializer securityContextSerializer;
@BeforeEach
void before() {
final SecurityContextTenantAware tenantAware = new SecurityContextTenantAware(authoritiesResolver, securityContextSerializer);
authenticator = new GatewayTokenAuthenticator(
tenantConfigurationManagementMock, tenantAware,
new SystemSecurityContext(tenantAware));
}
@Test
@Description("Tests successful authentication with gateway token")
void testWithGwToken() {
final ControllerSecurityToken securityToken = prepareSecurityToken(GATEWAY_TOKEN);
when(tenantConfigurationManagementMock.getConfigurationValue(
TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY, String.class))
.thenReturn(CONFIG_VALUE_GW_TOKEN);
when(tenantConfigurationManagementMock.getConfigurationValue(
TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_ENABLED, Boolean.class))
.thenReturn(CONFIG_VALUE_ENABLED);
assertThat(authenticator.authenticate(securityToken))
.isNotNull()
.hasFieldOrPropertyWithValue("principal", CONTROLLER_ID);
}
@Test
@Description("Tests that if gateway token doesn't match, the authentication fails")
void testWithBadGwToken() {
final ControllerSecurityToken securityToken = prepareSecurityToken(UNKNOWN_TOKEN);
when(tenantConfigurationManagementMock.getConfigurationValue(
TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY, String.class))
.thenReturn(CONFIG_VALUE_GW_TOKEN);
when(tenantConfigurationManagementMock.getConfigurationValue(
TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_ENABLED, Boolean.class))
.thenReturn(CONFIG_VALUE_ENABLED);
assertThat(authenticator.authenticate(securityToken)).isNull();
}
@Test
@Description("Tests that if gateway token miss, the authentication fails")
void testWithoutGwToken() {
assertThat(authenticator.authenticate(new ControllerSecurityToken("DEFAULT", CONTROLLER_ID))).isNull();
}
@Test
@Description("Tests that if disabled, the authentication fails")
void testWithGwTokenButDisabled() {
final ControllerSecurityToken securityToken = prepareSecurityToken(GATEWAY_TOKEN);
when(tenantConfigurationManagementMock.getConfigurationValue(
TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_ENABLED, Boolean.class))
.thenReturn(CONFIG_VALUE_DISABLED);
assertThat(authenticator.authenticate(securityToken)).isNull();
}
private static ControllerSecurityToken prepareSecurityToken(final String gwToken) {
final ControllerSecurityToken securityToken = new ControllerSecurityToken("DEFAULT", CONTROLLER_ID);
securityToken.putHeader(ControllerSecurityToken.AUTHORIZATION_HEADER, GatewayTokenAuthenticator.GATEWAY_SECURITY_TOKEN_AUTH_SCHEME + gwToken);
return securityToken;
}
}

View File

@@ -12,8 +12,6 @@ package org.eclipse.hawkbit.security.controller;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
import java.util.Collection;
import io.qameta.allure.Description;
import io.qameta.allure.Feature;
import io.qameta.allure.Story;
@@ -31,9 +29,9 @@ import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@Feature("Unit Tests - Security")
@Story("Issuer hash based authentication")
@Story("Security header authenticator")
@ExtendWith(MockitoExtension.class)
class ControllerPreAuthenticatedSecurityHeaderFilterTest {
class SecurityHeaderAuthenticatorTest {
private static final String CA_COMMON_NAME = "ca-cn";
private static final String CA_COMMON_NAME_VALUE = "box1";
@@ -49,11 +47,14 @@ class ControllerPreAuthenticatedSecurityHeaderFilterTest {
private static final TenantConfigurationValue<String> CONFIG_VALUE_SINGLE_HASH = TenantConfigurationValue
.<String> builder().value(SINGLE_HASH).build();
private static final TenantConfigurationValue<String> CONFIG_VALUE_MULTI_HASH = TenantConfigurationValue
.<String> builder().value(MULTI_HASH).build();
private static final TenantConfigurationValue<Boolean> CONFIG_VALUE_ENABLED = TenantConfigurationValue
.<Boolean> builder().value(true).build();
private static final TenantConfigurationValue<Boolean> CONFIG_VALUE_DISABLED = TenantConfigurationValue
.<Boolean> builder().value(false).build();
private ControllerPreAuthenticatedSecurityHeaderFilter underTest;
private Authenticator authenticator;
@Mock
private TenantConfigurationManagement tenantConfigurationManagementMock;
@@ -65,72 +66,88 @@ class ControllerPreAuthenticatedSecurityHeaderFilterTest {
@BeforeEach
void before() {
final SecurityContextTenantAware tenantAware = new SecurityContextTenantAware(authoritiesResolver, securityContextSerializer);
underTest = new ControllerPreAuthenticatedSecurityHeaderFilter(
CA_COMMON_NAME, "X-Ssl-Issuer-Hash-%d",
tenantConfigurationManagementMock, tenantAware, new SystemSecurityContext(tenantAware));
authenticator = new SecurityHeaderAuthenticator(
tenantConfigurationManagementMock, tenantAware,
new SystemSecurityContext(tenantAware), CA_COMMON_NAME, "X-Ssl-Issuer-Hash-%d"
);
}
@Test
@Description("Tests the filter for issuer hash based authentication with a single known hash")
void testIssuerHashBasedAuthenticationWithSingleKnownHash() {
@Description("Tests successful authentication with multiple a single hashes")
void testWithSingleKnownHash() {
final ControllerSecurityToken securityToken = prepareSecurityToken(SINGLE_HASH);
// use single known hash
when(tenantConfigurationManagementMock.getConfigurationValue(
TenantConfigurationKey.AUTHENTICATION_MODE_HEADER_AUTHORITY_NAME, String.class))
.thenReturn(CONFIG_VALUE_SINGLE_HASH);
assertThat(underTest.getPreAuthenticatedPrincipal(securityToken)).isNotNull();
when(tenantConfigurationManagementMock.getConfigurationValue(
TenantConfigurationKey.AUTHENTICATION_MODE_HEADER_ENABLED, Boolean.class))
.thenReturn(CONFIG_VALUE_ENABLED);
assertThat(authenticator.authenticate(securityToken))
.isNotNull()
.hasFieldOrPropertyWithValue("principal", CA_COMMON_NAME_VALUE);
}
@Test
@Description("Tests the filter for issuer hash based authentication with multiple known hashes")
void testIssuerHashBasedAuthenticationWithMultipleKnownHashes() {
// use multiple known hashes
@Description("Tests successful authentication with multiple hashes")
void testWithMultipleKnownHashes() {
when(tenantConfigurationManagementMock.getConfigurationValue(
TenantConfigurationKey.AUTHENTICATION_MODE_HEADER_AUTHORITY_NAME, String.class))
.thenReturn(CONFIG_VALUE_MULTI_HASH);
assertThat(underTest.getPreAuthenticatedPrincipal(prepareSecurityToken(SINGLE_HASH))).isNotNull();
assertThat(underTest.getPreAuthenticatedPrincipal(prepareSecurityToken(SECOND_HASH))).isNotNull();
assertThat(underTest.getPreAuthenticatedPrincipal(prepareSecurityToken(THIRD_HASH))).isNotNull();
when(tenantConfigurationManagementMock.getConfigurationValue(
TenantConfigurationKey.AUTHENTICATION_MODE_HEADER_ENABLED, Boolean.class))
.thenReturn(CONFIG_VALUE_ENABLED);
assertThat(authenticator.authenticate(prepareSecurityToken(SINGLE_HASH)))
.isNotNull()
.hasFieldOrPropertyWithValue("principal", CA_COMMON_NAME_VALUE);
assertThat(authenticator.authenticate(prepareSecurityToken(SECOND_HASH)))
.isNotNull()
.hasFieldOrPropertyWithValue("principal", CA_COMMON_NAME_VALUE);
assertThat(authenticator.authenticate(prepareSecurityToken(THIRD_HASH)))
.isNotNull()
.hasFieldOrPropertyWithValue("principal", CA_COMMON_NAME_VALUE);
}
@Test
@Description("Tests the filter for issuer hash based authentication with unknown hash")
void testIssuerHashBasedAuthenticationWithUnknownHash() {
@Description("Tests that if the hash is unknown, the authentication fails")
void testWithUnknownHash() {
final ControllerSecurityToken securityToken = prepareSecurityToken(UNKNOWN_HASH);
// use single known hash
when(tenantConfigurationManagementMock.getConfigurationValue(
TenantConfigurationKey.AUTHENTICATION_MODE_HEADER_AUTHORITY_NAME, String.class))
.thenReturn(CONFIG_VALUE_MULTI_HASH);
assertThat(underTest.getPreAuthenticatedPrincipal(securityToken)).isNull();
when(tenantConfigurationManagementMock.getConfigurationValue(
TenantConfigurationKey.AUTHENTICATION_MODE_HEADER_ENABLED, Boolean.class))
.thenReturn(CONFIG_VALUE_ENABLED);
assertThat(authenticator.authenticate(securityToken)).isNull();
}
@Test
@Description("Tests different values for issuer hash header and inspects the credentials")
void useDifferentValuesForIssuerHashHeader() {
final ControllerSecurityToken securityToken1 = prepareSecurityToken(SINGLE_HASH);
final ControllerSecurityToken securityToken2 = prepareSecurityToken(SECOND_HASH);
@Description("Tests that if CN doesn't match the CN in the security token, the authentication fails")
void testWithNonMatchingCN() {
final ControllerSecurityToken securityToken = new ControllerSecurityToken("DEFAULT", "otherControllerID");
securityToken.putHeader(CA_COMMON_NAME, CA_COMMON_NAME_VALUE);
securityToken.putHeader(X_SSL_ISSUER_HASH_1, SINGLE_HASH);
final HeaderAuthentication expected1 = new HeaderAuthentication(CA_COMMON_NAME_VALUE, SINGLE_HASH);
final HeaderAuthentication expected2 = new HeaderAuthentication(CA_COMMON_NAME_VALUE, SECOND_HASH);
assertThat(authenticator.authenticate(securityToken)).isNull();
}
@Test
@Description("Tests that if the hash miss, the authentication fails")
void testWithoutHash() {
assertThat(authenticator.authenticate(new ControllerSecurityToken("DEFAULT", CA_COMMON_NAME_VALUE))).isNull();
}
@Test
@Description("Tests that if disabled, the authentication fails")
void testWithSingleKnownHashButDisabled() {
final ControllerSecurityToken securityToken = prepareSecurityToken(SINGLE_HASH);
when(tenantConfigurationManagementMock.getConfigurationValue(
TenantConfigurationKey.AUTHENTICATION_MODE_HEADER_AUTHORITY_NAME, String.class))
.thenReturn(CONFIG_VALUE_MULTI_HASH);
final Collection<HeaderAuthentication> credentials1 = (Collection<HeaderAuthentication>) underTest
.getPreAuthenticatedCredentials(securityToken1);
final Collection<HeaderAuthentication> credentials2 = (Collection<HeaderAuthentication>) underTest
.getPreAuthenticatedCredentials(securityToken2);
final Object principal1 = underTest.getPreAuthenticatedPrincipal(securityToken1);
final Object principal2 = underTest.getPreAuthenticatedPrincipal(securityToken2);
assertThat(credentials1).contains(expected1);
assertThat(credentials2).contains(expected2);
assertThat(expected1).as("hash1 expected in principal!").isEqualTo(principal1);
assertThat(expected2).as("hash2 expected in principal!").isEqualTo(principal2);
TenantConfigurationKey.AUTHENTICATION_MODE_HEADER_ENABLED, Boolean.class))
.thenReturn(CONFIG_VALUE_DISABLED);
assertThat(authenticator.authenticate(securityToken)).isNull();
}
private static ControllerSecurityToken prepareSecurityToken(final String issuerHashHeaderValue) {
@@ -139,5 +156,4 @@ class ControllerPreAuthenticatedSecurityHeaderFilterTest {
securityToken.putHeader(X_SSL_ISSUER_HASH_1, issuerHashHeaderValue);
return securityToken;
}
}

View File

@@ -0,0 +1,122 @@
/**
* 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.security.controller;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
import java.util.Optional;
import io.qameta.allure.Description;
import io.qameta.allure.Feature;
import io.qameta.allure.Story;
import org.eclipse.hawkbit.repository.ControllerManagement;
import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
import org.eclipse.hawkbit.repository.model.Target;
import org.eclipse.hawkbit.repository.model.TenantConfigurationValue;
import org.eclipse.hawkbit.security.SecurityContextSerializer;
import org.eclipse.hawkbit.security.SecurityContextTenantAware;
import org.eclipse.hawkbit.security.SystemSecurityContext;
import org.eclipse.hawkbit.tenancy.UserAuthoritiesResolver;
import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
@Feature("Unit Tests - Security")
@Story("Gateway token authentication")
@ExtendWith(MockitoExtension.class)
class SecurityTokenAuthenticatorTest {
private static final String CONTROLLER_ID = "controllerId_gwtoken";
private static final String SECURITY_TOKEN = "test-sec-token";
private static final String UNKNOWN_TOKEN = "unknown";
private static final TenantConfigurationValue<String> CONFIG_VALUE_GW_TOKEN = TenantConfigurationValue
.<String> builder().value(SECURITY_TOKEN).build();
private static final TenantConfigurationValue<Boolean> CONFIG_VALUE_ENABLED = TenantConfigurationValue
.<Boolean> builder().value(true).build();
private static final TenantConfigurationValue<Boolean> CONFIG_VALUE_DISABLED = TenantConfigurationValue
.<Boolean> builder().value(false).build();
private Authenticator authenticator;
@Mock
private TenantConfigurationManagement tenantConfigurationManagementMock;
@Mock
private ControllerManagement controllerManagementMock;
@Mock
private UserAuthoritiesResolver authoritiesResolver;
@Mock
private SecurityContextSerializer securityContextSerializer;
@BeforeEach
void before() {
final SecurityContextTenantAware tenantAware = new SecurityContextTenantAware(authoritiesResolver, securityContextSerializer);
authenticator = new SecurityTokenAuthenticator(
tenantConfigurationManagementMock, tenantAware,
new SystemSecurityContext(tenantAware), controllerManagementMock);
}
@Test
@Description("Tests successful authentication with gateway token")
void testWithSecToken() {
final ControllerSecurityToken securityToken = prepareSecurityToken(SECURITY_TOKEN);
when(tenantConfigurationManagementMock.getConfigurationValue(
TenantConfigurationKey.AUTHENTICATION_MODE_TARGET_SECURITY_TOKEN_ENABLED, Boolean.class))
.thenReturn(CONFIG_VALUE_ENABLED);
final Target target = Mockito.mock(Target.class);
when(target.getControllerId()).thenReturn(CONTROLLER_ID);
when(target.getSecurityToken()).thenReturn(SECURITY_TOKEN);
when(controllerManagementMock.getByControllerId(CONTROLLER_ID)).thenReturn(Optional.of(target));
assertThat(authenticator.authenticate(securityToken))
.isNotNull()
.hasFieldOrPropertyWithValue("principal", CONTROLLER_ID);
}
@Test
@Description("Tests that if gateway token doesn't match, the authentication fails")
void testWithBadSecToken() {
final ControllerSecurityToken securityToken = prepareSecurityToken(UNKNOWN_TOKEN);
when(tenantConfigurationManagementMock.getConfigurationValue(
TenantConfigurationKey.AUTHENTICATION_MODE_TARGET_SECURITY_TOKEN_ENABLED, Boolean.class))
.thenReturn(CONFIG_VALUE_ENABLED);
assertThat(authenticator.authenticate(securityToken)).isNull();
}
@Test
@Description("Tests that if gateway token miss, the authentication fails")
void testWithoutSecToken() {
assertThat(authenticator.authenticate(new ControllerSecurityToken("DEFAULT", CONTROLLER_ID))).isNull();
}
@Test
@Description("Tests that if disabled, the authentication fails")
void testWithSecTokenButDisabled() {
final ControllerSecurityToken securityToken = prepareSecurityToken(SECURITY_TOKEN);
when(tenantConfigurationManagementMock.getConfigurationValue(
TenantConfigurationKey.AUTHENTICATION_MODE_TARGET_SECURITY_TOKEN_ENABLED, Boolean.class))
.thenReturn(CONFIG_VALUE_DISABLED);
assertThat(authenticator.authenticate(securityToken)).isNull();
}
private static ControllerSecurityToken prepareSecurityToken(final String secToken) {
final ControllerSecurityToken securityToken = new ControllerSecurityToken("DEFAULT", CONTROLLER_ID);
securityToken.putHeader(ControllerSecurityToken.AUTHORIZATION_HEADER, SecurityTokenAuthenticator.TARGET_SECURITY_TOKEN_AUTH_SCHEME + secToken);
return securityToken;
}
}

View File

@@ -25,7 +25,7 @@
<!-- hawkBit - START -->
<dependency>
<groupId>org.eclipse.hawkbit</groupId>
<artifactId>hawkbit-security-controller</artifactId>
<artifactId>hawkbit-ddi-security</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>

View File

@@ -12,10 +12,6 @@ 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.repository.ControllerManagement;
import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
@@ -25,7 +21,15 @@ 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.security.controller.AuthenticationFilters;
import org.eclipse.hawkbit.security.controller.Authenticator;
import org.eclipse.hawkbit.security.controller.ControllerSecurityToken;
import org.eclipse.hawkbit.security.controller.GatewayTokenAuthenticator;
import org.eclipse.hawkbit.security.controller.SecurityHeaderAuthenticator;
import org.eclipse.hawkbit.security.controller.SecurityTokenAuthenticator;
import org.eclipse.hawkbit.tenancy.TenantAware;
import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
@@ -33,11 +37,12 @@ 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.Authentication;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.intercept.AuthorizationFilter;
/**
* Security configuration for the hawkBit server DDI download interface.
@@ -89,8 +94,6 @@ class ControllerDownloadSecurityConfiguration {
@Bean
@Order(300) // higher priority than HawkBit DDI security, so that the DDI DL security is applied first
protected SecurityFilterChain filterChainDDIDL(final HttpSecurity http) throws Exception {
final AuthenticationManager authenticationManager = ControllerSecurityConfiguration.setAuthenticationManager(http, ddiSecurityConfiguration);
http
.securityMatcher(DDI_DL_ANT_MATCHER)
.csrf(AbstractHttpConfigurer::disable);
@@ -99,34 +102,27 @@ class ControllerDownloadSecurityConfiguration {
http.requiresChannel(crmRegistry -> crmRegistry.anyRequest().requiresSecure());
}
final ControllerTenantAwareAuthenticationDetailsSource authenticationDetailsSource = new ControllerTenantAwareAuthenticationDetailsSource();
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)
.addFilterBefore(new AuthenticationFilters.SecurityHeaderAuthenticationFilter(
new SecurityHeaderAuthenticator(
tenantConfigurationManagement, tenantAware, systemSecurityContext,
ddiSecurityConfiguration.getRp().getCnHeader(), ddiSecurityConfiguration.getRp().getSslIssuerHashHeader()),
ddiSecurityConfiguration), AuthorizationFilter.class)
.addFilterBefore(new AuthenticationFilters.SecurityTokenAuthenticationFilter(
new SecurityTokenAuthenticator(
tenantConfigurationManagement, tenantAware, systemSecurityContext,
controllerManagement),
ddiSecurityConfiguration), AuthorizationFilter.class)
.addFilterBefore(new AuthenticationFilters.GatewayTokenAuthenticationFilter(
new GatewayTokenAuthenticator(
tenantConfigurationManagement, tenantAware, systemSecurityContext),
ddiSecurityConfiguration), AuthorizationFilter.class)
.addFilterBefore(new AuthenticationFilters.AbstractAuthenticationFilter(
new AnonymousAuthenticator(
tenantConfigurationManagement, tenantAware, systemSecurityContext),
ddiSecurityConfiguration) {}, AuthorizationFilter.class)
.exceptionHandling(configurer -> configurer.authenticationEntryPoint(
(request, response, authException) -> response.setStatus(HttpStatus.UNAUTHORIZED.value())))
.sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
@@ -135,4 +131,29 @@ class ControllerDownloadSecurityConfiguration {
return http.build();
}
@Slf4j
private static class AnonymousAuthenticator extends Authenticator.AbstractAuthenticator {
protected AnonymousAuthenticator(
final TenantConfigurationManagement tenantConfigurationManagement,
final TenantAware tenantAware, final SystemSecurityContext systemSecurityContext) {
super(tenantConfigurationManagement, tenantAware, systemSecurityContext);
}
@Override
public Authentication authenticate(final ControllerSecurityToken controllerSecurityToken) {
return isEnabled(controllerSecurityToken) ? authenticatedController(controllerSecurityToken.getTenant(), null) : null;
}
@Override
protected String getTenantConfigurationKey() {
return TenantConfigurationProperties.TenantConfigurationKey.ANONYMOUS_DOWNLOAD_MODE_ENABLED;
}
@Override
public Logger log() {
return log;
}
}
}

View File

@@ -12,10 +12,6 @@ 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.repository.ControllerManagement;
import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
@@ -25,7 +21,10 @@ 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.security.controller.PreAuthTokenSourceTrustAuthenticationProvider;
import org.eclipse.hawkbit.security.controller.AuthenticationFilters;
import org.eclipse.hawkbit.security.controller.GatewayTokenAuthenticator;
import org.eclipse.hawkbit.security.controller.SecurityHeaderAuthenticator;
import org.eclipse.hawkbit.security.controller.SecurityTokenAuthenticator;
import org.eclipse.hawkbit.tenancy.TenantAware;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@@ -34,13 +33,12 @@ 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.web.SecurityFilterChain;
import org.springframework.security.web.access.intercept.AuthorizationFilter;
/**
* Security configuration for the hawkBit server DDI interface.
@@ -93,8 +91,6 @@ class ControllerSecurityConfiguration {
@Bean
@Order(301)
protected SecurityFilterChain filterChainDDI(final HttpSecurity http) throws Exception {
final AuthenticationManager authenticationManager = setAuthenticationManager(http, ddiSecurityConfiguration);
http
.securityMatcher(DDI_ANT_MATCHERS)
.csrf(AbstractHttpConfigurer::disable);
@@ -103,34 +99,22 @@ class ControllerSecurityConfiguration {
http.requiresChannel(crmRegistry -> crmRegistry.anyRequest().requiresSecure());
}
final ControllerTenantAwareAuthenticationDetailsSource authenticationDetailsSource = new ControllerTenantAwareAuthenticationDetailsSource();
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)
.addFilterBefore(new AuthenticationFilters.SecurityHeaderAuthenticationFilter(
new SecurityHeaderAuthenticator(
tenantConfigurationManagement, tenantAware,
systemSecurityContext, ddiSecurityConfiguration.getRp().getCnHeader(), ddiSecurityConfiguration.getRp().getSslIssuerHashHeader()
), ddiSecurityConfiguration), AuthorizationFilter.class)
.addFilterBefore(new AuthenticationFilters.SecurityTokenAuthenticationFilter(
new SecurityTokenAuthenticator(
tenantConfigurationManagement, tenantAware,
systemSecurityContext, controllerManagement), ddiSecurityConfiguration), AuthorizationFilter.class)
.addFilterBefore(new AuthenticationFilters.GatewayTokenAuthenticationFilter(
new GatewayTokenAuthenticator(
tenantConfigurationManagement, tenantAware,
systemSecurityContext), ddiSecurityConfiguration), AuthorizationFilter.class)
.exceptionHandling(configurer -> configurer.authenticationEntryPoint(
(request, response, authException) -> response.setStatus(HttpStatus.UNAUTHORIZED.value())))
.sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
@@ -139,17 +123,4 @@ class ControllerSecurityConfiguration {
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

@@ -1,182 +0,0 @@
/**
* 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.SystemSecurityContext;
import org.eclipse.hawkbit.security.controller.ControllerSecurityToken;
import org.eclipse.hawkbit.security.controller.PreAuthenticationFilter;
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 ControllerSecurityToken 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 ControllerSecurityToken securityToken = createTenantSecurityTokenVariables(request);
if (securityToken == null) {
return null;
}
return abstractControllerAuthenticationFilter.getPreAuthenticatedPrincipal(securityToken);
}
@Override
protected Object getPreAuthenticatedCredentials(final HttpServletRequest request) {
final ControllerSecurityToken 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 ControllerSecurityToken} or {@code null} if the
* request does not match the pattern and no variables could be
* extracted
*/
protected ControllerSecurityToken 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 ControllerSecurityToken createTenantSecurityTokenVariables(final HttpServletRequest request,
final String tenant, final String controllerId) {
final ControllerSecurityToken securityToken = new ControllerSecurityToken(tenant, null, controllerId, null);
Collections.list(request.getHeaderNames()).forEach(header -> securityToken.putHeader(header, request.getHeader(header)));
return securityToken;
}
}

View File

@@ -1,65 +0,0 @@
/**
* 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.controller.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

@@ -1,67 +0,0 @@
/**
* 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.SystemSecurityContext;
import org.eclipse.hawkbit.security.controller.ControllerPreAuthenticatedSecurityTokenFilter;
import org.eclipse.hawkbit.security.controller.PreAuthenticationFilter;
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 ControllerPreAuthenticatedSecurityTokenFilter(tenantConfigurationManagement, controllerManagement,
tenantAware, systemSecurityContext);
}
@Override
protected Logger log() {
return log;
}
}

View File

@@ -1,56 +0,0 @@
/**
* 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.SystemSecurityContext;
import org.eclipse.hawkbit.security.controller.ControllerPreAuthenticatedGatewaySecurityTokenFilter;
import org.eclipse.hawkbit.security.controller.PreAuthenticationFilter;
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

@@ -1,66 +0,0 @@
/**
* 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.SystemSecurityContext;
import org.eclipse.hawkbit.security.controller.ControllerPreAuthenticatedSecurityHeaderFilter;
import org.eclipse.hawkbit.security.controller.PreAuthenticationFilter;
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,144 +0,0 @@
/**
* 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.controller.PreAuthTokenSourceTrustAuthenticationProvider;
import org.eclipse.hawkbit.security.controller.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)
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.")
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.")
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.")
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.")
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.")
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.")
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));
}
}

View File

@@ -25,6 +25,7 @@
<modules>
<module>hawkbit-ddi-api</module>
<module>hawkbit-ddi-resource</module>
<module>hawkbit-ddi-security</module>
<module>hawkbit-ddi-starter</module>
<module>hawkbit-ddi-server</module>
</modules>

View File

@@ -34,7 +34,7 @@
</dependency>
<dependency>
<groupId>org.eclipse.hawkbit</groupId>
<artifactId>hawkbit-security-controller</artifactId>
<artifactId>hawkbit-security-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>

View File

@@ -25,7 +25,7 @@
<!-- hawkBit - START -->
<dependency>
<groupId>org.eclipse.hawkbit</groupId>
<artifactId>hawkbit-security-controller</artifactId>
<artifactId>hawkbit-security-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>

View File

@@ -14,7 +14,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.eclipse.hawkbit</groupId>
<artifactId>hawkbit-security-parent</artifactId>
<artifactId>hawkbit-parent</artifactId>
<version>${revision}</version>
</parent>

View File

@@ -1,67 +0,0 @@
/**
* 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.security.controller;
import java.util.Arrays;
import java.util.Collection;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions;
import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
import org.eclipse.hawkbit.security.SystemSecurityContext;
import org.eclipse.hawkbit.tenancy.TenantAware;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
/**
* An abstraction for all controller based security. Check if the tenant
* configuration is enabled.
*/
@Slf4j
public abstract class AbstractControllerAuthenticationFilter implements PreAuthenticationFilter {
protected final TenantConfigurationManagement tenantConfigurationManagement;
protected final TenantAware tenantAware;
protected final SystemSecurityContext systemSecurityContext;
private final SecurityConfigurationKeyTenantRunner configurationKeyTenantRunner;
protected AbstractControllerAuthenticationFilter(
final TenantConfigurationManagement systemManagement, final TenantAware tenantAware,
final SystemSecurityContext systemSecurityContext) {
this.tenantConfigurationManagement = systemManagement;
this.tenantAware = tenantAware;
this.systemSecurityContext = systemSecurityContext;
this.configurationKeyTenantRunner = new SecurityConfigurationKeyTenantRunner();
}
@Override
public boolean isEnable(final ControllerSecurityToken securityToken) {
return tenantAware.runAsTenant(securityToken.getTenant(), configurationKeyTenantRunner);
}
@Override
public Collection<GrantedAuthority> getSuccessfulAuthenticationAuthorities() {
return Arrays.asList(new SimpleGrantedAuthority(SpringEvalExpressions.CONTROLLER_ROLE));
}
protected abstract String getTenantConfigurationKey();
private final class SecurityConfigurationKeyTenantRunner implements TenantAware.TenantRunner<Boolean> {
@Override
public Boolean run() {
log.trace("retrieving configuration value for configuration key {}", getTenantConfigurationKey());
return systemSecurityContext.runAsSystem(() -> tenantConfigurationManagement
.getConfigurationValue(getTenantConfigurationKey(), Boolean.class).getValue());
}
}
}

View File

@@ -1,95 +0,0 @@
/**
* 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.security.controller;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
import org.eclipse.hawkbit.security.SystemSecurityContext;
import org.eclipse.hawkbit.tenancy.TenantAware;
import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey;
/**
* An pre-authenticated processing filter which extracts (if enabled through
* configuration) the possibility to authenticate a target based through a
* gateway security token. This is commonly used for targets connected
* indirectly via a gateway. This gateway controls multiple targets under the
* gateway security token which can be set via the {@code TenantsecurityToken}
* header. {@code Example Header: Authorization: GatewayToken
* 5d8fSD54fdsFG98DDsa.}
*/
@Slf4j
public class ControllerPreAuthenticatedGatewaySecurityTokenFilter extends AbstractControllerAuthenticationFilter {
private static final String GATEWAY_SECURITY_TOKEN_AUTH_SCHEME = "GatewayToken ";
private static final int OFFSET_GATEWAY_TOKEN = GATEWAY_SECURITY_TOKEN_AUTH_SCHEME.length();
private final GetGatewaySecurityConfigurationKeyTenantRunner gatewaySecurityTokenKeyConfigRunner = new GetGatewaySecurityConfigurationKeyTenantRunner();
/**
* Constructor.
*
* @param tenantConfigurationManagement the tenant 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 to get access to tenant
* configuration
*/
public ControllerPreAuthenticatedGatewaySecurityTokenFilter(
final TenantConfigurationManagement tenantConfigurationManagement, final TenantAware tenantAware,
final SystemSecurityContext systemSecurityContext) {
super(tenantConfigurationManagement, tenantAware, systemSecurityContext);
}
@Override
public HeaderAuthentication getPreAuthenticatedPrincipal(final ControllerSecurityToken securityToken) {
final String authHeader = securityToken.getHeader(ControllerSecurityToken.AUTHORIZATION_HEADER);
if (authHeader != null &&
authHeader.startsWith(GATEWAY_SECURITY_TOKEN_AUTH_SCHEME) &&
authHeader.length() > OFFSET_GATEWAY_TOKEN) { // disables empty string token
log.debug("found authorization header with scheme {} using target security token for authentication",
GATEWAY_SECURITY_TOKEN_AUTH_SCHEME);
return new HeaderAuthentication(securityToken.getControllerId(),
authHeader.substring(OFFSET_GATEWAY_TOKEN));
}
log.debug(
"security token filter is enabled but request does not contain either the necessary security token {} or the authorization header with scheme {}",
securityToken, GATEWAY_SECURITY_TOKEN_AUTH_SCHEME);
return null;
}
@Override
public HeaderAuthentication getPreAuthenticatedCredentials(final ControllerSecurityToken securityToken) {
final String gatewayToken = tenantAware.runAsTenant(securityToken.getTenant(),
gatewaySecurityTokenKeyConfigRunner);
return new HeaderAuthentication(securityToken.getControllerId(), gatewayToken);
}
@Override
protected String getTenantConfigurationKey() {
return TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_ENABLED;
}
private final class GetGatewaySecurityConfigurationKeyTenantRunner implements TenantAware.TenantRunner<String> {
@Override
public String run() {
log.trace("retrieving configuration value for configuration key {}",
TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY);
return systemSecurityContext
.runAsSystem(() -> tenantConfigurationManagement
.getConfigurationValue(
TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY, String.class)
.getValue());
}
}
}

View File

@@ -1,157 +0,0 @@
/**
* 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.security.controller;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
import org.eclipse.hawkbit.security.SystemSecurityContext;
import org.eclipse.hawkbit.tenancy.TenantAware;
import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A pre-authenticated processing filter which extracts the principal from a
* request URI and the credential from a request header in a the
* {@link ControllerSecurityToken}.
*/
@Slf4j
public class ControllerPreAuthenticatedSecurityHeaderFilter extends AbstractControllerAuthenticationFilter {
private static final Logger LOG_SECURITY_AUTH = LoggerFactory.getLogger("server-security.authentication");
private final GetSecurityAuthorityNameTenantRunner sslIssuerNameConfigTenantRunner = new GetSecurityAuthorityNameTenantRunner();
// Example Headers with Cert Information
// Clientip: 217.24.201.180
// X-Forwarded-Proto: https
// X-Ssl-Client-Cn: my.name
// X-Ssl-Client-Dn:
// CN=my.name,CN=O,CN=R,CN=DE,CN=BOSCH,CN=pki,DC=bosch,DC=com
// X-Ssl-Client-Hash: 7f:87:cb:b5:9c:e0:c5:0a:1a:a6:57:69:0f:ca:0a:95
// X-Ssl-Client-Notafter: Dec 18 08:02:45 2017 GMT
// X-Ssl-Client-Notbefore: Dec 18 07:32:45 2014 GMT
// X-Ssl-Client-Verify: ok
// X-Ssl-Issuer: CN=Bosch-CA1-DE,CN=PKI,DC=Bosch,DC=com
// X-Ssl-Issuer-Dn-1: CN=Bosch-CA-DE,CN=PKI,DC=Bosch,DC=com
// X-Ssl-Issuer-Hash-1: ae:11:f5:6a:0a:e8:74:50:81:0e:0c:37:ec:c5:22:fc
private final String caCommonNameHeader;
// the X-Ssl-Issuer-Hash basic header: Contains the x509 fingerprint hash, this
// header exists multiple times in the request for all trusted chains.
private final String sslIssuerHashBasicHeader;
/**
* Constructor.
*
* @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 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 to get access to tenant
* configuration
*/
public ControllerPreAuthenticatedSecurityHeaderFilter(final String caCommonNameHeader,
final String caAuthorityNameHeader, final TenantConfigurationManagement tenantConfigurationManagement,
final TenantAware tenantAware, final SystemSecurityContext systemSecurityContext) {
super(tenantConfigurationManagement, tenantAware, systemSecurityContext);
this.caCommonNameHeader = caCommonNameHeader;
this.sslIssuerHashBasicHeader = caAuthorityNameHeader;
}
@Override
public HeaderAuthentication getPreAuthenticatedPrincipal(final ControllerSecurityToken securityToken) {
// retrieve the common name header and the authority name header from
// the http request and combine them together
final String commonNameValue = securityToken.getHeader(caCommonNameHeader);
final String knownSslIssuerConfigurationValue = tenantAware.runAsTenant(securityToken.getTenant(),
sslIssuerNameConfigTenantRunner);
final String sslIssuerHashValue = getIssuerHashHeader(securityToken, knownSslIssuerConfigurationValue);
if (commonNameValue != null && log.isTraceEnabled()) {
log.trace("Found commonNameHeader {}={}, using as credentials", caCommonNameHeader, commonNameValue);
}
if (sslIssuerHashValue != null && log.isTraceEnabled()) {
log.trace("Found sslIssuerHash ****, using as credentials for tenant {}", securityToken.getTenant());
}
if (commonNameValue != null && sslIssuerHashValue != null) {
return new HeaderAuthentication(commonNameValue, sslIssuerHashValue);
}
return null;
}
@Override
public Object getPreAuthenticatedCredentials(final ControllerSecurityToken securityToken) {
final String authorityNameConfigurationValue = tenantAware.runAsTenant(securityToken.getTenant(),
sslIssuerNameConfigTenantRunner);
// in case of legacy download artifact, the controller ID is not in the
// URL path, so then we just use the common name header
final String controllerId = //
(securityToken.getControllerId() == null || "anonymous".equals(securityToken.getControllerId()) //
? securityToken.getHeader(caCommonNameHeader)
: securityToken.getControllerId());
final List<String> knownHashes = splitMultiHashBySemicolon(authorityNameConfigurationValue);
return knownHashes.stream().map(hashItem -> new HeaderAuthentication(controllerId, hashItem)).collect(Collectors.toSet());
}
@Override
protected String getTenantConfigurationKey() {
return TenantConfigurationKey.AUTHENTICATION_MODE_HEADER_ENABLED;
}
private static List<String> splitMultiHashBySemicolon(final String knownIssuerHashes) {
return Arrays.stream(knownIssuerHashes.split("[;,]")).map(String::toLowerCase).toList();
}
/**
* Iterates over the {@link #sslIssuerHashBasicHeader} basic header
* {@code X-Ssl-Issuer-Hash-%d} and try to find the same hash as known. It's ok
* if we find the hash in any the trusted CA chain to accept this request for
* this tenant.
*/
@SuppressWarnings("java:S2629") // check if debug is enabled is maybe heavier then evaluation
private String getIssuerHashHeader(final ControllerSecurityToken securityToken, final String knownIssuerHashes) {
// there may be several knownIssuerHashes configured for the tenant
final List<String> knownHashes = splitMultiHashBySemicolon(knownIssuerHashes);
// iterate over the headers until we get a null header.
int iHeader = 1;
String foundHash;
while ((foundHash = securityToken.getHeader(String.format(sslIssuerHashBasicHeader, iHeader))) != null) {
if (knownHashes.contains(foundHash.toLowerCase())) {
if (log.isTraceEnabled()) {
log.trace("Found matching ssl issuer hash at position {}", iHeader);
}
return foundHash.toLowerCase();
}
iHeader++;
}
LOG_SECURITY_AUTH.debug(
"Certificate request but no matching hash found in headers {} for common name {} in request",
sslIssuerHashBasicHeader, securityToken.getHeader(caCommonNameHeader));
return null;
}
private final class GetSecurityAuthorityNameTenantRunner implements TenantAware.TenantRunner<String> {
@Override
public String run() {
return systemSecurityContext.runAsSystem(() -> tenantConfigurationManagement.getConfigurationValue(
TenantConfigurationKey.AUTHENTICATION_MODE_HEADER_AUTHORITY_NAME, String.class).getValue());
}
}
}

View File

@@ -1,99 +0,0 @@
/**
* 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.security.controller;
import java.util.Optional;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.repository.ControllerManagement;
import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
import org.eclipse.hawkbit.repository.model.Target;
import org.eclipse.hawkbit.security.SystemSecurityContext;
import org.eclipse.hawkbit.tenancy.TenantAware;
import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey;
/**
* 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.}
*/
@Slf4j
public class ControllerPreAuthenticatedSecurityTokenFilter extends AbstractControllerAuthenticationFilter {
private static final String TARGET_SECURITY_TOKEN_AUTH_SCHEME = "TargetToken ";
private static final int OFFSET_TARGET_TOKEN = TARGET_SECURITY_TOKEN_AUTH_SCHEME.length();
private final ControllerManagement controllerManagement;
/**
* Constructor.
*
* @param tenantConfigurationManagement the tenant management service to retrieve configuration
* properties
* @param controllerManagement the controller management to retrieve the specific target
* security token to verify
* @param tenantAware the tenant aware service to get configuration for the specific
* tenant
* @param systemSecurityContext the system security context to get access to tenant
* configuration
*/
public ControllerPreAuthenticatedSecurityTokenFilter(
final TenantConfigurationManagement tenantConfigurationManagement,
final ControllerManagement controllerManagement, final TenantAware tenantAware,
final SystemSecurityContext systemSecurityContext) {
super(tenantConfigurationManagement, tenantAware, systemSecurityContext);
this.controllerManagement = controllerManagement;
}
@Override
public HeaderAuthentication getPreAuthenticatedPrincipal(final ControllerSecurityToken securityToken) {
final String controllerId = resolveControllerId(securityToken);
final String authHeader = securityToken.getHeader(ControllerSecurityToken.AUTHORIZATION_HEADER);
if ((authHeader != null) && authHeader.startsWith(TARGET_SECURITY_TOKEN_AUTH_SCHEME)) {
log.debug("found authorization header with scheme {} using target security token for authentication",
TARGET_SECURITY_TOKEN_AUTH_SCHEME);
return new HeaderAuthentication(controllerId, authHeader.substring(OFFSET_TARGET_TOKEN));
}
log.debug(
"security token filter is enabled but requst does not contain either the necessary path variables {} or the authorization header with scheme {}",
securityToken, TARGET_SECURITY_TOKEN_AUTH_SCHEME);
return null;
}
@Override
public HeaderAuthentication getPreAuthenticatedCredentials(final ControllerSecurityToken securityToken) {
final Optional<Target> target = systemSecurityContext.runAsSystemAsTenant(() -> {
if (securityToken.getTargetId() != null) {
return controllerManagement.get(securityToken.getTargetId());
}
return controllerManagement.getByControllerId(securityToken.getControllerId());
}, securityToken.getTenant());
return target.map(t -> new HeaderAuthentication(
t.getControllerId(), systemSecurityContext.runAsSystemAsTenant(t::getSecurityToken, securityToken.getTenant())))
.orElse(null);
}
@Override
protected String getTenantConfigurationKey() {
return TenantConfigurationKey.AUTHENTICATION_MODE_TARGET_SECURITY_TOKEN_ENABLED;
}
private String resolveControllerId(final ControllerSecurityToken securityToken) {
if (securityToken.getControllerId() != null) {
return securityToken.getControllerId();
}
final Optional<Target> foundTarget = systemSecurityContext.runAsSystemAsTenant(
() -> controllerManagement.get(securityToken.getTargetId()), securityToken.getTenant());
return foundTarget.map(Target::getControllerId).orElse(null);
}
}

View File

@@ -1,73 +0,0 @@
/**
* 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.security.controller;
/**
* The authentication principal and credentials object which holds the
* controller-id and the authority name from the http-headers as principal or
* from the http-url and tenant configuration for the credentials.
*/
final class HeaderAuthentication {
private final String controllerId;
private final String headerAuth;
HeaderAuthentication(final String controllerId, final String headerAuth) {
this.controllerId = controllerId;
this.headerAuth = headerAuth;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((controllerId == null) ? 0 : controllerId.hashCode());
result = prime * result + ((headerAuth == null) ? 0 : headerAuth.hashCode());
return result;
}
@Override
public boolean equals(final Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final HeaderAuthentication other = (HeaderAuthentication) obj;
if (controllerId == null) {
if (other.controllerId != null) {
return false;
}
} else if (!controllerId.equals(other.controllerId)) {
return false;
}
if (headerAuth == null) {
if (other.headerAuth != null) {
return false;
}
} else if (!headerAuth.equals(other.headerAuth)) {
return false;
}
return true;
}
@Override
public String toString() {
// only the controller ID because the principal is stored as string for
// audit information
// etc.
return controllerId;
}
}

View File

@@ -1,173 +0,0 @@
/**
* 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.security.controller;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
/**
* An spring authentication provider which supports authentication tokens of
* type {@link PreAuthenticatedAuthenticationToken} created by the
* {@link ControllerPreAuthenticatedSecurityHeaderFilter}.
*
* Additionally to the authentication token providing the principal and the
* credentials which must be match, this authentication provider can also check
* the remote IP address of the request.
*
* E.g. The request path is /controller/v1/{controllerId} then the controllerId
* in the path is the principal. The credentials are the extracted information
* from e.g. a certificate provided by an reverse proxy. Due this request is
* only allowed from a specific source address this authentication manager can
* also check the remote IP address of the request.
*/
@Slf4j
public class PreAuthTokenSourceTrustAuthenticationProvider implements AuthenticationProvider {
private final List<String> authorizedSourceIps;
/**
* Creates a new PreAuthTokenSourceTrustAuthenticationProvider without
* source IPs, which disables the source IP check.
*/
public PreAuthTokenSourceTrustAuthenticationProvider() {
authorizedSourceIps = null;
}
/**
* Creates a new PreAuthTokenSourceTrustAuthenticationProvider with given
* source IP addresses which are trusted and should be checked against the
* request remote IP address.
*
* @param authorizedSourceIps a list of IP addresses.
*/
public PreAuthTokenSourceTrustAuthenticationProvider(final List<String> authorizedSourceIps) {
this.authorizedSourceIps = authorizedSourceIps;
}
/**
* Creates a new PreAuthTokenSourceTrustAuthenticationProvider with given
* source IP addresses which are trusted and should be checked against the
* request remote IP address.
*
* @param authorizedSourceIps a list of IP addresses.
*/
public PreAuthTokenSourceTrustAuthenticationProvider(final String... authorizedSourceIps) {
this.authorizedSourceIps = new ArrayList<>();
this.authorizedSourceIps.addAll(Arrays.asList(authorizedSourceIps));
}
@Override
public Authentication authenticate(final Authentication authentication) {
if (!supports(authentication.getClass())) {
return null;
}
final PreAuthenticatedAuthenticationToken token = (PreAuthenticatedAuthenticationToken) authentication;
final Object credentials = token.getCredentials();
final Object principal = token.getPrincipal();
final Object tokenDetails = token.getDetails();
final Collection<GrantedAuthority> authorities = token.getAuthorities();
if (principal == null) {
throw new BadCredentialsException("The provided principal and credentials are not match");
}
final boolean successAuthentication = calculateAuthenticationSuccess(principal, credentials, tokenDetails);
if (successAuthentication) {
final PreAuthenticatedAuthenticationToken successToken = new PreAuthenticatedAuthenticationToken(principal,
credentials, authorities);
successToken.setDetails(tokenDetails);
return successToken;
}
throw new BadCredentialsException("The provided principal and credentials are not match");
}
@Override
public boolean supports(final Class<?> authentication) {
return PreAuthenticatedAuthenticationToken.class.isAssignableFrom(authentication);
}
/**
* The credentials may either be of type HeaderAuthentication or of type
* Collection<HeaderAuthentication> depending on the authentication mode in
* use (the latter is used in case of trusted reverse-proxy). It is checked
* whether principal equals credentials (respectively if credentials
* contains principal in case of collection) because we want to check if
* e.g. controllerId containing in the URL equals the controllerId in the
* special header set by the reverse-proxy which extracted the CN from the
* certificate.
*
* @param principal the {@link HeaderAuthentication} from the header
* @param credentials a single {@link HeaderAuthentication} or a Collection of
* HeaderAuthentication
* @param tokenDetails authentication details
* @return <code>true</code> if authentication succeeded, otherwise
* <code>false</code>
*/
private boolean calculateAuthenticationSuccess(final Object principal, final Object credentials,
final Object tokenDetails) {
boolean successAuthentication = false;
if (credentials instanceof Collection) {
final Collection<?> multiValueCredentials = (Collection<?>) credentials;
if (multiValueCredentials.contains(principal)) {
successAuthentication = checkSourceIPAddressIfNeccessary(tokenDetails);
}
} else if (principal.equals(credentials)) {
successAuthentication = checkSourceIPAddressIfNeccessary(tokenDetails);
}
return successAuthentication;
}
private boolean checkSourceIPAddressIfNeccessary(final Object tokenDetails) {
boolean success = authorizedSourceIps == null;
String remoteAddress = null;
// controllerIds in URL path and request header are the same but is the
// request coming
// from a trustful source, like the reverse proxy.
if (authorizedSourceIps != null) {
if (tokenDetails instanceof TenantAwareWebAuthenticationDetails tenantAwareWebAuthenticationDetails) {
remoteAddress = tenantAwareWebAuthenticationDetails.getRemoteAddress();
if (authorizedSourceIps.contains(remoteAddress)) {
// source ip matches the given pattern -> authenticated
success = true;
}
} else {
// is not of type WebAuthenticationDetails, then we cannot
// determine the remote address!
log.error(
"Cannot determine the controller remote-ip-address based on the given authentication token - {} , token details are not TenantAwareWebAuthenticationDetails! ",
tokenDetails);
success = false;
}
}
if (!success) {
throw new InsufficientAuthenticationException("The remote source IP address " + remoteAddress
+ " is not in the list of trusted IP addresses " + authorizedSourceIps);
}
// no trusted IP check, because no authorizedSourceIPs configuration
return true;
}
}

View File

@@ -1,58 +0,0 @@
/**
* 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.security.controller;
import java.util.Collection;
import java.util.Collections;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
/**
* Interface for Pre Authentication.
*/
public interface PreAuthenticationFilter {
/**
* Check if the filter is enabled.
*
* @param securityToken the secruity info
* @return <code>true</code> is enabled <code>false</code> diabled
*/
boolean isEnable(ControllerSecurityToken securityToken);
/**
* Extract the principal information from the current securityToken.
*
* @param securityToken the securityToken
* @return the extracted tenant and controller id
*/
HeaderAuthentication getPreAuthenticatedPrincipal(ControllerSecurityToken securityToken);
/**
* Extract the principal credentials from the current securityToken.
*
* @param securityToken the securityToken
* @return the extracted tenant and controller id
*/
Object getPreAuthenticatedCredentials(ControllerSecurityToken securityToken);
/**
* Allows to add additional authorities to the successful authenticated token.
*
* @return the authorities granted to the principal, or an empty collection if
* the token has not been authenticated. Never null.
* @see Authentication#getAuthorities()
*/
default Collection<GrantedAuthority> getSuccessfulAuthenticationAuthorities() {
return Collections.emptyList();
}
}

View File

@@ -1,50 +0,0 @@
/**
* 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.security.controller;
import java.io.Serial;
import jakarta.servlet.http.HttpServletRequest;
import org.eclipse.hawkbit.tenancy.TenantAwareAuthenticationDetails;
/**
* Extends the {@link TenantAwareAuthenticationDetails} to web information to
* retrieve also e.g. the remoteAddress of the {@link HttpServletRequest} when
* authenticating the requested controller e.g. based on the security header and
* trusted IP address we need the remote address of the http request to verify
* the e.g. the reverse proxy is trusted and allowed to set the header.
*/
public class TenantAwareWebAuthenticationDetails extends TenantAwareAuthenticationDetails {
@Serial
private static final long serialVersionUID = 1L;
private final String remoteAddress;
/**
* @param tenant the current tenant
* @param remoteAddress the remote address of this web request
* @param controller {@code true} indicates this is an controller HTTP request
* otherwise {@code false}.
*/
public TenantAwareWebAuthenticationDetails(final String tenant, final String remoteAddress,
final boolean controller) {
super(tenant, controller);
this.remoteAddress = remoteAddress;
}
/**
* @return the remoteAddress
*/
public String getRemoteAddress() {
return remoteAddress;
}
}

View File

@@ -1,29 +0,0 @@
<!--
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
-->
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.eclipse.hawkbit</groupId>
<artifactId>hawkbit-parent</artifactId>
<version>${revision}</version>
</parent>
<artifactId>hawkbit-security-parent</artifactId>
<name>hawkBit :: Security :: Parent</name>
<packaging>pom</packaging>
<modules>
<module>hawkbit-security-core</module>
<module>hawkbit-security-controller</module>
</modules>
</project>

View File

@@ -34,7 +34,7 @@
</dependency>
<dependency>
<groupId>org.eclipse.hawkbit</groupId>
<artifactId>hawkbit-security-controller</artifactId>
<artifactId>hawkbit-ddi-security</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>

View File

@@ -132,10 +132,10 @@
<modules>
<module>hawkbit-core</module>
<module>hawkbit-rest-core</module>
<module>hawkbit-security</module>
<module>hawkbit-security-core</module>
<module>hawkbit-artifact</module>
<module>hawkbit-repository</module>
<module>hawkbit-rest-core</module>
<module>hawkbit-autoconfigure</module>
<module>hawkbit-mgmt</module>
@@ -144,7 +144,6 @@
<module>hawkbit-monolith</module>
<module>hawkbit-simple-ui</module>
<module>hawkbit-sdk</module>
<module>hawkbit-test-report</module>