diff --git a/.ci/dev/regression/Jenkinsfile b/.ci/dev/regression/Jenkinsfile index aa22fce3f3..82d0f66399 100644 --- a/.ci/dev/regression/Jenkinsfile +++ b/.ci/dev/regression/Jenkinsfile @@ -319,7 +319,7 @@ pipeline { './gradlew', COMMON_GRADLE_PARAMS, 'docker:pushDockerImage', - '-Pdocker.image.repository=corda/corda', + '-Pdocker.image.repository=corda/community', '--image OFFICIAL' ].join(' ') } diff --git a/build.gradle b/build.gradle index 3964ac44ad..a70e192e53 100644 --- a/build.gradle +++ b/build.gradle @@ -63,7 +63,8 @@ buildscript { ext.asm_version = '7.1' ext.artemis_version = '2.19.1' // TODO Upgrade Jackson only when corda is using kotlin 1.3.10 - ext.jackson_version = '2.9.7' + ext.jackson_version = '2.11.1' + ext.jackson_kotlin_version = '2.9.7' ext.jetty_version = '9.4.19.v20190610' ext.jersey_version = '2.25' ext.servlet_version = '4.0.1' @@ -80,7 +81,7 @@ buildscript { ext.deterministic_rt_version = constants.getProperty('deterministicRtVersion') ext.okhttp_version = '3.14.2' ext.netty_version = '4.1.68.Final' - ext.tcnative_version = '2.0.42.Final' + ext.tcnative_version = constants.getProperty("tcnativeVersion") ext.typesafe_config_version = constants.getProperty("typesafeConfigVersion") ext.fileupload_version = '1.4' ext.kryo_version = '4.0.2' @@ -101,7 +102,7 @@ buildscript { ext.hibernate_version = '5.4.3.Final' ext.h2_version = '1.4.199' // Update docs if renamed or removed. ext.rxjava_version = '1.3.8' - ext.dokka_version = '0.9.17' + ext.dokka_version = '0.10.1' ext.eddsa_version = '0.3.0' ext.dependency_checker_version = '5.2.0' ext.commons_collections_version = '4.3' @@ -405,7 +406,6 @@ allprojects { includeGroup 'org.crashub' includeGroup 'com.github.bft-smart' includeGroup 'com.github.detro' - includeGroup 'org.apache.activemq' } } maven { diff --git a/client/jackson/build.gradle b/client/jackson/build.gradle index e586479b80..b86798a8b3 100644 --- a/client/jackson/build.gradle +++ b/client/jackson/build.gradle @@ -9,7 +9,9 @@ dependencies { compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" // Jackson and its plugins: parsing to/from JSON and other textual formats. - compile "com.fasterxml.jackson.module:jackson-module-kotlin:$jackson_version" + compile("com.fasterxml.jackson.module:jackson-module-kotlin:$jackson_kotlin_version") { + exclude module: "jackson-databind" + } // Yaml is useful for parsing strings to method calls. compile "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:$jackson_version" // This adds support for java.time types. diff --git a/constants.properties b/constants.properties index 62ec448848..44f83464ee 100644 --- a/constants.properties +++ b/constants.properties @@ -36,3 +36,4 @@ openSourceBranch=https://github.com/corda/corda/blob/release/os/4.4 openSourceSamplesBranch=https://github.com/corda/samples/blob/release-V4 jolokiaAgentVersion=1.6.1 detektVersion=1.0.1 +tcnativeVersion=2.0.48.Final diff --git a/core/src/main/kotlin/net/corda/core/internal/PlatformVersionSwitches.kt b/core/src/main/kotlin/net/corda/core/internal/PlatformVersionSwitches.kt index 4370f998b9..c6d93f272f 100644 --- a/core/src/main/kotlin/net/corda/core/internal/PlatformVersionSwitches.kt +++ b/core/src/main/kotlin/net/corda/core/internal/PlatformVersionSwitches.kt @@ -16,5 +16,6 @@ object PlatformVersionSwitches { const val LIMIT_KEYS_IN_SIGNATURE_CONSTRAINTS = 5 const val BATCH_DOWNLOAD_COUNTERPARTY_BACKCHAIN = 6 const val ENABLE_P2P_COMPRESSION = 7 + const val RESTRICTED_DATABASE_OPERATIONS = 7 const val CERTIFICATE_ROTATION = 9 } \ No newline at end of file diff --git a/docker/src/docker/Dockerfile b/docker/src/docker/Dockerfile index 41be49ad20..84200d542b 100644 --- a/docker/src/docker/Dockerfile +++ b/docker/src/docker/Dockerfile @@ -2,6 +2,7 @@ FROM azul/zulu-openjdk:8u312 ## Add packages, clean cache, create dirs, create corda user and change ownership RUN apt-get update && \ + apt-mark hold zulu8-jdk && \ apt-get -y upgrade && \ apt-get -y install bash curl unzip && \ rm -rf /var/lib/apt/lists/* && \ diff --git a/docker/src/docker/DockerfileAL b/docker/src/docker/DockerfileAL index f3c8496604..8c5ccd46ff 100644 --- a/docker/src/docker/DockerfileAL +++ b/docker/src/docker/DockerfileAL @@ -1,11 +1,10 @@ -FROM amazonlinux:2 +FROM amazoncorretto:8u312-al2 ## Add packages, clean cache, create dirs, create corda user and change ownership -RUN amazon-linux-extras enable corretto8 && \ - yum -y install java-1.8.0-amazon-corretto-devel && \ - yum -y install bash && \ +RUN yum -y install bash && \ yum -y install curl && \ yum -y install unzip && \ + yum -y install shadow-utils.x86_64 && \ yum clean all && \ rm -rf /var/cache/yum && \ mkdir -p /opt/corda/cordapps && \ diff --git a/docs/build.gradle b/docs/build.gradle index aa50560300..94fd4e6043 100644 --- a/docs/build.gradle +++ b/docs/build.gradle @@ -5,6 +5,10 @@ apply plugin: 'net.corda.plugins.publish-utils' apply plugin: 'maven-publish' apply plugin: 'com.jfrog.artifactory' +dependencies { + compile rootProject +} + def internalPackagePrefixes(sourceDirs) { def prefixes = [] // Kotlin allows packages to deviate from the directory structure, but let's assume they don't: @@ -36,10 +40,13 @@ task dokkaJavadoc(type: org.jetbrains.dokka.gradle.DokkaTask) { } [dokka, dokkaJavadoc].collect { - it.configure { + it.configuration { moduleName = 'corda' - processConfigurations = ['compile'] - sourceDirs = dokkaSourceDirs + dokkaSourceDirs.collect { sourceDir -> + sourceRoot { + path = sourceDir.path + } + } includes = ['packages.md'] jdkVersion = 8 externalDocumentationLink { @@ -52,7 +59,7 @@ task dokkaJavadoc(type: org.jetbrains.dokka.gradle.DokkaTask) { url = new URL("https://www.bouncycastle.org/docs/docs1.5on/") } internalPackagePrefixes.collect { packagePrefix -> - packageOptions { + perPackageOption { prefix = packagePrefix suppress = true } diff --git a/node-api-tests/src/test/kotlin/net/corda/nodeapitests/internal/persistence/RestrictedConnectionFlowTest.kt b/node-api-tests/src/test/kotlin/net/corda/nodeapitests/internal/persistence/RestrictedConnectionFlowTest.kt index f948c75f37..e00e65ec44 100644 --- a/node-api-tests/src/test/kotlin/net/corda/nodeapitests/internal/persistence/RestrictedConnectionFlowTest.kt +++ b/node-api-tests/src/test/kotlin/net/corda/nodeapitests/internal/persistence/RestrictedConnectionFlowTest.kt @@ -4,14 +4,15 @@ import co.paralleluniverse.fibers.Suspendable import net.corda.core.flows.FlowLogic import net.corda.core.flows.InitiatingFlow import net.corda.core.identity.CordaX500Name +import net.corda.core.internal.PLATFORM_VERSION import net.corda.core.utilities.getOrThrow import net.corda.nodeapi.internal.persistence.RestrictedConnection import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNetworkParameters import net.corda.testing.node.StartedMockNode +import net.corda.testing.node.internal.enclosedCordapp import org.assertj.core.api.Assertions import org.junit.After -import org.junit.Before import org.junit.Test import kotlin.test.assertTrue @@ -38,40 +39,63 @@ class RestrictedConnectionFlowTest { } @InitiatingFlow - class TestCloseMethodIsBlocked : FlowLogic() { + class TestClearWarningsMethodIsBlocked : FlowLogic() { @Suspendable override fun call() { val connection = serviceHub.jdbcSession() - connection.close() + connection.clearWarnings() } } - @Before - fun init() { - mockNetwork = MockNetwork(MockNetworkParameters()) - aliceNode = mockNetwork.createPartyNode(CordaX500Name("Alice", "London", "GB")) - } - @After fun done() { mockNetwork.stopNodes() } @Test(timeout=300_000) - fun testIfItIsRestrictedConnection() { + fun `restricted connection is returned from ServiceHub#jdbcSession`() { + mockNetwork = MockNetwork(MockNetworkParameters(listOf(enclosedCordapp().copy(targetPlatformVersion = PLATFORM_VERSION)))) + aliceNode = mockNetwork.createPartyNode(CordaX500Name("Alice", "London", "GB")) assertTrue { aliceNode.startFlow(TestIfItIsRestrictedConnection()).get() } mockNetwork.runNetwork() } @Test(timeout=300_000) - fun testMethodsAreBlocked() { + fun `restricted methods are blocked when the target platform is the current corda version`() { + mockNetwork = MockNetwork(MockNetworkParameters(listOf(enclosedCordapp().copy(targetPlatformVersion = PLATFORM_VERSION)))) + aliceNode = mockNetwork.createPartyNode(CordaX500Name("Alice", "London", "GB")) Assertions.assertThatExceptionOfType(UnsupportedOperationException::class.java) .isThrownBy { aliceNode.startFlow(TestAutoCommitMethodIsBlocked()).getOrThrow() } - .withMessageContaining("This method cannot be called via ServiceHub.jdbcSession.") + .withMessageContaining("ServiceHub.jdbcSession.setAutoCommit is restricted and cannot be called") Assertions.assertThatExceptionOfType(UnsupportedOperationException::class.java) - .isThrownBy { aliceNode.startFlow(TestCloseMethodIsBlocked()).getOrThrow() } - .withMessageContaining("This method cannot be called via ServiceHub.jdbcSession.") + .isThrownBy { aliceNode.startFlow(TestClearWarningsMethodIsBlocked()).getOrThrow() } + .withMessageContaining("ServiceHub.jdbcSession.clearWarnings is restricted and cannot be called") + + mockNetwork.runNetwork() + } + + @Test(timeout=300_000) + fun `restricted methods are blocked when the target platform is 7`() { + mockNetwork = MockNetwork(MockNetworkParameters(listOf(enclosedCordapp().copy(targetPlatformVersion = 7)))) + aliceNode = mockNetwork.createPartyNode(CordaX500Name("Alice", "London", "GB")) + Assertions.assertThatExceptionOfType(UnsupportedOperationException::class.java) + .isThrownBy { aliceNode.startFlow(TestAutoCommitMethodIsBlocked()).getOrThrow() } + .withMessageContaining("ServiceHub.jdbcSession.setAutoCommit is restricted and cannot be called") + + Assertions.assertThatExceptionOfType(UnsupportedOperationException::class.java) + .isThrownBy { aliceNode.startFlow(TestClearWarningsMethodIsBlocked()).getOrThrow() } + .withMessageContaining("ServiceHub.jdbcSession.clearWarnings is restricted and cannot be called") + + mockNetwork.runNetwork() + } + + @Test(timeout=300_000) + fun `restricted methods are not blocked when the target platform is 6`() { + mockNetwork = MockNetwork(MockNetworkParameters(listOf(enclosedCordapp().copy(targetPlatformVersion = 6)))) + aliceNode = mockNetwork.createPartyNode(CordaX500Name("Alice", "London", "GB")) + aliceNode.startFlow(TestAutoCommitMethodIsBlocked()).getOrThrow() + aliceNode.startFlow(TestClearWarningsMethodIsBlocked()).getOrThrow() mockNetwork.runNetwork() } diff --git a/node-api-tests/src/test/kotlin/net/corda/nodeapitests/internal/persistence/RestrictedEntityManagerFlowTest.kt b/node-api-tests/src/test/kotlin/net/corda/nodeapitests/internal/persistence/RestrictedEntityManagerFlowTest.kt index 7da2ff26ff..995440973d 100644 --- a/node-api-tests/src/test/kotlin/net/corda/nodeapitests/internal/persistence/RestrictedEntityManagerFlowTest.kt +++ b/node-api-tests/src/test/kotlin/net/corda/nodeapitests/internal/persistence/RestrictedEntityManagerFlowTest.kt @@ -4,14 +4,15 @@ import co.paralleluniverse.fibers.Suspendable import net.corda.core.flows.FlowLogic import net.corda.core.flows.InitiatingFlow import net.corda.core.identity.CordaX500Name +import net.corda.core.internal.PLATFORM_VERSION import net.corda.core.utilities.getOrThrow import net.corda.nodeapi.internal.persistence.RestrictedEntityManager import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNetworkParameters import net.corda.testing.node.StartedMockNode +import net.corda.testing.node.internal.enclosedCordapp import org.assertj.core.api.Assertions import org.junit.After -import org.junit.Before import org.junit.Test import kotlin.test.assertTrue @@ -25,7 +26,7 @@ class RestrictedEntityManagerFlowTest { @Suspendable override fun call() : Boolean { var result = false - serviceHub.withEntityManager() { + serviceHub.withEntityManager { result = this is RestrictedEntityManager } return result @@ -33,11 +34,11 @@ class RestrictedEntityManagerFlowTest { } @InitiatingFlow - class TestCloseMethodIsBlocked : FlowLogic() { + class TestGetMetamodelMethodIsBlocked : FlowLogic() { @Suspendable override fun call() { - serviceHub.withEntityManager() { - this.close() + serviceHub.withEntityManager { + this.metamodel } } } @@ -46,38 +47,61 @@ class RestrictedEntityManagerFlowTest { class TestJoinTransactionMethodIsBlocked : FlowLogic() { @Suspendable override fun call() { - serviceHub.withEntityManager() { + serviceHub.withEntityManager { this.joinTransaction() } } } - @Before - fun init() { - mockNetwork = MockNetwork(MockNetworkParameters()) - aliceNode = mockNetwork.createPartyNode(CordaX500Name("Alice", "London", "GB")) - } - @After fun done() { mockNetwork.stopNodes() } @Test(timeout=300_000) - fun testIfItIsRestrictedConnection() { + fun `restricted connection is returned from ServiceHub#withEntityManager`() { + mockNetwork = MockNetwork(MockNetworkParameters(listOf(enclosedCordapp().copy(targetPlatformVersion = PLATFORM_VERSION)))) + aliceNode = mockNetwork.createPartyNode(CordaX500Name("Alice", "London", "GB")) assertTrue { aliceNode.startFlow(TestIfItIsRestrictedEntityManager()).get() } mockNetwork.runNetwork() } @Test(timeout=300_000) - fun testMethodsAreBlocked() { + fun `restricted methods are blocked when the target platform is the current corda version`() { + mockNetwork = MockNetwork(MockNetworkParameters(listOf(enclosedCordapp().copy(targetPlatformVersion = PLATFORM_VERSION)))) + aliceNode = mockNetwork.createPartyNode(CordaX500Name("Alice", "London", "GB")) Assertions.assertThatExceptionOfType(UnsupportedOperationException::class.java) - .isThrownBy { aliceNode.startFlow(TestCloseMethodIsBlocked()).getOrThrow() } - .withMessageContaining("This method cannot be called via ServiceHub.withEntityManager.") + .isThrownBy { aliceNode.startFlow(TestGetMetamodelMethodIsBlocked()).getOrThrow() } + .withMessageContaining("ServiceHub.withEntityManager.getMetamodel is restricted and cannot be called") Assertions.assertThatExceptionOfType(UnsupportedOperationException::class.java) .isThrownBy { aliceNode.startFlow(TestJoinTransactionMethodIsBlocked()).getOrThrow() } - .withMessageContaining("This method cannot be called via ServiceHub.withEntityManager.") + .withMessageContaining("ServiceHub.withEntityManager.joinTransaction is restricted and cannot be called") + + mockNetwork.runNetwork() + } + + @Test(timeout=300_000) + fun `restricted methods are blocked when the target platform is 7`() { + mockNetwork = MockNetwork(MockNetworkParameters(listOf(enclosedCordapp().copy(targetPlatformVersion = 7)))) + aliceNode = mockNetwork.createPartyNode(CordaX500Name("Alice", "London", "GB")) + Assertions.assertThatExceptionOfType(UnsupportedOperationException::class.java) + .isThrownBy { aliceNode.startFlow(TestGetMetamodelMethodIsBlocked()).getOrThrow() } + .withMessageContaining("ServiceHub.withEntityManager.getMetamodel is restricted and cannot be called") + + Assertions.assertThatExceptionOfType(UnsupportedOperationException::class.java) + .isThrownBy { aliceNode.startFlow(TestJoinTransactionMethodIsBlocked()).getOrThrow() } + .withMessageContaining("ServiceHub.withEntityManager.joinTransaction is restricted and cannot be called") + + mockNetwork.runNetwork() + } + + @Test(timeout=300_000) + fun `restricted methods are not blocked when the target platform is 6`() { + mockNetwork = MockNetwork(MockNetworkParameters(listOf(enclosedCordapp().copy(targetPlatformVersion = 6)))) + aliceNode = mockNetwork.createPartyNode(CordaX500Name("Alice", "London", "GB")) + aliceNode.startFlow(TestGetMetamodelMethodIsBlocked()).getOrThrow() + aliceNode.startFlow(TestJoinTransactionMethodIsBlocked()).getOrThrow() mockNetwork.runNetwork() } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/MessageSizeChecksInterceptor.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/MessageSizeChecksInterceptor.kt index 9f789ac655..18fed0658a 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/MessageSizeChecksInterceptor.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/MessageSizeChecksInterceptor.kt @@ -8,14 +8,13 @@ import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.MessagePac import org.apache.activemq.artemis.protocol.amqp.broker.AMQPMessage import org.apache.activemq.artemis.protocol.amqp.broker.AmqpInterceptor import org.apache.activemq.artemis.spi.core.protocol.RemotingConnection -import org.apache.qpid.proton.amqp.messaging.Data class ArtemisMessageSizeChecksInterceptor(maxMessageSize: Int) : MessageSizeChecksInterceptor(maxMessageSize), Interceptor { - override fun getMessageSize(packet: Packet?): Int? { + override fun getMessageSize(packet: Packet?): Long? { return when (packet) { // This is an estimate of how much memory a Message body takes up. // Note, it is only an estimate - is MessagePacket -> (packet.message.persistentSize - packet.message.headersAndPropertiesEncodeSize - 4).toInt() + is MessagePacket -> (packet.message.persistentSize - packet.message.headersAndPropertiesEncodeSize - 4) // Skip all artemis control messages. else -> null } @@ -23,7 +22,7 @@ class ArtemisMessageSizeChecksInterceptor(maxMessageSize: Int) : MessageSizeChec } class AmqpMessageSizeChecksInterceptor(maxMessageSize: Int) : MessageSizeChecksInterceptor(maxMessageSize), AmqpInterceptor { - override fun getMessageSize(packet: AMQPMessage?): Int? = (packet?.protonMessage?.body as? Data)?.value?.length + override fun getMessageSize(packet: AMQPMessage?): Long? = packet?.wholeMessageSize } /** @@ -46,6 +45,6 @@ sealed class MessageSizeChecksInterceptor(private val maxMessageSize: I } // get size of the message in byte, returns null if the message is null or size don't need to be checked. - abstract fun getMessageSize(packet: T?): Int? + abstract fun getMessageSize(packet: T?): Long? } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/RestrictedConnection.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/RestrictedConnection.kt index 997cdc3ebd..a2a471c364 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/RestrictedConnection.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/RestrictedConnection.kt @@ -1,5 +1,6 @@ package net.corda.nodeapi.internal.persistence +import net.corda.core.node.ServiceHub import java.sql.Connection import java.sql.Savepoint import java.util.concurrent.Executor @@ -8,73 +9,73 @@ import java.util.concurrent.Executor * A delegate of [Connection] which disallows some operations. */ @Suppress("TooManyFunctions") -class RestrictedConnection(private val delegate : Connection) : Connection by delegate { +class RestrictedConnection(private val delegate: Connection, private val serviceHub: ServiceHub) : Connection by delegate { override fun abort(executor: Executor?) { - throw UnsupportedOperationException("This method cannot be called via ServiceHub.jdbcSession.") + restrictDatabaseOperationFromJdbcSession("abort", serviceHub) { delegate.abort(executor) } } override fun clearWarnings() { - throw UnsupportedOperationException("This method cannot be called via ServiceHub.jdbcSession.") + restrictDatabaseOperationFromJdbcSession("clearWarnings", serviceHub) { delegate.clearWarnings() } } override fun close() { - throw UnsupportedOperationException("This method cannot be called via ServiceHub.jdbcSession.") + restrictDatabaseOperationFromJdbcSession("close", serviceHub) { delegate.close() } } override fun commit() { - throw UnsupportedOperationException("This method cannot be called via ServiceHub.jdbcSession.") + restrictDatabaseOperationFromJdbcSession("commit", serviceHub) { delegate.commit() } } override fun setSavepoint(): Savepoint? { - throw UnsupportedOperationException("This method cannot be called via ServiceHub.jdbcSession.") + return restrictDatabaseOperationFromJdbcSession("setSavepoint", serviceHub) { delegate.setSavepoint() } } - override fun setSavepoint(name : String?): Savepoint? { - throw UnsupportedOperationException("This method cannot be called via ServiceHub.jdbcSession.") + override fun setSavepoint(name: String?): Savepoint? { + return restrictDatabaseOperationFromJdbcSession("setSavepoint", serviceHub) { delegate.setSavepoint(name) } } override fun releaseSavepoint(savepoint: Savepoint?) { - throw UnsupportedOperationException("This method cannot be called via ServiceHub.jdbcSession.") + restrictDatabaseOperationFromJdbcSession("releaseSavepoint", serviceHub) { delegate.releaseSavepoint(savepoint) } } override fun rollback() { - throw UnsupportedOperationException("This method cannot be called via ServiceHub.jdbcSession.") + restrictDatabaseOperationFromJdbcSession("rollback", serviceHub) { delegate.rollback() } } override fun rollback(savepoint: Savepoint?) { - throw UnsupportedOperationException("This method cannot be called via ServiceHub.jdbcSession.") + restrictDatabaseOperationFromJdbcSession("rollback", serviceHub) { delegate.rollback(savepoint) } } - override fun setCatalog(catalog : String?) { - throw UnsupportedOperationException("This method cannot be called via ServiceHub.jdbcSession.") + override fun setCatalog(catalog: String?) { + restrictDatabaseOperationFromJdbcSession("setCatalog", serviceHub) { delegate.catalog = catalog } } override fun setTransactionIsolation(level: Int) { - throw UnsupportedOperationException("This method cannot be called via ServiceHub.jdbcSession.") + restrictDatabaseOperationFromJdbcSession("setTransactionIsolation", serviceHub) { delegate.transactionIsolation = level } } override fun setTypeMap(map: MutableMap>?) { - throw UnsupportedOperationException("This method cannot be called via ServiceHub.jdbcSession.") + restrictDatabaseOperationFromJdbcSession("setTypeMap", serviceHub) { delegate.typeMap = map } } override fun setHoldability(holdability: Int) { - throw UnsupportedOperationException("This method cannot be called via ServiceHub.jdbcSession.") + restrictDatabaseOperationFromJdbcSession("setHoldability", serviceHub) { delegate.holdability = holdability } } override fun setSchema(schema: String?) { - throw UnsupportedOperationException("This method cannot be called via ServiceHub.jdbcSession.") + restrictDatabaseOperationFromJdbcSession("setSchema", serviceHub) { delegate.schema = schema } } override fun setNetworkTimeout(executor: Executor?, milliseconds: Int) { - throw UnsupportedOperationException("This method cannot be called via ServiceHub.jdbcSession.") + restrictDatabaseOperationFromJdbcSession("setNetworkTimeout", serviceHub) { delegate.setNetworkTimeout(executor, milliseconds) } } override fun setAutoCommit(autoCommit: Boolean) { - throw UnsupportedOperationException("This method cannot be called via ServiceHub.jdbcSession.") + restrictDatabaseOperationFromJdbcSession("setAutoCommit", serviceHub) { delegate.autoCommit = autoCommit } } override fun setReadOnly(readOnly: Boolean) { - throw UnsupportedOperationException("This method cannot be called via ServiceHub.jdbcSession.") + restrictDatabaseOperationFromJdbcSession("setReadOnly", serviceHub) { delegate.isReadOnly = readOnly } } } \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/RestrictedDatabaseOperations.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/RestrictedDatabaseOperations.kt new file mode 100644 index 0000000000..bc5cbc055f --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/RestrictedDatabaseOperations.kt @@ -0,0 +1,31 @@ +package net.corda.nodeapi.internal.persistence + +import net.corda.core.internal.PlatformVersionSwitches.RESTRICTED_DATABASE_OPERATIONS +import net.corda.core.internal.warnOnce +import net.corda.core.node.ServiceHub +import org.slf4j.LoggerFactory + +private val log = LoggerFactory.getLogger("RestrictedDatabaseOperations") + +internal inline fun restrictDatabaseOperationFromJdbcSession(method: String, serviceHub: ServiceHub, operation: () -> T): T { + return restrictDatabaseOperation("ServiceHub.jdbcSession.$method", serviceHub, operation) +} + +internal inline fun restrictDatabaseOperationFromEntityManager(method: String, serviceHub: ServiceHub, operation: () -> T): T { + return restrictDatabaseOperation("ServiceHub.withEntityManager.$method", serviceHub, operation) +} + +internal inline fun restrictDatabaseOperation(method: String, serviceHub: ServiceHub, operation: () -> T): T { + return if (serviceHub.getAppContext().cordapp.targetPlatformVersion >= RESTRICTED_DATABASE_OPERATIONS) { + throw UnsupportedOperationException("$method is restricted and cannot be called") + } else { + log.warnOnce( + "$method should not be called, as manipulating database transactions and connections breaks the Corda flow state machine in " + + "ways that only become evident in failure scenarios. Purely for API backwards compatibility reasons, the prior " + + "behaviour is continued for target platform versions less than $RESTRICTED_DATABASE_OPERATIONS. You should evolve " + + "the CorDapp away from using these problematic APIs as soon as possible. For target platform version of " + + "$RESTRICTED_DATABASE_OPERATIONS or above, an exception will be thrown instead." + ) + operation() + } +} \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/RestrictedEntityManager.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/RestrictedEntityManager.kt index 1ea4f2c4fd..d6d2672c0d 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/RestrictedEntityManager.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/RestrictedEntityManager.kt @@ -1,5 +1,6 @@ package net.corda.nodeapi.internal.persistence +import net.corda.core.node.ServiceHub import javax.persistence.EntityManager import javax.persistence.EntityTransaction import javax.persistence.LockModeType @@ -8,56 +9,59 @@ import javax.persistence.metamodel.Metamodel /** * A delegate of [EntityManager] which disallows some operations. */ -class RestrictedEntityManager(private val delegate: EntityManager) : EntityManager by delegate { +class RestrictedEntityManager(private val delegate: EntityManager, private val serviceHub: ServiceHub) : EntityManager by delegate { override fun getTransaction(): EntityTransaction { - return RestrictedEntityTransaction(delegate.transaction) + return RestrictedEntityTransaction(delegate.transaction, serviceHub) } override fun close() { - throw UnsupportedOperationException("This method cannot be called via ServiceHub.withEntityManager.") + restrictDatabaseOperationFromEntityManager("close", serviceHub) { delegate.close() } } override fun unwrap(cls: Class?): T { - throw UnsupportedOperationException("This method cannot be called via ServiceHub.withEntityManager.") + return restrictDatabaseOperationFromEntityManager("unwrap", serviceHub) { delegate.unwrap(cls) } } override fun getDelegate(): Any { - throw UnsupportedOperationException("This method cannot be called via ServiceHub.withEntityManager.") + return restrictDatabaseOperationFromEntityManager("getDelegate", serviceHub) { delegate.delegate } } override fun getMetamodel(): Metamodel? { - throw UnsupportedOperationException("This method cannot be called via ServiceHub.withEntityManager.") + return restrictDatabaseOperationFromEntityManager("getMetamodel", serviceHub) { delegate.metamodel } } override fun joinTransaction() { - throw UnsupportedOperationException("This method cannot be called via ServiceHub.withEntityManager.") + restrictDatabaseOperationFromEntityManager("joinTransaction", serviceHub) { delegate.joinTransaction() } } override fun lock(entity: Any?, lockMode: LockModeType?) { - throw UnsupportedOperationException("This method cannot be called via ServiceHub.withEntityManager.") + restrictDatabaseOperationFromEntityManager("lock", serviceHub) { delegate.lock(entity, lockMode) } } override fun lock(entity: Any?, lockMode: LockModeType?, properties: MutableMap?) { - throw UnsupportedOperationException("This method cannot be called via ServiceHub.withEntityManager.") + restrictDatabaseOperationFromEntityManager("lock", serviceHub) { delegate.lock(entity, lockMode, properties) } } override fun setProperty(propertyName: String?, value: Any?) { - throw UnsupportedOperationException("This method cannot be called via ServiceHub.withEntityManager.") + restrictDatabaseOperationFromEntityManager("lock", serviceHub) { delegate.setProperty(propertyName, value) } } } -class RestrictedEntityTransaction(private val delegate: EntityTransaction) : EntityTransaction by delegate { +class RestrictedEntityTransaction( + private val delegate: EntityTransaction, + private val serviceHub: ServiceHub +) : EntityTransaction by delegate { override fun rollback() { - throw UnsupportedOperationException("This method cannot be called via ServiceHub.withEntityManager.") + restrictDatabaseOperationFromEntityManager("EntityTransaction.rollback", serviceHub) { delegate.rollback() } } override fun commit() { - throw UnsupportedOperationException("This method cannot be called via ServiceHub.withEntityManager.") + restrictDatabaseOperationFromEntityManager("EntityTransaction.commit", serviceHub) { delegate.commit() } } override fun begin() { - throw UnsupportedOperationException("This method cannot be called via ServiceHub.withEntityManager.") + restrictDatabaseOperationFromEntityManager("EntityTransaction.begin", serviceHub) { delegate.begin() } } } \ No newline at end of file diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/persistence/RestrictedConnectionTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/persistence/RestrictedConnectionTest.kt index 3708360bfc..39f2d7af73 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/persistence/RestrictedConnectionTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/persistence/RestrictedConnectionTest.kt @@ -1,104 +1,337 @@ package net.corda.nodeapi.internal.persistence import com.nhaarman.mockito_kotlin.mock +import com.nhaarman.mockito_kotlin.whenever +import net.corda.core.cordapp.Cordapp +import net.corda.core.cordapp.CordappContext +import net.corda.core.internal.PLATFORM_VERSION +import net.corda.core.node.ServiceHub import org.junit.Test import java.sql.Connection import java.sql.Savepoint class RestrictedConnectionTest { - private val connection : Connection = mock() - private val savePoint : Savepoint = mock() - private val restrictedConnection : RestrictedConnection = RestrictedConnection(connection) + private val connection: Connection = mock() + private val savePoint: Savepoint = mock() + private val cordapp = mock() + private val cordappContext = CordappContext.create(cordapp, null, javaClass.classLoader, mock()) + private val serviceHub = mock().apply { + whenever(getAppContext()).thenReturn(cordappContext) + } + private val restrictedConnection: RestrictedConnection = RestrictedConnection(connection, serviceHub) companion object { - private const val TEST_STRING : String = "test" - private const val TEST_INT : Int = 1 + private const val TEST_STRING: String = "test" + private const val TEST_INT: Int = 1 } - @Test(expected = UnsupportedOperationException::class, timeout=300_000) - fun testAbort() { + @Test(expected = UnsupportedOperationException::class, timeout = 300_000) + fun `abort with target platform version of current corda version throws unsupported exception`() { + whenever(cordapp.targetPlatformVersion).thenReturn(PLATFORM_VERSION) restrictedConnection.abort { println("I'm just an executor for this test...") } } - @Test(expected = UnsupportedOperationException::class, timeout=300_000) - fun testClearWarnings() { + @Test(expected = UnsupportedOperationException::class, timeout = 300_000) + fun `clearWarnings with target platform version of current corda version throws unsupported exception`() { + whenever(cordapp.targetPlatformVersion).thenReturn(PLATFORM_VERSION) restrictedConnection.clearWarnings() } - @Test(expected = UnsupportedOperationException::class, timeout=300_000) - fun testClose() { + @Test(expected = UnsupportedOperationException::class, timeout = 300_000) + fun `close with target platform version of current corda version throws unsupported exception`() { + whenever(cordapp.targetPlatformVersion).thenReturn(PLATFORM_VERSION) restrictedConnection.close() } - @Test(expected = UnsupportedOperationException::class, timeout=300_000) - fun testCommit() { + @Test(expected = UnsupportedOperationException::class, timeout = 300_000) + fun `commit with target platform version of current corda version throws unsupported exception`() { + whenever(cordapp.targetPlatformVersion).thenReturn(PLATFORM_VERSION) restrictedConnection.commit() } - @Test(expected = UnsupportedOperationException::class, timeout=300_000) - fun testSetSavepoint() { + @Test(expected = UnsupportedOperationException::class, timeout = 300_000) + fun `setSavepoint with target platform version of current corda version throws unsupported exception`() { + whenever(cordapp.targetPlatformVersion).thenReturn(PLATFORM_VERSION) restrictedConnection.setSavepoint() } - @Test(expected = UnsupportedOperationException::class, timeout=300_000) - fun testSetSavepointWithName() { + @Test(expected = UnsupportedOperationException::class, timeout = 300_000) + fun `setSavepoint with name with target platform version of current corda version throws unsupported exception`() { + whenever(cordapp.targetPlatformVersion).thenReturn(PLATFORM_VERSION) restrictedConnection.setSavepoint(TEST_STRING) } - @Test(expected = UnsupportedOperationException::class, timeout=300_000) - fun testReleaseSavepoint() { + @Test(expected = UnsupportedOperationException::class, timeout = 300_000) + fun `releaseSavepoint with target platform version of current corda version throws unsupported exception`() { + whenever(cordapp.targetPlatformVersion).thenReturn(PLATFORM_VERSION) restrictedConnection.releaseSavepoint(savePoint) } - @Test(expected = UnsupportedOperationException::class, timeout=300_000) - fun testRollback() { + @Test(expected = UnsupportedOperationException::class, timeout = 300_000) + fun `rollback with target platform version of current corda version throws unsupported exception`() { + whenever(cordapp.targetPlatformVersion).thenReturn(PLATFORM_VERSION) restrictedConnection.rollback() } - @Test(expected = UnsupportedOperationException::class, timeout=300_000) - fun testRollbackWithSavepoint() { + @Test(expected = UnsupportedOperationException::class, timeout = 300_000) + fun `rollbackWithSavepoint with target platform version of current corda version throws unsupported exception`() { + whenever(cordapp.targetPlatformVersion).thenReturn(PLATFORM_VERSION) restrictedConnection.rollback(savePoint) } - @Test(expected = UnsupportedOperationException::class, timeout=300_000) - fun testSetCatalog() { + @Test(expected = UnsupportedOperationException::class, timeout = 300_000) + fun `setCatalog with target platform version of current corda version throws unsupported exception`() { + whenever(cordapp.targetPlatformVersion).thenReturn(PLATFORM_VERSION) restrictedConnection.catalog = TEST_STRING } - @Test(expected = UnsupportedOperationException::class, timeout=300_000) - fun testSetTransactionIsolation() { + @Test(expected = UnsupportedOperationException::class, timeout = 300_000) + fun `setTransactionIsolation with target platform version of current corda version throws unsupported exception`() { + whenever(cordapp.targetPlatformVersion).thenReturn(PLATFORM_VERSION) restrictedConnection.transactionIsolation = TEST_INT } - @Test(expected = UnsupportedOperationException::class, timeout=300_000) - fun testSetTypeMap() { + @Test(expected = UnsupportedOperationException::class, timeout = 300_000) + fun `setTypeMap with target platform version of current corda version throws unsupported exception`() { + whenever(cordapp.targetPlatformVersion).thenReturn(PLATFORM_VERSION) val map: MutableMap> = mutableMapOf() restrictedConnection.typeMap = map } - @Test(expected = UnsupportedOperationException::class, timeout=300_000) - fun testSetHoldability() { + @Test(expected = UnsupportedOperationException::class, timeout = 300_000) + fun `setHoldability with target platform version of current corda version throws unsupported exception`() { + whenever(cordapp.targetPlatformVersion).thenReturn(PLATFORM_VERSION) restrictedConnection.holdability = TEST_INT } - @Test(expected = UnsupportedOperationException::class, timeout=300_000) - fun testSetSchema() { + @Test(expected = UnsupportedOperationException::class, timeout = 300_000) + fun `setSchema with target platform version of current corda version throws unsupported exception`() { + whenever(cordapp.targetPlatformVersion).thenReturn(PLATFORM_VERSION) restrictedConnection.schema = TEST_STRING } - @Test(expected = UnsupportedOperationException::class, timeout=300_000) - fun testSetNetworkTimeout() { + @Test(expected = UnsupportedOperationException::class, timeout = 300_000) + fun `setNetworkTimeout with target platform version of current corda version throws unsupported exception`() { + whenever(cordapp.targetPlatformVersion).thenReturn(PLATFORM_VERSION) restrictedConnection.setNetworkTimeout({ println("I'm just an executor for this test...") }, TEST_INT) } - @Test(expected = UnsupportedOperationException::class, timeout=300_000) - fun testSetAutoCommit() { + @Test(expected = UnsupportedOperationException::class, timeout = 300_000) + fun `setAutoCommit with target platform version of current corda version throws unsupported exception`() { + whenever(cordapp.targetPlatformVersion).thenReturn(PLATFORM_VERSION) restrictedConnection.autoCommit = true } - @Test(expected = UnsupportedOperationException::class, timeout=300_000) - fun testSetReadOnly() { + @Test(expected = UnsupportedOperationException::class, timeout = 300_000) + fun `setReadOnly with target platform version of current corda version throws unsupported exception`() { + whenever(cordapp.targetPlatformVersion).thenReturn(PLATFORM_VERSION) + restrictedConnection.isReadOnly = true + } + + @Test(expected = UnsupportedOperationException::class, timeout = 300_000) + fun `abort with target platform version of 7 throws unsupported exception`() { + whenever(cordapp.targetPlatformVersion).thenReturn(7) + restrictedConnection.abort { println("I'm just an executor for this test...") } + } + + @Test(expected = UnsupportedOperationException::class, timeout = 300_000) + fun `clearWarnings with target platform version of 7 throws unsupported exception`() { + whenever(cordapp.targetPlatformVersion).thenReturn(7) + restrictedConnection.clearWarnings() + } + + @Test(expected = UnsupportedOperationException::class, timeout = 300_000) + fun `close with target platform version of 7 throws unsupported exception`() { + whenever(cordapp.targetPlatformVersion).thenReturn(7) + restrictedConnection.close() + } + + @Test(expected = UnsupportedOperationException::class, timeout = 300_000) + fun `commit with target platform version of 7 throws unsupported exception`() { + whenever(cordapp.targetPlatformVersion).thenReturn(7) + restrictedConnection.commit() + } + + @Test(expected = UnsupportedOperationException::class, timeout = 300_000) + fun `setSavepoint with target platform version of 7 throws unsupported exception`() { + whenever(cordapp.targetPlatformVersion).thenReturn(7) + restrictedConnection.setSavepoint() + } + + @Test(expected = UnsupportedOperationException::class, timeout = 300_000) + fun `setSavepoint with name with target platform version of 7 throws unsupported exception`() { + whenever(cordapp.targetPlatformVersion).thenReturn(7) + restrictedConnection.setSavepoint(TEST_STRING) + } + + @Test(expected = UnsupportedOperationException::class, timeout = 300_000) + fun `releaseSavepoint with target platform version of 7 throws unsupported exception`() { + whenever(cordapp.targetPlatformVersion).thenReturn(7) + restrictedConnection.releaseSavepoint(savePoint) + } + + @Test(expected = UnsupportedOperationException::class, timeout = 300_000) + fun `rollback with target platform version of 7 throws unsupported exception`() { + whenever(cordapp.targetPlatformVersion).thenReturn(7) + restrictedConnection.rollback() + } + + @Test(expected = UnsupportedOperationException::class, timeout = 300_000) + fun `rollbackWithSavepoint with target platform version of 7 throws unsupported exception`() { + whenever(cordapp.targetPlatformVersion).thenReturn(7) + restrictedConnection.rollback(savePoint) + } + + @Test(expected = UnsupportedOperationException::class, timeout = 300_000) + fun `setCatalog with target platform version of 7 throws unsupported exception`() { + whenever(cordapp.targetPlatformVersion).thenReturn(7) + restrictedConnection.catalog = TEST_STRING + } + + @Test(expected = UnsupportedOperationException::class, timeout = 300_000) + fun `setTransactionIsolation with target platform version of 7 throws unsupported exception`() { + whenever(cordapp.targetPlatformVersion).thenReturn(7) + restrictedConnection.transactionIsolation = TEST_INT + } + + @Test(expected = UnsupportedOperationException::class, timeout = 300_000) + fun `setTypeMap with target platform version of 7 throws unsupported exception`() { + whenever(cordapp.targetPlatformVersion).thenReturn(7) + val map: MutableMap> = mutableMapOf() + restrictedConnection.typeMap = map + } + + @Test(expected = UnsupportedOperationException::class, timeout = 300_000) + fun `setHoldability with target platform version of 7 throws unsupported exception`() { + whenever(cordapp.targetPlatformVersion).thenReturn(7) + restrictedConnection.holdability = TEST_INT + } + + @Test(expected = UnsupportedOperationException::class, timeout = 300_000) + fun `setSchema with target platform version of current 7 unsupported exception`() { + whenever(cordapp.targetPlatformVersion).thenReturn(7) + restrictedConnection.schema = TEST_STRING + } + + @Test(expected = UnsupportedOperationException::class, timeout = 300_000) + fun `setNetworkTimeout with target platform version of 7 throws unsupported exception`() { + whenever(cordapp.targetPlatformVersion).thenReturn(7) + restrictedConnection.setNetworkTimeout({ println("I'm just an executor for this test...") }, TEST_INT) + } + + @Test(expected = UnsupportedOperationException::class, timeout = 300_000) + fun `setAutoCommit with target platform version of 7 throws unsupported exception`() { + whenever(cordapp.targetPlatformVersion).thenReturn(7) + restrictedConnection.autoCommit = true + } + + @Test(expected = UnsupportedOperationException::class, timeout = 300_000) + fun `setReadOnly with target platform version of 7 throws unsupported exception`() { + whenever(cordapp.targetPlatformVersion).thenReturn(7) + restrictedConnection.isReadOnly = true + } + + @Test(timeout = 300_000) + fun `abort with target platform version of 6 executes successfully`() { + whenever(cordapp.targetPlatformVersion).thenReturn(6) + restrictedConnection.abort { println("I'm just an executor for this test...") } + } + + @Test(timeout = 300_000) + fun `clearWarnings with target platform version of 6 executes successfully`() { + whenever(cordapp.targetPlatformVersion).thenReturn(6) + restrictedConnection.clearWarnings() + } + + @Test(timeout = 300_000) + fun `close with target platform version of 6 executes successfully`() { + whenever(cordapp.targetPlatformVersion).thenReturn(6) + restrictedConnection.close() + } + + @Test(timeout = 300_000) + fun `commit with target platform version of 6 executes successfully`() { + whenever(cordapp.targetPlatformVersion).thenReturn(6) + restrictedConnection.commit() + } + + @Test(timeout = 300_000) + fun `setSavepoint with target platform version of 6 executes successfully`() { + whenever(cordapp.targetPlatformVersion).thenReturn(6) + restrictedConnection.setSavepoint() + } + + @Test(timeout = 300_000) + fun `setSavepoint with name with target platform version of 6 executes successfully`() { + whenever(cordapp.targetPlatformVersion).thenReturn(6) + restrictedConnection.setSavepoint(TEST_STRING) + } + + @Test(timeout = 300_000) + fun `releaseSavepoint with target platform version of 6 executes successfully`() { + whenever(cordapp.targetPlatformVersion).thenReturn(6) + restrictedConnection.releaseSavepoint(savePoint) + } + + @Test(timeout = 300_000) + fun `rollback with target platform version of 6 executes successfully`() { + whenever(cordapp.targetPlatformVersion).thenReturn(6) + restrictedConnection.rollback() + } + + @Test(timeout = 300_000) + fun `rollbackWithSavepoint with target platform version of 6 executes successfully`() { + whenever(cordapp.targetPlatformVersion).thenReturn(6) + restrictedConnection.rollback(savePoint) + } + + @Test(timeout = 300_000) + fun `setCatalog with target platform version of 6 executes successfully`() { + whenever(cordapp.targetPlatformVersion).thenReturn(6) + restrictedConnection.catalog = TEST_STRING + } + + @Test(timeout = 300_000) + fun `setTransactionIsolation with target platform version of 6 executes successfully`() { + whenever(cordapp.targetPlatformVersion).thenReturn(6) + restrictedConnection.transactionIsolation = TEST_INT + } + + @Test(timeout = 300_000) + fun `setTypeMap with target platform version of 6 executes successfully`() { + whenever(cordapp.targetPlatformVersion).thenReturn(6) + val map: MutableMap> = mutableMapOf() + restrictedConnection.typeMap = map + } + + @Test(timeout = 300_000) + fun `setHoldability with target platform version of 6 executes successfully`() { + whenever(cordapp.targetPlatformVersion).thenReturn(6) + restrictedConnection.holdability = TEST_INT + } + + @Test(timeout = 300_000) + fun `setSchema with target platform version of current 6 unsupported exception`() { + whenever(cordapp.targetPlatformVersion).thenReturn(6) + restrictedConnection.schema = TEST_STRING + } + + @Test(timeout = 300_000) + fun `setNetworkTimeout with target platform version of 6 executes successfully`() { + whenever(cordapp.targetPlatformVersion).thenReturn(6) + restrictedConnection.setNetworkTimeout({ println("I'm just an executor for this test...") }, TEST_INT) + } + + @Test(timeout = 300_000) + fun `setAutoCommit with target platform version of 6 executes successfully`() { + whenever(cordapp.targetPlatformVersion).thenReturn(6) + restrictedConnection.autoCommit = true + } + + @Test(timeout = 300_000) + fun `setReadOnly with target platform version of 6 executes successfully`() { + whenever(cordapp.targetPlatformVersion).thenReturn(6) restrictedConnection.isReadOnly = true } } \ No newline at end of file diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/persistence/RestrictedEntityManagerTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/persistence/RestrictedEntityManagerTest.kt index 6f53ade01c..92994a7fab 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/persistence/RestrictedEntityManagerTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/persistence/RestrictedEntityManagerTest.kt @@ -3,6 +3,10 @@ package net.corda.nodeapi.internal.persistence import com.nhaarman.mockito_kotlin.doReturn import com.nhaarman.mockito_kotlin.mock import com.nhaarman.mockito_kotlin.whenever +import net.corda.core.cordapp.Cordapp +import net.corda.core.cordapp.CordappContext +import net.corda.core.internal.PLATFORM_VERSION +import net.corda.core.node.ServiceHub import org.junit.Test import javax.persistence.EntityManager import javax.persistence.EntityTransaction @@ -12,47 +16,160 @@ import kotlin.test.assertTrue class RestrictedEntityManagerTest { private val entitymanager = mock() private val transaction = mock() - private val restrictedEntityManager = RestrictedEntityManager(entitymanager) + private val cordapp = mock() + private val cordappContext = CordappContext.create(cordapp, null, javaClass.classLoader, mock()) + private val serviceHub = mock().apply { + whenever(getAppContext()).thenReturn(cordappContext) + } + private val restrictedEntityManager = RestrictedEntityManager(entitymanager, serviceHub) - @Test(expected = UnsupportedOperationException::class, timeout=300_000) - fun testClose() { + @Test(expected = UnsupportedOperationException::class, timeout = 300_000) + fun `close with target platform version of current corda version throws unsupported exception`() { + whenever(cordapp.targetPlatformVersion).thenReturn(PLATFORM_VERSION) restrictedEntityManager.close() } @Test(timeout = 300_000) - fun testClear() { + fun `clear with target platform version of current corda version throws unsupported exception`() { + whenever(cordapp.targetPlatformVersion).thenReturn(PLATFORM_VERSION) restrictedEntityManager.clear() } - @Test(expected = UnsupportedOperationException::class, timeout=300_000) - fun testGetMetaModel() { - restrictedEntityManager.getMetamodel() + @Test(expected = UnsupportedOperationException::class, timeout = 300_000) + fun `getMetaModel with target platform version of current corda version throws unsupported exception`() { + whenever(cordapp.targetPlatformVersion).thenReturn(PLATFORM_VERSION) + restrictedEntityManager.metamodel } @Test(timeout = 300_000) - fun testGetTransaction() { + fun `getTransaction with target platform version of current corda version executes successfully`() { + whenever(cordapp.targetPlatformVersion).thenReturn(PLATFORM_VERSION) whenever(entitymanager.transaction).doReturn(transaction) assertTrue(restrictedEntityManager.transaction is RestrictedEntityTransaction) } - @Test(expected = UnsupportedOperationException::class, timeout=300_000) - fun testJoinTransaction() { + @Test(expected = UnsupportedOperationException::class, timeout = 300_000) + fun `joinTransaction with target platform version of current corda version throws unsupported exception`() { + whenever(cordapp.targetPlatformVersion).thenReturn(PLATFORM_VERSION) restrictedEntityManager.joinTransaction() } - @Test(expected = UnsupportedOperationException::class, timeout=300_000) - fun testLockWithTwoParameters() { + @Test(expected = UnsupportedOperationException::class, timeout = 300_000) + fun `lock with two parameters with target platform version of current corda version throws unsupported exception`() { + whenever(cordapp.targetPlatformVersion).thenReturn(PLATFORM_VERSION) restrictedEntityManager.lock(Object(), LockModeType.OPTIMISTIC) } - @Test(expected = UnsupportedOperationException::class, timeout=300_000) - fun testLockWithThreeParameters() { - val map: MutableMap = mutableMapOf() - restrictedEntityManager.lock(Object(), LockModeType.OPTIMISTIC,map) + @Test(expected = UnsupportedOperationException::class, timeout = 300_000) + fun `lock with three parameters with target platform version of current corda version throws unsupported exception`() { + whenever(cordapp.targetPlatformVersion).thenReturn(PLATFORM_VERSION) + val map: MutableMap = mutableMapOf() + restrictedEntityManager.lock(Object(), LockModeType.OPTIMISTIC, map) } - @Test(expected = UnsupportedOperationException::class, timeout=300_000) - fun testSetProperty() { + @Test(expected = UnsupportedOperationException::class, timeout = 300_000) + fun `setProperty with target platform version of current corda version throws unsupported exception`() { + whenever(cordapp.targetPlatformVersion).thenReturn(PLATFORM_VERSION) + restrictedEntityManager.setProperty("number", 12) + } + + @Test(expected = UnsupportedOperationException::class, timeout = 300_000) + fun `close with target platform version of 7 throws unsupported exception`() { + whenever(cordapp.targetPlatformVersion).thenReturn(7) + restrictedEntityManager.close() + } + + @Test(timeout = 300_000) + fun `clear with target platform version of 7 executes successfully`() { + whenever(cordapp.targetPlatformVersion).thenReturn(7) + restrictedEntityManager.clear() + } + + @Test(expected = UnsupportedOperationException::class, timeout = 300_000) + fun `getMetaModel with target platform version of 7 throws unsupported exception`() { + whenever(cordapp.targetPlatformVersion).thenReturn(7) + restrictedEntityManager.metamodel + } + + @Test(timeout = 300_000) + fun `getTransaction with target platform version of 7 throws unsupported exception`() { + whenever(cordapp.targetPlatformVersion).thenReturn(7) + whenever(entitymanager.transaction).doReturn(transaction) + assertTrue(restrictedEntityManager.transaction is RestrictedEntityTransaction) + } + + @Test(expected = UnsupportedOperationException::class, timeout = 300_000) + fun `joinTransaction with target platform version of 7 throws unsupported exception`() { + whenever(cordapp.targetPlatformVersion).thenReturn(7) + restrictedEntityManager.joinTransaction() + } + + @Test(expected = UnsupportedOperationException::class, timeout = 300_000) + fun `lock with two parameters with target platform version of 7 throws unsupported exception`() { + whenever(cordapp.targetPlatformVersion).thenReturn(7) + restrictedEntityManager.lock(Object(), LockModeType.OPTIMISTIC) + } + + @Test(expected = UnsupportedOperationException::class, timeout = 300_000) + fun `lock with three parameters with target platform version of 7 throws unsupported exception`() { + whenever(cordapp.targetPlatformVersion).thenReturn(7) + val map: MutableMap = mutableMapOf() + restrictedEntityManager.lock(Object(), LockModeType.OPTIMISTIC, map) + } + + @Test(expected = UnsupportedOperationException::class, timeout = 300_000) + fun `setProperty with target platform version of 7 throws unsupported exception`() { + whenever(cordapp.targetPlatformVersion).thenReturn(7) + restrictedEntityManager.setProperty("number", 12) + } + + @Test(timeout = 300_000) + fun `close with target platform version of 6 executes successfully`() { + whenever(cordapp.targetPlatformVersion).thenReturn(6) + restrictedEntityManager.close() + } + + @Test(timeout = 300_000) + fun `clear with target platform version of 6 executes successfully`() { + whenever(cordapp.targetPlatformVersion).thenReturn(6) + restrictedEntityManager.clear() + } + + @Test(timeout = 300_000) + fun `getMetaModel with target platform version of 6 executes successfully`() { + whenever(cordapp.targetPlatformVersion).thenReturn(6) + restrictedEntityManager.metamodel + } + + @Test(timeout = 300_000) + fun `getTransaction with target platform version of 6 executes successfully`() { + whenever(cordapp.targetPlatformVersion).thenReturn(6) + whenever(entitymanager.transaction).doReturn(transaction) + assertTrue(restrictedEntityManager.transaction is RestrictedEntityTransaction) + } + + @Test(timeout = 300_000) + fun `joinTransaction with target platform version of 6 executes successfully`() { + whenever(cordapp.targetPlatformVersion).thenReturn(6) + restrictedEntityManager.joinTransaction() + } + + @Test(timeout = 300_000) + fun `lock with two parameters with target platform version of 6 executes successfully`() { + whenever(cordapp.targetPlatformVersion).thenReturn(6) + restrictedEntityManager.lock(Object(), LockModeType.OPTIMISTIC) + } + + @Test(timeout = 300_000) + fun `lock with three parameters with target platform version of 6 executes successfully`() { + whenever(cordapp.targetPlatformVersion).thenReturn(6) + val map: MutableMap = mutableMapOf() + restrictedEntityManager.lock(Object(), LockModeType.OPTIMISTIC, map) + } + + @Test(timeout = 300_000) + fun `setProperty with target platform version of 6 executes successfully`() { + whenever(cordapp.targetPlatformVersion).thenReturn(6) restrictedEntityManager.setProperty("number", 12) } } \ No newline at end of file diff --git a/node/src/integration-test/kotlin/net/corda/node/amqp/ProtonWrapperTests.kt b/node/src/integration-test/kotlin/net/corda/node/amqp/ProtonWrapperTests.kt index dcbf40c145..3f0ac98f4c 100644 --- a/node/src/integration-test/kotlin/net/corda/node/amqp/ProtonWrapperTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/amqp/ProtonWrapperTests.kt @@ -291,9 +291,11 @@ class ProtonWrapperTests { @Test(timeout=300_000) fun `Send a message larger then maxMessageSize from AMQP to Artemis inbox`() { - val maxMessageSize = 100_000 - val (server, artemisClient) = createArtemisServerAndClient(maxMessageSize) - val amqpClient = createClient(maxMessageSize) + val maxUserPayloadSize = 100_000 + val maxMessageSizeWithHeaders = maxUserPayloadSize + 512 // Adding a small "shim" to account for headers + // and other non-payload bits of data + val (server, artemisClient) = createArtemisServerAndClient(maxMessageSizeWithHeaders) + val amqpClient = createClient(maxMessageSizeWithHeaders) val clientConnected = amqpClient.onConnection.toFuture() amqpClient.start() assertEquals(true, clientConnected.get().connected) @@ -308,7 +310,7 @@ class ProtonWrapperTests { testProperty["TestProp"] = "1" // Send normal message. - val testData = ByteArray(maxMessageSize) + val testData = ByteArray(maxUserPayloadSize) val message = amqpClient.createMessage(testData, sendAddress, CHARLIE_NAME.toString(), testProperty) amqpClient.write(message) assertEquals(MessageStatus.Acknowledged, message.onComplete.get()) @@ -317,7 +319,7 @@ class ProtonWrapperTests { assertArrayEquals(testData, ByteArray(received.bodySize).apply { received.bodyBuffer.readBytes(this) }) // Send message larger than max message size. - val largeData = ByteArray(maxMessageSize + 1) + val largeData = ByteArray(maxMessageSizeWithHeaders + 1) // Create message will fail. assertThatThrownBy { amqpClient.createMessage(largeData, sendAddress, CHARLIE_NAME.toString(), testProperty) diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index 7deb9ae1d4..2f2479c139 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -455,7 +455,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, } } - fun clearNetworkMapCache() { + open fun clearNetworkMapCache() { Node.printBasicNodeInfo("Clearing network map cache entries") log.info("Starting clearing of network map cache entries...") startDatabase() @@ -690,9 +690,13 @@ abstract class AbstractNode(val configuration: NodeConfiguration, val isShellStarted = InteractiveShell.startShellIfInstalled(configuration, cordappLoader) configuration.sshd?.port?.let { if (isShellStarted) { - log.info("Binding Shell SSHD server on port $it.") + Node.printBasicNodeInfo("SSH server listening on port", configuration.sshd!!.port.toString()) + log.info("SSH server listening on port: $it.") } else { - log.info("SSH port defined but corda-shell is not installed in node's drivers directory") + Node.printBasicNodeInfo( + "SSH server not started. SSH port is defined but the corda-shell is not installed in node's drivers directory" + ) + log.info("SSH server not started. SSH port is defined but the corda-shell is not installed in node's drivers directory") } } } @@ -1157,7 +1161,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, /** * Exposes the database connection as a [RestrictedConnection] to the users. */ - override fun jdbcSession(): Connection = RestrictedConnection(database.createSession()) + override fun jdbcSession(): Connection = RestrictedConnection(database.createSession(), services) @Suppress("TooGenericExceptionCaught") override fun withEntityManager(block: EntityManager.() -> T): T { @@ -1167,7 +1171,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, withSavePoint { savepoint -> // Restrict what entity manager they can use inside the block try { - block(RestrictedEntityManager(manager)).also { + block(RestrictedEntityManager(manager, services)).also { if (!manager.transaction.rollbackOnly) { manager.flush() } else { diff --git a/node/src/main/kotlin/net/corda/node/internal/Node.kt b/node/src/main/kotlin/net/corda/node/internal/Node.kt index 4ee0da30ec..87f27d7415 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -564,6 +564,11 @@ open class Node(configuration: NodeConfiguration, return super.generateAndSaveNodeInfo() } + override fun clearNetworkMapCache() { + initialiseSerialization() + super.clearNetworkMapCache() + } + override fun runDatabaseMigrationScripts( updateCoreSchemas: Boolean, updateAppSchemas: Boolean, diff --git a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt index 0e90920616..1772210e56 100644 --- a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt +++ b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt @@ -48,7 +48,6 @@ import net.corda.node.internal.subcommands.ValidateConfigurationCli.Companion.lo import net.corda.node.internal.subcommands.ValidateConfigurationCli.Companion.logRawConfig import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.shouldStartLocalShell -import net.corda.node.services.config.shouldStartSSHDaemon import net.corda.node.utilities.registration.NodeRegistrationException import net.corda.nodeapi.internal.JVMAgentUtilities import net.corda.nodeapi.internal.addShutdownHook @@ -263,19 +262,8 @@ open class NodeStartup : NodeStartupLogging { Node.printBasicNodeInfo("Node for \"$name\" started up and registered in $elapsed sec") // Don't start the shell if there's no console attached. - val isShellStarted = if (node.configuration.shouldStartLocalShell()) { + if (node.configuration.shouldStartLocalShell()) { InteractiveShell.runLocalShellIfInstalled(node::stop) - } else { - false - } - if (node.configuration.shouldStartSSHDaemon()) { - if (isShellStarted) { - Node.printBasicNodeInfo("SSH server listening on port", node.configuration.sshd!!.port.toString()) - } else { - Node.printBasicNodeInfo( - "SSH server not started. SSH port is defined but the corda-shell is not installed in node's drivers directory" - ) - } } }, { th -> diff --git a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt index 7e46961de7..eb79a58302 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt @@ -455,7 +455,10 @@ open class PersistentNetworkMapCache(cacheFactory: NamedCacheFactory, database.transaction { val result = getAllNodeInfos(session) logger.debug { "Number of node infos to be cleared: ${result.size}" } - for (nodeInfo in result) session.remove(nodeInfo) + for (nodeInfo in result) { + session.remove(nodeInfo) + archiveNamedIdentity(session, nodeInfo.toNodeInfo()) + } } } diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt b/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt index e6aee94e7f..6ea173ef4d 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt @@ -88,6 +88,9 @@ class NodeAttachmentService @JvmOverloads constructor( while (true) { val cursor = jar.nextJarEntry ?: break + // Security check to stop directory traversal from filename entry + require(!(cursor.name.contains("../"))) { "Bad character in ${cursor.name}" } + require(!(cursor.name.contains("..\\"))) { "Bad character in ${cursor.name}" } if (manifestHasEntries && !allManifestEntries!!.remove(cursor.name)) extraFilesNotFoundInEntries.add(cursor) val entryPath = Paths.get(cursor.name) // Security check to stop zips trying to escape their rightful place. diff --git a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt index e7846b2821..6db962cdce 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt @@ -271,7 +271,7 @@ class NodeVaultService( // This will cause a failure as we can't deserialize such states in the context of the `appClassloader`. // For now we ignore these states. // In the future we will use the AttachmentsClassloader to correctly deserialize and asses the relevancy. - log.debug { "Could not deserialize state $idx from transaction $txId. Cause: $e" } + log.warn("Could not deserialize state $idx from transaction $txId. Cause: $e") null } }.toMap() diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt index e419df5d01..7c52587a3f 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt @@ -46,14 +46,19 @@ import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Ignore import org.junit.Test +import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream +import java.io.InputStream import java.net.URL import java.nio.charset.StandardCharsets import java.nio.file.FileAlreadyExistsException import java.nio.file.FileSystem import java.nio.file.Path import java.util.* +import java.util.jar.JarEntry import java.util.jar.JarInputStream +import java.util.jar.JarOutputStream +import java.util.jar.Manifest import kotlin.streams.toList import kotlin.test.* @@ -788,6 +793,32 @@ class NodeAttachmentServiceTest { } } + @Test(timeout=300_000) + fun `attachments containing jar entries whose names expose malicious directory traversal are prevented`() { + + fun createJarWithJarEntryTraversalAttack(jarEntryName: String): InputStream { + val byteArrayOutputStream = ByteArrayOutputStream() + JarOutputStream(byteArrayOutputStream, Manifest()).apply { + putNextEntry(JarEntry(jarEntryName)) + write("some-text".toByteArray()) + closeEntry() + close() + } + return ByteArrayInputStream(byteArrayOutputStream.toByteArray()) + } + + val traversalAttackJarWin = createJarWithJarEntryTraversalAttack("..\\attack") + val traversalAttackJarUnix = createJarWithJarEntryTraversalAttack("../attack") + + assertFailsWith(IllegalArgumentException::class) { + NodeAttachmentService.checkIsAValidJAR(traversalAttackJarWin) + } + + assertFailsWith(IllegalArgumentException::class) { + NodeAttachmentService.checkIsAValidJAR(traversalAttackJarUnix) + } + } + @Test(timeout=300_000) fun `attachments can be queried by providing a intersection of signers using an EQUAL statement - EQUAL containing a single public key`() { SelfCleaningDir().use { file -> diff --git a/samples/irs-demo/cordapp/workflows-irs/build.gradle b/samples/irs-demo/cordapp/workflows-irs/build.gradle index ce09b2a803..ff88428b24 100644 --- a/samples/irs-demo/cordapp/workflows-irs/build.gradle +++ b/samples/irs-demo/cordapp/workflows-irs/build.gradle @@ -16,7 +16,7 @@ dependencies { cordaCompile project(':core') - compile("com.fasterxml.jackson.module:jackson-module-kotlin:$jackson_version") + compile("com.fasterxml.jackson.module:jackson-module-kotlin:$jackson_kotlin_version") // only included to control the `DemoClock` as part of the demo application // normally `:node` should not be depended on in any CorDapps diff --git a/samples/irs-demo/web/build.gradle b/samples/irs-demo/web/build.gradle index 8064af4347..b887d036cb 100644 --- a/samples/irs-demo/web/build.gradle +++ b/samples/irs-demo/web/build.gradle @@ -70,7 +70,7 @@ dependencies { } compile('org.springframework.boot:spring-boot-starter-log4j2') runtimeOnly("org.apache.logging.log4j:log4j-web:$log4j_version") - compile("com.fasterxml.jackson.module:jackson-module-kotlin:$jackson_version") + compile("com.fasterxml.jackson.module:jackson-module-kotlin:$jackson_kotlin_version") compile project(":client:rpc") compile project(":client:jackson") compile project(":finance:workflows") diff --git a/testing/test-cli/build.gradle b/testing/test-cli/build.gradle index a668c605f4..d4de7deba0 100644 --- a/testing/test-cli/build.gradle +++ b/testing/test-cli/build.gradle @@ -6,7 +6,7 @@ dependencies { compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" compile "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:$jackson_version" compile "com.fasterxml.jackson.core:jackson-databind:$jackson_version" - compile "com.fasterxml.jackson.module:jackson-module-kotlin:$jackson_version" + compile "com.fasterxml.jackson.module:jackson-module-kotlin:$jackson_kotlin_version" compile "org.junit.jupiter:junit-jupiter-api:${junit_jupiter_version}" compile "junit:junit:${junit_version}" diff --git a/tools/network-builder/build.gradle b/tools/network-builder/build.gradle index 1790147579..51ec4d6339 100644 --- a/tools/network-builder/build.gradle +++ b/tools/network-builder/build.gradle @@ -52,7 +52,7 @@ dependencies { compile "com.typesafe:config:$typesafe_config_version" compile "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:$jackson_version" compile "com.fasterxml.jackson.core:jackson-databind:$jackson_version" - compile "com.fasterxml.jackson.module:jackson-module-kotlin:$jackson_version" + compile "com.fasterxml.jackson.module:jackson-module-kotlin:$jackson_kotlin_version" compile "info.picocli:picocli:$picocli_version" // TornadoFX: A lightweight Kotlin framework for working with JavaFX UI's.