From 94576bd6fef036f3d6283bea9f69f20e4baaf80c Mon Sep 17 00:00:00 2001 From: Avgustin Marinov Date: Thu, 15 Feb 2024 15:56:01 +0200 Subject: [PATCH] [#1580] Software Module & Distribution Set lock: apply (#1648) forbid software modules / artifacts modification for locked distribution sets / software modules respectively Signed-off-by: Marinov Avgustin --- .../hawkbit/exception/SpServerError.java | 4 ++ .../exception/EntityNotFoundException.java | 3 ++ .../repository/exception/LockedException.java | 36 +++++++++++++++++ .../jpa/model/JpaDistributionSet.java | 11 ++++- .../jpa/model/JpaSoftwareModule.java | 9 +++++ .../DistributionSetManagementTest.java | 35 +++++++++++++++- .../SoftwareModuleManagementTest.java | 40 ++++++++++++++++++- 7 files changed, 135 insertions(+), 3 deletions(-) create mode 100644 hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/LockedException.java diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/exception/SpServerError.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/exception/SpServerError.java index 3748475e7..e3ff4a8cf 100644 --- a/hawkbit-core/src/main/java/org/eclipse/hawkbit/exception/SpServerError.java +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/exception/SpServerError.java @@ -205,6 +205,10 @@ public enum SpServerError { */ SP_DS_INCOMPLETE("hawkbit.server.error.distributionset.incomplete", "Distribution set is assigned/locked to a target that is incomplete (i.e. mandatory modules are missing)"), + /** + * + */ + SP_LOCKED("hawkbit.server.error.locked", "Entry is locked. Could not be modified"), /** * diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/EntityNotFoundException.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/EntityNotFoundException.java index 326a25a47..9b0da68f3 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/EntityNotFoundException.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/EntityNotFoundException.java @@ -9,6 +9,7 @@ */ package org.eclipse.hawkbit.repository.exception; +import java.io.Serial; import java.util.Collection; import java.util.stream.Collectors; @@ -23,7 +24,9 @@ import org.eclipse.hawkbit.repository.model.MetaData; */ public class EntityNotFoundException extends AbstractServerRtException { + @Serial private static final long serialVersionUID = 1L; + private static final SpServerError THIS_ERROR = SpServerError.SP_REPO_ENTITY_NOT_EXISTS; /** diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/LockedException.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/LockedException.java new file mode 100644 index 000000000..95c00ff76 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/LockedException.java @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2024 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.exception; + +import org.eclipse.hawkbit.exception.AbstractServerRtException; +import org.eclipse.hawkbit.exception.SpServerError; +import org.eclipse.hawkbit.repository.model.BaseEntity; + +import java.io.Serial; + +/** + * Thrown if assignment quota is exceeded + */ +public class LockedException extends AbstractServerRtException { + + @Serial + private static final long serialVersionUID = 1L; + + private static final String ASSIGNMENT_QUOTA_EXCEEDED_MESSAGE = "Quota exceeded: Cannot assign %s more %s entities to %s '%s'. The maximum is %s."; + private static final SpServerError THIS_ERROR = SpServerError.SP_LOCKED; + + public LockedException( + final Class type, final Object entityId, final String operation) { + super( + type.getSimpleName() + " with given identifier {" + entityId + "} is locked and " + operation + + " is forbidden!", + THIS_ERROR); + } +} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaDistributionSet.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaDistributionSet.java index 11b4af8b8..43a7e72eb 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaDistributionSet.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaDistributionSet.java @@ -44,6 +44,7 @@ import org.eclipse.hawkbit.repository.event.remote.entity.DistributionSetCreated import org.eclipse.hawkbit.repository.event.remote.entity.DistributionSetUpdatedEvent; import org.eclipse.hawkbit.repository.exception.DistributionSetTypeUndefinedException; import org.eclipse.hawkbit.repository.exception.IncompleteDistributionSetException; +import org.eclipse.hawkbit.repository.exception.LockedException; import org.eclipse.hawkbit.repository.exception.UnsupportedSoftwareModuleForThisDistributionSetException; import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.DistributionSet; @@ -185,6 +186,10 @@ public class JpaDistributionSet extends AbstractJpaNamedVersionedEntity implemen } public boolean addModule(final SoftwareModule softwareModule) { + if (isLocked()) { + throw new LockedException(JpaDistributionSet.class, getId(), "ADD_SOFTWARE_MODULE"); + } + if (modules == null) { modules = new HashSet<>(); } @@ -215,6 +220,10 @@ public class JpaDistributionSet extends AbstractJpaNamedVersionedEntity implemen } public void removeModule(final SoftwareModule softwareModule) { + if (isLocked()) { + throw new LockedException(JpaDistributionSet.class, getId(), "REMOVE_SOFTWARE_MODULE"); + } + if (modules != null && modules.removeIf(m -> m.getId().equals(softwareModule.getId()))) { complete = type.checkComplete(this); } @@ -253,7 +262,7 @@ public class JpaDistributionSet extends AbstractJpaNamedVersionedEntity implemen } public void lock() { - if (!complete) { + if (!isComplete()) { throw new IncompleteDistributionSetException("Could not be locked while incomplete!"); } locked = true; diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaSoftwareModule.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaSoftwareModule.java index efa9931ea..6827ce62e 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaSoftwareModule.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaSoftwareModule.java @@ -39,6 +39,7 @@ import lombok.ToString; import org.eclipse.hawkbit.repository.event.remote.SoftwareModuleDeletedEvent; import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleCreatedEvent; import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleUpdatedEvent; +import org.eclipse.hawkbit.repository.exception.LockedException; import org.eclipse.hawkbit.repository.model.Artifact; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.SoftwareModule; @@ -140,6 +141,10 @@ public class JpaSoftwareModule extends AbstractJpaNamedVersionedEntity implement } public void addArtifact(final Artifact artifact) { + if (isLocked()) { + throw new LockedException(JpaSoftwareModule.class, getId(), "ADD_ARTIFACT"); + } + if (artifacts == null) { artifacts = new ArrayList<>(4); artifacts.add((JpaArtifact) artifact); @@ -155,6 +160,10 @@ public class JpaSoftwareModule extends AbstractJpaNamedVersionedEntity implement * @param artifact is removed from the assigned {@link Artifact}s. */ public void removeArtifact(final Artifact artifact) { + if (isLocked()) { + throw new LockedException(JpaSoftwareModule.class, getId(), "REMOVE_ARTIFACT"); + } + if (artifacts != null) { artifacts.remove(artifact); } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/DistributionSetManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/DistributionSetManagementTest.java index 946762652..3c73bf172 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/DistributionSetManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/DistributionSetManagementTest.java @@ -43,6 +43,7 @@ import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; import org.eclipse.hawkbit.repository.exception.EntityReadOnlyException; import org.eclipse.hawkbit.repository.exception.IncompleteDistributionSetException; import org.eclipse.hawkbit.repository.exception.InvalidDistributionSetException; +import org.eclipse.hawkbit.repository.exception.LockedException; import org.eclipse.hawkbit.repository.exception.UnsupportedSoftwareModuleForThisDistributionSetException; import org.eclipse.hawkbit.repository.jpa.AbstractJpaIntegrationTest; import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet; @@ -1010,7 +1011,39 @@ class DistributionSetManagementTest extends AbstractJpaIntegrationTest { } @Test - @Description("Locks an incomplete DS. Expected behaviour is to throw exception and to do not lock it.") + @Description("Software modules of a locked DS can't be modified. Expected behaviour is to throw an exception and to do not modify them.") + void lockDistributionSetApplied() { + final DistributionSet distributionSet = testdataFactory.createDistributionSet("ds-1"); + final int softwareModuleCount = distributionSet.getModules().size(); + assertThat(softwareModuleCount).isNotEqualTo(0); + distributionSetManagement.lock(distributionSet.getId()); + assertThat( + distributionSetManagement.get(distributionSet.getId()).map(DistributionSet::isLocked) + .orElse(false)) + .isTrue(); + + + // try add + assertThatExceptionOfType(LockedException.class) + .as("Attempt to modify a locked DS software modules should throw an exception") + .isThrownBy(() -> distributionSetManagement.assignSoftwareModules( + distributionSet.getId(), List.of(testdataFactory.createSoftwareModule("sm-1").getId()))); + assertThat(distributionSetManagement.get(distributionSet.getId()).get().getModules().size()) + .as("Software module shall not be added to a locked DS.") + .isEqualTo(softwareModuleCount); + + // try remove + assertThatExceptionOfType(LockedException.class) + .as("Attempt to modify a locked DS software modules should throw an exception") + .isThrownBy(() -> distributionSetManagement.unassignSoftwareModule( + distributionSet.getId(), distributionSet.getModules().stream().findFirst().get().getId())); + assertThat(distributionSetManagement.get(distributionSet.getId()).get().getModules().size()) + .as("Software module shall not be removed from a locked DS.") + .isEqualTo(softwareModuleCount); + } + + @Test + @Description("Locks an incomplete DS. Expected behaviour is to throw an exception and to do not lock it.") void lockIncompleteDistributionSetFails() { final DistributionSet incompleteDistributionSet = testdataFactory.createIncompleteDistributionSet(); assertThatExceptionOfType(IncompleteDistributionSetException.class) diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/SoftwareModuleManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/SoftwareModuleManagementTest.java index 66db1c2a9..7ce9a78d1 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/SoftwareModuleManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/SoftwareModuleManagementTest.java @@ -28,6 +28,7 @@ import org.eclipse.hawkbit.repository.builder.SoftwareModuleMetadataCreate; import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleCreatedEvent; import org.eclipse.hawkbit.repository.exception.AssignmentQuotaExceededException; import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException; +import org.eclipse.hawkbit.repository.exception.LockedException; import org.eclipse.hawkbit.repository.jpa.AbstractJpaIntegrationTest; import org.eclipse.hawkbit.repository.jpa.RandomGeneratedInputStream; import org.eclipse.hawkbit.repository.jpa.model.JpaAction_; @@ -824,7 +825,7 @@ public class SoftwareModuleManagementTest extends AbstractJpaIntegrationTest { @Test @Description("Locks a SM.") void lockSoftwareModule() { - final SoftwareModule softwareModule = testdataFactory.createSoftwareModule("ds-1"); + final SoftwareModule softwareModule = testdataFactory.createSoftwareModule("sm-1"); assertThat( softwareModuleManagement.get(softwareModule.getId()).map(SoftwareModule::isLocked).orElse(true)) .isFalse(); @@ -834,6 +835,43 @@ public class SoftwareModuleManagementTest extends AbstractJpaIntegrationTest { .isTrue(); } + @Test + @Description("Artifacts of a locked SM can't be modified. Expected behaviour is to throw an exception and to do not modify them.") + void lockSoftwareModuleApplied() { + final SoftwareModule softwareModule = testdataFactory.createSoftwareModule("sm-1"); + artifactManagement.create( + new ArtifactUpload(new ByteArrayInputStream(new byte[] {1}), softwareModule.getId(), + "artifact1", false, 1)); + final int artifactCount = softwareModuleManagement.get(softwareModule.getId()).get().getArtifacts().size(); + assertThat(artifactCount).isNotEqualTo(0); + softwareModuleManagement.lock(softwareModule.getId()); + assertThat( + softwareModuleManagement.get(softwareModule.getId()).map(SoftwareModule::isLocked).orElse(false)) + .isTrue(); + + + // try add + assertThatExceptionOfType(LockedException.class) + .as("Attempt to modify a locked SM artifacts should throw an exception") + .isThrownBy(() -> artifactManagement.create( + new ArtifactUpload(new ByteArrayInputStream(new byte[] {2}), softwareModule.getId(), + "artifact2", false, 1))); + assertThat(softwareModuleManagement.get(softwareModule.getId()).get().getArtifacts().size()) + .as("Artifacts shall not be added to a locked SM.") + .isEqualTo(artifactCount); + + // try remove + final long artifactId = softwareModuleManagement.get(softwareModule.getId()).get() + .getArtifacts().stream().findFirst().get().getId(); + assertThatExceptionOfType(LockedException.class) + .as("Attempt to modify a locked DS software modules should throw an exception") + .isThrownBy(() -> artifactManagement.delete(artifactId)); + assertThat(softwareModuleManagement.get(softwareModule.getId()).get().getArtifacts().size()) + .as("Software module shall not be removed from a locked DS.") + .isEqualTo(artifactCount); + assertThat(artifactManagement.get(artifactId)).isPresent(); + } + @Test @Description("Verifies that non existing metadata find results in exception.") public void findSoftwareModuleMetadataFailsIfEntryDoesNotExist() {