Add DeploymentManagement ACM test (#2726)

Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
This commit is contained in:
Avgustin Marinov
2025-10-08 11:07:15 +03:00
committed by GitHub
parent cc36ca8801
commit e23d2aa920
6 changed files with 212 additions and 56 deletions

View File

@@ -50,8 +50,7 @@ import org.springframework.security.access.prepost.PreAuthorize;
*/
public interface DeploymentManagement extends PermissionSupport {
String HAS_UPDATE_TARGET_AND_READ_DISTRIBUTION_SET =
SpringEvalExpressions.HAS_UPDATE_REPOSITORY + " and hasAuthority('READ_" + SpPermission.DISTRIBUTION_SET + "')";
String HAS_UPDATE_TARGET_AND_READ_DISTRIBUTION_SET = SpringEvalExpressions.HAS_UPDATE_REPOSITORY + " and hasAuthority('READ_" + SpPermission.DISTRIBUTION_SET + "')";
@Override
default String permissionGroup() {
@@ -64,12 +63,12 @@ public interface DeploymentManagement extends PermissionSupport {
* @param deploymentRequests information about all target-ds-assignments that shall be made
* @return the list of assignment results
* @throws IncompleteDistributionSetException if mandatory {@link SoftwareModuleType} are not assigned as
* defined by the {@link DistributionSetType}.
* defined by the {@link DistributionSetType}.
* @throws EntityNotFoundException if either provided {@link DistributionSet} or {@link Target}s do not exist
* @throws AssignmentQuotaExceededException if the maximum number of targets the distribution set can be
* assigned to at once is exceeded
* assigned to at once is exceeded
* @throws MultiAssignmentIsNotEnabledException if the request results in multiple assignments to the same
* target and multi-assignment is disabled
* target and multi-assignment is disabled
*/
@PreAuthorize(HAS_UPDATE_TARGET_AND_READ_DISTRIBUTION_SET)
List<DistributionSetAssignmentResult> assignDistributionSets(@Valid @NotEmpty List<DeploymentRequest> deploymentRequests);
@@ -82,12 +81,12 @@ public interface DeploymentManagement extends PermissionSupport {
* @param actionMessage an optional message for the action status
* @return the list of assignment results
* @throws IncompleteDistributionSetException if mandatory {@link SoftwareModuleType} are not assigned as
* defined by the {@link DistributionSetType}.
* defined by the {@link DistributionSetType}.
* @throws EntityNotFoundException if either provided {@link DistributionSet} or {@link Target}s do not exist
* @throws AssignmentQuotaExceededException if the maximum number of targets the distribution set can be
* assigned to at once is exceeded
* assigned to at once is exceeded
* @throws MultiAssignmentIsNotEnabledException if the request results in multiple assignments to the same
* target and multi-assignment is disabled
* target and multi-assignment is disabled
*/
@PreAuthorize(HAS_UPDATE_TARGET_AND_READ_DISTRIBUTION_SET)
List<DistributionSetAssignmentResult> assignDistributionSets(
@@ -109,11 +108,11 @@ public interface DeploymentManagement extends PermissionSupport {
* @param assignments target IDs with the respective distribution set ID which they are supposed to be assigned to
* @return the assignment results
* @throws IncompleteDistributionSetException if mandatory {@link SoftwareModuleType} are not assigned as
* defined by the {@link DistributionSetType}.
* defined by the {@link DistributionSetType}.
* @throws EntityNotFoundException if either provided {@link DistributionSet} or {@link Target}s do not exist
* @throws AssignmentQuotaExceededException if the maximum number of targets the distribution set can be assigned to at once is exceeded
* @throws MultiAssignmentIsNotEnabledException if the request results in multiple assignments to the same
* target and multi-assignment is disabled
* target and multi-assignment is disabled
*/
@PreAuthorize(HAS_UPDATE_TARGET_AND_READ_DISTRIBUTION_SET)
List<DistributionSetAssignmentResult> offlineAssignedDistributionSets(String initiatedBy, Collection<Entry<String, Long>> assignments);
@@ -140,7 +139,7 @@ public interface DeploymentManagement extends PermissionSupport {
* @param controllerId the target associated to the actions to count
* @return the count value of found actions associated to the target
* @throws RSQLParameterUnsupportedFieldException if a field in the RSQL string is used but not provided by the
* given {@code fieldNameProvider}
* given {@code fieldNameProvider}
* @throws RSQLParameterSyntaxException if the RSQL syntax is wrong
* @throws EntityNotFoundException if target with given ID does not exist
*/
@@ -148,8 +147,7 @@ public interface DeploymentManagement extends PermissionSupport {
long countActionsByTarget(@NotNull String rsql, @NotEmpty String controllerId);
/**
* Returns total count of all actions<p/>
* No access control applied.
* Returns total count of all actions
*
* @return the total amount of stored actions
*/
@@ -157,8 +155,7 @@ public interface DeploymentManagement extends PermissionSupport {
long countActionsAll();
/**
* Counts the actions which match the given query.<p/>
* No access control applied.
* Counts the actions which match the given query.
*
* @param rsql RSQL query.
* @return the total number of actions matching the given RSQL query.
@@ -187,8 +184,6 @@ public interface DeploymentManagement extends PermissionSupport {
/**
* Retrieves all {@link Action}s from repository.
* <p/>
* No access control applied.
*
* @param pageable pagination parameter
* @return a paged list of {@link Action}s
@@ -197,8 +192,7 @@ public interface DeploymentManagement extends PermissionSupport {
Slice<Action> findActionsAll(@NotNull Pageable pageable);
/**
* Retrieves all {@link Action} entities which match the given RSQL query.<p/>
* No access control applied.
* Retrieves all {@link Action} entities which match the given RSQL query.
*
* @param rsql RSQL query string
* @param pageable the page request parameter for paging and sorting the result
@@ -215,7 +209,7 @@ public interface DeploymentManagement extends PermissionSupport {
* @param pageable the page request
* @return a slice of actions assigned to the specific target and the specification
* @throws RSQLParameterUnsupportedFieldException if a field in the RSQL string is used but not provided by the
* given {@code fieldNameProvider}
* given {@code fieldNameProvider}
* @throws RSQLParameterSyntaxException if the RSQL syntax is wrong
*/
@PreAuthorize(SpringEvalExpressions.HAS_READ_REPOSITORY)
@@ -244,7 +238,6 @@ public interface DeploymentManagement extends PermissionSupport {
/**
* Retrieves all messages for an {@link ActionStatus}.<p/>
* No entity based access control applied.
*
* @param actionStatusId the id of {@link ActionStatus} to retrieve the messages from
* @param pageable the page request parameter for paging and sorting the result
@@ -273,17 +266,6 @@ public interface DeploymentManagement extends PermissionSupport {
@PreAuthorize(SpringEvalExpressions.HAS_READ_REPOSITORY)
Page<Action> findActiveActionsByTarget(@NotEmpty String controllerId, @NotNull Pageable pageable);
/**
* Retrieves all inactive {@link Action}s of a specific target.
*
* @param controllerId the target associated with the actions
* @param pageable the page request parameter for paging and sorting the result
* @return a list of actions associated with the given target
* @throws EntityNotFoundException if target with given ID does not exist
*/
@PreAuthorize(SpringEvalExpressions.HAS_READ_REPOSITORY)
Page<Action> findInActiveActionsByTarget(@NotEmpty String controllerId, @NotNull Pageable pageable);
/**
* Retrieves active {@link Action}s with highest weight that are assigned to a {@link Target}.
*
@@ -325,8 +307,7 @@ public interface DeploymentManagement extends PermissionSupport {
void cancelInactiveScheduledActionsForTargets(List<Long> targetIds);
/**
* Starts all scheduled actions of an RolloutGroup parent.<p/>
* No entity based access control applied.
* Starts all scheduled actions of an RolloutGroup parent.
*
* @param rolloutId the rollout the actions belong to
* @param distributionSetId to assign
@@ -365,8 +346,7 @@ public interface DeploymentManagement extends PermissionSupport {
/**
* Deletes actions which match one of the given action status and which have not been modified since the given (absolute) time-stamp.
* Used for obsolete actions cleanup.<p/>
* No entity based access control applied.
* Used for obsolete actions cleanup.
*
* @param status Set of action status.
* @param lastModified A time-stamp in milliseconds.

View File

@@ -29,6 +29,7 @@ import java.util.Objects;
import java.util.stream.Collectors;
import jakarta.annotation.Nonnull;
import jakarta.persistence.PersistenceException;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Expression;
@@ -378,19 +379,41 @@ public class SpecificationBuilder<T> {
}
}
@SuppressWarnings("java:S1872") // java:S1872 - sometimes class could be unavailable at runtime
private Predicate like(final Path<String> fieldPath, final String sqlValue) {
if (caseWise(fieldPath)) {
return cb.like(cb.upper(fieldPath), sqlValue.toUpperCase(), ESCAPE_CHAR);
} else {
return cb.like(fieldPath, sqlValue, ESCAPE_CHAR);
try {
if (caseWise(fieldPath)) {
return cb.like(cb.upper(fieldPath), sqlValue.toUpperCase(), ESCAPE_CHAR);
} else {
return cb.like(fieldPath, sqlValue, ESCAPE_CHAR);
}
} catch (final PersistenceException e) {
if ("%".equals(sqlValue) && fieldPath.getJavaType() != String.class &&
"org.hibernate.type.descriptor.java.CoercionException".equals(e.getClass().getName())) {
// hibernate throws an exception if we try to do == on non-string field with wildcard only
return fieldPath.isNotNull();
} else {
throw e;
}
}
}
@SuppressWarnings("java:S1872") // java:S1872 - sometimes class could be unavailable at runtime
private Predicate notLike(final Path<String> fieldPath, final String sqlValue) {
if (caseWise(fieldPath)) {
return cb.notLike(cb.upper(fieldPath), sqlValue.toUpperCase(), ESCAPE_CHAR);
} else {
return cb.notLike(fieldPath, sqlValue, ESCAPE_CHAR);
try {
if (caseWise(fieldPath)) {
return cb.notLike(cb.upper(fieldPath), sqlValue.toUpperCase(), ESCAPE_CHAR);
} else {
return cb.notLike(fieldPath, sqlValue, ESCAPE_CHAR);
}
} catch (final PersistenceException e) {
if ("%".equals(sqlValue) && fieldPath.getJavaType() != String.class &&
"org.hibernate.type.descriptor.java.CoercionException".equals(e.getClass().getName())) {
// hibernate throws an exception if we try to do == on non-string field with wildcard only
return fieldPath.isNull();
} else {
throw e;
}
}
}

View File

@@ -347,13 +347,6 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl
.map(Action.class::cast);
}
@Override
public Page<Action> findInActiveActionsByTarget(final String controllerId, final Pageable pageable) {
assertTargetReadAllowed(controllerId);
return actionRepository.findAll(ActionSpecifications.byTargetControllerIdAndActive(controllerId, false), pageable)
.map(Action.class::cast);
}
@Override
public List<Action> findActiveActionsWithHighestWeight(final String controllerId, final int maxActionCount) {
assertTargetReadAllowed(controllerId);

View File

@@ -0,0 +1,165 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.jpa.acm;
import static org.assertj.core.api.Assertions.assertThat;
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;
import static org.eclipse.hawkbit.im.authentication.SpPermission.UPDATE_TARGET;
import static org.eclipse.hawkbit.repository.test.util.SecurityContextSwitch.runAs;
import java.util.List;
import java.util.function.Consumer;
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
import org.eclipse.hawkbit.repository.exception.InsufficientPermissionException;
import org.eclipse.hawkbit.repository.model.Action;
import org.eclipse.hawkbit.repository.model.ActionCancellationType;
import org.eclipse.hawkbit.repository.model.DeploymentRequest;
import org.eclipse.hawkbit.repository.model.Target;
import org.junit.jupiter.api.Test;
class DeploymentManagementTest extends AbstractAccessControllerTest {
@Test
void verifyAssignments() {
runAs(withAuthorities(
READ_TARGET,
UPDATE_TARGET + "/type.id==" + targetType1.getId(), // has update permission for type1
READ_DISTRIBUTION_SET),
() -> assertThat(deploymentManagement.assignDistributionSets(List.of(new DeploymentRequest(
target1Type1.getControllerId(), ds1Type1.getId(), Action.ActionType.FORCED, 0,
null, null, null, null, false)))
.get(0).getAssignedEntity().stream().map(Action::getTarget)
.map(Target::getId))
.hasSize(1)
.containsExactly(target1Type1.getId()));
runAs(withAuthorities(
READ_TARGET,
UPDATE_TARGET + "/type.id==" + targetType2.getId(), // has no update permission for type1
READ_DISTRIBUTION_SET),
() -> assertThat(deploymentManagement.assignDistributionSets(List.of(new DeploymentRequest(
target1Type1.getControllerId(), ds1Type1.getId(), Action.ActionType.FORCED, 0,
null, null, null, null, false)))).isEmpty());
}
@Test
void verifyActionVisibility() {
final String controllerId = target1Type1.getControllerId();
verify(
assignedId -> {
assertThat(deploymentManagement.findActionsAll(UNPAGED)).isEmpty();
assertThat(deploymentManagement.findAction(assignedId)).isEmpty();
assertThatThrownBy(() -> deploymentManagement.findActionWithDetails(assignedId))
.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))
.isInstanceOf(EntityNotFoundException.class);
assertThatThrownBy(() -> deploymentManagement.findActiveActionsByTarget(controllerId, UNPAGED))
.isInstanceOf(EntityNotFoundException.class);
assertThatThrownBy(() -> deploymentManagement.findActiveActionsWithHighestWeight(controllerId, 99))
.isInstanceOf(EntityNotFoundException.class);
final Long targetId = target1Type1.getId();
assertThatThrownBy(() -> deploymentManagement.hasPendingCancellations(targetId)).isInstanceOf(
EntityNotFoundException.class);
},
assignedId -> {
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.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))
.hasSize(1).allMatch(actionStatus -> actionStatus.getStatus().equals(Action.Status.RUNNING));
assertThat(deploymentManagement.findActiveActionsByTarget(controllerId, UNPAGED))
.hasSize(1).allMatch(this::isActionOfTarget1Type1);
assertThat(deploymentManagement.findActiveActionsWithHighestWeight(controllerId, 99))
.hasSize(1).allMatch(this::isActionOfTarget1Type1);
assertThat(deploymentManagement.hasPendingCancellations(target1Type1.getId())).isFalse();
},
null);
}
@Test
void verifyCancellation() {
verify(
assignedId -> assertThatThrownBy(() -> deploymentManagement.cancelAction(assignedId))
.isInstanceOf(EntityNotFoundException.class),
assignedId -> assertThatThrownBy(() -> deploymentManagement.cancelAction(assignedId))
.isInstanceOf(InsufficientPermissionException.class),
assignedId -> assertThat(deploymentManagement.cancelAction(assignedId).getId()).isEqualTo(assignedId));
}
@Test
void verifyCancellationByDistributionSetId() {
verify(
assignedId -> {
deploymentManagement.cancelActionsForDistributionSet(ActionCancellationType.FORCE, ds1Type1);
assertThat(deploymentManagement.findAction(assignedId)).isEmpty();
},
assignedId -> assertThat(deploymentManagement.findAction(assignedId))
.hasValueSatisfying(action -> assertThat(action.getStatus()).isEqualTo(Action.Status.RUNNING)),
null);
}
@Test
void verifyForceActionIsNotAllowed() {
verify(
assignedId -> assertThatThrownBy(() -> deploymentManagement.forceTargetAction(assignedId))
.isInstanceOf(EntityNotFoundException.class),
assignedId -> assertThatThrownBy(() -> deploymentManagement.forceTargetAction(assignedId))
.isInstanceOf(InsufficientPermissionException.class),
assignedId -> assertThat(deploymentManagement.forceTargetAction(assignedId).getActionType())
.isEqualTo(Action.ActionType.FORCED));
}
private void verify(final Consumer<Long> noRead, final Consumer<Long> noUpdate, final Consumer<Long> readAndUpdate) {
final Long assignedId = systemSecurityContext.runAsSystem(() -> {
final List<Action> assignedEntity = assignDistributionSet(ds1Type1.getId(), target1Type1.getControllerId()).getAssignedEntity();
assertThat(assignedEntity).hasSize(1).allMatch(action -> action.getTarget().getId().equals(target1Type1.getId()));
return assignedEntity.get(0);
}).getId();
if (noRead != null) {
// no read permission
runAs(withAuthorities(READ_TARGET + "/type.id==" + targetType2.getId(), UPDATE_TARGET + "/type.id==" + targetType2.getId()),
() -> noRead.accept(assignedId));
}
if (noUpdate != null) {
// read but no update permission
runAs(withAuthorities(READ_TARGET + "/type.id==" + targetType1.getId(), UPDATE_TARGET + "/type.id==" + targetType2.getId()),
() -> noUpdate.accept(assignedId));
}
if (readAndUpdate != null) {
// read and update permissions
runAs(withAuthorities(READ_TARGET + "/type.id==" + targetType1.getId(), UPDATE_TARGET + "/type.id==" + targetType1.getId()),
() -> readAndUpdate.accept(assignedId));
}
}
private void assertActionOfTarget1Type1(final Action action) {
assertThat(action.getTarget().getId()).isEqualTo(target1Type1.getId());
}
private boolean isActionOfTarget1Type1(final Action action) {
return action.getTarget().getId().equals(target1Type1.getId());
}
}

View File

@@ -161,7 +161,6 @@ class DeploymentManagementTest extends AbstractJpaIntegrationTest {
verifyThrownExceptionBy(() -> deploymentManagement.findActionsByTarget("id==*", NOT_EXIST_ID, PAGE), "Target");
verifyThrownExceptionBy(() -> deploymentManagement.findActiveActionsByTarget(NOT_EXIST_ID, PAGE), "Target");
verifyThrownExceptionBy(() -> deploymentManagement.findInActiveActionsByTarget(NOT_EXIST_ID, PAGE), "Target");
verifyThrownExceptionBy(() -> deploymentManagement.forceQuitAction(NOT_EXIST_IDL), "Action");
verifyThrownExceptionBy(() -> deploymentManagement.forceTargetAction(NOT_EXIST_IDL), "Action");
}
@@ -1443,9 +1442,6 @@ class DeploymentManagementTest extends AbstractJpaIntegrationTest {
targ = targetManagement.getByControllerId(targ.getControllerId());
assertEquals(0, deploymentManagement.findActiveActionsByTarget(targ.getControllerId(), PAGE).getTotalElements(),
"active target actions are wrong");
assertEquals(1,
deploymentManagement.findInActiveActionsByTarget(targ.getControllerId(), PAGE).getTotalElements(),
"active actions are wrong");
assertEquals(TargetUpdateStatus.IN_SYNC, targ.getUpdateStatus(), "tagret update status is not correct");
assertEquals(dsA, deploymentManagement.findAssignedDistributionSet(targ.getControllerId()).get(),