Cleanup and improve the controller authentication (#2287)
Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* 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.Map;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude.Include;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* JSON representation to authenticate a tenant.
|
||||
*/
|
||||
@Data
|
||||
@JsonInclude(Include.NON_NULL)
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class ControllerSecurityToken {
|
||||
|
||||
public static final String AUTHORIZATION_HEADER = "Authorization";
|
||||
|
||||
@JsonProperty
|
||||
private final Long tenantId;
|
||||
@JsonProperty
|
||||
private final Long targetId;
|
||||
@JsonProperty
|
||||
private final String controllerId;
|
||||
@JsonProperty
|
||||
private String tenant;
|
||||
@JsonProperty
|
||||
private Map<String, String> headers;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param tenant the tenant for the security token
|
||||
* @param tenantId alternative tenant identification by technical ID
|
||||
* @param controllerId the ID of the controller for the security token
|
||||
* @param targetId alternative target identification by technical ID
|
||||
*/
|
||||
@JsonCreator
|
||||
public ControllerSecurityToken(
|
||||
@JsonProperty("tenant") final String tenant,
|
||||
@JsonProperty("tenantId") final Long tenantId, @JsonProperty("controllerId") final String controllerId,
|
||||
@JsonProperty("targetId") final Long targetId) {
|
||||
this.tenant = tenant;
|
||||
this.tenantId = tenantId;
|
||||
this.controllerId = controllerId;
|
||||
this.targetId = targetId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param tenant the tenant for the security token
|
||||
* @param controllerId the ID of the controller for the security token
|
||||
*/
|
||||
public ControllerSecurityToken(final String tenant, final String controllerId) {
|
||||
this(tenant, null, controllerId, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a header value.
|
||||
*
|
||||
* @param name of header
|
||||
* @return the value
|
||||
*/
|
||||
public String getHeader(final String name) {
|
||||
if (headers == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return headers.get(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Associates the specified header value with the specified name.
|
||||
*
|
||||
* @param name of the header
|
||||
* @param value of the header
|
||||
*/
|
||||
public void putHeader(final String name, final String value) {
|
||||
if (headers == null) {
|
||||
headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
|
||||
}
|
||||
|
||||
headers.put(name, value);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* 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("Security header authenticator")
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class SecurityHeaderAuthenticatorTest {
|
||||
|
||||
private static final String CA_COMMON_NAME = "ca-cn";
|
||||
private static final String CA_COMMON_NAME_VALUE = "box1";
|
||||
|
||||
private static final String X_SSL_ISSUER_HASH_1 = "X-Ssl-Issuer-Hash-1";
|
||||
|
||||
private static final String SINGLE_HASH = "hash1";
|
||||
private static final String SECOND_HASH = "hash2";
|
||||
private static final String THIRD_HASH = "hash3";
|
||||
private static final String UNKNOWN_HASH = "unknown";
|
||||
|
||||
private static final String MULTI_HASH = "HASH1;hash2,HASH3,HASH1";
|
||||
|
||||
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 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 SecurityHeaderAuthenticator(
|
||||
tenantConfigurationManagementMock, tenantAware,
|
||||
new SystemSecurityContext(tenantAware), CA_COMMON_NAME, "X-Ssl-Issuer-Hash-%d"
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Tests successful authentication with multiple a single hashes")
|
||||
void testWithSingleKnownHash() {
|
||||
final ControllerSecurityToken securityToken = prepareSecurityToken(SINGLE_HASH);
|
||||
when(tenantConfigurationManagementMock.getConfigurationValue(
|
||||
TenantConfigurationKey.AUTHENTICATION_MODE_HEADER_AUTHORITY_NAME, String.class))
|
||||
.thenReturn(CONFIG_VALUE_SINGLE_HASH);
|
||||
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 successful authentication with multiple hashes")
|
||||
void testWithMultipleKnownHashes() {
|
||||
when(tenantConfigurationManagementMock.getConfigurationValue(
|
||||
TenantConfigurationKey.AUTHENTICATION_MODE_HEADER_AUTHORITY_NAME, String.class))
|
||||
.thenReturn(CONFIG_VALUE_MULTI_HASH);
|
||||
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 that if the hash is unknown, the authentication fails")
|
||||
void testWithUnknownHash() {
|
||||
final ControllerSecurityToken securityToken = prepareSecurityToken(UNKNOWN_HASH);
|
||||
when(tenantConfigurationManagementMock.getConfigurationValue(
|
||||
TenantConfigurationKey.AUTHENTICATION_MODE_HEADER_AUTHORITY_NAME, String.class))
|
||||
.thenReturn(CONFIG_VALUE_MULTI_HASH);
|
||||
when(tenantConfigurationManagementMock.getConfigurationValue(
|
||||
TenantConfigurationKey.AUTHENTICATION_MODE_HEADER_ENABLED, Boolean.class))
|
||||
.thenReturn(CONFIG_VALUE_ENABLED);
|
||||
|
||||
assertThat(authenticator.authenticate(securityToken)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@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);
|
||||
|
||||
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_ENABLED, Boolean.class))
|
||||
.thenReturn(CONFIG_VALUE_DISABLED);
|
||||
|
||||
assertThat(authenticator.authenticate(securityToken)).isNull();
|
||||
}
|
||||
|
||||
private static ControllerSecurityToken prepareSecurityToken(final String issuerHashHeaderValue) {
|
||||
final ControllerSecurityToken securityToken = new ControllerSecurityToken("DEFAULT", CA_COMMON_NAME_VALUE);
|
||||
securityToken.putHeader(CA_COMMON_NAME, CA_COMMON_NAME_VALUE);
|
||||
securityToken.putHeader(X_SSL_ISSUER_HASH_1, issuerHashHeaderValue);
|
||||
return securityToken;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user