diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/DeploymentManagement.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/DeploymentManagement.java index d32a88be4..77590880b 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/DeploymentManagement.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/DeploymentManagement.java @@ -320,22 +320,22 @@ public interface DeploymentManagement extends PermissionSupport { void deleteActionsByIds(List 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 actionsIds); + void deleteTargetActionsByIds(final String controllerId, final List 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); } \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutExecutor.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutExecutor.java index 796b504c3..27e20d954 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutExecutor.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutExecutor.java @@ -385,14 +385,13 @@ public class JpaRolloutExecutor implements RolloutExecutor { } private void deleteScheduledActions(final JpaRollout rollout, final Slice scheduledActions) { - final boolean hasScheduledActions = scheduledActions.getNumberOfElements() > 0; - - if (hasScheduledActions) { + if (scheduledActions.getNumberOfElements() > 0) { + // has scheduled actions - delete them try { final List 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) { diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/acm/AccessControllerConfiguration.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/acm/AccessControllerConfiguration.java index 267590a12..2a70520ce 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/acm/AccessControllerConfiguration.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/acm/AccessControllerConfiguration.java @@ -80,7 +80,7 @@ public class AccessControllerConfiguration { @Override @SuppressWarnings("unchecked") public Optional> 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 targetJoin = actionRoot.join(JpaAction_.target); final EntityType targetModel = query.from(JpaTarget.class).getModel(); final Root targetRoot = (Root) 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; + }; } }; } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/AbstractDsAssignmentStrategy.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/AbstractDsAssignmentStrategy.java index 55e344d2f..9a0dbd935 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/AbstractDsAssignmentStrategy.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/AbstractDsAssignmentStrategy.java @@ -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 maxAssignmentExceededHandler; + private final Consumer 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 maxAssignmentExceededHandler) { + final Consumer 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 overrideObsoleteUpdateActions(final Collection targetsIds) { + protected void overrideObsoleteUpdateActions(final Collection targetsIds) { // Figure out if there are potential target/action combinations that // need to be considered for cancellation final List activeActions = actionRepository.findAll((root, query, cb) -> { @@ -157,22 +157,18 @@ public abstract class AbstractDsAssignmentStrategy { ); }); - final List 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 closeObsoleteUpdateActions(final Collection targetsIds) { + protected void closeObsoleteUpdateActions(final Collection targetsIds) { // Figure out if there are potential target/action combinations that // need to be considered for cancellation final List 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 findTargetsForAssignment(final List controllerIDs, final long distributionSetId); - /** - * @param set - * @param targets - */ abstract void sendTargetUpdatedEvents(final DistributionSet set, final List 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 cancelActiveActions(List> targetIds); + abstract void cancelActiveActions(List> targetIds); /** * Cancels actions that can be canceled (i.e. @@ -274,8 +262,6 @@ public abstract class AbstractDsAssignmentStrategy { */ abstract void closeActiveActions(List> targetIds); - abstract void sendDeploymentEvents(final DistributionSetAssignmentResult assignmentResult); - abstract void sendDeploymentEvents(final List 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)); } } } \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaDeploymentManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaDeploymentManagement.java index 279261a37..cd40ae50d 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaDeploymentManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaDeploymentManagement.java @@ -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 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 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 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 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 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 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 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); }); diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/OfflineDsAssignmentStrategy.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/OfflineDsAssignmentStrategy.java index d112a40c4..02eb4c527 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/OfflineDsAssignmentStrategy.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/OfflineDsAssignmentStrategy.java @@ -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 maxAssignmentExceededHandler) { + final Consumer maxAssignmentExceededHandler) { super(targetRepository, afterCommit, actionRepository, actionStatusRepository, quotaManagement, multiAssignmentsConfig, confirmationFlowConfig, repositoryProperties, maxAssignmentExceededHandler); } @@ -128,8 +124,7 @@ class OfflineDsAssignmentStrategy extends AbstractDsAssignmentStrategy { } @Override - public Set cancelActiveActions(final List> targetIds) { - return Collections.emptySet(); + public void cancelActiveActions(final List> 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 assignmentResults) { // no need to send deployment events in the offline case } - -} +} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/OnlineDsAssignmentStrategy.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/OnlineDsAssignmentStrategy.java index 1c20ea258..3e525e32f 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/OnlineDsAssignmentStrategy.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/OnlineDsAssignmentStrategy.java @@ -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 maxAssignmentExceededHandler) { + final Consumer 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> targetIds, final String currentUser) { + public void setAssignedDistributionSetAndTargetStatus(final JpaDistributionSet set, final List> 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 cancelActiveActions(final List> targetIds) { - return targetIds.stream().map(this::overrideObsoleteUpdateActions).flatMap(Collection::stream).collect(Collectors.toSet()); + public void cancelActiveActions(final List> 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 assignmentResults) { if (isMultiAssignmentsEnabled()) { @@ -190,7 +177,7 @@ class OnlineDsAssignmentStrategy extends AbstractDsAssignmentStrategy { } void sendCancellationMessages(final List 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 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, diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/repository/ActionRepository.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/repository/ActionRepository.java index da317975a..ae96d5c2a 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/repository/ActionRepository.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/repository/ActionRepository.java @@ -320,15 +320,4 @@ public interface ActionRepository extends BaseEntityRepository { @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 actionIDs); } \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/specifications/ActionSpecifications.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/specifications/ActionSpecifications.java index 7ac37ee94..842512849 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/specifications/ActionSpecifications.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/specifications/ActionSpecifications.java @@ -43,11 +43,6 @@ public final class ActionSpecifications { cb.equal(root.get(JpaAction_.active), true)); } - public static Specification byIdIn(final List actionIds) { - return ((root, query, cb) -> - root.get(AbstractJpaBaseEntity_.id).in(actionIds)); - } - public static Specification byTargetControllerId(final String controllerId) { return (root, query, cb) -> cb.equal(root.get(JpaAction_.target).get(JpaTarget_.controllerId), controllerId); } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/specifications/TargetSpecifications.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/specifications/TargetSpecifications.java index 5a976433f..2fdad69c7 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/specifications/TargetSpecifications.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/specifications/TargetSpecifications.java @@ -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 byControllerIdWithTagsInJoin(final Collection controllerIDs) { + public static Specification byControllerIdWithTagsInJoin(final Collection 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; diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/DeploymentManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/DeploymentManagementTest.java index 8e5504f9b..39ebd91ad 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/DeploymentManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/DeploymentManagementTest.java @@ -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 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 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 noRead, final Consumer noUpdate, final Consumer readAndUpdate) { - final Long assignedId = systemSecurityContext.runAsSystem(() -> { - final List 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 noRead, final Consumer readNoUpdate, final Consumer readAndUpdate) { + final Long actionId = systemSecurityContext.runAsSystem(() -> { + final List 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)); } } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/SystemExecutionTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/SystemExecutionTest.java index 7e55007d5..b57d4b519 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/SystemExecutionTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/SystemExecutionTest.java @@ -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()) {