Security artifacts moved in hawkbit-security-parent (#2016)

Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
This commit is contained in:
Avgustin Marinov
2024-11-12 12:50:36 +02:00
committed by GitHub
parent 5182217745
commit 849ea24632
34 changed files with 36 additions and 6 deletions

View 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>

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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());
}
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}

View File

@@ -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";
}

View File

@@ -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);
}
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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()));
}
}

View File

@@ -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;
}
}

View File

@@ -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
}
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View 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
*/
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);
}
}

View File

@@ -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");
}
}