Fix action delete access control - to require only target update (not delete also) (#2767)

Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
This commit is contained in:
Avgustin Marinov
2025-10-22 13:33:56 +03:00
committed by GitHub
parent 3caa9d9eda
commit 8a36ba6203
12 changed files with 180 additions and 181 deletions

View File

@@ -320,22 +320,22 @@ public interface DeploymentManagement extends PermissionSupport {
void deleteActionsByIds(List<Long> actionIds);
/**
* Deletes actions in scope of the target ONLY by list of action ids.
* Deletes actions in scope of the controllerId ONLY by list of action ids.
*
* @param target - target controllerId
* @param controllerId - controllerId controllerId
* @param actionsIds - list of action ids to be deleted
*/
@PreAuthorize("hasAuthority('UPDATE_" + SpPermission.TARGET + "')")
void deleteTargetActionsByIds(final String target, final List<Long> actionsIds);
void deleteTargetActionsByIds(final String controllerId, final List<Long> actionsIds);
/**
* Deletes target actions and leaves the LAST N actions in the action history only.
* Deletes controllerId actions and leaves the LAST N actions in the action history only.
*
* @param target - target controllerId
* @param controllerId - controllerId controllerId
* @param keepLast - number of actions to be left/kept (NOT deleted)
*/
@PreAuthorize("hasAuthority('UPDATE_" + SpPermission.TARGET + "')")
void deleteOldestTargetActions(final String target, final int keepLast);
void deleteOldestTargetActions(final String controllerId, final int keepLast);
/**
* Sets the status of inactive scheduled {@link Action}s for the specified {@link Target}s to {@link Status#CANCELED}
@@ -413,5 +413,5 @@ public interface DeploymentManagement extends PermissionSupport {
void cancelActionsForDistributionSet(final ActionCancellationType cancelationType, final DistributionSet set);
@PreAuthorize(SpringEvalExpressions.IS_SYSTEM_CODE)
void handleMaxAssignmentsExceeded(Long targetId, Long requested, AssignmentQuotaExceededException ex);
void handleMaxAssignmentsExceeded(Long targetId, Long requested, AssignmentQuotaExceededException quotaExceededException);
}

View File

@@ -385,14 +385,13 @@ public class JpaRolloutExecutor implements RolloutExecutor {
}
private void deleteScheduledActions(final JpaRollout rollout, final Slice<JpaAction> scheduledActions) {
final boolean hasScheduledActions = scheduledActions.getNumberOfElements() > 0;
if (hasScheduledActions) {
if (scheduledActions.getNumberOfElements() > 0) {
// has scheduled actions - delete them
try {
final List<Long> actionIds = StreamSupport.stream(scheduledActions.spliterator(), false)
.map(Action::getId)
.toList();
actionRepository.deleteByIdIn(actionIds);
actionRepository.deleteAllById(actionIds);
afterCommit.afterCommit(() -> EventPublisherHolder.getInstance().getEventPublisher()
.publishEvent(new RolloutUpdatedEvent(rollout)));
} catch (final RuntimeException e) {

View File

@@ -80,7 +80,7 @@ public class AccessControllerConfiguration {
@Override
@SuppressWarnings("unchecked")
public Optional<Specification<JpaAction>> getAccessRules(final Operation operation) {
return targetAccessController.getAccessRules(operation).map(targetSpec -> (actionRoot, query, cb) -> {
return targetAccessController.getAccessRules(map(operation)).map(targetSpec -> (actionRoot, query, cb) -> {
final Join<JpaAction, JpaTarget> targetJoin = actionRoot.join(JpaAction_.target);
final EntityType<JpaTarget> targetModel = query.from(JpaTarget.class).getModel();
final Root<JpaTarget> targetRoot = (Root<JpaTarget>) Proxy.newProxyInstance(
@@ -101,7 +101,15 @@ public class AccessControllerConfiguration {
@Override
public void assertOperationAllowed(final Operation operation, final JpaAction entity) throws InsufficientPermissionException {
targetAccessController.assertOperationAllowed(operation, entity.getTarget());
targetAccessController.assertOperationAllowed(map(operation), entity.getTarget());
}
// all CREATE/UPDATE/DELETE action operations are mapped to UPDATE_TARGET permissions / actions
private static Operation map(final Operation actionOperation) {
return switch (actionOperation) {
case READ -> Operation.READ;
case CREATE, UPDATE, DELETE -> Operation.UPDATE;
};
}
};
}

View File

@@ -13,13 +13,12 @@ import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
import jakarta.persistence.criteria.JoinType;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.function.TriConsumer;
import org.eclipse.hawkbit.repository.QuotaManagement;
import org.eclipse.hawkbit.repository.RepositoryConstants;
import org.eclipse.hawkbit.repository.RepositoryProperties;
@@ -29,6 +28,7 @@ import org.eclipse.hawkbit.repository.event.remote.entity.TargetUpdatedEvent;
import org.eclipse.hawkbit.repository.exception.AssignmentQuotaExceededException;
import org.eclipse.hawkbit.repository.jpa.configuration.Constants;
import org.eclipse.hawkbit.repository.jpa.executor.AfterTransactionCommitExecutor;
import org.eclipse.hawkbit.repository.jpa.management.JpaDeploymentManagement.MaxAssignmentsExceededInfo;
import org.eclipse.hawkbit.repository.jpa.model.AbstractJpaBaseEntity_;
import org.eclipse.hawkbit.repository.jpa.model.JpaAction;
import org.eclipse.hawkbit.repository.jpa.model.JpaActionStatus;
@@ -67,7 +67,7 @@ public abstract class AbstractDsAssignmentStrategy {
private final BooleanSupplier multiAssignmentsConfig;
private final BooleanSupplier confirmationFlowConfig;
private final RepositoryProperties repositoryProperties;
private final TriConsumer<Long, Long, AssignmentQuotaExceededException> maxAssignmentExceededHandler;
private final Consumer<MaxAssignmentsExceededInfo> maxAssignmentExceededHandler;
@SuppressWarnings("java:S107")
AbstractDsAssignmentStrategy(
@@ -76,7 +76,7 @@ public abstract class AbstractDsAssignmentStrategy {
final ActionRepository actionRepository, final ActionStatusRepository actionStatusRepository,
final QuotaManagement quotaManagement, final BooleanSupplier multiAssignmentsConfig,
final BooleanSupplier confirmationFlowConfig, final RepositoryProperties repositoryProperties,
final TriConsumer<Long, Long, AssignmentQuotaExceededException> maxAssignmentExceededHandler) {
final Consumer<MaxAssignmentsExceededInfo> maxAssignmentExceededHandler) {
this.targetRepository = targetRepository;
this.afterCommit = afterCommit;
this.actionRepository = actionRepository;
@@ -96,7 +96,7 @@ public abstract class AbstractDsAssignmentStrategy {
// create the action
return optTarget.map(target -> {
assertActionsPerTargetQuota(target, 1);
assertActionsPerTargetQuota(target);
final JpaAction actionForTarget = new JpaAction();
actionForTarget.setActionType(targetWithActionType.getActionType());
actionForTarget.setForcedTime(targetWithActionType.getForceTime());
@@ -144,7 +144,7 @@ public abstract class AbstractDsAssignmentStrategy {
*
* @param targetsIds to override {@link Action}s
*/
protected List<Long> overrideObsoleteUpdateActions(final Collection<Long> targetsIds) {
protected void overrideObsoleteUpdateActions(final Collection<Long> targetsIds) {
// Figure out if there are potential target/action combinations that
// need to be considered for cancellation
final List<JpaAction> activeActions = actionRepository.findAll((root, query, cb) -> {
@@ -157,22 +157,18 @@ public abstract class AbstractDsAssignmentStrategy {
);
});
final List<Long> targetIds = activeActions.stream().map(action -> {
activeActions.forEach(action -> {
action.setStatus(Status.CANCELING);
// document that the status has been retrieved
actionStatusRepository.save(new JpaActionStatus(action, Status.CANCELING, System.currentTimeMillis(),
RepositoryConstants.SERVER_MESSAGE_PREFIX + "cancel obsolete action due to new update"));
actionRepository.save(action);
return action.getTarget().getId();
}).toList();
});
if (!activeActions.isEmpty()) {
cancelAssignDistributionSetEvent(Collections.unmodifiableList(activeActions));
}
return targetIds;
}
/**
@@ -183,7 +179,7 @@ public abstract class AbstractDsAssignmentStrategy {
*
* @param targetsIds to override {@link Action}s
*/
protected List<Long> closeObsoleteUpdateActions(final Collection<Long> targetsIds) {
protected void closeObsoleteUpdateActions(final Collection<Long> targetsIds) {
// Figure out if there are potential target/action combinations that
// need to be considered for cancellation
final List<JpaAction> activeActions = actionRepository.findAll((root, query, cb) -> {
@@ -195,7 +191,7 @@ public abstract class AbstractDsAssignmentStrategy {
);
});
return activeActions.stream().map(action -> {
activeActions.forEach(action -> {
action.setStatus(Status.CANCELED);
action.setActive(false);
@@ -203,10 +199,7 @@ public abstract class AbstractDsAssignmentStrategy {
actionStatusRepository.save(new JpaActionStatus(action, Status.CANCELED, System.currentTimeMillis(),
RepositoryConstants.SERVER_MESSAGE_PREFIX + "close obsolete action due to new update"));
actionRepository.save(action);
return action.getTarget().getId();
}).toList();
});
}
/**
@@ -237,10 +230,6 @@ public abstract class AbstractDsAssignmentStrategy {
*/
abstract List<JpaTarget> findTargetsForAssignment(final List<String> controllerIDs, final long distributionSetId);
/**
* @param set
* @param targets
*/
abstract void sendTargetUpdatedEvents(final DistributionSet set, final List<JpaTarget> targets);
/**
@@ -260,9 +249,8 @@ public abstract class AbstractDsAssignmentStrategy {
* such actions existed.
*
* @param targetIds to cancel actions for
* @return {@link Set} of {@link Target#getId()}s
*/
abstract Set<Long> cancelActiveActions(List<List<Long>> targetIds);
abstract void cancelActiveActions(List<List<Long>> targetIds);
/**
* Cancels actions that can be canceled (i.e.
@@ -274,8 +262,6 @@ public abstract class AbstractDsAssignmentStrategy {
*/
abstract void closeActiveActions(List<List<Long>> targetIds);
abstract void sendDeploymentEvents(final DistributionSetAssignmentResult assignmentResult);
abstract void sendDeploymentEvents(final List<DistributionSetAssignmentResult> assignmentResults);
private static String getActionMessage(final Action action) {
@@ -297,12 +283,12 @@ public abstract class AbstractDsAssignmentStrategy {
.publishEvent(new CancelTargetAssignmentEvent(tenant, actions)));
}
private void assertActionsPerTargetQuota(final Target target, final long requested) {
private void assertActionsPerTargetQuota(final Target target) {
final int quota = quotaManagement.getMaxActionsPerTarget();
try {
QuotaHelper.assertAssignmentQuota(target.getId(), requested, quota, Action.class, Target.class, actionRepository::countByTargetId);
} catch (AssignmentQuotaExceededException ex) {
maxAssignmentExceededHandler.accept(target.getId(), requested, ex);
QuotaHelper.assertAssignmentQuota(target.getId(), 1, quota, Action.class, Target.class, actionRepository::countByTargetId);
} catch (AssignmentQuotaExceededException e) {
maxAssignmentExceededHandler.accept(new MaxAssignmentsExceededInfo(target.getId(), 1, e));
}
}
}

View File

@@ -24,6 +24,7 @@ import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
@@ -37,8 +38,6 @@ import jakarta.persistence.criteria.Root;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.ListUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.function.TriConsumer;
import org.eclipse.hawkbit.repository.ActionFields;
import org.eclipse.hawkbit.repository.DeploymentManagement;
import org.eclipse.hawkbit.repository.QuotaManagement;
@@ -162,6 +161,11 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl
this.targetRepository = targetRepository;
this.auditorProvider = auditorProvider;
this.txManager = txManager;
final Consumer<MaxAssignmentsExceededInfo> maxAssignmentsExceededHandler = maxAssignmentsExceededInfo ->
handleMaxAssignmentsExceeded(
maxAssignmentsExceededInfo.targetId,
maxAssignmentsExceededInfo.requested,
maxAssignmentsExceededInfo.quotaExceededException);
onlineDsAssignmentStrategy = new OnlineDsAssignmentStrategy(targetRepository, afterCommit, actionRepository, actionStatusRepository,
quotaManagement, this::isMultiAssignmentsEnabled, this::isConfirmationFlowEnabled, repositoryProperties,
maxAssignmentsExceededHandler);
@@ -409,14 +413,6 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl
@Transactional
public void deleteAction(final long actionId) {
log.info("Deleting action {}", actionId);
actionRepository.getAccessController().ifPresent(accessController -> {
// check update access
if (ObjectUtils.isEmpty(actionRepository.findAll(accessController.appendAccessRules(
AccessController.Operation.UPDATE, ((root, q, cb) -> cb.equal(root.get(AbstractJpaBaseEntity_.id), actionId)))))) {
// could be also InsufficientPermissionException but for security reasons we do not reveal that the entity exists
throw new EntityNotFoundException(Action.class, actionId);
}
});
actionRepository.deleteById(actionId);
}
@@ -424,56 +420,41 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl
@Transactional
public void deleteActionsByRsql(final String rsql) {
log.info("Deleting actions matching rsql {}", rsql);
final Specification<JpaAction> specification = QLSupport.getInstance().buildSpec(rsql, ActionFields.class);
actionRepository.delete(
actionRepository.getAccessController()
.map(accessController -> accessController.appendAccessRules(AccessController.Operation.UPDATE, specification))
.orElse(specification));
actionRepository.delete(QLSupport.getInstance().buildSpec(rsql, ActionFields.class));
}
@Override
@Transactional
public void deleteActionsByIds(final List<Long> actionIds) {
log.info("Deleting actions with ids {}", actionIds);
actionRepository.getAccessController().ifPresent(accessController ->
actionRepository.findAll(
accessController.appendAccessRules(AccessController.Operation.UPDATE, ActionSpecifications.byIdIn(actionIds))));
actionRepository.deleteByIdIn(actionIds);
actionRepository.deleteAllById(actionIds);
}
@Override
@Transactional
public void deleteTargetActionsByIds(final String target, final List<Long> actionsIds) {
log.info("Delete actions for target {} with action ids {}", target, actionsIds);
targetRepository.getAccessController()
.ifPresent(accessController ->
accessController.assertOperationAllowed(AccessController.Operation.UPDATE, targetRepository.getByControllerId(target)));
actionRepository.delete(ActionSpecifications.byControllerIdAndIdIn(target, actionsIds));
public void deleteTargetActionsByIds(final String controllerId, final List<Long> actionsIds) {
log.info("Delete actions for target {} with action ids {}", controllerId, actionsIds);
actionRepository.delete(ActionSpecifications.byControllerIdAndIdIn(controllerId, actionsIds));
}
@Override
@Transactional
public void deleteOldestTargetActions(final String target, final int keepLast) {
final JpaTarget jpaTarget = targetRepository.findByControllerId(target)
.orElseThrow(EntityNotFoundException::new);
public void deleteOldestTargetActions(final String controllerId, final int keepLast) {
final JpaTarget target = targetRepository.findByControllerId(controllerId).orElseThrow(EntityNotFoundException::new);
// check access to target since deletion will be executed via native query
targetRepository.getAccessController().ifPresent(accessController ->
accessController.assertOperationAllowed(AccessController.Operation.UPDATE, jpaTarget));
final long targetActions = actionRepository.countByTargetId(jpaTarget.getId());
long oldestToDelete;
accessController.assertOperationAllowed(AccessController.Operation.UPDATE, target));
final long targetActions = actionRepository.countByTargetId(target.getId());
if (targetActions > keepLast) {
oldestToDelete = targetActions - keepLast;
} else {
return;
final long oldestToDelete = targetActions - keepLast;
deleteOldestTargetActions(target.getId(), (int) oldestToDelete);
}
deleteOldestTargetActions(jpaTarget.getId(), (int) oldestToDelete);
}
@Override
@Transactional(isolation = Isolation.READ_COMMITTED)
@Retryable(retryFor = {
ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY))
@Retryable(retryFor = { ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX,
backoff = @Backoff(delay = Constants.TX_RT_DELAY))
public void cancelInactiveScheduledActionsForTargets(final List<Long> targetIds) {
if (!isMultiAssignmentsEnabled()) {
targetRepository.getAccessController().ifPresent(v -> {
@@ -598,12 +579,11 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl
}
}
@Override
public void handleMaxAssignmentsExceeded(final Long targetId, final Long requested, final AssignmentQuotaExceededException ex) {
maxAssignmentsExceededHandler.accept(targetId, requested, ex);
}
public record MaxAssignmentsExceededInfo(long targetId, long requested, AssignmentQuotaExceededException quotaExceededException) {}
private final TriConsumer<Long, Long, AssignmentQuotaExceededException> maxAssignmentsExceededHandler = (targetId, requested, quotaExceededException) -> {
@Override
public void handleMaxAssignmentsExceeded(
final Long targetId, final Long requested, final AssignmentQuotaExceededException quotaExceededException) {
int actionsPurgePercentage = getActionsPurgePercentage();
int quota = quotaManagement.getMaxActionsPerTarget();
if (actionsPurgePercentage > 0 && actionsPurgePercentage < 100) {
@@ -622,7 +602,9 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl
} else {
throw quotaExceededException;
}
};
}
;
/**
* Deletes the first n target actions of a target
@@ -630,7 +612,7 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl
* @param targetId - target id
* @param oldestToDelete - number of oldest actions to be deleted
*/
public void deleteOldestTargetActions(long targetId, int oldestToDelete) {
private void deleteOldestTargetActions(long targetId, int oldestToDelete) {
// Workaround for the case where JPQL or Criteria API do not support LIMIT
log.info("Deleting last {} actions of target {}", oldestToDelete, targetId);
final String SQL = "DELETE FROM sp_action WHERE id IN(" +
@@ -641,7 +623,7 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl
" LIMIT " + oldestToDelete
+ ") AS sub"
+ ")";
Query query = entityManager.createNativeQuery(SQL);
final Query query = entityManager.createNativeQuery(SQL);
query.setParameter("target", targetId);
query.executeUpdate();
}
@@ -912,7 +894,7 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl
} catch (final AssignmentQuotaExceededException ex) {
targetRepository.findByControllerId(controllerId).ifPresentOrElse(
// assume requested are always smaller than int size
target -> maxAssignmentsExceededHandler.accept(target.getId(), requested, ex),
target -> handleMaxAssignmentsExceeded(target.getId(), requested, ex),
() -> {
throw new EntityNotFoundException(Target.class, controllerId);
});

View File

@@ -9,25 +9,21 @@
*/
package org.eclipse.hawkbit.repository.jpa.management;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
import java.util.function.Function;
import org.apache.commons.collections4.ListUtils;
import org.apache.commons.lang3.function.TriConsumer;
import org.eclipse.hawkbit.repository.QuotaManagement;
import org.eclipse.hawkbit.repository.RepositoryConstants;
import org.eclipse.hawkbit.repository.RepositoryProperties;
import org.eclipse.hawkbit.repository.exception.AssignmentQuotaExceededException;
import org.eclipse.hawkbit.repository.exception.InsufficientPermissionException;
import org.eclipse.hawkbit.repository.jpa.JpaManagementHelper;
import org.eclipse.hawkbit.repository.jpa.acm.AccessController;
import org.eclipse.hawkbit.repository.jpa.configuration.Constants;
import org.eclipse.hawkbit.repository.jpa.executor.AfterTransactionCommitExecutor;
import org.eclipse.hawkbit.repository.jpa.management.JpaDeploymentManagement.MaxAssignmentsExceededInfo;
import org.eclipse.hawkbit.repository.jpa.model.JpaAction;
import org.eclipse.hawkbit.repository.jpa.model.JpaActionStatus;
import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet;
@@ -53,7 +49,7 @@ class OfflineDsAssignmentStrategy extends AbstractDsAssignmentStrategy {
final ActionRepository actionRepository, final ActionStatusRepository actionStatusRepository,
final QuotaManagement quotaManagement, final BooleanSupplier multiAssignmentsConfig,
final BooleanSupplier confirmationFlowConfig, final RepositoryProperties repositoryProperties,
final TriConsumer<Long, Long, AssignmentQuotaExceededException> maxAssignmentExceededHandler) {
final Consumer<MaxAssignmentsExceededInfo> maxAssignmentExceededHandler) {
super(targetRepository, afterCommit, actionRepository, actionStatusRepository,
quotaManagement, multiAssignmentsConfig, confirmationFlowConfig, repositoryProperties, maxAssignmentExceededHandler);
}
@@ -128,8 +124,7 @@ class OfflineDsAssignmentStrategy extends AbstractDsAssignmentStrategy {
}
@Override
public Set<Long> cancelActiveActions(final List<List<Long>> targetIds) {
return Collections.emptySet();
public void cancelActiveActions(final List<List<Long>> targetIds) {
}
@Override
@@ -137,14 +132,8 @@ class OfflineDsAssignmentStrategy extends AbstractDsAssignmentStrategy {
// Not supported by offline case
}
@Override
void sendDeploymentEvents(final DistributionSetAssignmentResult assignmentResult) {
// no need to send deployment events in the offline case
}
@Override
void sendDeploymentEvents(final List<DistributionSetAssignmentResult> assignmentResults) {
// no need to send deployment events in the offline case
}
}
}

View File

@@ -9,30 +9,25 @@
*/
package org.eclipse.hawkbit.repository.jpa.management;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.collections4.ListUtils;
import org.apache.commons.lang3.function.TriConsumer;
import org.eclipse.hawkbit.repository.QuotaManagement;
import org.eclipse.hawkbit.repository.RepositoryProperties;
import org.eclipse.hawkbit.repository.event.EventPublisherHolder;
import org.eclipse.hawkbit.repository.event.remote.MultiActionAssignEvent;
import org.eclipse.hawkbit.repository.event.remote.MultiActionCancelEvent;
import org.eclipse.hawkbit.repository.event.remote.TargetAssignDistributionSetEvent;
import org.eclipse.hawkbit.repository.exception.AssignmentQuotaExceededException;
import org.eclipse.hawkbit.repository.exception.InsufficientPermissionException;
import org.eclipse.hawkbit.repository.jpa.acm.AccessController;
import org.eclipse.hawkbit.repository.jpa.configuration.Constants;
import org.eclipse.hawkbit.repository.jpa.executor.AfterTransactionCommitExecutor;
import org.eclipse.hawkbit.repository.jpa.management.JpaDeploymentManagement.MaxAssignmentsExceededInfo;
import org.eclipse.hawkbit.repository.jpa.model.JpaAction;
import org.eclipse.hawkbit.repository.jpa.model.JpaActionStatus;
import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet;
@@ -59,7 +54,7 @@ class OnlineDsAssignmentStrategy extends AbstractDsAssignmentStrategy {
final ActionRepository actionRepository, final ActionStatusRepository actionStatusRepository,
final QuotaManagement quotaManagement, final BooleanSupplier multiAssignmentsConfig,
final BooleanSupplier confirmationFlowConfig, final RepositoryProperties repositoryProperties,
final TriConsumer<Long, Long, AssignmentQuotaExceededException> maxAssignmentExceededHandler) {
final Consumer<MaxAssignmentsExceededInfo> maxAssignmentExceededHandler) {
super(targetRepository, afterCommit, actionRepository, actionStatusRepository,
quotaManagement, multiAssignmentsConfig, confirmationFlowConfig, repositoryProperties, maxAssignmentExceededHandler);
}
@@ -129,7 +124,8 @@ class OnlineDsAssignmentStrategy extends AbstractDsAssignmentStrategy {
}
@Override
public void setAssignedDistributionSetAndTargetStatus(final JpaDistributionSet set, final List<List<Long>> targetIds, final String currentUser) {
public void setAssignedDistributionSetAndTargetStatus(final JpaDistributionSet set, final List<List<Long>> targetIds,
final String currentUser) {
final long now = System.currentTimeMillis();
targetIds.forEach(targetIdsChunk -> {
if (targetRepository.count(AccessController.Operation.UPDATE,
@@ -154,8 +150,8 @@ class OnlineDsAssignmentStrategy extends AbstractDsAssignmentStrategy {
}
@Override
public Set<Long> cancelActiveActions(final List<List<Long>> targetIds) {
return targetIds.stream().map(this::overrideObsoleteUpdateActions).flatMap(Collection::stream).collect(Collectors.toSet());
public void cancelActiveActions(final List<List<Long>> targetIds) {
targetIds.forEach(this::overrideObsoleteUpdateActions);
}
@Override
@@ -163,15 +159,6 @@ class OnlineDsAssignmentStrategy extends AbstractDsAssignmentStrategy {
targetIds.forEach(this::closeObsoleteUpdateActions);
}
@Override
public void sendDeploymentEvents(final DistributionSetAssignmentResult assignmentResult) {
if (isMultiAssignmentsEnabled()) {
sendDeploymentEvents(Collections.singletonList(assignmentResult));
} else {
sendDistributionSetAssignedEvent(assignmentResult);
}
}
@Override
public void sendDeploymentEvents(final List<DistributionSetAssignmentResult> assignmentResults) {
if (isMultiAssignmentsEnabled()) {
@@ -190,7 +177,7 @@ class OnlineDsAssignmentStrategy extends AbstractDsAssignmentStrategy {
}
void sendCancellationMessages(final List<JpaAction> actions, final String tenant) {
if(isMultiAssignmentsEnabled()) {
if (isMultiAssignmentsEnabled()) {
sendMultiActionCancelEvent(tenant, Collections.unmodifiableList(actions));
} else {
actions.forEach(this::cancelAssignDistributionSetEvent);
@@ -224,12 +211,10 @@ class OnlineDsAssignmentStrategy extends AbstractDsAssignmentStrategy {
sendMultiActionAssignEvent(tenant, filteredActions);
}
private DistributionSetAssignmentResult sendDistributionSetAssignedEvent(
final DistributionSetAssignmentResult assignmentResult) {
private void sendDistributionSetAssignedEvent(final DistributionSetAssignmentResult assignmentResult) {
final List<Action> filteredActions = filterCancellations(assignmentResult.getAssignedEntity()).toList();
final DistributionSet set = assignmentResult.getDistributionSet();
sendTargetAssignDistributionSetEvent(set.getTenant(), set.getId(), filteredActions);
return assignmentResult;
}
private void sendTargetAssignDistributionSetEvent(final String tenant, final long distributionSetId,

View File

@@ -320,15 +320,4 @@ public interface ActionRepository extends BaseEntityRepository<JpaAction> {
@Transactional
@Query("UPDATE JpaAction a SET a.externalRef = :externalRef WHERE a.id = :actionId")
void updateExternalRef(@Param("actionId") Long actionId, @Param("externalRef") String externalRef);
/**
* Deletes all actions with the given IDs.
*
* @param actionIDs the IDs of the actions to be deleted.
*/
@Modifying
@Transactional
// Workaround for https://bugs.eclipse.org/bugs/show_bug.cgi?id=349477
@Query("DELETE FROM JpaAction a WHERE a.id IN ?1")
void deleteByIdIn(Collection<Long> actionIDs);
}

View File

@@ -43,11 +43,6 @@ public final class ActionSpecifications {
cb.equal(root.get(JpaAction_.active), true));
}
public static Specification<JpaAction> byIdIn(final List<Long> actionIds) {
return ((root, query, cb) ->
root.get(AbstractJpaBaseEntity_.id).in(actionIds));
}
public static Specification<JpaAction> byTargetControllerId(final String controllerId) {
return (root, query, cb) -> cb.equal(root.get(JpaAction_.target).get(JpaTarget_.controllerId), controllerId);
}

View File

@@ -53,12 +53,12 @@ public final class TargetSpecifications {
/**
* {@link Specification} for retrieving {@link Target}s including {@link TargetTag}s.
*
* @param controllerIDs to search for
* @param controllerIds to search for
* @return the {@link Target} {@link Specification}
*/
public static Specification<JpaTarget> byControllerIdWithTagsInJoin(final Collection<String> controllerIDs) {
public static Specification<JpaTarget> byControllerIdWithTagsInJoin(final Collection<String> controllerIds) {
return (targetRoot, query, cb) -> {
final Predicate predicate = targetRoot.get(JpaTarget_.controllerId).in(controllerIDs);
final Predicate predicate = targetRoot.get(JpaTarget_.controllerId).in(controllerIds);
targetRoot.fetch(JpaTarget_.tags, JoinType.LEFT);
query.distinct(true);
return predicate;

View File

@@ -10,6 +10,8 @@
package org.eclipse.hawkbit.repository.jpa.acm;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatNoException;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.eclipse.hawkbit.im.authentication.SpPermission.READ_DISTRIBUTION_SET;
import static org.eclipse.hawkbit.im.authentication.SpPermission.READ_TARGET;
@@ -56,17 +58,17 @@ class DeploymentManagementTest extends AbstractAccessControllerManagementTest {
void verifyActionVisibility() {
final String controllerId = target1Type1.getControllerId();
verify(
assignedId -> {
actionId -> {
assertThat(deploymentManagement.findActionsAll(UNPAGED)).isEmpty();
assertThat(deploymentManagement.findAction(assignedId)).isEmpty();
assertThatThrownBy(() -> deploymentManagement.findActionWithDetails(assignedId))
assertThat(deploymentManagement.findAction(actionId)).isEmpty();
assertThatThrownBy(() -> deploymentManagement.findActionWithDetails(actionId))
.isInstanceOf(InsufficientPermissionException.class);
assertThat(deploymentManagement.findActions("id==*", UNPAGED)).isEmpty();
assertThatThrownBy(() -> deploymentManagement.findActionsByTarget(controllerId, UNPAGED))
.isInstanceOf(EntityNotFoundException.class);
assertThatThrownBy(() -> deploymentManagement.findActionsByTarget("id==*", controllerId, UNPAGED))
.isInstanceOf(EntityNotFoundException.class);
assertThatThrownBy(() -> deploymentManagement.findActionStatusByAction(assignedId, UNPAGED))
assertThatThrownBy(() -> deploymentManagement.findActionStatusByAction(actionId, UNPAGED))
.isInstanceOf(EntityNotFoundException.class);
assertThatThrownBy(() -> deploymentManagement.findActiveActionsByTarget(controllerId, UNPAGED))
.isInstanceOf(EntityNotFoundException.class);
@@ -76,16 +78,16 @@ class DeploymentManagementTest extends AbstractAccessControllerManagementTest {
assertThatThrownBy(() -> deploymentManagement.hasPendingCancellations(targetId)).isInstanceOf(
EntityNotFoundException.class);
},
assignedId -> {
actionId -> {
assertThat(deploymentManagement.findActionsAll(UNPAGED)).hasSize(1).allMatch(this::isActionOfTarget1Type1);
assertThat(deploymentManagement.findAction(assignedId)).hasValueSatisfying(this::assertActionOfTarget1Type1);
assertThat(deploymentManagement.findActionWithDetails(assignedId)).hasValueSatisfying(this::assertActionOfTarget1Type1);
assertThat(deploymentManagement.findAction(actionId)).hasValueSatisfying(this::assertActionOfTarget1Type1);
assertThat(deploymentManagement.findActionWithDetails(actionId)).hasValueSatisfying(this::assertActionOfTarget1Type1);
assertThat(deploymentManagement.findActions("id==*", UNPAGED)).hasSize(1).allMatch(this::isActionOfTarget1Type1);
assertThat(deploymentManagement.findActionsByTarget(controllerId, UNPAGED)).hasSize(1)
.allMatch(this::isActionOfTarget1Type1);
assertThat(deploymentManagement.findActionsByTarget("id==*", controllerId, UNPAGED))
.hasSize(1).allMatch(this::isActionOfTarget1Type1);
assertThat(deploymentManagement.findActionStatusByAction(assignedId, UNPAGED))
assertThat(deploymentManagement.findActionStatusByAction(actionId, UNPAGED))
.hasSize(1).allMatch(actionStatus -> actionStatus.getStatus().equals(Action.Status.RUNNING));
assertThat(deploymentManagement.findActiveActionsByTarget(controllerId, UNPAGED))
.hasSize(1).allMatch(this::isActionOfTarget1Type1);
@@ -96,24 +98,83 @@ class DeploymentManagementTest extends AbstractAccessControllerManagementTest {
null);
}
@Test
void verifyDeleteActionById() {
verify(
null,
actionId -> assertThatExceptionOfType(InsufficientPermissionException.class)
.isThrownBy(() -> deploymentManagement.deleteAction(actionId)),
actionId -> assertThatNoException().isThrownBy(() -> deploymentManagement.deleteAction(actionId)));
}
@Test
void verifyDeleteActionsById() {
verify(
null,
actionId -> {
final List<Long> actionIds = List.of(actionId);
assertThatExceptionOfType(InsufficientPermissionException.class)
.isThrownBy(() -> deploymentManagement.deleteActionsByIds(actionIds));
},
actionId -> assertThatNoException().isThrownBy(() -> deploymentManagement.deleteActionsByIds(List.of(actionId))));
}
@Test
void verifyDeleteActionByRSQL() {
verify(
null,
actionId -> {
assertThatNoException().isThrownBy(() -> deploymentManagement.deleteActionsByRsql("id==" + actionId));
// no exception but the action shall not be deleted
assertThat(deploymentManagement.findAction(actionId)).isPresent();
},
actionId -> {
assertThatNoException().isThrownBy(() -> deploymentManagement.deleteActionsByRsql("id==" + actionId));
// the action shall be deleted
assertThat(deploymentManagement.findAction(actionId)).isEmpty();
});
}
@Test
void verifyDeleteTargetActionsById() {
verify(
null,
actionId -> {
final String controllerId = deploymentManagement.findAction(actionId)
.map(Action::getTarget).map(Target::getControllerId).orElseThrow();
final List<Long> actionIds = List.of(actionId, -1L);
assertThatNoException().isThrownBy(() -> deploymentManagement.deleteTargetActionsByIds(controllerId, actionIds));
// no exception but the action shall not be deleted
assertThat(deploymentManagement.findAction(actionId)).isPresent();
},
actionId -> {
final String controllerId = deploymentManagement.findAction(actionId)
.map(Action::getTarget).map(Target::getControllerId).orElseThrow();
assertThatNoException().isThrownBy(
() -> deploymentManagement.deleteTargetActionsByIds(controllerId, List.of(actionId, -1L)));
// the action shall be deleted
assertThat(deploymentManagement.findAction(actionId)).isEmpty();
});
}
@Test
void verifyCancellation() {
verify(
assignedId -> assertThatThrownBy(() -> deploymentManagement.cancelAction(assignedId))
actionId -> assertThatThrownBy(() -> deploymentManagement.cancelAction(actionId))
.isInstanceOf(EntityNotFoundException.class),
assignedId -> assertThatThrownBy(() -> deploymentManagement.cancelAction(assignedId))
actionId -> assertThatThrownBy(() -> deploymentManagement.cancelAction(actionId))
.isInstanceOf(InsufficientPermissionException.class),
assignedId -> assertThat(deploymentManagement.cancelAction(assignedId).getId()).isEqualTo(assignedId));
actionId -> assertThat(deploymentManagement.cancelAction(actionId).getId()).isEqualTo(actionId));
}
@Test
void verifyCancellationByDistributionSetId() {
verify(
assignedId -> {
actionId -> {
deploymentManagement.cancelActionsForDistributionSet(ActionCancellationType.FORCE, ds1Type1);
assertThat(deploymentManagement.findAction(assignedId)).isEmpty();
assertThat(deploymentManagement.findAction(actionId)).isEmpty();
},
assignedId -> assertThat(deploymentManagement.findAction(assignedId))
actionId -> assertThat(deploymentManagement.findAction(actionId))
.hasValueSatisfying(action -> assertThat(action.getStatus()).isEqualTo(Action.Status.RUNNING)),
null);
}
@@ -121,37 +182,37 @@ class DeploymentManagementTest extends AbstractAccessControllerManagementTest {
@Test
void verifyForceActionIsNotAllowed() {
verify(
assignedId -> assertThatThrownBy(() -> deploymentManagement.forceTargetAction(assignedId))
actionId -> assertThatThrownBy(() -> deploymentManagement.forceTargetAction(actionId))
.isInstanceOf(EntityNotFoundException.class),
assignedId -> assertThatThrownBy(() -> deploymentManagement.forceTargetAction(assignedId))
actionId -> assertThatThrownBy(() -> deploymentManagement.forceTargetAction(actionId))
.isInstanceOf(InsufficientPermissionException.class),
assignedId -> assertThat(deploymentManagement.forceTargetAction(assignedId).getActionType())
actionId -> assertThat(deploymentManagement.forceTargetAction(actionId).getActionType())
.isEqualTo(Action.ActionType.FORCED));
}
private void verify(final Consumer<Long> noRead, final Consumer<Long> noUpdate, final Consumer<Long> readAndUpdate) {
final Long assignedId = systemSecurityContext.runAsSystem(() -> {
final List<Action> assignedEntity = assignDistributionSet(ds1Type1.getId(), target1Type1.getControllerId()).getAssignedEntity();
assertThat(assignedEntity).hasSize(1).allMatch(action -> action.getTarget().getId().equals(target1Type1.getId()));
return assignedEntity.get(0);
private void verify(final Consumer<Long> noRead, final Consumer<Long> readNoUpdate, final Consumer<Long> readAndUpdate) {
final Long actionId = systemSecurityContext.runAsSystem(() -> {
final List<Action> actions = assignDistributionSet(ds1Type1.getId(), target1Type1.getControllerId()).getAssignedEntity();
assertThat(actions).hasSize(1).allMatch(action -> action.getTarget().getId().equals(target1Type1.getId()));
return actions.get(0);
}).getId();
if (noRead != null) {
// no read permission
runAs(withAuthorities(READ_TARGET + "/type.id==" + targetType2.getId(), UPDATE_TARGET + "/type.id==" + targetType2.getId()),
() -> noRead.accept(assignedId));
() -> noRead.accept(actionId));
}
if (noUpdate != null) {
if (readNoUpdate != null) {
// read but no update permission
runAs(withAuthorities(READ_TARGET + "/type.id==" + targetType1.getId(), UPDATE_TARGET + "/type.id==" + targetType2.getId()),
() -> noUpdate.accept(assignedId));
() -> readNoUpdate.accept(actionId));
}
if (readAndUpdate != null) {
// read and update permissions
runAs(withAuthorities(READ_TARGET + "/type.id==" + targetType1.getId(), UPDATE_TARGET + "/type.id==" + targetType1.getId()),
() -> readAndUpdate.accept(assignedId));
() -> readAndUpdate.accept(actionId));
}
}

View File

@@ -11,7 +11,6 @@ package org.eclipse.hawkbit.repository.jpa.acm;
import static org.assertj.core.api.Assertions.assertThat;
import static org.eclipse.hawkbit.im.authentication.SpPermission.CREATE_PREFIX;
import static org.eclipse.hawkbit.im.authentication.SpPermission.CREATE_TARGET;
import static org.eclipse.hawkbit.im.authentication.SpPermission.DELETE_PREFIX;
import static org.eclipse.hawkbit.im.authentication.SpPermission.DELETE_TARGET;
import static org.eclipse.hawkbit.im.authentication.SpPermission.DISTRIBUTION_SET;
@@ -23,6 +22,7 @@ import static org.eclipse.hawkbit.im.authentication.SpPermission.SOFTWARE_MODULE
import static org.eclipse.hawkbit.im.authentication.SpPermission.SOFTWARE_MODULE_TYPE;
import static org.eclipse.hawkbit.im.authentication.SpPermission.TARGET_TYPE;
import static org.eclipse.hawkbit.im.authentication.SpPermission.UPDATE_PREFIX;
import static org.eclipse.hawkbit.im.authentication.SpPermission.UPDATE_TARGET;
import static org.eclipse.hawkbit.repository.test.util.SecurityContextSwitch.runAs;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
@@ -92,17 +92,22 @@ class SystemExecutionTest extends AbstractAccessControllerManagementTest {
() -> verifyAccessController(distributionSetTypeAccessController));
runAs(withAuthorities(READ_TARGET, DELETE_TARGET + "/type.id==1"),
() -> verifyAccessController(targetAccessController));
runAs(withAuthorities(UPDATE_PREFIX + TARGET_TYPE + "/id==1"), () -> verifyAccessController(targetTypeAccessController));
runAs(withAuthorities(CREATE_TARGET + "/type.id==1"), () -> verifyAccessController(actionAccessController));
runAs(withAuthorities(CREATE_PREFIX + TARGET_TYPE + "/id==1"), () -> verifyAccessController(targetTypeAccessController));
// Action Access Controller maps CREATE/UPDATE/DELETE to UPDATE - so only UPDATE (or READ scope) is relevant
// and update will be called for every of the mapped - so 3 times
runAs(withAuthorities(UPDATE_TARGET + "/type.id==1"), () -> verifyAccessController(actionAccessController, 3));
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private void verifyAccessController(final AccessController<?> accessController) {
verifyAccessController(accessController, 1);
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private void verifyAccessController(final AccessController<?> accessController, final int times) {
final Specification mock = mock(Specification.class);
for (final Operation operation : Operation.values()) {
accessController.appendAccessRules(operation, mock);
}
verify(mock, times(1)).and(any()); // once for every access controller is scoped only
verify(mock, times(times)).and(any()); // once for every access controller is scoped only
final Specification mockAsSystem = mock(Specification.class);
for (Operation operation : Operation.values()) {