Security artifacts moved in hawkbit-security-parent (#2016)
Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
This commit is contained in:
50
hawkbit-security/hawkbit-security-controller/pom.xml
Normal file
50
hawkbit-security/hawkbit-security-controller/pom.xml
Normal file
@@ -0,0 +1,50 @@
|
||||
<!--
|
||||
|
||||
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-security-parent</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hawkbit-security-controller</artifactId>
|
||||
<name>hawkBit :: Security :: Controller</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.hawkbit</groupId>
|
||||
<artifactId>hawkbit-repository-api</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>jakarta.servlet</groupId>
|
||||
<artifactId>jakarta.servlet-api</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- TEST -->
|
||||
<dependency>
|
||||
<groupId>org.eclipse.hawkbit</groupId>
|
||||
<artifactId>hawkbit-repository-jpa</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.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());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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.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 ControllerPreAuthenticateSecurityTokenFilter 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 ControllerPreAuthenticateSecurityTokenFilter(
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.security.controller;
|
||||
|
||||
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.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey;
|
||||
|
||||
/**
|
||||
* A pre-authenticated processing filter which add the
|
||||
* {@link SpringEvalExpressions#CONTROLLER_DOWNLOAD_ROLE_ANONYMOUS} to the
|
||||
* security context in case the anonymous download is allowed through
|
||||
* configuration.
|
||||
*/
|
||||
public class ControllerPreAuthenticatedAnonymousDownload extends AbstractControllerAuthenticationFilter {
|
||||
|
||||
/**
|
||||
* 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 ControllerPreAuthenticatedAnonymousDownload(
|
||||
final TenantConfigurationManagement tenantConfigurationManagement, final TenantAware tenantAware,
|
||||
final SystemSecurityContext systemSecurityContext) {
|
||||
super(tenantConfigurationManagement, tenantAware, systemSecurityContext);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HeaderAuthentication getPreAuthenticatedPrincipal(final ControllerSecurityToken securityToken) {
|
||||
return new HeaderAuthentication(securityToken.getControllerId(), securityToken.getControllerId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public HeaderAuthentication getPreAuthenticatedCredentials(final ControllerSecurityToken securityToken) {
|
||||
return new HeaderAuthentication(securityToken.getControllerId(), securityToken.getControllerId());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getTenantConfigurationKey() {
|
||||
return TenantConfigurationKey.ANONYMOUS_DOWNLOAD_MODE_ENABLED;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* 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 org.eclipse.hawkbit.security.DdiSecurityProperties;
|
||||
|
||||
/**
|
||||
* An anonymous controller filter which is only enabled in case of anonymous
|
||||
* access is granted. This should only be for development purposes.
|
||||
*
|
||||
* @see org.eclipse.hawkbit.security.DdiSecurityProperties
|
||||
*/
|
||||
public class ControllerPreAuthenticatedAnonymousFilter implements PreAuthenticationFilter {
|
||||
|
||||
private final DdiSecurityProperties ddiSecurityConfiguration;
|
||||
|
||||
/**
|
||||
* @param ddiSecurityConfiguration the security configuration which holds the configuration if
|
||||
* anonymous is enabled or not
|
||||
*/
|
||||
public ControllerPreAuthenticatedAnonymousFilter(final DdiSecurityProperties ddiSecurityConfiguration) {
|
||||
this.ddiSecurityConfiguration = ddiSecurityConfiguration;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnable(final ControllerSecurityToken securityToken) {
|
||||
return ddiSecurityConfiguration.getAuthentication().getAnonymous().isEnabled();
|
||||
}
|
||||
|
||||
@Override
|
||||
public HeaderAuthentication getPreAuthenticatedPrincipal(final ControllerSecurityToken securityToken) {
|
||||
return new HeaderAuthentication(securityToken.getControllerId(), securityToken.getControllerId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public HeaderAuthentication getPreAuthenticatedCredentials(final ControllerSecurityToken securityToken) {
|
||||
return new HeaderAuthentication(securityToken.getControllerId(), securityToken.getControllerId());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* 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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
/**
|
||||
* 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).collect(Collectors.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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,73 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
/**
|
||||
* 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.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<>();
|
||||
for (final String ip : authorizedSourceIps) {
|
||||
this.authorizedSourceIps.add(ip);
|
||||
}
|
||||
}
|
||||
|
||||
@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)) {
|
||||
// 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;
|
||||
} else {
|
||||
remoteAddress = ((TenantAwareWebAuthenticationDetails) tokenDetails).getRemoteAddress();
|
||||
if (authorizedSourceIps.contains(remoteAddress)) {
|
||||
// source ip matches the given pattern -> authenticated
|
||||
success = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* 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 io.qameta.allure.Feature;
|
||||
import io.qameta.allure.Story;
|
||||
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.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;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
@Feature("Unit Tests - Security")
|
||||
@Story("Exclude path aware shallow ETag filter")
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
public class ControllerPreAuthenticatedAnonymousDownloadTest {
|
||||
|
||||
private ControllerPreAuthenticatedAnonymousDownload underTest;
|
||||
|
||||
@Mock
|
||||
private TenantConfigurationManagement tenantConfigurationManagementMock;
|
||||
|
||||
@Mock
|
||||
private TenantAware tenantAwareMock;
|
||||
|
||||
@BeforeEach
|
||||
public void before() {
|
||||
underTest = new ControllerPreAuthenticatedAnonymousDownload(tenantConfigurationManagementMock, tenantAwareMock,
|
||||
new SystemSecurityContext(tenantAwareMock));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void useCorrectTenantConfiguationKey() {
|
||||
assertThat(underTest.getTenantConfigurationKey()).as("Should be using the correct tenant configuration key")
|
||||
.isEqualTo(underTest.getTenantConfigurationKey());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void successfulAuthenticationAdditionalAuthoritiesForDownload() {
|
||||
assertThat(underTest.getSuccessfulAuthenticationAuthorities())
|
||||
.as("Additional authorities should be containing the download anonymous role")
|
||||
.contains(new SimpleGrantedAuthority(SpringEvalExpressions.CONTROLLER_ROLE));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* 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.Collection;
|
||||
|
||||
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("Issuer hash based authentication")
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
public class ControllerPreAuthenticatedSecurityHeaderFilterTest {
|
||||
|
||||
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 ControllerPreAuthenticatedSecurityHeaderFilter underTest;
|
||||
|
||||
@Mock
|
||||
private TenantConfigurationManagement tenantConfigurationManagementMock;
|
||||
@Mock
|
||||
private UserAuthoritiesResolver authoritiesResolver;
|
||||
@Mock
|
||||
private SecurityContextSerializer securityContextSerializer;
|
||||
|
||||
@BeforeEach
|
||||
public 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));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Tests the filter for issuer hash based authentication with a single known hash")
|
||||
public void testIssuerHashBasedAuthenticationWithSingleKnownHash() {
|
||||
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();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Tests the filter for issuer hash based authentication with multiple known hashes")
|
||||
public void testIssuerHashBasedAuthenticationWithMultipleKnownHashes() {
|
||||
// use multiple known hashes
|
||||
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();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Tests the filter for issuer hash based authentication with unknown hash")
|
||||
public void testIssuerHashBasedAuthenticationWithUnknownHash() {
|
||||
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();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Tests different values for issuer hash header and inspects the credentials")
|
||||
public void useDifferentValuesForIssuerHashHeader() {
|
||||
final ControllerSecurityToken securityToken1 = prepareSecurityToken(SINGLE_HASH);
|
||||
final ControllerSecurityToken securityToken2 = prepareSecurityToken(SECOND_HASH);
|
||||
|
||||
final HeaderAuthentication expected1 = new HeaderAuthentication(CA_COMMON_NAME_VALUE, SINGLE_HASH);
|
||||
final HeaderAuthentication expected2 = new HeaderAuthentication(CA_COMMON_NAME_VALUE, SECOND_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);
|
||||
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
54
hawkbit-security/hawkbit-security-core/pom.xml
Normal file
54
hawkbit-security/hawkbit-security-core/pom.xml
Normal file
@@ -0,0 +1,54 @@
|
||||
<!--
|
||||
|
||||
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-security-parent</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hawkbit-security-core</artifactId>
|
||||
<name>hawkBit :: Security :: Core</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.hawkbit</groupId>
|
||||
<artifactId>hawkbit-core</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-autoconfigure</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-oauth2-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-commons</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>jakarta.servlet</groupId>
|
||||
<artifactId>jakarta.servlet-api</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,413 @@
|
||||
/**
|
||||
* 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.im.authentication;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Software provisioning permissions that are technically available as {@linkplain GrantedAuthority} based on
|
||||
* the authenticated users identity context.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* The permissions cover CRUD operations for various areas within eclipse hawkBit, like targets, software-artifacts,
|
||||
* distribution sets, config-options etc.
|
||||
* </p>
|
||||
*/
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
@Slf4j
|
||||
public final class SpPermission {
|
||||
|
||||
/**
|
||||
* Permission to read the targets (list and filter).
|
||||
*/
|
||||
public static final String READ_TARGET = "READ_TARGET";
|
||||
|
||||
/**
|
||||
* Permission to read the target security token. The security token is security
|
||||
* concerned and should be protected. So the combination
|
||||
* {@linkplain #READ_TARGET} and {@code READ_TARGET_SEC_TOKEN} is necessary to
|
||||
* be able to read the security token of a target.
|
||||
*/
|
||||
public static final String READ_TARGET_SEC_TOKEN = "READ_TARGET_SECURITY_TOKEN";
|
||||
|
||||
/**
|
||||
* Permission to change/edit/update targets and to assign updates.
|
||||
*/
|
||||
public static final String UPDATE_TARGET = "UPDATE_TARGET";
|
||||
|
||||
/**
|
||||
* Permission to add new targets including their meta information.
|
||||
*/
|
||||
public static final String CREATE_TARGET = "CREATE_TARGET";
|
||||
|
||||
/**
|
||||
* Permission to delete targets.
|
||||
*/
|
||||
public static final String DELETE_TARGET = "DELETE_TARGET";
|
||||
|
||||
/**
|
||||
* Permission to read distributions and artifacts.
|
||||
*/
|
||||
public static final String READ_REPOSITORY = "READ_REPOSITORY";
|
||||
|
||||
/**
|
||||
* Permission to edit/update distributions and artifacts.
|
||||
*/
|
||||
public static final String UPDATE_REPOSITORY = "UPDATE_REPOSITORY";
|
||||
|
||||
/**
|
||||
* Permission to add distributions and artifacts.
|
||||
*/
|
||||
public static final String CREATE_REPOSITORY = "CREATE_REPOSITORY";
|
||||
|
||||
/**
|
||||
* Permission to delete distributions and artifacts.
|
||||
*/
|
||||
public static final String DELETE_REPOSITORY = "DELETE_REPOSITORY";
|
||||
|
||||
/**
|
||||
* Permission to administrate the system on a global, i.e. tenant
|
||||
* independent scale. That includes the deletion of tenants.
|
||||
*/
|
||||
public static final String SYSTEM_ADMIN = "SYSTEM_ADMIN";
|
||||
|
||||
/**
|
||||
* Permission to download repository artifacts of a software module.
|
||||
*/
|
||||
public static final String DOWNLOAD_REPOSITORY_ARTIFACT = "DOWNLOAD_REPOSITORY_ARTIFACT";
|
||||
|
||||
/**
|
||||
* Permission to read the tenant settings.
|
||||
*/
|
||||
public static final String READ_TENANT_CONFIGURATION = "READ_TENANT_CONFIGURATION";
|
||||
|
||||
/**
|
||||
* Permission to read the gateway security token. The gateway security token is security
|
||||
* concerned and should be protected. So in addition to {@linkplain #READ_TENANT_CONFIGURATION},
|
||||
* {@code READ_GATEWAY_SEC_TOKEN} is necessary to read gateway security token. {@link #TENANT_CONFIGURATION}
|
||||
* implies both permissions - so it is sufficient to read the gateway security token.
|
||||
*/
|
||||
public static final String READ_GATEWAY_SEC_TOKEN = "READ_GATEWAY_SECURITY_TOKEN";
|
||||
|
||||
/**
|
||||
* Permission to administrate the tenant settings.
|
||||
*/
|
||||
public static final String TENANT_CONFIGURATION = "TENANT_CONFIGURATION";
|
||||
|
||||
/**
|
||||
* Permission to read a rollout.
|
||||
*/
|
||||
public static final String READ_ROLLOUT = "READ_ROLLOUT";
|
||||
|
||||
/**
|
||||
* Permission to create a rollout.
|
||||
*/
|
||||
public static final String CREATE_ROLLOUT = "CREATE_ROLLOUT";
|
||||
|
||||
/**
|
||||
* Permission to update a rollout.
|
||||
*/
|
||||
public static final String UPDATE_ROLLOUT = "UPDATE_ROLLOUT";
|
||||
|
||||
/**
|
||||
* Permission to delete a rollout.
|
||||
*/
|
||||
public static final String DELETE_ROLLOUT = "DELETE_ROLLOUT";
|
||||
|
||||
/**
|
||||
* Permission to start/stop/resume a rollout.
|
||||
*/
|
||||
public static final String HANDLE_ROLLOUT = "HANDLE_ROLLOUT";
|
||||
|
||||
/**
|
||||
* Permission to approve or deny a rollout prior to starting.
|
||||
*/
|
||||
public static final String APPROVE_ROLLOUT = "APPROVE_ROLLOUT";
|
||||
|
||||
/**
|
||||
* Return all permission.
|
||||
*
|
||||
* @return all permissions
|
||||
*/
|
||||
public static List<String> getAllAuthorities() {
|
||||
final List<String> allPermissions = new ArrayList<>();
|
||||
final Field[] declaredFields = SpPermission.class.getDeclaredFields();
|
||||
for (final Field field : declaredFields) {
|
||||
if (Modifier.isPublic(field.getModifiers()) && Modifier.isStatic(field.getModifiers())) {
|
||||
try {
|
||||
final String role = (String) field.get(null);
|
||||
allPermissions.add(role);
|
||||
} catch (final IllegalAccessException e) {
|
||||
log.error(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return allPermissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Contains all the spring security evaluation expressions for the {@link PreAuthorize} annotation for method security.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* Examples:
|
||||
*
|
||||
* {@code
|
||||
* hasRole([role]) Returns true if the current principal has the specified role.
|
||||
* hasAnyRole([role1,role2]) Returns true if the current principal has any of the supplied roles (given as a comma-separated list of strings)
|
||||
* principal Allows direct access to the principal object representing the current user
|
||||
* authentication Allows direct access to the current Authentication object obtained from the SecurityContext
|
||||
* permitAll Always evaluates to true
|
||||
* denyAll Always evaluates to false
|
||||
* isAnonymous() Returns true if the current principal is an anonymous user
|
||||
* isRememberMe() Returns true if the current principal is a remember-me user
|
||||
* isAuthenticated() Returns true if the user is not anonymous
|
||||
* isFullyAuthenticated() Returns true if the user is not an anonymous or a remember-me user
|
||||
* }
|
||||
* </p>
|
||||
*/
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static final class SpringEvalExpressions {
|
||||
|
||||
/*
|
||||
* Spring security eval expressions.
|
||||
*/
|
||||
public static final String BRACKET_OPEN = "(";
|
||||
public static final String BRACKET_CLOSE = ")";
|
||||
public static final String HAS_AUTH_PREFIX = "hasAuthority" + BRACKET_OPEN + "'";
|
||||
public static final String HAS_AUTH_SUFFIX = "'" + BRACKET_CLOSE;
|
||||
public static final String HAS_AUTH_AND = " and ";
|
||||
|
||||
/**
|
||||
* The role which contains the spring security context in case the
|
||||
* system is executing code which is necessary to be privileged.
|
||||
*/
|
||||
public static final String SYSTEM_ROLE = "ROLE_SYSTEM_CODE";
|
||||
/**
|
||||
* Spring security eval hasAnyRole expression to check if the spring
|
||||
* context contains system code role
|
||||
* {@link SpringEvalExpressions#SYSTEM_ROLE}.
|
||||
*/
|
||||
public static final String IS_SYSTEM_CODE = HAS_AUTH_PREFIX + SYSTEM_ROLE + HAS_AUTH_SUFFIX;
|
||||
/**
|
||||
* The spring security eval expression operator {@code or}.
|
||||
*/
|
||||
public static final String HAS_AUTH_OR = " or ";
|
||||
/**
|
||||
* Spring security eval hasAuthority expression to check if spring
|
||||
* context contains {@link SpPermission#UPDATE_TARGET} or
|
||||
* {@link #IS_SYSTEM_CODE}.
|
||||
*/
|
||||
public static final String HAS_AUTH_UPDATE_TARGET = HAS_AUTH_PREFIX + UPDATE_TARGET + HAS_AUTH_SUFFIX
|
||||
+ HAS_AUTH_OR + IS_SYSTEM_CODE;
|
||||
/**
|
||||
* Spring security eval hasAuthority expression to check if spring
|
||||
* context contains {@link SpPermission#SYSTEM_ADMIN} or
|
||||
* {@link #IS_SYSTEM_CODE}.
|
||||
*/
|
||||
public static final String HAS_AUTH_SYSTEM_ADMIN = HAS_AUTH_PREFIX + SYSTEM_ADMIN + HAS_AUTH_SUFFIX
|
||||
+ HAS_AUTH_OR + IS_SYSTEM_CODE;
|
||||
/**
|
||||
* Spring security eval hasAuthority expression to check if spring
|
||||
* context contains {@link SpPermission#READ_TARGET} or
|
||||
* {@link #IS_SYSTEM_CODE}.
|
||||
*/
|
||||
public static final String HAS_AUTH_READ_TARGET = HAS_AUTH_PREFIX + READ_TARGET + HAS_AUTH_SUFFIX + HAS_AUTH_OR
|
||||
+ IS_SYSTEM_CODE;
|
||||
/**
|
||||
* Spring security eval hasAuthority expression to check if spring
|
||||
* context contains {@link SpPermission#READ_TARGET_SEC_TOKEN} or
|
||||
* {@link #IS_SYSTEM_CODE}.
|
||||
*/
|
||||
public static final String HAS_AUTH_READ_TARGET_SEC_TOKEN = HAS_AUTH_PREFIX + READ_TARGET_SEC_TOKEN
|
||||
+ HAS_AUTH_SUFFIX + HAS_AUTH_OR + IS_SYSTEM_CODE;
|
||||
/**
|
||||
* Spring security eval hasAuthority expression to check if spring
|
||||
* context contains {@link SpPermission#CREATE_TARGET} or
|
||||
* {@link #IS_SYSTEM_CODE}.
|
||||
*/
|
||||
public static final String HAS_AUTH_CREATE_TARGET = HAS_AUTH_PREFIX + CREATE_TARGET + HAS_AUTH_SUFFIX
|
||||
+ HAS_AUTH_OR + IS_SYSTEM_CODE;
|
||||
/**
|
||||
* Spring security eval hasAuthority expression to check if spring
|
||||
* context contains {@link SpPermission#DELETE_TARGET} or
|
||||
* {@link #IS_SYSTEM_CODE}.
|
||||
*/
|
||||
public static final String HAS_AUTH_DELETE_TARGET = HAS_AUTH_PREFIX + DELETE_TARGET + HAS_AUTH_SUFFIX
|
||||
+ HAS_AUTH_OR + IS_SYSTEM_CODE;
|
||||
/**
|
||||
* Spring security eval hasAuthority expression to check if spring
|
||||
* context contains {@link SpPermission#READ_REPOSITORY} and
|
||||
* {@link SpPermission#UPDATE_TARGET} or {@link #IS_SYSTEM_CODE}.
|
||||
*/
|
||||
public static final String HAS_AUTH_READ_REPOSITORY_AND_UPDATE_TARGET = BRACKET_OPEN + HAS_AUTH_PREFIX
|
||||
+ READ_REPOSITORY + HAS_AUTH_SUFFIX + HAS_AUTH_AND + HAS_AUTH_PREFIX + UPDATE_TARGET + HAS_AUTH_SUFFIX
|
||||
+ BRACKET_CLOSE + HAS_AUTH_OR + IS_SYSTEM_CODE;
|
||||
/**
|
||||
* Spring security eval hasAuthority expression to check if spring
|
||||
* context contains {@link SpPermission#CREATE_REPOSITORY} or
|
||||
* {@link #IS_SYSTEM_CODE}.
|
||||
*/
|
||||
public static final String HAS_AUTH_CREATE_REPOSITORY = HAS_AUTH_PREFIX + CREATE_REPOSITORY + HAS_AUTH_SUFFIX
|
||||
+ HAS_AUTH_OR + IS_SYSTEM_CODE;
|
||||
/**
|
||||
* Spring security eval hasAuthority expression to check if spring
|
||||
* context contains {@link SpPermission#DELETE_REPOSITORY} or
|
||||
* {@link #IS_SYSTEM_CODE}.
|
||||
*/
|
||||
public static final String HAS_AUTH_DELETE_REPOSITORY = HAS_AUTH_PREFIX + DELETE_REPOSITORY + HAS_AUTH_SUFFIX
|
||||
+ HAS_AUTH_OR + IS_SYSTEM_CODE;
|
||||
/**
|
||||
* Spring security eval hasAuthority expression to check if spring
|
||||
* context contains {@link SpPermission#READ_REPOSITORY} or
|
||||
* {@link #IS_SYSTEM_CODE}.
|
||||
*/
|
||||
public static final String HAS_AUTH_READ_REPOSITORY = HAS_AUTH_PREFIX + READ_REPOSITORY + HAS_AUTH_SUFFIX
|
||||
+ HAS_AUTH_OR + IS_SYSTEM_CODE;
|
||||
/**
|
||||
* Spring security eval hasAuthority expression to check if spring
|
||||
* context contains {@link SpPermission#UPDATE_REPOSITORY} or
|
||||
* {@link #IS_SYSTEM_CODE}.
|
||||
*/
|
||||
public static final String HAS_AUTH_UPDATE_REPOSITORY = HAS_AUTH_PREFIX + UPDATE_REPOSITORY + HAS_AUTH_SUFFIX
|
||||
+ HAS_AUTH_OR + IS_SYSTEM_CODE;
|
||||
/**
|
||||
* Spring security eval hasAuthority expression to check if spring
|
||||
* context contains {@link SpPermission#READ_REPOSITORY} and
|
||||
* {@link SpPermission#UPDATE_REPOSITORY} or {@link #IS_SYSTEM_CODE}.
|
||||
*/
|
||||
public static final String HAS_AUTH_READ_REPOSITORY_AND_UPDATE_REPOSITORY = BRACKET_OPEN + HAS_AUTH_PREFIX
|
||||
+ READ_REPOSITORY + HAS_AUTH_SUFFIX + HAS_AUTH_AND + HAS_AUTH_PREFIX + UPDATE_REPOSITORY
|
||||
+ HAS_AUTH_SUFFIX + BRACKET_CLOSE + HAS_AUTH_OR + IS_SYSTEM_CODE;
|
||||
/**
|
||||
* Spring security eval hasAuthority expression to check if spring
|
||||
* context contains {@link SpPermission#READ_REPOSITORY} and
|
||||
* {@link SpPermission#READ_TARGET} or {@link #IS_SYSTEM_CODE}.
|
||||
*/
|
||||
public static final String HAS_AUTH_READ_REPOSITORY_AND_READ_TARGET = BRACKET_OPEN + HAS_AUTH_PREFIX
|
||||
+ READ_REPOSITORY + HAS_AUTH_SUFFIX + HAS_AUTH_AND + HAS_AUTH_PREFIX + READ_TARGET + HAS_AUTH_SUFFIX
|
||||
+ BRACKET_CLOSE + HAS_AUTH_OR + IS_SYSTEM_CODE;
|
||||
/**
|
||||
* Spring security eval hasAuthority expression to check if spring
|
||||
* context contains {@link SpPermission#DOWNLOAD_REPOSITORY_ARTIFACT} or
|
||||
* {@link #IS_SYSTEM_CODE}.
|
||||
*/
|
||||
public static final String HAS_AUTH_DOWNLOAD_ARTIFACT = HAS_AUTH_PREFIX + DOWNLOAD_REPOSITORY_ARTIFACT
|
||||
+ HAS_AUTH_SUFFIX + HAS_AUTH_OR + IS_SYSTEM_CODE;
|
||||
/**
|
||||
* Spring security eval hasAuthority expression to check if spring
|
||||
* context contains {@link SpPermission#CREATE_REPOSITORY} and
|
||||
* {@link SpPermission#CREATE_TARGET} or {@link #IS_SYSTEM_CODE}.
|
||||
*/
|
||||
public static final String HAS_AUTH_CREATE_REPOSITORY_AND_CREATE_TARGET = BRACKET_OPEN + HAS_AUTH_PREFIX
|
||||
+ CREATE_REPOSITORY + HAS_AUTH_SUFFIX + HAS_AUTH_AND + HAS_AUTH_PREFIX + CREATE_TARGET + HAS_AUTH_SUFFIX
|
||||
+ BRACKET_CLOSE + HAS_AUTH_OR + IS_SYSTEM_CODE;
|
||||
/**
|
||||
* Spring security eval hasAuthority expression to check if spring
|
||||
* context contains {@link SpPermission#READ_ROLLOUT} or
|
||||
* {@link #IS_SYSTEM_CODE}.
|
||||
*/
|
||||
public static final String HAS_AUTH_ROLLOUT_MANAGEMENT_READ = HAS_AUTH_PREFIX + READ_ROLLOUT + HAS_AUTH_SUFFIX
|
||||
+ HAS_AUTH_OR + IS_SYSTEM_CODE;
|
||||
/**
|
||||
* Spring security eval hasAuthority expression to check if spring
|
||||
* context contains {@link SpPermission#READ_ROLLOUT} and
|
||||
* {@link SpPermission#READ_TARGET} or {@link #IS_SYSTEM_CODE}.
|
||||
*/
|
||||
public static final String HAS_AUTH_ROLLOUT_MANAGEMENT_READ_AND_TARGET_READ = BRACKET_OPEN + HAS_AUTH_PREFIX
|
||||
+ READ_ROLLOUT + HAS_AUTH_SUFFIX + HAS_AUTH_AND + HAS_AUTH_PREFIX + READ_TARGET + HAS_AUTH_SUFFIX
|
||||
+ BRACKET_CLOSE + HAS_AUTH_OR + IS_SYSTEM_CODE;
|
||||
/**
|
||||
* Spring security eval hasAuthority expression to check if spring
|
||||
* context contains {@link SpPermission#CREATE_ROLLOUT} or
|
||||
* {@link #IS_SYSTEM_CODE}.
|
||||
*/
|
||||
public static final String HAS_AUTH_ROLLOUT_MANAGEMENT_CREATE = HAS_AUTH_PREFIX + CREATE_ROLLOUT
|
||||
+ HAS_AUTH_SUFFIX + HAS_AUTH_OR + IS_SYSTEM_CODE;
|
||||
/**
|
||||
* Spring security eval hasAuthority expression to check if spring
|
||||
* context contains {@link SpPermission#HANDLE_ROLLOUT} or
|
||||
* {@link #IS_SYSTEM_CODE}.
|
||||
*/
|
||||
public static final String HAS_AUTH_ROLLOUT_MANAGEMENT_HANDLE = HAS_AUTH_PREFIX + HANDLE_ROLLOUT
|
||||
+ HAS_AUTH_SUFFIX + HAS_AUTH_OR + IS_SYSTEM_CODE;
|
||||
/**
|
||||
* Spring security eval hasAuthority expression to check if spring
|
||||
* context contains {@link SpPermission#APPROVE_ROLLOUT} or
|
||||
* {@link #IS_SYSTEM_CODE}.
|
||||
*/
|
||||
public static final String HAS_AUTH_ROLLOUT_MANAGEMENT_APPROVE = HAS_AUTH_PREFIX + APPROVE_ROLLOUT
|
||||
+ HAS_AUTH_SUFFIX + HAS_AUTH_OR + IS_SYSTEM_CODE;
|
||||
/**
|
||||
* Spring security eval hasAuthority expression to check if spring
|
||||
* context contains {@link SpPermission#UPDATE_ROLLOUT} or
|
||||
* {@link #IS_SYSTEM_CODE}.
|
||||
*/
|
||||
public static final String HAS_AUTH_ROLLOUT_MANAGEMENT_UPDATE = HAS_AUTH_PREFIX + UPDATE_ROLLOUT
|
||||
+ HAS_AUTH_SUFFIX + HAS_AUTH_OR + IS_SYSTEM_CODE;
|
||||
/**
|
||||
* Spring security eval hasAuthority expression to check if spring
|
||||
* context contains {@link SpPermission#DELETE_ROLLOUT} or
|
||||
* {@link #IS_SYSTEM_CODE}.
|
||||
*/
|
||||
public static final String HAS_AUTH_ROLLOUT_MANAGEMENT_DELETE = HAS_AUTH_PREFIX + DELETE_ROLLOUT
|
||||
+ HAS_AUTH_SUFFIX + HAS_AUTH_OR + IS_SYSTEM_CODE;
|
||||
/**
|
||||
* Spring security eval hasAuthority expression to check if spring
|
||||
* context contains {@link SpPermission#READ_TENANT_CONFIGURATION} or
|
||||
* {@link #IS_SYSTEM_CODE}.
|
||||
*/
|
||||
public static final String HAS_AUTH_TENANT_CONFIGURATION_READ = HAS_AUTH_PREFIX + READ_TENANT_CONFIGURATION
|
||||
+ HAS_AUTH_SUFFIX + HAS_AUTH_OR + IS_SYSTEM_CODE;
|
||||
/**
|
||||
* Spring security eval hasAuthority expression to check if spring
|
||||
* context contains {@link SpPermission#TENANT_CONFIGURATION} or
|
||||
* {@link #IS_SYSTEM_CODE}.
|
||||
*/
|
||||
public static final String HAS_AUTH_TENANT_CONFIGURATION = HAS_AUTH_PREFIX + TENANT_CONFIGURATION
|
||||
+ HAS_AUTH_SUFFIX + HAS_AUTH_OR + IS_SYSTEM_CODE;
|
||||
/**
|
||||
* The role which contains in the spring security context in case an
|
||||
* controller is authenticated.
|
||||
*/
|
||||
public static final String CONTROLLER_ROLE = "ROLE_CONTROLLER";
|
||||
/**
|
||||
* The role which contained in the spring security context in case that a
|
||||
* controller is authenticated, but only as 'anonymous'.
|
||||
*/
|
||||
public static final String CONTROLLER_ROLE_ANONYMOUS = "ROLE_CONTROLLER_ANONYMOUS";
|
||||
/**
|
||||
* Spring security eval hasAnyRole expression to check if the spring
|
||||
* context contains the anonymous role or the controller specific role
|
||||
* {@link SpringEvalExpressions#CONTROLLER_ROLE}.
|
||||
*/
|
||||
public static final String IS_CONTROLLER = "hasAnyRole('" + CONTROLLER_ROLE_ANONYMOUS + "', '" + CONTROLLER_ROLE
|
||||
+ "')";
|
||||
/**
|
||||
* Spring security eval hasAuthority expression to check if spring
|
||||
* context contains {@link #IS_CONTROLLER} or
|
||||
* {@link #HAS_AUTH_READ_REPOSITORY_AND_UPDATE_TARGET}.
|
||||
*/
|
||||
public static final String IS_CONTROLLER_OR_HAS_AUTH_READ_REPOSITORY_AND_UPDATE_TARGET =
|
||||
IS_CONTROLLER + HAS_AUTH_OR + HAS_AUTH_READ_REPOSITORY_AND_UPDATE_TARGET;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Copyright (c) 2024 Contributors to the Eclipse Foundation
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.im.authentication;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* Software provisioning roles that implies set of permissions and reflects high-level roles.
|
||||
*/
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
@Slf4j
|
||||
public final class SpRole {
|
||||
|
||||
public static final String TARGET_ADMIN = "ROLE_TARGET_ADMIN";
|
||||
public static final String REPOSITORY_ADMIN = "ROLE_REPOSITORY_ADMIN";
|
||||
public static final String ROLLOUT_ADMIN = "ROLE_ROLLOUT_ADMIN";
|
||||
public static final String TENANT_ADMIN = "ROLE_TENANT_ADMIN";
|
||||
|
||||
private static final String IMPLIES = " > ";
|
||||
private static final String LINE_BREAK = "\n";
|
||||
public static final String TARGET_ADMIN_HIERARCHY =
|
||||
TARGET_ADMIN + IMPLIES + SpPermission.READ_TARGET + LINE_BREAK +
|
||||
TARGET_ADMIN + IMPLIES + SpPermission.READ_TARGET_SEC_TOKEN + LINE_BREAK +
|
||||
TARGET_ADMIN + IMPLIES + SpPermission.UPDATE_TARGET + LINE_BREAK +
|
||||
TARGET_ADMIN + IMPLIES + SpPermission.CREATE_TARGET + LINE_BREAK +
|
||||
TARGET_ADMIN + IMPLIES + SpPermission.DELETE_TARGET + LINE_BREAK;
|
||||
public static final String REPOSITORY_ADMIN_HIERARCHY =
|
||||
REPOSITORY_ADMIN + IMPLIES + SpPermission.READ_REPOSITORY + LINE_BREAK +
|
||||
REPOSITORY_ADMIN + IMPLIES + SpPermission.UPDATE_REPOSITORY + LINE_BREAK +
|
||||
REPOSITORY_ADMIN + IMPLIES + SpPermission.CREATE_REPOSITORY + LINE_BREAK +
|
||||
REPOSITORY_ADMIN + IMPLIES + SpPermission.DELETE_REPOSITORY + LINE_BREAK +
|
||||
REPOSITORY_ADMIN + IMPLIES + SpPermission.DOWNLOAD_REPOSITORY_ARTIFACT + LINE_BREAK;
|
||||
public static final String ROLLOUT_ADMIN_HIERARCHY =
|
||||
ROLLOUT_ADMIN + IMPLIES + SpPermission.READ_ROLLOUT + LINE_BREAK +
|
||||
ROLLOUT_ADMIN + IMPLIES + SpPermission.CREATE_ROLLOUT + LINE_BREAK +
|
||||
ROLLOUT_ADMIN + IMPLIES + SpPermission.UPDATE_ROLLOUT + LINE_BREAK +
|
||||
ROLLOUT_ADMIN + IMPLIES + SpPermission.DELETE_ROLLOUT + LINE_BREAK +
|
||||
ROLLOUT_ADMIN + IMPLIES + SpPermission.HANDLE_ROLLOUT + LINE_BREAK +
|
||||
ROLLOUT_ADMIN + IMPLIES + SpPermission.APPROVE_ROLLOUT + LINE_BREAK;
|
||||
public static final String TENANT_CONFIGURATION_HIERARCHY =
|
||||
SpPermission.TENANT_CONFIGURATION + IMPLIES + SpPermission.READ_TENANT_CONFIGURATION + LINE_BREAK +
|
||||
SpPermission.TENANT_CONFIGURATION + IMPLIES + SpPermission.READ_GATEWAY_SEC_TOKEN + LINE_BREAK;
|
||||
public static final String TENANT_ADMIN_HIERARCHY =
|
||||
TENANT_ADMIN + IMPLIES + TARGET_ADMIN + LINE_BREAK +
|
||||
TENANT_ADMIN + IMPLIES + REPOSITORY_ADMIN + LINE_BREAK +
|
||||
TENANT_ADMIN + IMPLIES + ROLLOUT_ADMIN + LINE_BREAK +
|
||||
TENANT_ADMIN + IMPLIES + SpPermission.TENANT_CONFIGURATION + LINE_BREAK;
|
||||
|
||||
public static final String SYSTEM_ADMIN_HIERARCHY =
|
||||
SpPermission.SYSTEM_ADMIN + IMPLIES + TENANT_ADMIN + LINE_BREAK;
|
||||
|
||||
public static String DEFAULT_ROLE_HIERARCHY =
|
||||
TARGET_ADMIN_HIERARCHY +
|
||||
REPOSITORY_ADMIN_HIERARCHY +
|
||||
ROLLOUT_ADMIN_HIERARCHY +
|
||||
TENANT_CONFIGURATION_HIERARCHY +
|
||||
TENANT_ADMIN_HIERARCHY +
|
||||
SYSTEM_ADMIN_HIERARCHY;
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* Copyright (c) 2024 Contributors to the Eclipse Foundation
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.im.authentication;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.eclipse.hawkbit.tenancy.TenantAwareAuthenticationDetails;
|
||||
import org.eclipse.hawkbit.tenancy.TenantAwareUser;
|
||||
import org.eclipse.hawkbit.tenancy.TenantAwareUserProperties;
|
||||
import org.springframework.boot.autoconfigure.security.SecurityProperties;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* Authentication provider for configured via spring application properties users.
|
||||
* The users could be tenant scoped ({@link TenantAwareUserProperties}) or global ({@link SecurityProperties}).
|
||||
*/
|
||||
public class StaticAuthenticationProvider extends DaoAuthenticationProvider {
|
||||
|
||||
public StaticAuthenticationProvider(
|
||||
final TenantAwareUserProperties tenantAwareUserProperties, final SecurityProperties securityProperties) {
|
||||
setUserDetailsService(userDetailsService(tenantAwareUserProperties, securityProperties));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Authentication createSuccessAuthentication(final Object principal, final Authentication authentication, final UserDetails user) {
|
||||
final UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
|
||||
principal, authentication.getCredentials(), user.getAuthorities());
|
||||
result.setDetails(user instanceof TenantAwareUser tenantAwareUser
|
||||
? new TenantAwareAuthenticationDetails(tenantAwareUser.getTenant(), false)
|
||||
: user);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static UserDetailsService userDetailsService(
|
||||
final TenantAwareUserProperties tenantAwareUserProperties, final SecurityProperties securityProperties) {
|
||||
final List<User> userPrincipals = new ArrayList<>();
|
||||
tenantAwareUserProperties.getUser().forEach((username, user) -> {
|
||||
final String password = password(user.getPassword());
|
||||
|
||||
final List<GrantedAuthority> credentials =
|
||||
createAuthorities(user.getRoles(), user.getPermissions(), Collections::emptyList);
|
||||
userPrincipals.add(ObjectUtils.isEmpty(user.getTenant())
|
||||
? new User(username, password, credentials)
|
||||
: new TenantAwareUser(username, password, credentials, user.getTenant()));
|
||||
});
|
||||
|
||||
if (securityProperties != null && securityProperties.getUser() != null &&
|
||||
!securityProperties.getUser().isPasswordGenerated()) {
|
||||
// explicitly setup system user - add is as a regular (non-tenant scoped) user
|
||||
userPrincipals.add(new User(
|
||||
securityProperties.getUser().getName(),
|
||||
password(securityProperties.getUser().getPassword()),
|
||||
createAuthorities(
|
||||
securityProperties.getUser().getRoles(), Collections.emptyList(),
|
||||
() -> SpPermission.getAllAuthorities().stream()
|
||||
.map(SimpleGrantedAuthority::new)
|
||||
.map(GrantedAuthority.class::cast)
|
||||
.toList())));
|
||||
}
|
||||
|
||||
return new FixedInMemoryTenantAwareUserDetailsService(userPrincipals);
|
||||
}
|
||||
|
||||
private static String password(final String password) {
|
||||
return !Pattern.compile("^\\{.+}.*$").matcher(password).matches() ? "{noop}" + password : password;
|
||||
}
|
||||
|
||||
private static List<GrantedAuthority> createAuthorities(
|
||||
final List<String> userRoles, final List<String> userPermissions,
|
||||
final Supplier<List<GrantedAuthority>> defaultRolesSupplier) {
|
||||
if (ObjectUtils.isEmpty(userRoles) && ObjectUtils.isEmpty(userPermissions)) {
|
||||
return defaultRolesSupplier.get();
|
||||
}
|
||||
|
||||
final List<GrantedAuthority> grantedAuthorityList = new ArrayList<>();
|
||||
if (userRoles != null) {
|
||||
for (final String role : userRoles) {
|
||||
grantedAuthorityList.add(new SimpleGrantedAuthority("ROLE_" + role));
|
||||
}
|
||||
}
|
||||
for (final String permission : userPermissions) {
|
||||
grantedAuthorityList.add(new SimpleGrantedAuthority(permission));
|
||||
}
|
||||
|
||||
return grantedAuthorityList;
|
||||
}
|
||||
|
||||
private static class FixedInMemoryTenantAwareUserDetailsService implements UserDetailsService {
|
||||
|
||||
private final HashMap<String, User> userMap = new HashMap<>();
|
||||
|
||||
private FixedInMemoryTenantAwareUserDetailsService(final Collection<User> userPrincipals) {
|
||||
for (final User user : userPrincipals) {
|
||||
userMap.put(user.getUsername(), user);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserDetails loadUserByUsername(final String username) {
|
||||
final User user = userMap.get(username);
|
||||
if (user == null) {
|
||||
throw new UsernameNotFoundException("No such user");
|
||||
}
|
||||
// Spring mutates the data, so we must return a copy here
|
||||
return clone(user);
|
||||
}
|
||||
|
||||
private static User clone(final User user) {
|
||||
if (user instanceof TenantAwareUser) {
|
||||
return new TenantAwareUser(user.getUsername(), user.getPassword(), user.getAuthorities(), ((TenantAwareUser) user).getTenant());
|
||||
} else {
|
||||
return new User(user.getUsername(), user.getPassword(), user.getAuthorities());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* 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;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
/**
|
||||
* The common properties for DDI security.
|
||||
*/
|
||||
@Getter
|
||||
@EqualsAndHashCode
|
||||
@ToString
|
||||
@ConfigurationProperties("hawkbit.server.ddi.security")
|
||||
public class DdiSecurityProperties {
|
||||
|
||||
private final Rp rp = new Rp();
|
||||
private final Authentication authentication = new Authentication();
|
||||
|
||||
public Authentication getAuthentication() {
|
||||
return authentication;
|
||||
}
|
||||
|
||||
public Rp getRp() {
|
||||
return rp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse proxy configuration. Defines the security properties for
|
||||
* authenticating controllers behind a reverse proxy which terminates the
|
||||
* SSL session at the reverse proxy but adding request header which contains
|
||||
* the CN of the certificate.
|
||||
*/
|
||||
@Data
|
||||
public static class Rp {
|
||||
|
||||
/**
|
||||
* HTTP header field for common name of a DDI target client certificate.
|
||||
*/
|
||||
private String cnHeader = "X-Ssl-Client-Cn";
|
||||
/**
|
||||
* HTTP header field for issuer hash of a DDI target client certificate.
|
||||
*/
|
||||
private String sslIssuerHashHeader = "X-Ssl-Issuer-Hash-%d";
|
||||
/**
|
||||
* List of trusted (reverse proxy) IP addresses for performing DDI
|
||||
* client certificate authentication.
|
||||
*/
|
||||
private List<String> trustedIPs;
|
||||
}
|
||||
|
||||
/**
|
||||
* DDI Authentication options.
|
||||
*/
|
||||
@Data
|
||||
public static class Authentication {
|
||||
|
||||
private final Anonymous anonymous = new Anonymous();
|
||||
private final Targettoken targettoken = new Targettoken();
|
||||
private final Gatewaytoken gatewaytoken = new Gatewaytoken();
|
||||
|
||||
/**
|
||||
* Target token authentication. Tokens are defined per target.
|
||||
*/
|
||||
@Data
|
||||
public static class Targettoken {
|
||||
|
||||
/**
|
||||
* Set to true to enable target token authentication.
|
||||
*/
|
||||
private boolean enabled = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gateway token authentication. Tokens are defined per tenant. Use with care!
|
||||
*/
|
||||
@Data
|
||||
public static class Gatewaytoken {
|
||||
|
||||
/**
|
||||
* Gateway token based authentication enabled.
|
||||
*/
|
||||
private boolean enabled = false;
|
||||
|
||||
/**
|
||||
* Default gateway token name.
|
||||
*/
|
||||
private String name = "";
|
||||
|
||||
/**
|
||||
* Default gateway token itself.
|
||||
*/
|
||||
@ToString.Exclude
|
||||
private String key = "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Anonymous authentication.
|
||||
*/
|
||||
@Data
|
||||
public static class Anonymous {
|
||||
|
||||
/**
|
||||
* Set to true to enable anonymous DDI client authentication.
|
||||
*/
|
||||
private boolean enabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
/**
|
||||
* 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;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
/**
|
||||
* Security related hawkBit configuration.
|
||||
*/
|
||||
@Data
|
||||
@ConfigurationProperties("hawkbit.server.security")
|
||||
public class HawkbitSecurityProperties {
|
||||
|
||||
private final Clients clients = new Clients();
|
||||
private final Dos dos = new Dos();
|
||||
private final Cors cors = new Cors();
|
||||
|
||||
/**
|
||||
* Secure access enforced.
|
||||
*/
|
||||
private boolean requireSsl;
|
||||
/**
|
||||
* With this property a list of allowed hostnames can be configured. All
|
||||
* requests with different Host headers will be rejected.
|
||||
*/
|
||||
private List<String> allowedHostNames;
|
||||
/**
|
||||
* Add paths that will be ignored by {@link org.springframework.security.web.firewall.StrictHttpFirewall}.
|
||||
*/
|
||||
private List<String> httpFirewallIgnoredPaths;
|
||||
/**
|
||||
* Basic authentication realm, see https://tools.ietf.org/html/rfc2617#page-3 .
|
||||
*/
|
||||
private String basicRealm = "hawkBit";
|
||||
/**
|
||||
* If to allow http authentication when there is OAuth2 authentication enabled.
|
||||
*/
|
||||
private boolean allowHttpBasicOnOAuthEnabled = false;
|
||||
|
||||
/**
|
||||
* Security configuration related to CORS.
|
||||
*/
|
||||
@Data
|
||||
public static class Cors {
|
||||
|
||||
/**
|
||||
* Flag to enable CORS.
|
||||
*/
|
||||
private boolean enabled = false;
|
||||
/**
|
||||
* Allowed origins for CORS.
|
||||
*/
|
||||
private List<String> allowedOrigins = Collections.singletonList("http://localhost");
|
||||
/**
|
||||
* Allowed headers for CORS.
|
||||
*/
|
||||
private List<String> allowedHeaders = Collections.singletonList("*");
|
||||
/**
|
||||
* Allowed methods for CORS.
|
||||
*/
|
||||
private List<String> allowedMethods = Arrays.asList("DELETE", "GET", "POST", "PATCH", "PUT");
|
||||
/**
|
||||
* Exposed headers for CORS.
|
||||
*/
|
||||
private List<String> exposedHeaders = Collections.emptyList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Security configuration related to clients.
|
||||
*/
|
||||
@Data
|
||||
public static class Clients {
|
||||
|
||||
public static final String X_FORWARDED_FOR = "X-Forwarded-For";
|
||||
/**
|
||||
* Blacklisted client (IP addresses) for for DDI and Management API.
|
||||
*/
|
||||
private String blacklist = "";
|
||||
/**
|
||||
* Name of the http header from which the remote ip is extracted.
|
||||
*/
|
||||
private String remoteIpHeader = X_FORWARDED_FOR;
|
||||
/**
|
||||
* Set to <code>true</code> if DDI clients remote IP should be stored.
|
||||
*/
|
||||
private boolean trackRemoteIp = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Denial of service protection related properties.
|
||||
*/
|
||||
@Data
|
||||
public static class Dos {
|
||||
|
||||
private final Filter filter = new Filter();
|
||||
private final Filter uiFilter = new Filter();
|
||||
/**
|
||||
* Maximum number of status updates that the controller can report for
|
||||
* an action (0 to disable).
|
||||
*/
|
||||
private int maxStatusEntriesPerAction = 1000;
|
||||
/**
|
||||
* Maximum number of attributes that the controller can report;
|
||||
*/
|
||||
private int maxAttributeEntriesPerTarget = 100;
|
||||
/**
|
||||
* Maximum number of allowed groups per Rollout.
|
||||
*/
|
||||
private int maxRolloutGroupsPerRollout = 500;
|
||||
/**
|
||||
* Maximum number of messages per ActionStatus
|
||||
*/
|
||||
private int maxMessagesPerActionStatus = 50;
|
||||
/**
|
||||
* Maximum number of meta data entries per software module
|
||||
*/
|
||||
private int maxMetaDataEntriesPerSoftwareModule = 100;
|
||||
/**
|
||||
* Maximum number of meta data entries per distribution set
|
||||
*/
|
||||
private int maxMetaDataEntriesPerDistributionSet = 100;
|
||||
/**
|
||||
* Maximum number of meta data entries per target
|
||||
*/
|
||||
private int maxMetaDataEntriesPerTarget = 100;
|
||||
/**
|
||||
* Maximum number of software modules per distribution set
|
||||
*/
|
||||
private int maxSoftwareModulesPerDistributionSet = 100;
|
||||
/**
|
||||
* Maximum number of software modules per distribution set
|
||||
*/
|
||||
private int maxSoftwareModuleTypesPerDistributionSetType = 50;
|
||||
/**
|
||||
* Maximum number of artifacts per software module
|
||||
*/
|
||||
private int maxArtifactsPerSoftwareModule = 50;
|
||||
/**
|
||||
* Maximum number of targets per rollout group
|
||||
*/
|
||||
private int maxTargetsPerRolloutGroup = 20000;
|
||||
/**
|
||||
* Maximum number of overall actions targets per target
|
||||
*/
|
||||
private int maxActionsPerTarget = 2000;
|
||||
/**
|
||||
* Maximum number of actions resulting from a manual assignment of
|
||||
* distribution sets and targets. Must be greater than 1000.
|
||||
*/
|
||||
private int maxTargetDistributionSetAssignmentsPerManualAssignment = 5000;
|
||||
/**
|
||||
* Maximum number of targets for an automatic distribution set
|
||||
* assignment
|
||||
*/
|
||||
private int maxTargetsPerAutoAssignment = 20000;
|
||||
/**
|
||||
* Maximum size of artifacts in bytes. Defaults to 1 GB.
|
||||
*/
|
||||
private long maxArtifactSize = 1_073_741_824;
|
||||
/**
|
||||
* Maximum size of all artifacts in bytes. Defaults to 20 GB.
|
||||
*/
|
||||
private long maxArtifactStorage = 21_474_836_480L;
|
||||
/**
|
||||
* Maximum number of distribution set types per target types
|
||||
*/
|
||||
private int maxDistributionSetTypesPerTargetType = 50;
|
||||
|
||||
/**
|
||||
* Configuration for hawkBits DOS prevention filter. This is usually an
|
||||
* infrastructure topic (e.g. Web Application Firewall (WAF)) but might
|
||||
* be useful in some cases, e.g. to prevent unintended misuse.
|
||||
*/
|
||||
@Data
|
||||
public static class Filter {
|
||||
|
||||
/**
|
||||
* True if filter is enabled.
|
||||
*/
|
||||
private boolean enabled = true;
|
||||
/**
|
||||
* White list of peer IP addresses for DOS filter (regular
|
||||
* expression).
|
||||
*/
|
||||
private String whitelist = "10\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|192\\.168\\.\\d{1,3}\\.\\d{1,3}|169\\.254\\.\\d{1,3}\\.\\d{1,3}|127\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|172\\.1[6-9]{1}\\.\\d{1,3}\\.\\d{1,3}|172\\.2[0-9]{1}\\.\\d{1,3}\\.\\d{1,3}|172\\.3[0-1]{1}\\.\\d{1,3}\\.\\d{1,3}";
|
||||
/**
|
||||
* # Maximum number of allowed REST read/GET requests per second per
|
||||
* client IP.
|
||||
*/
|
||||
private int maxRead = 200;
|
||||
/**
|
||||
* Maximum number of allowed REST write/(PUT/POST/etc.) requests per
|
||||
* second per client IP.
|
||||
*/
|
||||
private int maxWrite = 50;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Copyright (c) 2020 Bosch.IO 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;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.hawkbit.tenancy.UserAuthoritiesResolver;
|
||||
|
||||
/**
|
||||
* An implementation of the {@link UserAuthoritiesResolver} that is based on
|
||||
* in-memory user permissions.
|
||||
*/
|
||||
public class InMemoryUserAuthoritiesResolver implements UserAuthoritiesResolver {
|
||||
|
||||
private final Map<String, List<String>> usernamesToAuthorities;
|
||||
|
||||
/**
|
||||
* Constructs the resolver based on the given authority lookup map.
|
||||
*
|
||||
* @param usernamesToAuthorities The authority map to read from. Must not be <code>null</code>.
|
||||
*/
|
||||
public InMemoryUserAuthoritiesResolver(final Map<String, List<String>> usernamesToAuthorities) {
|
||||
this.usernamesToAuthorities = usernamesToAuthorities;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<String> getUserAuthorities(final String tenant, final String username) {
|
||||
// we can ignore the tenant here (no multi-tenancy by default)
|
||||
final Collection<String> authorities = usernamesToAuthorities.get(username);
|
||||
if (authorities == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return authorities;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
/**
|
||||
* Copyright (c) 2024 Contributors to the Eclipse Foundation
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.security;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
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.tenancy.TenantAwareAuthenticationDetails;
|
||||
import org.slf4j.MDC;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.web.access.intercept.AuthorizationFilter;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class MdcHandler {
|
||||
|
||||
public static String MDC_KEY_TENANT = "tenant";
|
||||
public static String MDC_KEY_USER = "user";
|
||||
|
||||
private static final MdcHandler SINGLETON = new MdcHandler();
|
||||
|
||||
@Value("${hawkbit.logging.mdchandler.enabled:true}")
|
||||
private boolean mdcEnabled;
|
||||
@Autowired(required = false)
|
||||
private SpringSecurityAuditorAware springSecurityAuditorAware = new SpringSecurityAuditorAware();
|
||||
|
||||
/**
|
||||
* @return The holder singleton instance.
|
||||
*/
|
||||
public static MdcHandler getInstance() {
|
||||
return SINGLETON;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes callable and returns the result. If MDC is enabled, it sets the tenant and / or user from the authentication in the MDC context.
|
||||
*
|
||||
* @param <T> the return type
|
||||
* @param callable the callable to execute
|
||||
* @return the result
|
||||
* @throws Exception if thrown by the callable
|
||||
*/
|
||||
public <T> T callWithAuth(final Callable<T> callable) throws Exception {
|
||||
if (!mdcEnabled) {
|
||||
return callable.call();
|
||||
}
|
||||
|
||||
final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (authentication == null) {
|
||||
return callable.call();
|
||||
}
|
||||
|
||||
final String tenant;
|
||||
if (authentication.getDetails() instanceof TenantAwareAuthenticationDetails tenantAwareAuthenticationDetails) {
|
||||
tenant = tenantAwareAuthenticationDetails.getTenant();
|
||||
} else {
|
||||
tenant = null;
|
||||
}
|
||||
|
||||
final String user = springSecurityAuditorAware
|
||||
.getCurrentAuditor()
|
||||
.filter(username -> !username.equals("system")) // null and system are the same - system user
|
||||
.orElse(null);
|
||||
|
||||
return callWithTenantAndUser0(callable, tenant, user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes callable and returns the result. If MDC is enabled, it sets the tenant and / or user from the authentication in the MDC context.
|
||||
* Calls the {@link #callWithAuth(Callable)} method and wraps any catchable exception into a {@link RuntimeException}.
|
||||
*
|
||||
* @param <T> the return type
|
||||
* @param callable the callable to execute
|
||||
* @return the result
|
||||
*/
|
||||
public <T> T callWithAuthRE(final Callable<T> callable) {
|
||||
try {
|
||||
return callWithAuth(callable);
|
||||
} catch (final RuntimeException re) {
|
||||
throw re;
|
||||
} catch (final Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes callable and returns the result. If MDC is enabled, it sets the tenant and / or user in the MDC context.
|
||||
*
|
||||
* @param <T> the return type
|
||||
* @param callable the callable to execute
|
||||
* @param tenant the tenant to set in the MDC context
|
||||
* @param user the user to set in the MDC context
|
||||
* @return the result
|
||||
*/
|
||||
public <T> T callWithTenantAndUser(final Callable<T> callable, final String tenant, final String user) throws Exception {
|
||||
if (!mdcEnabled) {
|
||||
return callable.call();
|
||||
}
|
||||
|
||||
return callWithTenantAndUser0(callable, tenant, user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes callable and returns the result. If MDC is enabled, it sets the tenant and / or user from the authentication in the MDC context.
|
||||
* Calls the {@link #callWithTenantAndUser(Callable, String, String)} method and wraps any catchable exception into a {@link RuntimeException}.
|
||||
*
|
||||
* @param <T> the return type
|
||||
* @param callable the callable to execute
|
||||
* @param tenant the tenant to set in the MDC context
|
||||
* @param user the user to set in the MDC context
|
||||
* @return the result
|
||||
*/
|
||||
public <T> T callWithTenantAndUserRE(final Callable<T> callable, final String tenant, final String user) {
|
||||
try {
|
||||
return callWithTenantAndUser(callable, tenant, user);
|
||||
} catch (final RuntimeException re) {
|
||||
throw re;
|
||||
} catch (final Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static <T> T callWithTenantAndUser0(final Callable<T> callable, final String tenant, final String user) throws Exception {
|
||||
final String currentTenant = MDC.get(MDC_KEY_TENANT);
|
||||
if (Objects.equals(currentTenant, tenant)) {
|
||||
return callWithUser(callable, user);
|
||||
} else {
|
||||
put(MDC_KEY_TENANT, tenant);
|
||||
try {
|
||||
return callWithUser(callable, user);
|
||||
} finally {
|
||||
put(MDC_KEY_TENANT, currentTenant);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static <T> T callWithUser(final Callable<T> callable, final String user) throws Exception {
|
||||
final String currentUser = MDC.get(MDC_KEY_USER);
|
||||
if (Objects.equals(currentUser, user)) {
|
||||
return callable.call();
|
||||
} else {
|
||||
put(MDC_KEY_USER, user);
|
||||
try {
|
||||
return callable.call();
|
||||
} finally {
|
||||
put(MDC_KEY_USER, currentUser);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void put(final String key, final String value) {
|
||||
if (value == null) {
|
||||
MDC.remove(key);
|
||||
} else {
|
||||
MDC.put(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static class Filter {
|
||||
|
||||
public static void addMdcFilter(final HttpSecurity httpSecurity) {
|
||||
httpSecurity.addFilterBefore(new OncePerRequestFilter() {
|
||||
|
||||
private final MdcHandler mdcFilter = MdcHandler.getInstance();
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(
|
||||
final HttpServletRequest request, final HttpServletResponse response, final FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
try {
|
||||
mdcFilter.callWithAuth(() -> {
|
||||
filterChain.doFilter(request, response);
|
||||
return null;
|
||||
});
|
||||
} catch (final ServletException | IOException | RuntimeException e) {
|
||||
throw e;
|
||||
} catch (final Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}, AuthorizationFilter.class);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* 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;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* Constants related to security.
|
||||
*/
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public final class SecurityConstants {
|
||||
|
||||
/**
|
||||
* Logger prefix used for security logging.
|
||||
*/
|
||||
public static final String SECURITY_LOG_PREFIX = "server-security";
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* Copyright (c) 2023 Bosch.IO 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;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.ObjectOutputStream;
|
||||
import java.util.Base64;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
|
||||
public interface SecurityContextSerializer {
|
||||
|
||||
/**
|
||||
* Serializer that do not serialize (returns null on {@link #serialize(SecurityContext)}) and
|
||||
* throws exception on {@link #deserialize(String)}.
|
||||
*/
|
||||
SecurityContextSerializer NOP = new Nop();
|
||||
/**
|
||||
* Serializer the uses Java serialization of {@link java.io.Serializable} objects.
|
||||
* <p/>
|
||||
* Note that serialized via java serialization context might become unreadable if incompatible
|
||||
* changes are made to the object classes.
|
||||
*/
|
||||
SecurityContextSerializer JAVA_SERIALIZATION = new JavaSerialization();
|
||||
|
||||
/**
|
||||
* Return security context as string (could be just a reference)
|
||||
*
|
||||
* @param securityContext the security context
|
||||
* @return the securityContext as string
|
||||
*/
|
||||
String serialize(SecurityContext securityContext);
|
||||
|
||||
/**
|
||||
* Deserialize security context
|
||||
*
|
||||
* @param securityContextString string representing the security context
|
||||
* @return deserialized security context
|
||||
*/
|
||||
SecurityContext deserialize(String securityContextString);
|
||||
|
||||
/**
|
||||
* Empty implementation. Could be used if the serialization shall not be used.
|
||||
* It returns <code>null</code> as serialized context and throws exception if
|
||||
* someone try to deserialize anything.
|
||||
*/
|
||||
class Nop implements SecurityContextSerializer {
|
||||
|
||||
private Nop() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String serialize(final SecurityContext securityContext) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SecurityContext deserialize(String securityContextString) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation based on the java serialization.
|
||||
*/
|
||||
class JavaSerialization implements SecurityContextSerializer {
|
||||
|
||||
private JavaSerialization() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String serialize(final SecurityContext securityContext) {
|
||||
Objects.requireNonNull(securityContext);
|
||||
try (final ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
final ObjectOutputStream oos = new ObjectOutputStream(baos)) {
|
||||
oos.writeObject(securityContext);
|
||||
oos.flush();
|
||||
return Base64.getEncoder().encodeToString(baos.toByteArray());
|
||||
} catch (final IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public SecurityContext deserialize(String securityContextString) {
|
||||
Objects.requireNonNull(securityContextString);
|
||||
try (final ByteArrayInputStream bais = new ByteArrayInputStream(Base64.getDecoder().decode(securityContextString));
|
||||
final ObjectInputStream ois = new ObjectInputStream(bais)) {
|
||||
return (SecurityContext) ois.readObject();
|
||||
} catch (final IOException | ClassNotFoundException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
/**
|
||||
* 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;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.eclipse.hawkbit.ContextAware;
|
||||
import org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions;
|
||||
import org.eclipse.hawkbit.tenancy.TenantAwareAuthenticationDetails;
|
||||
import org.eclipse.hawkbit.tenancy.TenantAwareUser;
|
||||
import org.eclipse.hawkbit.tenancy.TenantAware;
|
||||
import org.eclipse.hawkbit.tenancy.UserAuthoritiesResolver;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
|
||||
|
||||
/**
|
||||
* A {@link ContextAware} (hence of {@link TenantAware}) that uses spring security context propagation
|
||||
* mechanisms and which retrieves the ID of the tenant from the {@link SecurityContext#getAuthentication()}
|
||||
* {@link Authentication#getDetails()} which holds the {@link TenantAwareAuthenticationDetails} object.
|
||||
*/
|
||||
public class SecurityContextTenantAware implements ContextAware {
|
||||
|
||||
public static final String SYSTEM_USER = "system";
|
||||
|
||||
private static final Collection<? extends GrantedAuthority> SYSTEM_AUTHORITIES =
|
||||
Collections.singletonList(new SimpleGrantedAuthority(SpringEvalExpressions.SYSTEM_ROLE));
|
||||
|
||||
private final UserAuthoritiesResolver authoritiesResolver;
|
||||
private final SecurityContextSerializer securityContextSerializer;
|
||||
|
||||
/**
|
||||
* Creates the {@link SecurityContextTenantAware} based on the given {@link UserAuthoritiesResolver}.
|
||||
*
|
||||
* @param authoritiesResolver Resolver to retrieve the authorities for a given user. Must
|
||||
* not be <code>null</code>..
|
||||
*/
|
||||
public SecurityContextTenantAware(final UserAuthoritiesResolver authoritiesResolver) {
|
||||
this.authoritiesResolver = authoritiesResolver;
|
||||
this.securityContextSerializer = SecurityContextSerializer.NOP;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the {@link SecurityContextTenantAware} based on the given {@link UserAuthoritiesResolver}.
|
||||
*
|
||||
* @param authoritiesResolver Resolver to retrieve the authorities for a given user. Must not be <code>null</code>.
|
||||
* @param securityContextSerializer Serializer that is used to serialize / deserialize {@link SecurityContext}s.
|
||||
*/
|
||||
public SecurityContextTenantAware(final UserAuthoritiesResolver authoritiesResolver,
|
||||
@Nullable final SecurityContextSerializer securityContextSerializer) {
|
||||
this.authoritiesResolver = authoritiesResolver;
|
||||
this.securityContextSerializer = securityContextSerializer == null ? SecurityContextSerializer.NOP : securityContextSerializer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCurrentTenant() {
|
||||
final SecurityContext context = SecurityContextHolder.getContext();
|
||||
if (context.getAuthentication() != null) {
|
||||
final Object principal = context.getAuthentication().getPrincipal();
|
||||
if (context.getAuthentication().getDetails() instanceof TenantAwareAuthenticationDetails) {
|
||||
return ((TenantAwareAuthenticationDetails) context.getAuthentication().getDetails()).getTenant();
|
||||
} else if (principal instanceof TenantAwareUser) {
|
||||
return ((TenantAwareUser) principal).getTenant();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCurrentUsername() {
|
||||
final SecurityContext context = SecurityContextHolder.getContext();
|
||||
if (context.getAuthentication() != null) {
|
||||
final Object principal = context.getAuthentication().getPrincipal();
|
||||
if (principal instanceof OidcUser) {
|
||||
return ((OidcUser) principal).getPreferredUsername();
|
||||
}
|
||||
if (principal instanceof User) {
|
||||
return ((User) principal).getUsername();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T runAsTenant(final String tenant, final TenantRunner<T> tenantRunner) {
|
||||
return runInContext(buildUserSecurityContext(tenant, SYSTEM_USER, SYSTEM_AUTHORITIES), tenantRunner::run);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T runAsTenantAsUser(final String tenant, final String username, final TenantRunner<T> tenantRunner) {
|
||||
Objects.requireNonNull(tenant);
|
||||
Objects.requireNonNull(username);
|
||||
|
||||
final List<SimpleGrantedAuthority> authorities = runAsSystem(
|
||||
() -> authoritiesResolver.getUserAuthorities(tenant, username).stream()
|
||||
.map(SimpleGrantedAuthority::new)
|
||||
.toList());
|
||||
return runInContext(buildUserSecurityContext(tenant, username, authorities), tenantRunner::run);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<String> getCurrentContext() {
|
||||
return Optional.ofNullable(SecurityContextHolder.getContext()).map(securityContextSerializer::serialize);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T, R> R runInContext(final String serializedContext, final Function<T, R> function, final T t) {
|
||||
Objects.requireNonNull(serializedContext);
|
||||
Objects.requireNonNull(function);
|
||||
final SecurityContext securityContext = securityContextSerializer.deserialize(serializedContext);
|
||||
Objects.requireNonNull(securityContext);
|
||||
|
||||
return runInContext(securityContext, () -> function.apply(t));
|
||||
}
|
||||
|
||||
private static <T> T runInContext(final SecurityContext securityContext, final Supplier<T> supplier) {
|
||||
final SecurityContext originalContext = SecurityContextHolder.getContext();
|
||||
if (Objects.equals(securityContext, originalContext)) {
|
||||
return supplier.get();
|
||||
} else {
|
||||
SecurityContextHolder.setContext(securityContext);
|
||||
try {
|
||||
return MdcHandler.getInstance().callWithAuthRE(supplier::get);
|
||||
} finally {
|
||||
SecurityContextHolder.setContext(originalContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static <T> T runAsSystem(final TenantRunner<T> tenantRunner) {
|
||||
final SecurityContext currentContext = SecurityContextHolder.getContext();
|
||||
SystemSecurityContext.setSystemContext(currentContext);
|
||||
try {
|
||||
return MdcHandler.getInstance().callWithAuthRE(tenantRunner::run);
|
||||
} finally {
|
||||
SecurityContextHolder.setContext(currentContext);
|
||||
}
|
||||
}
|
||||
|
||||
private static SecurityContext buildUserSecurityContext(
|
||||
final String tenant, final String username, final Collection<? extends GrantedAuthority> authorities) {
|
||||
final SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
|
||||
securityContext.setAuthentication(new AuthenticationDelegate(
|
||||
SecurityContextHolder.getContext().getAuthentication(), tenant, username, authorities));
|
||||
return securityContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* An {@link Authentication} implementation to delegate to an existing
|
||||
* {@link Authentication} object except setting the details specifically for
|
||||
* a specific tenant and user.
|
||||
*/
|
||||
private static final class AuthenticationDelegate implements Authentication {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final Authentication delegate;
|
||||
private final TenantAwareUser principal;
|
||||
private final TenantAwareAuthenticationDetails tenantAwareAuthenticationDetails;
|
||||
|
||||
private AuthenticationDelegate(final Authentication delegate, final String tenant, final String username,
|
||||
final Collection<? extends GrantedAuthority> authorities) {
|
||||
this.delegate = delegate;
|
||||
principal = new TenantAwareUser(username, username, authorities, tenant);
|
||||
tenantAwareAuthenticationDetails = new TenantAwareAuthenticationDetails(tenant, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return delegate != null ? delegate.hashCode() : -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object another) {
|
||||
if (another instanceof Authentication anotherAuthentication) {
|
||||
return Objects.equals(delegate, anotherAuthentication) &&
|
||||
Objects.equals(principal, anotherAuthentication.getPrincipal()) &&
|
||||
Objects.equals(tenantAwareAuthenticationDetails, anotherAuthentication.getDetails());
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return delegate != null ? delegate.toString() : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return delegate != null ? delegate.getName() : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<? extends GrantedAuthority> getAuthorities() {
|
||||
return delegate != null ? delegate.getAuthorities() : Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getCredentials() {
|
||||
return delegate != null ? delegate.getCredentials() : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getDetails() {
|
||||
return tenantAwareAuthenticationDetails;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getPrincipal() {
|
||||
return principal;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAuthenticated() {
|
||||
return delegate == null || delegate.isAuthenticated();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAuthenticated(final boolean isAuthenticated) {
|
||||
if (delegate == null) {
|
||||
return;
|
||||
}
|
||||
delegate.setAuthenticated(isAuthenticated);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* 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;
|
||||
|
||||
import org.springframework.security.crypto.codec.Hex;
|
||||
import org.springframework.security.crypto.keygen.BytesKeyGenerator;
|
||||
import org.springframework.security.crypto.keygen.KeyGenerators;
|
||||
|
||||
/**
|
||||
* A security token generator service which can be used to generate security
|
||||
* tokens for e.g. target or gateway tokens which are valid for authenticates
|
||||
* against SP.
|
||||
*/
|
||||
public class SecurityTokenGenerator {
|
||||
|
||||
private static final int TOKEN_LENGTH = 16;
|
||||
private static final BytesKeyGenerator SECURE_RANDOM = KeyGenerators.secureRandom(TOKEN_LENGTH);
|
||||
|
||||
/**
|
||||
* Generates a random secure token of {@link #TOKEN_LENGTH} bytes length as
|
||||
* hexadecimal string.
|
||||
*
|
||||
* @return a new generated random alphanumeric string.
|
||||
*/
|
||||
public String generateToken() {
|
||||
return new String(Hex.encode(SECURE_RANDOM.generateKey()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* 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;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.data.domain.AuditorAware;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
|
||||
|
||||
/**
|
||||
* Auditor class that allows BaseEntitys to insert current logged in user for
|
||||
* repository changes.
|
||||
*/
|
||||
public class SpringSecurityAuditorAware implements AuditorAware<String> {
|
||||
|
||||
// Sometimes 'system' need to override the auditor when do create/modify actions in context of a tenant and user.
|
||||
// Though this could be made using runAsTenantAsUser sometimes (as in transaction) this override is needed
|
||||
// after runAsTenantAsUser (because it seems that auditor is got in commit time).
|
||||
// So this thread local variable provides option to override explicitly the auditor.
|
||||
private static final ThreadLocal<String> AUDITOR_OVERRIDE = new ThreadLocal<>();
|
||||
|
||||
// Always shall be followed by {@link #clearAuditorOverride}
|
||||
public static void setAuditorOverride(final String auditor) {
|
||||
if (auditor == null) {
|
||||
AUDITOR_OVERRIDE.remove();
|
||||
} else {
|
||||
AUDITOR_OVERRIDE.set(auditor);
|
||||
}
|
||||
}
|
||||
|
||||
public static void clearAuditorOverride() {
|
||||
AUDITOR_OVERRIDE.remove();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<String> getCurrentAuditor() {
|
||||
if (AUDITOR_OVERRIDE.get() != null) {
|
||||
return Optional.of(AUDITOR_OVERRIDE.get());
|
||||
}
|
||||
|
||||
final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
|
||||
if (isAuthenticationInvalid(authentication)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return Optional.ofNullable(getCurrentAuditor(authentication));
|
||||
}
|
||||
|
||||
protected String getCurrentAuditor(final Authentication authentication) {
|
||||
if (authentication.getPrincipal() instanceof UserDetails) {
|
||||
return ((UserDetails) authentication.getPrincipal()).getUsername();
|
||||
}
|
||||
if (authentication.getPrincipal() instanceof OidcUser) {
|
||||
return ((OidcUser) authentication.getPrincipal()).getPreferredUsername();
|
||||
}
|
||||
return authentication.getPrincipal().toString();
|
||||
}
|
||||
|
||||
private static boolean isAuthenticationInvalid(final Authentication authentication) {
|
||||
return authentication == null || !authentication.isAuthenticated() || authentication.getPrincipal() == null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
/**
|
||||
* 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;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions;
|
||||
import org.eclipse.hawkbit.tenancy.TenantAwareAuthenticationDetails;
|
||||
import org.eclipse.hawkbit.tenancy.TenantAware;
|
||||
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
|
||||
import org.springframework.security.authentication.AnonymousAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.context.SecurityContextImpl;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* A Service which provide to run system code.
|
||||
*/
|
||||
@Slf4j
|
||||
public class SystemSecurityContext {
|
||||
|
||||
private final TenantAware tenantAware;
|
||||
private final RoleHierarchy roleHierarchy;
|
||||
|
||||
/**
|
||||
* Autowired constructor.
|
||||
*
|
||||
* @param tenantAware the tenant aware bean to retrieve the current tenant
|
||||
*/
|
||||
public SystemSecurityContext(final TenantAware tenantAware) {
|
||||
this(tenantAware, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Autowired constructor.
|
||||
*
|
||||
* @param tenantAware the tenant aware bean to retrieve the current tenant
|
||||
* @param roleHierarchy the roleHierarchy that is applied
|
||||
*/
|
||||
public SystemSecurityContext(final TenantAware tenantAware, final RoleHierarchy roleHierarchy) {
|
||||
this.tenantAware = tenantAware;
|
||||
this.roleHierarchy = roleHierarchy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a given {@link Callable} within a system security context, which is
|
||||
* permitted to call secured system code. Often the system needs to call
|
||||
* secured methods by it's own without relying on the current security
|
||||
* context e.g. if the current security context does not contain the
|
||||
* necessary permission it's necessary to execute code as system code to
|
||||
* execute necessary methods and functionality.
|
||||
*
|
||||
* The security context will be switched to the system code and back after
|
||||
* the callable is called.
|
||||
*
|
||||
* The system code is executed for a current tenant by using the
|
||||
* {@link TenantAware#getCurrentTenant()}.
|
||||
*
|
||||
* @param callable the callable to call within the system security context
|
||||
* @return the return value of the {@link Callable#call()} method.
|
||||
*/
|
||||
// Exception squid:S2221 - Callable declares Exception
|
||||
@SuppressWarnings("squid:S2221")
|
||||
public <T> T runAsSystem(final Callable<T> callable) {
|
||||
return runAsSystemAsTenant(callable, tenantAware.getCurrentTenant());
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a given {@link Callable} within a system security context, which is
|
||||
* permitted to call secured system code. Often the system needs to call
|
||||
* secured methods by it's own without relying on the current security
|
||||
* context e.g. if the current security context does not contain the
|
||||
* necessary permission it's necessary to execute code as system code to
|
||||
* execute necessary methods and functionality.
|
||||
*
|
||||
* The security context will be switched to the system code and back after
|
||||
* the callable is called.
|
||||
*
|
||||
* The system code is executed for a specific given tenant by using the
|
||||
* {@link TenantAware}.
|
||||
*
|
||||
* @param callable the callable to call within the system security context
|
||||
* @param tenant the tenant to act as system code
|
||||
* @return the return value of the {@link Callable#call()} method.
|
||||
*/
|
||||
// The callable API throws a Exception and not a specific one
|
||||
@SuppressWarnings({ "squid:S2221", "squid:S00112" })
|
||||
public <T> T runAsSystemAsTenant(final Callable<T> callable, final String tenant) {
|
||||
final SecurityContext oldContext = SecurityContextHolder.getContext();
|
||||
try {
|
||||
log.debug("Entering system code execution");
|
||||
return tenantAware.runAsTenant(tenant, () -> {
|
||||
setSystemContext(SecurityContextHolder.getContext());
|
||||
return MdcHandler.getInstance().callWithAuthRE(callable);
|
||||
});
|
||||
} finally {
|
||||
SecurityContextHolder.setContext(oldContext);
|
||||
log.debug("Leaving system code execution");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a given {@link Callable} within a system security context, which has
|
||||
* the provided {@link GrantedAuthority}s to successfully run the
|
||||
* {@link Callable}.
|
||||
*
|
||||
* The security context will be switched to the a new
|
||||
* {@link SecurityContext} and back after the callable is called.
|
||||
*
|
||||
* @param tenant under which the {@link Callable#call()} must be executed.
|
||||
* @param callable to call within the security context
|
||||
* @return the return value of the {@link Callable#call()} method.
|
||||
*/
|
||||
// The callable API throws a Exception and not a specific one
|
||||
@SuppressWarnings({ "squid:S2221", "squid:S00112" })
|
||||
public <T> T runAsControllerAsTenant(@NotEmpty final String tenant, @NotNull final Callable<T> callable) {
|
||||
final SecurityContext oldContext = SecurityContextHolder.getContext();
|
||||
List<SimpleGrantedAuthority> authorities = Collections
|
||||
.singletonList(new SimpleGrantedAuthority(SpringEvalExpressions.CONTROLLER_ROLE_ANONYMOUS));
|
||||
try {
|
||||
return tenantAware.runAsTenant(tenant, () -> {
|
||||
setCustomSecurityContext(tenant, oldContext.getAuthentication().getPrincipal(), authorities);
|
||||
return MdcHandler.getInstance().callWithAuthRE(callable);
|
||||
});
|
||||
} finally {
|
||||
SecurityContextHolder.setContext(oldContext);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {@code true} if the current running code is running as system code block.
|
||||
*/
|
||||
public boolean isCurrentThreadSystemCode() {
|
||||
return SecurityContextHolder.getContext().getAuthentication() instanceof SystemCodeAuthentication;
|
||||
}
|
||||
|
||||
public boolean hasPermission(final String permission) {
|
||||
final SecurityContext context = SecurityContextHolder.getContext();
|
||||
if (context != null) {
|
||||
final Authentication authentication = context.getAuthentication();
|
||||
if (authentication != null) {
|
||||
Collection<? extends GrantedAuthority> grantedAuthorities = authentication.getAuthorities();
|
||||
if (!ObjectUtils.isEmpty(grantedAuthorities)) {
|
||||
if (roleHierarchy != null) {
|
||||
grantedAuthorities = roleHierarchy.getReachableGrantedAuthorities(grantedAuthorities);
|
||||
}
|
||||
for (final GrantedAuthority authority : grantedAuthorities) {
|
||||
if (authority.getAuthority().equals(permission)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void setSystemContext(final SecurityContext oldContext) {
|
||||
final Authentication oldAuthentication = oldContext.getAuthentication();
|
||||
final SecurityContextImpl securityContextImpl = new SecurityContextImpl();
|
||||
securityContextImpl.setAuthentication(new SystemCodeAuthentication(oldAuthentication));
|
||||
SecurityContextHolder.setContext(securityContextImpl);
|
||||
}
|
||||
|
||||
private void setCustomSecurityContext(final String tenantId, final Object principal,
|
||||
final Collection<? extends GrantedAuthority> authorities) {
|
||||
final AnonymousAuthenticationToken authenticationToken = new AnonymousAuthenticationToken(
|
||||
UUID.randomUUID().toString(), principal, authorities);
|
||||
authenticationToken.setDetails(new TenantAwareAuthenticationDetails(tenantId, true));
|
||||
final SecurityContextImpl securityContextImpl = new SecurityContextImpl();
|
||||
securityContextImpl.setAuthentication(authenticationToken);
|
||||
SecurityContextHolder.setContext(securityContextImpl);
|
||||
}
|
||||
|
||||
/**
|
||||
* An implementation of the Spring's {@link Authentication} object which is
|
||||
* used within a system security code block and wraps the original
|
||||
* authentication object. The wrapped object contains the necessary
|
||||
* {@link SpringEvalExpressions#SYSTEM_ROLE} which is allowed to execute all
|
||||
* secured methods.
|
||||
*/
|
||||
public static final class SystemCodeAuthentication implements Authentication {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final List<SimpleGrantedAuthority> AUTHORITIES = Collections
|
||||
.singletonList(new SimpleGrantedAuthority(SpringEvalExpressions.SYSTEM_ROLE));
|
||||
private final Authentication oldAuthentication;
|
||||
|
||||
private SystemCodeAuthentication(final Authentication oldAuthentication) {
|
||||
this.oldAuthentication = oldAuthentication;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<? extends GrantedAuthority> getAuthorities() {
|
||||
return AUTHORITIES;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getCredentials() {
|
||||
return oldAuthentication != null ? oldAuthentication.getCredentials() : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getDetails() {
|
||||
return oldAuthentication != null ? oldAuthentication.getDetails() : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getPrincipal() {
|
||||
return oldAuthentication != null ? oldAuthentication.getPrincipal() : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAuthenticated() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAuthenticated(final boolean isAuthenticated) {
|
||||
// not needed
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
/**
|
||||
* 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.util;
|
||||
|
||||
import java.lang.annotation.Target;
|
||||
import java.net.URI;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.eclipse.hawkbit.security.HawkbitSecurityProperties;
|
||||
|
||||
/**
|
||||
* A utility which determines the correct IP of a connected {@link Target}. E.g
|
||||
* from a {@link HttpServletRequest}.
|
||||
*/
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
// Exception squid:S2083 - false positive, file paths not handled here
|
||||
@SuppressWarnings("squid:S2083")
|
||||
public final class IpUtil {
|
||||
|
||||
private static final String HIDDEN_IP = "***";
|
||||
private static final String SCHEME_SEPARATOR = "://";
|
||||
private static final String HTTP_SCHEME = "http";
|
||||
private static final String AMQP_SCHEME = "amqp";
|
||||
|
||||
// v4 address with (optionally) port
|
||||
private static final Pattern IPV4_ADDRESS_PATTERN = Pattern
|
||||
.compile("([0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3})(:[0-9]{1,5})?");
|
||||
private static final Pattern IPV6_ADDRESS_PATTERN = Pattern.compile("([0-9a-f]{1,4}:){7}([0-9a-f]){1,4}");
|
||||
// v6 address with [] amd (optionally) port
|
||||
private static final Pattern IPV6_ADDRESS_WITH_PORT_PATTERN = Pattern.compile(
|
||||
"\\[(?<address>([0-9a-f]{1,4}:){7}([0-9a-f]){1,4})](:[0-9]{1,5})?");
|
||||
|
||||
/**
|
||||
* Retrieves the string based IP address from a given
|
||||
* {@link HttpServletRequest} by either the configured {@link HawkbitSecurityProperties.Clients#getRemoteIpHeader()}
|
||||
* (by default X-Forwarded-For) or by the {@link HttpServletRequest#getRemoteAddr()} method.
|
||||
*
|
||||
* @param request the {@link HttpServletRequest} to determine the IP address
|
||||
* where this request has been sent from
|
||||
* @param securityProperties hawkBit security properties.
|
||||
* @return the {@link URI} based IP address from the client which sent the
|
||||
* request
|
||||
*/
|
||||
public static URI getClientIpFromRequest(final HttpServletRequest request,
|
||||
final HawkbitSecurityProperties securityProperties) {
|
||||
|
||||
return getClientIpFromRequest(request, securityProperties.getClients().getRemoteIpHeader(),
|
||||
securityProperties.getClients().isTrackRemoteIp());
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the string based IP address from a given {@link HttpServletRequest} by either the
|
||||
* forward header or by the {@link HttpServletRequest#getRemoteAddr()} method.
|
||||
*
|
||||
* @param request the {@link HttpServletRequest} to determine the IP address
|
||||
* where this request has been sent from
|
||||
* @param forwardHeader the header name containing the IP address e.g. forwarded by a
|
||||
* proxy {@code x-forwarded-for}
|
||||
* @return the {@link URI} based IP address from the client which sent the
|
||||
* request
|
||||
*/
|
||||
public static URI getClientIpFromRequest(final HttpServletRequest request, final String forwardHeader) {
|
||||
return getClientIpFromRequest(request, forwardHeader, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link URI} with scheme and host.
|
||||
*
|
||||
* @param scheme the scheme
|
||||
* @param host the host
|
||||
* @return the {@link URI}
|
||||
* @throws IllegalArgumentException If the given string not parsable
|
||||
*/
|
||||
public static URI createUri(final String scheme, final String host) {
|
||||
final boolean isIpV6 = host.indexOf(':') >= 0 && host.indexOf('.') == -1 && host.charAt(0) != '[';
|
||||
if (isIpV6) {
|
||||
return URI.create(scheme + SCHEME_SEPARATOR + "[" + host + "]");
|
||||
}
|
||||
return URI.create(scheme + SCHEME_SEPARATOR + host);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link URI} with amqp scheme and host.
|
||||
*
|
||||
* @param host the host
|
||||
* @param exchange the exchange will store in the path
|
||||
* @return the {@link URI}
|
||||
* @throws IllegalArgumentException If the given string not parse able
|
||||
*/
|
||||
public static URI createAmqpUri(final String host, final String exchange) {
|
||||
return createUri(AMQP_SCHEME, host).resolve("/" + exchange);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link URI} with http scheme and host.
|
||||
*
|
||||
* @param host the host
|
||||
* @return the {@link URI}
|
||||
* @throws IllegalArgumentException If the given string not parsable
|
||||
*/
|
||||
public static URI createHttpUri(final String host) {
|
||||
return createUri(HTTP_SCHEME, host);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if scheme contains http and uri ist not <code>null</code>.
|
||||
*
|
||||
* @param uri the uri
|
||||
* @return true = is http host false = not
|
||||
*/
|
||||
public static boolean isHttpUri(final URI uri) {
|
||||
return uri != null && HTTP_SCHEME.equals(uri.getScheme());
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if host scheme amqp and uri ist not <code>null</code>.
|
||||
*
|
||||
* @param uri the uri
|
||||
* @return true = is http host false = not
|
||||
*/
|
||||
public static boolean isAmqpUri(final URI uri) {
|
||||
return uri != null && AMQP_SCHEME.equals(uri.getScheme());
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the IP address of that {@link URI} is known, i.e. not an AQMP
|
||||
* exchange in DMF case and not HIDDEN_IP in DDI case.
|
||||
*
|
||||
* @param uri the uri
|
||||
* @return <code>true</code> if IP address is actually known by the server
|
||||
*/
|
||||
public static boolean isIpAddresKnown(final URI uri) {
|
||||
return uri != null && !(AMQP_SCHEME.equals(uri.getScheme()) || HIDDEN_IP.equals(uri.getHost()));
|
||||
}
|
||||
|
||||
private static URI getClientIpFromRequest(final HttpServletRequest request, final String forwardHeader,
|
||||
final boolean trackRemoteIp) {
|
||||
String ip;
|
||||
|
||||
if (trackRemoteIp) {
|
||||
ip = request.getHeader(forwardHeader);
|
||||
if (ip == null || (ip = findClientIpAddress(ip)) == null) {
|
||||
ip = request.getRemoteAddr();
|
||||
}
|
||||
} else {
|
||||
ip = HIDDEN_IP;
|
||||
}
|
||||
|
||||
return createHttpUri(ip);
|
||||
}
|
||||
|
||||
private static String findClientIpAddress(final String s) {
|
||||
Matcher matcher = IPV4_ADDRESS_PATTERN.matcher(s);
|
||||
if (matcher.find()) {
|
||||
return matcher.group(1);
|
||||
}
|
||||
|
||||
matcher = IPV6_ADDRESS_PATTERN.matcher(s);
|
||||
if (matcher.find()) {
|
||||
return matcher.group(0);
|
||||
}
|
||||
|
||||
matcher = IPV6_ADDRESS_WITH_PORT_PATTERN.matcher(s);
|
||||
if (matcher.find()) {
|
||||
return matcher.group("address");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Copyright (c) 2023 Bosch.IO 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.util;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.springframework.web.util.UriUtils;
|
||||
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class UrlUtils {
|
||||
|
||||
public static String decodeUriValue(String value) {
|
||||
return UriUtils.decode(value, StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* 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.im.authentication;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import io.qameta.allure.Description;
|
||||
import io.qameta.allure.Feature;
|
||||
import io.qameta.allure.Story;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
/**
|
||||
* Test {@link SpPermission}.
|
||||
*/
|
||||
@Feature("Unit Tests - Security")
|
||||
@Story("Permission Test")
|
||||
public final class SpPermissionTest {
|
||||
|
||||
@Test
|
||||
@Description("Try to double check if all permissions works as expected")
|
||||
void shouldReturnAllPermissions() {
|
||||
List<String> expected = new LinkedList<>();
|
||||
ReflectionUtils.doWithFields(SpPermission.class, f -> {
|
||||
if (ReflectionUtils.isPublicStaticFinal(f) && String.class.equals(f.getType())) {
|
||||
try {
|
||||
expected.add((String) f.get(null));
|
||||
} catch (IllegalAccessException | IllegalArgumentException e) {
|
||||
// skip
|
||||
}
|
||||
}
|
||||
});
|
||||
final Collection<String> allAuthorities = SpPermission.getAllAuthorities();
|
||||
assertThat(allAuthorities).hasSize(20);
|
||||
assertThat(allAuthorities).containsAll(expected);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
/**
|
||||
* 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.util;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.reset;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import io.qameta.allure.Description;
|
||||
import io.qameta.allure.Feature;
|
||||
import io.qameta.allure.Story;
|
||||
import org.eclipse.hawkbit.security.HawkbitSecurityProperties;
|
||||
import org.eclipse.hawkbit.security.HawkbitSecurityProperties.Clients;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@Feature("Unit Tests - Security")
|
||||
@Story("IP Util Test")
|
||||
public class IpUtilTest {
|
||||
|
||||
private static final String X_FORWARDED_FOR = HawkbitSecurityProperties.Clients.X_FORWARDED_FOR;
|
||||
private static final String KNOWN_REQUEST_HEADER = "bumlux";
|
||||
|
||||
@Mock
|
||||
private HttpServletRequest requestMock;
|
||||
|
||||
@Mock
|
||||
private Clients clientMock;
|
||||
|
||||
@Mock
|
||||
private HawkbitSecurityProperties securityPropertyMock;
|
||||
|
||||
@Test
|
||||
@Description("Tests create uri from request")
|
||||
public void getRemoteAddrFromRequestIfForwardedHeaderNotPresent() {
|
||||
final URI knownRemoteClientIP = IpUtil.createHttpUri("127.0.0.1");
|
||||
when(requestMock.getRemoteAddr()).thenReturn(knownRemoteClientIP.getHost());
|
||||
|
||||
final URI remoteAddr = IpUtil.getClientIpFromRequest(requestMock, KNOWN_REQUEST_HEADER);
|
||||
|
||||
// verify
|
||||
assertThat(remoteAddr).as("The remote address should be as the known client IP address")
|
||||
.isEqualTo(knownRemoteClientIP);
|
||||
verify(requestMock, times(1)).getHeader(KNOWN_REQUEST_HEADER);
|
||||
verify(requestMock, times(1)).getRemoteAddr();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Tests create uri from request with masked IP when IP tracking is disabled")
|
||||
public void maskRemoteAddrIfDisabled() {
|
||||
final URI knownRemoteClientIP = IpUtil.createHttpUri("***");
|
||||
when(securityPropertyMock.getClients()).thenReturn(clientMock);
|
||||
when(clientMock.getRemoteIpHeader()).thenReturn(KNOWN_REQUEST_HEADER);
|
||||
when(clientMock.isTrackRemoteIp()).thenReturn(false);
|
||||
|
||||
final URI remoteAddr = IpUtil.getClientIpFromRequest(requestMock, securityPropertyMock);
|
||||
|
||||
assertThat(remoteAddr).as("The remote address should be as the known client IP address")
|
||||
.isEqualTo(knownRemoteClientIP);
|
||||
verify(requestMock, times(0)).getHeader(KNOWN_REQUEST_HEADER);
|
||||
verify(requestMock, times(0)).getRemoteAddr();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Tests create uri from x forward header")
|
||||
public void getRemoteAddrFromXForwardedForHeader() {
|
||||
final URI knownRemoteClientIP = IpUtil.createHttpUri("10.99.99.1");
|
||||
when(requestMock.getHeader(X_FORWARDED_FOR)).thenReturn(knownRemoteClientIP.getHost());
|
||||
|
||||
final URI remoteAddr = IpUtil.getClientIpFromRequest(requestMock, "X-Forwarded-For");
|
||||
|
||||
assertThat(remoteAddr).as("The remote address should be as the known client IP address")
|
||||
.isEqualTo(knownRemoteClientIP);
|
||||
verify(requestMock, times(1)).getHeader(X_FORWARDED_FOR);
|
||||
verify(requestMock, times(0)).getRemoteAddr();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Tests client uri from request")
|
||||
public void testCreateClientHttpUri() {
|
||||
checkHostInfoResolution("0:0:0:0:0:0:0:1", "[0:0:0:0:0:0:0:1]", true);
|
||||
checkHostInfoResolution("127.0.0.1", "127.0.0.1", true);
|
||||
checkHostInfoResolution("127.0.0.1:93493", "127.0.0.1", true);
|
||||
checkHostInfoResolution("myhost", "myhost", true);
|
||||
checkHostInfoResolution("myhost.my", "myhost.my", true);
|
||||
checkHostInfoResolution("myhost.my:4233", "myhost.my", true);
|
||||
checkHostInfoResolution("[0:0:0:0:0:0:0:1]", "[0:0:0:0:0:0:0:1]", true);
|
||||
checkHostInfoResolution("[0:0:0:0:0:0:0:1]:4233", "[0:0:0:0:0:0:0:1]", true);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Tests client uri from request")
|
||||
public void testResolveClientIpFromHeader() {
|
||||
checkHostInfoResolution("0:0:0:0:0:0:0:1", "[0:0:0:0:0:0:0:1]", false);
|
||||
checkHostInfoResolution("127.0.0.1", "127.0.0.1", false);
|
||||
checkHostInfoResolution("127.0.0.1:93493", "127.0.0.1", false);
|
||||
checkHostInfoResolution("[0:0:0:0:0:0:0:1]", "[0:0:0:0:0:0:0:1]", false);
|
||||
checkHostInfoResolution("[0:0:0:0:0:0:0:1]:4233", "[0:0:0:0:0:0:0:1]", false);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Tests create http uri ipv4 and ipv6")
|
||||
public void testCreateHttpUri() {
|
||||
final String ipv4 = "10.99.99.1";
|
||||
URI httpUri = IpUtil.createHttpUri(ipv4);
|
||||
assertHttpUri(ipv4, httpUri);
|
||||
|
||||
final String host = "myhost";
|
||||
httpUri = IpUtil.createHttpUri(host);
|
||||
assertHttpUri(host, httpUri);
|
||||
|
||||
final String ipv6 = "0:0:0:0:0:0:0:1";
|
||||
httpUri = IpUtil.createHttpUri(ipv6);
|
||||
assertHttpUri("[" + ipv6 + "]", httpUri);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Tests create amqp uri ipv4 and ipv6")
|
||||
public void testCreateAmqpUri() {
|
||||
final String ipv4 = "10.99.99.1";
|
||||
URI amqpUri = IpUtil.createAmqpUri(ipv4, "path");
|
||||
assertAmqpUri(ipv4, amqpUri);
|
||||
final String ipv4Port = ipv4 + ":12000";
|
||||
amqpUri = IpUtil.createAmqpUri(ipv4Port, "path");
|
||||
assertAmqpUri(ipv4, amqpUri);
|
||||
|
||||
final String host = "myhost";
|
||||
amqpUri = IpUtil.createAmqpUri(host, "path");
|
||||
assertAmqpUri(host, amqpUri);
|
||||
|
||||
final String hostDots = "myhost.my";
|
||||
amqpUri = IpUtil.createAmqpUri(hostDots, "path");
|
||||
assertAmqpUri(hostDots, amqpUri);
|
||||
|
||||
final String ipv6 = "0:0:0:0:0:0:0:1";
|
||||
amqpUri = IpUtil.createAmqpUri(ipv6, "path");
|
||||
assertAmqpUri("[" + ipv6 + "]", amqpUri);
|
||||
|
||||
final String ipv6Braces = "[0:0:0:0:0:0:0:1]";
|
||||
amqpUri = IpUtil.createAmqpUri(ipv6Braces, "path");
|
||||
assertAmqpUri(ipv6Braces, amqpUri);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Tests create invalid uri")
|
||||
public void testCreateInvalidUri() {
|
||||
|
||||
final String host = "10.99.99.1";
|
||||
final URI testUri = IpUtil.createUri("test", host);
|
||||
|
||||
assertThat(IpUtil.isAmqpUri(testUri)).as("The given URI is not an AMQP address").isFalse();
|
||||
assertThat(IpUtil.isHttpUri(testUri)).as("The given URI is not an HTTP address").isFalse();
|
||||
assertThat(host).as("The given host matches the URI host").isEqualTo(testUri.getHost());
|
||||
|
||||
try {
|
||||
IpUtil.createUri(":/", host);
|
||||
Assertions.fail("Missing expected IllegalArgumentException due invalid URI");
|
||||
} catch (final IllegalArgumentException e) {
|
||||
// expected
|
||||
}
|
||||
}
|
||||
|
||||
private void checkHostInfoResolution(final String hostInfo, final String expectedHost, final boolean remoteAddress) {
|
||||
reset(requestMock);
|
||||
when(remoteAddress ? requestMock.getRemoteAddr() : requestMock.getHeader(KNOWN_REQUEST_HEADER)).thenReturn(hostInfo);
|
||||
|
||||
final URI remoteAddr = IpUtil.getClientIpFromRequest(requestMock, KNOWN_REQUEST_HEADER);
|
||||
|
||||
// verify
|
||||
assertThat(remoteAddr.getHost()).as("The remote address should be as the known client IP address")
|
||||
.isEqualTo(expectedHost);
|
||||
verify(requestMock, times(1)).getHeader(KNOWN_REQUEST_HEADER);
|
||||
if (remoteAddress) {
|
||||
verify(requestMock, times(1)).getRemoteAddr();
|
||||
}
|
||||
}
|
||||
|
||||
private void assertHttpUri(final String host, final URI httpUri) {
|
||||
assertThat(IpUtil.isHttpUri(httpUri)).as("The given URI has an http scheme").isTrue();
|
||||
assertThat(IpUtil.isAmqpUri(httpUri)).as("The given URI is not an AMQP scheme").isFalse();
|
||||
assertThat(host).as("The URI hosts matches the given host").isEqualTo(httpUri.getHost());
|
||||
assertThat(httpUri.getScheme()).as("The given URI scheme is http").isEqualTo("http");
|
||||
}
|
||||
|
||||
private void assertAmqpUri(final String host, final URI amqpUri) {
|
||||
|
||||
assertThat(IpUtil.isAmqpUri(amqpUri)).as("The given URI is an AMQP scheme").isTrue();
|
||||
assertThat(IpUtil.isHttpUri(amqpUri)).as("The given URI is not an HTTP scheme").isFalse();
|
||||
assertThat(amqpUri.getHost()).as("The given host matches the URI host").isEqualTo(host);
|
||||
assertThat(amqpUri.getScheme()).as("The given URI has an AMQP scheme").isEqualTo("amqp");
|
||||
assertThat(amqpUri.getRawPath()).as("The given URI has an AMQP path").isEqualTo("/path");
|
||||
}
|
||||
}
|
||||
29
hawkbit-security/pom.xml
Normal file
29
hawkbit-security/pom.xml
Normal file
@@ -0,0 +1,29 @@
|
||||
<!--
|
||||
|
||||
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</name>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<modules>
|
||||
<module>hawkbit-security-core</module>
|
||||
<module>hawkbit-security-controller</module>
|
||||
</modules>
|
||||
</project>
|
||||
Reference in New Issue
Block a user