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