Refactor hawkbit core and security (#2833)

* Refactor hawkbit core and security

* improve access to the base core features - static
* thus easiear access
* and less boilerplate passing of instances

Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>

* Refactor context classes

* make JSON context serialization default

* AccessContext

* Split hawkbit-security-core to other modules and remove it

---------

Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
This commit is contained in:
Avgustin Marinov
2025-11-27 13:07:49 +02:00
committed by GitHub
parent 58dbc32a80
commit f6f62db0ad
274 changed files with 2534 additions and 4458 deletions

View File

@@ -1,61 +0,0 @@
/**
* 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;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import org.eclipse.hawkbit.tenancy.TenantAware;
/**
* {@link ContextAware} provides means for getting the current context (via {@link #getCurrentContext()}) and then
* to execute a {@link Runnable} or a {@link Function} in the same context using {@link #runInContext(String, Runnable)}
* or {@link #runInContext(String, Function, Object)}.
* <p/>
* This is useful for scheduled background operations like rollouts and auto assignments where they shall
* be processed in the scope of the creator.
*/
public interface ContextAware extends TenantAware {
/**
* Return the current context encoded as a {@link String}. Depending on the implementation it could,
* for instance, be a serialized context or a reference to such.
*
* @return could be empty if there is nothing to serialize or context aware is not supported.
*/
Optional<String> getCurrentContext();
/**
* Wrap a specific execution in a known and pre-serialized context.
*
* @param <T> the type of the input to the function
* @param <R> the type of the result of the function
* @param serializedContext created by {@link #getCurrentContext()}. Must be non-<code>null</code>.
* @param function function to call in the reconstructed context. Must be non-<code>null</code>.
* @param t the argument that will be passed to the function
* @return the function result
*/
<T, R> R runInContext(String serializedContext, Function<T, R> function, T t);
/**
* Wrap a specific execution in a known and pre-serialized context.
*
* @param serializedContext created by {@link #getCurrentContext()}. Must be non-<code>null</code>.
* @param runnable runnable to call in the reconstructed context. Must be non-<code>null</code>.
*/
default void runInContext(String serializedContext, Runnable runnable) {
Objects.requireNonNull(runnable);
runInContext(serializedContext, v -> {
runnable.run();
return null;
}, null);
}
}

View File

@@ -0,0 +1,28 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.audit;
import java.util.Optional;
import lombok.NoArgsConstructor;
import org.eclipse.hawkbit.context.AccessContext;
@NoArgsConstructor(access = lombok.AccessLevel.PRIVATE)
@SuppressWarnings("java:S6548") // java:S6548 - singleton holder ensures static access to spring resources in some places
public class AuditContextProvider {
public static AuditContext getAuditContext() {
return new AuditContext(
Optional.ofNullable(AccessContext.tenant()).orElse("n/a"),
Optional.ofNullable(AccessContext.actor()).orElse(AccessContext.SYSTEM_ACTOR));
}
public record AuditContext(String tenant, String username) {}
}

View File

@@ -0,0 +1,40 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.audit;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AuditLog {
enum Level {
INFO, WARN, ERROR
}
enum Type {
CREATE, READ, UPDATE, DELETE, EXECUTE
}
Level level() default Level.INFO;
Type type();
String entity();
String description() default "";
String[] logParams() default {"*"};
boolean logResponse() default false;
}

View File

@@ -0,0 +1,65 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.audit;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class AuditLogger {
public static void info(final String entity, final String message) {
logMessage(entity, message, AuditLog.Level.INFO);
}
public static void info(final String tenant, final String username, final String entity, final String message) {
logMessage(tenant, username, entity, message, AuditLog.Level.INFO);
}
public static void error(final String entity, final String message) {
logMessage(entity, message, AuditLog.Level.ERROR);
}
public static void error(final String tenant, final String username, final String entity, final String message) {
logMessage(tenant, username, entity, message, AuditLog.Level.ERROR);
}
public static void warn(final String entity, final String message) {
logMessage(entity, message, AuditLog.Level.WARN);
}
public static void warn(final String tenant, final String username, final String entity, final String message) {
logMessage(tenant, username, entity, message, AuditLog.Level.WARN);
}
private static void logMessage(final String entity, final String message, final AuditLog.Level level) {
final AuditContextProvider.AuditContext auditContext = AuditContextProvider.getAuditContext();
logMessage(auditContext.tenant(), auditContext.username(), entity, message, level);
}
private static void logMessage(
final String tenant, final String username, final String entity, final String message, final AuditLog.Level level) {
final String logMessage = String.format("[%s] User: %s, AccessContext: %s - %s", entity, username, tenant, message);
final Logger auditLogger = LoggerFactory.getLogger("AUDIT" + (entity != null ? ("." + entity.toUpperCase()) : ""));
switch (level) {
case INFO:
auditLogger.info(logMessage);
break;
case WARN:
auditLogger.warn(logMessage);
break;
case ERROR:
auditLogger.error(logMessage);
break;
}
}
}

View File

@@ -0,0 +1,147 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.audit;
import java.util.Arrays;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import lombok.Builder;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
@Slf4j
@Aspect
@Component
public class AuditLoggingAspect {
/**
* Provides around advice for methods annotated with {@code @AuditLog}.
* <p>
* By default, all method parameters are logged. To restrict logging to specific parameters,
* specify them via the {@code logParams} attribute (e.g., {@code = {"param1", "param2"}}).
* To disable parameter logging, use an empty array (i.e., {@code logParams = {}}).
* </p>
* <p>
* This advice logs the request details and, if {@code logResponse} is set to {@code true},
* logs the method response as well.
* </p>
*/
@Around("@annotation(auditLog)")
public Object handleAuditLogging(final ProceedingJoinPoint joinPoint, final AuditLog auditLog) throws Throwable {
try {
final Object result = joinPoint.proceed();
try { // log success
final ResultMessage resultMessage = getResultMessage(result, auditLog);
final String paramsToLog = getParamsToLog(joinPoint, auditLog);
logAudit(joinPoint, auditLog, resultMessage.message(), paramsToLog, resultMessage.level());
} catch (final Throwable logError) {
// should never fail!
log.debug("Failed to log success", logError);
}
return result;
} catch (final Throwable t) {
try {
final String paramsToLog = getParamsToLog(joinPoint, auditLog);
logAudit(joinPoint, auditLog, t.getMessage(), paramsToLog, AuditLog.Level.ERROR);
} catch (final Throwable logError) {
// should never fail!
log.debug("Failed to log error", logError);
}
throw t;
}
}
/**
* Logs both the request details and the response.
*/
private void logAudit(
final JoinPoint joinPoint,
final AuditLog auditLog, final String resultMessage, final String paramsToLog, final AuditLog.Level logLevel) {
final String methodName = joinPoint.getSignature().getName();
final String logMessage = String.format(
"Type: %s, Method: %s - Description: %s - Parameters: %s - Response: %s",
auditLog.type(), methodName, auditLog.description(), paramsToLog, resultMessage
);
switch (logLevel) {
case INFO:
AuditLogger.info(auditLog.entity(), logMessage);
break;
case WARN:
AuditLogger.warn(auditLog.entity(), logMessage);
break;
case ERROR:
AuditLogger.error(auditLog.entity(), logMessage);
break;
}
}
private String getParamsToLog(final JoinPoint joinPoint, final AuditLog auditLog) {
final Object[] args = joinPoint.getArgs();
final String[] logParams = auditLog.logParams();
if (isLogAll(logParams)) {
return Arrays.deepToString(args);
} else {
final MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
final String[] paramNames = methodSignature.getParameterNames();
final Map<String, Object> paramMap = IntStream.range(0, paramNames.length)
.boxed()
.collect(Collectors.toMap(i -> paramNames[i], i -> args[i]));
return Arrays.stream(logParams)
.filter(paramMap::containsKey)
.map(name -> name + "=" + paramMap.get(name))
.collect(Collectors.joining(", "));
}
}
private ResultMessage getResultMessage(final Object result, final AuditLog auditLog) {
final ResultMessage.ResultMessageBuilder resultMessageBuilder = ResultMessage.builder();
if (result instanceof ResponseEntity<?> responseEntity) {
int statusCode = responseEntity.getStatusCode().value();
if (statusCode >= 200 && statusCode < 300) {
resultMessageBuilder.level(AuditLog.Level.INFO);
if (auditLog.logResponse()) {
resultMessageBuilder.message(result.toString());
} else {
resultMessageBuilder.message("OK - " + statusCode);
}
} else {
resultMessageBuilder.level(AuditLog.Level.WARN);
if (auditLog.logResponse()) {
resultMessageBuilder.message(result.toString());
} else {
resultMessageBuilder.message("FAILED - " + statusCode);
}
}
return resultMessageBuilder.build();
}
resultMessageBuilder.message(result != null ? result.toString() : "null");
resultMessageBuilder.level(auditLog.level());
return resultMessageBuilder.build();
}
private boolean isLogAll(String [] logParams) {
return Arrays.asList(logParams).contains("*");
}
@Builder
private record ResultMessage(String message, AuditLog.Level level) {}
}

View File

@@ -0,0 +1,24 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.audit;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Constants related to security.
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class SecurityLogger {
public static final Logger LOGGER = LoggerFactory.getLogger("SERVER.SECURITY");
}

View File

@@ -0,0 +1,37 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.auth;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class Hierarchy {
// @formatter:off
public static final String DEFAULT =
SpPermission.TARGET_HIERARCHY +
SpPermission.SOFTWARE_MODULE_HIERARCHY +
SpPermission.DISTRIBUTION_SET_HIERARCHY +
SpPermission.TENANT_CONFIGURATION_HIERARCHY +
SpRole.DEFAULT_ROLE_HIERARCHY;
// @formatter:on
private static RoleHierarchy roleHierarchy;
public static RoleHierarchy getRoleHierarchy() {
return roleHierarchy;
}
public static void setRoleHierarchy(final RoleHierarchy roleHierarchy) {
Hierarchy.roleHierarchy = roleHierarchy;
}
}

View File

@@ -0,0 +1,196 @@
/**
* 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.auth;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.ObjectUtils;
import org.springframework.util.function.SingletonSupplier;
/**
* <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 prefixes
public static final String CREATE_PREFIX = "CREATE_";
public static final String READ_PREFIX = "READ_";
public static final String UPDATE_PREFIX = "UPDATE_";
public static final String DELETE_PREFIX = "DELETE_";
// Permission groups
public static final String TARGET = "TARGET";
public static final String TARGET_TYPE = "TARGET_TYPE";
public static final String SOFTWARE_MODULE = "SOFTWARE_MODULE";
public static final String SOFTWARE_MODULE_TYPE = "SOFTWARE_MODULE_TYPE";
public static final String DISTRIBUTION_SET = "DISTRIBUTION_SET";
public static final String DISTRIBUTION_SET_TYPE = "DISTRIBUTION_SET_TYPE";
public static final String ROLLOUT = "ROLLOUT";
public static final String TENANT_CONFIGURATION = "TENANT_CONFIGURATION";
public static final String CREATE_TARGET = CREATE_PREFIX + TARGET;
public static final String READ_TARGET = READ_PREFIX + TARGET;
public static final String UPDATE_TARGET = UPDATE_PREFIX + TARGET;
public static final String DELETE_TARGET = DELETE_PREFIX + 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_SECURITY_TOKEN = READ_TARGET + "_SECURITY_TOKEN";
public static final String READ_TARGET_TYPE = READ_PREFIX + TARGET_TYPE;
public static final String UPDATE_TARGET_TYPE = UPDATE_PREFIX + TARGET_TYPE;
public static final String DELETE_TARGET_TYPE = DELETE_PREFIX + TARGET_TYPE;
public static final String READ_DISTRIBUTION_SET = READ_PREFIX + DISTRIBUTION_SET;
public static final String UPDATE_DISTRIBUTION_SET = UPDATE_PREFIX + DISTRIBUTION_SET;
public static final String READ_SOFTWARE_MODULE_ARTIFACT = READ_PREFIX + SOFTWARE_MODULE + "_ARTIFACT";
/**
* Permission to read the tenant settings.
*/
public static final String READ_TENANT_CONFIGURATION = READ_PREFIX + 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_SECURITY_TOKEN = "READ_GATEWAY_SECURITY_TOKEN";
public static final String CREATE_ROLLOUT = CREATE_PREFIX + ROLLOUT;
public static final String READ_ROLLOUT = READ_PREFIX + ROLLOUT;
public static final String UPDATE_ROLLOUT = UPDATE_PREFIX + ROLLOUT;
public static final String DELETE_ROLLOUT = DELETE_PREFIX + ROLLOUT;
/** Permission to approve or deny a rollout prior to starting. */
public static final String APPROVE_ROLLOUT = "APPROVE_" + ROLLOUT;
/** Permission to start/stop/resume a rollout. */
public static final String HANDLE_ROLLOUT = "HANDLE_" + ROLLOUT;
/** 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";
public static final String IMPLY = " > ";
public static final String IMPLY_CREATE = IMPLY + CREATE_PREFIX;
public static final String IMPLY_READ = IMPLY + READ_PREFIX;
public static final String IMPLY_UPDATE = IMPLY + UPDATE_PREFIX;
public static final String IMPLY_DELETE = IMPLY + DELETE_PREFIX;
public static final String LINE_BREAK = "\n";
// @formatter:off
public static final String TARGET_HIERARCHY =
CREATE_TARGET + IMPLY_READ + TARGET_TYPE + LINE_BREAK +
READ_TARGET + IMPLY_READ + TARGET_TYPE + LINE_BREAK +
UPDATE_TARGET + IMPLY_READ + TARGET_TYPE + LINE_BREAK +
DELETE_TARGET + IMPLY_READ + TARGET_TYPE + LINE_BREAK;
public static final String SOFTWARE_MODULE_HIERARCHY =
CREATE_PREFIX + SOFTWARE_MODULE + IMPLY_READ + SOFTWARE_MODULE_TYPE + LINE_BREAK +
READ_PREFIX + SOFTWARE_MODULE + IMPLY_READ + SOFTWARE_MODULE_TYPE + LINE_BREAK +
UPDATE_PREFIX + SOFTWARE_MODULE + IMPLY_READ + SOFTWARE_MODULE_TYPE + LINE_BREAK +
DELETE_PREFIX + SOFTWARE_MODULE + IMPLY_READ + SOFTWARE_MODULE_TYPE + LINE_BREAK;
public static final String DISTRIBUTION_SET_HIERARCHY =
CREATE_PREFIX + DISTRIBUTION_SET + IMPLY_READ + DISTRIBUTION_SET_TYPE + LINE_BREAK +
READ_PREFIX + DISTRIBUTION_SET + IMPLY_READ + DISTRIBUTION_SET_TYPE + LINE_BREAK +
UPDATE_PREFIX + DISTRIBUTION_SET + IMPLY_READ + DISTRIBUTION_SET_TYPE + LINE_BREAK +
DELETE_PREFIX + DISTRIBUTION_SET + IMPLY_READ + DISTRIBUTION_SET_TYPE + LINE_BREAK;
public static final String TENANT_CONFIGURATION_HIERARCHY =
TENANT_CONFIGURATION + IMPLY_CREATE + TENANT_CONFIGURATION + LINE_BREAK +
TENANT_CONFIGURATION + IMPLY_READ + TENANT_CONFIGURATION + LINE_BREAK +
TENANT_CONFIGURATION + IMPLY_UPDATE + TENANT_CONFIGURATION + LINE_BREAK +
TENANT_CONFIGURATION + IMPLY_DELETE + TENANT_CONFIGURATION + LINE_BREAK +
TENANT_CONFIGURATION + IMPLY + READ_GATEWAY_SECURITY_TOKEN + LINE_BREAK;
// @formatter:on
private static final SingletonSupplier<Set<String>> ALL_AUTHORITIES = SingletonSupplier.of(() -> getAuthorities(false));
private static final SingletonSupplier<Set<String>> ALL_TENANT_AUTHORITIES = SingletonSupplier.of(() -> getAuthorities(true));
private static Set<String> getAuthorities(final boolean tenant) {
final Set<String> allPermissions = new HashSet<>();
// groups with access, canonical
for (final String group : new String[] {
TARGET, TARGET_TYPE,
SOFTWARE_MODULE, SOFTWARE_MODULE_TYPE, DISTRIBUTION_SET, DISTRIBUTION_SET_TYPE,
ROLLOUT,
TENANT_CONFIGURATION }) {
for (final String access_prefix : new String[] { CREATE_PREFIX, READ_PREFIX, UPDATE_PREFIX, DELETE_PREFIX }) {
allPermissions.add(access_prefix + group);
}
}
// special
allPermissions.add(READ_TARGET_SECURITY_TOKEN);
allPermissions.add(READ_GATEWAY_SECURITY_TOKEN);
allPermissions.add(READ_SOFTWARE_MODULE_ARTIFACT);
allPermissions.add(APPROVE_ROLLOUT);
allPermissions.add(HANDLE_ROLLOUT);
allPermissions.add(TENANT_CONFIGURATION);
if (!tenant) {
// system permission, (!) take care with
allPermissions.add(SYSTEM_ADMIN);
}
return Collections.unmodifiableSet(allPermissions);
}
public static Set<String> getAllAuthorities() {
return ALL_AUTHORITIES.get();
}
public static Set<String> getAllTenantAuthorities() {
return ALL_TENANT_AUTHORITIES.get();
}
@SuppressWarnings("java:S3776") // java:S3776 - better in one place for better readability
public static 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();
final RoleHierarchy roleHierarchy = Hierarchy.getRoleHierarchy();
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;
}
}

View File

@@ -0,0 +1,90 @@
/**
* 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.auth;
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";
/** 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";
/** The role which contains in the spring security context in case a 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";
private static final String IMPLIES = " > ";
private static final String LINE_BREAK = "\n";
// @formatter:off
public static final String TARGET_ADMIN_HIERARCHY =
TARGET_ADMIN + IMPLIES + SpPermission.CREATE_TARGET + LINE_BREAK +
TARGET_ADMIN + IMPLIES + SpPermission.READ_TARGET + LINE_BREAK +
TARGET_ADMIN + IMPLIES + SpPermission.READ_TARGET_SECURITY_TOKEN + LINE_BREAK +
TARGET_ADMIN + IMPLIES + SpPermission.UPDATE_TARGET + LINE_BREAK +
TARGET_ADMIN + IMPLIES + SpPermission.DELETE_TARGET + LINE_BREAK +
TARGET_ADMIN + IMPLIES + SpPermission.CREATE_PREFIX + SpPermission.TARGET_TYPE + LINE_BREAK +
TARGET_ADMIN + IMPLIES + SpPermission.READ_TARGET_TYPE + LINE_BREAK +
TARGET_ADMIN + IMPLIES + SpPermission.UPDATE_TARGET_TYPE + LINE_BREAK +
TARGET_ADMIN + IMPLIES + SpPermission.DELETE_TARGET_TYPE + LINE_BREAK;
public static final String REPOSITORY_ADMIN_HIERARCHY =
REPOSITORY_ADMIN + IMPLIES + SpPermission.CREATE_PREFIX + SpPermission.SOFTWARE_MODULE + LINE_BREAK +
REPOSITORY_ADMIN + IMPLIES + SpPermission.READ_PREFIX + SpPermission.SOFTWARE_MODULE + LINE_BREAK +
REPOSITORY_ADMIN + IMPLIES + SpPermission.UPDATE_PREFIX + SpPermission.SOFTWARE_MODULE + LINE_BREAK +
REPOSITORY_ADMIN + IMPLIES + SpPermission.DELETE_PREFIX + SpPermission.SOFTWARE_MODULE + LINE_BREAK +
REPOSITORY_ADMIN + IMPLIES + SpPermission.READ_SOFTWARE_MODULE_ARTIFACT + LINE_BREAK +
REPOSITORY_ADMIN + IMPLIES + SpPermission.CREATE_PREFIX + SpPermission.SOFTWARE_MODULE_TYPE + LINE_BREAK +
REPOSITORY_ADMIN + IMPLIES + SpPermission.READ_PREFIX + SpPermission.SOFTWARE_MODULE_TYPE + LINE_BREAK +
REPOSITORY_ADMIN + IMPLIES + SpPermission.UPDATE_PREFIX + SpPermission.SOFTWARE_MODULE_TYPE + LINE_BREAK +
REPOSITORY_ADMIN + IMPLIES + SpPermission.DELETE_PREFIX + SpPermission.SOFTWARE_MODULE_TYPE + LINE_BREAK +
REPOSITORY_ADMIN + IMPLIES + SpPermission.CREATE_PREFIX + SpPermission.DISTRIBUTION_SET + LINE_BREAK +
REPOSITORY_ADMIN + IMPLIES + SpPermission.READ_PREFIX + SpPermission.DISTRIBUTION_SET + LINE_BREAK +
REPOSITORY_ADMIN + IMPLIES + SpPermission.UPDATE_PREFIX + SpPermission.DISTRIBUTION_SET + LINE_BREAK +
REPOSITORY_ADMIN + IMPLIES + SpPermission.DELETE_PREFIX + SpPermission.DISTRIBUTION_SET + LINE_BREAK +
REPOSITORY_ADMIN + IMPLIES + SpPermission.CREATE_PREFIX + SpPermission.DISTRIBUTION_SET_TYPE + LINE_BREAK +
REPOSITORY_ADMIN + IMPLIES + SpPermission.READ_PREFIX + SpPermission.DISTRIBUTION_SET_TYPE + LINE_BREAK +
REPOSITORY_ADMIN + IMPLIES + SpPermission.UPDATE_PREFIX + SpPermission.DISTRIBUTION_SET_TYPE + LINE_BREAK +
REPOSITORY_ADMIN + IMPLIES + SpPermission.DELETE_PREFIX + SpPermission.DISTRIBUTION_SET_TYPE + LINE_BREAK;
public static final String ROLLOUT_ADMIN_HIERARCHY =
ROLLOUT_ADMIN + IMPLIES + SpPermission.CREATE_ROLLOUT + LINE_BREAK +
ROLLOUT_ADMIN + IMPLIES + SpPermission.READ_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_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_ROLE_HIERARCHY =
SYSTEM_ROLE + IMPLIES + TENANT_ADMIN + LINE_BREAK +
SYSTEM_ROLE + IMPLIES + SpPermission.SYSTEM_ADMIN + LINE_BREAK;
public static final String DEFAULT_ROLE_HIERARCHY =
TARGET_ADMIN_HIERARCHY +
REPOSITORY_ADMIN_HIERARCHY +
ROLLOUT_ADMIN_HIERARCHY +
TENANT_ADMIN_HIERARCHY +
SYSTEM_ROLE_HIERARCHY;
// @formatter:on
}

View File

@@ -0,0 +1,50 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.auth;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
/**
* <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
* auth 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 final class SpringEvalExpressions {
public static final String IS_SYSTEM_CODE = "hasAuthority('ROLE_SYSTEM_CODE')";
public static final String HAS_AUTH_SYSTEM_ADMIN = "hasAuthority('SYSTEM_ADMIN')";
public static final String PERMISSION_GROUP_PLACEHOLDER = "${permissionGroup}";
// evaluated to <permission>_<permissionGroup> (e.g. CREATE_DISTRIBUTION_SET)
public static final String HAS_CREATE_REPOSITORY = "hasPermission(#root, 'CREATE_${permissionGroup}')";
public static final String HAS_READ_REPOSITORY = "hasPermission(#root, 'READ_${permissionGroup}')";
public static final String HAS_UPDATE_REPOSITORY = "hasPermission(#root, 'UPDATE_${permissionGroup}')";
public static final String HAS_DELETE_REPOSITORY = "hasPermission(#root, 'DELETE_${permissionGroup}')";
public static final String IS_CONTROLLER = "hasAnyRole('" + SpRole.CONTROLLER_ROLE_ANONYMOUS + "', '" + SpRole.CONTROLLER_ROLE + "')";
}

View File

@@ -0,0 +1,139 @@
/**
* 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.auth;
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) {
super(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);
}
public static final Pattern HAS_SCHEMA = Pattern.compile("^\\{[^{]+}.+$");
private static String password(final String password) {
return !HAS_SCHEMA.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,449 @@
/**
* 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.context;
import java.io.Serial;
import java.io.Serializable;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Supplier;
import java.util.stream.Stream;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.AccessLevel;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.auth.SpRole;
import org.eclipse.hawkbit.tenancy.TenantAwareAuthenticationDetails;
import org.eclipse.hawkbit.tenancy.TenantAwareUser;
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.UserDetails;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
/**
* A 'static' class providing methods related to access context:
* <ul>
* <li>read / lookup - find out the current tenant, principal (actor), security context</li>
* <li>switch context - run code as system, as system but scoped for a tenant, with a specific context</li>
* </ul>
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@Slf4j
public class AccessContext {
// Note! There shall be no regular 'system'!
public static final String SYSTEM_ACTOR = "system";
/**
* Return the current context encoded as a {@link String}. Depending on the implementation it could,
* for instance, be a serialized context or a reference to such.
*
* @return could be empty if there is nothing to serialize or context aware is not supported.
*/
public static Optional<String> securityContext() {
return Optional.ofNullable(SecurityContextHolder.getContext()).map(AccessContext::serialize);
}
/**
* Implementation might retrieve the current tenant from a session or thread-local.
*
* @return the current tenant
*/
public static String tenant() {
final SecurityContext context = SecurityContextHolder.getContext();
if (context.getAuthentication() != null) {
final Object principal = context.getAuthentication().getPrincipal();
if (context.getAuthentication().getDetails() instanceof TenantAwareAuthenticationDetails tenantAwareAuthenticationDetails) {
return tenantAwareAuthenticationDetails.tenant();
} else if (principal instanceof TenantAwareUser tenantAwareUser) {
return tenantAwareUser.getTenant();
}
}
return null;
}
// 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> ACTOR_OVERRIDE = new ThreadLocal<>();
// Return the current actor / auditor / principal name. It could be a user (person), technical user, device, etc.
public static String actor() {
if (ACTOR_OVERRIDE.get() != null) {
return ACTOR_OVERRIDE.get();
} else {
final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (isAuthenticationInvalid(authentication)) {
return null;
} else {
return resolve(authentication);
}
}
}
/**
* Wrap a specific execution in a known and pre-serialized context.
*
* @param serializedContext created by {@link #securityContext()}. Must be non-<code>null</code>.
* @param runnable runnable to run in the reconstructed context. Must be non-<code>null</code>.
*/
public static void withSecurityContext(final String serializedContext, final Runnable runnable) {
Objects.requireNonNull(runnable);
withSecurityContext(serializedContext, () -> {
runnable.run();
return null;
});
}
/**
* Wrap a specific execution / call in a known and pre-serialized context.
*
* @param <T> the type of the output of the supplier
* @param serializedContext created by {@link #securityContext()}. Must be non-<code>null</code>.
* @param supplier function to call in the reconstructed context. Must be non-<code>null</code>.
* @return the function result
*/
public static <T> T withSecurityContext(final String serializedContext, final Supplier<T> supplier) {
Objects.requireNonNull(serializedContext);
Objects.requireNonNull(supplier);
final SecurityContext securityContext = deserialize(serializedContext);
Objects.requireNonNull(securityContext);
return withSecurityContext(securityContext, supplier);
}
public static <T> T withSecurityContext(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 Mdc.withAuthRe(supplier::get);
} finally {
SecurityContextHolder.setContext(originalContext);
}
}
}
public static void asActor(final String actor, final Runnable runnable) {
asActor(actor, () -> {
runnable.run();
return null;
});
}
public static <T> T asActor(final String actor, final Supplier<T> supplier) {
final String currentAuditor = ACTOR_OVERRIDE.get();
try {
setActor(actor);
return supplier.get();
} finally {
setActor(currentAuditor);
}
}
/**
* Runs a given {@link Runnable} 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 supplier is called. <br/>
* The system code is executed for a current tenant by using the {@link AccessContext#tenant()}.
*
* @param runnable the runnable to run within the system security context
*/
public static void asSystem(final Runnable runnable) {
asSystemAsTenant(tenant(), () -> {
runnable.run();
return null;
});
}
/**
* Runs a given {@link Supplier} 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 supplier is called. <br/>
* The system code is executed for a current tenant by using the {@link AccessContext#tenant()}.
*
* @param supplier the supplier to call within the system security context
* @return the return value of the {@link Supplier#get()} method.
*/
public static <T> T asSystem(final Supplier<T> supplier) {
return asSystemAsTenant(tenant(), supplier);
}
/**
* Runs a given {@link Runnable} 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 runnable is run.<br/>
* The system code is executed for a specific given tenant by using the {@link AccessContext}.
*
* @param tenant the tenant to act as system code
* @param runnable the runnable to run within the system security context
*/
public static void asSystemAsTenant(final String tenant, final Runnable runnable) {
asSystemAsTenant(tenant, () -> {
runnable.run();
return null;
});
}
/**
* Runs a given {@link Supplier} 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 supplier is run.<br/>
* The system code is executed for a specific given tenant by using the {@link AccessContext}.
*
* @param tenant the tenant to act as system code
* @param supplier the supplier to call within the system security context
* @return the return value of the {@link Supplier#get()} method.
*/
public static <T> T asSystemAsTenant(final String tenant, final Supplier<T> supplier) {
final SecurityContext currentContext = SecurityContextHolder.getContext();
try {
log.debug("Entering system code execution");
final SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
securityContext.setAuthentication(new SystemCodeAuthentication(tenant));
return withSecurityContext(securityContext, supplier);
} finally {
SecurityContextHolder.setContext(currentContext);
log.debug("Leaving system code execution");
}
}
private static void setActor(final String currentAuditor) {
if (currentAuditor == null) {
ACTOR_OVERRIDE.remove();
} else {
ACTOR_OVERRIDE.set(currentAuditor);
}
}
/**
* @return {@code true} if the current running code is running as system code block.
*/
public static boolean isCurrentThreadSystemCode() {
return SecurityContextHolder.getContext().getAuthentication() instanceof SystemCodeAuthentication;
}
private static String resolve(final Authentication authentication) {
if (authentication.getDetails() instanceof TenantAwareAuthenticationDetails tenantAwareDetails && tenantAwareDetails.controller()) {
return "CONTROLLER_PLUG_AND_PLAY";
}
final Object principal = authentication.getPrincipal();
if (principal instanceof ActorAware actorAware) {
return actorAware.getActor();
}
if (principal instanceof UserDetails userDetails) {
return userDetails.getUsername();
}
if (principal instanceof OidcUser oidcUser) {
return oidcUser.getPreferredUsername();
}
return principal.toString();
}
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
/**
* Return security context as string (could be just a reference)
*
* @param securityContext the security context
* @return the securityContext as string
*/
@SuppressWarnings("java:S112") // java:S112 - generic method
private static String serialize(final SecurityContext securityContext) {
Objects.requireNonNull(securityContext);
try {
return OBJECT_MAPPER.writeValueAsString(new SecCtxInfo(securityContext));
} catch (final JsonProcessingException e) {
throw new RuntimeException(e);
}
}
/**
* Deserialize security context
*
* @param securityContextString string representing the security context
* @return deserialized security context
*/
@SuppressWarnings("java:S112") // java:S112 - generic method
private static SecurityContext deserialize(final String securityContextString) {
Objects.requireNonNull(securityContextString);
final String securityContextTrimmed = securityContextString.trim();
try {
return OBJECT_MAPPER.readerFor(SecCtxInfo.class).<SecCtxInfo> readValue(securityContextTrimmed).toSecurityContext();
} catch (final JsonProcessingException e) {
throw new RuntimeException(e);
}
}
private static boolean isAuthenticationInvalid(final Authentication authentication) {
return authentication == null || !authentication.isAuthenticated() || authentication.getPrincipal() == null;
}
public interface ActorAware {
String getActor();
}
// simplified info for the security context keeping just the basic info needed for background execution of
// controller auth is not supported - always is false
// only authenticated user is supported
@NoArgsConstructor
@Data
private static class SecCtxInfo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private String tenant;
// auditor / username (auth principal name)
private String auditor = "n/a"; // default value "n/a" is used only on deserialization if field is missing
@JsonProperty(required = true)
private String[] authorities;
private SecCtxInfo(final SecurityContext securityContext) {
final Authentication authentication = securityContext.getAuthentication();
if (!authentication.isAuthenticated()) {
throw new IllegalStateException("Only authenticated context could be serialized");
}
if (authentication.getDetails() instanceof TenantAwareAuthenticationDetails tenantAwareDetails) {
if (tenantAwareDetails.controller()) {
throw new IllegalStateException("Controller auth context is not supported");
}
tenant = tenantAwareDetails.tenant();
} else if (authentication.getPrincipal() instanceof TenantAwareUser tenantAwareUser) {
tenant = tenantAwareUser.getTenant();
}
// keep the auditor, ofr audit purposes,
// sets principal to the resolved auditor and then deserialized auth will return it as principal
// since the class is not known to auditor aware - it shall used default - principal as auditor
auditor = resolve(authentication);
authorities = authentication.getAuthorities().stream().map(Object::toString).toArray(String[]::new);
}
private SecurityContext toSecurityContext() {
final SecurityContext ctx = SecurityContextHolder.createEmptyContext();
final Object details = tenant == null ? null : new TenantAwareAuthenticationDetails(tenant, false);
final ActorAware principal = () -> auditor;
final Collection<? extends GrantedAuthority> grantedAuthorities =
Stream.of(authorities).map(SimpleGrantedAuthority::new).toList();
ctx.setAuthentication(new Authentication() {
@Override
public Object getPrincipal() {
return principal;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return grantedAuthorities;
}
@Override
public boolean isAuthenticated() {
return true;
}
@Override
public Object getDetails() {
return details;
}
@Override
public Object getCredentials() {
return null;
}
@Override
public void setAuthenticated(final boolean isAuthenticated) throws IllegalArgumentException {
throw new UnsupportedOperationException();
}
@Override
public String getName() {
return auditor;
}
});
return ctx;
}
}
/**
* An implementation of the Spring's {@link Authentication} object which is used within a system security code block and
* wraps the original auth object. The wrapped object contains the necessary {@link SpRole#SYSTEM_ROLE}
* which is allowed to execute all secured methods.
*/
static final class SystemCodeAuthentication implements Authentication {
@Serial
private static final long serialVersionUID = 1L;
private static final List<SimpleGrantedAuthority> AUTHORITIES = List.of(new SimpleGrantedAuthority(SpRole.SYSTEM_ROLE));
private final TenantAwareAuthenticationDetails details;
private final TenantAwareUser principal;
private SystemCodeAuthentication(final String tenant) {
details = new TenantAwareAuthenticationDetails(tenant, false);
principal = new TenantAwareUser(SYSTEM_ACTOR, SYSTEM_ACTOR, AUTHORITIES, tenant);
}
@Override
public String getName() {
return null;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return AUTHORITIES;
}
@Override
public Object getCredentials() {
return null;
}
@Override
public Object getDetails() {
return details;
}
@Override
public Object getPrincipal() {
return principal;
}
@Override
public boolean isAuthenticated() {
return true;
}
@Override
public void setAuthenticated(final boolean isAuthenticated) {
throw new UnsupportedOperationException();
}
}
}

