Fix EntityMatcher when for Identifiable.getId (#2724)

* Fix EntityMatcher to process properly filters of type targetType.id - to resolve correctly the getter return type Long not T
* Add AutoAsssignTest access control test
* Simplify rest of the ACM tests

Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
This commit is contained in:
Avgustin Marinov
2025-10-07 15:26:04 +03:00
committed by GitHub
parent 6907931eb6
commit cc36ca8801
18 changed files with 508 additions and 297 deletions

View File

@@ -0,0 +1,107 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.jpa.acm;
import java.util.Set;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.eclipse.hawkbit.im.authentication.SpPermission;
import org.eclipse.hawkbit.repository.DistributionSetFields;
import org.eclipse.hawkbit.repository.DistributionSetTypeFields;
import org.eclipse.hawkbit.repository.SoftwareModuleFields;
import org.eclipse.hawkbit.repository.SoftwareModuleTypeFields;
import org.eclipse.hawkbit.repository.TargetFields;
import org.eclipse.hawkbit.repository.TargetTagFields;
import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet;
import org.eclipse.hawkbit.repository.jpa.model.JpaSoftwareModule;
import org.eclipse.hawkbit.repository.jpa.model.JpaSoftwareModuleType;
import org.eclipse.hawkbit.repository.jpa.model.JpaTarget;
import org.eclipse.hawkbit.repository.jpa.ql.QLSupport;
// utility class to validate authorities when ACM is enabled
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class AuthorityChecker {
private static final Set<String> ALL_AUTHORITIES = SpPermission.getAllTenantAuthorities();
public static String[] validateAuthorities(final String... authorities) {
for (final String authority : authorities) {
validateAuthority(authority);
}
return authorities;
}
public static void validateAuthority(final String authority) {
final int index = authority.indexOf('/');
final String unscopedPermission = index > 0 ? authority.substring(0, index) : authority;
if (index > 0) {
validateScope(group(unscopedPermission), authority.substring(index + 1), authority);
}
if (!ALL_AUTHORITIES.contains(unscopedPermission)) {
throw new IllegalArgumentException(
"Unknown permission: " + unscopedPermission + (index > 0 ? " (unscoped of " + authority + ")" : ""));
}
}
private static String group(final String unscopedPermission) {
if (unscopedPermission.startsWith(SpPermission.CREATE_PREFIX)) {
return unscopedPermission.substring(SpPermission.CREATE_PREFIX.length());
} else if (unscopedPermission.startsWith(SpPermission.READ_PREFIX)) {
return unscopedPermission.substring(SpPermission.READ_PREFIX.length());
} else if (unscopedPermission.startsWith(SpPermission.UPDATE_PREFIX)) {
return unscopedPermission.substring(SpPermission.UPDATE_PREFIX.length());
} else if (unscopedPermission.startsWith(SpPermission.DELETE_PREFIX)) {
return unscopedPermission.substring(SpPermission.DELETE_PREFIX.length());
} else {
throw new IllegalArgumentException(unscopedPermission + " doesn't support targetTypeScope");
}
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private static void validateScope(final String permission, final String rsql, final String authority) {
// validate RSQL
final Class<?> rsqlQueryFieldType;
final Class<?> jpaType;
switch (permission) {
case SpPermission.TARGET -> {
rsqlQueryFieldType = TargetFields.class;
jpaType = JpaTarget.class;
}
case SpPermission.TARGET_TYPE -> {
rsqlQueryFieldType = TargetTagFields.class;
jpaType = JpaTarget.class;
}
case SpPermission.SOFTWARE_MODULE -> {
rsqlQueryFieldType = SoftwareModuleFields.class;
jpaType = JpaSoftwareModule.class;
}
case SpPermission.SOFTWARE_MODULE_TYPE -> {
rsqlQueryFieldType = SoftwareModuleTypeFields.class;
jpaType = JpaSoftwareModuleType.class;
}
case SpPermission.DISTRIBUTION_SET -> {
rsqlQueryFieldType = DistributionSetFields.class;
jpaType = JpaDistributionSet.class;
}
case SpPermission.DISTRIBUTION_SET_TYPE -> {
rsqlQueryFieldType = DistributionSetTypeFields.class;
jpaType = JpaTarget.class;
}
default -> throw new IllegalArgumentException(permission + " doesn't support targetTypeScope");
}
try {
QLSupport.getInstance().validate(rsql, (Class) rsqlQueryFieldType, jpaType);
} catch (final RuntimeException e) {
throw new IllegalArgumentException(
"Scope of " + authority + " is not a valid RSQL for " + permission + ": " + e.getMessage(), e);
}
}
}

View File

@@ -1,178 +0,0 @@
/**
* Copyright (c) 2021 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.repository.jpa.autoassign;
import java.util.List;
import java.util.function.Consumer;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.ContextAware;
import org.eclipse.hawkbit.repository.DeploymentManagement;
import org.eclipse.hawkbit.repository.TargetFilterQueryManagement;
import org.eclipse.hawkbit.repository.autoassign.AutoAssignExecutor;
import org.eclipse.hawkbit.repository.jpa.utils.DeploymentHelper;
import org.eclipse.hawkbit.repository.model.Action;
import org.eclipse.hawkbit.repository.model.DeploymentRequest;
import org.eclipse.hawkbit.repository.model.TargetFilterQuery;
import org.eclipse.hawkbit.tenancy.TenantAware;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.util.StringUtils;
/**
* Abstract implementation of an AutoAssignExecutor
*/
@Slf4j
public abstract class AbstractAutoAssignExecutor implements AutoAssignExecutor {
/**
* The message which is added to the action status when a distribution set is
* assigned to an target. First %s is the name of the target filter.
*/
private static final String ACTION_MESSAGE = "Auto assignment by target filter: %s";
/**
* Maximum for target filter queries with auto assign DS activated.
*/
private static final int PAGE_SIZE = 1000;
private final TargetFilterQueryManagement<? extends TargetFilterQuery> targetFilterQueryManagement;
private final DeploymentManagement deploymentManagement;
private final PlatformTransactionManager transactionManager;
private final ContextAware contextAware;
/**
* Constructor
*
* @param targetFilterQueryManagement to get all target filter queries
* @param deploymentManagement to assign distribution sets to targets
* @param transactionManager to run transactions
* @param contextAware to handle the context
*/
protected AbstractAutoAssignExecutor(
final TargetFilterQueryManagement<? extends TargetFilterQuery> targetFilterQueryManagement,
final DeploymentManagement deploymentManagement,
final PlatformTransactionManager transactionManager, final ContextAware contextAware) {
this.targetFilterQueryManagement = targetFilterQueryManagement;
this.deploymentManagement = deploymentManagement;
this.transactionManager = transactionManager;
this.contextAware = contextAware;
}
protected static String getAutoAssignmentInitiatedBy(final TargetFilterQuery targetFilterQuery) {
return StringUtils.hasText(targetFilterQuery.getAutoAssignInitiatedBy())
? targetFilterQuery.getAutoAssignInitiatedBy()
: targetFilterQuery.getCreatedBy();
}
protected DeploymentManagement getDeploymentManagement() {
return deploymentManagement;
}
protected PlatformTransactionManager getTransactionManager() {
return transactionManager;
}
protected TenantAware getContextAware() {
return contextAware;
}
// run in the context the auto assignment is made in, i.e. if there is access control context it runs in it
// otherwise in the tenant & user context built by createdBy
// Note! It must be called in a tenant context, i.e. contextAware.getCurrentTenant() returns the tenant
protected void forEachFilterWithAutoAssignDS(final Consumer<TargetFilterQuery> consumer) {
Slice<TargetFilterQuery> filterQueries;
Pageable query = PageRequest.of(0, PAGE_SIZE);
do {
filterQueries = targetFilterQueryManagement.findWithAutoAssignDS(query);
filterQueries.forEach(filterQuery -> {
try {
filterQuery.getAccessControlContext().ifPresentOrElse(
context -> // has stored context - executes it with it
contextAware.runInContext(
context,
() -> consumer.accept(filterQuery)),
() -> // has no stored context - executes it in the tenant & user scope
contextAware.runAsTenantAsUser(
contextAware.getCurrentTenant(),
getAutoAssignmentInitiatedBy(filterQuery), () -> {
consumer.accept(filterQuery);
return null;
})
);
} catch (final RuntimeException ex) {
log.debug(
"Exception on forEachFilterWithAutoAssignDS execution for tenant {} with filter id {}. Continue with next filter query.",
filterQuery.getTenant(), filterQuery.getId(), ex);
log.error(
"Exception on forEachFilterWithAutoAssignDS execution for tenant {} with filter id {} and error message [{}]. "
+ "Continue with next filter query.",
filterQuery.getTenant(), filterQuery.getId(), ex.getMessage());
}
});
} while ((query = filterQueries.nextPageable()) != Pageable.unpaged());
}
/**
* Runs target assignments within a dedicated transaction for a given list of
* controllerIDs
*
* @param targetFilterQuery the target filter query
* @param controllerIds the controllerIDs
* @return count of targets
*/
protected int runTransactionalAssignment(final TargetFilterQuery targetFilterQuery,
final List<String> controllerIds) {
final String actionMessage = String.format(ACTION_MESSAGE, targetFilterQuery.getName());
return DeploymentHelper.runInNewTransaction(getTransactionManager(), "autoAssignDSToTargets",
Isolation.READ_COMMITTED.value(), status -> {
final List<DeploymentRequest> deploymentRequests = mapToDeploymentRequests(controllerIds,
targetFilterQuery);
final int count = deploymentRequests.size();
if (count > 0) {
getDeploymentManagement().assignDistributionSets(
getAutoAssignmentInitiatedBy(targetFilterQuery), deploymentRequests, actionMessage);
}
return count;
});
}
/**
* Creates a list of {@link DeploymentRequest} for given list of controllerIds
* and {@link TargetFilterQuery}
*
* @param controllerIds list of controllerIds
* @param filterQuery the query the targets have to match
* @return list of deployment request
*/
protected List<DeploymentRequest> mapToDeploymentRequests(final List<String> controllerIds,
final TargetFilterQuery filterQuery) {
// the action type is set to FORCED per default (when not explicitly
// specified)
final Action.ActionType autoAssignActionType = filterQuery.getAutoAssignActionType() == null
? Action.ActionType.FORCED
: filterQuery.getAutoAssignActionType();
return controllerIds.stream()
.map(controllerId -> DeploymentRequest
.builder(controllerId, filterQuery.getAutoAssignDistributionSet().getId())
.actionType(autoAssignActionType).weight(filterQuery.getAutoAssignWeight().orElse(null))
.confirmationRequired(filterQuery.isConfirmationRequired()).build())
.toList();
}
}

View File

@@ -11,6 +11,7 @@ package org.eclipse.hawkbit.repository.jpa.autoassign;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
import jakarta.persistence.PersistenceException;
@@ -20,109 +21,195 @@ import org.eclipse.hawkbit.exception.AbstractServerRtException;
import org.eclipse.hawkbit.repository.DeploymentManagement;
import org.eclipse.hawkbit.repository.TargetFilterQueryManagement;
import org.eclipse.hawkbit.repository.TargetManagement;
import org.eclipse.hawkbit.repository.autoassign.AutoAssignExecutor;
import org.eclipse.hawkbit.repository.jpa.configuration.Constants;
import org.eclipse.hawkbit.repository.jpa.utils.DeploymentHelper;
import org.eclipse.hawkbit.repository.model.Action;
import org.eclipse.hawkbit.repository.model.DeploymentRequest;
import org.eclipse.hawkbit.repository.model.Target;
import org.eclipse.hawkbit.repository.model.TargetFilterQuery;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
/**
* Checks if targets need a new distribution set (DS) based on the target filter
* queries and assigns the new DS when necessary. First all target filter
* queries are listed. For every target filter query (TFQ) the auto assign DS is
* retrieved. All targets get listed per target filter query, that match the TFQ
* and that don't have the auto assign DS in their action history.
* Checks if targets need a new distribution set (DS) based on the target filter queries and assigns the new DS when necessary. First all target
* filter queries are listed. For every target filter query (TFQ) the auto assign DS is retrieved. All targets get listed per target filter
* query, that match the TFQ and that don't have the auto assign DS in their action history.
*/
@Slf4j
public class AutoAssignChecker extends AbstractAutoAssignExecutor {
private final TargetManagement<? extends Target> targetManagement;
public class AutoAssignChecker implements AutoAssignExecutor {
/**
* Instantiates a new auto assign checker
*
* @param targetFilterQueryManagement to get all target filter queries
* @param targetManagement to get targets
* @param deploymentManagement to assign distribution sets to targets
* @param transactionManager to run transactions
* @param contextAware to handle the context
* The message which is added to the action status when a distribution set is assigned to a target.
* First %s is the name of the target filter.
*/
private static final String ACTION_MESSAGE = "Auto assignment by target filter: %s";
/**
* Maximum for target filter queries with auto assign DS activated.
*/
private static final int PAGE_SIZE = 1000;
private final TargetFilterQueryManagement<? extends TargetFilterQuery> targetFilterQueryManagement;
private final TargetManagement<? extends Target> targetManagement;
private final DeploymentManagement deploymentManagement;
private final PlatformTransactionManager transactionManager;
private final ContextAware contextAware;
public AutoAssignChecker(
final TargetFilterQueryManagement<? extends TargetFilterQuery> targetFilterQueryManagement,
final TargetManagement<? extends Target> targetManagement, final DeploymentManagement deploymentManagement,
final PlatformTransactionManager transactionManager, final ContextAware contextAware) {
super(targetFilterQueryManagement, deploymentManagement, transactionManager, contextAware);
this.targetFilterQueryManagement = targetFilterQueryManagement;
this.targetManagement = targetManagement;
this.deploymentManagement = deploymentManagement;
this.transactionManager = transactionManager;
this.contextAware = contextAware;
}
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void checkAllTargets() {
log.debug("Auto assign check call for tenant {} started", getContextAware().getCurrentTenant());
log.debug("Auto assign check call started");
forEachFilterWithAutoAssignDS(this::checkByTargetFilterQueryAndAssignDS);
log.debug("Auto assign check call for tenant {} finished", getContextAware().getCurrentTenant());
log.debug("Auto assign check call finished");
}
@Override
public void checkSingleTarget(String controllerId) {
log.debug("Auto assign check call for tenant {} and device {} started", getContextAware().getCurrentTenant(), controllerId);
log.debug("Auto assign check call for device {} started", controllerId);
forEachFilterWithAutoAssignDS(filter -> checkForDevice(controllerId, filter));
log.debug("Auto assign check call for tenant {} and device {} finished", getContextAware().getCurrentTenant(), controllerId);
log.debug("Auto assign check call for device {} finished", controllerId);
}
/**
* Fetches the distribution set, gets all controllerIds and assigns the DS to
* them. Catches PersistenceException and own exceptions derived from
* AbstractServerRtException
* Fetches the distribution set, gets all controllerIds and assigns the DS to them. Catches PersistenceException and own exceptions derived
* from AbstractServerRtException
*
* @param targetFilterQuery the target filter query
*/
private void checkByTargetFilterQueryAndAssignDS(final TargetFilterQuery targetFilterQuery) {
log.debug("Auto assign check call for tenant {} and target filter query id {} started",
getContextAware().getCurrentTenant(), targetFilterQuery.getId());
log.debug("Auto assign check call for target filter query id {} started", targetFilterQuery.getId());
try {
int count;
do {
final List<String> controllerIds = targetManagement
.findByTargetFilterQueryAndNonDSAndCompatibleAndUpdatable(
targetFilterQuery.getAutoAssignDistributionSet().getId(), targetFilterQuery.getQuery(),
PageRequest.of(0, Constants.MAX_ENTRIES_IN_STATEMENT)
)
PageRequest.of(0, Constants.MAX_ENTRIES_IN_STATEMENT))
.getContent().stream().map(Target::getControllerId).toList();
log.debug(
"Retrieved {} auto assign targets for tenant {} and target filter query id {}, starting with assignment",
controllerIds.size(), getContextAware().getCurrentTenant(), targetFilterQuery.getId());
log.debug("Retrieved {} auto assign targets for target filter query id {}, starting with assignment",
controllerIds.size(), targetFilterQuery.getId());
count = runTransactionalAssignment(targetFilterQuery, controllerIds);
log.debug(
"Assignment for {} auto assign targets for tenant {} and target filter query id {} finished",
controllerIds.size(), getContextAware().getCurrentTenant(), targetFilterQuery.getId());
log.debug("Assignment for {} auto assign targets for target filter query id {} finished",
controllerIds.size(), targetFilterQuery.getId());
} while (count == Constants.MAX_ENTRIES_IN_STATEMENT);
} catch (final PersistenceException | AbstractServerRtException e) {
log.error("Error during auto assign check of target filter query id {}", targetFilterQuery.getId(), e);
}
log.debug("Auto assign check call for tenant {} and target filter query id {} finished",
getContextAware().getCurrentTenant(), targetFilterQuery.getId());
log.debug("Auto assign check call for target filter query id {} finished", targetFilterQuery.getId());
}
private static String getAutoAssignmentInitiatedBy(final TargetFilterQuery targetFilterQuery) {
return StringUtils.hasText(targetFilterQuery.getAutoAssignInitiatedBy())
? targetFilterQuery.getAutoAssignInitiatedBy()
: targetFilterQuery.getCreatedBy();
}
// run in the context the auto assignment is made in, i.e. if there is access control context it runs in it
// otherwise in the tenant & user context built by createdBy
// Note: It must be called in a tenant context, i.e. contextAware.getCurrentTenant() returns the tenant
private void forEachFilterWithAutoAssignDS(final Consumer<TargetFilterQuery> consumer) {
Slice<TargetFilterQuery> filterQueries;
Pageable query = PageRequest.of(0, PAGE_SIZE);
do {
filterQueries = targetFilterQueryManagement.findWithAutoAssignDS(query);
filterQueries.forEach(filterQuery -> {
try {
filterQuery.getAccessControlContext().ifPresentOrElse(
context -> // has stored context - executes it with it
contextAware.runInContext(
context,
() -> consumer.accept(filterQuery)),
() -> // has no stored context - executes it in the tenant & user scope
contextAware.runAsTenantAsUser(
contextAware.getCurrentTenant(),
getAutoAssignmentInitiatedBy(filterQuery), () -> {
consumer.accept(filterQuery);
return null;
})
);
} catch (final RuntimeException ex) {
if (log.isDebugEnabled()) {
log.debug("Exception on forEachFilterWithAutoAssignDS execution for filter id {}. Continue with next filter query.",
filterQuery.getId(), ex);
} else {
log.error(
"Exception on forEachFilterWithAutoAssignDS execution for filter id {} and error message [{}]. Continue with next filter query.",
filterQuery.getId(), ex.getMessage());
}
}
});
} while (filterQueries.hasNext() && (query = filterQueries.nextPageable()) != Pageable.unpaged());
}
/**
* Runs target assignments within a dedicated transaction for a given list of controllerIDs
*
* @param targetFilterQuery the target filter query
* @param controllerIds the controllerIDs
* @return count of targets
*/
private int runTransactionalAssignment(final TargetFilterQuery targetFilterQuery, final List<String> controllerIds) {
final String actionMessage = String.format(ACTION_MESSAGE, targetFilterQuery.getName());
return DeploymentHelper.runInNewTransaction(transactionManager, "autoAssignDSToTargets", Isolation.READ_COMMITTED.value(), status -> {
final List<DeploymentRequest> deploymentRequests = mapToDeploymentRequests(controllerIds, targetFilterQuery);
final int count = deploymentRequests.size();
if (count > 0) {
deploymentManagement.assignDistributionSets(getAutoAssignmentInitiatedBy(targetFilterQuery), deploymentRequests, actionMessage);
}
return count;
});
}
/**
* Creates a list of {@link DeploymentRequest} for given list of controllerIds and {@link TargetFilterQuery}
*
* @param controllerIds list of controllerIds
* @param filterQuery the query the targets have to match
* @return list of deployment request
*/
private List<DeploymentRequest> mapToDeploymentRequests(final List<String> controllerIds, final TargetFilterQuery filterQuery) {
// the action type is set to FORCED per default (when not explicitly specified)
final Action.ActionType autoAssignActionType = filterQuery.getAutoAssignActionType() == null
? Action.ActionType.FORCED
: filterQuery.getAutoAssignActionType();
return controllerIds.stream()
.map(controllerId -> DeploymentRequest
.builder(controllerId, filterQuery.getAutoAssignDistributionSet().getId())
.actionType(autoAssignActionType).weight(filterQuery.getAutoAssignWeight().orElse(null))
.confirmationRequired(filterQuery.isConfirmationRequired()).build())
.toList();
}
private void checkForDevice(final String controllerId, final TargetFilterQuery targetFilterQuery) {
log.debug("Auto assign check call for tenant {} and target filter query id {} for device {} started",
getContextAware().getCurrentTenant(), targetFilterQuery.getId(), controllerId);
log.debug("Auto assign check call for target filter query id {} for device {} started", targetFilterQuery.getId(), controllerId);
try {
final boolean controllerIdMatches = targetManagement.isTargetMatchingQueryAndDSNotAssignedAndCompatibleAndUpdatable(
controllerId, targetFilterQuery.getAutoAssignDistributionSet().getId(),
targetFilterQuery.getQuery());
if (controllerIdMatches) {
if (targetManagement.isTargetMatchingQueryAndDSNotAssignedAndCompatibleAndUpdatable(
controllerId, targetFilterQuery.getAutoAssignDistributionSet().getId(), targetFilterQuery.getQuery())) {
runTransactionalAssignment(targetFilterQuery, Collections.singletonList(controllerId));
}
} catch (final PersistenceException | AbstractServerRtException e) {
log.error("Error during auto assign check of target filter query id {}", targetFilterQuery.getId(), e);
}
log.debug("Auto assign check call for tenant {} and target filter query id {} for device {} finished",
getContextAware().getCurrentTenant(), targetFilterQuery.getId(), controllerId);
log.debug("Auto assign check call for target filter query id {} for device {} finished", targetFilterQuery.getId(), controllerId);
}
}
}