From ae2c208266fced6c1ea1eacea0bf17301c9b9124 Mon Sep 17 00:00:00 2001 From: Ondrej Charvat Date: Wed, 8 Nov 2023 07:34:25 +0100 Subject: [PATCH] Enable specifying target type when created using DMF API Extension of DMF API with possibility of setting target type name when creating target. If a target type with the provided name is found (was created beforehand) then it is associated with the new target. Signed-off-by: Ondrej Charvat --- docs/content/apis/dmf_api.md | 13 +++ .../amqp/AmqpMessageHandlerService.java | 6 +- .../amqp/AmqpMessageHandlerServiceTest.java | 28 ++++- .../matcher/SoftwareModuleJsonMatcher.java | 2 +- .../dmf/json/model/DmfCreateThing.java | 11 ++ .../repository/ControllerManagement.java | 8 +- .../repository/TargetTypeManagement.java | 2 +- .../jpa/JpaControllerManagement.java | 67 ++++++++--- .../repository/jpa/TargetTypeRepository.java | 8 +- .../jpa/ControllerManagementTest.java | 107 +++++++++++++++++- .../im/authentication/SpPermission.java | 8 ++ 11 files changed, 233 insertions(+), 27 deletions(-) diff --git a/docs/content/apis/dmf_api.md b/docs/content/apis/dmf_api.md index 56085b5e12..7cd544f13d 100644 --- a/docs/content/apis/dmf_api.md +++ b/docs/content/apis/dmf_api.md @@ -63,6 +63,7 @@ Payload Template (optional): ```json { "name": "String", + "type": "String", "attributeUpdate": { "attributes": { "exampleKey1" : "exampleValue1", @@ -74,6 +75,18 @@ Payload Template (optional): ``` The "name" property specifies the name of the thing, which by default is the thing ID. This property is optional.
+
+The "type" property specifies name of a target type which should be assigned to the created/updated target. The +target type with the specified name should be created in advance, otherwise it can't be assigned to the target, +resulting in: +* error is logged +* if the target does not exist then it is created without any target type assigned +* if it exists already then no changes to its target type assignment are made. + +If the "type" property is set to a blank string while updating an existing target then any eventual target type +assignment is removed from the target. This property is optional and if omitted then no changes to the target type +assignment are made.
+
The "attributeUpdate" property provides the attributes of the thing, for details see UPDATE_ATTRIBUTES message. This property is optional. diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerService.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerService.java index 80ca28762f..ad5c33d553 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerService.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerService.java @@ -231,14 +231,18 @@ private void registerTarget(final Message message, final String virtualHost) { final URI amqpUri = IpUtil.createAmqpUri(virtualHost, replyTo); final Target target; if (isOptionalMessageBodyEmpty(message)) { + LOG.debug("Received \"THING_CREATED\" AMQP message for thing \"{}\" without body.", thingId); target = controllerManagement.findOrRegisterTargetIfItDoesNotExist(thingId, amqpUri); } else { checkContentTypeJson(message); final DmfCreateThing thingCreateBody = convertMessage(message, DmfCreateThing.class); final DmfAttributeUpdate thingAttributeUpdateBody = thingCreateBody.getAttributeUpdate(); + LOG.debug("Received \"THING_CREATED\" AMQP message for thing \"{}\" with target name \"{}\" and type " + + "\"{}\".", thingId, thingCreateBody.getName(), thingCreateBody.getType()); + target = controllerManagement.findOrRegisterTargetIfItDoesNotExist(thingId, amqpUri, - thingCreateBody.getName()); + thingCreateBody.getName(), thingCreateBody.getType()); if (thingAttributeUpdateBody != null) { controllerManagement.updateControllerAttributes(thingId, thingAttributeUpdateBody.getAttributes(), diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerServiceTest.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerServiceTest.java index 64e7895334..e39097c213 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerServiceTest.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerServiceTest.java @@ -50,7 +50,6 @@ import org.eclipse.hawkbit.repository.builder.ActionStatusBuilder; import org.eclipse.hawkbit.repository.builder.ActionStatusCreate; import org.eclipse.hawkbit.repository.exception.AssignmentQuotaExceededException; -import org.eclipse.hawkbit.repository.jpa.model.JpaAction; import org.eclipse.hawkbit.repository.jpa.builder.JpaActionStatusBuilder; import org.eclipse.hawkbit.repository.jpa.model.JpaActionStatus; import org.eclipse.hawkbit.repository.jpa.model.helper.SecurityTokenGeneratorHolder; @@ -163,6 +162,9 @@ public class AmqpMessageHandlerServiceTest { @Captor private ArgumentCaptor targetNameCaptor; + @Captor + private ArgumentCaptor targetTypeNameCaptor; + @Captor private ArgumentCaptor uriCaptor; @@ -224,7 +226,8 @@ private void processThingCreatedMessage(final String thingId, final DmfCreateThi uriCaptor.capture())).thenReturn(targetMock); } else { when(controllerManagementMock.findOrRegisterTargetIfItDoesNotExist(targetIdCaptor.capture(), - uriCaptor.capture(), targetNameCaptor.capture())).thenReturn(targetMock); + uriCaptor.capture(), targetNameCaptor.capture(), targetTypeNameCaptor.capture())) + .thenReturn(targetMock); if (payload.getAttributeUpdate() != null) { when(controllerManagementMock.updateControllerAttributes(targetIdCaptor.capture(), attributesCaptor.capture(), modeCaptor.capture())).thenReturn(null); @@ -276,6 +279,27 @@ private void assertThingNameCapturedField(final String thingName) { assertThat(targetNameCaptor.getValue()).as("Thing name is wrong").isEqualTo(thingName); } + @Test + @Description("Tests the creation of a target/thing with specified type name by calling the same method that incoming RabbitMQ messages would access.") + public void createThingWithType() { + final String knownThingId = "2"; + final String knownThingTypeName = "TargetTypeName"; + + final DmfCreateThing payload = new DmfCreateThing(); + payload.setType(knownThingTypeName); + + processThingCreatedMessage(knownThingId, payload); + + assertThingIdCapturedField(knownThingId); + assertReplyToCapturedField("MyTest"); + assertThingTypeCapturedField(knownThingTypeName); + } + + @Step + private void assertThingTypeCapturedField(final String thingType) { + assertThat(targetTypeNameCaptor.getValue()).as("Thing type is wrong").isEqualTo(thingType); + } + @Test @Description("Tests not allowed body in message") public void createThingWithWrongBody() { diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/matcher/SoftwareModuleJsonMatcher.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/matcher/SoftwareModuleJsonMatcher.java index 95d48fc40d..9dc5eec4fc 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/matcher/SoftwareModuleJsonMatcher.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/matcher/SoftwareModuleJsonMatcher.java @@ -26,7 +26,7 @@ public final class SoftwareModuleJsonMatcher { /** * Creates a matcher that matches when the list of repository software - * modules arelogically equal to the specified JSON software modules. + * modules are logically equal to the specified JSON software modules. *

* If the specified repository software modules are null then * the created matcher will only match if the JSON software modules are diff --git a/hawkbit-dmf/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/json/model/DmfCreateThing.java b/hawkbit-dmf/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/json/model/DmfCreateThing.java index c356e72348..c5380bf848 100644 --- a/hawkbit-dmf/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/json/model/DmfCreateThing.java +++ b/hawkbit-dmf/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/json/model/DmfCreateThing.java @@ -24,6 +24,9 @@ public class DmfCreateThing { @JsonProperty private String name; + @JsonProperty + private String type; + @JsonProperty private DmfAttributeUpdate attributeUpdate; @@ -35,6 +38,14 @@ public void setName(final String name) { this.name = name; } + public String getType() { + return type; + } + + public void setType(final String type) { + this.type = type; + } + public DmfAttributeUpdate getAttributeUpdate() { return attributeUpdate; } diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/ControllerManagement.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/ControllerManagement.java index 7645555983..e926be50de 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/ControllerManagement.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/ControllerManagement.java @@ -223,7 +223,8 @@ Map> findTargetVisibleMetaDataBySoftwareModul /** * Register new target in the repository (plug-and-play) and in case it * already exists updates {@link Target#getAddress()} and - * {@link Target#getLastTargetQuery()} and {@link Target#getName()} and + * {@link Target#getLastTargetQuery()} and {@link Target#getName()} + * and {@link Target#getTargetType()} and * switches if {@link TargetUpdateStatus#UNKNOWN} to * {@link TargetUpdateStatus#REGISTERED}. * @@ -233,10 +234,13 @@ Map> findTargetVisibleMetaDataBySoftwareModul * the client IP address of the target, might be {@code null} * @param name * the name of the target + * @param type + * the target type name of the target * @return target reference */ @PreAuthorize(SpringEvalExpressions.IS_CONTROLLER) - Target findOrRegisterTargetIfItDoesNotExist(@NotEmpty String controllerId, @NotNull URI address, String name); + Target findOrRegisterTargetIfItDoesNotExist(@NotEmpty String controllerId, @NotNull URI address, String name, + String type); /** * Retrieves last {@link Action} for a download of an artifact of given diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/TargetTypeManagement.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/TargetTypeManagement.java index 52b2410366..2e449f0a25 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/TargetTypeManagement.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/TargetTypeManagement.java @@ -37,7 +37,7 @@ public interface TargetTypeManagement { * as {@link TargetType#getName()} * @return {@link TargetType} */ - @PreAuthorize(SpPermission.SpringEvalExpressions.HAS_AUTH_READ_TARGET) + @PreAuthorize(SpPermission.SpringEvalExpressions.IS_CONTROLLER_OR_HAS_AUTH_READ_TARGET) Optional getByName(@NotEmpty String name); /** diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaControllerManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaControllerManagement.java index 6f98c31689..54f4a1caca 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaControllerManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaControllerManagement.java @@ -47,6 +47,7 @@ import org.eclipse.hawkbit.repository.RepositoryConstants; import org.eclipse.hawkbit.repository.RepositoryProperties; import org.eclipse.hawkbit.repository.TenantConfigurationManagement; +import org.eclipse.hawkbit.repository.TargetTypeManagement; import org.eclipse.hawkbit.repository.UpdateMode; import org.eclipse.hawkbit.repository.builder.ActionStatusCreate; import org.eclipse.hawkbit.repository.event.remote.CancelTargetAssignmentEvent; @@ -78,6 +79,7 @@ import org.eclipse.hawkbit.repository.model.SoftwareModule; import org.eclipse.hawkbit.repository.model.SoftwareModuleMetadata; import org.eclipse.hawkbit.repository.model.Target; +import org.eclipse.hawkbit.repository.model.TargetType; import org.eclipse.hawkbit.repository.model.TargetUpdateStatus; import org.eclipse.hawkbit.repository.model.helper.EventPublisherHolder; import org.eclipse.hawkbit.security.SystemSecurityContext; @@ -154,6 +156,9 @@ public class JpaControllerManagement extends JpaActionManagement implements Cont @Autowired private ConfirmationManagement confirmationManagement; + @Autowired + private TargetTypeManagement targetTypeManagement; + public JpaControllerManagement(final ScheduledExecutorService executorService, final ActionRepository actionRepository, final ActionStatusRepository actionStatusRepository, final QuotaManagement quotaManagement, final RepositoryProperties repositoryProperties) { @@ -377,28 +382,43 @@ public void deleteExistingTarget(@NotEmpty final String controllerId) { @Transactional(isolation = Isolation.READ_COMMITTED) @Retryable(include = ConcurrencyFailureException.class, exclude = EntityAlreadyExistsException.class, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY)) public Target findOrRegisterTargetIfItDoesNotExist(final String controllerId, final URI address) { - return findOrRegisterTargetIfItDoesNotExist(controllerId, address, null); + return findOrRegisterTargetIfItDoesNotExist(controllerId, address, null, null); } @Override @Transactional(isolation = Isolation.READ_COMMITTED) @Retryable(include = ConcurrencyFailureException.class, exclude = EntityAlreadyExistsException.class, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY)) public Target findOrRegisterTargetIfItDoesNotExist(final String controllerId, final URI address, - final String name) { - final Specification spec = (targetRoot, query, cb) -> cb - .equal(targetRoot.get(JpaTarget_.controllerId), controllerId); + final String name, final String type) { + final Specification spec = + (targetRoot, query, cb) -> cb.equal(targetRoot.get(JpaTarget_.controllerId), controllerId); - return targetRepository.findOne(spec).map(target -> updateTarget(target, address, name)) - .orElseGet(() -> createTarget(controllerId, address, name)); + return targetRepository.findOne(spec).map(target -> updateTarget(target, address, name, type)) + .orElseGet(() -> createTarget(controllerId, address, name, type)); } - private Target createTarget(final String controllerId, final URI address, final String name) { + private Target createTarget(final String controllerId, final URI address, final String name, final String type) { - final Target result = targetRepository.save((JpaTarget) entityFactory.target().create() + LOG.debug("Creating target for thing ID \"{}\".", controllerId); + JpaTarget jpaTarget = (JpaTarget) entityFactory.target().create() .controllerId(controllerId).description("Plug and Play target: " + controllerId) .name((StringUtils.hasText(name) ? name : controllerId)).status(TargetUpdateStatus.REGISTERED) .lastTargetQuery(System.currentTimeMillis()) - .address(Optional.ofNullable(address).map(URI::toString).orElse(null)).build()); + .address(Optional.ofNullable(address).map(URI::toString).orElse(null)).build(); + + + if (StringUtils.hasText(type)) { + var targetTypeOptional = targetTypeManagement.getByName(type); + if (targetTypeOptional.isPresent()) { + LOG.debug("Setting target type for thing ID \"{}\" to \"{}\".", controllerId, type); + jpaTarget.setTargetType(targetTypeOptional.get()); + } else { + LOG.error("Target type with the provided name \"{}\" was not found. Creating target for thing ID" + + " \"{}\" without target type assignment", type, controllerId); + } + } + + final Target result = targetRepository.save(jpaTarget); afterCommit.afterCommit(() -> eventPublisherHolder.getEventPublisher() .publishEvent(new TargetPollEvent(result, eventPublisherHolder.getApplicationId()))); @@ -494,14 +514,30 @@ private static String formatQueryInStatementParams(final Collection para * or the buffer queue is full. * */ - private Target updateTarget(final JpaTarget toUpdate, final URI address, final String name) { - if (isStoreEager(toUpdate, address, name) || !queue.offer(new TargetPoll(toUpdate))) { + private Target updateTarget(final JpaTarget toUpdate, final URI address, final String name, final String type) { + if (isStoreEager(toUpdate, address, name, type) || !queue.offer(new TargetPoll(toUpdate))) { if (isAddressChanged(toUpdate.getAddress(), address)) { toUpdate.setAddress(address.toString()); } if (isNameChanged(toUpdate.getName(), name)) { toUpdate.setName(name); } + + if (isTypeChanged(toUpdate.getTargetType(), type)) { + if (StringUtils.hasText(type)) { + var targetTypeOptional = targetTypeManagement.getByName(type); + if (targetTypeOptional.isPresent()) { + LOG.debug("Updating target type for thing ID \"{}\" to \"{}\".", toUpdate.getControllerId(), type); + toUpdate.setTargetType(targetTypeOptional.get()); + } else { + LOG.error("Target type with the provided name \"{}\" was not found. Target type for thing ID" + + " \"{}\" will not be updated", type, toUpdate.getControllerId()); + } + } else { + LOG.debug("Removing target type assignment for thing ID \"{}\".", toUpdate.getControllerId()); + toUpdate.setTargetType(null); //unassign target type if "" target type name was provided + } + } if (isStatusUnknown(toUpdate.getUpdateStatus())) { toUpdate.setUpdateStatus(TargetUpdateStatus.REGISTERED); } @@ -513,9 +549,10 @@ private Target updateTarget(final JpaTarget toUpdate, final URI address, final S return toUpdate; } - private boolean isStoreEager(final JpaTarget toUpdate, final URI address, final String name) { + private boolean isStoreEager(final JpaTarget toUpdate, final URI address, final String name, final String type) { return repositoryProperties.isEagerPollPersistence() || isAddressChanged(toUpdate.getAddress(), address) - || isNameChanged(toUpdate.getName(), name) || isStatusUnknown(toUpdate.getUpdateStatus()); + || isNameChanged(toUpdate.getName(), name) || isTypeChanged(toUpdate.getTargetType(), type) + || isStatusUnknown(toUpdate.getUpdateStatus()); } private static boolean isAddressChanged(final URI addressToUpdate, final URI address) { @@ -526,6 +563,10 @@ private static boolean isNameChanged(final String nameToUpdate, final String nam return StringUtils.hasText(name) && !nameToUpdate.equals(name); } + private static boolean isTypeChanged(final TargetType targetTypeToUpdate, final String type) { + return (type != null) && (targetTypeToUpdate == null || !targetTypeToUpdate.getName().equals(type)); + } + private static boolean isStatusUnknown(final TargetUpdateStatus statusToUpdate) { return TargetUpdateStatus.UNKNOWN == statusToUpdate; } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/TargetTypeRepository.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/TargetTypeRepository.java index 08bc55042a..9e0b742bec 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/TargetTypeRepository.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/TargetTypeRepository.java @@ -18,6 +18,7 @@ import org.eclipse.hawkbit.repository.jpa.model.JpaTargetType; import org.eclipse.hawkbit.repository.jpa.specifications.TargetTypeSpecification; import org.eclipse.hawkbit.repository.model.TargetType; +import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; @@ -186,8 +187,11 @@ default List findByDsType(@Param("id") final Long dsTypeId) { * * @param name * to search for - * @return all {@link TargetType}s in the repository with given - * {@link TargetType#getName()} + * @return a single {@link TargetType} from the repository with given + * {@link TargetType#getName()} or empty Optional if none is found + * + * @throws IncorrectResultSizeDataAccessException – if more than one target + * type with the given name is found. */ default Optional findByName(final String name) { return this.findOne(Specification.where(TargetTypeSpecification.hasName(name))); diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/ControllerManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/ControllerManagementTest.java index 8672a49c61..fb064c0e72 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/ControllerManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/ControllerManagementTest.java @@ -52,6 +52,7 @@ import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleUpdatedEvent; import org.eclipse.hawkbit.repository.event.remote.entity.TargetCreatedEvent; import org.eclipse.hawkbit.repository.event.remote.entity.TargetUpdatedEvent; +import org.eclipse.hawkbit.repository.event.remote.entity.TargetTypeCreatedEvent; import org.eclipse.hawkbit.repository.exception.AssignmentQuotaExceededException; import org.eclipse.hawkbit.repository.exception.CancelActionNotAllowedException; import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException; @@ -522,12 +523,108 @@ void findOrRegisterTargetIfItDoesNotExist() { @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), @Expect(type = TargetPollEvent.class, count = 2), @Expect(type = TargetUpdatedEvent.class, count = 1) }) void findOrRegisterTargetIfItDoesNotExistWithName() { - final Target target = controllerManagement.findOrRegisterTargetIfItDoesNotExist("AA", LOCALHOST, "TestName"); + final Target target = controllerManagement.findOrRegisterTargetIfItDoesNotExist("AA", LOCALHOST, "TestName", null); final Target sameTarget = controllerManagement.findOrRegisterTargetIfItDoesNotExist("AA", LOCALHOST, - "ChangedTestName"); + "ChangedTestName", null); assertThat(target.getId()).as("Target should be the equals").isEqualTo(sameTarget.getId()); - assertThat(target.getName()).as("Taget names should be different").isNotEqualTo(sameTarget.getName()); - assertThat(sameTarget.getName()).as("Taget name should be changed").isEqualTo("ChangedTestName"); + assertThat(target.getName()).as("Target names should be different").isNotEqualTo(sameTarget.getName()); + assertThat(sameTarget.getName()).as("Target name should be changed").isEqualTo("ChangedTestName"); + assertThat(targetRepository.count()).as("Only 1 target should be registred").isEqualTo(1L); + } + + @Test + @Description("Register a controller which does not exist with existing target type and update its target type to another existing one") + @ExpectEvents({ @Expect(type = TargetTypeCreatedEvent.class, count = 2), + @Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = TargetPollEvent.class, count = 2), + @Expect(type = TargetUpdatedEvent.class, count = 1) }) + void findOrRegisterTargetIfItDoesNotExistWithExistingTypeAndUpdateToExistingType() { + targetTypeManagement.create(entityFactory.targetType().create().name("knownTargetTypeName1")); + targetTypeManagement.create(entityFactory.targetType().create().name("knownTargetTypeName2")); + final Target target = controllerManagement.findOrRegisterTargetIfItDoesNotExist("AA", LOCALHOST, + null, "knownTargetTypeName1"); + final Target sameTarget = controllerManagement.findOrRegisterTargetIfItDoesNotExist("AA", LOCALHOST, + null, "knownTargetTypeName2"); + assertThat(target.getId()).as("Target should be the same").isEqualTo(sameTarget.getId()); + assertThat(target.getTargetType().getName()).as("Target type should be set") + .isEqualTo("knownTargetTypeName1"); + assertThat(sameTarget.getTargetType().getName()).as("Target type should be changed") + .isEqualTo("knownTargetTypeName2"); + assertThat(targetRepository.count()).as("Only 1 target should be registred").isEqualTo(1L); + } + + @Test + @Description("Register a controller which does not exist with existing target type and update its target type to non existing one") + @ExpectEvents({ @Expect(type = TargetTypeCreatedEvent.class, count = 1), + @Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = TargetPollEvent.class, count = 2) }) + void findOrRegisterTargetIfItDoesNotExistWithExistingTypeAndUpdateToNonExistingType() { + targetTypeManagement.create(entityFactory.targetType().create().name("knownTargetTypeName")); + final Target target = controllerManagement.findOrRegisterTargetIfItDoesNotExist("AA", LOCALHOST, + null,"knownTargetTypeName"); + final Target sameTarget = controllerManagement.findOrRegisterTargetIfItDoesNotExist("AA", LOCALHOST, + null,"unknownTargetTypeName"); + assertThat(target.getId()).as("Target should be the same").isEqualTo(sameTarget.getId()); + assertThat(sameTarget.getTargetType().getName()).as("Target type should be unchanged") + .isEqualTo("knownTargetTypeName"); + assertThat(targetRepository.count()).as("Only 1 target should be registred").isEqualTo(1L); + } + + @Test + @Description("Register a controller which does not exist with existing target type and unassign its target type") + @ExpectEvents({ @Expect(type = TargetTypeCreatedEvent.class, count = 1), + @Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = TargetPollEvent.class, count = 2), + @Expect(type = TargetUpdatedEvent.class, count = 1) }) + void findOrRegisterTargetIfItDoesNotExistWithExistingTypeAndUnassignType() { + targetTypeManagement.create(entityFactory.targetType().create().name("knownTargetTypeName")); + final Target target = controllerManagement.findOrRegisterTargetIfItDoesNotExist("AA", LOCALHOST, + null, "knownTargetTypeName"); + final Target sameTarget = controllerManagement.findOrRegisterTargetIfItDoesNotExist("AA", LOCALHOST, + null, ""); + assertThat(target.getId()).as("Target should be the same").isEqualTo(sameTarget.getId()); + assertThat(sameTarget.getTargetType()).as("Target type should be unassigned") + .isNull(); + assertThat(targetRepository.count()).as("Only 1 target should be registred").isEqualTo(1L); + } + + @Test + @Description("Register a controller which does not exist without target type and update its target type to existing one") + @ExpectEvents({ @Expect(type = TargetTypeCreatedEvent.class, count = 1), + @Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = TargetPollEvent.class, count = 2), + @Expect(type = TargetUpdatedEvent.class, count = 1) }) + void findOrRegisterTargetIfItDoesNotExistWithoutTypeAndUpdateToExistingType() { + targetTypeManagement.create(entityFactory.targetType().create().name("knownTargetTypeName")); + final Target target = controllerManagement.findOrRegisterTargetIfItDoesNotExist("AA", LOCALHOST, + null, null); + final Target sameTarget = controllerManagement.findOrRegisterTargetIfItDoesNotExist("AA", LOCALHOST, + null, "knownTargetTypeName"); + assertThat(target.getId()).as("Target should be the equals").isEqualTo(sameTarget.getId()); + assertThat(target.getTargetType()).as("Target type should not be assigned") + .isNull(); + assertThat(sameTarget.getTargetType().getName()).as("Target type should be assigned") + .isEqualTo("knownTargetTypeName"); + assertThat(targetRepository.count()).as("Only 1 target should be registred").isEqualTo(1L); + } + + @Test + @Description("Register a controller which does not exist with non existing target type and update its target type to existing one") + @ExpectEvents({ @Expect(type = TargetTypeCreatedEvent.class, count = 1), + @Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = TargetPollEvent.class, count = 2), + @Expect(type = TargetUpdatedEvent.class, count = 1) }) + void findOrRegisterTargetIfItDoesNotExistWithNonExistingTypeAndUpdateToExistingType() { + targetTypeManagement.create(entityFactory.targetType().create().name("knownTargetTypeName")); + final Target target = controllerManagement.findOrRegisterTargetIfItDoesNotExist("AA", LOCALHOST, + null, "unknownTargetTypeName"); + final Target sameTarget = controllerManagement.findOrRegisterTargetIfItDoesNotExist("AA", LOCALHOST, + null, "knownTargetTypeName"); + assertThat(target.getId()).as("Target should be the equals").isEqualTo(sameTarget.getId()); + assertThat(target.getTargetType()).as("Target type should not be assigned") + .isNull(); + assertThat(sameTarget.getTargetType().getName()).as("Target type should be assigned") + .isEqualTo("knownTargetTypeName"); assertThat(targetRepository.count()).as("Only 1 target should be registred").isEqualTo(1L); } @@ -612,7 +709,7 @@ void findOrRegisterTargetIfItDoesNotExistDoesUpdateNameOnExistingTargetProperly( assertThat(newTarget.getName()).isEqualTo(controllerId); final Target firstTimeUpdatedTarget = controllerManagement.findOrRegisterTargetIfItDoesNotExist(controllerId, - LOCALHOST, targetName); + LOCALHOST, targetName, null); assertThat(firstTimeUpdatedTarget.getName()).isEqualTo(targetName); // Name should not change to default (name=targetId) if target is diff --git a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/im/authentication/SpPermission.java b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/im/authentication/SpPermission.java index 0b9b60a348..c21d6be1b3 100644 --- a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/im/authentication/SpPermission.java +++ b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/im/authentication/SpPermission.java @@ -422,6 +422,14 @@ public static final class SpringEvalExpressions { public static final String IS_CONTROLLER_OR_HAS_AUTH_READ_REPOSITORY_AND_UPDATE_TARGET = IS_CONTROLLER + HAS_AUTH_OR + HAS_AUTH_READ_REPOSITORY_AND_UPDATE_TARGET; + /** + * Spring security eval hasAuthority expression to check if spring + * context contains {@link SpPermission#IS_CONTROLLER} or + * {@link #HAS_AUTH_READ_TARGET}. + */ + public static final String IS_CONTROLLER_OR_HAS_AUTH_READ_TARGET = + IS_CONTROLLER + HAS_AUTH_OR + HAS_AUTH_READ_TARGET; + private SpringEvalExpressions() { // utility class }