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:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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) {}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) {}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 + "')";
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
198
hawkbit-core/src/main/java/org/eclipse/hawkbit/context/Mdc.java
Normal file
198
hawkbit-core/src/main/java/org/eclipse/hawkbit/context/Mdc.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<>() {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
195
hawkbit-core/src/main/java/org/eclipse/hawkbit/utils/IpUtil.java
Normal file
195
hawkbit-core/src/main/java/org/eclipse/hawkbit/utils/IpUtil.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user