Cleanup and improve the controller authentication (#2287)

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

View File

@@ -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 final 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 tenantAwareUser) {
return new TenantAwareUser(user.getUsername(), user.getPassword(), user.getAuthorities(), tenantAwareUser.getTenant());
} else {
return new User(user.getUsername(), user.getPassword(), user.getAuthorities());
}
}
}
}

View File

@@ -0,0 +1,108 @@
/**
* 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 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 = "";
}
}
}

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,210 @@
/**
* 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)
// java:S6548 - singleton holder ensures static access to spring resources in some places
// java:S112 - it is generic class so a generic exception is fine
@SuppressWarnings({ "java:S6548", "java:S112" })
public class MdcHandler {
public static final String MDC_KEY_TENANT = "tenant";
public static final String MDC_KEY_USER = "user";
private static final MdcHandler SINGLETON = new MdcHandler();
@Value("${hawkbit.logging.mdchandler.enabled:true}")
private boolean mdcEnabled;
private SpringSecurityAuditorAware springSecurityAuditorAware = new SpringSecurityAuditorAware();
/**
* @return The holder singleton instance.
*/
public static MdcHandler getInstance() {
return SINGLETON;
}
@Autowired(required = false) // spring setter injection
public void setSpringSecurityAuditorAware(final SpringSecurityAuditorAware springSecurityAuditorAware) {
this.springSecurityAuditorAware = springSecurityAuditorAware;
}
/**
* 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,107 @@
/**
* 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 lombok.AccessLevel;
import lombok.NoArgsConstructor;
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.
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@SuppressWarnings("java:S112") // accepted
class JavaSerialization implements SecurityContextSerializer {
@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,251 @@
/**
* 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.TenantAware;
import org.eclipse.hawkbit.tenancy.TenantAwareAuthenticationDetails;
import org.eclipse.hawkbit.tenancy.TenantAwareUser;
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 =
List.of(new SimpleGrantedAuthority(SpringEvalExpressions.SYSTEM_ROLE));
private final UserAuthoritiesResolver authoritiesResolver;
private final SecurityContextSerializer securityContextSerializer;
private final TenantResolver tenantResolver;
/**
* 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, null, null);
}
/**
* 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, securityContextSerializer, null);
}
/**
* 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,
@Nullable final TenantResolver tenantResolver) {
this.authoritiesResolver = authoritiesResolver;
this.securityContextSerializer = securityContextSerializer == null ? SecurityContextSerializer.NOP : securityContextSerializer;
this.tenantResolver = tenantResolver == null ? new DefaultTenantResolver() : tenantResolver;
}
@Override
public String getCurrentTenant() {
return tenantResolver.resolveTenant();
}
@Override
public String getCurrentUsername() {
final SecurityContext context = SecurityContextHolder.getContext();
if (context.getAuthentication() != null) {
final Object principal = context.getAuthentication().getPrincipal();
if (principal instanceof OidcUser oidcUser) {
return oidcUser.getPreferredUsername();
}
if (principal instanceof User user) {
return user.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,77 @@
/**
* 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.eclipse.hawkbit.tenancy.TenantAwareAuthenticationDetails;
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.getDetails() instanceof TenantAwareAuthenticationDetails tenantAwareDetails && tenantAwareDetails.isController()) {
return "CONTROLLER_PLUG_AND_PLAY";
}
if (authentication.getPrincipal() instanceof UserDetails userDetails) {
return userDetails.getUsername();
}
if (authentication.getPrincipal() instanceof OidcUser oidcUser) {
return oidcUser.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,246 @@
/**
* 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.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.TenantAware;
import org.eclipse.hawkbit.tenancy.TenantAwareAuthenticationDetails;
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 its 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.
* <br/>
* The security context will be switched to the system code and back after
* the callable is called.
* <br/>
* 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 its 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}.
* <br/>
* The security context will be switched to 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 {
@Serial
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,179 @@
/**
* 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,49 @@
/**
* 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 io.qameta.allure.Description;
import io.qameta.allure.Feature;
import io.qameta.allure.Story;
import org.junit.jupiter.api.Test;
import org.springframework.util.ReflectionUtils;
/**
* Test {@link SpPermission}.
*/
@Feature("Unit Tests - Security")
@Story("Permission Test")
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)
.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")
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")
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")
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")
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")
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")
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")
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")
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")
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");
}
}