diff --git a/datavault-broker/pom.xml b/datavault-broker/pom.xml index a677302c3..b94345fa9 100644 --- a/datavault-broker/pom.xml +++ b/datavault-broker/pom.xml @@ -77,13 +77,7 @@ org.mariadb.jdbc mariadb-java-client - test - - - - mysql - mysql-connector-java - runtime + ${maria.java.client.version} diff --git a/datavault-broker/src/test/java/org/datavaultplatform/broker/actuator/ActuatorTest.java b/datavault-broker/src/test/java/org/datavaultplatform/broker/actuator/ActuatorTest.java index 9243a94c9..f07977b5c 100644 --- a/datavault-broker/src/test/java/org/datavaultplatform/broker/actuator/ActuatorTest.java +++ b/datavault-broker/src/test/java/org/datavaultplatform/broker/actuator/ActuatorTest.java @@ -4,14 +4,18 @@ import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.datavaultplatform.broker.app.DataVaultBrokerApp; +import org.datavaultplatform.broker.config.MockServicesConfig; import org.datavaultplatform.broker.queue.Sender; import org.datavaultplatform.broker.services.FileStoreService; import org.datavaultplatform.broker.test.AddTestProperties; -import org.datavaultplatform.broker.test.BaseDatabaseTest; import org.datavaultplatform.broker.test.TestClockConfig; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration; +import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; @@ -32,10 +36,12 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @SpringBootTest(classes = DataVaultBrokerApp.class) -@Import(TestClockConfig.class) +@Import({TestClockConfig.class, MockServicesConfig.class}) @AddTestProperties @Slf4j @TestPropertySource(properties = { + "broker.services.enabled=false", + "broker.database.enabled=false", "broker.email.enabled=true", "broker.controllers.enabled=true", "broker.initialise.enabled=true", @@ -44,7 +50,11 @@ "management.endpoints.web.exposure.include=*", "management.health.rabbit.enabled=false"}) @AutoConfigureMockMvc -public class ActuatorTest extends BaseDatabaseTest { +@EnableAutoConfiguration(exclude= { + DataSourceAutoConfiguration.class, + DataSourceTransactionManagerAutoConfiguration.class, + HibernateJpaAutoConfiguration.class }) +public class ActuatorTest { @Autowired MockMvc mvc; @@ -55,7 +65,7 @@ public class ActuatorTest extends BaseDatabaseTest { @Autowired ObjectMapper mapper; - @MockBean + @Autowired FileStoreService mFileStoreService; @Test diff --git a/datavault-broker/src/test/java/org/datavaultplatform/common/storage/impl/ssh/stack/jsch/BaseSFTPFileSystemPrivatePublicKeyPairSizeIT.java b/datavault-broker/src/test/java/org/datavaultplatform/common/storage/impl/ssh/stack/jsch/BaseSFTPFileSystemPrivatePublicKeyPairSizeIT.java new file mode 100644 index 000000000..1d0577535 --- /dev/null +++ b/datavault-broker/src/test/java/org/datavaultplatform/common/storage/impl/ssh/stack/jsch/BaseSFTPFileSystemPrivatePublicKeyPairSizeIT.java @@ -0,0 +1,103 @@ +package org.datavaultplatform.common.storage.impl.ssh.stack.jsch; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.security.interfaces.RSAPublicKey; +import java.util.Map; +import javax.crypto.SecretKey; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.bouncycastle.util.encoders.Base64; +import org.datavaultplatform.broker.services.UserKeyPairService; +import org.datavaultplatform.broker.services.UserKeyPairService.KeyPairInfo; +import org.datavaultplatform.broker.services.UserKeyPairServiceJSchImpl; +import org.datavaultplatform.common.PropNames; +import org.datavaultplatform.common.crypto.Encryption; +import org.datavaultplatform.common.crypto.SshRsaKeyUtils; +import org.datavaultplatform.common.docker.DockerImage; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; + +@Slf4j +public abstract class BaseSFTPFileSystemPrivatePublicKeyPairSizeIT extends BaseSFTPFileSystemSizeIT { + + static final String TEST_PASSPHRASE = "tenet"; + static final String ENV_PUBLIC_KEY = "PUBLIC_KEY"; + static final String KEY_STORE_PASSWORD = "keyStorePassword"; + static final String SSH_KEY_NAME = "sshKeyName"; + + static File keyStoreTempDir; + static KeyPairInfo keyPairInfo; + + + + static GenericContainer initialiseContainer(String tcName) { + + try { + keyStoreTempDir = Files.createTempDirectory("tmpKeyStoreDir").toFile(); + }catch(IOException ex){ + throw new RuntimeException(ex); + } + + keyPairInfo = generateKeyPair(); + + return new GenericContainer<>(DockerImage.OPEN_SSH_8pt6_IMAGE_NAME) + .withEnv("TC_NAME", tcName) + .withEnv(ENV_USER_NAME, TEST_USER) + .withEnv(ENV_PUBLIC_KEY, + keyPairInfo.getPublicKey()) //this causes the public key to be added to /config/.ssh/authorized_keys + .withExposedPorts(SFTP_SERVER_PORT) + //.withFileSystemBind("/Users/davidhay/SPARSE_FILES/files","/tmp/files", BindMode.READ_ONLY) + .waitingFor(Wait.forListeningPort()); + } + + + @SneakyThrows + @Override + public void addAuthenticationProps(Map props) { + byte[] iv = Encryption.generateIV(); + byte[] encrypted = Encryption.encryptSecret(keyPairInfo.getPrivateKey(), null, iv); + + props.put(PropNames.PASSPHRASE, TEST_PASSPHRASE); + props.put(PropNames.IV, Base64.toBase64String(iv)); + props.put(PropNames.PRIVATE_KEY, Base64.toBase64String(encrypted)); + + RSAPublicKey publicKey = SshRsaKeyUtils.readPublicKey( + keyPairInfo.getPublicKey()); + if(getLog().isTraceEnabled()) { + getLog().trace("ORIG PUBLIC KEY MODULUS [{}]", publicKey.getModulus().toString(16)); + } + } + + @SneakyThrows + private static KeyPairInfo generateKeyPair() { + + Encryption.addBouncyCastleSecurityProvider(); + String keyStorePath = keyStoreTempDir.toPath().resolve("test.ks").toString(); + log.info("TEMP KEY IS AT [{}]", keyStorePath); + + Encryption enc = new Encryption(); + enc.setVaultEnable(false); + enc.setVaultPrivateKeyEncryptionKeyName(SSH_KEY_NAME); + + enc.setKeystoreEnable(true); + enc.setKeystorePath(keyStorePath); + enc.setKeystorePassword(KEY_STORE_PASSWORD); + + SecretKey keyForKeyStore = Encryption.generateSecretKey(); + + assertFalse(new File(keyStorePath).exists()); + + // Encryption class uses 'vaultPrivateKeyEncryptionKeyName' property as the default key name for JavaKeyStore + Encryption.saveSecretKeyToKeyStore(Encryption.getVaultPrivateKeyEncryptionKeyName(), + keyForKeyStore); + + assertTrue(new File(keyStorePath).exists()); + UserKeyPairService userKeyPairService = new UserKeyPairServiceJSchImpl(TEST_PASSPHRASE); + return userKeyPairService.generateNewKeyPair(); + } +} diff --git a/datavault-broker/src/test/java/org/datavaultplatform/common/storage/impl/ssh/stack/jsch/BaseSFTPFileSystemSizeIT.java b/datavault-broker/src/test/java/org/datavaultplatform/common/storage/impl/ssh/stack/jsch/BaseSFTPFileSystemSizeIT.java new file mode 100644 index 000000000..a2805d4e9 --- /dev/null +++ b/datavault-broker/src/test/java/org/datavaultplatform/common/storage/impl/ssh/stack/jsch/BaseSFTPFileSystemSizeIT.java @@ -0,0 +1,58 @@ +package org.datavaultplatform.common.storage.impl.ssh.stack.jsch; + +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneId; +import java.util.HashMap; +import java.util.Map; +import lombok.SneakyThrows; +import org.datavaultplatform.common.PropNames; +import org.datavaultplatform.common.storage.SFTPFileSystemDriver; +import org.slf4j.Logger; +import org.testcontainers.containers.GenericContainer; + +public abstract class BaseSFTPFileSystemSizeIT { + + static final int SFTP_SERVER_PORT = 2222; + + static final String ENV_USER_NAME = "USER_NAME"; + static final String TEST_USER = "testuser"; + + static final String SFTP_ROOT_DIR = "/config"; + static final Clock TEST_CLOCK = Clock.fixed(Instant.parse("2022-03-26T09:44:33.22Z"), + ZoneId.of("Europe/London")); + static final String TEMP_PREFIX = "dvSftpTempDir"; + static final long EXPECTED_SPACE_AVAILABLE_ON_SFTP_SERVER = 100_000; + private static final int FREE_SPACE_FACTOR = 10; + + public abstract GenericContainer getContainer(); + + public abstract SFTPFileSystemDriver getSftpDriver(); + + public abstract void addAuthenticationProps(Map props); + + public final int getSftpServerPort() { + return getContainer().getMappedPort(SFTP_SERVER_PORT); + } + + public final String getSftpServerHost() { + return getContainer().getHost(); + } + + @SneakyThrows + protected Map getStoreProperties() { + HashMap props = new HashMap<>(); + + //standard sftp properties + props.put(PropNames.USERNAME, TEST_USER); + props.put(PropNames.ROOT_PATH, SFTP_ROOT_DIR); //this is the directory ON THE SFTP SERVER - for ALL OpenSSH containers, it's config + props.put(PropNames.HOST, getSftpServerHost()); + props.put(PropNames.PORT, String.valueOf(getSftpServerPort())); + + addAuthenticationProps(props); + + return props; + } + + abstract Logger getLog(); +} diff --git a/datavault-broker/src/test/java/org/datavaultplatform/common/storage/impl/ssh/stack/jsch/CombinedSizeSFTPJSchIT.java b/datavault-broker/src/test/java/org/datavaultplatform/common/storage/impl/ssh/stack/jsch/CombinedSizeSFTPJSchIT.java new file mode 100644 index 000000000..54623ee09 --- /dev/null +++ b/datavault-broker/src/test/java/org/datavaultplatform/common/storage/impl/ssh/stack/jsch/CombinedSizeSFTPJSchIT.java @@ -0,0 +1,122 @@ +package org.datavaultplatform.common.storage.impl.ssh.stack.jsch; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Map; +import java.util.stream.Stream; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.datavaultplatform.common.storage.SFTPFileSystemDriver; +import org.datavaultplatform.common.storage.impl.SFTPFileSystemJSch; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.slf4j.Logger; +import org.testcontainers.containers.Container.ExecResult; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.MountableFile; + +@Testcontainers(disabledWithoutDocker = true) +@Slf4j +public class CombinedSizeSFTPJSchIT extends BaseSFTPFileSystemPrivatePublicKeyPairSizeIT { + + @Container + static final GenericContainer container = initialiseContainer("SftpPrivatePublicJSchDIT"); + private static final int BYTES_PER_FILE = 4; + + @BeforeAll + @SneakyThrows + static void setupContainer() { + log.info("Copying script createFileTree.sh to /tmp in SFTP Container"); + container.copyFileToContainer(MountableFile.forClasspathResource("sftpsize/createFileTree.sh"), + "/tmp/createFileTree.sh"); + log.info("Creating directory /config/files in SFTP Container"); + container.execInContainer("mkdir", "-p", "/config/files"); + } + + @BeforeEach + @SneakyThrows + void cleanupAnyTestFilesInContainer() { + container.execInContainer("/bin/bash", "-c", "cd /config/files; rm -rf *"); + } + + static Stream getTestCaseParams() { + return Stream.of( + TestCaseParam.SMALL + //, TestCaseParam.SMALLISH + //, TestCaseParam.MEDIUM + //, TestCaseParam.LARGE + ).map(param -> Arguments.of(param, String.format("%s files[%s] depth[%s]",param,param.fileCount, param.depth))); + } + + @ParameterizedTest(name="{index}-{1}") + @MethodSource("getTestCaseParams") + @SneakyThrows + void testTotalSizeOfFilesViaSftp(TestCaseParam testCaseParam, String testDescription) { + + log.info("Creating files beneath /config/files in SFTP Container"); + container.execInContainer("/tmp/createFileTree.sh", "/config/files", ""+ testCaseParam.depth); + log.info("Created files beneath /config/files in SFTP Container"); + + // count the number of files in the beneath /config/files in the SFTP Container + ExecResult result = container.execInContainer( + "/bin/bash","-c","cd /config/files; find . -type f | wc -l"); + + log.info("output message [{}]", result.getStdout()); + log.info("error message [{}]", result.getStderr()); + log.info("exit code [{}]", result.getExitCode()); + + assertEquals(0, result.getExitCode()); + assertEquals(testCaseParam.fileCount, Integer.parseInt(result.getStdout().trim())); + + // check that the getSize method of SFTPDriver returns the expected size + log.info("BEFORE GET SIZE"); + + // get the total size files beneath /config/files + long size = getSftpDriver().getSize("files"); + log.info("AFTER GET SIZE"); + assertEquals(testCaseParam.fileCount * BYTES_PER_FILE, size); + } + + /** + * @See DirectrorySizeTest + */ + enum TestCaseParam { + + SMALL(4, 62), + SMALLISH(10, 4094), + MEDIUM(13, 32766), + LARGE(17, 524286); + + public final int depth; + public final long fileCount; + + TestCaseParam(int depth, long fileCount) { + this.depth = depth; + this.fileCount = fileCount; + } + + + } + + @Override + public SFTPFileSystemDriver getSftpDriver() { + Map props = getStoreProperties(); + return new SFTPFileSystemJSch("sftp-jsch", props, TEST_CLOCK); + } + + @Override + public GenericContainer getContainer() { + return container; + } + + @Override + Logger getLog() { + return log; + } +} diff --git a/datavault-broker/src/test/java/org/datavaultplatform/common/storage/impl/ssh/stack/path/BaseNestedFilesTest.java b/datavault-broker/src/test/java/org/datavaultplatform/common/storage/impl/ssh/stack/path/BaseNestedFilesTest.java new file mode 100644 index 000000000..5f4685e57 --- /dev/null +++ b/datavault-broker/src/test/java/org/datavaultplatform/common/storage/impl/ssh/stack/path/BaseNestedFilesTest.java @@ -0,0 +1,65 @@ +package org.datavaultplatform.common.storage.impl.ssh.stack.path; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.File; +import java.io.PrintWriter; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.io.TempDir; + + +@Slf4j +public abstract class BaseNestedFilesTest { + + long fileCounter; + + @TempDir + File baseDir; + + @BeforeEach + void setup() { + fileCounter = 0; + assertTrue(baseDir.exists() && baseDir.isDirectory()); + log.info("BASE DIR [{}]", baseDir); + } + + void createFiles(File base, int depth) { + createFilesInternal(base, depth); + log.info("Created [{}] files", this.fileCounter); + } + + private void createFilesInternal(File base, int depth) { + if (depth > 0) { + File left = new File(base, "left_" + depth); + left.mkdirs(); + File right = new File(base, "right_" + depth); + right.mkdirs(); + createFilesInternal(left, depth - 1); + createFilesInternal(right, depth - 1); + } + File aaa = new File(base, "aaa.txt"); + write(aaa, "aaa"); + File bbb = new File(base, "bbb.txt"); + write(bbb, "bbb"); + } + + + @SneakyThrows + void write(File file, String contents) { + try (PrintWriter pw = new PrintWriter(file)) { + pw.write(contents); + } + fileCounter++; + } + + public static final long getExpectedFileCountAtLevel(int level) { + long total = 0; + for (int depth = 0; depth <= level; depth++) { + total += Math.pow(2, depth + 1); + } + log.info("expected file count for depth[{}] is [{}]", level, total); + return total; + } +} diff --git a/datavault-broker/src/test/java/org/datavaultplatform/common/storage/impl/ssh/stack/path/CountFilesInDirectoryTest.java b/datavault-broker/src/test/java/org/datavaultplatform/common/storage/impl/ssh/stack/path/CountFilesInDirectoryTest.java new file mode 100644 index 000000000..3c01125e5 --- /dev/null +++ b/datavault-broker/src/test/java/org/datavaultplatform/common/storage/impl/ssh/stack/path/CountFilesInDirectoryTest.java @@ -0,0 +1,79 @@ +package org.datavaultplatform.common.storage.impl.ssh.stack.path; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +/* +This tests that the number of files created by createFilesInternal +is the same calculated by getExpectedFileCountAtLevel. +These (level, number of files) pairs are important for SftpDirectorySizeTest. + */ +@Slf4j +public class CountFilesInDirectoryTest extends BaseNestedFilesTest { + + @Test + void testLevel0() { + createFiles(this.baseDir, 0); + assertEquals(getExpectedFileCountAtLevel(0), this.fileCounter); + } + + @Test + void testLevel1() { + createFiles(this.baseDir, 1); + assertEquals(getExpectedFileCountAtLevel(1), this.fileCounter); + } + + @Test + void testLevel2() { + createFiles(this.baseDir, 2); + assertEquals(getExpectedFileCountAtLevel(2), this.fileCounter); + } + + @Test + void testLevel3() { + createFiles(this.baseDir, 3); + assertEquals(getExpectedFileCountAtLevel(3), this.fileCounter); + } + + @Test + void testLevel4() { + createFiles(this.baseDir, 4); + assertEquals(getExpectedFileCountAtLevel(4), this.fileCounter); + } + + @Test + void testNumberOfFilesAtLevel10() { + assertEquals(4_094, getExpectedFileCountAtLevel(10)); + } + + @Test + void testNumberOfFilesAtLevel11() { + assertEquals(8_190, getExpectedFileCountAtLevel(11)); + } + + @Test + void testNumberOfFilesAtLevel12() { + assertEquals(16_382, getExpectedFileCountAtLevel(12)); + } + + @Test + void testNumberOfFilesAtLevel16() { + assertEquals(262_142, getExpectedFileCountAtLevel(16)); + } + + @Test + @Disabled + @SneakyThrows + void testLevels() { + for (int level = 0; level < 30; level++) { + long expected = getExpectedFileCountAtLevel(level); + log.info("level[{}] files[{}]", level, expected); + } + } +} diff --git a/datavault-broker/src/test/java/org/datavaultplatform/common/storage/impl/ssh/stack/path/DirEntryPath.java b/datavault-broker/src/test/java/org/datavaultplatform/common/storage/impl/ssh/stack/path/DirEntryPath.java new file mode 100644 index 000000000..fa9a91380 --- /dev/null +++ b/datavault-broker/src/test/java/org/datavaultplatform/common/storage/impl/ssh/stack/path/DirEntryPath.java @@ -0,0 +1,41 @@ +package org.datavaultplatform.common.storage.impl.ssh.stack.path; + +import java.io.File; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.Stack; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.datavaultplatform.common.storage.impl.ssh.stack.Item; +import org.datavaultplatform.common.storage.impl.ssh.stack.ItemContext; + +public class DirEntryPath implements Item { + + private final File dirEntry; + + public DirEntryPath(File dirEntry) { + this.dirEntry = dirEntry; + } + + @Override + public void process(Stack> stack, ItemContext ctx) { + if (dirEntry.isFile()) { + ctx.increment(dirEntry.length()); + ctx.incrementCount(); + } else { + ctx.setContext(dirEntry.toPath()); + Map> groupedItems = Stream.of(dirEntry.listFiles()) + .collect(Collectors.partitioningBy(File::isDirectory)); + + //push directory 'pop' + stack.push(EndDirItemPath.getInstance()); + + //push all directories + groupedItems.get(true).forEach(dir -> stack.push(new DirEntryPath(dir))); + + //push all 'non directories' + groupedItems.get(false).forEach(nonDir -> stack.push(new DirEntryPath(nonDir))); + } + } +} diff --git a/datavault-broker/src/test/java/org/datavaultplatform/common/storage/impl/ssh/stack/path/EndDirItemPath.java b/datavault-broker/src/test/java/org/datavaultplatform/common/storage/impl/ssh/stack/path/EndDirItemPath.java new file mode 100644 index 000000000..aae2919db --- /dev/null +++ b/datavault-broker/src/test/java/org/datavaultplatform/common/storage/impl/ssh/stack/path/EndDirItemPath.java @@ -0,0 +1,27 @@ +package org.datavaultplatform.common.storage.impl.ssh.stack.path; + +import java.nio.file.Path; +import java.util.Stack; +import org.datavaultplatform.common.storage.impl.ssh.stack.Item; +import org.datavaultplatform.common.storage.impl.ssh.stack.ItemContext; + +public class EndDirItemPath implements Item { + + private static final EndDirItemPath INSTANCE = new EndDirItemPath(); + + private EndDirItemPath() { + } + + public static EndDirItemPath getInstance(){ + return INSTANCE; + } + + /** + * When we process this Item, the context goes back up a level + */ + @Override + public void process(Stack> stack, ItemContext ctx) { + Path parent = ctx.getContext().getParent(); + ctx.setContext(parent); + } +} diff --git a/datavault-broker/src/test/java/org/datavaultplatform/common/storage/impl/ssh/stack/path/ItemContextPath.java b/datavault-broker/src/test/java/org/datavaultplatform/common/storage/impl/ssh/stack/path/ItemContextPath.java new file mode 100644 index 000000000..c90e241ba --- /dev/null +++ b/datavault-broker/src/test/java/org/datavaultplatform/common/storage/impl/ssh/stack/path/ItemContextPath.java @@ -0,0 +1,8 @@ +package org.datavaultplatform.common.storage.impl.ssh.stack.path; + +import java.nio.file.Path; +import org.datavaultplatform.common.storage.impl.ssh.stack.ItemContext; + +public class ItemContextPath extends ItemContext { + +} diff --git a/datavault-broker/src/test/java/org/datavaultplatform/common/storage/impl/ssh/stack/path/StackProcessorTest.java b/datavault-broker/src/test/java/org/datavaultplatform/common/storage/impl/ssh/stack/path/StackProcessorTest.java new file mode 100644 index 000000000..8a7f7cab8 --- /dev/null +++ b/datavault-broker/src/test/java/org/datavaultplatform/common/storage/impl/ssh/stack/path/StackProcessorTest.java @@ -0,0 +1,40 @@ +package org.datavaultplatform.common.storage.impl.ssh.stack.path; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.nio.file.Path; +import lombok.extern.slf4j.Slf4j; +import org.datavaultplatform.common.storage.impl.ssh.stack.StackProcessor; +import org.junit.jupiter.api.Test; + +/* +This tests that we can crawl a tree of files, calculating the total size +by using an explicit stack and not using recursion. + */ +@Slf4j +public class StackProcessorTest extends BaseNestedFilesTest { + + @Test + void testCrawlFilesUsingStackProcessor() { + createFiles(this.baseDir, 10); + assertEquals(getExpectedFileCountAtLevel(10), this.fileCounter); + + ItemContextPath ctx = new ItemContextPath(); + ctx.setContext(this.baseDir.toPath().getParent()); + + DirEntryPath initialItem = new DirEntryPath(this.baseDir); + + StackProcessor stackProcessor = new StackProcessor<>( + ctx, initialItem); + + stackProcessor.process(); + long size = ctx.getSize(); + long count = ctx.getCount(); + + log.info("COUNT [{}]", count); + log.info("SIZE [{}]", size); + + assertEquals(count, fileCounter); + assertEquals(3 * count, size); + } +} diff --git a/datavault-broker/src/test/resources/sftpsize/createFileTree.sh b/datavault-broker/src/test/resources/sftpsize/createFileTree.sh new file mode 100755 index 000000000..03e6c838d --- /dev/null +++ b/datavault-broker/src/test/resources/sftpsize/createFileTree.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +# This is used to create files in a SFTP Container. +# These files are used to test SFTPFileSystemDriver.getSize +# See CombinedSizeSFTPJSchIT +WD=$1 +DEPTH=$2 + +echo "WorkingDir is [$WD]" +echo "Depth is [$DEPTH]" +FILES_CREATED=0 +function createFiles() { + local base=$1 + local depth=$2 + #echo "in createFiles ${base} ${depth}" + + echo "aaa" > $base/a.txt + #ls -l $base/a.txt + echo "bbb" > $base/b.txt + #ls -l $base/b.txt + FILES_CREATED=$((FILES_CREATED+2)) + if (( $depth > 0 )); then + local left="${base}/L" + local right="${base}/R" + local nextDepth=$((depth-1)) + mkdir -p $left + createFiles $left $nextDepth + mkdir -p $right + createFiles $right $nextDepth + fi +} +createFiles $WD $DEPTH +echo "FILES_CREATED ${FILES_CREATED}" \ No newline at end of file diff --git a/datavault-common/pom.xml b/datavault-common/pom.xml index c8421fb78..4e925b459 100644 --- a/datavault-common/pom.xml +++ b/datavault-common/pom.xml @@ -88,7 +88,7 @@ com.oracle.oci.sdk oci-java-sdk-objectstorage - + org.bouncycastle diff --git a/datavault-common/src/main/java/org/datavaultplatform/common/storage/impl/SFTPFileSystemJSch.java b/datavault-common/src/main/java/org/datavaultplatform/common/storage/impl/SFTPFileSystemJSch.java index 4b07bee51..96f4116ae 100644 --- a/datavault-common/src/main/java/org/datavaultplatform/common/storage/impl/SFTPFileSystemJSch.java +++ b/datavault-common/src/main/java/org/datavaultplatform/common/storage/impl/SFTPFileSystemJSch.java @@ -2,7 +2,7 @@ import com.jcraft.jsch.*; import java.io.InputStream; -import java.nio.file.Paths; + import java.time.Clock; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; @@ -20,6 +20,9 @@ import java.util.List; import java.util.Map; import java.util.Vector; +import org.datavaultplatform.common.storage.impl.ssh.UtilityJSchImproved; +import org.datavaultplatform.common.storage.impl.ssh.UtilityJSchNonRecurse; +import org.springframework.util.Assert; /** * An implementation of SFTPFileSystemDriver to use JCraft's Jsch ssh/sftp library. @@ -259,9 +262,33 @@ public long getSize(String path) throws Exception { if (!path.endsWith("/")) { path = path + "/"; } - - return UtilityJSch.calculateSize(channelSftp, path); - + + Runtime.getRuntime().gc(); + long t5 = System.currentTimeMillis(); + long sizeNonRecurse = UtilityJSchNonRecurse.calculateSize(channelSftp, path); + long t6 = System.currentTimeMillis(); + long timeNonRecurse = t6 - t5; + log.info("NON RECURSE TIME [{}]", timeNonRecurse); + + Runtime.getRuntime().gc(); + long t3 = System.currentTimeMillis(); + long sizeImproved = UtilityJSchImproved.calculateSize(channelSftp, path); + long t4 = System.currentTimeMillis(); + long timeImproved = t4 - t3; + log.info("IMPROVED TIME [{}]", timeImproved); + + Runtime.getRuntime().gc(); + long t1 = System.currentTimeMillis(); + long sizeOriginal = UtilityJSch.calculateSize(channelSftp, path); + long t2 = System.currentTimeMillis(); + long timeOriginal = t2 - t1; + log.info("ORIG TIME [{}]", timeOriginal); + + Assert.isTrue(sizeOriginal == sizeImproved, + String.format("original size[%s] != improved size[%s]", sizeOriginal, sizeImproved)); + Assert.isTrue(sizeOriginal == sizeNonRecurse, + String.format("original size[%s] != non-recurse size[%s]", sizeOriginal, sizeNonRecurse)); + return sizeOriginal; } else { return attrs.getSize(); } diff --git a/datavault-common/src/main/java/org/datavaultplatform/common/storage/impl/ssh/UtilityJSchImproved.java b/datavault-common/src/main/java/org/datavaultplatform/common/storage/impl/ssh/UtilityJSchImproved.java new file mode 100644 index 000000000..b6dc2cfa4 --- /dev/null +++ b/datavault-common/src/main/java/org/datavaultplatform/common/storage/impl/ssh/UtilityJSchImproved.java @@ -0,0 +1,60 @@ +package org.datavaultplatform.common.storage.impl.ssh; + +import com.jcraft.jsch.ChannelSftp; +import com.jcraft.jsch.ChannelSftp.LsEntry; +import com.jcraft.jsch.SftpException; +import java.util.Comparator; +import java.util.Iterator; + +public abstract class UtilityJSchImproved { + + public static long calculateSize(final ChannelSftp channel, + String remoteFile) throws SftpException { + + long bytes = 0; + String pwd = remoteFile; + + if (remoteFile.lastIndexOf('/') != -1) { + if (remoteFile.length() > 1) { + pwd = remoteFile.substring(0, remoteFile.lastIndexOf('/')); + } + } + + channel.cd(pwd); + + @SuppressWarnings("unchecked") final java.util.Vector files = channel.ls(remoteFile); + // Sort the entries, we will process directories last. + files.sort(COMPARATOR); + Iterator iter = files.iterator(); + //process each entry, removing it from Vector after processing, go to aid GC. + while(iter.hasNext()) { + final ChannelSftp.LsEntry le = iter.next(); + + // remove items from the Vector as soon as possible + iter.remove(); + + final String name = le.getFilename(); + if (le.getAttrs().isDir()) { + if (name.equals(".") || name.equals("..")) { + continue; + } + bytes += calculateSize(channel, channel.pwd() + "/" + name + "/"); + } else { + bytes += le.getAttrs().getSize(); + } + } + + channel.cd(".."); + return bytes; + } + + private static final Comparator COMPARATOR_DIR = + Comparator.comparing(entry -> entry.getAttrs().isDir()); + + private static final Comparator COMPARATOR_FILENAME = + Comparator.comparing(entry -> entry.getFilename()); + + public static final Comparator COMPARATOR = + COMPARATOR_DIR.thenComparing(COMPARATOR_FILENAME); + +} diff --git a/datavault-common/src/main/java/org/datavaultplatform/common/storage/impl/ssh/UtilityJSchNonRecurse.java b/datavault-common/src/main/java/org/datavaultplatform/common/storage/impl/ssh/UtilityJSchNonRecurse.java new file mode 100644 index 000000000..07a824445 --- /dev/null +++ b/datavault-common/src/main/java/org/datavaultplatform/common/storage/impl/ssh/UtilityJSchNonRecurse.java @@ -0,0 +1,58 @@ +package org.datavaultplatform.common.storage.impl.ssh; + +import com.jcraft.jsch.ChannelSftp; +import com.jcraft.jsch.ChannelSftp.LsEntry; +import com.jcraft.jsch.SftpException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Vector; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.datavaultplatform.common.storage.impl.ssh.stack.jsch.ItemContextChannel; +import org.datavaultplatform.common.storage.impl.ssh.stack.jsch.ItemLsEntryDir; +import org.datavaultplatform.common.storage.impl.ssh.stack.jsch.ItemLsEntryNonDir; +import org.datavaultplatform.common.storage.impl.ssh.stack.Item; +import org.datavaultplatform.common.storage.impl.ssh.stack.StackProcessor; + +@Slf4j +public abstract class UtilityJSchNonRecurse { + + public static long calculateSize(final ChannelSftp channel, + String remoteFile) throws SftpException { + + if (remoteFile.lastIndexOf('/') != -1) { + if (remoteFile.length() > 1) { + remoteFile = remoteFile.substring(0, remoteFile.lastIndexOf('/')); + } + } + channel.cd(remoteFile); + channel.cd(".."); + + ItemContextChannel ctx = new ItemContextChannel(channel); + + LsEntry initialEntry = getInitialEntry(channel, remoteFile); + + Item initialItem = + initialEntry.getAttrs().isDir() + ? new ItemLsEntryDir(initialEntry) + : new ItemLsEntryNonDir(initialEntry); + + StackProcessor processor = new StackProcessor<>(ctx, initialItem); + processor.process(); + return ctx.getSize(); + } + + @SneakyThrows + private static LsEntry getInitialEntry(ChannelSftp channel, String remoteFile) { + Path path = Paths.get(remoteFile); + int count = path.getNameCount(); + String endPath = path.getName(count - 1).toString(); + + Vector entries = channel.ls("*"); + LsEntry initialEntry = entries.stream() + .filter((LsEntry entry) -> endPath.equals(entry.getFilename())) + .findFirst() + .get(); + return initialEntry; + } +} diff --git a/datavault-common/src/main/java/org/datavaultplatform/common/storage/impl/ssh/stack/Item.java b/datavault-common/src/main/java/org/datavaultplatform/common/storage/impl/ssh/stack/Item.java new file mode 100644 index 000000000..0485217be --- /dev/null +++ b/datavault-common/src/main/java/org/datavaultplatform/common/storage/impl/ssh/stack/Item.java @@ -0,0 +1,8 @@ +package org.datavaultplatform.common.storage.impl.ssh.stack; + +import java.util.Stack; + +public interface Item { + + void process(Stack> stack, ItemContext context); +} diff --git a/datavault-common/src/main/java/org/datavaultplatform/common/storage/impl/ssh/stack/ItemContext.java b/datavault-common/src/main/java/org/datavaultplatform/common/storage/impl/ssh/stack/ItemContext.java new file mode 100644 index 000000000..f28c303e9 --- /dev/null +++ b/datavault-common/src/main/java/org/datavaultplatform/common/storage/impl/ssh/stack/ItemContext.java @@ -0,0 +1,29 @@ +package org.datavaultplatform.common.storage.impl.ssh.stack; + +public abstract class ItemContext { + + private T ctx; + private long size; + private long count; + + public long getSize(){ + return size; + } + public T getContext() { + return ctx; + } + + public void setContext(T ctx) { + this.ctx = ctx; + } + + public void increment(long inc){ + size += inc; + } + public long getCount(){ + return count; + } + public void incrementCount(){ + count+=1; + } +} diff --git a/datavault-common/src/main/java/org/datavaultplatform/common/storage/impl/ssh/stack/StackProcessor.java b/datavault-common/src/main/java/org/datavaultplatform/common/storage/impl/ssh/stack/StackProcessor.java new file mode 100644 index 000000000..b72e78849 --- /dev/null +++ b/datavault-common/src/main/java/org/datavaultplatform/common/storage/impl/ssh/stack/StackProcessor.java @@ -0,0 +1,40 @@ +package org.datavaultplatform.common.storage.impl.ssh.stack; + +import java.util.Stack; + +/* +An abstract stack processor which can be used to crawl a tree of directories/files +without using recursion. +@See org.datavaultplatform.common.storage.impl.ssh.UtilityJSchNonRecurse +@see + */ +public class StackProcessor { + + private final ItemContext context; + private final Item initialItem; + + public StackProcessor(ItemContext context, Item initialItem) { + this.context = context; + this.initialItem = initialItem; + } + + /* + when crawling directories, the Stack represents files/directories discovered but not processed + and the context can represent the current directory and any calculated results + */ + public ItemContext process() { + + + Stack> stack = new Stack(); + + stack.push(initialItem); + + while (!stack.isEmpty()) { + Item item = stack.pop(); + // when we process an item it may push more items on the stack, it may affect the context + item.process(stack, context); + } + return context; + } +} + diff --git a/datavault-common/src/main/java/org/datavaultplatform/common/storage/impl/ssh/stack/jsch/ItemContextChannel.java b/datavault-common/src/main/java/org/datavaultplatform/common/storage/impl/ssh/stack/jsch/ItemContextChannel.java new file mode 100644 index 000000000..e973b75f7 --- /dev/null +++ b/datavault-common/src/main/java/org/datavaultplatform/common/storage/impl/ssh/stack/jsch/ItemContextChannel.java @@ -0,0 +1,17 @@ +package org.datavaultplatform.common.storage.impl.ssh.stack.jsch; + +import com.jcraft.jsch.ChannelSftp; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.datavaultplatform.common.storage.impl.ssh.stack.ItemContext; + +@Slf4j +public class ItemContextChannel extends ItemContext { + + @SneakyThrows + public ItemContextChannel(ChannelSftp channel){ + log.info("initial pwd [{}]", channel.pwd()); + setContext(channel); + } + +} diff --git a/datavault-common/src/main/java/org/datavaultplatform/common/storage/impl/ssh/stack/jsch/ItemLsEntryDir.java b/datavault-common/src/main/java/org/datavaultplatform/common/storage/impl/ssh/stack/jsch/ItemLsEntryDir.java new file mode 100644 index 000000000..9a30061d5 --- /dev/null +++ b/datavault-common/src/main/java/org/datavaultplatform/common/storage/impl/ssh/stack/jsch/ItemLsEntryDir.java @@ -0,0 +1,46 @@ +package org.datavaultplatform.common.storage.impl.ssh.stack.jsch; + +import com.jcraft.jsch.ChannelSftp; +import com.jcraft.jsch.ChannelSftp.LsEntry; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Stack; +import java.util.Vector; +import java.util.stream.Collectors; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.datavaultplatform.common.storage.impl.ssh.stack.Item; +import org.datavaultplatform.common.storage.impl.ssh.stack.ItemContext; +import org.springframework.util.Assert; + +@Slf4j +public class ItemLsEntryDir implements Item { + + private final LsEntry listEntry; + + public ItemLsEntryDir(LsEntry listEntry) { + Assert.isTrue(listEntry.getAttrs().isDir()); + this.listEntry = listEntry; + } + + @Override + @SneakyThrows + public void process(Stack> stack, ItemContext ctx) { + ctx.getContext().cd(this.listEntry.getFilename()); + + Vector entries = ctx.getContext().ls("*"); + + Map> groupedItems = entries.stream() + .collect(Collectors.partitioningBy((LsEntry item) -> item.getAttrs().isDir())); + + // push 'cd ..' - process this third (last) + stack.push(ItemLsEntryEndDir.getInstance()); + + // push all directories - process these second + groupedItems.get(true).forEach(dirEntry -> stack.push(new ItemLsEntryDir(dirEntry))); + + // push all 'non directories' - process these first + groupedItems.get(false).forEach(nonDirEntry -> stack.push(new ItemLsEntryNonDir(nonDirEntry))); + } +} diff --git a/datavault-common/src/main/java/org/datavaultplatform/common/storage/impl/ssh/stack/jsch/ItemLsEntryEndDir.java b/datavault-common/src/main/java/org/datavaultplatform/common/storage/impl/ssh/stack/jsch/ItemLsEntryEndDir.java new file mode 100644 index 000000000..10cf9a5b6 --- /dev/null +++ b/datavault-common/src/main/java/org/datavaultplatform/common/storage/impl/ssh/stack/jsch/ItemLsEntryEndDir.java @@ -0,0 +1,29 @@ +package org.datavaultplatform.common.storage.impl.ssh.stack.jsch; + +import com.jcraft.jsch.ChannelSftp; +import com.jcraft.jsch.ChannelSftp.LsEntry; +import java.util.Stack; +import lombok.SneakyThrows; +import org.datavaultplatform.common.storage.impl.ssh.stack.Item; +import org.datavaultplatform.common.storage.impl.ssh.stack.ItemContext; + +public class ItemLsEntryEndDir implements Item { + + private static final ItemLsEntryEndDir INSTANCE = new ItemLsEntryEndDir(); + + private ItemLsEntryEndDir() { + } + + public static ItemLsEntryEndDir getInstance() { + return INSTANCE; + } + + /** + * When we process this Item, the context goes back up a level + */ + @Override + @SneakyThrows + public void process(Stack> stack, ItemContext ctx) { + ctx.getContext().cd(".."); + } +} diff --git a/datavault-common/src/main/java/org/datavaultplatform/common/storage/impl/ssh/stack/jsch/ItemLsEntryNonDir.java b/datavault-common/src/main/java/org/datavaultplatform/common/storage/impl/ssh/stack/jsch/ItemLsEntryNonDir.java new file mode 100644 index 000000000..6ae757a18 --- /dev/null +++ b/datavault-common/src/main/java/org/datavaultplatform/common/storage/impl/ssh/stack/jsch/ItemLsEntryNonDir.java @@ -0,0 +1,27 @@ +package org.datavaultplatform.common.storage.impl.ssh.stack.jsch; + +import com.jcraft.jsch.ChannelSftp; +import com.jcraft.jsch.ChannelSftp.LsEntry; +import java.util.Stack; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.datavaultplatform.common.storage.impl.ssh.stack.Item; +import org.datavaultplatform.common.storage.impl.ssh.stack.ItemContext; +import org.springframework.util.Assert; + +@Slf4j +public class ItemLsEntryNonDir implements Item { + + private final LsEntry listEntry; + + public ItemLsEntryNonDir(LsEntry listEntry) { + Assert.isTrue(!listEntry.getAttrs().isDir()); + this.listEntry = listEntry; + } + + @Override + @SneakyThrows + public void process(Stack> stack, ItemContext ctx) { + ctx.increment(this.listEntry.getAttrs().getSize()); + } +} diff --git a/datavault-common/src/test/java/org/datavaultplatform/common/storage/impl/ssh/UtilityJSchImprovedTest.java b/datavault-common/src/test/java/org/datavaultplatform/common/storage/impl/ssh/UtilityJSchImprovedTest.java new file mode 100644 index 000000000..4a5827882 --- /dev/null +++ b/datavault-common/src/test/java/org/datavaultplatform/common/storage/impl/ssh/UtilityJSchImprovedTest.java @@ -0,0 +1,88 @@ +package org.datavaultplatform.common.storage.impl.ssh; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.when; + +import com.jcraft.jsch.ChannelSftp; +import com.jcraft.jsch.ChannelSftp.LsEntry; +import com.jcraft.jsch.SftpATTRS; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Vector; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * This is a test of + * 1) the Comparator used in UtilityJSchImproved + * 2) the removal of items from an underlying Vector using Iterator.remove() + */ +@ExtendWith(MockitoExtension.class) +public class UtilityJSchImprovedTest { + + @Mock + ChannelSftp.LsEntry entry1; + @Mock + ChannelSftp.LsEntry entry2; + @Mock + ChannelSftp.LsEntry entry3; + @Mock + ChannelSftp.LsEntry entry4; + + @Mock + SftpATTRS attrs1; + + @Mock + SftpATTRS attrs2; + @Mock + SftpATTRS attrs3; + @Mock + SftpATTRS attrs4; + + @Test + void testComparator() { + + when(entry1.getAttrs()).thenReturn(attrs1); + when(entry2.getAttrs()).thenReturn(attrs2); + when(entry3.getAttrs()).thenReturn(attrs3); + when(entry4.getAttrs()).thenReturn(attrs4); + + when(attrs1.isDir()).thenReturn(true); + when(attrs2.isDir()).thenReturn(true); + when(attrs3.isDir()).thenReturn( false); + when(attrs4.isDir()).thenReturn(false); + when(entry1.getFilename()).thenReturn("D-BBBB"); + when(entry2.getFilename()).thenReturn("D-AAAA"); + when(entry3.getFilename()).thenReturn("F-BBBB"); + when(entry4.getFilename()).thenReturn("F-AAAA"); + + List items = Arrays.asList(this.entry1, this.entry2, this.entry3, this.entry4); + Collections.shuffle(items); + + items.forEach(item -> System.out.printf("BEFORE[%s]%n",item.getFilename())); + items.sort(UtilityJSchImproved.COMPARATOR); + + items.forEach(item -> System.out.printf("AFTER[%s]%n",item.getFilename())); + + assertEquals(entry4, items.get(0)); + assertEquals(entry3, items.get(1)); + assertEquals(entry2, items.get(2)); + assertEquals(entry1, items.get(3)); + } + + @Test + void testRemoveFromVectorIterator(){ + Vector items = new Vector<>(Arrays.asList("1","2","3","4","5")); + Iterator iter = items.iterator(); + while(iter.hasNext()) { + String item = iter.next(); + System.out.printf("item[%s]items count[%d]%n", item, items.size()); + iter.remove(); + } + assertEquals(0, items.size()); + } +} diff --git a/datavault-webapp/src/main/java/org/datavaultplatform/webapp/authentication/shib/ShibUtils.java b/datavault-webapp/src/main/java/org/datavaultplatform/webapp/authentication/authorization/AuthorizationUtils.java similarity index 84% rename from datavault-webapp/src/main/java/org/datavaultplatform/webapp/authentication/shib/ShibUtils.java rename to datavault-webapp/src/main/java/org/datavaultplatform/webapp/authentication/authorization/AuthorizationUtils.java index d50a1bdee..9656a210c 100644 --- a/datavault-webapp/src/main/java/org/datavaultplatform/webapp/authentication/shib/ShibUtils.java +++ b/datavault-webapp/src/main/java/org/datavaultplatform/webapp/authentication/authorization/AuthorizationUtils.java @@ -1,10 +1,10 @@ -package org.datavaultplatform.webapp.authentication.shib; +package org.datavaultplatform.webapp.authentication.authorization; import org.datavaultplatform.common.model.RoleName; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; -public abstract class ShibUtils { +public abstract class AuthorizationUtils { public static final GrantedAuthority ROLE_USER = new SimpleGrantedAuthority(RoleName.ROLE_USER); public static final GrantedAuthority ROLE_ADMIN = new SimpleGrantedAuthority(RoleName.ROLE_ADMIN); public static final GrantedAuthority ROLE_IS_ADMIN = new SimpleGrantedAuthority(RoleName.ROLE_IS_ADMIN); diff --git a/datavault-webapp/src/main/java/org/datavaultplatform/webapp/authentication/shib/ShibGrantedAuthorityService.java b/datavault-webapp/src/main/java/org/datavaultplatform/webapp/authentication/authorization/GrantedAuthorityService.java similarity index 88% rename from datavault-webapp/src/main/java/org/datavaultplatform/webapp/authentication/shib/ShibGrantedAuthorityService.java rename to datavault-webapp/src/main/java/org/datavaultplatform/webapp/authentication/authorization/GrantedAuthorityService.java index aacc2af51..3011dcab9 100644 --- a/datavault-webapp/src/main/java/org/datavaultplatform/webapp/authentication/shib/ShibGrantedAuthorityService.java +++ b/datavault-webapp/src/main/java/org/datavaultplatform/webapp/authentication/authorization/GrantedAuthorityService.java @@ -1,4 +1,4 @@ -package org.datavaultplatform.webapp.authentication.shib; +package org.datavaultplatform.webapp.authentication.authorization; import java.security.Principal; import java.util.ArrayList; @@ -17,13 +17,13 @@ import org.springframework.security.core.authority.SimpleGrantedAuthority; @Slf4j -public class ShibGrantedAuthorityService { +public class GrantedAuthorityService { private final RestService restService; private final PermissionsService permissionsService; - public ShibGrantedAuthorityService(RestService restService, PermissionsService permissionsService) { + public GrantedAuthorityService(RestService restService, PermissionsService permissionsService) { this.restService = restService; this.permissionsService = permissionsService; } @@ -40,7 +40,7 @@ public List getGrantedAuthoritiesForUser(String name, Authenti try { isAdmin = restService.isAdmin(new ValidateUser(name, null)); if (isAdmin) { - grantedAuths.add(ShibUtils.ROLE_IS_ADMIN); + grantedAuths.add(AuthorizationUtils.ROLE_IS_ADMIN); } } catch (Exception ex) { log.error("Error when trying to check if user is admin with Broker!", ex); @@ -49,7 +49,7 @@ public List getGrantedAuthoritiesForUser(String name, Authenti Collection adminAuthorities = getAdminAuthorities(authentication); if (!adminAuthorities.isEmpty()) { log.info("Granting user {} ROLE_ADMIN", name); - grantedAuths.add(ShibUtils.ROLE_ADMIN); + grantedAuths.add(AuthorizationUtils.ROLE_ADMIN); grantedAuths.addAll(adminAuthorities); } return grantedAuths; diff --git a/datavault-webapp/src/main/java/org/datavaultplatform/webapp/authentication/database/DatabaseAuthenticationProvider.java b/datavault-webapp/src/main/java/org/datavaultplatform/webapp/authentication/database/DatabaseAuthenticationProvider.java index f8fef46c7..3a6c89dde 100644 --- a/datavault-webapp/src/main/java/org/datavaultplatform/webapp/authentication/database/DatabaseAuthenticationProvider.java +++ b/datavault-webapp/src/main/java/org/datavaultplatform/webapp/authentication/database/DatabaseAuthenticationProvider.java @@ -1,11 +1,8 @@ package org.datavaultplatform.webapp.authentication.database; -import org.datavaultplatform.common.model.RoleAssignment; import org.datavaultplatform.common.model.RoleName; import org.datavaultplatform.common.request.ValidateUser; -import org.datavaultplatform.webapp.security.ScopedGrantedAuthority; -import org.datavaultplatform.webapp.model.AdminDashboardPermissionsModel; -import org.datavaultplatform.webapp.services.PermissionsService; +import org.datavaultplatform.webapp.authentication.authorization.GrantedAuthorityService; import org.datavaultplatform.webapp.services.RestService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -17,11 +14,8 @@ import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; -import java.security.Principal; import java.util.ArrayList; -import java.util.Collection; import java.util.List; -import java.util.stream.Collectors; /** * User: Robin Taylor @@ -35,11 +29,11 @@ public class DatabaseAuthenticationProvider implements AuthenticationProvider { private final RestService restService; - private final PermissionsService permissionsService; + private final GrantedAuthorityService grantedAuthorityService; - public DatabaseAuthenticationProvider(RestService restService, PermissionsService permissionsService) { + public DatabaseAuthenticationProvider(RestService restService, GrantedAuthorityService grantedAuthorityService) { this.restService = restService; - this.permissionsService = permissionsService; + this.grantedAuthorityService = grantedAuthorityService; } @Override @@ -63,40 +57,13 @@ public Authentication authenticate(Authentication authentication) throws Authent logger.info("Authentication success for " + name); List grantedAuths = new ArrayList<>(); - List roles = restService.getRoleAssignmentsForUser(name); - List scopedAuthorities = ScopedGrantedAuthority.fromRoleAssignments(roles); - - grantedAuths.addAll(scopedAuthorities); - - boolean isAdmin = false; - try{ - isAdmin = restService.isAdmin(new ValidateUser(name, null)); - if (isAdmin) { - grantedAuths.add(new SimpleGrantedAuthority(RoleName.ROLE_IS_ADMIN)); - } - } catch(Exception e){ - logger.error("Error when trying to check if user is admin with Broker!",e); - } - Collection adminAuthorities = getAdminAuthorities(authentication); - if (!adminAuthorities.isEmpty()) { - logger.info("Granting user " + name + " " + RoleName.ROLE_ADMIN); - grantedAuths.add(new SimpleGrantedAuthority(RoleName.ROLE_ADMIN)); - grantedAuths.addAll(adminAuthorities); - } + List grantedAuthsForUser = this.grantedAuthorityService.getGrantedAuthoritiesForUser(name, authentication); + grantedAuths.addAll(grantedAuthsForUser); logger.info("Granting user " + name + " " + RoleName.ROLE_USER); grantedAuths.add(new SimpleGrantedAuthority(RoleName.ROLE_USER)); return new UsernamePasswordAuthenticationToken(name, password, grantedAuths); - - } - - private Collection getAdminAuthorities(Principal principal) { - AdminDashboardPermissionsModel adminPermissions = permissionsService.getDashboardPermissions(principal); - return adminPermissions.getUnscopedPermissions().stream() - .filter(p -> p.getPermission().getRoleName() != null) - .map(p -> new SimpleGrantedAuthority(p.getPermission().getRoleName())) - .collect(Collectors.toSet()); } @Override diff --git a/datavault-webapp/src/main/java/org/datavaultplatform/webapp/authentication/shib/ShibAuthenticationProvider.java b/datavault-webapp/src/main/java/org/datavaultplatform/webapp/authentication/shib/ShibAuthenticationProvider.java index 5cd5d1eec..3be03c166 100644 --- a/datavault-webapp/src/main/java/org/datavaultplatform/webapp/authentication/shib/ShibAuthenticationProvider.java +++ b/datavault-webapp/src/main/java/org/datavaultplatform/webapp/authentication/shib/ShibAuthenticationProvider.java @@ -4,6 +4,8 @@ import org.datavaultplatform.common.request.ValidateUser; import org.datavaultplatform.common.services.LDAPService; import org.datavaultplatform.webapp.authentication.AuthenticationSuccess; +import org.datavaultplatform.webapp.authentication.authorization.AuthorizationUtils; +import org.datavaultplatform.webapp.authentication.authorization.GrantedAuthorityService; import org.datavaultplatform.webapp.services.RestService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -35,18 +37,18 @@ public class ShibAuthenticationProvider implements AuthenticationProvider { private final RestService restService; private final LDAPService ldapService; - private final ShibGrantedAuthorityService shibGrantedAuthorityService; + private final GrantedAuthorityService grantedAuthorityService; private final boolean ldapEnabled; @Autowired private AuthenticationSuccess authenticationSuccess; - public ShibAuthenticationProvider(RestService restService, LDAPService ldapService, boolean ldapEnabled, ShibGrantedAuthorityService shibGrantedAuthorityService ) { + public ShibAuthenticationProvider(RestService restService, LDAPService ldapService, boolean ldapEnabled, GrantedAuthorityService grantedAuthorityService) { this.restService = restService; this.ldapService = ldapService; this.ldapEnabled = ldapEnabled; - this.shibGrantedAuthorityService = shibGrantedAuthorityService; + this.grantedAuthorityService = grantedAuthorityService; } @@ -84,11 +86,11 @@ public Authentication authenticate(Authentication authentication) throws Authent logger.error("Error when trying to add user with Broker!",e); } } else { - List grantedAuthsForUser = this.shibGrantedAuthorityService.getGrantedAuthoritiesForUser(name, authentication); + List grantedAuthsForUser = this.grantedAuthorityService.getGrantedAuthoritiesForUser(name, authentication); grantedAuths.addAll(grantedAuthsForUser); } - grantedAuths.add(ShibUtils.ROLE_USER); + grantedAuths.add(AuthorizationUtils.ROLE_USER); return new PreAuthenticatedAuthenticationToken(name, password, grantedAuths); } diff --git a/datavault-webapp/src/main/java/org/datavaultplatform/webapp/config/database/DatabaseProfileConfig.java b/datavault-webapp/src/main/java/org/datavaultplatform/webapp/config/database/DatabaseProfileConfig.java index f55270536..0875aad6f 100644 --- a/datavault-webapp/src/main/java/org/datavaultplatform/webapp/config/database/DatabaseProfileConfig.java +++ b/datavault-webapp/src/main/java/org/datavaultplatform/webapp/config/database/DatabaseProfileConfig.java @@ -1,5 +1,6 @@ package org.datavaultplatform.webapp.config.database; +import org.datavaultplatform.webapp.authentication.authorization.GrantedAuthorityService; import org.datavaultplatform.webapp.authentication.database.DatabaseAuthenticationProvider; import org.datavaultplatform.webapp.services.PermissionsService; import org.datavaultplatform.webapp.services.RestService; @@ -15,7 +16,12 @@ public class DatabaseProfileConfig { @Bean DatabaseAuthenticationProvider databaseAuthenticationProvider(RestService restService, PermissionsService permissionsService){ - return new DatabaseAuthenticationProvider(restService, permissionsService); + return new DatabaseAuthenticationProvider(restService, grantedAuthorityService(restService,permissionsService)); + } + + @Bean + GrantedAuthorityService grantedAuthorityService(RestService restService, PermissionsService permissionsService) { + return new GrantedAuthorityService(restService, permissionsService); } } diff --git a/datavault-webapp/src/main/java/org/datavaultplatform/webapp/config/shib/ShibProfileConfig.java b/datavault-webapp/src/main/java/org/datavaultplatform/webapp/config/shib/ShibProfileConfig.java index 0979c9252..e343d7d24 100644 --- a/datavault-webapp/src/main/java/org/datavaultplatform/webapp/config/shib/ShibProfileConfig.java +++ b/datavault-webapp/src/main/java/org/datavaultplatform/webapp/config/shib/ShibProfileConfig.java @@ -4,7 +4,7 @@ import org.datavaultplatform.common.services.LDAPService; import org.datavaultplatform.webapp.authentication.shib.ShibAuthenticationListener; import org.datavaultplatform.webapp.authentication.shib.ShibAuthenticationProvider; -import org.datavaultplatform.webapp.authentication.shib.ShibGrantedAuthorityService; +import org.datavaultplatform.webapp.authentication.authorization.GrantedAuthorityService; import org.datavaultplatform.webapp.authentication.shib.ShibWebAuthenticationDetailsSource; import org.datavaultplatform.webapp.services.PermissionsService; import org.datavaultplatform.webapp.services.RestService; @@ -34,9 +34,9 @@ ShibAuthenticationProvider shibAuthenticationProvider( RestService restService, LDAPService ldapService, @Value("${ldap.enabled}") boolean ldapEnabled, - ShibGrantedAuthorityService shibGrantedAuthorityService + GrantedAuthorityService grantedAuthorityService ){ - ShibAuthenticationProvider result = new ShibAuthenticationProvider(restService, ldapService, ldapEnabled, shibGrantedAuthorityService); + ShibAuthenticationProvider result = new ShibAuthenticationProvider(restService, ldapService, ldapEnabled, grantedAuthorityService); return result; } @@ -67,8 +67,8 @@ Http403ForbiddenEntryPoint http403EntryPoint() { } @Bean - ShibGrantedAuthorityService shibGrantedAuthorityService(RestService restService, PermissionsService permissionsService) { - return new ShibGrantedAuthorityService(restService, permissionsService); + GrantedAuthorityService shibGrantedAuthorityService(RestService restService, PermissionsService permissionsService) { + return new GrantedAuthorityService(restService, permissionsService); } @Bean diff --git a/datavault-webapp/src/test/java/org/datavaultplatform/webapp/app/ajp/AjpConnectorIT.java b/datavault-webapp/src/test/java/org/datavaultplatform/webapp/app/ajp/AjpConnectorIT.java index 93557ce42..9fe9d3815 100644 --- a/datavault-webapp/src/test/java/org/datavaultplatform/webapp/app/ajp/AjpConnectorIT.java +++ b/datavault-webapp/src/test/java/org/datavaultplatform/webapp/app/ajp/AjpConnectorIT.java @@ -19,7 +19,7 @@ import org.datavaultplatform.common.services.LDAPService; import org.datavaultplatform.common.util.DisabledInsideDocker; import org.datavaultplatform.webapp.authentication.shib.ShibAuthenticationListener; -import org.datavaultplatform.webapp.authentication.shib.ShibGrantedAuthorityService; +import org.datavaultplatform.webapp.authentication.authorization.GrantedAuthorityService; import org.datavaultplatform.webapp.services.RestService; import org.datavaultplatform.webapp.test.ProfileShib; import org.junit.jupiter.api.BeforeEach; @@ -72,7 +72,7 @@ class AjpConnectorIT { LDAPService mLdapService; @MockBean - ShibGrantedAuthorityService mGrantedAuthorityService; + GrantedAuthorityService mGrantedAuthorityService; @Mock User mUser; diff --git a/datavault-webapp/src/test/java/org/datavaultplatform/webapp/app/authentication/shib/LoginUsingShibTest.java b/datavault-webapp/src/test/java/org/datavaultplatform/webapp/app/authentication/shib/LoginUsingShibTest.java index 1aa4a0654..6b3b94234 100644 --- a/datavault-webapp/src/test/java/org/datavaultplatform/webapp/app/authentication/shib/LoginUsingShibTest.java +++ b/datavault-webapp/src/test/java/org/datavaultplatform/webapp/app/authentication/shib/LoginUsingShibTest.java @@ -25,8 +25,8 @@ import org.datavaultplatform.common.request.ValidateUser; import org.datavaultplatform.common.services.LDAPService; import org.datavaultplatform.webapp.authentication.shib.ShibAuthenticationListener; -import org.datavaultplatform.webapp.authentication.shib.ShibGrantedAuthorityService; -import org.datavaultplatform.webapp.authentication.shib.ShibUtils; +import org.datavaultplatform.webapp.authentication.authorization.GrantedAuthorityService; +import org.datavaultplatform.webapp.authentication.authorization.AuthorizationUtils; import org.datavaultplatform.webapp.authentication.shib.ShibWebAuthenticationDetails; import org.datavaultplatform.webapp.services.RestService; import org.datavaultplatform.webapp.test.ProfileShib; @@ -70,7 +70,7 @@ public class LoginUsingShibTest { LDAPService mLdapService; @MockBean - ShibGrantedAuthorityService mGrantedAuthorityService; + GrantedAuthorityService mGrantedAuthorityService; @Mock GrantedAuthority mGA1; @@ -124,7 +124,7 @@ void testShibLoginExistingUser() throws Exception { assertEquals("N/A", argAuthentication.getValue().getCredentials()); Mockito.verify(mGrantedAuthorityService).getGrantedAuthoritiesForUser(argName.getValue(), argAuthentication.getValue()); - checkLoggedInUser(result, sessionId, new HashSet<>(Arrays.asList(ShibUtils.ROLE_USER, mGA1, mGA2))); + checkLoggedInUser(result, sessionId, new HashSet<>(Arrays.asList(AuthorizationUtils.ROLE_USER, mGA1, mGA2))); Mockito.verify(mAuthListener).onApplicationEvent(argAuthSuccessEvent.getValue()); PreAuthenticatedAuthenticationToken auth = (PreAuthenticatedAuthenticationToken)argAuthSuccessEvent.getValue().getAuthentication(); @@ -171,7 +171,7 @@ void testShibLoginNewUser() throws Exception { assertNull(argUser.getValue().getFileStores()); assertNull(argUser.getValue().getVaults()); - checkLoggedInUser(result, sessionId, Collections.singleton(ShibUtils.ROLE_USER)); + checkLoggedInUser(result, sessionId, Collections.singleton(AuthorizationUtils.ROLE_USER)); Mockito.verify(mAuthListener).onApplicationEvent(argAuthSuccessEvent.getValue()); @@ -180,7 +180,7 @@ void testShibLoginNewUser() throws Exception { } void checkLoggedInUser(MvcResult result, String expectedSessionId, Set expectedAuthorities){ - SecurityContext sc = (SecurityContext) result.getRequest().getSession().getAttribute(ShibUtils.SPRING_SECURITY_CONTEXT); + SecurityContext sc = (SecurityContext) result.getRequest().getSession().getAttribute(AuthorizationUtils.SPRING_SECURITY_CONTEXT); Authentication auth = sc.getAuthentication(); assertTrue(auth instanceof PreAuthenticatedAuthenticationToken); diff --git a/datavault-worker/src/test/java/org/datavaultplatform/broker/services/UserKeyPairService.java b/datavault-worker/src/test/java/org/datavaultplatform/broker/services/UserKeyPairService.java new file mode 100644 index 000000000..a39fc94c4 --- /dev/null +++ b/datavault-worker/src/test/java/org/datavaultplatform/broker/services/UserKeyPairService.java @@ -0,0 +1,24 @@ +package org.datavaultplatform.broker.services; + +import lombok.Builder; +import lombok.Data; + +public interface UserKeyPairService { + + // comment added at the end of public key + String PUBKEY_COMMENT = "datavault"; + + String getPassphrase(); + + KeyPairInfo generateNewKeyPair(); + + @Data + @Builder + class KeyPairInfo { + + private final String publicKey; + private final String privateKey; + private final String fingerPrint; + private final Integer keySize; + } +} diff --git a/datavault-worker/src/test/java/org/datavaultplatform/broker/services/UserKeyPairServiceImpl.java b/datavault-worker/src/test/java/org/datavaultplatform/broker/services/UserKeyPairServiceImpl.java new file mode 100644 index 000000000..f5be5ec36 --- /dev/null +++ b/datavault-worker/src/test/java/org/datavaultplatform/broker/services/UserKeyPairServiceImpl.java @@ -0,0 +1,71 @@ +package org.datavaultplatform.broker.services; + + +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.datavaultplatform.common.crypto.Encryption; +import org.datavaultplatform.common.crypto.SshRsaKeyUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; + +/** + * User: David Hay + * Date: 22/Aug/2022 + * Time: 16:45 + */ +@Service +@Slf4j +@Transactional +@Primary +public class UserKeyPairServiceImpl implements UserKeyPairService { + + static { + Encryption.addBouncyCastleSecurityProvider(); + } + + private final String passphrase; + + private static final int KEY_SIZE = 1024; + + @Autowired + public UserKeyPairServiceImpl(@Value("${sftp.passphrase}") String passphrase) { + this.passphrase = passphrase; + } + + @Override + public String getPassphrase() { + return passphrase; + } + + @Override + @SneakyThrows + public KeyPairInfo generateNewKeyPair() { + + KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA"); + generator.initialize(1024); + + KeyPair kp = generator.generateKeyPair(); + RSAPublicKey publicKey = (RSAPublicKey) kp.getPublic(); + RSAPrivateKey privateKey = (RSAPrivateKey) kp.getPrivate(); + + String privateKeyValue = SshRsaKeyUtils.encodePrivateKey(privateKey, passphrase); + + String publicKeyValue = SshRsaKeyUtils.encodePublicKey(publicKey, PUBKEY_COMMENT); + + return KeyPairInfo.builder() + .privateKey(privateKeyValue) + .publicKey(publicKeyValue) + .fingerPrint(SshRsaKeyUtils.calculateFingerprint(publicKeyValue)) + .keySize(KEY_SIZE) + .build(); + + } +} diff --git a/datavault-worker/src/test/java/org/datavaultplatform/worker/tasks/sftp/BasePerformDepositThenRetrieveUsingSftpIT.java b/datavault-worker/src/test/java/org/datavaultplatform/worker/tasks/sftp/BasePerformDepositThenRetrieveUsingSftpIT.java new file mode 100644 index 000000000..5ea5a1347 --- /dev/null +++ b/datavault-worker/src/test/java/org/datavaultplatform/worker/tasks/sftp/BasePerformDepositThenRetrieveUsingSftpIT.java @@ -0,0 +1,560 @@ +package org.datavaultplatform.worker.tasks.sftp; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.rabbitmq.client.Channel; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.compress.archivers.ArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.awaitility.Awaitility; +import org.datavaultplatform.broker.services.UserKeyPairService; +import org.datavaultplatform.broker.services.UserKeyPairServiceImpl; +import org.datavaultplatform.common.PropNames; +import org.datavaultplatform.common.config.BaseQueueConfig; +import org.datavaultplatform.common.crypto.Encryption; +import org.datavaultplatform.common.docker.DockerImage; +import org.datavaultplatform.common.event.Event; +import org.datavaultplatform.common.event.deposit.Complete; +import org.datavaultplatform.common.event.deposit.ComputedDigest; +import org.datavaultplatform.common.event.deposit.ComputedEncryption; +import org.datavaultplatform.common.event.retrieve.RetrieveComplete; +import org.datavaultplatform.common.io.FileUtils; +import org.datavaultplatform.common.storage.Verify; +import org.datavaultplatform.common.storage.impl.SFTPFileSystem; +import org.datavaultplatform.common.task.Context.AESMode; +import org.datavaultplatform.common.util.StorageClassNameResolver; +import org.datavaultplatform.worker.rabbit.BaseRabbitTCTest; +import org.datavaultplatform.worker.tasks.Deposit; +import org.datavaultplatform.worker.utils.DepositEvents; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.amqp.core.AmqpAdmin; +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessageProperties; +import org.springframework.amqp.core.Queue; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.support.AmqpHeaders; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.messaging.handler.annotation.Header; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.springframework.test.context.TestPropertySource; +import org.testcontainers.containers.Container; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.utility.DockerImageName; +import org.testcontainers.utility.MountableFile; + +import javax.crypto.SecretKey; +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.time.Duration; +import java.util.*; +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +@Slf4j +@TestPropertySource(properties = {"chunking.enabled=true","chunking.size=20MB"}) +public abstract class BasePerformDepositThenRetrieveUsingSftpIT extends BaseRabbitTCTest { + + private static final String TEST_USER = "testuser"; + private static final String ENV_USER_NAME = "USER_NAME"; + private static final String ENV_PUBLIC_KEY = "PUBLIC_KEY"; + private static final String TEST_PASSPHRASE = "tenet"; + + static final String KEY_NAME_FOR_SSH = "key-name-for-ssh"; + static final String KEY_NAME_FOR_DATA = "key-name-for-data"; + static final String KEY_STORE_PASSWORD = "testPassword"; + + final Resource depositMessage = new ClassPathResource("sampleMessages/sampleDepositMessage.json"); + + final List events = new ArrayList<>(); + @Autowired + protected AmqpAdmin rabbitAdmin; + @Autowired + protected RabbitTemplate template; + @Autowired + @Qualifier("workerQueue") //the name of the bean, not the Q + protected Queue workerQueue; + String keyStorePath; + + @Value("${metaDir}") + String metaDir; + File sourceDir; + File destDir; + File retrieveBaseDir; + File retrieveDir; + @Autowired + ObjectMapper mapper; + + @Value("classpath:big_data/50MB_file") + Resource largeFile; + + @Value("${chunking.enabled:false}") + private boolean chunkingEnabled; + + @Value("${chunking.size:0}") + private String chunkingByteSize; + + @Autowired + private StorageClassNameResolver resolver; + + private String sftpPublicKey; + private String sftpPrivateKey; + + private GenericContainer userDataSourceContainer; + + private HashMap sftpSrcProps; + private HashMap sftpTargetProps; + + @SneakyThrows + static Set getPathsWithinTarFile(File tarFile) { + Set paths = new HashSet<>(); + try (TarArchiveInputStream tarIn = new TarArchiveInputStream(Files.newInputStream(tarFile.toPath()))) { + TarArchiveEntry entry; + while ((entry = tarIn.getNextTarEntry()) != null) { + if (entry.isDirectory()) { + continue; + } + paths.add(Paths.get(entry.getName())); + } + } + return paths; + } + + @DynamicPropertySource + @SneakyThrows + static void setupProperties(DynamicPropertyRegistry registry) { + File baseTemp = Files.createTempDirectory("test").toFile(); + File tempDir = new File(baseTemp, "temp"); + assertTrue(tempDir.mkdir()); + + File metaDir = new File(baseTemp, "meta"); + assertTrue(metaDir.mkdir()); + + String tempDirValue = tempDir.getCanonicalPath(); + String metaDirValue = metaDir.getCanonicalPath(); + + registry.add("tempDir", () -> tempDirValue); + registry.add("metaDir", () -> metaDirValue); + } + + protected String sendNormalMessage(String msgBody) { + MessageProperties props = new MessageProperties(); + props.setMessageId(UUID.randomUUID().toString()); + props.setPriority(NORMAL_PRIORITY); + Message msg = new Message(msgBody.getBytes(StandardCharsets.UTF_8), props); + template.send(workerQueue.getActualName(), msg); + return props.getMessageId(); + } + + @BeforeEach + @SneakyThrows + void setup() { + checkChunkingProps(this.chunkingEnabled, this.chunkingByteSize); + purgeQueues(); + setupKeystore(); + setupDirectoriesAndFiles(); + setupSFTP(); + } + + final void checkChunkingProps(boolean chunkingEnabled, String chunkingByteSize) { + assertTrue(chunkingEnabled); + assertEquals("20MB", chunkingByteSize); + } + + @SneakyThrows + private void setupDirectoriesAndFiles() { + Path baseTemp = Files.createTempDirectory("tmpSftp"); + + sourceDir = baseTemp.resolve("source").toFile(); + assertTrue(sourceDir.mkdir()); + destDir = baseTemp.resolve("dest").toFile(); + assertTrue(destDir.mkdir()); + retrieveBaseDir = baseTemp.resolve("retrieve").toFile(); + assertTrue(retrieveBaseDir.mkdir()); + retrieveDir = retrieveBaseDir.toPath().resolve("ret-folder").toFile(); + assertTrue(retrieveDir.mkdir()); + + log.info("source dir [{}]", sourceDir); + log.info("dest dir [{}]", destDir); + log.info("retrieve base dir [{}]", retrieveBaseDir); + log.info("retrieve dir [{}]", retrieveDir); + + File sourceFileDir = new File(sourceDir, "src-path-1"); + assertTrue(sourceFileDir.mkdir()); + assertTrue(sourceFileDir.exists() && sourceFileDir.isDirectory()); + + File sourceFile = new File(sourceFileDir, "src-file-1"); + Files.copy(this.largeFile.getFile().toPath(), sourceFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + + assertTrue(sourceFile.exists() && sourceFile.isFile()); + assertEquals(50_000_000, sourceFile.length()); + } + + @SneakyThrows + void setupKeystore() { + Path baseTemp = Files.createTempDirectory("tmpKeyStore"); + Encryption.addBouncyCastleSecurityProvider(); + keyStorePath = baseTemp.resolve("test.ks").toFile().getCanonicalPath(); + log.info("BASE TEMP IS AT [{}]", baseTemp.toFile().getCanonicalPath()); + log.info("TEMP KEY IS AT [{}]", keyStorePath); + Encryption enc = new Encryption(); + enc.setVaultEnable(false); + enc.setVaultPrivateKeyEncryptionKeyName(KEY_NAME_FOR_SSH); + enc.setVaultDataEncryptionKeyName(KEY_NAME_FOR_DATA); + + enc.setKeystoreEnable(true); + enc.setKeystorePath(keyStorePath); + enc.setKeystorePassword(KEY_STORE_PASSWORD); + + SecretKey keyForSSH = Encryption.generateSecretKey(); + SecretKey keyForData = Encryption.generateSecretKey(); + + assertFalse(new File(keyStorePath).exists()); + + Encryption.saveSecretKeyToKeyStore(Encryption.getVaultPrivateKeyEncryptionKeyName(), + keyForSSH); + Encryption.saveSecretKeyToKeyStore(Encryption.getVaultDataEncryptionKeyName(), + keyForData); + assertTrue(new File(keyStorePath).exists()); + } + + void purgeQueues() { + rabbitAdmin.purgeQueue(workerQueue.getActualName(), false); + rabbitAdmin.purgeQueue(workerQueue.getActualName(), false); + assertEquals(0, rabbitAdmin.getQueueInfo(workerQueue.getActualName()).getMessageCount()); + assertEquals(0, rabbitAdmin.getQueueInfo(workerQueue.getActualName()).getMessageCount()); + } + + @Test + @SneakyThrows + void testDepositThenRetrieve() { + assertEquals(0, destDir.listFiles().length); + String depositMessage = getSampleDepositMessage(); + Deposit deposit = new ObjectMapper().readValue(depositMessage, Deposit.class); + //log.info("depositMessage {}", depositMessage); + sendNormalMessage(depositMessage); + waitUntil(this::foundComplete); + + DepositEvents depositEvents = new DepositEvents(deposit, this.events); + + checkDepositWorkedOkay(depositMessage, depositEvents); + + buildAndSendRetrieveMessage(depositEvents); + checkRetrieve(); + + } + + void waitUntil(Callable test) { + Awaitility.await().atMost(5, TimeUnit.MINUTES) + .pollInterval(Duration.ofSeconds(15)) + .until(test); + } + + @SneakyThrows + private void checkRetrieve() { + + waitUntil(this::foundRetrieveComplete); + + Container.ExecResult execResult = userDataSourceContainer.execInContainer("/tmp/findSrcFile1.sh"); + assertEquals(0, execResult.getExitCode(), "cannot find 50MB file in container - exit code"); + + String filesInContainerStdOut = execResult.getStdout(); + assertTrue(filesInContainerStdOut.contains("50000000"), "cannot find 50MB file in container - file size"); + + // we tar up the retrieved files on the container and copy them back to the local file system. + // we tried using a shared directory via a bind mount but had problems with it in CI/CD pipeline which uses DockerInDocker + Container.ExecResult tarResult = userDataSourceContainer.execInContainer("tar","cvf","retFolder.tar","-C","/tmp/retrieve", "ret-folder"); + assertEquals(0, tarResult.getExitCode()); + + File destination = new File(retrieveDir.toString()); + assertTrue(destination.isDirectory()); + assertTrue(destination.exists()); + + File localTarFile = new File(retrieveBaseDir.toString(), "retFolder.tar"); + userDataSourceContainer.copyFileFromContainer("/retFolder.tar", localTarFile.toString()); + unTar(localTarFile); + + log.info("FIN {}", retrieveDir.getCanonicalPath()); + File[] dvTimestampDirs = retrieveDir.listFiles(file -> file.isDirectory() && file.getName().startsWith("dv_")); + File dvLatestTimestampDir = Arrays.stream(dvTimestampDirs).sorted(Comparator.comparing((File::lastModified)).reversed()).findFirst().get(); + File retrieved = new File(dvLatestTimestampDir + "/src-path-1/src-file-1"); + + String digestOriginal = Verify.getDigest(this.largeFile.getFile()); + String digestRetrieved = Verify.getDigest(retrieved); + + assertEquals(digestOriginal, digestRetrieved); + } + + private void unTar(File tarFile) { + unTar(tarFile.toPath(), tarFile.toPath().getParent()); + } + + @SneakyThrows + public static void unTar( Path pathInput, Path pathOutput ) { + TarArchiveInputStream tararchiveinputstream = + new TarArchiveInputStream(new BufferedInputStream(Files.newInputStream(pathInput))); + + ArchiveEntry archiveEntry; + while( (archiveEntry = tararchiveinputstream.getNextEntry()) != null ) { + Path pathEntryOutput = pathOutput.resolve( archiveEntry.getName() ); + if( archiveEntry.isDirectory() ) { + if( !Files.exists( pathEntryOutput ) ) + Files.createDirectory( pathEntryOutput ); + } + else + Files.copy( tararchiveinputstream, pathEntryOutput ); + } + + tararchiveinputstream.close(); + } + + boolean foundRetrieveComplete() { + return events.stream() + .anyMatch(e -> e.getClass().equals(RetrieveComplete.class)); + } + + boolean foundComplete() { + return events.stream() + .anyMatch(e -> e.getClass().equals(Complete.class)); + } + + Optional getExpectedNumberChunks(){ + return Optional.of(3); + } + + @SneakyThrows + private void checkDepositWorkedOkay(String depositMessage, DepositEvents depositEvents) { + Deposit deposit = mapper.readValue(depositMessage, Deposit.class); + String bagId = deposit.getProperties().get("bagId"); + + log.info("BROKER MSG COUNT {}", events.size()); + File[] destFiles = destDir.listFiles(); + + int expectedNumFiles = getExpectedNumberChunks().isPresent() ? getExpectedNumberChunks().get() : 1; + assertEquals(expectedNumFiles, destFiles.length); + + Arrays.sort(destFiles, Comparator.comparing(File::getName)); + + final Map chunkNumToEncChunk = new HashMap<>(); + + final ComputedEncryption computedEncryption = depositEvents.getComputedEncryption(); + AESMode aesMode = AESMode.valueOf(computedEncryption.getAesMode()); + assertEquals(AESMode.GCM, aesMode); + final ComputedDigest computedDigest = depositEvents.getComputedDigest(); + + final File decryptedTarFile; + + if (getExpectedNumberChunks().isPresent()) { + + int expectedNumberChunks = getExpectedNumberChunks().get(); + + for (int chunkNum = 1; chunkNum <= expectedNumberChunks; chunkNum++) { + File expectedEncChunk = destDir.toPath().resolve(bagId + ".tar." + chunkNum).toFile(); + assertEquals(expectedEncChunk, destFiles[chunkNum - 1]); + chunkNumToEncChunk.put(chunkNum, expectedEncChunk); + } + + for (int chunkNum = 1; chunkNum <= expectedNumberChunks; chunkNum++) { + File expectedEncChunk = chunkNumToEncChunk.get(chunkNum); + String encHash = computedEncryption.getEncChunkDigests().get(chunkNum); + assertEquals(encHash, getSha1Hash(expectedEncChunk)); + } + + Map chunkNumToDecryptedChunk = new HashMap<>(); + for (int chunkNum = 1; chunkNum <= expectedNumberChunks; chunkNum++) { + File expectedEncChunk = chunkNumToEncChunk.get(chunkNum); + byte[] iv = computedEncryption.getChunkIVs().get(chunkNum); + File decryptedChunkFile = Files.createTempFile("decryptedChunk", ".plain").toFile(); + chunkNumToDecryptedChunk.put(chunkNum, decryptedChunkFile); + FileUtils.copyFile(expectedEncChunk, decryptedChunkFile); + Encryption.decryptFile(aesMode, decryptedChunkFile, iv); + assertTrue(decryptedChunkFile.length() > 0); + assertTrue(decryptedChunkFile.length() != expectedEncChunk.length()); + } + + decryptedTarFile = Files.createTempFile("decryptedTar", ".plain").toFile(); + + try (FileOutputStream fos = new FileOutputStream(decryptedTarFile)) { + for (int chunkNum = 1; chunkNum <= expectedNumberChunks; chunkNum++) { + File decryptedChunkFile = chunkNumToDecryptedChunk.get(chunkNum); + Files.copy(decryptedChunkFile.toPath(), fos); + } + } + + } else { + File expectedEncTar = destDir.toPath().resolve(bagId + ".tar").toFile(); + assertEquals(expectedEncTar, destFiles[0]); + + String encTarHash = computedEncryption.getEncTarDigest(); + assertEquals(encTarHash, getSha1Hash(expectedEncTar)); + + decryptedTarFile = Files.createTempFile("decryptedTar", ".plain").toFile(); + + byte[] iv = computedEncryption.getTarIV(); + FileUtils.copyFile(destFiles[0], decryptedTarFile); + Encryption.decryptFile(aesMode, decryptedTarFile, iv); + assertTrue(decryptedTarFile.length() > 0); + assertTrue(decryptedTarFile.length() != expectedEncTar.length()); + } + + Set tarEntryPaths = getPathsWithinTarFile(decryptedTarFile); + + Path base = Paths.get(bagId); + assertThat(tarEntryPaths).containsExactlyInAnyOrder( + base.resolve("bagit.txt"), + base.resolve("manifest-md5.txt"), + base.resolve("tagmanifest-md5.txt"), + + base.resolve("data/src-path-1/src-file-1"), + + base.resolve("metadata/filetype.json"), + base.resolve("metadata/vault.json"), + base.resolve("metadata/external.txt"), + base.resolve("metadata/deposit.json")); + + assertEquals(computedDigest.getDigest(), getSha1Hash(decryptedTarFile)); + } + + @SneakyThrows + private void buildAndSendRetrieveMessage(DepositEvents depositEvents) { + String retrieveMessage2 = depositEvents.generateRetrieveMessage(this.retrieveBaseDir, this.retrieveDir.getName()); + + ObjectNode childNode = mapper.convertValue(this.sftpTargetProps, ObjectNode.class); + JsonNode parentNode = mapper.readTree(retrieveMessage2); + + ObjectNode locatedNode1 = (ObjectNode)parentNode.path("userFileStoreProperties"); + locatedNode1.put("FILE-STORE-SRC-ID", childNode); + + ObjectNode locatedNode2 = (ObjectNode)parentNode.path("userFileStoreClasses"); + locatedNode2.put("FILE-STORE-SRC-ID", new TextNode(SFTPFileSystem.class.getName())); + + String sftpRetrieveMessage = mapper.writer(SerializationFeature.INDENT_OUTPUT).writeValueAsString(parentNode); + + sendNormalMessage(sftpRetrieveMessage); + } + + @SneakyThrows + private String getSampleDepositMessage() { + String temp1 = FileUtils.readFileToString(this.depositMessage.getFile(), + StandardCharsets.UTF_8); + String temp2 = temp1.replaceAll("/tmp/dv/src", sourceDir.getCanonicalPath()); + String localFSresult = temp2.replaceAll("/tmp/dv/dest", destDir.getCanonicalPath()); + + ObjectNode childNode = mapper.convertValue(this.sftpSrcProps, ObjectNode.class); + JsonNode parentNode = mapper.readTree(localFSresult); + + ObjectNode locatedNode1 = (ObjectNode)parentNode.path("userFileStoreProperties"); + locatedNode1.put("FILE-STORE-SRC-ID", childNode); + + ObjectNode locatedNode2 = (ObjectNode)parentNode.path("userFileStoreClasses"); + locatedNode2.put("FILE-STORE-SRC-ID", new TextNode(SFTPFileSystem.class.getName())); + + String sftpFSresult = mapper.writer(SerializationFeature.INDENT_OUTPUT).writeValueAsString(parentNode); + + return sftpFSresult; + } + + @RabbitListener(queues = BaseQueueConfig.BROKER_QUEUE_NAME) + @SneakyThrows + void receiveBrokerMessage(Message message, Channel channel, + @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) { + channel.basicAck(deliveryTag, false); + + String msgBody = new String(message.getBody(), StandardCharsets.UTF_8); + events.add(extractEvent(msgBody)); + + log.info("Received message for broker [{}]", events.size()); + } + + @SneakyThrows + private Event extractEvent(String message) { + Event event = mapper.readValue(message, Event.class); + String eventClassName = event.getEventClass(); + Class eventClass = (Class) Class.forName(eventClassName); + return mapper.readValue(message, eventClass); + } + + @SneakyThrows + private String getSha1Hash(File file) { + return Verify.getDigest(file); + } + + public DockerImageName getDockerImageForOpenSSH() { + return DockerImageName.parse(DockerImage.OPEN_SSH_8pt6_IMAGE_NAME); + } + + + @SneakyThrows + public void setupSFTP() { + + UserKeyPairService keyPairService = new UserKeyPairServiceImpl(TEST_PASSPHRASE); + + UserKeyPairService.KeyPairInfo keypairInfo = keyPairService.generateNewKeyPair(); + this.sftpPrivateKey = keypairInfo.getPrivateKey(); + this.sftpPublicKey = keypairInfo.getPublicKey(); + + userDataSourceContainer = new GenericContainer<>(getDockerImageForOpenSSH()) + .withEnv(ENV_USER_NAME, TEST_USER) + .withEnv(ENV_PUBLIC_KEY, sftpPublicKey) //this causes the public key to be added to /config/.ssh/authorized_keys + .withExposedPorts(2222) + //.withFileSystemBind(this.sourceDir.getCanonicalPath(), "/tmp/source") + //.withFileSystemBind(this.retrieveDir.getCanonicalPath(), "/tmp/retrieve/ret-folder") + .withCopyFileToContainer(MountableFile.forHostPath(sourceDir.toPath()),"/tmp/source") + .withCopyFileToContainer(MountableFile.forClasspathResource("docker/findSrcFile1.sh"),"/tmp/findSrcFile1.sh") + .waitingFor(Wait.forListeningPort()); + + userDataSourceContainer.start(); + + Container.ExecResult mkdirResult = userDataSourceContainer.execInContainer("mkdir", "-p", "/tmp/retrieve/ret-folder"); + log.info("mkdir exit code [{}]", mkdirResult.getExitCode()); + + Container.ExecResult execresult1 = userDataSourceContainer.execInContainer("chown", "-R", "testuser", "/tmp/retrieve"); + log.info("chown exit code [{}]", execresult1.getExitCode()); + + Container.ExecResult execresult2 = userDataSourceContainer.execInContainer("chmod", "a+r", "/tmp/retrieve"); + log.info("chmod exit code [{}]", execresult2.getExitCode()); + + Container.ExecResult execresult3 = userDataSourceContainer.execInContainer("chmod", "+x", "/tmp/findSrcFile1.sh"); + log.info("chmod exit code [{}]", execresult3.getExitCode()); + + byte[] iv = Encryption.generateIV(); + + byte[] encSftpPrivateKey = Encryption.encryptSecret(sftpPrivateKey, iv); + + sftpSrcProps = new HashMap<>(); + sftpSrcProps.put(PropNames.PRIVATE_KEY, Base64.getEncoder().encodeToString(encSftpPrivateKey)); + sftpSrcProps.put(PropNames.IV, Base64.getEncoder().encodeToString(iv)); + sftpSrcProps.put(PropNames.PASSPHRASE, TEST_PASSPHRASE); + sftpSrcProps.put(PropNames.USERNAME, TEST_USER); + sftpSrcProps.put(PropNames.HOST, this.userDataSourceContainer.getHost()); + sftpSrcProps.put(PropNames.PORT, ""+this.userDataSourceContainer.getMappedPort(2222)); + sftpSrcProps.put(PropNames.ROOT_PATH, "/tmp/source"); + + sftpTargetProps = new HashMap<>(sftpSrcProps); + sftpTargetProps.put(PropNames.ROOT_PATH, "/tmp/retrieve"); + + assertNotEquals(sftpTargetProps.get(PropNames.ROOT_PATH), sftpSrcProps.get(PropNames.ROOT_PATH)); + } +} \ No newline at end of file diff --git a/datavault-worker/src/test/java/org/datavaultplatform/worker/tasks/sftp/PerformDepositThenRetrieveUsingSftpWithApacheSshdIT.java b/datavault-worker/src/test/java/org/datavaultplatform/worker/tasks/sftp/PerformDepositThenRetrieveUsingSftpWithApacheSshdIT.java new file mode 100644 index 000000000..b08ca4a66 --- /dev/null +++ b/datavault-worker/src/test/java/org/datavaultplatform/worker/tasks/sftp/PerformDepositThenRetrieveUsingSftpWithApacheSshdIT.java @@ -0,0 +1,18 @@ +package org.datavaultplatform.worker.tasks.sftp; + +import lombok.extern.slf4j.Slf4j; +import org.datavaultplatform.worker.app.DataVaultWorkerInstanceApp; +import org.datavaultplatform.worker.test.AddTestProperties; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.TestPropertySource; +import org.testcontainers.junit.jupiter.Testcontainers; + +@SpringBootTest(classes = DataVaultWorkerInstanceApp.class) +@Testcontainers(disabledWithoutDocker = true) +@AddTestProperties +@DirtiesContext +@TestPropertySource(properties = {"sftp.driver.use.apache.sshd=false"}) +@Slf4j +public class PerformDepositThenRetrieveUsingSftpWithApacheSshdIT extends BasePerformDepositThenRetrieveUsingSftpIT { +} \ No newline at end of file diff --git a/datavault-worker/src/test/java/org/datavaultplatform/worker/tasks/sftp/PerformDepositThenRetrieveUsingSftpWithJschIT.java b/datavault-worker/src/test/java/org/datavaultplatform/worker/tasks/sftp/PerformDepositThenRetrieveUsingSftpWithJschIT.java new file mode 100644 index 000000000..d9c07988b --- /dev/null +++ b/datavault-worker/src/test/java/org/datavaultplatform/worker/tasks/sftp/PerformDepositThenRetrieveUsingSftpWithJschIT.java @@ -0,0 +1,18 @@ +package org.datavaultplatform.worker.tasks.sftp; + +import lombok.extern.slf4j.Slf4j; +import org.datavaultplatform.worker.app.DataVaultWorkerInstanceApp; +import org.datavaultplatform.worker.test.AddTestProperties; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.TestPropertySource; +import org.testcontainers.junit.jupiter.Testcontainers; + +@SpringBootTest(classes = DataVaultWorkerInstanceApp.class) +@Testcontainers(disabledWithoutDocker = true) +@AddTestProperties +@DirtiesContext +@TestPropertySource(properties = {"sftp.driver.use.apache.sshd=true"}) +@Slf4j +public class PerformDepositThenRetrieveUsingSftpWithJschIT extends BasePerformDepositThenRetrieveUsingSftpIT { +} \ No newline at end of file diff --git a/datavault-worker/src/test/resources/docker/findSrcFile1.sh b/datavault-worker/src/test/resources/docker/findSrcFile1.sh new file mode 100644 index 000000000..29a9eac53 --- /dev/null +++ b/datavault-worker/src/test/resources/docker/findSrcFile1.sh @@ -0,0 +1,3 @@ +#!/bin/sh +# see BasePerformDepositThenRetrieveUsingSftpIT.java +find /tmp/retrieve -type f -name src-file-1 -exec ls -l {} \; \ No newline at end of file diff --git a/pom.xml b/pom.xml index 3ff80c88c..1a3cf8816 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ org.springframework.boot spring-boot-starter-parent - 2.7.6 + 2.7.9 @@ -35,23 +35,37 @@ false + + + 3.1.2 + - 1.12.334 + 1.12.426 - 3.0.1 - - - 1.17.6 + 3.7.0 - 3.1.5 + 3.1.6 - 4.9.0 + 4.11.0 - 7.3.1 + 8.1.2 + + + 1.15.4 + + + + + 7.5 + + + + + 1.17.6 1.22 @@ -73,9 +87,6 @@ 1.10.0 - - 1.15.3 - 5.1.0 @@ -94,9 +105,6 @@ 5.2.0 - - 6.14.3 - 2.3 @@ -191,13 +199,6 @@ pom import - - com.oracle.oci.sdk - oci-java-sdk-bom - ${oci.java.sdk.bom.version} - pom - import - com.oracle.oci.sdk oci-java-sdk-common-httpclient-jersey