View File

@@ -0,0 +1,198 @@
/**
* 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.context;
import java.io.IOException;
import java.util.Objects;
import java.util.Optional;
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.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:S112 - it is generic class so a generic exception is fine
@SuppressWarnings("java:S112")
public class Mdc {
public static final String MDC_KEY_TENANT = "tenant";
// in the MDC the default actor key is "user"
public static final String MDC_KEY_ACTOR = System.getProperty(
"hawkbit.mdc.actor.key", // first by priority: system property
Optional.ofNullable(System.getenv("HAWKBIT_MDC_ACTOR_KEY")) // second by priority: environment variable
.orElse("user")); // default if not set
private static boolean enabled = true;
// hook to disable (otherwise enabled by default) MDC context management
public static void setEnabled(final boolean enabled) {
Mdc.enabled = enabled;
}
/**
* Executes callable and returns the result. If MDC is enabled, it sets the tenant and / or actor from the auth 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 static <T> T withAuth(final Callable<T> callable) throws Exception {
if (!enabled) {
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.tenant();
} else {
tenant = null;
}
final String actor = Optional.ofNullable(AccessContext.actor())
.filter(ctxActor -> !ctxActor.equals(AccessContext.SYSTEM_ACTOR)) // null and system are the same - system actor
.orElse(null);
return asTenantAsActor0(tenant, actor, callable);
}
/**
* Executes callable and returns the result. If MDC is enabled, it sets the tenant and / or actor from the auth in the MDC context.
* Calls the {@link #withAuth(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 static <T> T withAuthRe(final Callable<T> callable) {
try {
return withAuth(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 actor in the MDC context.
*
* @param <T> the return type
* @param tenant the tenant to set in the MDC context
* @param actor the actor to set in the MDC context
* @param callable the callable to execute
* @return the result
*/
public static <T> T asTenantAsActor(final String tenant, final String actor, final Callable<T> callable) throws Exception {
if (!enabled) {
return callable.call();
}
return asTenantAsActor0(tenant, actor, callable);
}
/**
* Executes callable and returns the result. If MDC is enabled, it sets the tenant and / or actor from the auth in the MDC context.
* Calls the {@link #asTenantAsActor(String, String, Callable)} method and wraps any catchable exception into a {@link RuntimeException}.
*
* @param <T> the return type
* @param tenant the tenant to set in the MDC context
* @param actor the actor to set in the MDC context
* @param callable the callable to execute
* @return the result
*/
public static <T> T asTenantAsActorRe(final String tenant, final String actor, final Callable<T> callable) {
try {
return asTenantAsActor(tenant, actor, callable);
} catch (final RuntimeException re) {
throw re;
} catch (final Exception e) {
throw new RuntimeException(e);
}
}
private static <T> T asTenantAsActor0(final String tenant, final String actor, final Callable<T> callable) throws Exception {
final String currentTenant = MDC.get(MDC_KEY_TENANT);
if (Objects.equals(currentTenant, tenant)) {
return asActor(callable, actor);
} else {
put(MDC_KEY_TENANT, tenant);
try {
return asActor(callable, actor);
} finally {
put(MDC_KEY_TENANT, currentTenant);
}
}
}
private static <T> T asActor(final Callable<T> callable, final String actor) throws Exception {
final String currentActor = MDC.get(MDC_KEY_ACTOR);
if (Objects.equals(currentActor, actor)) {
return callable.call();
} else {
put(MDC_KEY_ACTOR, actor);
try {
return callable.call();
} finally {
put(MDC_KEY_ACTOR, currentActor);
}
}
}
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() {
@Override
protected void doFilterInternal(
final HttpServletRequest request, final HttpServletResponse response, final FilterChain filterChain)
throws ServletException, IOException {
try {
Mdc.withAuth(() -> {
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

@@ -12,7 +12,7 @@ package org.eclipse.hawkbit.exception;
import lombok.Getter;
/**
* Define the Error codes for Error handling
* Define the error codes for error handling
*/
@Getter
public enum SpServerError {
@@ -182,9 +182,6 @@ public enum SpServerError {
private final String key;
private final String message;
/**
* Repository side Error codes
*/
SpServerError(final String key, final String message) {
this.key = key;
this.message = message;

View File

@@ -0,0 +1,228 @@
/**
* 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;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
/**
* 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 auth realm, see https://tools.ietf.org/html/rfc2617#page-3 .
*/
private String basicRealm = "hawkBit";
/**
* If to allow http auth when there is OAuth2 auth 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();
public CorsConfiguration toCorsConfiguration() {
final CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowedOrigins(getAllowedOrigins());
corsConfiguration.setAllowCredentials(true);
corsConfiguration.setAllowedHeaders(getAllowedHeaders());
corsConfiguration.setAllowedMethods(getAllowedMethods());
corsConfiguration.setExposedHeaders(getExposedHeaders());
return corsConfiguration;
}
public CorsConfigurationSource toCorsConfigurationSource() {
final CorsConfiguration corsConfiguration = toCorsConfiguration();
return request -> corsConfiguration;
}
}
/**
* 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 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 (DoS) 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

@@ -18,6 +18,7 @@ import io.micrometer.common.KeyValues;
import io.micrometer.core.instrument.Tag;
import io.micrometer.observation.ObservationRegistry;
import lombok.NonNull;
import org.eclipse.hawkbit.context.AccessContext;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.actuate.autoconfigure.observation.ObservationProperties;
import org.springframework.boot.actuate.autoconfigure.observation.web.servlet.WebMvcObservationAutoConfiguration;
@@ -44,12 +45,6 @@ public class DefaultTenantConfiguration {
public static final String TENANT_TAG = "tenant";
@Bean
@ConditionalOnMissingBean
TenantAware.TenantResolver tenantResolver() {
return new TenantAware.DefaultTenantResolver();
}
@Bean
@ConditionalOnMissingBean
TenantAwareCacheManager cacheManager() {
@@ -62,12 +57,12 @@ public class DefaultTenantConfiguration {
@ConditionalOnProperty(name = "hawkbit.metrics.tenancy.web.enabled", havingValue = "true", matchIfMissing = true)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnClass(name = { "org.springframework.web.servlet.DispatcherServlet", "io.micrometer.observation.Observation" })
@ConditionalOnBean({ ObservationRegistry.class, TenantAware.TenantResolver.class })
@ConditionalOnBean(ObservationRegistry.class)
public static class WebConfig {
@Bean
@Primary
public DefaultServerRequestObservationConvention serverRequestObservationConvention(final TenantAware.TenantResolver tenantResolver) {
public DefaultServerRequestObservationConvention serverRequestObservationConvention() {
return new DefaultServerRequestObservationConvention() {
@NonNull
@@ -78,7 +73,7 @@ public class DefaultTenantConfiguration {
}
private KeyValue tenant() {
return KeyValue.of(TENANT_TAG, Optional.ofNullable(tenantResolver.resolveTenant()).orElse("n/a"));
return KeyValue.of(TENANT_TAG, Optional.ofNullable(AccessContext.tenant()).orElse("n/a"));
}
};
}
@@ -104,17 +99,16 @@ public class DefaultTenantConfiguration {
@ConditionalOnClass(name = {
"io.micrometer.core.instrument.Tag",
"org.springframework.data.repository.core.support.RepositoryMethodInvocationListener" })
@ConditionalOnBean(TenantAware.TenantResolver.class)
public static class RepositoryConfig {
@Bean
public RepositoryTagsProvider repositoryTagsProvider(final TenantAware.TenantResolver tenantResolver) {
public RepositoryTagsProvider repositoryTagsProvider() {
return new DefaultRepositoryTagsProvider() {
@Override
public Iterable<Tag> repositoryTags(final RepositoryMethodInvocationListener.RepositoryMethodInvocation invocation) {
final Iterable<Tag> defaultTags = super.repositoryTags(invocation);
final String tenant = Optional.ofNullable(tenantResolver.resolveTenant()).orElse("n/a");
final String tenant = Optional.ofNullable(AccessContext.tenant()).orElse("n/a");
return () -> {
final Iterator<Tag> defaultTagsIterator = defaultTags.iterator();
return new Iterator<>() {

View File

@@ -1,79 +0,0 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.tenancy;
import java.util.concurrent.Callable;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
/**
* Interface for components that are aware of the application's current tenant.
*/
public interface TenantAware {
/**
* Implementation might retrieve the current tenant from a session or thread-local.
*
* @return the current tenant
*/
String getCurrentTenant();
/**
* @return the username of the currently logged-in user
*/
String getCurrentUsername();
/**
* Gives the possibility to run a certain code under a specific given {@code tenant}. Only the given {@link Callable} is executed
* under the specific tenant e.g. under control of an {@link ThreadLocal}. After the {@link Callable} it must be ensured that the
* original tenant before this invocation is reset.
*
* @param tenant the tenant which the specific code should run
* @param callable the runner which is implemented to run this specific code under the given tenant
* @return the return type of the {@link Callable}
*/
<T> T runAsTenant(String tenant, Callable<T> callable);
/**
* Gives the possibility to run a certain code under a specific given {@code tenant} and {@code username}.
* Only the given {@link Runnable} is executed under the specific tenant and user e.g. under control of an {@link ThreadLocal}.
* After the {@link Runnable} it must be ensured that the original tenant before this invocation is reset.
*
* @param tenant the tenant which the specific code should run with
* @param username the username which the specific code should run with
*/
void runAsTenantAsUser(String tenant, String username, Runnable runnable);
/**
* Resolves the tenant from the current context.
*/
interface TenantResolver {
String resolveTenant();
}
class DefaultTenantResolver implements TenantResolver {
@Override
public String resolveTenant() {
final SecurityContext context = SecurityContextHolder.getContext();
if (context.getAuthentication() != null) {
final Object principal = context.getAuthentication().getPrincipal();
if (context.getAuthentication().getDetails() instanceof TenantAwareAuthenticationDetails tenantAwareAuthenticationDetails) {
return tenantAwareAuthenticationDetails.tenant();
} else if (principal instanceof TenantAwareUser tenantAwareUser) {
return tenantAwareUser.getTenant();
}
}
return null;
}
}
}

View File

@@ -15,8 +15,8 @@ import java.io.Serializable;
import org.springframework.security.authentication.AbstractAuthenticationToken;
/**
* An authentication details object {@link AbstractAuthenticationToken#getDetails()} which is stored in the
* spring security authentication token details to transport the principal and tenant in the security context session.
* An auth details object {@link AbstractAuthenticationToken#getDetails()} which is stored in the
* spring security auth token details to transport the principal and tenant in the security context session.
*/
public record TenantAwareAuthenticationDetails(String tenant, boolean controller) implements Serializable {

View File

@@ -22,7 +22,7 @@ import lombok.NoArgsConstructor;
import lombok.NonNull;
import lombok.Value;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.tenancy.TenantAware.TenantResolver;
import org.eclipse.hawkbit.context.AccessContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
@@ -34,7 +34,7 @@ import org.springframework.lang.Nullable;
/**
* A spring Cache Manager that handles the multi tenancy.
* <ul>
* <li>If a tenant is resolved by the {@link TenantResolver}, a dedicated cache manager for that tenant is used/created.</li>
* <li>If a tenant is resolved by the {@link AccessContext}, a dedicated cache manager for that tenant is used/created.</li>
* <li>If no tenant is resolved, a global cache manager is used.</li>
* </ul>
*/
@@ -50,7 +50,6 @@ public class TenantAwareCacheManager implements CacheManager {
private CacheManager globalCacheManager;
private final Map<String, CacheManager> tenant2CacheManager = new ConcurrentHashMap<>();
private TenantResolver resolver;
private Environment env;
// default caffeine cache spec - see com.github.benmanes.caffeine.cache.CaffeineSpec javadoc for format details
@@ -61,8 +60,7 @@ public class TenantAwareCacheManager implements CacheManager {
}
@Autowired
public void init(final TenantResolver resolver, final Environment env) {
this.resolver = resolver;
public void init(final Environment env) {
this.env = env;
defaultSpec = env.resolvePlaceholders("${" + CONFIG_PREFIX + CONFIG_SPEC + ":expireAfterWrite=${hawkbit.cache.ttl:10s}}");
globalCacheManager = new TenantCacheManager(null);
@@ -71,7 +69,7 @@ public class TenantAwareCacheManager implements CacheManager {
@NonNull
@Override
public Cache getCache(@NonNull final String name) {
final Cache cache = Optional.ofNullable(resolver.resolveTenant())
final Cache cache = Optional.ofNullable(AccessContext.tenant())
.map(currentTenant -> tenant2CacheManager.computeIfAbsent(currentTenant, TenantCacheManager::new))
.orElse(globalCacheManager)
.getCache(name);
@@ -81,7 +79,7 @@ public class TenantAwareCacheManager implements CacheManager {
@NonNull
@Override
public Collection<String> getCacheNames() {
final String currentTenant = resolver.resolveTenant();
final String currentTenant = AccessContext.tenant();
if (currentTenant == null) {
return globalCacheManager.getCacheNames();
} else {

View File

@@ -1,27 +0,0 @@
/**
* 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.tenancy;
import java.util.Collection;
/**
* The service responsible for making the lookup for user authorities/roles based on his tenant and username
*/
@FunctionalInterface
public interface UserAuthoritiesResolver {
/**
* User authorities/roles lookup based on the username and the tenant context
*
* @param username The username of the user
* @return a {@link Collection} of authorities/roles for this user
*/
Collection<String> getUserAuthorities(String username);
}

View File

@@ -0,0 +1,195 @@
/**
* 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.utils;
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 lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.security.HawkbitSecurityProperties;
/**
* A utility which determines the correct IP of a connected {@link Target}. E.g from a {@link HttpServletRequest}.
*/
@Slf4j
@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})?");
/**
* Converts address to URI. If the address is not parsable, it will log and return <code>null</code>.
* @param address the address to convert
* @return the {@link URI} or <code>null</code> if the address is not parsable
*/
public static URI addressToUri(final String address) {
if (address == null) {
return null;
}
try {
return URI.create(address);
} catch (final IllegalArgumentException e) {
log.debug("Failed to parse URI: {}", address, e);
return null;
}
}
/**
* 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,88 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.context;
import static org.assertj.core.api.Assertions.assertThat;
import static org.eclipse.hawkbit.context.AccessContext.withSecurityContext;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.eclipse.hawkbit.auth.SpPermission;
import org.eclipse.hawkbit.tenancy.TenantAwareAuthenticationDetails;
import org.junit.jupiter.api.Test;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
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;
class SecurityContextSerializerTest {
private static final Set<String> AUTHORITIES = SpPermission.getAllAuthorities();
@Test
void testJsonSerialization() {
final SecurityContext securityContext = SecurityContextHolder.getContext();
final UsernamePasswordAuthenticationToken userPassAuthentication = new UsernamePasswordAuthenticationToken(
"user", null, AUTHORITIES.stream().map(SimpleGrantedAuthority::new).toList());
final TenantAwareAuthenticationDetails details = new TenantAwareAuthenticationDetails("my_tenant", false);
userPassAuthentication.setDetails(details);
securityContext.setAuthentication(userPassAuthentication);
final String serialized = serialize(securityContext);
final SecurityContext deserialized = deserialize(serialized);
final Authentication authentication = deserialized.getAuthentication();
assertThat(resolve(authentication)).hasToString("user");
assertThat(authentication.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toSet()))
.isEqualTo(AUTHORITIES);
assertThat(authentication.isAuthenticated()).isTrue();
assertThat(authentication.getDetails()).isEqualTo(details);
}
@Test
void testJsonSerializationSize() {
final SecurityContext securityContext = SecurityContextHolder.getContext();
final UsernamePasswordAuthenticationToken userPassAuthentication = new UsernamePasswordAuthenticationToken(
"FirstName.FamilyName@domain1.domain0.com",
Map.of("should not be in" + bigString(10_000), "the output" + bigString(15_000)),
AUTHORITIES.stream().map(SimpleGrantedAuthority::new).toList());
final TenantAwareAuthenticationDetails details = new TenantAwareAuthenticationDetails("my_test_enant", false);
userPassAuthentication.setDetails(details);
securityContext.setAuthentication(userPassAuthentication);
final String serialized = serialize(securityContext);
assertThat(serialized).hasSizeLessThan(4096); // ensure that it is not too big
}
private static String bigString(final int length) {
final StringBuilder sb = new StringBuilder(length);
for (int i = 0; i < length; i++) {
sb.append((char) ('a' + i % 26));
}
return sb.toString();
}
private static String serialize(final SecurityContext securityContext) {
return withSecurityContext(securityContext, () -> AccessContext.securityContext().orElseThrow());
}
private static SecurityContext deserialize(final String serialized) {
return withSecurityContext(serialized, SecurityContextHolder::getContext);
}
private static String resolve(final Authentication authentication) {
final SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
securityContext.setAuthentication(authentication);
return withSecurityContext(securityContext, AccessContext::actor);
}
}

View File

@@ -0,0 +1,69 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.context;
import static org.eclipse.hawkbit.context.AccessContext.asSystemAsTenant;
import java.util.List;
import org.assertj.core.api.Assertions;
import org.eclipse.hawkbit.auth.SpRole;
import org.eclipse.hawkbit.tenancy.TenantAwareAuthenticationDetails;
import org.junit.jupiter.api.Test;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
class SystemSecurityContextTest {
@Test
void test() {
final UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken("test", "pass", List.of(new SimpleGrantedAuthority("anonymous")));
auth.setDetails("string details");
test(auth);
}
@Test
void testWithNullPrincipal() {
final UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(null, "pass", List.of(new SimpleGrantedAuthority("anonymous")));
auth.setDetails("string details");
test(auth);
}
@Test
void testWithNullCredentials() {
final UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken("test", null, List.of(new SimpleGrantedAuthority("anonymous")));
auth.setDetails("string details");
test(auth);
}
@Test
void testWitAllNull() {
final UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(null, null, List.of(new SimpleGrantedAuthority("anonymous")));
auth.setDetails(null);
test(auth);
}
private static void test(final UsernamePasswordAuthenticationToken auth) {
final SecurityContext sc = SecurityContextHolder.createEmptyContext();
sc.setAuthentication(auth);
SecurityContextHolder.setContext(sc);
asSystemAsTenant("tenant", () -> {
final Authentication currentAuth = SecurityContextHolder.getContext().getAuthentication();
Assertions.assertThat(currentAuth.getClass().getSimpleName()).isEqualTo("SystemCodeAuthentication");
Assertions.assertThat(currentAuth.getCredentials()).isNull();
Assertions.assertThat(currentAuth.getAuthorities()).isEqualTo(List.of(new SimpleGrantedAuthority(SpRole.SYSTEM_ROLE)));
Assertions.assertThat(currentAuth.getDetails()).isEqualTo(new TenantAwareAuthenticationDetails("tenant", false));
});
SecurityContextHolder.clearContext();
}
}

View File

@@ -0,0 +1,35 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.im.authentication;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.Collection;
import org.eclipse.hawkbit.auth.SpPermission;
import org.junit.jupiter.api.Test;
/**
* Test {@link SpPermission}.
* <p/>
* Feature: Unit Tests - Security<br/>
* Story: Permission Test
*/
final class SpPermissionTest {
/**
* Double-checks that all permissions doesn't contain any hierarchies.
*/
@Test
void allAuthoritiesShouldNotContainHierarchies() {
final Collection<String> allAuthorities = SpPermission.getAllAuthorities();
assertThat(allAuthorities).isNotEmpty().as("Are not hierarchies").allMatch(permission -> !permission.contains("\n"));
}
}

View File

@@ -0,0 +1,226 @@
/**
* 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 org.eclipse.hawkbit.security.HawkbitSecurityProperties;
import org.eclipse.hawkbit.security.HawkbitSecurityProperties.Clients;
import org.eclipse.hawkbit.utils.IpUtil;
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;
/**
* Feature: Unit Tests - Security<br/>
* Story: IP Util Test
*/
@ExtendWith(MockitoExtension.class)
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;
/**
* Tests create uri from request
*/
@Test
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();
}
/**
* Tests create uri from request with masked IP when IP tracking is disabled
*/
@Test
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();
}
/**
* Tests create uri from x forward header
*/
@Test
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();
}
/**
* Tests client uri from request
*/
@Test
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);
}
/**
* Tests client uri from request
*/
@Test
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);
}
/**
* Tests create http uri ipv4 and ipv6
*/
@Test
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);
}
/**
* Tests create amqp uri ipv4 and ipv6
*/
@Test
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);
}
/**
* Tests create invalid uri
*/
@Test
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");
}
}