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); 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 * @param actionsIds - list of action ids to be deleted
*/ */
@PreAuthorize("hasAuthority('UPDATE_" + SpPermission.TARGET + "')") @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) * @param keepLast - number of actions to be left/kept (NOT deleted)
*/ */
@PreAuthorize("hasAuthority('UPDATE_" + SpPermission.TARGET + "')") @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} * 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); void cancelActionsForDistributionSet(final ActionCancellationType cancelationType, final DistributionSet set);
@PreAuthorize(SpringEvalExpressions.IS_SYSTEM_CODE) @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) { private void deleteScheduledActions(final JpaRollout rollout, final Slice<JpaAction> scheduledActions) {
final boolean hasScheduledActions = scheduledActions.getNumberOfElements() > 0; if (scheduledActions.getNumberOfElements() > 0) {
// has scheduled actions - delete them
if (hasScheduledActions) {
try { try {
final List<Long> actionIds = StreamSupport.stream(scheduledActions.spliterator(), false) final List<Long> actionIds = StreamSupport.stream(scheduledActions.spliterator(), false)
.map(Action::getId) .map(Action::getId)
.toList(); .toList();
actionRepository.deleteByIdIn(actionIds); actionRepository.deleteAllById(actionIds);
afterCommit.afterCommit(() -> EventPublisherHolder.getInstance().getEventPublisher() afterCommit.afterCommit(() -> EventPublisherHolder.getInstance().getEventPublisher()
.publishEvent(new RolloutUpdatedEvent(rollout))); .publishEvent(new RolloutUpdatedEvent(rollout)));
} catch (final RuntimeException e) { } catch (final RuntimeException e) {

View File

@@ -80,7 +80,7 @@ public class AccessControllerConfiguration {
@Override @Override
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public Optional<Specification<JpaAction>> getAccessRules(final Operation operation) { 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 Join<JpaAction, JpaTarget> targetJoin = actionRoot.join(JpaAction_.target);
final EntityType<JpaTarget> targetModel = query.from(JpaTarget.class).getModel(); final EntityType<JpaTarget> targetModel = query.from(JpaTarget.class).getModel();
final Root<JpaTarget> targetRoot = (Root<JpaTarget>) Proxy.newProxyInstance( final Root<JpaTarget> targetRoot = (Root<JpaTarget>) Proxy.newProxyInstance(
@@ -101,7 +101,15 @@ public class AccessControllerConfiguration {
@Override @Override
public void assertOperationAllowed(final Operation operation, final JpaAction entity) throws InsufficientPermissionException { 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.Collections;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import java.util.function.BooleanSupplier; import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
import jakarta.persistence.criteria.JoinType; import jakarta.persistence.criteria.JoinType;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.function.TriConsumer;
import org.eclipse.hawkbit.repository.QuotaManagement; import org.eclipse.hawkbit.repository.QuotaManagement;
import org.eclipse.hawkbit.repository.RepositoryConstants; import org.eclipse.hawkbit.repository.RepositoryConstants;
import org.eclipse.hawkbit.repository.RepositoryProperties; 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.exception.AssignmentQuotaExceededException;
import org.eclipse.hawkbit.repository.jpa.configuration.Constants; import org.eclipse.hawkbit.repository.jpa.configuration.Constants;
import org.eclipse.hawkbit.repository.jpa.executor.AfterTransactionCommitExecutor; 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.AbstractJpaBaseEntity_;
import org.eclipse.hawkbit.repository.jpa.model.JpaAction; import org.eclipse.hawkbit.repository.jpa.model.JpaAction;
import org.eclipse.hawkbit.repository.jpa.model.JpaActionStatus; import org.eclipse.hawkbit.repository.jpa.model.JpaActionStatus;
@@ -67,7 +67,7 @@ public abstract class AbstractDsAssignmentStrategy {
private final BooleanSupplier multiAssignmentsConfig; private final BooleanSupplier multiAssignmentsConfig;
private final BooleanSupplier confirmationFlowConfig; private final BooleanSupplier confirmationFlowConfig;
private final RepositoryProperties repositoryProperties; private final RepositoryProperties repositoryProperties;
private final TriConsumer<Long, Long, AssignmentQuotaExceededException> maxAssignmentExceededHandler; private final Consumer<MaxAssignmentsExceededInfo> maxAssignmentExceededHandler;
@SuppressWarnings("java:S107") @SuppressWarnings("java:S107")
AbstractDsAssignmentStrategy( AbstractDsAssignmentStrategy(
@@ -76,7 +76,7 @@ public abstract class AbstractDsAssignmentStrategy {
final ActionRepository actionRepository, final ActionStatusRepository actionStatusRepository, final ActionRepository actionRepository, final ActionStatusRepository actionStatusRepository,
final QuotaManagement quotaManagement, final BooleanSupplier multiAssignmentsConfig, final QuotaManagement quotaManagement, final BooleanSupplier multiAssignmentsConfig,
final BooleanSupplier confirmationFlowConfig, final RepositoryProperties repositoryProperties, final BooleanSupplier confirmationFlowConfig, final RepositoryProperties repositoryProperties,
final TriConsumer<Long, Long, AssignmentQuotaExceededException> maxAssignmentExceededHandler) { final Consumer<MaxAssignmentsExceededInfo> maxAssignmentExceededHandler) {
this.targetRepository = targetRepository; this.targetRepository = targetRepository;
this.afterCommit = afterCommit; this.afterCommit = afterCommit;
this.actionRepository = actionRepository; this.actionRepository = actionRepository;
@@ -96,7 +96,7 @@ public abstract class AbstractDsAssignmentStrategy {
// create the action // create the action
return optTarget.map(target -> { return optTarget.map(target -> {
assertActionsPerTargetQuota(target, 1); assertActionsPerTargetQuota(target);
final JpaAction actionForTarget = new JpaAction(); final JpaAction actionForTarget = new JpaAction();
actionForTarget.setActionType(targetWithActionType.getActionType()); actionForTarget.setActionType(targetWithActionType.getActionType());
actionForTarget.setForcedTime(targetWithActionType.getForceTime()); actionForTarget.setForcedTime(targetWithActionType.getForceTime());
@@ -144,7 +144,7 @@ public abstract class AbstractDsAssignmentStrategy {
* *
* @param targetsIds to override {@link Action}s * @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 // Figure out if there are potential target/action combinations that
// need to be considered for cancellation // need to be considered for cancellation
final List<JpaAction> activeActions = actionRepository.findAll((root, query, cb) -> { 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); action.setStatus(Status.CANCELING);
// document that the status has been retrieved // document that the status has been retrieved
actionStatusRepository.save(new JpaActionStatus(action, Status.CANCELING, System.currentTimeMillis(), actionStatusRepository.save(new JpaActionStatus(action, Status.CANCELING, System.currentTimeMillis(),
RepositoryConstants.SERVER_MESSAGE_PREFIX + "cancel obsolete action due to new update")); RepositoryConstants.SERVER_MESSAGE_PREFIX + "cancel obsolete action due to new update"));
actionRepository.save(action); actionRepository.save(action);
});
return action.getTarget().getId();
}).toList();
if (!activeActions.isEmpty()) { if (!activeActions.isEmpty()) {
cancelAssignDistributionSetEvent(Collections.unmodifiableList(activeActions)); cancelAssignDistributionSetEvent(Collections.unmodifiableList(activeActions));
} }
return targetIds;
} }
/** /**
@@ -183,7 +179,7 @@ public abstract class AbstractDsAssignmentStrategy {
* *
* @param targetsIds to override {@link Action}s * @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 // Figure out if there are potential target/action combinations that
// need to be considered for cancellation // need to be considered for cancellation
final List<JpaAction> activeActions = actionRepository.findAll((root, query, cb) -> { 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.setStatus(Status.CANCELED);
action.setActive(false); action.setActive(false);
@@ -203,10 +199,7 @@ public abstract class AbstractDsAssignmentStrategy {
actionStatusRepository.save(new JpaActionStatus(action, Status.CANCELED, System.currentTimeMillis(), actionStatusRepository.save(new JpaActionStatus(action, Status.CANCELED, System.currentTimeMillis(),
RepositoryConstants.SERVER_MESSAGE_PREFIX + "close obsolete action due to new update")); RepositoryConstants.SERVER_MESSAGE_PREFIX + "close obsolete action due to new update"));
actionRepository.save(action); 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); 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); abstract void sendTargetUpdatedEvents(final DistributionSet set, final List<JpaTarget> targets);
/** /**
@@ -260,9 +249,8 @@ public abstract class AbstractDsAssignmentStrategy {
* such actions existed. * such actions existed.
* *
* @param targetIds to cancel actions for * @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. * 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 closeActiveActions(List<List<Long>> targetIds);
abstract void sendDeploymentEvents(final DistributionSetAssignmentResult assignmentResult);
abstract void sendDeploymentEvents(final List<DistributionSetAssignmentResult> assignmentResults); abstract void sendDeploymentEvents(final List<DistributionSetAssignmentResult> assignmentResults);
private static String getActionMessage(final Action action) { private static String getActionMessage(final Action action) {
@@ -297,12 +283,12 @@ public abstract class AbstractDsAssignmentStrategy {
.publishEvent(new CancelTargetAssignmentEvent(tenant, actions))); .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(); final int quota = quotaManagement.getMaxActionsPerTarget();
try { try {
QuotaHelper.assertAssignmentQuota(target.getId(), requested, quota, Action.class, Target.class, actionRepository::countByTargetId); QuotaHelper.assertAssignmentQuota(target.getId(), 1, quota, Action.class, Target.class, actionRepository::countByTargetId);
} catch (AssignmentQuotaExceededException ex) { } catch (AssignmentQuotaExceededException e) {
maxAssignmentExceededHandler.accept(target.getId(), requested, ex); 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.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -37,8 +38,6 @@ import jakarta.persistence.criteria.Root;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.ListUtils; 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.ActionFields;
import org.eclipse.hawkbit.repository.DeploymentManagement; import org.eclipse.hawkbit.repository.DeploymentManagement;
import org.eclipse.hawkbit.repository.QuotaManagement; import org.eclipse.hawkbit.repository.QuotaManagement;
@@ -162,6 +161,11 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl
this.targetRepository = targetRepository; this.targetRepository = targetRepository;
this.auditorProvider = auditorProvider; this.auditorProvider = auditorProvider;
this.txManager = txManager; this.txManager = txManager;
final Consumer<MaxAssignmentsExceededInfo> maxAssignmentsExceededHandler = maxAssignmentsExceededInfo ->
handleMaxAssignmentsExceeded(
maxAssignmentsExceededInfo.targetId,
maxAssignmentsExceededInfo.requested,
maxAssignmentsExceededInfo.quotaExceededException);
onlineDsAssignmentStrategy = new OnlineDsAssignmentStrategy(targetRepository, afterCommit, actionRepository, actionStatusRepository, onlineDsAssignmentStrategy = new OnlineDsAssignmentStrategy(targetRepository, afterCommit, actionRepository, actionStatusRepository,
quotaManagement, this::isMultiAssignmentsEnabled, this::isConfirmationFlowEnabled, repositoryProperties, quotaManagement, this::isMultiAssignmentsEnabled, this::isConfirmationFlowEnabled, repositoryProperties,
maxAssignmentsExceededHandler); maxAssignmentsExceededHandler);
@@ -409,14 +413,6 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl
@Transactional @Transactional
public void deleteAction(final long actionId) { public void deleteAction(final long actionId) {
log.info("Deleting action {}", 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); actionRepository.deleteById(actionId);
} }
@@ -424,56 +420,41 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl
@Transactional @Transactional
public void deleteActionsByRsql(final String rsql) { public void deleteActionsByRsql(final String rsql) {
log.info("Deleting actions matching rsql {}", rsql); log.info("Deleting actions matching rsql {}", rsql);
final Specification<JpaAction> specification = QLSupport.getInstance().buildSpec(rsql, ActionFields.class); actionRepository.delete(QLSupport.getInstance().buildSpec(rsql, ActionFields.class));
actionRepository.delete(
actionRepository.getAccessController()
.map(accessController -> accessController.appendAccessRules(AccessController.Operation.UPDATE, specification))
.orElse(specification));
} }
@Override @Override
@Transactional @Transactional
public void deleteActionsByIds(final List<Long> actionIds) { public void deleteActionsByIds(final List<Long> actionIds) {
log.info("Deleting actions with ids {}", actionIds); log.info("Deleting actions with ids {}", actionIds);
actionRepository.getAccessController().ifPresent(accessController -> actionRepository.deleteAllById(actionIds);
actionRepository.findAll(
accessController.appendAccessRules(AccessController.Operation.UPDATE, ActionSpecifications.byIdIn(actionIds))));
actionRepository.deleteByIdIn(actionIds);
} }
@Override @Override
@Transactional @Transactional
public void deleteTargetActionsByIds(final String target, final List<Long> actionsIds) { public void deleteTargetActionsByIds(final String controllerId, final List<Long> actionsIds) {
log.info("Delete actions for target {} with action ids {}", target, actionsIds); log.info("Delete actions for target {} with action ids {}", controllerId, actionsIds);
targetRepository.getAccessController() actionRepository.delete(ActionSpecifications.byControllerIdAndIdIn(controllerId, actionsIds));
.ifPresent(accessController ->
accessController.assertOperationAllowed(AccessController.Operation.UPDATE, targetRepository.getByControllerId(target)));
actionRepository.delete(ActionSpecifications.byControllerIdAndIdIn(target, actionsIds));
} }
@Override @Override
@Transactional @Transactional
public void deleteOldestTargetActions(final String target, final int keepLast) { public void deleteOldestTargetActions(final String controllerId, final int keepLast) {
final JpaTarget jpaTarget = targetRepository.findByControllerId(target) final JpaTarget target = targetRepository.findByControllerId(controllerId).orElseThrow(EntityNotFoundException::new);
.orElseThrow(EntityNotFoundException::new); // check access to target since deletion will be executed via native query
targetRepository.getAccessController().ifPresent(accessController -> targetRepository.getAccessController().ifPresent(accessController ->
accessController.assertOperationAllowed(AccessController.Operation.UPDATE, jpaTarget)); accessController.assertOperationAllowed(AccessController.Operation.UPDATE, target));
final long targetActions = actionRepository.countByTargetId(target.getId());
final long targetActions = actionRepository.countByTargetId(jpaTarget.getId());
long oldestToDelete;
if (targetActions > keepLast) { if (targetActions > keepLast) {
oldestToDelete = targetActions - keepLast; final long oldestToDelete = targetActions - keepLast;
} else { deleteOldestTargetActions(target.getId(), (int) oldestToDelete);
return;
} }
deleteOldestTargetActions(jpaTarget.getId(), (int) oldestToDelete);
} }
@Override @Override
@Transactional(isolation = Isolation.READ_COMMITTED) @Transactional(isolation = Isolation.READ_COMMITTED)
@Retryable(retryFor = { @Retryable(retryFor = { ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX,
ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY)) backoff = @Backoff(delay = Constants.TX_RT_DELAY))
public void cancelInactiveScheduledActionsForTargets(final List<Long> targetIds) { public void cancelInactiveScheduledActionsForTargets(final List<Long> targetIds) {
if (!isMultiAssignmentsEnabled()) { if (!isMultiAssignmentsEnabled()) {
targetRepository.getAccessController().ifPresent(v -> { targetRepository.getAccessController().ifPresent(v -> {
@@ -598,12 +579,11 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl
} }
} }
@Override public record MaxAssignmentsExceededInfo(long targetId, long requested, AssignmentQuotaExceededException quotaExceededException) {}
public void handleMaxAssignmentsExceeded(final Long targetId, final Long requested, final AssignmentQuotaExceededException ex) {
maxAssignmentsExceededHandler.accept(targetId, requested, ex);
}
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 actionsPurgePercentage = getActionsPurgePercentage();
int quota = quotaManagement.getMaxActionsPerTarget(); int quota = quotaManagement.getMaxActionsPerTarget();
if (actionsPurgePercentage > 0 && actionsPurgePercentage < 100) { if (actionsPurgePercentage > 0 && actionsPurgePercentage < 100) {
@@ -622,7 +602,9 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl
} else { } else {
throw quotaExceededException; throw quotaExceededException;
} }
}; }
;
/** /**
* Deletes the first n target actions of a target * 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 targetId - target id
* @param oldestToDelete - number of oldest actions to be deleted * @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 // Workaround for the case where JPQL or Criteria API do not support LIMIT
log.info("Deleting last {} actions of target {}", oldestToDelete, targetId); log.info("Deleting last {} actions of target {}", oldestToDelete, targetId);
final String SQL = "DELETE FROM sp_action WHERE id IN(" + final String SQL = "DELETE FROM sp_action WHERE id IN(" +
@@ -641,7 +623,7 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl
" LIMIT " + oldestToDelete " LIMIT " + oldestToDelete
+ ") AS sub" + ") AS sub"
+ ")"; + ")";
Query query = entityManager.createNativeQuery(SQL); final Query query = entityManager.createNativeQuery(SQL);
query.setParameter("target", targetId); query.setParameter("target", targetId);
query.executeUpdate(); query.executeUpdate();
} }
@@ -912,7 +894,7 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl
} catch (final AssignmentQuotaExceededException ex) { } catch (final AssignmentQuotaExceededException ex) {
targetRepository.findByControllerId(controllerId).ifPresentOrElse( targetRepository.findByControllerId(controllerId).ifPresentOrElse(
// assume requested are always smaller than int size // 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); throw new EntityNotFoundException(Target.class, controllerId);
}); });

View File

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

View File

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

View File

@@ -320,15 +320,4 @@ public interface ActionRepository extends BaseEntityRepository<JpaAction> {
@Transactional @Transactional
@Query("UPDATE JpaAction a SET a.externalRef = :externalRef WHERE a.id = :actionId") @Query("UPDATE JpaAction a SET a.externalRef = :externalRef WHERE a.id = :actionId")
void updateExternalRef(@Param("actionId") Long actionId, @Param("externalRef") String externalRef); 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)); 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) { public static Specification<JpaAction> byTargetControllerId(final String controllerId) {
return (root, query, cb) -> cb.equal(root.get(JpaAction_.target).get(JpaTarget_.controllerId), 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. * {@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} * @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) -> { 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); targetRoot.fetch(JpaTarget_.tags, JoinType.LEFT);
query.distinct(true); query.distinct(true);
return predicate; return predicate;

View File

@@ -10,6 +10,8 @@
package org.eclipse.hawkbit.repository.jpa.acm; package org.eclipse.hawkbit.repository.jpa.acm;
import static org.assertj.core.api.Assertions.assertThat; 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.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_DISTRIBUTION_SET;
import static org.eclipse.hawkbit.im.authentication.SpPermission.READ_TARGET; import static org.eclipse.hawkbit.im.authentication.SpPermission.READ_TARGET;
@@ -56,17 +58,17 @@ class DeploymentManagementTest extends AbstractAccessControllerManagementTest {
void verifyActionVisibility() { void verifyActionVisibility() {
final String controllerId = target1Type1.getControllerId(); final String controllerId = target1Type1.getControllerId();
verify( verify(
assignedId -> { actionId -> {
assertThat(deploymentManagement.findActionsAll(UNPAGED)).isEmpty(); assertThat(deploymentManagement.findActionsAll(UNPAGED)).isEmpty();
assertThat(deploymentManagement.findAction(assignedId)).isEmpty(); assertThat(deploymentManagement.findAction(actionId)).isEmpty();
assertThatThrownBy(() -> deploymentManagement.findActionWithDetails(assignedId)) assertThatThrownBy(() -> deploymentManagement.findActionWithDetails(actionId))
.isInstanceOf(InsufficientPermissionException.class); .isInstanceOf(InsufficientPermissionException.class);
assertThat(deploymentManagement.findActions("id==*", UNPAGED)).isEmpty(); assertThat(deploymentManagement.findActions("id==*", UNPAGED)).isEmpty();
assertThatThrownBy(() -> deploymentManagement.findActionsByTarget(controllerId, UNPAGED)) assertThatThrownBy(() -> deploymentManagement.findActionsByTarget(controllerId, UNPAGED))
.isInstanceOf(EntityNotFoundException.class); .isInstanceOf(EntityNotFoundException.class);
assertThatThrownBy(() -> deploymentManagement.findActionsByTarget("id==*", controllerId, UNPAGED)) assertThatThrownBy(() -> deploymentManagement.findActionsByTarget("id==*", controllerId, UNPAGED))
.isInstanceOf(EntityNotFoundException.class); .isInstanceOf(EntityNotFoundException.class);
assertThatThrownBy(() -> deploymentManagement.findActionStatusByAction(assignedId, UNPAGED)) assertThatThrownBy(() -> deploymentManagement.findActionStatusByAction(actionId, UNPAGED))
.isInstanceOf(EntityNotFoundException.class); .isInstanceOf(EntityNotFoundException.class);
assertThatThrownBy(() -> deploymentManagement.findActiveActionsByTarget(controllerId, UNPAGED)) assertThatThrownBy(() -> deploymentManagement.findActiveActionsByTarget(controllerId, UNPAGED))
.isInstanceOf(EntityNotFoundException.class); .isInstanceOf(EntityNotFoundException.class);
@@ -76,16 +78,16 @@ class DeploymentManagementTest extends AbstractAccessControllerManagementTest {
assertThatThrownBy(() -> deploymentManagement.hasPendingCancellations(targetId)).isInstanceOf( assertThatThrownBy(() -> deploymentManagement.hasPendingCancellations(targetId)).isInstanceOf(
EntityNotFoundException.class); EntityNotFoundException.class);
}, },
assignedId -> { actionId -> {
assertThat(deploymentManagement.findActionsAll(UNPAGED)).hasSize(1).allMatch(this::isActionOfTarget1Type1); assertThat(deploymentManagement.findActionsAll(UNPAGED)).hasSize(1).allMatch(this::isActionOfTarget1Type1);
assertThat(deploymentManagement.findAction(assignedId)).hasValueSatisfying(this::assertActionOfTarget1Type1); assertThat(deploymentManagement.findAction(actionId)).hasValueSatisfying(this::assertActionOfTarget1Type1);
assertThat(deploymentManagement.findActionWithDetails(assignedId)).hasValueSatisfying(this::assertActionOfTarget1Type1); assertThat(deploymentManagement.findActionWithDetails(actionId)).hasValueSatisfying(this::assertActionOfTarget1Type1);
assertThat(deploymentManagement.findActions("id==*", UNPAGED)).hasSize(1).allMatch(this::isActionOfTarget1Type1); assertThat(deploymentManagement.findActions("id==*", UNPAGED)).hasSize(1).allMatch(this::isActionOfTarget1Type1);
assertThat(deploymentManagement.findActionsByTarget(controllerId, UNPAGED)).hasSize(1) assertThat(deploymentManagement.findActionsByTarget(controllerId, UNPAGED)).hasSize(1)
.allMatch(this::isActionOfTarget1Type1); .allMatch(this::isActionOfTarget1Type1);
assertThat(deploymentManagement.findActionsByTarget("id==*", controllerId, UNPAGED)) assertThat(deploymentManagement.findActionsByTarget("id==*", controllerId, UNPAGED))
.hasSize(1).allMatch(this::isActionOfTarget1Type1); .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)); .hasSize(1).allMatch(actionStatus -> actionStatus.getStatus().equals(Action.Status.RUNNING));
assertThat(deploymentManagement.findActiveActionsByTarget(controllerId, UNPAGED)) assertThat(deploymentManagement.findActiveActionsByTarget(controllerId, UNPAGED))
.hasSize(1).allMatch(this::isActionOfTarget1Type1); .hasSize(1).allMatch(this::isActionOfTarget1Type1);
@@ -96,24 +98,83 @@ class DeploymentManagementTest extends AbstractAccessControllerManagementTest {
null); 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 @Test
void verifyCancellation() { void verifyCancellation() {
verify( verify(
assignedId -> assertThatThrownBy(() -> deploymentManagement.cancelAction(assignedId)) actionId -> assertThatThrownBy(() -> deploymentManagement.cancelAction(actionId))
.isInstanceOf(EntityNotFoundException.class), .isInstanceOf(EntityNotFoundException.class),
assignedId -> assertThatThrownBy(() -> deploymentManagement.cancelAction(assignedId)) actionId -> assertThatThrownBy(() -> deploymentManagement.cancelAction(actionId))
.isInstanceOf(InsufficientPermissionException.class), .isInstanceOf(InsufficientPermissionException.class),
assignedId -> assertThat(deploymentManagement.cancelAction(assignedId).getId()).isEqualTo(assignedId)); actionId -> assertThat(deploymentManagement.cancelAction(actionId).getId()).isEqualTo(actionId));
} }
@Test @Test
void verifyCancellationByDistributionSetId() { void verifyCancellationByDistributionSetId() {
verify( verify(
assignedId -> { actionId -> {
deploymentManagement.cancelActionsForDistributionSet(ActionCancellationType.FORCE, ds1Type1); 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)), .hasValueSatisfying(action -> assertThat(action.getStatus()).isEqualTo(Action.Status.RUNNING)),
null); null);
} }
@@ -121,37 +182,37 @@ class DeploymentManagementTest extends AbstractAccessControllerManagementTest {
@Test @Test
void verifyForceActionIsNotAllowed() { void verifyForceActionIsNotAllowed() {
verify( verify(
assignedId -> assertThatThrownBy(() -> deploymentManagement.forceTargetAction(assignedId)) actionId -> assertThatThrownBy(() -> deploymentManagement.forceTargetAction(actionId))
.isInstanceOf(EntityNotFoundException.class), .isInstanceOf(EntityNotFoundException.class),
assignedId -> assertThatThrownBy(() -> deploymentManagement.forceTargetAction(assignedId)) actionId -> assertThatThrownBy(() -> deploymentManagement.forceTargetAction(actionId))
.isInstanceOf(InsufficientPermissionException.class), .isInstanceOf(InsufficientPermissionException.class),
assignedId -> assertThat(deploymentManagement.forceTargetAction(assignedId).getActionType()) actionId -> assertThat(deploymentManagement.forceTargetAction(actionId).getActionType())
.isEqualTo(Action.ActionType.FORCED)); .isEqualTo(Action.ActionType.FORCED));
} }
private void verify(final Consumer<Long> noRead, final Consumer<Long> noUpdate, final Consumer<Long> readAndUpdate) { private void verify(final Consumer<Long> noRead, final Consumer<Long> readNoUpdate, final Consumer<Long> readAndUpdate) {
final Long assignedId = systemSecurityContext.runAsSystem(() -> { final Long actionId = systemSecurityContext.runAsSystem(() -> {
final List<Action> assignedEntity = assignDistributionSet(ds1Type1.getId(), target1Type1.getControllerId()).getAssignedEntity(); final List<Action> actions = assignDistributionSet(ds1Type1.getId(), target1Type1.getControllerId()).getAssignedEntity();
assertThat(assignedEntity).hasSize(1).allMatch(action -> action.getTarget().getId().equals(target1Type1.getId())); assertThat(actions).hasSize(1).allMatch(action -> action.getTarget().getId().equals(target1Type1.getId()));
return assignedEntity.get(0); return actions.get(0);
}).getId(); }).getId();
if (noRead != null) { if (noRead != null) {
// no read permission // no read permission
runAs(withAuthorities(READ_TARGET + "/type.id==" + targetType2.getId(), UPDATE_TARGET + "/type.id==" + targetType2.getId()), 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 // read but no update permission
runAs(withAuthorities(READ_TARGET + "/type.id==" + targetType1.getId(), UPDATE_TARGET + "/type.id==" + targetType2.getId()), runAs(withAuthorities(READ_TARGET + "/type.id==" + targetType1.getId(), UPDATE_TARGET + "/type.id==" + targetType2.getId()),
() -> noUpdate.accept(assignedId)); () -> readNoUpdate.accept(actionId));
} }
if (readAndUpdate != null) { if (readAndUpdate != null) {
// read and update permissions // read and update permissions
runAs(withAuthorities(READ_TARGET + "/type.id==" + targetType1.getId(), UPDATE_TARGET + "/type.id==" + targetType1.getId()), 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.assertj.core.api.Assertions.assertThat;
import static org.eclipse.hawkbit.im.authentication.SpPermission.CREATE_PREFIX; 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_PREFIX;
import static org.eclipse.hawkbit.im.authentication.SpPermission.DELETE_TARGET; import static org.eclipse.hawkbit.im.authentication.SpPermission.DELETE_TARGET;
import static org.eclipse.hawkbit.im.authentication.SpPermission.DISTRIBUTION_SET; 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.SOFTWARE_MODULE_TYPE;
import static org.eclipse.hawkbit.im.authentication.SpPermission.TARGET_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_PREFIX;
import static org.eclipse.hawkbit.im.authentication.SpPermission.UPDATE_TARGET;
import static org.eclipse.hawkbit.repository.test.util.SecurityContextSwitch.runAs; import static org.eclipse.hawkbit.repository.test.util.SecurityContextSwitch.runAs;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
@@ -92,17 +92,22 @@ class SystemExecutionTest extends AbstractAccessControllerManagementTest {
() -> verifyAccessController(distributionSetTypeAccessController)); () -> verifyAccessController(distributionSetTypeAccessController));
runAs(withAuthorities(READ_TARGET, DELETE_TARGET + "/type.id==1"), runAs(withAuthorities(READ_TARGET, DELETE_TARGET + "/type.id==1"),
() -> verifyAccessController(targetAccessController)); () -> verifyAccessController(targetAccessController));
runAs(withAuthorities(UPDATE_PREFIX + TARGET_TYPE + "/id==1"), () -> verifyAccessController(targetTypeAccessController)); runAs(withAuthorities(CREATE_PREFIX + TARGET_TYPE + "/id==1"), () -> verifyAccessController(targetTypeAccessController));
runAs(withAuthorities(CREATE_TARGET + "/type.id==1"), () -> verifyAccessController(actionAccessController)); // 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) { 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); final Specification mock = mock(Specification.class);
for (final Operation operation : Operation.values()) { for (final Operation operation : Operation.values()) {
accessController.appendAccessRules(operation, mock); 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); final Specification mockAsSystem = mock(Specification.class);
for (Operation operation : Operation.values()) { for (Operation operation : Operation.values()) {