From 146735012aeba9efa81ac3ecbc3900a68122e74c Mon Sep 17 00:00:00 2001 From: Bondar Bogdan <36962546+bogdan-bondar@users.noreply.github.com> Date: Thu, 18 Nov 2021 09:07:05 +0100 Subject: [PATCH] Artifact Encryption plug point (#1202) * added ArtifactEncryption interface, injected it into SM creation UI module, added encryption metadata key generation upon SM creation, used encryptor during file upload Signed-off-by: Bogdan Bondar * add default artifact encryption implementation based on gcm aes algorithm Signed-off-by: Bogdan Bondar * changed ArtifactEncryptor interface to manage encryption secrets by itself Signed-off-by: Bogdan Bondar * cleaned up stale code, fixed sonar Signed-off-by: Bogdan Bondar * fixed software module encryption within transaction Signed-off-by: Bogdan Bondar * added artifact encryption secrets store Signed-off-by: Bogdan Bondar * extended ArtifactEncryption interface to allow decryption, secrets store provides removeSecret, added missing javadocs Signed-off-by: Bogdan Bondar * intriduced DbArtifact interface, use EncryptionAwareDbArtifact for artifact decryption during download Signed-off-by: Bogdan Bondar * introduced ArtifactEncryptionService to minimize duplications and unneccessary dependency injections Signed-off-by: Bogdan Bondar * declared ArtifactEncryptionService as a bean Signed-off-by: Bogdan Bondar * added persistant encryption flag to software module Signed-off-by: Bogdan Bondar * further adptations for encryption flag persistence Signed-off-by: Bogdan Bondar * added ArtifactEncryptionException, fixed encryption check in UI Signed-off-by: Bogdan Bondar * added encryption error handling Signed-off-by: Bogdan Bondar * added encrypted flag to DDI/DMF, adapted exception handling Signed-off-by: Bogdan Bondar * adapted rest docs Signed-off-by: Bogdan Bondar * Add test to verify artifact encryption is not given by default Signed-off-by: Florian Ruschbaschan * Add isEncrypted() to toString() of JpaSoftwareModule, fix typos Signed-off-by: Florian Ruschbaschan * Fix sql migration scripts Signed-off-by: Florian Ruschbaschan * Calculate encrypted artifact size by subtract encryption size overhead Signed-off-by: Florian Ruschbaschan * publish upload failed without waiting for interuption during UI file upload Signed-off-by: Bogdan Bondar * upgraded cron utils to 9.1.6 Signed-off-by: Bogdan Bondar Co-authored-by: Florian Ruschbaschan --- .../repository/model/AbstractDbArtifact.java | 31 ++--- .../artifact/repository/model/DbArtifact.java | 45 +++++++ .../hawkbit/exception/SpServerError.java | 23 +++- .../amqp/AmqpMessageDispatcherService.java | 57 ++++---- .../AmqpControllerAuthenticationTest.java | 6 +- .../dmf/json/model/DmfSoftwareModule.java | 15 ++- .../repository/ArtifactEncryption.java | 68 ++++++++++ .../ArtifactEncryptionSecretsStore.java | 66 +++++++++ .../repository/ArtifactEncryptionService.java | 127 ++++++++++++++++++ .../repository/ArtifactManagement.java | 17 ++- .../builder/SoftwareModuleCreate.java | 7 + .../ArtifactEncryptionFailedException.java | 49 +++++++ ...rtifactEncryptionUnsupportedException.java | 35 +++++ .../repository/model/SoftwareModule.java | 5 + .../ArtifactEncryptionServiceTest.java | 42 ++++++ .../jpa/EncryptionAwareDbArtifact.java | 65 +++++++++ .../repository/jpa/JpaArtifactManagement.java | 51 +++++-- .../jpa/JpaSoftwareModuleManagement.java | 14 +- .../RepositoryApplicationConfiguration.java | 56 +++++--- .../jpa/builder/JpaSoftwareModuleCreate.java | 15 ++- .../jpa/model/JpaSoftwareModule.java | 47 ++++++- ...12_20__add_encryption_flag_to_sm___DB2.sql | 3 + ..._12_20__add_encryption_flag_to_sm___H2.sql | 3 + ..._20__add_encryption_flag_to_sm___MYSQL.sql | 3 + ...add_encryption_flag_to_sm___POSTGRESQL.sql | 3 + ..._19__add_valid_flag_to_ds___SQL_SERVER.sql | 4 +- ...add_encryption_flag_to_sm___SQL_SERVER.sql | 1 + .../jpa/ArtifactManagementTest.java | 65 ++++----- .../jpa/SoftwareModuleManagementTest.java | 20 +-- .../repository/test/util/TestdataFactory.java | 14 +- .../hawkbit/ddi/json/model/DdiChunk.java | 15 ++- .../hawkbit/ddi/json/model/DdiChunkTest.java | 26 ++-- .../rest/resource/DataConversionHelper.java | 2 +- .../ddi/rest/resource/DdiRootController.java | 5 +- .../softwaremodule/MgmtSoftwareModule.java | 11 ++ .../MgmtSoftwareModuleRequestBodyPost.java | 21 +++ .../MgmtDownloadArtifactResource.java | 5 +- .../resource/MgmtSoftwareModuleMapper.java | 4 +- .../MgmtSoftwareModuleResourceTest.java | 22 ++- .../exception/ResponseExceptionHandler.java | 7 +- .../hawkbit/rest/util/FileStreamingUtil.java | 18 +-- .../hawkbit/rest/util/JsonBuilder.java | 15 ++- .../documentation/DdiApiModelProperties.java | 10 +- .../RootControllerDocumentationTest.java | 10 +- .../documentation/MgmtApiModelProperties.java | 1 + .../SoftwaremodulesDocumentationTest.java | 11 +- .../hawkbit/ui/MgmtUiConfiguration.java | 13 ++ .../details/ArtifactDetailsGrid.java | 41 ++++-- .../smtable/AddSmWindowController.java | 10 +- .../ui/artifacts/smtable/SmWindowBuilder.java | 8 +- .../ui/artifacts/smtable/SmWindowLayout.java | 15 ++- .../SmWindowLayoutComponentBuilder.java | 19 ++- .../smtable/UpdateSmWindowController.java | 2 + .../upload/AbstractFileTransferHandler.java | 24 +++- .../ui/artifacts/upload/FileUploadId.java | 94 ++++--------- .../common/builder/FormComponentBuilder.java | 6 +- .../mappers/SoftwareModuleToProxyMapper.java | 1 + .../data/proxies/ProxySoftwareModule.java | 10 ++ ...AddUpdateWindowLayoutComponentBuilder.java | 2 +- .../detailslayout/SoftwareModuleDetails.java | 7 +- .../grid/support/MasterEntitySupport.java | 22 +++ .../disttype/SmTypeSelectedGrid.java | 2 +- .../DsWindowLayoutComponentBuilder.java | 2 +- .../ArtifactEncryptionErrorExtractor.java | 59 ++++++++ .../extractors/UploadErrorExtractor.java | 2 +- ...ssignmentWindowLayoutComponentBuilder.java | 2 +- ...ssignmentWindowLayoutComponentBuilder.java | 2 +- .../AuthenticationConfigurationView.java | 8 +- .../RepositoryConfigurationView.java | 6 +- .../RolloutConfigurationView.java | 2 +- .../ui/utils/UIComponentIdProvider.java | 21 ++- .../hawkbit/ui/utils/UIMessageIdProvider.java | 8 ++ .../src/main/resources/messages.properties | 8 ++ pom.xml | 2 +- 74 files changed, 1214 insertions(+), 324 deletions(-) create mode 100644 hawkbit-core/src/main/java/org/eclipse/hawkbit/artifact/repository/model/DbArtifact.java create mode 100644 hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/ArtifactEncryption.java create mode 100644 hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/ArtifactEncryptionSecretsStore.java create mode 100644 hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/ArtifactEncryptionService.java create mode 100644 hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/ArtifactEncryptionFailedException.java create mode 100644 hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/ArtifactEncryptionUnsupportedException.java create mode 100644 hawkbit-repository/hawkbit-repository-api/src/test/java/org/eclipse/hawkbit/repository/ArtifactEncryptionServiceTest.java create mode 100644 hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/EncryptionAwareDbArtifact.java create mode 100644 hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/DB2/V1_12_20__add_encryption_flag_to_sm___DB2.sql create mode 100644 hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/H2/V1_12_20__add_encryption_flag_to_sm___H2.sql create mode 100644 hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/MYSQL/V1_12_20__add_encryption_flag_to_sm___MYSQL.sql create mode 100644 hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/POSTGRESQL/V1_12_20__add_encryption_flag_to_sm___POSTGRESQL.sql create mode 100644 hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/SQL_SERVER/V1_12_20__add_encryption_flag_to_sm___SQL_SERVER.sql create mode 100644 hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/error/extractors/ArtifactEncryptionErrorExtractor.java diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/artifact/repository/model/AbstractDbArtifact.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/artifact/repository/model/AbstractDbArtifact.java index adffe3f99..4ef06268d 100644 --- a/hawkbit-core/src/main/java/org/eclipse/hawkbit/artifact/repository/model/AbstractDbArtifact.java +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/artifact/repository/model/AbstractDbArtifact.java @@ -8,15 +8,13 @@ */ package org.eclipse.hawkbit.artifact.repository.model; -import java.io.InputStream; - import org.springframework.util.Assert; /** * Database representation of artifact. * */ -public abstract class AbstractDbArtifact { +public abstract class AbstractDbArtifact implements DbArtifact { private final String artifactId; private final long size; @@ -34,46 +32,33 @@ public abstract class AbstractDbArtifact { this.contentType = contentType; } - /** - * @return ID of the artifact - */ + @Override public String getArtifactId() { return artifactId; } - /** - * @return hashes of the artifact - */ + @Override public DbArtifactHash getHashes() { return hashes; } /** * Set hashes of the artifact + * + * @param hashes + * artifact hashes */ public void setHashes(final DbArtifactHash hashes) { this.hashes = hashes; } - /** - * @return site of the artifact in bytes - */ + @Override public long getSize() { return size; } - /** - * @return content-type if known by the repository or null - */ + @Override public String getContentType() { return contentType; } - - /** - * Creates an {@link InputStream} on this artifact. Caller has to take care of - * closing the stream. Repeatable calls open a new {@link InputStream}. - * - * @return {@link InputStream} to read from artifact. - */ - public abstract InputStream getFileInputStream(); } diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/artifact/repository/model/DbArtifact.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/artifact/repository/model/DbArtifact.java new file mode 100644 index 000000000..0af9d3fae --- /dev/null +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/artifact/repository/model/DbArtifact.java @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2021 Bosch.IO GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.artifact.repository.model; + +import java.io.InputStream; + +/** + * Interface definition for artifact binary. + */ +public interface DbArtifact { + + /** + * @return ID of the artifact + */ + String getArtifactId(); + + /** + * @return hashes of the artifact + */ + DbArtifactHash getHashes(); + + /** + * @return size of the artifact in bytes + */ + long getSize(); + + /** + * @return content-type if known by the repository or null + */ + String getContentType(); + + /** + * Creates an {@link InputStream} on this artifact. Caller has to take care of + * closing the stream. Repeatable calls open a new {@link InputStream}. + * + * @return {@link InputStream} to read from artifact. + */ + InputStream getFileInputStream(); +} 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 dd780f30f..c640d597a 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 @@ -97,6 +97,18 @@ public enum SpServerError { SP_ARTIFACT_UPLOAD_FAILED("hawkbit.server.error.artifact.uploadFailed", "Upload of artifact failed with internal server error."), + /** + * + */ + SP_ARTIFACT_ENCRYPTION_NOT_SUPPORTED("hawkbit.server.error.artifact.encryptionNotSupported", + "Artifact encryption is not supported."), + + /** + * + */ + SP_ARTIFACT_ENCRYPTION_FAILED("hawkbit.server.error.artifact.encryptionFailed", + "Artifact encryption operation failed."), + /** * */ @@ -161,15 +173,15 @@ public enum SpServerError { "Storage quota will be exceeded if file is uploaded."), /** - * error message, which describes that the action can not be canceled cause the - * action is inactive. + * error message, which describes that the action can not be canceled cause + * the action is inactive. */ SP_ACTION_NOT_CANCELABLE("hawkbit.server.error.action.notcancelable", "Only active actions which are in status pending are cancelable."), /** - * error message, which describes that the action can not be force quit cause - * the action is inactive. + * error message, which describes that the action can not be force quit + * cause the action is inactive. */ SP_ACTION_NOT_FORCE_QUITABLE("hawkbit.server.error.action.notforcequitable", "Only active actions which are in status pending can be force quit."), @@ -250,7 +262,8 @@ public enum SpServerError { "Information for schedule, duration or timezone is missing; or there is no valid maintenance window available in future."), /** - * Error message informing that the action type for auto-assignment is invalid. + * Error message informing that the action type for auto-assignment is + * invalid. */ SP_AUTO_ASSIGN_ACTION_TYPE_INVALID("hawkbit.server.error.repo.invalidAutoAssignActionType", "The given action type for auto-assignment is invalid: allowed values are ['forced', 'soft', 'downloadonly']"), diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpMessageDispatcherService.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpMessageDispatcherService.java index 8daa8916c..80e3113a2 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpMessageDispatcherService.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpMessageDispatcherService.java @@ -8,6 +8,18 @@ */ package org.eclipse.hawkbit.amqp; +import static org.eclipse.hawkbit.repository.RepositoryConstants.MAX_ACTION_COUNT; + +import java.net.URI; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + import org.eclipse.hawkbit.api.ApiType; import org.eclipse.hawkbit.api.ArtifactUrl; import org.eclipse.hawkbit.api.ArtifactUrlHandler; @@ -55,18 +67,6 @@ import org.springframework.context.event.EventListener; import org.springframework.data.domain.PageRequest; import org.springframework.util.CollectionUtils; -import java.net.URI; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; -import java.util.function.Function; -import java.util.stream.Collectors; - -import static org.eclipse.hawkbit.repository.RepositoryConstants.MAX_ACTION_COUNT; - /** * {@link AmqpMessageDispatcherService} create all outgoing AMQP messages and * delegate the messages to a {@link AmqpMessageSenderService}. @@ -105,8 +105,8 @@ public class AmqpMessageDispatcherService extends BaseAmqpService { * @param targetManagement * to access target information * @param serviceMatcher - * to check in cluster case if the message is from the same cluster - * node + * to check in cluster case if the message is from the same + * cluster node * @param distributionSetManagement * to retrieve modules */ @@ -129,8 +129,8 @@ public class AmqpMessageDispatcherService extends BaseAmqpService { } /** - * Method to send a message to a RabbitMQ Exchange after the Distribution set - * has been assign to a Target. + * Method to send a message to a RabbitMQ Exchange after the Distribution + * set has been assign to a Target. * * @param assignedEvent * the object to be send. @@ -257,9 +257,9 @@ public class AmqpMessageDispatcherService extends BaseAmqpService { /** * Method to get the type of event depending on whether the action is a - * DOWNLOAD_ONLY action or if it has a valid maintenance window available or not - * based on defined maintenance schedule. In case of no maintenance schedule or - * if there is a valid window available, the topic + * DOWNLOAD_ONLY action or if it has a valid maintenance window available or + * not based on defined maintenance schedule. In case of no maintenance + * schedule or if there is a valid window available, the topic * {@link EventTopic#DOWNLOAD_AND_INSTALL} is returned else * {@link EventTopic#DOWNLOAD} is returned. * @@ -275,8 +275,8 @@ public class AmqpMessageDispatcherService extends BaseAmqpService { } /** - * Determines the {@link EventTopic} for the given {@link Action}, depending on - * its action type. + * Determines the {@link EventTopic} for the given {@link Action}, depending + * on its action type. * * @param action * to obtain the corresponding {@link EventTopic} for @@ -291,8 +291,8 @@ public class AmqpMessageDispatcherService extends BaseAmqpService { } /** - * Method to send a message to a RabbitMQ Exchange after the assignment of the - * Distribution set to a Target has been canceled. + * Method to send a message to a RabbitMQ Exchange after the assignment of + * the Distribution set to a Target has been canceled. * * @param cancelEvent * that is to be converted to a DMF message @@ -315,11 +315,12 @@ public class AmqpMessageDispatcherService extends BaseAmqpService { } /** - * Method to send a message to a RabbitMQ Exchange after a Target was deleted. + * Method to send a message to a RabbitMQ Exchange after a Target was + * deleted. * * @param deleteEvent - * the TargetDeletedEvent which holds the necessary data for sending - * a target delete message. + * the TargetDeletedEvent which holds the necessary data for + * sending a target delete message. */ @EventListener(classes = TargetDeletedEvent.class) protected void targetDelete(final TargetDeletedEvent deleteEvent) { @@ -345,7 +346,8 @@ public class AmqpMessageDispatcherService extends BaseAmqpService { return; } - final DmfDownloadAndUpdateRequest downloadAndUpdateRequest = createDownloadAndUpdateRequest(target, action.getId(), modules); + final DmfDownloadAndUpdateRequest downloadAndUpdateRequest = createDownloadAndUpdateRequest(target, + action.getId(), modules); final Message message = getMessageConverter().toMessage(downloadAndUpdateRequest, createConnectorMessagePropertiesEvent(tenant, target.getControllerId(), getEventTypeForTarget(action))); amqpSenderService.sendMessage(message, targetAddress); @@ -445,6 +447,7 @@ public class AmqpMessageDispatcherService extends BaseAmqpService { amqpSoftwareModule.setModuleId(entry.getKey().getId()); amqpSoftwareModule.setModuleType(entry.getKey().getType().getKey()); amqpSoftwareModule.setModuleVersion(entry.getKey().getVersion()); + amqpSoftwareModule.setEncrypted(entry.getKey().isEncrypted() ? Boolean.TRUE : null); amqpSoftwareModule.setArtifacts(convertArtifacts(target, entry.getKey().getArtifacts())); if (!CollectionUtils.isEmpty(entry.getValue())) { diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpControllerAuthenticationTest.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpControllerAuthenticationTest.java index df894849f..b123ee483 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpControllerAuthenticationTest.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpControllerAuthenticationTest.java @@ -150,8 +150,8 @@ public class AmqpControllerAuthenticationTest { authenticationManager.postConstruct(); - testArtifact = new JpaArtifact(SHA1, "afilename", new JpaSoftwareModule( - new JpaSoftwareModuleType("a key", "a name", null, 1), "a name", null, null, null)); + testArtifact = new JpaArtifact(SHA1, "afilename", + new JpaSoftwareModule(new JpaSoftwareModuleType("a key", "a name", null, 1), "a name", "a version")); testArtifact.setId(1L); amqpMessageHandlerService = new AmqpMessageHandlerService(rabbitTemplate, @@ -260,7 +260,7 @@ public class AmqpControllerAuthenticationTest { when(tenantConfigurationManagementMock.getConfigurationValue( eq(TenantConfigurationKey.AUTHENTICATION_MODE_TARGET_SECURITY_TOKEN_ENABLED), eq(Boolean.class))) - .thenReturn(CONFIG_VALUE_TRUE); + .thenReturn(CONFIG_VALUE_TRUE); when(rabbitTemplate.getMessageConverter()).thenReturn(messageConverter); diff --git a/hawkbit-dmf/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/json/model/DmfSoftwareModule.java b/hawkbit-dmf/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/json/model/DmfSoftwareModule.java index 63f285d50..995b3bc4d 100644 --- a/hawkbit-dmf/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/json/model/DmfSoftwareModule.java +++ b/hawkbit-dmf/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/json/model/DmfSoftwareModule.java @@ -32,6 +32,8 @@ public class DmfSoftwareModule { @JsonProperty private String moduleVersion; @JsonProperty + private Boolean encrypted; + @JsonProperty private List artifacts; @JsonProperty private List metadata; @@ -83,11 +85,18 @@ public class DmfSoftwareModule { } } + public Boolean getEncrypted() { + return encrypted; + } + + public void setEncrypted(final Boolean encrypted) { + this.encrypted = encrypted; + } + @Override public String toString() { return String.format( - "DmfSoftwareModule [moduleId=%d, moduleType='%s', moduleVersion='%s', artifacts=%s, metadata=%s]", - moduleId, moduleType, moduleVersion, artifacts, metadata); + "DmfSoftwareModule [moduleId=%d, moduleType='%s', moduleVersion='%s', encrypted='%s' artifacts=%s, metadata=%s]", + moduleId, moduleType, moduleVersion, encrypted, artifacts, metadata); } - } diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/ArtifactEncryption.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/ArtifactEncryption.java new file mode 100644 index 000000000..febad31c9 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/ArtifactEncryption.java @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2021 Bosch.IO GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +import java.io.InputStream; +import java.util.Map; +import java.util.Set; + +import org.eclipse.hawkbit.repository.exception.ArtifactEncryptionFailedException; + +/** + * Interface definition for artifact encryption. + * + */ +public interface ArtifactEncryption { + + /** + * Defines the required secret keys for particular encryption algorithm. + * + * @return list of required secret keys + */ + Set requiredSecretKeys(); + + /** + * Generates required secrets key/value pairs. + * + * @return secrets key/value pairs + * @throws ArtifactEncryptionFailedException + */ + Map generateSecrets(); + + /** + * Encrypts artifact stream with provided secrets. + * + * @param secrets + * secrets key/value pairs to be used for encryption + * @param stream + * artifact stream to encrypt + * @return encrypted input stream + * @throws ArtifactEncryptionFailedException + */ + InputStream encryptStream(final Map secrets, final InputStream stream); + + /** + * Decrypts encrypted artifact stream based on provided secrets. + * + * @param secrets + * secrets key/value pairs to be used for decryption + * @param stream + * artifact stream to decrypt + * @return decrypted input stream + * @throws ArtifactEncryptionFailedException + */ + InputStream decryptStream(final Map secrets, final InputStream stream); + + /** + * Size of the underlying encryption algorithm overhead in bytes + * + * @return encryption overhead in byte + */ + int encryptionSizeOverhead(); +} diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/ArtifactEncryptionSecretsStore.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/ArtifactEncryptionSecretsStore.java new file mode 100644 index 000000000..dcbeb1a87 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/ArtifactEncryptionSecretsStore.java @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2021 Bosch.IO GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +import java.util.Optional; + +import org.eclipse.hawkbit.repository.model.SoftwareModule; + +/** + * Interface definition for artifact encryption secrets store. + * + */ +public interface ArtifactEncryptionSecretsStore { + + /** + * Adds secret key/value pair associated with particular + * {@link SoftwareModule} id to the store. + * + * @param softwareModuleId + * {@link SoftwareModule} id associated with the secret + * @param secretKey + * key of the secret + * @param secretValue + * value of the secret + */ + void addSecret(final long softwareModuleId, final String secretKey, final String secretValue); + + /** + * Checks if secret is present for particular {@link SoftwareModule} id and + * key in the store. + * + * @param softwareModuleId + * {@link SoftwareModule} id associated with the secret + * @param secretKey + * key of the secret + */ + boolean secretExists(final long softwareModuleId, final String secretKey); + + /** + * Retrieves secret value associated with particular {@link SoftwareModule} + * id and key from the store. + * + * @param softwareModuleId + * {@link SoftwareModule} id associated with the secret + * @param secretKey + * key of the secret + */ + Optional getSecret(final long softwareModuleId, final String secretKey); + + /** + * Removes secret key/value pair associated with particular + * {@link SoftwareModule} id from the store. + * + * @param softwareModuleId + * {@link SoftwareModule} id associated with the secret + * @param secretKey + * key of the secret + */ + void removeSecret(final long softwareModuleId, final String secretKey); +} diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/ArtifactEncryptionService.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/ArtifactEncryptionService.java new file mode 100644 index 000000000..81b12b629 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/ArtifactEncryptionService.java @@ -0,0 +1,127 @@ +/** + * Copyright (c) 2021 Bosch.IO GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import org.eclipse.hawkbit.repository.exception.ArtifactEncryptionUnsupportedException; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Service responsible for encryption operations. + * + */ +public final class ArtifactEncryptionService { + + private static final ArtifactEncryptionService SINGLETON = new ArtifactEncryptionService(); + + @Autowired(required = false) + private ArtifactEncryption artifactEncryption; + + @Autowired(required = false) + private ArtifactEncryptionSecretsStore artifactEncryptionSecretsStore; + + private ArtifactEncryptionService() { + } + + /** + * @return the artifact encryption service singleton instance + */ + public static ArtifactEncryptionService getInstance() { + return SINGLETON; + } + + /** + * Checks if required encryption and secrets store beans are present. + * + * @return if encryption is supported + */ + public boolean isEncryptionSupported() { + return artifactEncryption != null && artifactEncryptionSecretsStore != null; + } + + /** + * Generates encryption secrets and saves them in secret store by software + * module id reference. + * + * @param smId + * software module id + */ + public void addSoftwareModuleEncryptionSecrets(final long smId) { + if (!isEncryptionSupported()) { + throw new ArtifactEncryptionUnsupportedException("Encryption secrets generation is not supported."); + } + + final Map secrets = artifactEncryption.generateSecrets(); + secrets.forEach((key, value) -> artifactEncryptionSecretsStore.addSecret(smId, key, value)); + // we want to clear secrets from memory as soon as possible + secrets.clear(); + } + + /** + * Encrypts artifact stream using the keys retrieved from secrets store by + * software module id reference. + * + * @param smId + * software module id + * @param artifactStream + * artifact stream to encrypt + * @return encrypted input stream + */ + public InputStream encryptSoftwareModuleArtifact(final long smId, final InputStream artifactStream) { + if (!isEncryptionSupported()) { + throw new ArtifactEncryptionUnsupportedException("Artifact encryption is not supported."); + } + + return artifactEncryption.encryptStream(getSoftwareModuleEncryptionSecrets(smId), artifactStream); + } + + private Map getSoftwareModuleEncryptionSecrets(final long smId) { + final Set requiredSecretsKeys = artifactEncryption.requiredSecretKeys(); + final Map requiredSecrets = new HashMap<>(); + for (final String requiredSecretsKey : requiredSecretsKeys) { + final Optional requiredSecretsValue = artifactEncryptionSecretsStore.getSecret(smId, + requiredSecretsKey); + requiredSecretsValue.ifPresent(secretValue -> requiredSecrets.put(requiredSecretsKey, secretValue)); + } + + return requiredSecrets; + } + + /** + * Decrypts artifact stream using the keys retrieved from secrets store by + * software module id reference. + * + * @param smId + * software module id + * @param encryptedArtifactStream + * artifact stream to decrypt + * @return decrypted input stream + */ + public InputStream decryptSoftwareModuleArtifact(final long smId, final InputStream encryptedArtifactStream) { + if (!isEncryptionSupported()) { + throw new ArtifactEncryptionUnsupportedException("Artifact decryption is not supported."); + } + + return artifactEncryption.decryptStream(getSoftwareModuleEncryptionSecrets(smId), encryptedArtifactStream); + } + + /** + * Size of the underlying encryption algorithm overhead in bytes + * + * @return encryption overhead in byte + */ + public int encryptionSizeOverhead() { + return artifactEncryption.encryptionSizeOverhead(); + } +} diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/ArtifactManagement.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/ArtifactManagement.java index bb0b5f360..2f7adbcb4 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/ArtifactManagement.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/ArtifactManagement.java @@ -15,7 +15,7 @@ import javax.validation.Valid; import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotNull; -import org.eclipse.hawkbit.artifact.repository.model.AbstractDbArtifact; +import org.eclipse.hawkbit.artifact.repository.model.DbArtifact; import org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions; import org.eclipse.hawkbit.repository.exception.ArtifactDeleteFailedException; import org.eclipse.hawkbit.repository.exception.ArtifactUploadFailedException; @@ -69,8 +69,8 @@ public interface ArtifactManagement { /** * Garbage collects artifact binaries if only referenced by given - * {@link SoftwareModule#getId()} or {@link SoftwareModule}'s that are marked - * as deleted. + * {@link SoftwareModule#getId()} or {@link SoftwareModule}'s that are + * marked as deleted. * * * @param artifactSha1Hash @@ -161,15 +161,20 @@ public interface ArtifactManagement { Page findBySoftwareModule(@NotNull Pageable pageReq, long swId); /** - * Loads {@link AbstractDbArtifact} from store for given {@link Artifact}. + * Loads {@link DbArtifact} from store for given {@link Artifact}. * * @param sha1Hash * to search for - * @return loaded {@link AbstractDbArtifact} + * @param softwareModuleId + * software module id. + * @param isEncrypted + * flag to indicate if artifact is encrypted. + * @return loaded {@link DbArtifact} * */ @PreAuthorize(SpringEvalExpressions.HAS_AUTH_DOWNLOAD_ARTIFACT + SpringEvalExpressions.HAS_AUTH_OR + SpringEvalExpressions.IS_CONTROLLER) - Optional loadArtifactBinary(@NotEmpty String sha1Hash); + Optional loadArtifactBinary(@NotEmpty String sha1Hash, long softwareModuleId, + final boolean isEncrypted); } diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/SoftwareModuleCreate.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/SoftwareModuleCreate.java index edba33722..552c41855 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/SoftwareModuleCreate.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/SoftwareModuleCreate.java @@ -70,6 +70,13 @@ public interface SoftwareModuleCreate { return type(Optional.ofNullable(type).map(SoftwareModuleType::getKey).orElse(null)); } + /** + * @param encrypted + * if should be encrypted + * @return updated builder instance + */ + SoftwareModuleCreate encrypted(boolean encrypted); + /** * @return peek on current state of {@link SoftwareModule} in the builder */ diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/ArtifactEncryptionFailedException.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/ArtifactEncryptionFailedException.java new file mode 100644 index 000000000..df2e1b04f --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/ArtifactEncryptionFailedException.java @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2021 Bosch.IO GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.exception; + +import org.eclipse.hawkbit.exception.AbstractServerRtException; +import org.eclipse.hawkbit.exception.SpServerError; + +/** + * Exception being thrown in case of error while generating encryption secrets, + * encrypting or decrypting artifacts. + */ +public final class ArtifactEncryptionFailedException extends AbstractServerRtException { + + private static final long serialVersionUID = 1L; + + private final EncryptionOperation encryptionOperation; + + public ArtifactEncryptionFailedException(final EncryptionOperation encryptionOperation) { + this(encryptionOperation, null, null); + } + + public ArtifactEncryptionFailedException(final EncryptionOperation encryptionOperation, final String message) { + this(encryptionOperation, message, null); + } + + public ArtifactEncryptionFailedException(final EncryptionOperation encryptionOperation, final String message, + final Throwable cause) { + super(message, SpServerError.SP_ARTIFACT_ENCRYPTION_FAILED, cause); + this.encryptionOperation = encryptionOperation; + } + + public EncryptionOperation getEncryptionOperation() { + return encryptionOperation; + } + + /** + * Encryption operation that caused the exception. + */ + public enum EncryptionOperation { + GENERATE_SECRETS, ENCRYPT, DECRYPT; + } + +} diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/ArtifactEncryptionUnsupportedException.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/ArtifactEncryptionUnsupportedException.java new file mode 100644 index 000000000..30a2caa6a --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/ArtifactEncryptionUnsupportedException.java @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2021 Bosch.IO GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.exception; + +import org.eclipse.hawkbit.exception.AbstractServerRtException; +import org.eclipse.hawkbit.exception.SpServerError; + +/** + * Exception being thrown when artifact encryption is not supported + */ +public final class ArtifactEncryptionUnsupportedException extends AbstractServerRtException { + + private static final long serialVersionUID = 1L; + + /** + * Constructor. + */ + public ArtifactEncryptionUnsupportedException() { + super(SpServerError.SP_ARTIFACT_ENCRYPTION_NOT_SUPPORTED); + } + + /** + * @param message + * of the error + */ + public ArtifactEncryptionUnsupportedException(final String message) { + super(message, SpServerError.SP_ARTIFACT_ENCRYPTION_NOT_SUPPORTED); + } +} diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/SoftwareModule.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/SoftwareModule.java index 8ab2ecfa8..e00086a2d 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/SoftwareModule.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/SoftwareModule.java @@ -67,4 +67,9 @@ public interface SoftwareModule extends NamedVersionedEntity { */ List getAssignedTo(); + /** + * @return {@code true} if this software module is marked as encrypted + * otherwise {@code false} + */ + boolean isEncrypted(); } diff --git a/hawkbit-repository/hawkbit-repository-api/src/test/java/org/eclipse/hawkbit/repository/ArtifactEncryptionServiceTest.java b/hawkbit-repository/hawkbit-repository-api/src/test/java/org/eclipse/hawkbit/repository/ArtifactEncryptionServiceTest.java new file mode 100644 index 000000000..682dca590 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-api/src/test/java/org/eclipse/hawkbit/repository/ArtifactEncryptionServiceTest.java @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2021 Bosch.IO GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +import org.eclipse.hawkbit.repository.exception.ArtifactEncryptionUnsupportedException; +import org.junit.jupiter.api.Test; + +import io.qameta.allure.Description; +import io.qameta.allure.Feature; +import io.qameta.allure.Story; + +/** + * Test class to verify that no {@link ArtifactEncryptionService} required beans + * are loaded and therefore the encryption support is not given. + */ +@Feature("Unit Tests - Repository") +@Story("Artifact Encryption Service") +class ArtifactEncryptionServiceTest { + + @Test + @Description("Verify that no artifact encryption support is given") + void verifyNoArtifactEncryptionSupport() { + final ArtifactEncryptionService artifactEncryptionService = ArtifactEncryptionService.getInstance(); + + assertThat(artifactEncryptionService.isEncryptionSupported()).isFalse(); + assertThatExceptionOfType(ArtifactEncryptionUnsupportedException.class) + .isThrownBy(() -> artifactEncryptionService.addSoftwareModuleEncryptionSecrets(1L)); + assertThatExceptionOfType(ArtifactEncryptionUnsupportedException.class) + .isThrownBy(() -> artifactEncryptionService.encryptSoftwareModuleArtifact(1L, null)); + assertThatExceptionOfType(ArtifactEncryptionUnsupportedException.class) + .isThrownBy(() -> artifactEncryptionService.decryptSoftwareModuleArtifact(1L, null)); + } +} diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/EncryptionAwareDbArtifact.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/EncryptionAwareDbArtifact.java new file mode 100644 index 000000000..0c41dd5b1 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/EncryptionAwareDbArtifact.java @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2021 Bosch.IO GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.jpa; + +import java.io.InputStream; +import java.util.function.UnaryOperator; + +import org.eclipse.hawkbit.artifact.repository.model.DbArtifact; +import org.eclipse.hawkbit.artifact.repository.model.DbArtifactHash; + +/** + * {@link DbArtifact} implementation that decrypts the underlying artifact + * binary input stream. + */ +public class EncryptionAwareDbArtifact implements DbArtifact { + + private final DbArtifact encryptedDbArtifact; + private final UnaryOperator decryptionFunction; + private final int encryptionOverhead; + + public EncryptionAwareDbArtifact(final DbArtifact encryptedDbArtifact, + final UnaryOperator decryptionFunction) { + this.encryptedDbArtifact = encryptedDbArtifact; + this.decryptionFunction = decryptionFunction; + this.encryptionOverhead = 0; + } + + public EncryptionAwareDbArtifact(final DbArtifact encryptedDbArtifact, + final UnaryOperator decryptionFunction, final int encryptionOverhead) { + this.encryptedDbArtifact = encryptedDbArtifact; + this.decryptionFunction = decryptionFunction; + this.encryptionOverhead = encryptionOverhead; + } + + @Override + public String getArtifactId() { + return encryptedDbArtifact.getArtifactId(); + } + + @Override + public DbArtifactHash getHashes() { + return encryptedDbArtifact.getHashes(); + } + + @Override + public long getSize() { + return encryptedDbArtifact.getSize() - encryptionOverhead; + } + + @Override + public String getContentType() { + return encryptedDbArtifact.getContentType(); + } + + @Override + public InputStream getFileInputStream() { + return decryptionFunction.apply(encryptedDbArtifact.getFileInputStream()); + } +} diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaArtifactManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaArtifactManagement.java index fcbd25d5d..68e0b36a0 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaArtifactManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaArtifactManagement.java @@ -16,7 +16,9 @@ import org.eclipse.hawkbit.artifact.repository.ArtifactRepository; import org.eclipse.hawkbit.artifact.repository.ArtifactStoreException; import org.eclipse.hawkbit.artifact.repository.HashNotMatchException; import org.eclipse.hawkbit.artifact.repository.model.AbstractDbArtifact; +import org.eclipse.hawkbit.artifact.repository.model.DbArtifact; import org.eclipse.hawkbit.artifact.repository.model.DbArtifactHash; +import org.eclipse.hawkbit.repository.ArtifactEncryptionService; import org.eclipse.hawkbit.repository.ArtifactManagement; import org.eclipse.hawkbit.repository.QuotaManagement; import org.eclipse.hawkbit.repository.exception.ArtifactDeleteFailedException; @@ -104,15 +106,24 @@ public class JpaArtifactManagement implements ArtifactManagement { assertArtifactQuota(moduleId, 1); - final AbstractDbArtifact artifact = storeArtifact(artifactUpload); + final AbstractDbArtifact artifact = storeArtifact(artifactUpload, softwareModule.isEncrypted()); return storeArtifactMetadata(softwareModule, filename, artifact, existing); } - private AbstractDbArtifact storeArtifact(final ArtifactUpload artifactUpload) { - try (final InputStream quotaStream = wrapInQuotaStream(artifactUpload.getInputStream())) { - return artifactRepository.store(tenantAware.getCurrentTenant(), quotaStream, artifactUpload.getFilename(), - artifactUpload.getContentType(), new DbArtifactHash(artifactUpload.getProvidedSha1Sum(), - artifactUpload.getProvidedMd5Sum(), artifactUpload.getProvidedSha256Sum())); + private AbstractDbArtifact storeArtifact(final ArtifactUpload artifactUpload, final boolean isSmEncrypted) { + final String tenant = tenantAware.getCurrentTenant(); + final long smId = artifactUpload.getModuleId(); + final InputStream stream = artifactUpload.getInputStream(); + final String fileName = artifactUpload.getFilename(); + final String contentType = artifactUpload.getContentType(); + final String providedSha1 = artifactUpload.getProvidedSha1Sum(); + final String providedMd5 = artifactUpload.getProvidedMd5Sum(); + final String providedSha256 = artifactUpload.getProvidedSha256Sum(); + + try (final InputStream wrappedStream = wrapInQuotaStream( + isSmEncrypted ? wrapInEncryptionStream(smId, stream) : stream)) { + return artifactRepository.store(tenant, wrappedStream, fileName, contentType, + new DbArtifactHash(providedSha1, providedMd5, providedSha256)); } catch (final ArtifactStoreException | IOException e) { throw new ArtifactUploadFailedException(e); } catch (final HashNotMatchException e) { @@ -126,6 +137,10 @@ public class JpaArtifactManagement implements ArtifactManagement { } } + private InputStream wrapInEncryptionStream(final long smId, final InputStream stream) { + return ArtifactEncryptionService.getInstance().encryptSoftwareModuleArtifact(smId, stream); + } + private void assertArtifactQuota(final long id, final int requested) { QuotaHelper.assertAssignmentQuota(id, requested, quotaManagement.getMaxArtifactsPerSoftwareModule(), Artifact.class, SoftwareModule.class, localArtifactRepository::countBySoftwareModuleId); @@ -212,10 +227,26 @@ public class JpaArtifactManagement implements ArtifactManagement { } @Override - public Optional loadArtifactBinary(final String sha1Hash) { - return Optional.ofNullable(artifactRepository.existsByTenantAndSha1(tenantAware.getCurrentTenant(), sha1Hash) - ? artifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), sha1Hash) - : null); + public Optional loadArtifactBinary(final String sha1Hash, final long softwareModuleId, + final boolean isEncrypted) { + final String tenant = tenantAware.getCurrentTenant(); + if (artifactRepository.existsByTenantAndSha1(tenant, sha1Hash)) { + final DbArtifact dbArtifact = artifactRepository.getArtifactBySha1(tenant, sha1Hash); + return Optional.ofNullable( + isEncrypted ? wrapInEncryptionAwareDbArtifact(softwareModuleId, dbArtifact) : dbArtifact); + } + + return Optional.empty(); + } + + private final DbArtifact wrapInEncryptionAwareDbArtifact(final long smId, final DbArtifact dbArtifact) { + if (dbArtifact == null) { + return null; + } + final ArtifactEncryptionService encryptionService = ArtifactEncryptionService.getInstance(); + return new EncryptionAwareDbArtifact(dbArtifact, + stream -> encryptionService.decryptSoftwareModuleArtifact(smId, stream), + encryptionService.encryptionSizeOverhead()); } private Artifact storeArtifactMetadata(final SoftwareModule softwareModule, final String providedFilename, diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaSoftwareModuleManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaSoftwareModuleManagement.java index 1352c675f..93a755f18 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaSoftwareModuleManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaSoftwareModuleManagement.java @@ -31,6 +31,7 @@ import javax.persistence.criteria.Predicate; import javax.persistence.criteria.Root; import org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions; +import org.eclipse.hawkbit.repository.ArtifactEncryptionService; import org.eclipse.hawkbit.repository.ArtifactManagement; import org.eclipse.hawkbit.repository.QuotaManagement; import org.eclipse.hawkbit.repository.RepositoryConstants; @@ -155,7 +156,14 @@ public class JpaSoftwareModuleManagement implements SoftwareModuleManagement { public SoftwareModule create(final SoftwareModuleCreate c) { final JpaSoftwareModuleCreate create = (JpaSoftwareModuleCreate) c; - return softwareModuleRepository.save(create.build()); + final JpaSoftwareModule sm = softwareModuleRepository.save(create.build()); + if (create.isEncrypted()) { + // flush sm creation in order to get an Id + entityManager.flush(); + ArtifactEncryptionService.getInstance().addSoftwareModuleEncryptionSecrets(sm.getId()); + } + + return sm; } @Override @@ -295,8 +303,8 @@ public class JpaSoftwareModuleManagement implements SoftwareModuleManagement { @Override public Page findByRsql(final Pageable pageable, final String rsqlParam) { - final Specification spec = RSQLUtility.buildRsqlSpecification(rsqlParam, SoftwareModuleFields.class, - virtualPropertyReplacer, database); + final Specification spec = RSQLUtility.buildRsqlSpecification(rsqlParam, + SoftwareModuleFields.class, virtualPropertyReplacer, database); return convertSmPage(softwareModuleRepository.findAll(spec, pageable), pageable); } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RepositoryApplicationConfiguration.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RepositoryApplicationConfiguration.java index 66ae45bf4..5a887bac2 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RepositoryApplicationConfiguration.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RepositoryApplicationConfiguration.java @@ -16,6 +16,9 @@ import javax.persistence.EntityManager; import javax.sql.DataSource; import org.eclipse.hawkbit.artifact.repository.ArtifactRepository; +import org.eclipse.hawkbit.repository.ArtifactEncryption; +import org.eclipse.hawkbit.repository.ArtifactEncryptionSecretsStore; +import org.eclipse.hawkbit.repository.ArtifactEncryptionService; import org.eclipse.hawkbit.repository.ArtifactManagement; import org.eclipse.hawkbit.repository.BaseRepositoryTypeProvider; import org.eclipse.hawkbit.repository.ControllerManagement; @@ -245,7 +248,8 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { /** * @param dsTypeManagement - * for loading {@link TargetType#getCompatibleDistributionSetTypes()} + * for loading + * {@link TargetType#getCompatibleDistributionSetTypes()} * @return TargetTypeBuilder bean */ @Bean @@ -261,8 +265,9 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { /** * @param softwareManagement - * for loading {@link DistributionSetType#getMandatoryModuleTypes()} - * and {@link DistributionSetType#getOptionalModuleTypes()} + * for loading + * {@link DistributionSetType#getMandatoryModuleTypes()} and + * {@link DistributionSetType#getOptionalModuleTypes()} * @return DistributionSetTypeBuilder bean */ @Bean @@ -304,8 +309,8 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { /** * @return the {@link SystemSecurityContext} singleton bean which make it - * accessible in beans which cannot access the service directly, e.g. - * JPA entities. + * accessible in beans which cannot access the service directly, + * e.g. JPA entities. */ @Bean SystemSecurityContextHolder systemSecurityContextHolder() { @@ -313,9 +318,9 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { } /** - * @return the {@link TenantConfigurationManagement} singleton bean which make - * it accessible in beans which cannot access the service directly, e.g. - * JPA entities. + * @return the {@link TenantConfigurationManagement} singleton bean which + * make it accessible in beans which cannot access the service + * directly, e.g. JPA entities. */ @Bean TenantConfigurationManagementHolder tenantConfigurationManagementHolder() { @@ -324,8 +329,9 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { /** * @return the {@link SystemManagementHolder} singleton bean which holds the - * current {@link SystemManagement} service and make it accessible in - * beans which cannot access the service directly, e.g. JPA entities. + * current {@link SystemManagement} service and make it accessible + * in beans which cannot access the service directly, e.g. JPA + * entities. */ @Bean SystemManagementHolder systemManagementHolder() { @@ -333,9 +339,10 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { } /** - * @return the {@link TenantAwareHolder} singleton bean which holds the current - * {@link TenantAware} service and make it accessible in beans which - * cannot access the service directly, e.g. JPA entities. + * @return the {@link TenantAwareHolder} singleton bean which holds the + * current {@link TenantAware} service and make it accessible in + * beans which cannot access the service directly, e.g. JPA + * entities. */ @Bean TenantAwareHolder tenantAwareHolder() { @@ -343,9 +350,10 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { } /** - * @return the {@link SecurityTokenGeneratorHolder} singleton bean which holds - * the current {@link SecurityTokenGenerator} service and make it - * accessible in beans which cannot access the service via injection + * @return the {@link SecurityTokenGeneratorHolder} singleton bean which + * holds the current {@link SecurityTokenGenerator} service and make + * it accessible in beans which cannot access the service via + * injection */ @Bean SecurityTokenGeneratorHolder securityTokenGeneratorHolder() { @@ -940,7 +948,8 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { } /** - * Our default {@link BaseRepositoryTypeProvider} bean always provides the NoCountBaseRepository + * Our default {@link BaseRepositoryTypeProvider} bean always provides the + * NoCountBaseRepository * * @return a {@link BaseRepositoryTypeProvider} bean */ @@ -950,4 +959,17 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { return new NoCountBaseRepositoryTypeProvider(); } + + /** + * Default artifact encryption service bean that internally uses + * {@link ArtifactEncryption} and {@link ArtifactEncryptionSecretsStore} + * beans for {@link SoftwareModule} artifacts encryption/decryption + * + * @return a {@link ArtifactEncryptionService} bean + */ + @Bean + @ConditionalOnMissingBean + ArtifactEncryptionService artifactEncryptionService() { + return ArtifactEncryptionService.getInstance(); + } } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/builder/JpaSoftwareModuleCreate.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/builder/JpaSoftwareModuleCreate.java index 964aee9bc..377e1cd36 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/builder/JpaSoftwareModuleCreate.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/builder/JpaSoftwareModuleCreate.java @@ -26,13 +26,26 @@ public class JpaSoftwareModuleCreate extends AbstractSoftwareModuleUpdateCreate< private final SoftwareModuleTypeManagement softwareModuleTypeManagement; + private boolean encrypted; + JpaSoftwareModuleCreate(final SoftwareModuleTypeManagement softwareModuleTypeManagement) { this.softwareModuleTypeManagement = softwareModuleTypeManagement; } + @Override + public SoftwareModuleCreate encrypted(final boolean encrypted) { + this.encrypted = encrypted; + return this; + } + + public boolean isEncrypted() { + return encrypted; + } + @Override public JpaSoftwareModule build() { - return new JpaSoftwareModule(getSoftwareModuleTypeFromKeyString(type), name, version, description, vendor); + return new JpaSoftwareModule(getSoftwareModuleTypeFromKeyString(type), name, version, description, vendor, + encrypted); } private SoftwareModuleType getSoftwareModuleTypeFromKeyString(final String type) { 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 33622a14a..c3f172304 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 @@ -90,6 +90,9 @@ public class JpaSoftwareModule extends AbstractJpaNamedVersionedEntity implement @OneToMany(mappedBy = "softwareModule", fetch = FetchType.LAZY, targetEntity = JpaSoftwareModuleMetadata.class) private List metadata; + @Column(name = "encrypted") + private boolean encrypted; + /** * Default constructor. */ @@ -97,6 +100,20 @@ public class JpaSoftwareModule extends AbstractJpaNamedVersionedEntity implement // Default constructor for JPA } + /** + * parameterized constructor. + * + * @param type + * of the {@link SoftwareModule} + * @param name + * abstract name of the {@link SoftwareModule} + * @param version + * of the {@link SoftwareModule} + */ + public JpaSoftwareModule(final SoftwareModuleType type, final String name, final String version) { + this(type, name, version, null, null, false); + } + /** * parameterized constructor. * @@ -110,12 +127,15 @@ public class JpaSoftwareModule extends AbstractJpaNamedVersionedEntity implement * of the {@link SoftwareModule} * @param vendor * of the {@link SoftwareModule} + * @param encrypted + * encryption flag of the {@link SoftwareModule} */ public JpaSoftwareModule(final SoftwareModuleType type, final String name, final String version, - final String description, final String vendor) { + final String description, final String vendor, final boolean encrypted) { super(name, version, description); this.vendor = vendor; this.type = (JpaSoftwareModuleType) type; + this.encrypted = encrypted; } public void addArtifact(final Artifact artifact) { @@ -175,8 +195,8 @@ public class JpaSoftwareModule extends AbstractJpaNamedVersionedEntity implement * Marks or un-marks this software module as deleted. * * @param deleted - * {@code true} if the software module should be marked as - * deleted otherwise {@code false} + * {@code true} if the software module should be marked as deleted + * otherwise {@code false} */ public void setDeleted(final boolean deleted) { this.deleted = deleted; @@ -186,10 +206,27 @@ public class JpaSoftwareModule extends AbstractJpaNamedVersionedEntity implement this.type = type; } + @Override + public boolean isEncrypted() { + return encrypted; + } + + /** + * Marks this software module as encrypted. + * + * @param encrypted + * {@code true} if the software module should be marked as encrypted + * otherwise {@code false} + */ + public void setEncrypted(final boolean encrypted) { + this.encrypted = encrypted; + } + @Override public String toString() { - return "SoftwareModule [deleted=" + deleted + ", name=" + getName() + ", version=" + getVersion() - + ", revision=" + getOptLockRevision() + ", Id=" + getId() + ", type=" + getType().getName() + "]"; + return "SoftwareModule [deleted=" + isDeleted() + ", encrypted=" + isEncrypted() + ", name=" + getName() + + ", version=" + getVersion() + ", revision=" + getOptLockRevision() + ", Id=" + getId() + ", type=" + + getType().getName() + "]"; } @Override diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/DB2/V1_12_20__add_encryption_flag_to_sm___DB2.sql b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/DB2/V1_12_20__add_encryption_flag_to_sm___DB2.sql new file mode 100644 index 000000000..9249d14a1 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/DB2/V1_12_20__add_encryption_flag_to_sm___DB2.sql @@ -0,0 +1,3 @@ +ALTER TABLE sp_base_software_module ADD COLUMN encrypted BOOLEAN; + +UPDATE sp_base_software_module SET encrypted = 0; \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/H2/V1_12_20__add_encryption_flag_to_sm___H2.sql b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/H2/V1_12_20__add_encryption_flag_to_sm___H2.sql new file mode 100644 index 000000000..9249d14a1 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/H2/V1_12_20__add_encryption_flag_to_sm___H2.sql @@ -0,0 +1,3 @@ +ALTER TABLE sp_base_software_module ADD COLUMN encrypted BOOLEAN; + +UPDATE sp_base_software_module SET encrypted = 0; \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/MYSQL/V1_12_20__add_encryption_flag_to_sm___MYSQL.sql b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/MYSQL/V1_12_20__add_encryption_flag_to_sm___MYSQL.sql new file mode 100644 index 000000000..9249d14a1 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/MYSQL/V1_12_20__add_encryption_flag_to_sm___MYSQL.sql @@ -0,0 +1,3 @@ +ALTER TABLE sp_base_software_module ADD COLUMN encrypted BOOLEAN; + +UPDATE sp_base_software_module SET encrypted = 0; \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/POSTGRESQL/V1_12_20__add_encryption_flag_to_sm___POSTGRESQL.sql b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/POSTGRESQL/V1_12_20__add_encryption_flag_to_sm___POSTGRESQL.sql new file mode 100644 index 000000000..9249d14a1 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/POSTGRESQL/V1_12_20__add_encryption_flag_to_sm___POSTGRESQL.sql @@ -0,0 +1,3 @@ +ALTER TABLE sp_base_software_module ADD COLUMN encrypted BOOLEAN; + +UPDATE sp_base_software_module SET encrypted = 0; \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/SQL_SERVER/V1_12_19__add_valid_flag_to_ds___SQL_SERVER.sql b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/SQL_SERVER/V1_12_19__add_valid_flag_to_ds___SQL_SERVER.sql index 3ab0e7502..1ec1d0407 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/SQL_SERVER/V1_12_19__add_valid_flag_to_ds___SQL_SERVER.sql +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/SQL_SERVER/V1_12_19__add_valid_flag_to_ds___SQL_SERVER.sql @@ -1,3 +1 @@ -ALTER TABLE sp_distribution_set ADD COLUMN valid BOOLEAN; - -UPDATE sp_distribution_set SET valid = 1; \ No newline at end of file +ALTER TABLE sp_distribution_set ADD valid BIT DEFAULT 1; \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/SQL_SERVER/V1_12_20__add_encryption_flag_to_sm___SQL_SERVER.sql b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/SQL_SERVER/V1_12_20__add_encryption_flag_to_sm___SQL_SERVER.sql new file mode 100644 index 000000000..092fdbdcc --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/SQL_SERVER/V1_12_20__add_encryption_flag_to_sm___SQL_SERVER.sql @@ -0,0 +1 @@ +ALTER TABLE sp_base_software_module ADD encrypted BIT DEFAULT 0; \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/ArtifactManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/ArtifactManagementTest.java index b92565d71..c8b945f62 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/ArtifactManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/ArtifactManagementTest.java @@ -26,7 +26,7 @@ import javax.validation.ConstraintViolationException; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.RandomStringUtils; -import org.eclipse.hawkbit.artifact.repository.model.AbstractDbArtifact; +import org.eclipse.hawkbit.artifact.repository.model.DbArtifact; import org.eclipse.hawkbit.artifact.repository.model.DbArtifactHash; import org.eclipse.hawkbit.im.authentication.SpPermission; import org.eclipse.hawkbit.repository.ArtifactManagement; @@ -77,7 +77,8 @@ public class ArtifactManagementTest extends AbstractJpaIntegrationTest { .isFalse(); assertThat(artifactManagement.findFirstBySHA1(NOT_EXIST_ID)).isNotPresent(); - assertThat(artifactManagement.loadArtifactBinary(NOT_EXIST_ID)).isNotPresent(); + assertThat(artifactManagement.loadArtifactBinary(NOT_EXIST_ID, module.getId(), module.isEncrypted())) + .isNotPresent(); } @Test @@ -116,10 +117,10 @@ public class ArtifactManagementTest extends AbstractJpaIntegrationTest { assertThat(artifactRepository.findAll()).hasSize(0); final JpaSoftwareModule sm = softwareModuleRepository - .save(new JpaSoftwareModule(osType, "name 1", "version 1", null, null)); + .save(new JpaSoftwareModule(osType, "name 1", "version 1")); final JpaSoftwareModule sm2 = softwareModuleRepository - .save(new JpaSoftwareModule(osType, "name 2", "version 2", null, null)); - softwareModuleRepository.save(new JpaSoftwareModule(osType, "name 3", "version 3", null, null)); + .save(new JpaSoftwareModule(osType, "name 2", "version 2")); + softwareModuleRepository.save(new JpaSoftwareModule(osType, "name 3", "version 3")); final int artifactSize = 5 * 1024; final byte[] randomBytes = randomBytes(artifactSize); @@ -165,8 +166,8 @@ public class ArtifactManagementTest extends AbstractJpaIntegrationTest { final String artifactData = "test"; final int artifactSize = artifactData.length(); - final long smID = softwareModuleRepository - .save(new JpaSoftwareModule(osType, "smIllegalFilenameTest", "1.0", null, null)).getId(); + final long smID = softwareModuleRepository.save(new JpaSoftwareModule(osType, "smIllegalFilenameTest", "1.0")) + .getId(); assertThatExceptionOfType(ConstraintViolationException.class).isThrownBy( () -> artifactManagement.create(new ArtifactUpload(IOUtils.toInputStream(artifactData, "UTF-8"), smID, illegalFilename, false, artifactSize))); @@ -178,8 +179,7 @@ public class ArtifactManagementTest extends AbstractJpaIntegrationTest { public void createArtifactsUntilQuotaIsExceeded() throws IOException { // create a software module - final long smId = softwareModuleRepository.save(new JpaSoftwareModule(osType, "sm1", "1.0", null, null)) - .getId(); + final long smId = softwareModuleRepository.save(new JpaSoftwareModule(osType, "sm1", "1.0")).getId(); // now create artifacts for this module until the quota is exceeded final long maxArtifacts = quotaManagement.getMaxArtifactsPerSoftwareModule(); @@ -217,14 +217,13 @@ public class ArtifactManagementTest extends AbstractJpaIntegrationTest { final int artifactSize = Math.toIntExact(quotaManagement.getMaxArtifactSize() / 10); final int numArtifacts = Math.toIntExact(maxBytes / artifactSize); for (int i = 0; i < numArtifacts; ++i) { - final JpaSoftwareModule sm = softwareModuleRepository - .save(new JpaSoftwareModule(osType, "smd" + i, "1.0", null, null)); + final JpaSoftwareModule sm = softwareModuleRepository.save(new JpaSoftwareModule(osType, "smd" + i, "1.0")); artifactIds.add(createArtifactForSoftwareModule("file" + i, sm.getId(), artifactSize).getId()); } // upload one more artifact to trigger the quota exceeded error final JpaSoftwareModule sm = softwareModuleRepository - .save(new JpaSoftwareModule(osType, "smd" + numArtifacts, "1.0", null, null)); + .save(new JpaSoftwareModule(osType, "smd" + numArtifacts, "1.0")); assertThatExceptionOfType(StorageQuotaExceededException.class) .isThrownBy(() -> createArtifactForSoftwareModule("file" + numArtifacts, sm.getId(), artifactSize)); @@ -240,8 +239,7 @@ public class ArtifactManagementTest extends AbstractJpaIntegrationTest { public void createArtifactFailsIfTooLarge() { // create a software module - final JpaSoftwareModule sm1 = softwareModuleRepository - .save(new JpaSoftwareModule(osType, "sm1", "1.0", null, null)); + final JpaSoftwareModule sm1 = softwareModuleRepository.save(new JpaSoftwareModule(osType, "sm1", "1.0")); // create an artifact that exceeds the configured quota final long maxSize = quotaManagement.getMaxArtifactSize(); @@ -254,7 +252,7 @@ public class ArtifactManagementTest extends AbstractJpaIntegrationTest { public void hardDeleteSoftwareModule() throws IOException { final JpaSoftwareModule sm = softwareModuleRepository - .save(new JpaSoftwareModule(osType, "name 1", "version 1", null, null)); + .save(new JpaSoftwareModule(osType, "name 1", "version 1")); createArtifactForSoftwareModule("file1", sm.getId(), 5 * 1024); assertThat(artifactRepository.findAll()).hasSize(1); @@ -273,9 +271,9 @@ public class ArtifactManagementTest extends AbstractJpaIntegrationTest { @Description("Tests the deletion of a local artifact including metadata.") public void deleteArtifact() throws IOException { final JpaSoftwareModule sm = softwareModuleRepository - .save(new JpaSoftwareModule(osType, "name 1", "version 1", null, null)); + .save(new JpaSoftwareModule(osType, "name 1", "version 1")); final JpaSoftwareModule sm2 = softwareModuleRepository - .save(new JpaSoftwareModule(osType, "name 2", "version 2", null, null)); + .save(new JpaSoftwareModule(osType, "name 2", "version 2")); assertThat(artifactRepository.findAll()).isEmpty(); @@ -326,9 +324,9 @@ public class ArtifactManagementTest extends AbstractJpaIntegrationTest { public void deleteDuplicateArtifacts() throws IOException { final JpaSoftwareModule sm = softwareModuleRepository - .save(new JpaSoftwareModule(osType, "name 1", "version 1", null, null)); + .save(new JpaSoftwareModule(osType, "name 1", "version 1")); final JpaSoftwareModule sm2 = softwareModuleRepository - .save(new JpaSoftwareModule(osType, "name 2", "version 2", null, null)); + .save(new JpaSoftwareModule(osType, "name 2", "version 2")); final int artifactSize = 5 * 1024; final byte[] randomBytes = randomBytes(artifactSize); @@ -368,9 +366,9 @@ public class ArtifactManagementTest extends AbstractJpaIntegrationTest { public void deleteArtifactWithSameHashAndSoftwareModuleIsNotDeletedInSameTenants() throws IOException { final JpaSoftwareModule sm = softwareModuleRepository - .save(new JpaSoftwareModule(osType, "name 1", "version 1", null, null)); + .save(new JpaSoftwareModule(osType, "name 1", "version 1")); final JpaSoftwareModule sm2 = softwareModuleRepository - .save(new JpaSoftwareModule(osType, "name 2", "version 2", null, null)); + .save(new JpaSoftwareModule(osType, "name 2", "version 2")); final int artifactSize = 5 * 1024; final byte[] randomBytes = randomBytes(artifactSize); @@ -382,7 +380,9 @@ public class ArtifactManagementTest extends AbstractJpaIntegrationTest { final Artifact artifact2 = createArtifactForSoftwareModule("file2", sm2.getId(), artifactSize, inputStream2); - assertEqualFileContents(artifactManagement.loadArtifactBinary(artifact2.getSha1Hash()), randomBytes); + assertEqualFileContents( + artifactManagement.loadArtifactBinary(artifact2.getSha1Hash(), sm2.getId(), sm2.isEncrypted()), + randomBytes); assertThat(artifactRepository.findAll()).hasSize(2); @@ -447,9 +447,11 @@ public class ArtifactManagementTest extends AbstractJpaIntegrationTest { final int artifactSize = 5 * 1024; final byte[] randomBytes = randomBytes(artifactSize); try (final InputStream input = new ByteArrayInputStream(randomBytes)) { - final Artifact artifact = createArtifactForSoftwareModule("file1", - testdataFactory.createSoftwareModuleOs().getId(), artifactSize, input); - assertEqualFileContents(artifactManagement.loadArtifactBinary(artifact.getSha1Hash()), randomBytes); + final SoftwareModule smOs = testdataFactory.createSoftwareModuleOs(); + final Artifact artifact = createArtifactForSoftwareModule("file1", smOs.getId(), artifactSize, input); + assertEqualFileContents( + artifactManagement.loadArtifactBinary(artifact.getSha1Hash(), smOs.getId(), smOs.isEncrypted()), + randomBytes); } } @@ -459,7 +461,7 @@ public class ArtifactManagementTest extends AbstractJpaIntegrationTest { public void loadArtifactBinaryWithoutDownloadArtifactThrowsPermissionDenied() { assertThatExceptionOfType(InsufficientPermissionException.class) .as("Should not have worked with missing permission.") - .isThrownBy(() -> artifactManagement.loadArtifactBinary("123")); + .isThrownBy(() -> artifactManagement.loadArtifactBinary("123", 1, false)); } @Test @@ -541,7 +543,8 @@ public class ArtifactManagementTest extends AbstractJpaIntegrationTest { assertThat(createdArtifact.getSha256Hash()).isEqualTo(artifactHashes.getSha256()); } - final Optional dbArtifact = artifactManagement.loadArtifactBinary(artifactHashes.getSha1()); + final Optional dbArtifact = artifactManagement.loadArtifactBinary(artifactHashes.getSha1(), + sm.getId(), sm.isEncrypted()); assertThat(dbArtifact).isPresent(); } @@ -561,7 +564,8 @@ public class ArtifactManagementTest extends AbstractJpaIntegrationTest { final Artifact artifact = artifactManagement.create(artifactUpload); assertThat(artifact).isNotNull(); } - final Optional dbArtifact = artifactManagement.loadArtifactBinary(artifactHashes.getSha1()); + final Optional dbArtifact = artifactManagement.loadArtifactBinary(artifactHashes.getSha1(), + smOs.getId(), smOs.isEncrypted()); assertThat(dbArtifact).isPresent(); try (final InputStream inputStream = new ByteArrayInputStream(testData)) { @@ -622,10 +626,9 @@ public class ArtifactManagementTest extends AbstractJpaIntegrationTest { assertThat(runAsTenant(tenant, () -> artifactRepository.findAll())).hasSize(count); } - private void assertEqualFileContents(final Optional artifact, final byte[] randomBytes) + private void assertEqualFileContents(final Optional artifact, final byte[] randomBytes) throws IOException { - try (final InputStream inputStream = artifactManagement.loadArtifactBinary(artifact.get().getHashes().getSha1()) - .get().getFileInputStream()) { + try (final InputStream inputStream = artifact.get().getFileInputStream()) { assertTrue(IOUtils.contentEquals(new ByteArrayInputStream(randomBytes), inputStream), "The stored binary matches the given binary"); } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/SoftwareModuleManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/SoftwareModuleManagementTest.java index ed93502e6..7713f030c 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/SoftwareModuleManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/SoftwareModuleManagementTest.java @@ -520,13 +520,13 @@ public class SoftwareModuleManagementTest extends AbstractJpaIntegrationTest { Arrays.asList(testType.getId())); // found in test - final SoftwareModule unassigned = testdataFactory.createSoftwareModule("thetype", "unassignedfound"); - final SoftwareModule one = testdataFactory.createSoftwareModule("thetype", "bfound"); - final SoftwareModule two = testdataFactory.createSoftwareModule("thetype", "cfound"); - final SoftwareModule differentName = testdataFactory.createSoftwareModule("thetype", "a"); + final SoftwareModule unassigned = testdataFactory.createSoftwareModule("thetype", "unassignedfound", false); + final SoftwareModule one = testdataFactory.createSoftwareModule("thetype", "bfound", false); + final SoftwareModule two = testdataFactory.createSoftwareModule("thetype", "cfound", false); + final SoftwareModule differentName = testdataFactory.createSoftwareModule("thetype", "a", false); // ignored - final SoftwareModule deleted = testdataFactory.createSoftwareModule("thetype", "deleted"); + final SoftwareModule deleted = testdataFactory.createSoftwareModule("thetype", "deleted", false); final SoftwareModule four = testdataFactory.createSoftwareModuleOs("e"); final DistributionSet set = distributionSetManagement @@ -572,13 +572,13 @@ public class SoftwareModuleManagementTest extends AbstractJpaIntegrationTest { Arrays.asList(testType.getId())); // found in test - testdataFactory.createSoftwareModule("thetype", "unassignedfound"); - final SoftwareModule one = testdataFactory.createSoftwareModule("thetype", "bfound"); - final SoftwareModule two = testdataFactory.createSoftwareModule("thetype", "cfound"); - final SoftwareModule differentName = testdataFactory.createSoftwareModule("thetype", "d"); + testdataFactory.createSoftwareModule("thetype", "unassignedfound", false); + final SoftwareModule one = testdataFactory.createSoftwareModule("thetype", "bfound", false); + final SoftwareModule two = testdataFactory.createSoftwareModule("thetype", "cfound", false); + final SoftwareModule differentName = testdataFactory.createSoftwareModule("thetype", "d", false); // ignored - final SoftwareModule deleted = testdataFactory.createSoftwareModule("thetype", "deleted"); + final SoftwareModule deleted = testdataFactory.createSoftwareModule("thetype", "deleted", false); final SoftwareModule four = testdataFactory.createSoftwareModuleOs("e"); distributionSetManagement diff --git a/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/TestdataFactory.java b/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/TestdataFactory.java index 1725a22e7..137ca235f 100644 --- a/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/TestdataFactory.java +++ b/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/TestdataFactory.java @@ -540,7 +540,7 @@ public class TestdataFactory { * @return persisted {@link SoftwareModule}. */ public SoftwareModule createSoftwareModule(final String typeKey) { - return createSoftwareModule(typeKey, ""); + return createSoftwareModule(typeKey, "", false); } /** @@ -552,7 +552,7 @@ public class TestdataFactory { * @return persisted {@link SoftwareModule}. */ public SoftwareModule createSoftwareModuleApp() { - return createSoftwareModule(Constants.SMT_DEFAULT_APP_KEY, ""); + return createSoftwareModule(Constants.SMT_DEFAULT_APP_KEY, "", false); } /** @@ -567,7 +567,7 @@ public class TestdataFactory { * @return persisted {@link SoftwareModule}. */ public SoftwareModule createSoftwareModuleApp(final String prefix) { - return createSoftwareModule(Constants.SMT_DEFAULT_APP_KEY, prefix); + return createSoftwareModule(Constants.SMT_DEFAULT_APP_KEY, prefix, false); } /** @@ -579,7 +579,7 @@ public class TestdataFactory { * @return persisted {@link SoftwareModule}. */ public SoftwareModule createSoftwareModuleOs() { - return createSoftwareModule(Constants.SMT_DEFAULT_OS_KEY, ""); + return createSoftwareModule(Constants.SMT_DEFAULT_OS_KEY, "", false); } /** @@ -594,7 +594,7 @@ public class TestdataFactory { * @return persisted {@link SoftwareModule}. */ public SoftwareModule createSoftwareModuleOs(final String prefix) { - return createSoftwareModule(Constants.SMT_DEFAULT_OS_KEY, prefix); + return createSoftwareModule(Constants.SMT_DEFAULT_OS_KEY, prefix, false); } /** @@ -609,10 +609,10 @@ public class TestdataFactory { * * @return persisted {@link SoftwareModule}. */ - public SoftwareModule createSoftwareModule(final String typeKey, final String prefix) { + public SoftwareModule createSoftwareModule(final String typeKey, final String prefix, final boolean encrypted) { return softwareModuleManagement.create(entityFactory.softwareModule().create() .type(findOrCreateSoftwareModuleType(typeKey)).name(prefix + typeKey).version(prefix + DEFAULT_VERSION) - .description(LOREM.words(10)).vendor(DEFAULT_VENDOR)); + .description(LOREM.words(10)).vendor(DEFAULT_VENDOR).encrypted(encrypted)); } /** diff --git a/hawkbit-rest/hawkbit-ddi-api/src/main/java/org/eclipse/hawkbit/ddi/json/model/DdiChunk.java b/hawkbit-rest/hawkbit-ddi-api/src/main/java/org/eclipse/hawkbit/ddi/json/model/DdiChunk.java index 45c4ae73c..760104ac9 100644 --- a/hawkbit-rest/hawkbit-ddi-api/src/main/java/org/eclipse/hawkbit/ddi/json/model/DdiChunk.java +++ b/hawkbit-rest/hawkbit-ddi-api/src/main/java/org/eclipse/hawkbit/ddi/json/model/DdiChunk.java @@ -35,6 +35,10 @@ public class DdiChunk { @NotNull private String name; + @JsonProperty("encrypted") + @JsonInclude(JsonInclude.Include.NON_NULL) + private Boolean encrypted; + @JsonProperty("artifacts") @JsonInclude(JsonInclude.Include.NON_NULL) private List artifacts; @@ -56,16 +60,19 @@ public class DdiChunk { * of the artifact * @param name * of the artifact + * @param encrypted + * if artifacts are encrypted * @param artifacts * download information * @param metadata * optional as additional information for the target/device */ - public DdiChunk(final String part, final String version, final String name, final List artifacts, - final List metadata) { + public DdiChunk(final String part, final String version, final String name, final Boolean encrypted, + final List artifacts, final List metadata) { this.part = part; this.version = version; this.name = name; + this.encrypted = encrypted; this.artifacts = artifacts; this.metadata = metadata; } @@ -82,6 +89,10 @@ public class DdiChunk { return name; } + public Boolean isEncrypted() { + return encrypted; + } + public List getArtifacts() { if (artifacts == null) { return Collections.emptyList(); diff --git a/hawkbit-rest/hawkbit-ddi-api/src/test/java/org/eclipse/hawkbit/ddi/json/model/DdiChunkTest.java b/hawkbit-rest/hawkbit-ddi-api/src/test/java/org/eclipse/hawkbit/ddi/json/model/DdiChunkTest.java index e2b17707a..bb928337e 100644 --- a/hawkbit-rest/hawkbit-ddi-api/src/test/java/org/eclipse/hawkbit/ddi/json/model/DdiChunkTest.java +++ b/hawkbit-rest/hawkbit-ddi-api/src/test/java/org/eclipse/hawkbit/ddi/json/model/DdiChunkTest.java @@ -32,21 +32,21 @@ import io.qameta.allure.Story; @Story("Serializability of DDI api Models") public class DdiChunkTest { - private ObjectMapper mapper = new ObjectMapper(); + private final ObjectMapper mapper = new ObjectMapper(); @Test @Description("Verify the correct serialization and deserialization of the model") public void shouldSerializeAndDeserializeObject() throws IOException { // Setup - String part = "1234"; - String version = "1.0"; - String name = "Dummy-Artifact"; - List dummyArtifacts = Collections.emptyList(); - DdiChunk ddiChunk = new DdiChunk(part, version, name, dummyArtifacts, null); + final String part = "1234"; + final String version = "1.0"; + final String name = "Dummy-Artifact"; + final List dummyArtifacts = Collections.emptyList(); + final DdiChunk ddiChunk = new DdiChunk(part, version, name, null, dummyArtifacts, null); // Test - String serializedDdiChunk = mapper.writeValueAsString(ddiChunk); - DdiChunk deserializedDdiChunk = mapper.readValue(serializedDdiChunk, DdiChunk.class); + final String serializedDdiChunk = mapper.writeValueAsString(ddiChunk); + final DdiChunk deserializedDdiChunk = mapper.readValue(serializedDdiChunk, DdiChunk.class); assertThat(serializedDdiChunk).contains(part, version, name); assertThat(deserializedDdiChunk.getPart()).isEqualTo(part); @@ -59,10 +59,10 @@ public class DdiChunkTest { @Description("Verify the correct deserialization of a model with a additional unknown property") public void shouldDeserializeObjectWithUnknownProperty() throws IOException { // Setup - String serializedDdiChunk = "{\"part\":\"1234\",\"version\":\"1.0\",\"name\":\"Dummy-Artifact\",\"artifacts\":[],\"unknownProperty\":\"test\"}"; + final String serializedDdiChunk = "{\"part\":\"1234\",\"version\":\"1.0\",\"name\":\"Dummy-Artifact\",\"artifacts\":[],\"unknownProperty\":\"test\"}"; // Test - DdiChunk ddiChunk = mapper.readValue(serializedDdiChunk, DdiChunk.class); + final DdiChunk ddiChunk = mapper.readValue(serializedDdiChunk, DdiChunk.class); assertThat(ddiChunk.getPart()).isEqualTo("1234"); assertThat(ddiChunk.getVersion()).isEqualTo("1.0"); @@ -74,10 +74,10 @@ public class DdiChunkTest { @Description("Verify that deserialization fails for known properties with a wrong datatype") public void shouldFailForObjectWithWrongDataTypes() throws IOException { // Setup - String serializedDdiChunk = "{\"part\":[\"1234\"],\"version\":\"1.0\",\"name\":\"Dummy-Artifact\",\"artifacts\":[]}"; + final String serializedDdiChunk = "{\"part\":[\"1234\"],\"version\":\"1.0\",\"name\":\"Dummy-Artifact\",\"artifacts\":[]}"; // Test - assertThatExceptionOfType(MismatchedInputException.class).isThrownBy( - () -> mapper.readValue(serializedDdiChunk, DdiChunk.class)); + assertThatExceptionOfType(MismatchedInputException.class) + .isThrownBy(() -> mapper.readValue(serializedDdiChunk, DdiChunk.class)); } } \ No newline at end of file diff --git a/hawkbit-rest/hawkbit-ddi-resource/src/main/java/org/eclipse/hawkbit/ddi/rest/resource/DataConversionHelper.java b/hawkbit-rest/hawkbit-ddi-resource/src/main/java/org/eclipse/hawkbit/ddi/rest/resource/DataConversionHelper.java index abcb81613..3635cebb6 100644 --- a/hawkbit-rest/hawkbit-ddi-resource/src/main/java/org/eclipse/hawkbit/ddi/rest/resource/DataConversionHelper.java +++ b/hawkbit-rest/hawkbit-ddi-resource/src/main/java/org/eclipse/hawkbit/ddi/rest/resource/DataConversionHelper.java @@ -57,7 +57,7 @@ public final class DataConversionHelper { return new ResponseList<>(uAction.getDistributionSet().getModules().stream() .map(module -> new DdiChunk(mapChunkLegacyKeys(module.getType().getKey()), module.getVersion(), - module.getName(), + module.getName(), module.isEncrypted() ? Boolean.TRUE : null, createArtifacts(target, module, artifactUrlHandler, systemManagement, request), mapMetadata(metadata.get(module.getId())))) .collect(Collectors.toList())); diff --git a/hawkbit-rest/hawkbit-ddi-resource/src/main/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootController.java b/hawkbit-rest/hawkbit-ddi-resource/src/main/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootController.java index c64e7cc27..23cef1d4c 100644 --- a/hawkbit-rest/hawkbit-ddi-resource/src/main/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootController.java +++ b/hawkbit-rest/hawkbit-ddi-resource/src/main/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootController.java @@ -18,7 +18,7 @@ import javax.validation.Valid; import javax.validation.constraints.NotEmpty; import org.eclipse.hawkbit.api.ArtifactUrlHandler; -import org.eclipse.hawkbit.artifact.repository.model.AbstractDbArtifact; +import org.eclipse.hawkbit.artifact.repository.model.DbArtifact; import org.eclipse.hawkbit.ddi.json.model.DdiActionFeedback; import org.eclipse.hawkbit.ddi.json.model.DdiActionHistory; import org.eclipse.hawkbit.ddi.json.model.DdiArtifact; @@ -183,7 +183,8 @@ public class DdiRootController implements DdiRootControllerRestApi { @SuppressWarnings("squid:S3655") final Artifact artifact = module.getArtifactByFilename(fileName).get(); - final AbstractDbArtifact file = artifactManagement.loadArtifactBinary(artifact.getSha1Hash()) + final DbArtifact file = artifactManagement + .loadArtifactBinary(artifact.getSha1Hash(), module.getId(), module.isEncrypted()) .orElseThrow(() -> new ArtifactBinaryNotFoundException(artifact.getSha1Hash())); final String ifMatch = requestResponseContextHolder.getHttpServletRequest().getHeader(HttpHeaders.IF_MATCH); diff --git a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/softwaremodule/MgmtSoftwareModule.java b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/softwaremodule/MgmtSoftwareModule.java index 9eb69006f..fbc09f9b4 100644 --- a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/softwaremodule/MgmtSoftwareModule.java +++ b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/softwaremodule/MgmtSoftwareModule.java @@ -38,6 +38,9 @@ public class MgmtSoftwareModule extends MgmtNamedEntity { @JsonProperty private boolean deleted; + @JsonProperty + private boolean encrypted; + public void setDeleted(final boolean deleted) { this.deleted = deleted; } @@ -79,4 +82,12 @@ public class MgmtSoftwareModule extends MgmtNamedEntity { this.vendor = vendor; } + public void setEncrypted(final boolean encrypted) { + this.encrypted = encrypted; + } + + public boolean isEncrypted() { + return encrypted; + } + } diff --git a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/softwaremodule/MgmtSoftwareModuleRequestBodyPost.java b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/softwaremodule/MgmtSoftwareModuleRequestBodyPost.java index e39413915..c3a29d2dc 100644 --- a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/softwaremodule/MgmtSoftwareModuleRequestBodyPost.java +++ b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/softwaremodule/MgmtSoftwareModuleRequestBodyPost.java @@ -31,6 +31,9 @@ public class MgmtSoftwareModuleRequestBodyPost { @JsonProperty private String vendor; + @JsonProperty + private boolean encrypted; + /** * @return the name */ @@ -121,4 +124,22 @@ public class MgmtSoftwareModuleRequestBodyPost { return this; } + /** + * @return if encrypted + */ + public boolean isEncrypted() { + return encrypted; + } + + /** + * @param encrypted + * if should be encrypted + * + * @return updated body + */ + public MgmtSoftwareModuleRequestBodyPost setEncrypted(final boolean encrypted) { + this.encrypted = encrypted; + return this; + } + } diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDownloadArtifactResource.java b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDownloadArtifactResource.java index 869149d3d..d1ae84aef 100644 --- a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDownloadArtifactResource.java +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDownloadArtifactResource.java @@ -12,7 +12,7 @@ import java.io.InputStream; import javax.servlet.http.HttpServletRequest; -import org.eclipse.hawkbit.artifact.repository.model.AbstractDbArtifact; +import org.eclipse.hawkbit.artifact.repository.model.DbArtifact; import org.eclipse.hawkbit.mgmt.rest.api.MgmtDownloadArtifactRestApi; import org.eclipse.hawkbit.repository.ArtifactManagement; import org.eclipse.hawkbit.repository.SoftwareModuleManagement; @@ -70,7 +70,8 @@ public class MgmtDownloadArtifactResource implements MgmtDownloadArtifactRestApi final Artifact artifact = module.getArtifact(artifactId) .orElseThrow(() -> new EntityNotFoundException(Artifact.class, artifactId)); - final AbstractDbArtifact file = artifactManagement.loadArtifactBinary(artifact.getSha1Hash()) + final DbArtifact file = artifactManagement + .loadArtifactBinary(artifact.getSha1Hash(), module.getId(), module.isEncrypted()) .orElseThrow(() -> new ArtifactBinaryNotFoundException(artifact.getSha1Hash())); final HttpServletRequest request = requestResponseContextHolder.getHttpServletRequest(); final String ifMatch = request.getHeader(HttpHeaders.IF_MATCH); diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtSoftwareModuleMapper.java b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtSoftwareModuleMapper.java index 027bc9a3a..3bb4ef5ef 100644 --- a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtSoftwareModuleMapper.java +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtSoftwareModuleMapper.java @@ -45,7 +45,8 @@ public final class MgmtSoftwareModuleMapper { private static SoftwareModuleCreate fromRequest(final EntityFactory entityFactory, final MgmtSoftwareModuleRequestBodyPost smsRest) { return entityFactory.softwareModule().create().type(smsRest.getType()).name(smsRest.getName()) - .version(smsRest.getVersion()).description(smsRest.getDescription()).vendor(smsRest.getVendor()); + .version(smsRest.getVersion()).description(smsRest.getDescription()).vendor(smsRest.getVendor()) + .encrypted(smsRest.isEncrypted()); } static List fromRequestSwMetadata(final EntityFactory entityFactory, @@ -107,6 +108,7 @@ public final class MgmtSoftwareModuleMapper { response.setType(softwareModule.getType().getKey()); response.setVendor(softwareModule.getVendor()); response.setDeleted(softwareModule.isDeleted()); + response.setEncrypted(softwareModule.isEncrypted()); response.add(linkTo(methodOn(MgmtSoftwareModuleRestApi.class).getSoftwareModule(response.getModuleId())) .withSelfRel()); diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtSoftwareModuleResourceTest.java b/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtSoftwareModuleResourceTest.java index 268005e37..45494654b 100644 --- a/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtSoftwareModuleResourceTest.java +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtSoftwareModuleResourceTest.java @@ -29,6 +29,7 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.util.Arrays; +import java.util.Collections; import java.util.List; import org.apache.commons.io.IOUtils; @@ -199,7 +200,7 @@ public class MgmtSoftwareModuleResourceTest extends AbstractManagementApiIntegra @Test @Description("Verifies that artifacts which exceed the configured maximum size cannot be uploaded.") public void uploadArtifactFailsIfTooLarge() throws Exception { - final SoftwareModule sm = testdataFactory.createSoftwareModule("quota", "quota"); + final SoftwareModule sm = testdataFactory.createSoftwareModule("quota", "quota", false); final long maxSize = quotaManagement.getMaxArtifactSize(); // create a file which exceeds the configured maximum size @@ -218,7 +219,7 @@ public class MgmtSoftwareModuleResourceTest extends AbstractManagementApiIntegra @Test @Description("Verifies that artifact with invalid filename cannot be uploaded to prevent cross site scripting.") public void uploadArtifactFailsIfFilenameInvalide() throws Exception { - final SoftwareModule sm = testdataFactory.createSoftwareModule("quota", "quota"); + final SoftwareModule sm = testdataFactory.createSoftwareModule("quota", "quota", false); final String illegalFilename = ".xml"; final byte[] randomBytes = randomBytes(5 * 1024); @@ -236,11 +237,12 @@ public class MgmtSoftwareModuleResourceTest extends AbstractManagementApiIntegra assertThat(artifactManagement.count()).as("Wrong artifact size").isEqualTo(1); // binary - try (InputStream fileInputStream = artifactManagement - .loadArtifactBinary(softwareModuleManagement.get(sm.getId()).get().getArtifacts().get(0).getSha1Hash()) + try (final InputStream fileInputStream = artifactManagement + .loadArtifactBinary(softwareModuleManagement.get(sm.getId()).get().getArtifacts().get(0).getSha1Hash(), + sm.getId(), sm.isEncrypted()) .get().getFileInputStream()) { - assertTrue( - IOUtils.contentEquals(new ByteArrayInputStream(random), fileInputStream), "Wrong artifact content"); + assertTrue(IOUtils.contentEquals(new ByteArrayInputStream(random), fileInputStream), + "Wrong artifact content"); } // hashes @@ -662,6 +664,14 @@ public class MgmtSoftwareModuleResourceTest extends AbstractManagementApiIntegra .contentType(MediaType.APPLICATION_OCTET_STREAM)).andDo(MockMvcResultPrinter.print()) .andExpect(status().isUnsupportedMediaType()); + final SoftwareModule swm = entityFactory.softwareModule().create().name("encryptedModule").type(osType) + .version("version").vendor("vendor").description("description").encrypted(true).build(); + // artifact decryption is not supported + mvc.perform( + post("/rest/v1/softwaremodules").content(JsonBuilder.softwareModules(Collections.singletonList(swm))) + .contentType(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isBadRequest()); + // not allowed methods mvc.perform(put("/rest/v1/softwaremodules")).andDo(MockMvcResultPrinter.print()) .andExpect(status().isMethodNotAllowed()); diff --git a/hawkbit-rest/hawkbit-rest-core/src/main/java/org/eclipse/hawkbit/rest/exception/ResponseExceptionHandler.java b/hawkbit-rest/hawkbit-rest-core/src/main/java/org/eclipse/hawkbit/rest/exception/ResponseExceptionHandler.java index ffc6f629d..30f152dbf 100644 --- a/hawkbit-rest/hawkbit-rest-core/src/main/java/org/eclipse/hawkbit/rest/exception/ResponseExceptionHandler.java +++ b/hawkbit-rest/hawkbit-rest-core/src/main/java/org/eclipse/hawkbit/rest/exception/ResponseExceptionHandler.java @@ -8,14 +8,15 @@ */ package org.eclipse.hawkbit.rest.exception; -import com.google.common.collect.Iterables; import java.util.EnumMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; + import javax.servlet.http.HttpServletRequest; import javax.validation.ConstraintViolationException; import javax.validation.ValidationException; + import org.apache.commons.lang3.exception.ExceptionUtils; import org.eclipse.hawkbit.exception.AbstractServerRtException; import org.eclipse.hawkbit.exception.SpServerError; @@ -31,6 +32,8 @@ import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.multipart.MultipartException; +import com.google.common.collect.Iterables; + /** * General controller advice for exception handling. */ @@ -54,6 +57,8 @@ public class ResponseExceptionHandler { ERROR_TO_HTTP_STATUS.put(SpServerError.SP_REST_RSQL_SEARCH_PARAM_SYNTAX, HttpStatus.BAD_REQUEST); ERROR_TO_HTTP_STATUS.put(SpServerError.SP_INSUFFICIENT_PERMISSION, HttpStatus.FORBIDDEN); ERROR_TO_HTTP_STATUS.put(SpServerError.SP_ARTIFACT_UPLOAD_FAILED, HttpStatus.INTERNAL_SERVER_ERROR); + ERROR_TO_HTTP_STATUS.put(SpServerError.SP_ARTIFACT_ENCRYPTION_NOT_SUPPORTED, HttpStatus.BAD_REQUEST); + ERROR_TO_HTTP_STATUS.put(SpServerError.SP_ARTIFACT_ENCRYPTION_FAILED, HttpStatus.INTERNAL_SERVER_ERROR); ERROR_TO_HTTP_STATUS.put(SpServerError.SP_ARTIFACT_UPLOAD_FAILED_SHA1_MATCH, HttpStatus.BAD_REQUEST); ERROR_TO_HTTP_STATUS.put(SpServerError.SP_ARTIFACT_UPLOAD_FAILED_SHA256_MATCH, HttpStatus.BAD_REQUEST); ERROR_TO_HTTP_STATUS.put(SpServerError.SP_ARTIFACT_UPLOAD_FAILED_MD5_MATCH, HttpStatus.BAD_REQUEST); diff --git a/hawkbit-rest/hawkbit-rest-core/src/main/java/org/eclipse/hawkbit/rest/util/FileStreamingUtil.java b/hawkbit-rest/hawkbit-rest-core/src/main/java/org/eclipse/hawkbit/rest/util/FileStreamingUtil.java index 98ea2454a..6d06a13e4 100644 --- a/hawkbit-rest/hawkbit-rest-core/src/main/java/org/eclipse/hawkbit/rest/util/FileStreamingUtil.java +++ b/hawkbit-rest/hawkbit-rest-core/src/main/java/org/eclipse/hawkbit/rest/util/FileStreamingUtil.java @@ -20,7 +20,7 @@ import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import org.eclipse.hawkbit.artifact.repository.model.AbstractDbArtifact; +import org.eclipse.hawkbit.artifact.repository.model.DbArtifact; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpHeaders; @@ -118,9 +118,9 @@ public final class FileStreamingUtil { * @throws FileStreamingFailedException * if streaming fails */ - public static ResponseEntity writeFileResponse(final AbstractDbArtifact artifact, - final String filename, final long lastModified, final HttpServletResponse response, - final HttpServletRequest request, final FileStreamingProgressListener progressListener) { + public static ResponseEntity writeFileResponse(final DbArtifact artifact, final String filename, + final long lastModified, final HttpServletResponse response, final HttpServletRequest request, + final FileStreamingProgressListener progressListener) { ResponseEntity result; @@ -189,9 +189,9 @@ public final class FileStreamingUtil { return result; } - private static ResponseEntity handleFullFileRequest(final AbstractDbArtifact artifact, - final String filename, final HttpServletResponse response, - final FileStreamingProgressListener progressListener, final ByteRange full) { + private static ResponseEntity handleFullFileRequest(final DbArtifact artifact, final String filename, + final HttpServletResponse response, final FileStreamingProgressListener progressListener, + final ByteRange full) { final ByteRange r = full; response.setHeader(HttpHeaders.CONTENT_RANGE, "bytes " + r.getStart() + "-" + r.getEnd() + "/" + r.getTotal()); response.setContentLengthLong(r.getLength()); @@ -257,7 +257,7 @@ public final class FileStreamingUtil { } } - private static ResponseEntity handleMultipartRangeRequest(final AbstractDbArtifact artifact, + private static ResponseEntity handleMultipartRangeRequest(final DbArtifact artifact, final String filename, final HttpServletResponse response, final FileStreamingProgressListener progressListener, final List ranges) { @@ -291,7 +291,7 @@ public final class FileStreamingUtil { return ResponseEntity.status(HttpStatus.PARTIAL_CONTENT).build(); } - private static ResponseEntity handleStandardRangeRequest(final AbstractDbArtifact artifact, + private static ResponseEntity handleStandardRangeRequest(final DbArtifact artifact, final String filename, final HttpServletResponse response, final FileStreamingProgressListener progressListener, final List ranges) { final ByteRange r = ranges.get(0); diff --git a/hawkbit-rest/hawkbit-rest-core/src/test/java/org/eclipse/hawkbit/rest/util/JsonBuilder.java b/hawkbit-rest/hawkbit-rest-core/src/test/java/org/eclipse/hawkbit/rest/util/JsonBuilder.java index fb613df1b..aa92f5a5e 100644 --- a/hawkbit-rest/hawkbit-rest-core/src/test/java/org/eclipse/hawkbit/rest/util/JsonBuilder.java +++ b/hawkbit-rest/hawkbit-rest-core/src/test/java/org/eclipse/hawkbit/rest/util/JsonBuilder.java @@ -97,7 +97,8 @@ public abstract class JsonBuilder { builder.append(new JSONObject().put("name", module.getName()).put("description", module.getDescription()) .put("type", module.getType().getKey()).put("id", Long.MAX_VALUE).put("vendor", module.getVendor()) .put("version", module.getVersion()).put("createdAt", "0").put("updatedAt", "0") - .put("createdBy", "fghdfkjghdfkjh").put("updatedBy", "fghdfkjghdfkjh").toString()); + .put("createdBy", "fghdfkjghdfkjh").put("updatedBy", "fghdfkjghdfkjh") + .put("encrypted", module.isEncrypted()).toString()); if (++i < modules.size()) { builder.append(","); @@ -447,7 +448,8 @@ public abstract class JsonBuilder { return builder.toString(); } - public static String targets(final List targets, final boolean withToken, final long targetTypeId) throws JSONException { + public static String targets(final List targets, final boolean withToken, final long targetTypeId) + throws JSONException { final StringBuilder builder = new StringBuilder(); builder.append("["); @@ -487,8 +489,8 @@ public abstract class JsonBuilder { }); result.put(new JSONObject().put("name", type.getName()).put("description", type.getDescription()) - .put("id", Long.MAX_VALUE).put("colour", type.getColour()).put("createdAt", "0").put("updatedAt", "0") - .put("createdBy", "fghdfkjghdfkjh").put("updatedBy", "fghdfkjghdfkjh") + .put("id", Long.MAX_VALUE).put("colour", type.getColour()).put("createdAt", "0") + .put("updatedAt", "0").put("createdBy", "fghdfkjghdfkjh").put("updatedBy", "fghdfkjghdfkjh") .put("distributionsets", dsTypes)); } @@ -510,11 +512,10 @@ public abstract class JsonBuilder { } }); - JSONObject json = new JSONObject().put("name", type.getName()).put("description", type.getDescription()) + final JSONObject json = new JSONObject().put("name", type.getName()).put("description", type.getDescription()) .put("colour", type.getColour()); - if(dsTypes.length() != 0) - { + if (dsTypes.length() != 0) { json.put("compatibledistributionsettypes", dsTypes); } diff --git a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/ddi/documentation/DdiApiModelProperties.java b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/ddi/documentation/DdiApiModelProperties.java index 9e091451d..772ced4a9 100644 --- a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/ddi/documentation/DdiApiModelProperties.java +++ b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/ddi/documentation/DdiApiModelProperties.java @@ -60,7 +60,7 @@ final class DdiApiModelProperties { static final String CHUNK_TYPE = "Type of the chunk, e.g. firmware, bundle, app. In update server mapped to Software Module Type."; - static final String SOFTWARE_MODUL_TYPE = "type of the software module, e.g. firmware, bundle, app"; + static final String SOFTWARE_MODULE_TYPE = "type of the software module, e.g. firmware, bundle, app"; static final String SOFTWARE_MODULE_VERSION = "version of the software module"; @@ -68,7 +68,7 @@ final class DdiApiModelProperties { static final String SOFTWARE_MODULE_ARTIFACT_LINKS = "artifact links of the software module"; - static final String SOFTWARE_MODUL_ID = "id of the software module"; + static final String SOFTWARE_MODULE_ID = "id of the software module"; static final String CHUNK_VERSION = "software version of the chunk"; @@ -102,15 +102,15 @@ final class DdiApiModelProperties { static final String CHUNK = "Software chunks of an update. In server mapped by Software Module."; - static final String SOFTWARE_MODUL = "software modules of an update"; + static final String SOFTWARE_MODULE = "software modules of an update"; static final String ARTIFACT = "artifact modules of an update"; static final String FILENAME = "file name of artifact"; static final String TARGET_CONFIG_DATA = "Link which is provided whenever the provisioning target or device is supposed " - + "to push its configuration data (aka. \"controller attributes\") to the server. Only shown for the initial " - + "configuration, after a successful update action, or if requested explicitly (e.g. via the management UI)."; + + "to push its configuration data (aka. \"controller attributes\") to the server. Only shown for the initial " + + "configuration, after a successful update action, or if requested explicitly (e.g. via the management UI)."; static final String ARTIFACT_HASHES_SHA1 = "SHA1 hash of the artifact in Base 16 format"; static final String ARTIFACT_HASHES_MD5 = "MD5 hash of the artifact"; diff --git a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/ddi/documentation/RootControllerDocumentationTest.java b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/ddi/documentation/RootControllerDocumentationTest.java index efa7fd72c..e4333d7bf 100644 --- a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/ddi/documentation/RootControllerDocumentationTest.java +++ b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/ddi/documentation/RootControllerDocumentationTest.java @@ -183,7 +183,9 @@ public class RootControllerDocumentationTest extends AbstractApiRestDocumentatio pathParameters(parameterWithName("tenant").description(ApiModelPropertiesGeneric.TENANT), parameterWithName("controllerId").description(DdiApiModelProperties.CONTROLLER_ID), parameterWithName("actionId").description(DdiApiModelProperties.ACTION_ID_CANCELED)), - requestFields(optionalRequestFieldWithPath("id").description(DdiApiModelProperties.FEEDBACK_ACTION_ID), + requestFields( + optionalRequestFieldWithPath("id") + .description(DdiApiModelProperties.FEEDBACK_ACTION_ID), requestFieldWithPath("status").description(DdiApiModelProperties.TARGET_STATUS), requestFieldWithPath("status.execution") .description(DdiApiModelProperties.TARGET_EXEC_STATUS).type("enum") @@ -386,7 +388,9 @@ public class RootControllerDocumentationTest extends AbstractApiRestDocumentatio parameterWithName("controllerId").description(DdiApiModelProperties.CONTROLLER_ID), parameterWithName("actionId").description(DdiApiModelProperties.ACTION_ID)), - requestFields(optionalRequestFieldWithPath("id").description(DdiApiModelProperties.FEEDBACK_ACTION_ID), + requestFields( + optionalRequestFieldWithPath("id") + .description(DdiApiModelProperties.FEEDBACK_ACTION_ID), requestFieldWithPath("status").description(DdiApiModelProperties.TARGET_STATUS), requestFieldWithPath("status.execution") .description(DdiApiModelProperties.TARGET_EXEC_STATUS).type("enum") @@ -428,7 +432,7 @@ public class RootControllerDocumentationTest extends AbstractApiRestDocumentatio .andDo(this.document.document( pathParameters(parameterWithName("tenant").description(ApiModelPropertiesGeneric.TENANT), parameterWithName("controllerId").description(DdiApiModelProperties.CONTROLLER_ID), - parameterWithName("moduleId").description(DdiApiModelProperties.SOFTWARE_MODUL_ID)), + parameterWithName("moduleId").description(DdiApiModelProperties.SOFTWARE_MODULE_ID)), responseFields(fieldWithPath("[]filename").description(DdiApiModelProperties.ARTIFACTS), fieldWithPath("[]hashes").description(DdiApiModelProperties.ARTIFACTS), fieldWithPath("[]hashes.sha1").description(DdiApiModelProperties.ARTIFACT_HASHES_SHA1), diff --git a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/documentation/MgmtApiModelProperties.java b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/documentation/MgmtApiModelProperties.java index 0e0a887b2..bc5660da4 100644 --- a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/documentation/MgmtApiModelProperties.java +++ b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/documentation/MgmtApiModelProperties.java @@ -43,6 +43,7 @@ public final class MgmtApiModelProperties { // software module public static final String SM_TYPE = "The software module type " + ApiModelPropertiesGeneric.ENDING; + public static final String ENCRYPTED = "Encryption flag, used to identify that artifacts should be encrypted upon upload."; public static final String ARTIFACT_HASHES = "Hashes of the artifact."; public static final String ARTIFACT_SIZE = "Size of the artifact."; public static final String ARTIFACT_PROVIDED_FILE = "Binary of file."; diff --git a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/SoftwaremodulesDocumentationTest.java b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/SoftwaremodulesDocumentationTest.java index fb3a0273a..041ca5e20 100644 --- a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/SoftwaremodulesDocumentationTest.java +++ b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/SoftwaremodulesDocumentationTest.java @@ -84,6 +84,7 @@ public class SoftwaremodulesDocumentationTest extends AbstractApiRestDocumentati fieldWithPath("content[].name").description(ApiModelPropertiesGeneric.NAME), fieldWithPath("content[].description").description(ApiModelPropertiesGeneric.DESCRPTION), fieldWithPath("content[].vendor").description(MgmtApiModelProperties.VENDOR), + fieldWithPath("content[].encrypted").description(MgmtApiModelProperties.ENCRYPTED), fieldWithPath("content[].createdBy").description(ApiModelPropertiesGeneric.CREATED_BY), fieldWithPath("content[].createdAt").description(ApiModelPropertiesGeneric.CREATED_AT), fieldWithPath("content[].lastModifiedBy") @@ -141,6 +142,7 @@ public class SoftwaremodulesDocumentationTest extends AbstractApiRestDocumentati fieldWithPath("[].name").description(ApiModelPropertiesGeneric.NAME), fieldWithPath("[].description").description(ApiModelPropertiesGeneric.DESCRPTION), fieldWithPath("[].vendor").description(MgmtApiModelProperties.VENDOR), + fieldWithPath("[].encrypted").description(MgmtApiModelProperties.ENCRYPTED), fieldWithPath("[].deleted").description(ApiModelPropertiesGeneric.DELETED), fieldWithPath("[].createdBy").description(ApiModelPropertiesGeneric.CREATED_BY), fieldWithPath("[].createdAt").description(ApiModelPropertiesGeneric.CREATED_AT), @@ -188,6 +190,7 @@ public class SoftwaremodulesDocumentationTest extends AbstractApiRestDocumentati fieldWithPath("lastModifiedBy").description(ApiModelPropertiesGeneric.LAST_MODIFIED_BY), fieldWithPath("lastModifiedAt").description(ApiModelPropertiesGeneric.LAST_MODIFIED_AT), fieldWithPath("vendor").description(MgmtApiModelProperties.VENDOR), + fieldWithPath("encrypted").description(MgmtApiModelProperties.ENCRYPTED), fieldWithPath("deleted").description(ApiModelPropertiesGeneric.DELETED), fieldWithPath("type").description(MgmtApiModelProperties.SM_TYPE), fieldWithPath("version").description(MgmtApiModelProperties.VERSION), @@ -227,6 +230,7 @@ public class SoftwaremodulesDocumentationTest extends AbstractApiRestDocumentati fieldWithPath("type").description(MgmtApiModelProperties.SM_TYPE), fieldWithPath("version").description(MgmtApiModelProperties.VERSION), fieldWithPath("vendor").description(MgmtApiModelProperties.VENDOR), + fieldWithPath("encrypted").description(MgmtApiModelProperties.ENCRYPTED), fieldWithPath("deleted").description(ApiModelPropertiesGeneric.DELETED), fieldWithPath("_links.self").ignored(), fieldWithPath("_links.type").description(MgmtApiModelProperties.SM_TYPE), @@ -279,7 +283,9 @@ public class SoftwaremodulesDocumentationTest extends AbstractApiRestDocumentati final byte random[] = RandomStringUtils.random(5).getBytes(); final MockMultipartFile file = new MockMultipartFile("file", "origFilename", null, random); - mockMvc.perform(fileUpload(MgmtRestConstants.SOFTWAREMODULE_V1_REQUEST_MAPPING + "/{softwareModuleId}/artifacts", sm.getId()).file(file)) + mockMvc.perform( + fileUpload(MgmtRestConstants.SOFTWAREMODULE_V1_REQUEST_MAPPING + "/{softwareModuleId}/artifacts", + sm.getId()).file(file)) .andDo(MockMvcResultPrinter.print()).andExpect(status().isCreated()) .andExpect(content().contentType(MediaTypes.HAL_JSON)) .andDo(this.document.document( @@ -313,7 +319,8 @@ public class SoftwaremodulesDocumentationTest extends AbstractApiRestDocumentati final byte random[] = RandomStringUtils.random(5).getBytes(); final MockMultipartFile file = new MockMultipartFile("file", "origFilename", null, random); - mockMvc.perform(fileUpload(MgmtRestConstants.SOFTWAREMODULE_V1_REQUEST_MAPPING + "/{softwareModuleId}/artifacts", + mockMvc.perform( + fileUpload(MgmtRestConstants.SOFTWAREMODULE_V1_REQUEST_MAPPING + "/{softwareModuleId}/artifacts", sm.getId()).file(file).param("filename", "filename").param("file", "s") .param("md5sum", "md5sum").param("sha1sum", "sha1sum")) .andDo(MockMvcResultPrinter.print()).andExpect(status().isBadRequest()) diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/MgmtUiConfiguration.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/MgmtUiConfiguration.java index 204fd90bc..7f60b3df3 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/MgmtUiConfiguration.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/MgmtUiConfiguration.java @@ -18,6 +18,7 @@ import org.eclipse.hawkbit.ui.common.data.suppliers.TargetFilterStateDataSupplie import org.eclipse.hawkbit.ui.common.data.suppliers.TargetManagementStateDataSupplier; import org.eclipse.hawkbit.ui.common.data.suppliers.TargetManagementStateDataSupplierImpl; import org.eclipse.hawkbit.ui.error.HawkbitUIErrorHandler; +import org.eclipse.hawkbit.ui.error.extractors.ArtifactEncryptionErrorExtractor; import org.eclipse.hawkbit.ui.error.extractors.ConstraintViolationErrorExtractor; import org.eclipse.hawkbit.ui.error.extractors.EntityNotFoundErrorExtractor; import org.eclipse.hawkbit.ui.error.extractors.IncompatibleTargetTypeErrorExtractor; @@ -182,6 +183,18 @@ public class MgmtUiConfiguration { return new InvalidDistributionSetErrorExtractor(i18n); } + /** + * UI Artifact ecnryption operations Error details extractor bean. + * + * @param i18n + * VaadinMessageSource + * @return UI Artifact ecnryption operations Error details extractor + */ + @Bean + UiErrorDetailsExtractor artifactEncryptionErrorExtractor(final VaadinMessageSource i18n) { + return new ArtifactEncryptionErrorExtractor(i18n); + } + /** * Vaadin4Spring servlet bean. * diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/details/ArtifactDetailsGrid.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/details/ArtifactDetailsGrid.java index f94eee552..4cd8b4320 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/details/ArtifactDetailsGrid.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/details/ArtifactDetailsGrid.java @@ -12,7 +12,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.stream.Collectors; -import org.eclipse.hawkbit.artifact.repository.model.AbstractDbArtifact; +import org.eclipse.hawkbit.artifact.repository.model.DbArtifact; import org.eclipse.hawkbit.repository.ArtifactManagement; import org.eclipse.hawkbit.ui.common.CommonUiDependencies; import org.eclipse.hawkbit.ui.common.builder.GridComponentBuilder; @@ -33,11 +33,14 @@ import org.eclipse.hawkbit.ui.utils.SPUIStyleDefinitions; import org.eclipse.hawkbit.ui.utils.UIComponentIdProvider; import org.eclipse.hawkbit.ui.utils.UIMessageIdProvider; import org.eclipse.hawkbit.ui.utils.UINotification; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import com.vaadin.icons.VaadinIcons; import com.vaadin.server.FileDownloader; import com.vaadin.server.StreamResource; import com.vaadin.ui.Button; +import com.vaadin.ui.UI; /** * Artifact Details grid which is shown on the Upload View. @@ -45,6 +48,8 @@ import com.vaadin.ui.Button; public class ArtifactDetailsGrid extends AbstractGrid { private static final long serialVersionUID = 1L; + private static final Logger LOG = LoggerFactory.getLogger(ArtifactDetailsGrid.class); + private static final String ARTIFACT_NAME_ID = "artifactName"; private static final String ARTIFACT_SIZE_ID = "artifactSize"; private static final String ARTIFACT_MODIFIED_DATE_ID = "artifactModifiedDate"; @@ -60,6 +65,8 @@ public class ArtifactDetailsGrid extends AbstractGrid { private final transient DeleteSupport artifactDeleteSupport; private final transient MasterEntitySupport masterEntitySupport; + private boolean artifactsEncrypted; + /** * Constructor * @@ -82,7 +89,8 @@ public class ArtifactDetailsGrid extends AbstractGrid { new FilterSupport<>(new ArtifactDataProvider(artifactManagement, new ArtifactToProxyArtifactMapper()))); initFilterMappings(); - this.masterEntitySupport = new MasterEntitySupport<>(getFilterSupport()); + this.masterEntitySupport = new MasterEntitySupport<>(getFilterSupport(), null, + sm -> artifactsEncrypted = sm != null && sm.isEncrypted()); init(); } @@ -107,9 +115,8 @@ public class ArtifactDetailsGrid extends AbstractGrid { .map(ProxyIdentifiableEntity::getId).collect(Collectors.toList()); artifactToBeDeletedIds.forEach(artifactManagement::delete); - eventBus.publish(EventTopics.ENTITY_MODIFIED, this, - new EntityModifiedEventPayload(EntityModifiedEventType.ENTITY_UPDATED, ProxySoftwareModule.class, - getMasterEntitySupport().getMasterId())); + eventBus.publish(EventTopics.ENTITY_MODIFIED, this, new EntityModifiedEventPayload( + EntityModifiedEventType.ENTITY_UPDATED, ProxySoftwareModule.class, masterEntitySupport.getMasterId())); return true; } @@ -171,12 +178,20 @@ public class ArtifactDetailsGrid extends AbstractGrid { private void attachFileDownloader(final ProxyArtifact artifact, final Button downloadButton) { final StreamResource artifactStreamResource = new StreamResource(() -> artifactManagement - .loadArtifactBinary(artifact.getSha1Hash()).map(AbstractDbArtifact::getFileInputStream).orElse(null), - artifact.getFilename()); + .loadArtifactBinary(artifact.getSha1Hash(), masterEntitySupport.getMasterId(), artifactsEncrypted) + .map(DbArtifact::getFileInputStream).orElse(null), artifact.getFilename()); final FileDownloader fileDownloader = new FileDownloader(artifactStreamResource); - fileDownloader.setErrorHandler(event -> notification - .displayValidationError(i18n.getMessage(UIMessageIdProvider.ARTIFACT_DOWNLOAD_FAILURE_MSG))); + fileDownloader.setErrorHandler(event -> { + LOG.error("Download failed for artifact with id {}, filename {}", artifact.getId(), artifact.getFilename(), + event.getThrowable()); + notification.displayValidationError(i18n.getMessage(UIMessageIdProvider.ARTIFACT_DOWNLOAD_FAILURE_MSG)); + UI.getCurrent().access(() -> { + // give error details extractors a chance to process specific + // error + throw new DownloadException(event.getThrowable()); + }); + }); fileDownloader.extend(downloadButton); } @@ -225,4 +240,12 @@ public class ArtifactDetailsGrid extends AbstractGrid { public MasterEntitySupport getMasterEntitySupport() { return masterEntitySupport; } + + private static class DownloadException extends RuntimeException { + private static final long serialVersionUID = 1L; + + public DownloadException(final Throwable cause) { + super(cause); + } + } } diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/smtable/AddSmWindowController.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/smtable/AddSmWindowController.java index 0b49288f9..6e1688b71 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/smtable/AddSmWindowController.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/smtable/AddSmWindowController.java @@ -8,6 +8,7 @@ */ package org.eclipse.hawkbit.ui.artifacts.smtable; +import org.eclipse.hawkbit.repository.ArtifactEncryptionService; import org.eclipse.hawkbit.repository.SoftwareModuleManagement; import org.eclipse.hawkbit.repository.builder.SoftwareModuleCreate; import org.eclipse.hawkbit.repository.model.SoftwareModule; @@ -62,6 +63,13 @@ public class AddSmWindowController return layout; } + @Override + protected void adaptLayout(final ProxySoftwareModule proxyEntity) { + if (!ArtifactEncryptionService.getInstance().isEncryptionSupported()) { + layout.disableEncryptionField(); + } + } + @Override protected ProxySoftwareModule buildEntityFromProxy(final ProxySoftwareModule proxyEntity) { // We ignore the method parameter, because we are interested in the @@ -73,7 +81,7 @@ public class AddSmWindowController protected SoftwareModule persistEntityInRepository(final ProxySoftwareModule entity) { final SoftwareModuleCreate smCreate = getEntityFactory().softwareModule().create() .type(entity.getTypeInfo().getKey()).name(entity.getName()).version(entity.getVersion()) - .vendor(entity.getVendor()).description(entity.getDescription()); + .vendor(entity.getVendor()).description(entity.getDescription()).encrypted(entity.isEncrypted()); return smManagement.create(smCreate); } diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/smtable/SmWindowBuilder.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/smtable/SmWindowBuilder.java index 576b4f04c..0a158b1da 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/smtable/SmWindowBuilder.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/smtable/SmWindowBuilder.java @@ -60,8 +60,8 @@ public class SmWindowBuilder extends AbstractEntityWindowBuilder smTypeDataProvider = new SoftwareModuleTypeDataProvider<>( - smTypeManagement, new TypeToTypeInfoMapper()); + smTypeManagement, new TypeToTypeInfoMapper<>()); this.smComponentBuilder = new SmWindowLayoutComponentBuilder(i18n, smTypeDataProvider); this.smTypeSelect = smComponentBuilder.createSoftwareModuleTypeCombo(binder); @@ -55,6 +56,7 @@ public class SmWindowLayout extends AbstractEntityWindowLayout smTypeDataProvider; @@ -56,7 +58,8 @@ public class SmWindowLayoutComponentBuilder { */ public ComboBox createSoftwareModuleTypeCombo(final Binder binder) { return FormComponentBuilder - .createTypeCombo(binder, smTypeDataProvider, i18n, UIComponentIdProvider.SW_MODULE_TYPE, true).getComponent(); + .createTypeCombo(binder, smTypeDataProvider, i18n, UIComponentIdProvider.SW_MODULE_TYPE, true) + .getComponent(); } /** @@ -113,4 +116,18 @@ public class SmWindowLayoutComponentBuilder { return FormComponentBuilder .createDescriptionInput(binder, i18n, UIComponentIdProvider.ADD_SW_MODULE_DESCRIPTION).getComponent(); } + + /** + * Create checkbox for artifact encryption + * + * @param binder + * binder the input will be bound to + * + * @return input component + */ + public CheckBox createArtifactEncryptionCheck(final Binder binder) { + return FormComponentBuilder.createCheckBox(i18n.getMessage(ARTIFACT_ENCRYPTION), + UIComponentIdProvider.ARTIFACT_ENCRYPTION_ID, binder, ProxySoftwareModule::isEncrypted, + ProxySoftwareModule::setEncrypted); + } } diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/smtable/UpdateSmWindowController.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/smtable/UpdateSmWindowController.java index ac3ff67a9..11d3f9ef4 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/smtable/UpdateSmWindowController.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/smtable/UpdateSmWindowController.java @@ -61,6 +61,7 @@ public class UpdateSmWindowController sm.setVersion(proxyEntity.getVersion()); sm.setVendor(proxyEntity.getVendor()); sm.setDescription(proxyEntity.getDescription()); + sm.setEncrypted(proxyEntity.isEncrypted()); nameBeforeEdit = proxyEntity.getName(); versionBeforeEdit = proxyEntity.getVersion(); @@ -78,6 +79,7 @@ public class UpdateSmWindowController layout.disableSmTypeSelect(); layout.disableNameField(); layout.disableVersionField(); + layout.disableEncryptionField(); } @Override diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/upload/AbstractFileTransferHandler.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/upload/AbstractFileTransferHandler.java index a4334c7f5..b38afd387 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/upload/AbstractFileTransferHandler.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/upload/AbstractFileTransferHandler.java @@ -18,6 +18,8 @@ import java.util.concurrent.locks.Lock; import org.eclipse.hawkbit.repository.ArtifactManagement; import org.eclipse.hawkbit.repository.RegexCharacterCollection; import org.eclipse.hawkbit.repository.RegexCharacterCollection.RegexChar; +import org.eclipse.hawkbit.repository.exception.ArtifactEncryptionFailedException; +import org.eclipse.hawkbit.repository.exception.ArtifactEncryptionUnsupportedException; import org.eclipse.hawkbit.repository.exception.ArtifactUploadFailedException; import org.eclipse.hawkbit.repository.exception.AssignmentQuotaExceededException; import org.eclipse.hawkbit.repository.exception.FileSizeQuotaExceededException; @@ -137,6 +139,10 @@ public abstract class AbstractFileTransferHandler implements Serializable { interruptUploadAndSetReason(i18n.getMessage("message.uploadedfile.illegalFilename")); } + protected void interruptUploadDueToEncryptionError() { + interruptUploadAndSetReason(i18n.getMessage("message.encryption.failed")); + } + protected boolean isFileAlreadyContainedInSoftwareModule(final FileUploadId newFileUploadId, final SoftwareModule softwareModule) { for (final Artifact artifact : softwareModule.getArtifacts()) { @@ -215,7 +221,7 @@ public abstract class AbstractFileTransferHandler implements Serializable { try { outputStream.close(); } catch (final IOException e1) { - LOG.warn("Closing output stream caused an exception {}", e1); + LOG.warn("Closing output stream caused by", e1); } } @@ -226,7 +232,7 @@ public abstract class AbstractFileTransferHandler implements Serializable { try { inputStream.close(); } catch (final IOException e1) { - LOG.warn("Closing input stream caused an exception {}", e1); + LOG.warn("Closing input stream caused by", e1); } } } @@ -276,13 +282,20 @@ public abstract class AbstractFileTransferHandler implements Serializable { streamToRepository(); } catch (final FileSizeQuotaExceededException e) { interruptUploadDueToFileSizeQuotaExceeded(e.getExceededQuotaValueString()); + publishUploadFailedAndFinishedEvent(fileUploadId); LOG.debug("Upload failed due to file size quota exceeded:", e); } catch (final StorageQuotaExceededException e) { interruptUploadDueToStorageQuotaExceeded(e.getExceededQuotaValueString()); + publishUploadFailedAndFinishedEvent(fileUploadId); LOG.debug("Upload failed due to storage quota exceeded:", e); } catch (final AssignmentQuotaExceededException e) { interruptUploadDueToAssignmentQuotaExceeded(); + publishUploadFailedAndFinishedEvent(fileUploadId); LOG.debug("Upload failed due to assignment quota exceeded:", e); + } catch (final ArtifactEncryptionUnsupportedException | ArtifactEncryptionFailedException e) { + interruptUploadDueToEncryptionError(); + publishUploadFailedAndFinishedEvent(fileUploadId); + LOG.warn("Upload failed due to encryption error", e); } catch (final RuntimeException e) { interruptUploadDueToUploadFailed(); publishUploadFailedAndFinishedEvent(fileUploadId); @@ -297,25 +310,28 @@ public abstract class AbstractFileTransferHandler implements Serializable { if (fileUploadId == null) { throw new ArtifactUploadFailedException(); } + final String filename = fileUploadId.getFilename(); - LOG.debug("Transfering file {} directly to repository", filename); final Artifact artifact = uploadArtifact(filename); + if (isUploadInterrupted()) { LOG.warn("Upload of {} was interrupted", filename); handleUploadFailure(artifact); publishUploadFinishedEvent(fileUploadId); return; } + publishUploadSucceeded(fileUploadId, artifact.getSize()); publishUploadFinishedEvent(fileUploadId); publishArtifactsChanged(fileUploadId); } private Artifact uploadArtifact(final String filename) { + LOG.debug("Transfering file {} directly to repository", filename); try { return artifactManagement.create(new ArtifactUpload(inputStream, fileUploadId.getSoftwareModuleId(), filename, null, null, null, true, mimeType, -1)); - } catch (final ArtifactUploadFailedException | InvalidSHA1HashException | InvalidMD5HashException e) { + } catch (final InvalidSHA1HashException | InvalidMD5HashException e) { throw new ArtifactUploadFailedException(e); } } diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/upload/FileUploadId.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/upload/FileUploadId.java index 339075b14..3639c551e 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/upload/FileUploadId.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/upload/FileUploadId.java @@ -9,9 +9,8 @@ package org.eclipse.hawkbit.ui.artifacts.upload; import java.io.Serializable; +import java.util.Objects; -import org.apache.commons.lang3.builder.EqualsBuilder; -import org.apache.commons.lang3.builder.HashCodeBuilder; import org.eclipse.hawkbit.repository.model.SoftwareModule; import org.eclipse.hawkbit.ui.utils.HawkbitCommonUtil; @@ -21,19 +20,14 @@ import org.eclipse.hawkbit.ui.utils.HawkbitCommonUtil; * */ public class FileUploadId implements Serializable { - private static final long serialVersionUID = 1L; private final String filename; + private final Long softwareModuleId; private final String softwareModuleName; - private final String softwareModuleVersion; - private Long softwareModuleId; - - private final String id; - /** * Creates a new {@link FileUploadId} instance. * @@ -44,36 +38,25 @@ public class FileUploadId implements Serializable { */ public FileUploadId(final String filename, final SoftwareModule softwareModule) { this.filename = filename; + this.softwareModuleId = softwareModule.getId(); this.softwareModuleName = softwareModule.getName(); this.softwareModuleVersion = softwareModule.getVersion(); - this.softwareModuleId = softwareModule.getId(); - this.id = createFileUploadIdString(filename, softwareModuleName, softwareModuleVersion); } - /** - * Creates a new {@link FileUploadId} instance. - * - * @param filename - * the name of the file - * @param softwareModuleName - * the name of a {@link SoftwareModule} for which the file is - * uploaded - * @param softwareModuleVersion - * the version of a {@link SoftwareModule} for which the file is - * uploaded - */ - public FileUploadId(final String filename, final String softwareModuleName, final String softwareModuleVersion) { - this.filename = filename; - this.softwareModuleName = softwareModuleName; - this.softwareModuleVersion = softwareModuleVersion; - this.id = createFileUploadIdString(filename, softwareModuleName, softwareModuleVersion); + public String getFilename() { + return filename; } - private static String createFileUploadIdString(final String filename, final String softwareModuleName, - final String softwareModuleVersion) { - return new StringBuilder(filename).append(":") - .append(HawkbitCommonUtil.getFormattedNameVersion(softwareModuleName, softwareModuleVersion)) - .toString(); + public Long getSoftwareModuleId() { + return softwareModuleId; + } + + public String getSoftwareModuleName() { + return softwareModuleName; + } + + public String getSoftwareModuleVersion() { + return softwareModuleVersion; } @Override @@ -88,52 +71,21 @@ public class FileUploadId implements Serializable { return false; } final FileUploadId other = (FileUploadId) obj; - return new EqualsBuilder().append(id, other.id).isEquals(); + return Objects.equals(this.getFilename(), other.getFilename()) + && Objects.equals(this.getSoftwareModuleId(), other.getSoftwareModuleId()) + && Objects.equals(this.getSoftwareModuleName(), other.getSoftwareModuleName()) + && Objects.equals(this.getSoftwareModuleVersion(), other.getSoftwareModuleVersion()); } @Override public int hashCode() { - return new HashCodeBuilder().append(id).toHashCode(); + return Objects.hash(getFilename(), getSoftwareModuleId(), getSoftwareModuleName(), getSoftwareModuleVersion()); } @Override public String toString() { - return id; - } - - /** - * Getter for the uploaded file name - * - * @return String - */ - public String getFilename() { - return filename; - } - - /** - * Getter for the software module name - * - * @return String - */ - public String getSoftwareModuleName() { - return softwareModuleName; - } - - /** - * Getter for the software module version - * - * @return String - */ - public String getSoftwareModuleVersion() { - return softwareModuleVersion; - } - - /** - * Getter for the software module ID - * - * @return Long - */ - public Long getSoftwareModuleId() { - return softwareModuleId; + return new StringBuilder(filename).append(":") + .append(HawkbitCommonUtil.getFormattedNameVersion(softwareModuleName, softwareModuleVersion)) + .toString(); } } diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/builder/FormComponentBuilder.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/builder/FormComponentBuilder.java index 93e5e6358..354ca1bb5 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/builder/FormComponentBuilder.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/builder/FormComponentBuilder.java @@ -377,9 +377,9 @@ public final class FormComponentBuilder { * setter for the binder * @return the bound box */ - public static CheckBox getCheckBox(final String id, final Binder binder, + public static CheckBox createCheckBox(final String id, final Binder binder, final ValueProvider getter, final Setter setter) { - return getCheckBox(null, id, binder, getter, setter); + return createCheckBox(null, id, binder, getter, setter); } /** @@ -399,7 +399,7 @@ public final class FormComponentBuilder { * setter for the binder * @return the bound box */ - public static CheckBox getCheckBox(final String caption, final String id, final Binder binder, + public static CheckBox createCheckBox(final String caption, final String id, final Binder binder, final ValueProvider getter, final Setter setter) { final CheckBox checkBox; if (StringUtils.isEmpty(caption)) { diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/data/mappers/SoftwareModuleToProxyMapper.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/data/mappers/SoftwareModuleToProxyMapper.java index 14e1acabd..c3e610176 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/data/mappers/SoftwareModuleToProxyMapper.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/data/mappers/SoftwareModuleToProxyMapper.java @@ -31,6 +31,7 @@ public class SoftwareModuleToProxyMapper proxySoftwareModule.setNameAndVersion( HawkbitCommonUtil.concatStrings(":", softwareModule.getName(), softwareModule.getVersion())); proxySoftwareModule.setVendor(softwareModule.getVendor()); + proxySoftwareModule.setEncrypted(softwareModule.isEncrypted()); final SoftwareModuleType type = softwareModule.getType(); final ProxyTypeInfo typeInfo = new ProxyTypeInfo(type.getId(), type.getName(), type.getKey()); diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/data/proxies/ProxySoftwareModule.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/data/proxies/ProxySoftwareModule.java index a917f4502..af42dc1c8 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/data/proxies/ProxySoftwareModule.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/data/proxies/ProxySoftwareModule.java @@ -29,6 +29,8 @@ public class ProxySoftwareModule extends ProxyNamedEntity implements VersionAwar private boolean assigned; + private boolean encrypted; + /** * Gets the software module vendor * @@ -129,4 +131,12 @@ public class ProxySoftwareModule extends ProxyNamedEntity implements VersionAwar public void setVersion(final String version) { this.version = version; } + + public boolean isEncrypted() { + return encrypted; + } + + public void setEncrypted(final boolean encrypted) { + this.encrypted = encrypted; + } } diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/detailslayout/MetaDataAddUpdateWindowLayoutComponentBuilder.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/detailslayout/MetaDataAddUpdateWindowLayoutComponentBuilder.java index afad332a7..ca3183915 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/detailslayout/MetaDataAddUpdateWindowLayoutComponentBuilder.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/detailslayout/MetaDataAddUpdateWindowLayoutComponentBuilder.java @@ -91,7 +91,7 @@ public class MetaDataAddUpdateWindowLayoutComponentBuilder { * @return Target field CheckBox */ public CheckBox createVisibleForTargetsField(final Binder binder) { - return FormComponentBuilder.getCheckBox(i18n.getMessage(TARGET_VISIBLE), + return FormComponentBuilder.createCheckBox(i18n.getMessage(TARGET_VISIBLE), UIComponentIdProvider.METADATA_TARGET_VISIBLE_ID, binder, ProxyMetaData::isVisibleForTargets, ProxyMetaData::setVisibleForTargets); } diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/detailslayout/SoftwareModuleDetails.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/detailslayout/SoftwareModuleDetails.java index 227bf6763..8cf08fc59 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/detailslayout/SoftwareModuleDetails.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/detailslayout/SoftwareModuleDetails.java @@ -54,7 +54,8 @@ public class SoftwareModuleDetails extends AbstractGridDetailsLayout implements MasterEntityAwareComponent { private final FilterSupport filterSupport; private final Function masterEntityToFilterMapper; + private final Consumer postMasterChangeCallback; private Long masterId; @@ -47,8 +49,24 @@ public class MasterEntitySupport implements M */ public MasterEntitySupport(final FilterSupport filterSupport, final Function masterEntityToFilterMapper) { + this(filterSupport, masterEntityToFilterMapper, null); + } + + /** + * Constructor for MasterEntitySupport + * + * @param filterSupport + * Filter support + * @param masterEntityToFilterMapper + * Master entity to filter mapper + * @param postMasterChangeCallback + * Callback called after master entity change + */ + public MasterEntitySupport(final FilterSupport filterSupport, final Function masterEntityToFilterMapper, + final Consumer postMasterChangeCallback) { this.filterSupport = filterSupport; this.masterEntityToFilterMapper = masterEntityToFilterMapper; + this.postMasterChangeCallback = postMasterChangeCallback; } @Override @@ -60,6 +78,10 @@ public class MasterEntitySupport implements M filterSupport.updateFilter(FilterType.MASTER, getMasterEntityFilter(masterEntity)); masterId = masterEntity != null ? masterEntity.getId() : null; + + if (postMasterChangeCallback != null) { + postMasterChangeCallback.accept(masterEntity); + } } private Object getMasterEntityFilter(final M masterEntity) { diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/distributions/disttype/SmTypeSelectedGrid.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/distributions/disttype/SmTypeSelectedGrid.java index 7e29dc538..5accbe6a3 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/distributions/disttype/SmTypeSelectedGrid.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/distributions/disttype/SmTypeSelectedGrid.java @@ -82,6 +82,6 @@ public class SmTypeSelectedGrid extends Grid { binder.addValueChangeListener(event -> mandatoryPropertyChangedCallback.run()); final String id = "selected.sm.type." + smType.getId(); - return FormComponentBuilder.getCheckBox(id, binder, ProxyType::isMandatory, ProxyType::setMandatory); + return FormComponentBuilder.createCheckBox(id, binder, ProxyType::isMandatory, ProxyType::setMandatory); } } diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/distributions/dstable/DsWindowLayoutComponentBuilder.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/distributions/dstable/DsWindowLayoutComponentBuilder.java index 65edb79ce..16f8d506b 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/distributions/dstable/DsWindowLayoutComponentBuilder.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/distributions/dstable/DsWindowLayoutComponentBuilder.java @@ -107,7 +107,7 @@ public class DsWindowLayoutComponentBuilder { * @return Migration step required checkbox */ public CheckBox createMigrationStepField(final Binder binder) { - final CheckBox migrationRequired = FormComponentBuilder.getCheckBox(i18n.getMessage(MIGRATION_STEP), + final CheckBox migrationRequired = FormComponentBuilder.createCheckBox(i18n.getMessage(MIGRATION_STEP), UIComponentIdProvider.DIST_ADD_MIGRATION_CHECK, binder, ProxyDistributionSet::isRequiredMigrationStep, ProxyDistributionSet::setRequiredMigrationStep); diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/error/extractors/ArtifactEncryptionErrorExtractor.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/error/extractors/ArtifactEncryptionErrorExtractor.java new file mode 100644 index 000000000..09fd6e035 --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/error/extractors/ArtifactEncryptionErrorExtractor.java @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2021 Bosch.IO GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.ui.error.extractors; + +import java.util.Optional; + +import org.eclipse.hawkbit.repository.exception.ArtifactEncryptionFailedException; +import org.eclipse.hawkbit.repository.exception.ArtifactEncryptionUnsupportedException; +import org.eclipse.hawkbit.ui.error.UiErrorDetails; +import org.eclipse.hawkbit.ui.utils.UIMessageIdProvider; +import org.eclipse.hawkbit.ui.utils.VaadinMessageSource; + +/** + * UI error details extractor for {@link ArtifactEncryptionUnsupportedException} + * and {@link ArtifactEncryptionFailedException}. + */ +public class ArtifactEncryptionErrorExtractor extends AbstractSingleUiErrorDetailsExtractor { + private final VaadinMessageSource i18n; + + /** + * Constructor for {@link ArtifactEncryptionErrorExtractor}. + * + * @param i18n + * Message source used for localization + */ + public ArtifactEncryptionErrorExtractor(final VaadinMessageSource i18n) { + this.i18n = i18n; + } + + @Override + protected Optional findDetails(final Throwable error) { + return findExceptionOf(error, ArtifactEncryptionUnsupportedException.class) + .map(ex -> UiErrorDetails.create(i18n.getMessage(UIMessageIdProvider.CAPTION_ERROR), + i18n.getMessage(UIMessageIdProvider.MESSAGE_ERROR_ENCRYPTION_NOT_SUPPORTED))) + .map(Optional::of) + .orElseGet(() -> findExceptionOf(error, ArtifactEncryptionFailedException.class) + .map(ex -> UiErrorDetails.create(i18n.getMessage(UIMessageIdProvider.CAPTION_ERROR), + getEncryptionFailedDetailsMsg(ex)))); + } + + private String getEncryptionFailedDetailsMsg(final ArtifactEncryptionFailedException ex) { + switch (ex.getEncryptionOperation()) { + case GENERATE_SECRETS: + return i18n.getMessage(UIMessageIdProvider.MESSAGE_ERROR_ENCRYPTION_SECRETS_FAILED); + case ENCRYPT: + return i18n.getMessage(UIMessageIdProvider.MESSAGE_ERROR_ENCRYPTION_FAILED); + case DECRYPT: + return i18n.getMessage(UIMessageIdProvider.MESSAGE_ERROR_DECRYPTION_FAILED); + default: + return i18n.getMessage(UIMessageIdProvider.MESSAGE_ERROR_ENCRYPTION_FAILED); + } + } +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/error/extractors/UploadErrorExtractor.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/error/extractors/UploadErrorExtractor.java index 284dd50ad..16cb95884 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/error/extractors/UploadErrorExtractor.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/error/extractors/UploadErrorExtractor.java @@ -21,7 +21,7 @@ public class UploadErrorExtractor extends AbstractSingleUiErrorDetailsExtractor @Override protected Optional findDetails(final Throwable error) { - // UploadException is ignored + // UploadException is ignored as it is handled explicitly return findExceptionOf(error, UploadException.class).map(ex -> UiErrorDetails.empty()); } } diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/AutoAssignmentWindowLayoutComponentBuilder.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/AutoAssignmentWindowLayoutComponentBuilder.java index 60a208dfc..9f7696748 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/AutoAssignmentWindowLayoutComponentBuilder.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/AutoAssignmentWindowLayoutComponentBuilder.java @@ -65,7 +65,7 @@ public class AutoAssignmentWindowLayoutComponentBuilder { */ public CheckBox createEnableCheckbox(final Binder binder) { final String caption = i18n.getMessage(UIMessageIdProvider.LABEL_AUTO_ASSIGNMENT_ENABLE); - return FormComponentBuilder.getCheckBox(caption, UIComponentIdProvider.DIST_SET_SELECT_ENABLE_ID, binder, + return FormComponentBuilder.createCheckBox(caption, UIComponentIdProvider.DIST_SET_SELECT_ENABLE_ID, binder, ProxyTargetFilterQuery::isAutoAssignmentEnabled, ProxyTargetFilterQuery::setAutoAssignmentEnabled); } diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/miscs/AssignmentWindowLayoutComponentBuilder.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/miscs/AssignmentWindowLayoutComponentBuilder.java index 24bf626e8..e9382dab7 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/miscs/AssignmentWindowLayoutComponentBuilder.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/miscs/AssignmentWindowLayoutComponentBuilder.java @@ -81,7 +81,7 @@ public class AssignmentWindowLayoutComponentBuilder { * @return Maintenance window checkbox */ public CheckBox createEnableMaintenanceWindowToggle(final Binder binder) { - final CheckBox maintenanceWindowToggle = FormComponentBuilder.getCheckBox( + final CheckBox maintenanceWindowToggle = FormComponentBuilder.createCheckBox( i18n.getMessage("caption.maintenancewindow.enabled"), UIComponentIdProvider.MAINTENANCE_WINDOW_ENABLED_ID, binder, ProxyAssignmentWindow::isMaintenanceWindowEnabled, ProxyAssignmentWindow::setMaintenanceWindowEnabled); diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/tenantconfiguration/AuthenticationConfigurationView.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/tenantconfiguration/AuthenticationConfigurationView.java index 0d286a6b7..8807961b9 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/tenantconfiguration/AuthenticationConfigurationView.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/tenantconfiguration/AuthenticationConfigurationView.java @@ -106,7 +106,7 @@ public class AuthenticationConfigurationView extends BaseConfigurationView3.9.0 - 9.1.3 + 9.1.6 1.14.2 2.13.6 2.7.9