diff --git a/examples/hawkbit-device-simulator/src/main/java/org/eclipse/hawkbit/simulator/SimulatorStartup.java b/examples/hawkbit-device-simulator/src/main/java/org/eclipse/hawkbit/simulator/SimulatorStartup.java index 624c26b72..71aeade1c 100644 --- a/examples/hawkbit-device-simulator/src/main/java/org/eclipse/hawkbit/simulator/SimulatorStartup.java +++ b/examples/hawkbit-device-simulator/src/main/java/org/eclipse/hawkbit/simulator/SimulatorStartup.java @@ -16,8 +16,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.ApplicationListener; -import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.stereotype.Component; /** @@ -26,7 +26,7 @@ import org.springframework.stereotype.Component; */ @Component @ConditionalOnProperty(prefix = "hawkbit.device.simulator", name = "autostart", matchIfMissing = true) -public class SimulatorStartup implements ApplicationListener { +public class SimulatorStartup implements ApplicationListener { private static final Logger LOGGER = LoggerFactory.getLogger(SimulatorStartup.class); @Autowired @@ -42,7 +42,7 @@ public class SimulatorStartup implements ApplicationListener { for (int i = 0; i < autostart.getAmount(); i++) { final String deviceId = autostart.getName() + i; diff --git a/extensions/hawkbit-extension-artifact-repository-mongo/pom.xml b/extensions/hawkbit-extension-artifact-repository-mongo/pom.xml index e4aa90a2c..e796a0d24 100644 --- a/extensions/hawkbit-extension-artifact-repository-mongo/pom.xml +++ b/extensions/hawkbit-extension-artifact-repository-mongo/pom.xml @@ -54,6 +54,11 @@ allure-junit-adaptor test + + org.springframework.boot + spring-boot-starter-logging + test + de.flapdoodle.embed diff --git a/extensions/hawkbit-extension-artifact-repository-mongo/src/main/java/org/eclipse/hawkbit/artifact/repository/MongoDBArtifactStore.java b/extensions/hawkbit-extension-artifact-repository-mongo/src/main/java/org/eclipse/hawkbit/artifact/repository/MongoDBArtifactStore.java index 2b514d39e..5e8909ed3 100644 --- a/extensions/hawkbit-extension-artifact-repository-mongo/src/main/java/org/eclipse/hawkbit/artifact/repository/MongoDBArtifactStore.java +++ b/extensions/hawkbit-extension-artifact-repository-mongo/src/main/java/org/eclipse/hawkbit/artifact/repository/MongoDBArtifactStore.java @@ -24,13 +24,13 @@ import java.util.stream.Collectors; import org.eclipse.hawkbit.artifact.repository.model.DbArtifact; import org.eclipse.hawkbit.artifact.repository.model.DbArtifactHash; +import org.eclipse.hawkbit.tenancy.TenantAware; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.gridfs.GridFsOperations; +import org.springframework.validation.annotation.Validated; import com.google.common.io.BaseEncoding; import com.google.common.io.ByteStreams; @@ -44,6 +44,7 @@ import com.mongodb.gridfs.GridFSFile; * The file management which looks up all the file in the file tore. * */ +@Validated public class MongoDBArtifactStore implements ArtifactRepository { private static final Logger LOGGER = LoggerFactory.getLogger(MongoDBArtifactStore.class); @@ -56,9 +57,14 @@ public class MongoDBArtifactStore implements ArtifactRepository { private static final String FILENAME = "filename"; /** - * The mongoDB field which automatically calculated by the mongoDB. + * The mongoDB field which holds the tenant of the file to download. */ - private static final String MD5 = "md5"; + private static final String TENANT = "tenant"; + + /** + * Query by {@link TenantAware} field. + */ + private static final String TENANT_QUERY = "metadata." + TENANT; /** * The mongoDB field which holds the SHA1 hash, stored in the meta data @@ -68,10 +74,11 @@ public class MongoDBArtifactStore implements ArtifactRepository { private static final String ID = "_id"; - @Autowired - private GridFsOperations gridFs; + private final GridFsOperations gridFs; - MongoTemplate mongoTemplate; + MongoDBArtifactStore(final GridFsOperations gridFs) { + this.gridFs = gridFs; + } /** * Retrieves a {@link GridFSDBFile} from the store by it's SHA1 hash. @@ -82,36 +89,28 @@ public class MongoDBArtifactStore implements ArtifactRepository { * @return The DbArtifact object or {@code null} if no file exists. */ @Override - public DbArtifact getArtifactBySha1(final String sha1Hash) { - return map(gridFs.findOne(new Query().addCriteria(Criteria.where(FILENAME).is(sha1Hash)))); - } + public DbArtifact getArtifactBySha1(final String tenant, final String sha1Hash) { - /** - * Retrieves a {@link GridFSDBFile} from the store by it's MD5 hash. - * - * @param md5Hash - * the md5-hash of the file to lookup. - * @return The gridfs file object or {@code null} if no file exists. - */ - public DbArtifact getArtifactByMd5(final String md5Hash) { - return map(gridFs.findOne(new Query().addCriteria(Criteria.where(MD5).is(md5Hash)))); + return map(gridFs.findOne(new Query() + .addCriteria(Criteria.where(FILENAME).is(sha1Hash).and(TENANT_QUERY).is(sanitizeTenant(tenant))))); } @Override - public DbArtifact store(final InputStream content, final String filename, final String contentType) { - return store(content, filename, contentType, null); + public DbArtifact store(final String tenant, final InputStream content, final String filename, + final String contentType) { + return store(tenant, content, filename, contentType, null); } @Override - public DbArtifact store(final InputStream content, final String filename, final String contentType, - final DbArtifactHash hash) { + public DbArtifact store(final String tenant, final InputStream content, final String filename, + final String contentType, final DbArtifactHash hash) { File tempFile = null; try { LOGGER.debug("storing file {} of content {}", filename, contentType); tempFile = File.createTempFile("uploadFile", null); try (final BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(tempFile))) { try (BufferedInputStream bis = new BufferedInputStream(content)) { - return store(bis, contentType, bos, tempFile, hash); + return store(tenant, bis, contentType, bos, tempFile, hash); } } } catch (final IOException | MongoException e1) { @@ -123,10 +122,15 @@ public class MongoDBArtifactStore implements ArtifactRepository { } } + private static String sanitizeTenant(final String tenant) { + return tenant.trim().toUpperCase(); + } + @Override - public void deleteBySha1(final String sha1Hash) { + public void deleteBySha1(final String tenant, final String sha1Hash) { try { - deleteArtifact(gridFs.findOne(new Query().addCriteria(Criteria.where(FILENAME).is(sha1Hash)))); + deleteArtifact(gridFs.findOne(new Query() + .addCriteria(Criteria.where(FILENAME).is(sha1Hash).and(TENANT_QUERY).is(sanitizeTenant(tenant))))); } catch (final MongoException e) { throw new ArtifactStoreException(e.getMessage(), e); } @@ -143,18 +147,21 @@ public class MongoDBArtifactStore implements ArtifactRepository { } - private DbArtifact store(final InputStream content, final String contentType, final OutputStream os, + private DbArtifact store(final String t, final InputStream content, final String contentType, final OutputStream os, final File tempFile, final DbArtifactHash hash) { final GridFsArtifact storedArtifact; + final String tenant = sanitizeTenant(t); try { final String sha1Hash = computeSHA1Hash(content, os, hash != null ? hash.getSha1() : null); // upload if it does not exist already, check if file exists, not // tenant specific. - final GridFSDBFile result = gridFs.findOne(new Query().addCriteria(Criteria.where(FILENAME).is(sha1Hash))); - if (null == result) { + final GridFSDBFile result = gridFs.findOne( + new Query().addCriteria(Criteria.where(FILENAME).is(sha1Hash).and(TENANT_QUERY).is(tenant))); + if (result == null) { try (FileInputStream inputStream = new FileInputStream(tempFile)) { final BasicDBObject metadata = new BasicDBObject(); metadata.put(SHA1, sha1Hash); + metadata.put(TENANT, tenant); storedArtifact = map(gridFs.store(inputStream, sha1Hash, contentType, metadata)); } } else { @@ -244,4 +251,13 @@ public class MongoDBArtifactStore implements ArtifactRepository { artifact.setHashes(new DbArtifactHash(fsFile.getFilename(), fsFile.getMD5())); return artifact; } + + @Override + public void deleteByTenant(final String tenant) { + try { + gridFs.delete(new Query().addCriteria(Criteria.where(TENANT_QUERY).is(sanitizeTenant(tenant)))); + } catch (final MongoClientException e) { + throw new ArtifactStoreException(e.getMessage(), e); + } + } } diff --git a/extensions/hawkbit-extension-artifact-repository-mongo/src/main/java/org/eclipse/hawkbit/artifact/repository/MongoDBArtifactStoreAutoConfiguration.java b/extensions/hawkbit-extension-artifact-repository-mongo/src/main/java/org/eclipse/hawkbit/artifact/repository/MongoDBArtifactStoreAutoConfiguration.java index 9308600d3..bc3d9afda 100644 --- a/extensions/hawkbit-extension-artifact-repository-mongo/src/main/java/org/eclipse/hawkbit/artifact/repository/MongoDBArtifactStoreAutoConfiguration.java +++ b/extensions/hawkbit-extension-artifact-repository-mongo/src/main/java/org/eclipse/hawkbit/artifact/repository/MongoDBArtifactStoreAutoConfiguration.java @@ -12,6 +12,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; +import org.springframework.data.mongodb.gridfs.GridFsOperations; /** * Auto configuration for the {@link MongoDBArtifactStore}. @@ -25,7 +26,7 @@ public class MongoDBArtifactStoreAutoConfiguration { * @return Default {@link ArtifactRepository} implementation. */ @Bean - public ArtifactRepository artifactRepository() { - return new MongoDBArtifactStore(); + ArtifactRepository artifactRepository(final GridFsOperations gridFs) { + return new MongoDBArtifactStore(gridFs); } } diff --git a/extensions/hawkbit-extension-artifact-repository-mongo/src/test/java/org/eclipse/hawkbit/artifact/repository/MongoDBArtifactStoreTest.java b/extensions/hawkbit-extension-artifact-repository-mongo/src/test/java/org/eclipse/hawkbit/artifact/repository/MongoDBArtifactStoreTest.java index 3e565de1c..b2bfdfde1 100644 --- a/extensions/hawkbit-extension-artifact-repository-mongo/src/test/java/org/eclipse/hawkbit/artifact/repository/MongoDBArtifactStoreTest.java +++ b/extensions/hawkbit-extension-artifact-repository-mongo/src/test/java/org/eclipse/hawkbit/artifact/repository/MongoDBArtifactStoreTest.java @@ -20,54 +20,75 @@ import org.eclipse.hawkbit.artifact.TestConfiguration; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.SpringApplicationConfiguration; -import org.springframework.data.mongodb.gridfs.GridFsOperations; -import org.springframework.test.context.TestPropertySource; +import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import com.google.common.io.BaseEncoding; import ru.yandex.qatools.allure.annotations.Description; import ru.yandex.qatools.allure.annotations.Features; +import ru.yandex.qatools.allure.annotations.Step; import ru.yandex.qatools.allure.annotations.Stories; @Features("Component Tests - Repository") @Stories("Artifact Store MongoDB") @RunWith(SpringJUnit4ClassRunner.class) -@SpringApplicationConfiguration(classes = { MongoDBArtifactStoreAutoConfiguration.class, TestConfiguration.class }) -@TestPropertySource(properties = { "spring.data.mongodb.port=0", "spring.mongodb.embedded.version=3.2.7" }) +@SpringBootTest(classes = { MongoDBArtifactStoreAutoConfiguration.class, TestConfiguration.class }, properties = { + "spring.data.mongodb.port=0", "spring.mongodb.embedded.version=3.4.4" }) public class MongoDBArtifactStoreTest { + private static final String TENANT = "test_tenant"; + private static final String TENANT2 = "test_tenant2"; @Autowired private MongoDBArtifactStore artifactStoreUnderTest; - @Autowired - private GridFsOperations gridFs; - @Test @Description("Ensures that search by SHA1 hash (which is used by hawkBit as artifact ID) finds the expected results.") public void findArtifactBySHA1Hash() throws NoSuchAlgorithmException { + + final String sha1 = storeRandomArifactAndVerify(TENANT); + final String sha2 = storeRandomArifactAndVerify(TENANT2); + + assertThat(artifactStoreUnderTest.getArtifactBySha1(TENANT2, sha1)).isNull(); + assertThat(artifactStoreUnderTest.getArtifactBySha1(TENANT, sha2)).isNull(); + } + + @Step + private String storeRandomArifactAndVerify(final String tenant) throws NoSuchAlgorithmException { final int filelengthBytes = 128; final String filename = "testfile.json"; final String contentType = "application/json"; final DigestInputStream digestInputStream = digestInputStream(generateInputStream(filelengthBytes), "SHA-1"); - artifactStoreUnderTest.store(digestInputStream, filename, contentType); - assertThat(artifactStoreUnderTest.getArtifactBySha1( - BaseEncoding.base16().lowerCase().encode(digestInputStream.getMessageDigest().digest()))).isNotNull(); + artifactStoreUnderTest.store(tenant, digestInputStream, filename, contentType); + + final String sha1 = BaseEncoding.base16().lowerCase().encode(digestInputStream.getMessageDigest().digest()); + assertThat(artifactStoreUnderTest.getArtifactBySha1(tenant, sha1)).isNotNull(); + return sha1; } @Test - @Description("Ensures that search by MD5 hash finds the expected results.") - public void findArtifactByMD5Hash() throws NoSuchAlgorithmException { - final int filelengthBytes = 128; - final String filename = "testfile.json"; - final String contentType = "application/json"; + @Description("Deletes file from repository identified by SHA1 hash as filename.") + public void deleteArtifactBySHA1Hash() throws NoSuchAlgorithmException { - final DigestInputStream digestInputStream = digestInputStream(generateInputStream(filelengthBytes), "MD5"); - artifactStoreUnderTest.store(digestInputStream, filename, contentType); - assertThat(artifactStoreUnderTest.getArtifactByMd5( - BaseEncoding.base16().lowerCase().encode(digestInputStream.getMessageDigest().digest()))).isNotNull(); + final String sha1 = storeRandomArifactAndVerify(TENANT); + + artifactStoreUnderTest.deleteBySha1(TENANT, sha1); + assertThat(artifactStoreUnderTest.getArtifactBySha1(TENANT, sha1)).isNull(); + } + + @Test + @Description("Verfies that all data of a tenant is erased if repository is asked to do so. " + + "Data of other tenants is not affected.") + public void deleteTenant() throws NoSuchAlgorithmException { + + final String shaDeleted = storeRandomArifactAndVerify(TENANT); + final String shaUndeleted = storeRandomArifactAndVerify("another_tenant"); + + artifactStoreUnderTest.deleteByTenant("tenant_that_does_not_exist"); + artifactStoreUnderTest.deleteByTenant(TENANT); + assertThat(artifactStoreUnderTest.getArtifactBySha1(TENANT, shaDeleted)).isNull(); + assertThat(artifactStoreUnderTest.getArtifactBySha1("another_tenant", shaUndeleted)).isNotNull(); } private static ByteArrayInputStream generateInputStream(final int length) { diff --git a/extensions/hawkbit-extension-artifact-repository-s3/src/main/java/org/eclipse/hawkbit/artifact/repository/S3Artifact.java b/extensions/hawkbit-extension-artifact-repository-s3/src/main/java/org/eclipse/hawkbit/artifact/repository/S3Artifact.java index 30603722b..1e1da2543 100644 --- a/extensions/hawkbit-extension-artifact-repository-s3/src/main/java/org/eclipse/hawkbit/artifact/repository/S3Artifact.java +++ b/extensions/hawkbit-extension-artifact-repository-s3/src/main/java/org/eclipse/hawkbit/artifact/repository/S3Artifact.java @@ -22,22 +22,22 @@ public class S3Artifact extends DbArtifact { private final AmazonS3 amazonS3; private final S3RepositoryProperties s3Properties; - private final String sha1; + private final String key; - S3Artifact(final AmazonS3 amazonS3, final S3RepositoryProperties s3Properties, final String sha1) { + S3Artifact(final AmazonS3 amazonS3, final S3RepositoryProperties s3Properties, final String key) { this.amazonS3 = amazonS3; this.s3Properties = s3Properties; - this.sha1 = sha1; + this.key = key; } @Override public InputStream getFileInputStream() { - return amazonS3.getObject(s3Properties.getBucketName(), sha1).getObjectContent(); + return amazonS3.getObject(s3Properties.getBucketName(), key).getObjectContent(); } @Override public String toString() { - return "S3Artifact [sha1=" + sha1 + ", getArtifactId()=" + getArtifactId() + ", getHashes()=" + getHashes() + return "S3Artifact [key=" + key + ", getArtifactId()=" + getArtifactId() + ", getHashes()=" + getHashes() + ", getSize()=" + getSize() + ", getContentType()=" + getContentType() + "]"; } } diff --git a/extensions/hawkbit-extension-artifact-repository-s3/src/main/java/org/eclipse/hawkbit/artifact/repository/S3Repository.java b/extensions/hawkbit-extension-artifact-repository-s3/src/main/java/org/eclipse/hawkbit/artifact/repository/S3Repository.java index 18a77414c..f522ef215 100644 --- a/extensions/hawkbit-extension-artifact-repository-s3/src/main/java/org/eclipse/hawkbit/artifact/repository/S3Repository.java +++ b/extensions/hawkbit-extension-artifact-repository-s3/src/main/java/org/eclipse/hawkbit/artifact/repository/S3Repository.java @@ -24,14 +24,17 @@ import org.eclipse.hawkbit.artifact.repository.model.DbArtifact; import org.eclipse.hawkbit.artifact.repository.model.DbArtifactHash; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.validation.annotation.Validated; import com.amazonaws.AmazonClientException; import com.amazonaws.RequestClientOptions; import com.amazonaws.services.s3.AmazonS3; import com.amazonaws.services.s3.Headers; import com.amazonaws.services.s3.model.DeleteObjectRequest; +import com.amazonaws.services.s3.model.ObjectListing; import com.amazonaws.services.s3.model.ObjectMetadata; import com.amazonaws.services.s3.model.S3Object; +import com.amazonaws.services.s3.model.S3ObjectSummary; import com.google.common.io.BaseEncoding; import com.google.common.io.ByteStreams; @@ -48,6 +51,7 @@ import com.google.common.io.ByteStreams; * across several buckets. *

*/ +@Validated public class S3Repository implements ArtifactRepository { private static final Logger LOG = LoggerFactory.getLogger(S3Repository.class); @@ -73,16 +77,17 @@ public class S3Repository implements ArtifactRepository { } @Override - public DbArtifact store(final InputStream content, final String filename, final String contentType) { - return store(content, filename, contentType, null); + public DbArtifact store(final String tenant, final InputStream content, final String filename, + final String contentType) { + return store(tenant, content, filename, contentType, null); } @Override // suppress warning, of not strong enough hashing algorithm, SHA-1 and MD5 // is not used security related @SuppressWarnings("squid:S2070") - public DbArtifact store(final InputStream content, final String filename, final String contentType, - final DbArtifactHash hash) { + public DbArtifact store(final String tenant, final InputStream content, final String filename, + final String contentType, final DbArtifactHash hash) { final MessageDigest mdSHA1; final MessageDigest mdMD5; try { @@ -102,7 +107,7 @@ public class S3Repository implements ArtifactRepository { final String sha1Hash16 = BaseEncoding.base16().lowerCase().encode(mdSHA1.digest()); final String md5Hash16 = BaseEncoding.base16().lowerCase().encode(mdMD5.digest()); - return store(sha1Hash16, md5Hash16, contentType, file, hash); + return store(tenant, sha1Hash16, md5Hash16, contentType, file, hash); } catch (final IOException e) { throw new ArtifactStoreException(e.getMessage(), e); } finally { @@ -112,15 +117,16 @@ public class S3Repository implements ArtifactRepository { } } - private DbArtifact store(final String sha1Hash16, final String mdMD5Hash16, final String contentType, - final File file, final DbArtifactHash hash) { - final S3Artifact s3Artifact = createS3Artifact(sha1Hash16, mdMD5Hash16, contentType, file); + private DbArtifact store(final String tenant, final String sha1Hash16, final String mdMD5Hash16, + final String contentType, final File file, final DbArtifactHash hash) { + final S3Artifact s3Artifact = createS3Artifact(tenant, sha1Hash16, mdMD5Hash16, contentType, file); checkHashes(s3Artifact, hash); + final String key = objectKey(tenant, sha1Hash16); LOG.info("Storing file {} with length {} to AWS S3 bucket {} as SHA1 {}", file.getName(), file.length(), - s3Properties.getBucketName(), sha1Hash16); + s3Properties.getBucketName(), key); - if (exists(sha1Hash16)) { + if (exists(key)) { LOG.debug("Artifact {} already exists on S3 bucket {}, don't need to upload twice", sha1Hash16, s3Properties.getBucketName()); return s3Artifact; @@ -129,7 +135,7 @@ public class S3Repository implements ArtifactRepository { try (final InputStream inputStream = new BufferedInputStream(new FileInputStream(file), RequestClientOptions.DEFAULT_STREAM_BUFFER_SIZE)) { final ObjectMetadata objectMetadata = createObjectMetadata(mdMD5Hash16, contentType, file); - amazonS3.putObject(s3Properties.getBucketName(), sha1Hash16, inputStream, objectMetadata); + amazonS3.putObject(s3Properties.getBucketName(), key, inputStream, objectMetadata); return s3Artifact; } catch (final IOException | AmazonClientException e) { @@ -137,14 +143,15 @@ public class S3Repository implements ArtifactRepository { } } - private S3Artifact createS3Artifact(final String sha1Hash16, final String mdMD5Hash16, final String contentType, - final File file) { - final S3Artifact s3Artifact = new S3Artifact(amazonS3, s3Properties, sha1Hash16); + private S3Artifact createS3Artifact(final String tenant, final String sha1Hash, final String mdMD5Hash16, + final String contentType, final File file) { + final String key = objectKey(tenant, sha1Hash); + final S3Artifact s3Artifact = new S3Artifact(amazonS3, s3Properties, key); s3Artifact.setContentType(contentType); - s3Artifact.setArtifactId(sha1Hash16); + s3Artifact.setArtifactId(sha1Hash); s3Artifact.setSize(file.length()); s3Artifact.setContentType(contentType); - s3Artifact.setHashes(new DbArtifactHash(sha1Hash16, mdMD5Hash16)); + s3Artifact.setHashes(new DbArtifactHash(sha1Hash, mdMD5Hash16)); return s3Artifact; } @@ -162,26 +169,34 @@ public class S3Repository implements ArtifactRepository { } @Override - public void deleteBySha1(final String sha1Hash) { - LOG.info("Deleting S3 object from bucket {} and key {}", s3Properties.getBucketName(), sha1Hash); - amazonS3.deleteObject(new DeleteObjectRequest(s3Properties.getBucketName(), sha1Hash)); + public void deleteBySha1(final String tenant, final String sha1Hash) { + final String key = objectKey(tenant, sha1Hash); + + LOG.info("Deleting S3 object from bucket {} and key {}", s3Properties.getBucketName(), key); + amazonS3.deleteObject(new DeleteObjectRequest(s3Properties.getBucketName(), key)); + } + + private static String objectKey(final String tenant, final String sha1Hash) { + return sanitizeTenant(tenant) + "/" + sha1Hash; } @Override - public DbArtifact getArtifactBySha1(final String sha1) { - LOG.info("Retrieving S3 object from bucket {} and key {}", s3Properties.getBucketName(), sha1); - final S3Object s3Object = amazonS3.getObject(s3Properties.getBucketName(), sha1); + public DbArtifact getArtifactBySha1(final String tenant, final String sha1Hash) { + final String key = objectKey(tenant, sha1Hash); + + LOG.info("Retrieving S3 object from bucket {} and key {}", s3Properties.getBucketName(), key); + final S3Object s3Object = amazonS3.getObject(s3Properties.getBucketName(), key); if (s3Object == null) { return null; } final ObjectMetadata s3ObjectMetadata = s3Object.getObjectMetadata(); - final S3Artifact s3Artifact = new S3Artifact(amazonS3, s3Properties, sha1); - s3Artifact.setArtifactId(sha1); + final S3Artifact s3Artifact = new S3Artifact(amazonS3, s3Properties, key); + s3Artifact.setArtifactId(sha1Hash); s3Artifact.setSize(s3ObjectMetadata.getContentLength()); // the MD5Content is stored in the ETag - s3Artifact.setHashes(new DbArtifactHash(sha1, + s3Artifact.setHashes(new DbArtifactHash(sha1Hash, BaseEncoding.base16().lowerCase().encode(BaseEncoding.base64().decode(s3ObjectMetadata.getETag())))); s3Artifact.setContentType(s3ObjectMetadata.getContentType()); return s3Artifact; @@ -220,4 +235,25 @@ public class S3Repository implements ArtifactRepository { private boolean exists(final String sha1) { return amazonS3.doesObjectExist(s3Properties.getBucketName(), sha1); } + + @Override + public void deleteByTenant(final String tenant) { + final String folder = sanitizeTenant(tenant); + + LOG.info("Deleting S3 object folder (tenant) from bucket {} and key {}", s3Properties.getBucketName(), folder); + + // Delete artifacts + ObjectListing objects = amazonS3.listObjects(s3Properties.getBucketName(), folder + "/"); + do { + for (final S3ObjectSummary objectSummary : objects.getObjectSummaries()) { + amazonS3.deleteObject(s3Properties.getBucketName(), objectSummary.getKey()); + } + objects = amazonS3.listNextBatchOfObjects(objects); + } while (objects.isTruncated()); + + } + + private static String sanitizeTenant(final String tenant) { + return tenant.trim().toUpperCase(); + } } diff --git a/extensions/hawkbit-extension-artifact-repository-s3/src/test/java/org/eclipse/hawkbit/artifact/repository/S3RepositoryTest.java b/extensions/hawkbit-extension-artifact-repository-s3/src/test/java/org/eclipse/hawkbit/artifact/repository/S3RepositoryTest.java index 033e9e00e..6eb79e2bd 100644 --- a/extensions/hawkbit-extension-artifact-repository-s3/src/test/java/org/eclipse/hawkbit/artifact/repository/S3RepositoryTest.java +++ b/extensions/hawkbit-extension-artifact-repository-s3/src/test/java/org/eclipse/hawkbit/artifact/repository/S3RepositoryTest.java @@ -54,6 +54,7 @@ import ru.yandex.qatools.allure.annotations.Stories; @Features("Unit Tests - S3 Repository") @Stories("S3 Artifact Repository") public class S3RepositoryTest { + private static final String TENANT = "test_tenant"; @Mock private AmazonS3 amazonS3Mock; @@ -89,8 +90,9 @@ public class S3RepositoryTest { storeRandomBytes(rndBytes, knownContentType); // verify - Mockito.verify(amazonS3Mock).putObject(eq(s3Properties.getBucketName()), eq(knownSHA1), - inputStreamCaptor.capture(), objectMetaDataCaptor.capture()); + Mockito.verify(amazonS3Mock).putObject(eq(s3Properties.getBucketName()), + eq(TENANT.toUpperCase() + "/" + knownSHA1), inputStreamCaptor.capture(), + objectMetaDataCaptor.capture()); final ObjectMetadata recordedObjectMetadata = objectMetaDataCaptor.getValue(); assertThat(recordedObjectMetadata.getContentType()).isEqualTo(knownContentType); @@ -115,7 +117,7 @@ public class S3RepositoryTest { when(s3ObjectMetadataMock.getContentType()).thenReturn(knownContentType); // test - final DbArtifact artifactBySha1 = s3RepositoryUnderTest.getArtifactBySha1(knownSHA1Hash); + final DbArtifact artifactBySha1 = s3RepositoryUnderTest.getArtifactBySha1(TENANT, knownSHA1Hash); // verify assertThat(artifactBySha1.getArtifactId()).isEqualTo(knownSHA1Hash); @@ -150,7 +152,7 @@ public class S3RepositoryTest { when(amazonS3Mock.getObject(s3Properties.getBucketName(), knownSHA1Hash)).thenReturn(null); // test - final DbArtifact artifactBySha1NotExists = s3RepositoryUnderTest.getArtifactBySha1(knownSHA1Hash); + final DbArtifact artifactBySha1NotExists = s3RepositoryUnderTest.getArtifactBySha1(TENANT, knownSHA1Hash); // verify assertThat(artifactBySha1NotExists).isNull(); @@ -201,7 +203,7 @@ public class S3RepositoryTest { throws IOException { final String knownFileName = "randomBytes"; try (InputStream content = new BufferedInputStream(new ByteArrayInputStream(rndBytes))) { - s3RepositoryUnderTest.store(content, knownFileName, contentType, hashes); + s3RepositoryUnderTest.store(TENANT, content, knownFileName, contentType, hashes); } } diff --git a/hawkbit-artifact-repository-filesystem/src/main/java/org/eclipse/hawkbit/artifact/repository/ArtifactFilesystemRepository.java b/hawkbit-artifact-repository-filesystem/src/main/java/org/eclipse/hawkbit/artifact/repository/ArtifactFilesystemRepository.java index 411432be0..abf6b1510 100644 --- a/hawkbit-artifact-repository-filesystem/src/main/java/org/eclipse/hawkbit/artifact/repository/ArtifactFilesystemRepository.java +++ b/hawkbit-artifact-repository-filesystem/src/main/java/org/eclipse/hawkbit/artifact/repository/ArtifactFilesystemRepository.java @@ -26,6 +26,7 @@ import org.eclipse.hawkbit.artifact.repository.model.DbArtifact; import org.eclipse.hawkbit.artifact.repository.model.DbArtifactHash; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.validation.annotation.Validated; import com.google.common.base.Splitter; import com.google.common.io.BaseEncoding; @@ -44,6 +45,7 @@ import com.google.common.io.Files; * are stored in different sub-directories based on the last four digits of the * SHA1-hash {@code (/basepath/[two digit sha1]/[two digit sha1])}. */ +@Validated public class ArtifactFilesystemRepository implements ArtifactRepository { private static final Logger LOG = LoggerFactory.getLogger(ArtifactFilesystemRepository.class); @@ -64,16 +66,17 @@ public class ArtifactFilesystemRepository implements ArtifactRepository { } @Override - public ArtifactFilesystem store(final InputStream content, final String filename, final String contentType) { - return store(content, filename, contentType, null); + public ArtifactFilesystem store(final String tenant, final InputStream content, final String filename, + final String contentType) { + return store(tenant, content, filename, contentType, null); } @Override // suppress warning, of not strong enough hashing algorithm, SHA-1 and MD5 // is not used security related @SuppressWarnings("squid:S2070") - public ArtifactFilesystem store(final InputStream content, final String filename, final String contentType, - final DbArtifactHash hash) { + public ArtifactFilesystem store(final String tenant, final InputStream content, final String filename, + final String contentType, final DbArtifactHash hash) { final MessageDigest mdSHA1; final MessageDigest mdMD5; @@ -86,17 +89,17 @@ public class ArtifactFilesystemRepository implements ArtifactRepository { final File file = createTempFile(); final DbArtifact artifact = store(content, contentType, hash, mdSHA1, mdMD5, file); - return renameFileToSHA1Naming(file, artifact); + return renameFileToSHA1Naming(tenant, file, artifact); } @Override - public void deleteBySha1(final String sha1Hash) { - FileUtils.deleteQuietly(getFile(sha1Hash)); + public void deleteBySha1(final String tenant, final String sha1Hash) { + FileUtils.deleteQuietly(getFile(tenant, sha1Hash)); } @Override - public ArtifactFilesystem getArtifactBySha1(final String sha1) { - final File file = getFile(sha1); + public ArtifactFilesystem getArtifactBySha1(final String tenant, final String sha1) { + final File file = getFile(tenant, sha1); if (!file.exists()) { return null; } @@ -131,8 +134,8 @@ public class ArtifactFilesystemRepository implements ArtifactRepository { return artifact; } - private ArtifactFilesystem renameFileToSHA1Naming(final File file, final DbArtifact artifact) { - final File fileSHA1Naming = getFile(artifact.getHashes().getSha1()); + private ArtifactFilesystem renameFileToSHA1Naming(final String tenant, final File file, final DbArtifact artifact) { + final File fileSHA1Naming = getFile(tenant, artifact.getHashes().getSha1()); final ArtifactFilesystem fileSystemArtifact = new ArtifactFilesystem(fileSHA1Naming); if (fileSHA1Naming.exists()) { FileUtils.deleteQuietly(file); @@ -179,18 +182,18 @@ public class ArtifactFilesystemRepository implements ArtifactRepository { } } - private File getFile(final String sha1) { - final File aritfactDirectory = getSha1DirectoryPath(sha1).toFile(); + private File getFile(final String tenant, final String sha1) { + final File aritfactDirectory = getSha1DirectoryPath(tenant, sha1).toFile(); aritfactDirectory.mkdirs(); return new File(aritfactDirectory, sha1); } - private Path getSha1DirectoryPath(final String sha1) { + private Path getSha1DirectoryPath(final String tenant, final String sha1) { final int length = sha1.length(); final List folders = Splitter.fixedLength(2).splitToList(sha1.substring(length - 4, length)); final String folder1 = folders.get(0); final String folder2 = folders.get(1); - return Paths.get(artifactResourceProperties.getPath(), folder1, folder2); + return Paths.get(artifactResourceProperties.getPath(), sanitizeTenant(tenant), folder1, folder2); } private DigestOutputStream openFileOutputStream(final File file, final MessageDigest mdSHA1, @@ -198,4 +201,13 @@ public class ArtifactFilesystemRepository implements ArtifactRepository { return new DigestOutputStream( new DigestOutputStream(new BufferedOutputStream(new FileOutputStream(file)), mdMD5), mdSHA1); } + + @Override + public void deleteByTenant(final String tenant) { + FileUtils.deleteQuietly(Paths.get(artifactResourceProperties.getPath(), sanitizeTenant(tenant)).toFile()); + } + + private static String sanitizeTenant(final String tenant) { + return tenant.trim().toUpperCase(); + } } diff --git a/hawkbit-artifact-repository-filesystem/src/test/java/org/eclipse/hawkbit/artifact/repository/ArtifactFilesystemRepositoryTest.java b/hawkbit-artifact-repository-filesystem/src/test/java/org/eclipse/hawkbit/artifact/repository/ArtifactFilesystemRepositoryTest.java index 6ecce0438..baef32bb5 100644 --- a/hawkbit-artifact-repository-filesystem/src/test/java/org/eclipse/hawkbit/artifact/repository/ArtifactFilesystemRepositoryTest.java +++ b/hawkbit-artifact-repository-filesystem/src/test/java/org/eclipse/hawkbit/artifact/repository/ArtifactFilesystemRepositoryTest.java @@ -26,6 +26,7 @@ import ru.yandex.qatools.allure.annotations.Stories; @Features("Unit Tests - Artifact File System Repository") @Stories("Test storing artifact binaries in the file-system") public class ArtifactFilesystemRepositoryTest { + private static final String TENANT = "test_tenant"; private final ArtifactFilesystemProperties artifactResourceProperties = new ArtifactFilesystemProperties(); @@ -51,8 +52,8 @@ public class ArtifactFilesystemRepositoryTest { final byte[] fileContent = randomBytes(); final ArtifactFilesystem artifact = storeRandomArtifact(fileContent); - final DbArtifact artifactBySha1 = artifactFilesystemRepository - .getArtifactBySha1(artifact.getHashes().getSha1()); + final DbArtifact artifactBySha1 = artifactFilesystemRepository.getArtifactBySha1(TENANT, + artifact.getHashes().getSha1()); assertThat(artifactBySha1).isNotNull(); } @@ -61,16 +62,33 @@ public class ArtifactFilesystemRepositoryTest { public void deleteStoredArtifactBySHA1Hash() { final ArtifactFilesystem artifact = storeRandomArtifact(randomBytes()); - artifactFilesystemRepository.deleteBySha1(artifact.getHashes().getSha1()); + artifactFilesystemRepository.deleteBySha1(TENANT, artifact.getHashes().getSha1()); - assertThat(artifactFilesystemRepository.getArtifactBySha1(artifact.getHashes().getSha1())).isNull(); + assertThat(artifactFilesystemRepository.getArtifactBySha1(TENANT, artifact.getHashes().getSha1())).isNull(); + } + + @Test + @Description("Verfies that all artifacts of a tenant can be deleted in the file-system repository") + public void deleteStoredArtifactOfTenant() { + final ArtifactFilesystem artifact = storeRandomArtifact(randomBytes()); + + artifactFilesystemRepository.deleteByTenant(TENANT); + + assertThat(artifactFilesystemRepository.getArtifactBySha1(TENANT, artifact.getHashes().getSha1())).isNull(); } @Test @Description("Verfies that an artifact which does not exists is deleted quietly in the file-system repository") public void deleteArtifactWhichDoesNotExistsBySHA1HashWithoutException() { try { - artifactFilesystemRepository.deleteBySha1("sha1HashWhichDoesNotExists"); + artifactFilesystemRepository.deleteBySha1(TENANT, "sha1HashWhichDoesNotExists"); + } catch (final Exception e) { + Assertions.fail("did not expect an exception while deleting a file which does not exists"); + } + + final ArtifactFilesystem artifact = storeRandomArtifact(randomBytes()); + try { + artifactFilesystemRepository.deleteBySha1("tenantWhichDoesNotExist", artifact.getHashes().getSha1()); } catch (final Exception e) { Assertions.fail("did not expect an exception while deleting a file which does not exists"); } @@ -79,7 +97,8 @@ public class ArtifactFilesystemRepositoryTest { private ArtifactFilesystem storeRandomArtifact(final byte[] fileContent) { final String fileName = "filename.tmp"; final ByteArrayInputStream inputStream = new ByteArrayInputStream(fileContent); - final ArtifactFilesystem store = artifactFilesystemRepository.store(inputStream, fileName, "application/txt"); + final ArtifactFilesystem store = artifactFilesystemRepository.store(TENANT, inputStream, fileName, + "application/txt"); return store; } diff --git a/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/security/SecurityManagedConfiguration.java b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/security/SecurityManagedConfiguration.java index 583ab7106..0aaf7060a 100644 --- a/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/security/SecurityManagedConfiguration.java +++ b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/security/SecurityManagedConfiguration.java @@ -313,7 +313,8 @@ public class SecurityManagedConfiguration { http.regexMatcher(HttpDownloadAuthenticationFilter.REQUEST_ID_REGEX_PATTERN) .addFilterBefore(downloadIdAuthenticationFilter, FilterSecurityInterceptor.class); - http.authorizeRequests().anyRequest().authenticated(); + http.authorizeRequests().anyRequest().authenticated().and().sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.STATELESS); } @Override @@ -401,6 +402,7 @@ public class SecurityManagedConfiguration { httpSec.httpBasic().and().exceptionHandling().authenticationEntryPoint(basicAuthEntryPoint); httpSec.anonymous().disable(); + httpSec.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); } } diff --git a/hawkbit-core/pom.xml b/hawkbit-core/pom.xml index ec74d4aeb..86effb80b 100644 --- a/hawkbit-core/pom.xml +++ b/hawkbit-core/pom.xml @@ -28,6 +28,14 @@ org.slf4j slf4j-api
+ + javax.validation + validation-api + + + org.hibernate + hibernate-validator + diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/artifact/repository/ArtifactRepository.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/artifact/repository/ArtifactRepository.java index 0cdddcdfc..a62e7942b 100644 --- a/hawkbit-core/src/main/java/org/eclipse/hawkbit/artifact/repository/ArtifactRepository.java +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/artifact/repository/ArtifactRepository.java @@ -10,8 +10,11 @@ package org.eclipse.hawkbit.artifact.repository; import java.io.InputStream; +import javax.validation.constraints.NotNull; + import org.eclipse.hawkbit.artifact.repository.model.DbArtifact; import org.eclipse.hawkbit.artifact.repository.model.DbArtifactHash; +import org.hibernate.validator.constraints.NotEmpty; /** * ArtifactRepository service interface. @@ -39,11 +42,14 @@ public interface ArtifactRepository { * @throws ArtifactStoreException * in case storing of the artifact was not successful */ - DbArtifact store(final InputStream content, final String filename, final String contentType); + DbArtifact store(@NotEmpty String tenant, @NotNull InputStream content, @NotEmpty String filename, + String contentType); /** * Stores an artifact into the repository. * + * @param tenant + * the tenant to store the artifact * @param content * the content to store * @param filename @@ -63,23 +69,27 @@ public interface ArtifactRepository { * in case {@code hash} is provided and not matching to the * calculated hashes during storing */ - DbArtifact store(final InputStream content, final String filename, final String contentType, DbArtifactHash hash); + DbArtifact store(@NotEmpty String tenant, @NotNull InputStream content, @NotEmpty String filename, + String contentType, DbArtifactHash hash); /** * Deletes an artifact by its SHA1 hash. * - * + * @param tenant + * the tenant to store the artifact * @param sha1Hash * the sha1-hash of the artifact to delete * * @throws MethodNotSupportedException * if implementation does not support the operation */ - void deleteBySha1(final String sha1Hash); + void deleteBySha1(@NotEmpty String tenant, @NotEmpty String sha1Hash); /** * Retrieves a {@link DbArtifact} from the store by it's SHA1 hash. * + * @param tenant + * the tenant to store the artifact * @param sha1Hash * the sha1-hash of the file to lookup. * @return The artifact file object or {@code null} if no file exists. @@ -87,5 +97,13 @@ public interface ArtifactRepository { * @throws MethodNotSupportedException * if implementation does not support the operation */ - DbArtifact getArtifactBySha1(String sha1Hash); + DbArtifact getArtifactBySha1(@NotEmpty String tenant, @NotEmpty String sha1Hash); + + /** + * Deletes all artifacts of given tenant. + * + * @param tenant + * to erase + */ + void deleteByTenant(@NotEmpty String tenant); } diff --git a/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDownloadResource.java b/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDownloadResource.java index da9935752..6fcbf1da7 100644 --- a/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDownloadResource.java +++ b/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDownloadResource.java @@ -20,6 +20,7 @@ import org.eclipse.hawkbit.cache.DownloadIdCache; import org.eclipse.hawkbit.cache.DownloadType; import org.eclipse.hawkbit.mgmt.rest.api.MgmtDownloadRestApi; import org.eclipse.hawkbit.rest.util.RequestResponseContextHolder; +import org.eclipse.hawkbit.tenancy.TenantAware; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -37,9 +38,6 @@ import com.google.common.net.HttpHeaders; /** * A resource for download artifacts. - * - * - * */ @RestController @Scope(value = WebApplicationContext.SCOPE_REQUEST) @@ -56,6 +54,9 @@ public class MgmtDownloadResource implements MgmtDownloadRestApi { @Autowired private RequestResponseContextHolder requestResponseContextHolder; + @Autowired + private TenantAware tenantAware; + /** * Handles the GET request for downloading an artifact. * @@ -77,7 +78,7 @@ public class MgmtDownloadResource implements MgmtDownloadRestApi { DbArtifact artifact = null; if (DownloadType.BY_SHA1.equals(artifactCache.getDownloadType())) { - artifact = artifactRepository.getArtifactBySha1(artifactCache.getId()); + artifact = artifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), artifactCache.getId()); } else { LOGGER.warn("Download Type {} not supported", artifactCache.getDownloadType()); } diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/ControllerManagement.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/ControllerManagement.java index d381c6b1e..1164d8668 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/ControllerManagement.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/ControllerManagement.java @@ -23,11 +23,11 @@ import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; import org.eclipse.hawkbit.repository.exception.QuotaExceededException; import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.Action.Status; -import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey; import org.eclipse.hawkbit.repository.model.ActionStatus; import org.eclipse.hawkbit.repository.model.SoftwareModule; import org.eclipse.hawkbit.repository.model.Target; import org.eclipse.hawkbit.repository.model.TargetUpdateStatus; +import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey; import org.hibernate.validator.constraints.NotEmpty; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -118,13 +118,15 @@ public interface ControllerManagement { /** * Retrieves oldest {@link Action} that is active and assigned to a * {@link Target}. + * + * For performance reasons this method does not throw + * {@link EntityNotFoundException} in case target with given controlelrId + * does not exist but will return an {@link Optional#empty()} instead. * * @param controllerId * identifies the target to retrieve the actions from * @return a list of actions assigned to given target which are active * - * @throws EntityNotFoundException - * if target with given ID does not exist */ @PreAuthorize(SpringEvalExpressions.IS_CONTROLLER) Optional findOldestActiveActionByTarget(@NotNull String controllerId); diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/ActionRepository.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/ActionRepository.java index 680ca6066..d6879b6c5 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/ActionRepository.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/ActionRepository.java @@ -116,6 +116,17 @@ public interface ActionRepository extends BaseEntityRepository, @EntityGraph(value = "Action.ds", type = EntityGraphType.LOAD) Optional findFirstByTargetControllerIdAndActive(final Sort sort, final String controllerId, boolean active); + /** + * Checks if an active action exists for given + * {@link Target#getControllerId()}. + * + * @param controllerId + * of target to check + * @return true if an active action for the target exists. + */ + @Query("SELECT CASE WHEN COUNT(a)>0 THEN 'true' ELSE 'false' END FROM JpaAction a JOIN a.target t WHERE t.controllerId=:controllerId AND a.active=1") + boolean activeActionExistsForControllerId(@Param("controllerId") String controllerId); + /** * Retrieves latest {@link Action} for given target and * {@link SoftwareModule}. 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 67293f212..76d8b8a29 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 @@ -28,9 +28,9 @@ import org.eclipse.hawkbit.repository.jpa.model.JpaArtifact; import org.eclipse.hawkbit.repository.jpa.model.JpaSoftwareModule; import org.eclipse.hawkbit.repository.model.Artifact; import org.eclipse.hawkbit.repository.model.SoftwareModule; +import org.eclipse.hawkbit.tenancy.TenantAware; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.ConcurrencyFailureException; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -49,14 +49,22 @@ public class JpaArtifactManagement implements ArtifactManagement { private static final Logger LOG = LoggerFactory.getLogger(JpaArtifactManagement.class); - @Autowired - private LocalArtifactRepository localArtifactRepository; + private final LocalArtifactRepository localArtifactRepository; - @Autowired - private SoftwareModuleRepository softwareModuleRepository; + private final SoftwareModuleRepository softwareModuleRepository; - @Autowired - private ArtifactRepository artifactRepository; + private final ArtifactRepository artifactRepository; + + private final TenantAware tenantAware; + + JpaArtifactManagement(final LocalArtifactRepository localArtifactRepository, + final SoftwareModuleRepository softwareModuleRepository, final ArtifactRepository artifactRepository, + final TenantAware tenantAware) { + this.localArtifactRepository = localArtifactRepository; + this.softwareModuleRepository = softwareModuleRepository; + this.artifactRepository = artifactRepository; + this.tenantAware = tenantAware; + } private static Artifact checkForExistingArtifact(final String filename, final boolean overrideExisting, final SoftwareModule softwareModule) { @@ -87,7 +95,7 @@ public class JpaArtifactManagement implements ArtifactManagement { final Artifact existing = checkForExistingArtifact(filename, overrideExisting, softwareModule); try { - result = artifactRepository.store(stream, filename, contentType, + result = artifactRepository.store(tenantAware.getCurrentTenant(), stream, filename, contentType, new DbArtifactHash(providedSha1Sum, providedMd5Sum)); } catch (final ArtifactStoreException e) { throw new ArtifactUploadFailedException(e); @@ -118,7 +126,7 @@ public class JpaArtifactManagement implements ArtifactManagement { try { LOG.debug("deleting artifact from repository {}", sha1Hash); - artifactRepository.deleteBySha1(sha1Hash); + artifactRepository.deleteBySha1(tenantAware.getCurrentTenant(), sha1Hash); return true; } catch (final ArtifactStoreException e) { throw new ArtifactDeleteFailedException(e); @@ -177,7 +185,7 @@ public class JpaArtifactManagement implements ArtifactManagement { @Override public Optional loadArtifactBinary(final String sha1Hash) { - return Optional.ofNullable(artifactRepository.getArtifactBySha1(sha1Hash)); + return Optional.ofNullable(artifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), sha1Hash)); } 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/JpaControllerManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaControllerManagement.java index da42c6e99..36d40cfdf 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaControllerManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaControllerManagement.java @@ -188,7 +188,9 @@ public class JpaControllerManagement implements ControllerManagement { @Override public Optional findOldestActiveActionByTarget(final String controllerId) { - throwExceptionIfTargetDoesNotExist(controllerId); + if (!actionRepository.activeActionExistsForControllerId(controllerId)) { + return Optional.empty(); + } // used in favorite to findFirstByTargetAndActiveOrderByIdAsc due to // DATAJPA-841 issue. 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 2f9c7babc..4e3e3598a 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 @@ -73,7 +73,6 @@ import org.springframework.util.StringUtils; import org.springframework.validation.annotation.Validated; import com.google.common.collect.Lists; -import com.google.common.collect.Sets; /** * JPA implementation of {@link SoftwareModuleManagement}. @@ -591,7 +590,7 @@ public class JpaSoftwareModuleManagement implements SoftwareModuleManagement { @Retryable(include = { ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY)) public void deleteSoftwareModule(final Long moduleId) { - deleteSoftwareModules(Sets.newHashSet(moduleId)); + deleteSoftwareModules(Arrays.asList(moduleId)); } @Override diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaSystemManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaSystemManagement.java index 83f1dd70e..a4c1c2f5f 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaSystemManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaSystemManagement.java @@ -16,6 +16,7 @@ import java.util.stream.Collectors; import javax.persistence.EntityManager; +import org.eclipse.hawkbit.artifact.repository.ArtifactRepository; import org.eclipse.hawkbit.cache.TenancyCacheManager; import org.eclipse.hawkbit.repository.RolloutStatusCache; import org.eclipse.hawkbit.repository.SystemManagement; @@ -116,6 +117,9 @@ public class JpaSystemManagement implements CurrentTenantCacheKeyGenerator, Syst @Autowired private RolloutStatusCache rolloutStatusCache; + @Autowired + private ArtifactRepository artifactRepository; + @Override public SystemUsageReport getSystemUsageStatistics() { @@ -236,6 +240,7 @@ public class JpaSystemManagement implements CurrentTenantCacheKeyGenerator, Syst distributionSetRepository.deleteByTenant(tenant); distributionSetTypeRepository.deleteByTenant(tenant); softwareModuleRepository.deleteByTenant(tenant); + artifactRepository.deleteByTenant(tenant); softwareModuleTypeRepository.deleteByTenant(tenant); return null; }); 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 65c89615d..db3b3f485 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 @@ -13,6 +13,7 @@ import java.util.Map; import javax.persistence.EntityManager; import javax.sql.DataSource; +import org.eclipse.hawkbit.artifact.repository.ArtifactRepository; import org.eclipse.hawkbit.repository.ArtifactManagement; import org.eclipse.hawkbit.repository.ControllerManagement; import org.eclipse.hawkbit.repository.DeploymentManagement; @@ -502,16 +503,13 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { return new JpaControllerManagement(); } - /** - * {@link JpaArtifactManagement} bean. - * - * @return a new {@link ArtifactManagement} - */ - @Bean @ConditionalOnMissingBean - ArtifactManagement artifactManagement() { - return new JpaArtifactManagement(); + ArtifactManagement artifactManagement(final LocalArtifactRepository localArtifactRepository, + final SoftwareModuleRepository softwareModuleRepository, final ArtifactRepository artifactRepository, + final TenantAware tenantAware) { + return new JpaArtifactManagement(localArtifactRepository, softwareModuleRepository, artifactRepository, + tenantAware); } /** 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 6b2ea7322..1933ab96e 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 @@ -175,16 +175,21 @@ public class ArtifactManagementTest extends AbstractJpaIntegrationTest { assertThat(result2.getId()).isNotNull(); assertThat(((JpaArtifact) result).getSha1Hash()).isNotEqualTo(((JpaArtifact) result2).getSha1Hash()); - assertThat(binaryArtifactRepository.getArtifactBySha1(((JpaArtifact) result).getSha1Hash())).isNotNull(); - assertThat(binaryArtifactRepository.getArtifactBySha1(((JpaArtifact) result2).getSha1Hash())).isNotNull(); + assertThat(binaryArtifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), result.getSha1Hash())) + .isNotNull(); + assertThat(binaryArtifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), result2.getSha1Hash())) + .isNotNull(); artifactManagement.deleteArtifact(result.getId()); - assertThat(binaryArtifactRepository.getArtifactBySha1(((JpaArtifact) result).getSha1Hash())).isNull(); - assertThat(binaryArtifactRepository.getArtifactBySha1(((JpaArtifact) result2).getSha1Hash())).isNotNull(); + assertThat(binaryArtifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), result.getSha1Hash())) + .isNull(); + assertThat(binaryArtifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), result2.getSha1Hash())) + .isNotNull(); artifactManagement.deleteArtifact(result2.getId()); - assertThat(binaryArtifactRepository.getArtifactBySha1(((JpaArtifact) result2).getSha1Hash())).isNull(); + assertThat(binaryArtifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), result2.getSha1Hash())) + .isNull(); assertThat(artifactRepository.findAll()).hasSize(0); } @@ -211,12 +216,15 @@ public class ArtifactManagementTest extends AbstractJpaIntegrationTest { assertThat(result2.getId()).isNotNull(); assertThat(((JpaArtifact) result).getSha1Hash()).isEqualTo(((JpaArtifact) result2).getSha1Hash()); - assertThat(binaryArtifactRepository.getArtifactBySha1(((JpaArtifact) result).getSha1Hash())).isNotNull(); + assertThat(binaryArtifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), result.getSha1Hash())) + .isNotNull(); artifactManagement.deleteArtifact(result.getId()); - assertThat(binaryArtifactRepository.getArtifactBySha1(((JpaArtifact) result).getSha1Hash())).isNotNull(); + assertThat(binaryArtifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), result.getSha1Hash())) + .isNotNull(); artifactManagement.deleteArtifact(result2.getId()); - assertThat(binaryArtifactRepository.getArtifactBySha1(((JpaArtifact) result).getSha1Hash())).isNull(); + assertThat(binaryArtifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), result.getSha1Hash())) + .isNull(); } @Test diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/ControllerManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/ControllerManagementTest.java index b12cc14f1..357b4bbdc 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/ControllerManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/ControllerManagementTest.java @@ -78,6 +78,8 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { assertThat(controllerManagement.getActionForDownloadByTargetAndSoftwareModule(target.getControllerId(), module.getId())).isNotPresent(); + assertThat(controllerManagement.findOldestActiveActionByTarget(NOT_EXIST_ID)).isNotPresent(); + assertThat(controllerManagement.hasTargetArtifactAssigned(target.getControllerId(), "XXX")).isFalse(); assertThat(controllerManagement.hasTargetArtifactAssigned(target.getId(), "XXX")).isFalse(); } @@ -100,8 +102,6 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { verifyThrownExceptionBy(() -> controllerManagement.addUpdateActionStatus( entityFactory.actionStatus().create(NOT_EXIST_IDL).status(Action.Status.FINISHED)), "Action"); - verifyThrownExceptionBy(() -> controllerManagement.findOldestActiveActionByTarget(NOT_EXIST_ID), "Target"); - verifyThrownExceptionBy(() -> controllerManagement .getActionForDownloadByTargetAndSoftwareModule(target.getControllerId(), NOT_EXIST_IDL), "SoftwareModule"); 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 011384746..9b9093d99 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 @@ -22,7 +22,6 @@ import java.util.List; import org.apache.commons.lang3.RandomUtils; import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleCreatedEvent; import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException; -import org.eclipse.hawkbit.repository.jpa.model.JpaArtifact; import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet; import org.eclipse.hawkbit.repository.jpa.model.JpaSoftwareModuleMetadata; import org.eclipse.hawkbit.repository.jpa.model.JpaTarget; @@ -490,13 +489,15 @@ public class SoftwareModuleManagementTest extends AbstractJpaIntegrationTest { assertThat(artifactRepository.findAll()).hasSize(results.length); for (final Artifact result : results) { assertThat(result.getId()).isNotNull(); - assertThat(binaryArtifactRepository.getArtifactBySha1(((JpaArtifact) result).getSha1Hash())).isNotNull(); + assertThat(binaryArtifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), result.getSha1Hash())) + .isNotNull(); } } private void assertArtfiactNull(final Artifact... results) { for (final Artifact result : results) { - assertThat(binaryArtifactRepository.getArtifactBySha1(((JpaArtifact) result).getSha1Hash())).isNull(); + assertThat(binaryArtifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), result.getSha1Hash())) + .isNull(); } } diff --git a/hawkbit-repository/hawkbit-repository-test/src/main/resources/hawkbit-test-defaults.properties b/hawkbit-repository/hawkbit-repository-test/src/main/resources/hawkbit-test-defaults.properties index 4af1530be..0306a4dfe 100644 --- a/hawkbit-repository/hawkbit-repository-test/src/main/resources/hawkbit-test-defaults.properties +++ b/hawkbit-repository/hawkbit-repository-test/src/main/resources/hawkbit-test-defaults.properties @@ -10,8 +10,9 @@ # Test utility properties for easier fault investigation - START ## Logging - START logging.level.=INFO -logging.level.org.eclipse.persistence=ERROR logging.level.org.eclipse.hawkbit.repository.test.matcher.EventVerifier=ERROR +logging.level.org.eclipse.persistence=ERROR +spring.datasource.eclipselink.logging.logger=JavaLogger spring.jpa.properties.eclipselink.logging.level=FINE spring.jpa.properties.eclipselink.logging.level.sql=FINE spring.jpa.properties.eclipselink.logging.parameters=true diff --git a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/DosFilter.java b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/DosFilter.java index 468e522ee..ab5e0d7e5 100644 --- a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/DosFilter.java +++ b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/DosFilter.java @@ -48,10 +48,10 @@ public class DosFilter extends OncePerRequestFilter { private final Pattern ipAdressBlacklist; private final Cache readCountCache = Caffeine.newBuilder() - .expireAfterAccess(1, TimeUnit.SECONDS).build(); + .expireAfterWrite(1, TimeUnit.SECONDS).build(); private final Cache writeCountCache = Caffeine.newBuilder() - .expireAfterAccess(1, TimeUnit.SECONDS).build(); + .expireAfterWrite(1, TimeUnit.SECONDS).build(); private final int maxRead; private final int maxWrite; diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/dstag/CreateUpdateDistributionTagLayoutWindow.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/dstag/CreateUpdateDistributionTagLayoutWindow.java index f39c2347f..478304060 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/dstag/CreateUpdateDistributionTagLayoutWindow.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/dstag/CreateUpdateDistributionTagLayoutWindow.java @@ -52,6 +52,10 @@ public class CreateUpdateDistributionTagLayoutWindow extends AbstractCreateUpdat @Override protected void populateTagNameCombo() { + if (tagNameComboBox == null) { + return; + } + tagNameComboBox.removeAllItems(); final List distTagNameList = tagManagement .findAllDistributionSetTags(new PageRequest(0, MAX_TAGS)).getContent();