From d89ce6608add763f7a448a18b4fff1abac03621c Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Wed, 5 Feb 2020 10:46:42 +0000 Subject: [PATCH] Create a unit test for serialization whitelists via driver. --- .../corda/core/internal/ClassLoadingUtils.kt | 1 + .../whitelist/WhitelistContract.kt | 38 ++++++++ .../serialization/whitelist/WhitelistData.kt | 15 +++ .../djvm/attachment/SandboxAttachmentFlow.kt | 2 - .../flows/djvm/broken/NonDeterministicFlow.kt | 2 - .../djvm/crypto/DeterministicCryptoFlow.kt | 2 - .../whitelist/DeterministicWhitelistFlow.kt | 2 - .../net/corda/flows/fixup/CordappFixupFlow.kt | 2 - .../custom/CustomSerializerFlow.kt | 2 - .../missing/MissingSerializerBuilderFlow.kt | 2 - .../missing/MissingSerializerFlow.kt | 2 - .../serialization/whitelist/WhitelistFlow.kt | 26 +++++ .../ContractWithSerializationWhitelistTest.kt | 96 +++++++++++++++++++ .../cordapp/JarScanningCordappLoader.kt | 31 +++++- .../testing/node/internal/DriverDSLImpl.kt | 21 ++-- 15 files changed, 212 insertions(+), 32 deletions(-) create mode 100644 node/src/integration-test/kotlin/net/corda/contracts/serialization/whitelist/WhitelistContract.kt create mode 100644 node/src/integration-test/kotlin/net/corda/contracts/serialization/whitelist/WhitelistData.kt create mode 100644 node/src/integration-test/kotlin/net/corda/flows/serialization/whitelist/WhitelistFlow.kt create mode 100644 node/src/integration-test/kotlin/net/corda/node/ContractWithSerializationWhitelistTest.kt diff --git a/core/src/main/kotlin/net/corda/core/internal/ClassLoadingUtils.kt b/core/src/main/kotlin/net/corda/core/internal/ClassLoadingUtils.kt index fbe359c55d..72070a46ed 100644 --- a/core/src/main/kotlin/net/corda/core/internal/ClassLoadingUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/ClassLoadingUtils.kt @@ -36,6 +36,7 @@ fun getNamesOfClassesImplementing(classloader: ClassLoader, clazz: Clas return ClassGraph().overrideClassLoaders(classloader) .enableURLScheme(attachmentScheme) .ignoreParentClassLoaders() + .disableDirScanning() .enableClassInfo() .pooledScan() .use { result -> diff --git a/node/src/integration-test/kotlin/net/corda/contracts/serialization/whitelist/WhitelistContract.kt b/node/src/integration-test/kotlin/net/corda/contracts/serialization/whitelist/WhitelistContract.kt new file mode 100644 index 0000000000..b67d2134c9 --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/contracts/serialization/whitelist/WhitelistContract.kt @@ -0,0 +1,38 @@ +package net.corda.contracts.serialization.whitelist + +import net.corda.core.contracts.CommandData +import net.corda.core.contracts.Contract +import net.corda.core.contracts.ContractState +import net.corda.core.identity.AbstractParty +import net.corda.core.transactions.LedgerTransaction + +class WhitelistContract : Contract { + companion object { + const val MAX_VALUE = 2000L + } + + override fun verify(tx: LedgerTransaction) { + val states = tx.outputsOfType() + require(states.isNotEmpty()) { + "Requires at least one data state" + } + + states.forEach { + require(it.whitelistData in WhitelistData(0)..WhitelistData(MAX_VALUE)) { + "WhitelistData $it exceeds maximum value!" + } + } + } + + @Suppress("CanBeParameter", "MemberVisibilityCanBePrivate") + class State(val owner: AbstractParty, val whitelistData: WhitelistData) : ContractState { + override val participants: List = listOf(owner) + + @Override + override fun toString(): String { + return whitelistData.toString() + } + } + + class Operate : CommandData +} \ No newline at end of file diff --git a/node/src/integration-test/kotlin/net/corda/contracts/serialization/whitelist/WhitelistData.kt b/node/src/integration-test/kotlin/net/corda/contracts/serialization/whitelist/WhitelistData.kt new file mode 100644 index 0000000000..42c3e1e5c7 --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/contracts/serialization/whitelist/WhitelistData.kt @@ -0,0 +1,15 @@ +package net.corda.contracts.serialization.whitelist + +import net.corda.core.serialization.SerializationWhitelist + +data class WhitelistData(val value: Long) : Comparable { + override fun compareTo(other: WhitelistData): Int { + return value.compareTo(other.value) + } + + override fun toString(): String = "$value things" +} + +class Whitelist : SerializationWhitelist { + override val whitelist = listOf(WhitelistData::class.java) +} diff --git a/node/src/integration-test/kotlin/net/corda/flows/djvm/attachment/SandboxAttachmentFlow.kt b/node/src/integration-test/kotlin/net/corda/flows/djvm/attachment/SandboxAttachmentFlow.kt index e51313602e..ae277dfda2 100644 --- a/node/src/integration-test/kotlin/net/corda/flows/djvm/attachment/SandboxAttachmentFlow.kt +++ b/node/src/integration-test/kotlin/net/corda/flows/djvm/attachment/SandboxAttachmentFlow.kt @@ -6,11 +6,9 @@ import net.corda.core.contracts.Command import net.corda.core.contracts.CommandData import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowLogic -import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.StartableByRPC import net.corda.core.transactions.TransactionBuilder -@InitiatingFlow @StartableByRPC class SandboxAttachmentFlow(private val command: CommandData) : FlowLogic() { @Suspendable diff --git a/node/src/integration-test/kotlin/net/corda/flows/djvm/broken/NonDeterministicFlow.kt b/node/src/integration-test/kotlin/net/corda/flows/djvm/broken/NonDeterministicFlow.kt index ca9ddad1ef..076f17bdff 100644 --- a/node/src/integration-test/kotlin/net/corda/flows/djvm/broken/NonDeterministicFlow.kt +++ b/node/src/integration-test/kotlin/net/corda/flows/djvm/broken/NonDeterministicFlow.kt @@ -6,11 +6,9 @@ import net.corda.core.contracts.Command import net.corda.core.contracts.TypeOnlyCommandData import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowLogic -import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.StartableByRPC import net.corda.core.transactions.TransactionBuilder -@InitiatingFlow @StartableByRPC class NonDeterministicFlow(private val trouble: TypeOnlyCommandData) : FlowLogic() { @Suspendable diff --git a/node/src/integration-test/kotlin/net/corda/flows/djvm/crypto/DeterministicCryptoFlow.kt b/node/src/integration-test/kotlin/net/corda/flows/djvm/crypto/DeterministicCryptoFlow.kt index 18fb73c380..65e0065eb1 100644 --- a/node/src/integration-test/kotlin/net/corda/flows/djvm/crypto/DeterministicCryptoFlow.kt +++ b/node/src/integration-test/kotlin/net/corda/flows/djvm/crypto/DeterministicCryptoFlow.kt @@ -6,12 +6,10 @@ import net.corda.core.contracts.Command import net.corda.core.contracts.CommandData import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowLogic -import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.StartableByRPC import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.OpaqueBytes -@InitiatingFlow @StartableByRPC class DeterministicCryptoFlow( private val command: CommandData, diff --git a/node/src/integration-test/kotlin/net/corda/flows/djvm/whitelist/DeterministicWhitelistFlow.kt b/node/src/integration-test/kotlin/net/corda/flows/djvm/whitelist/DeterministicWhitelistFlow.kt index 8a5573f030..fe8824e03c 100644 --- a/node/src/integration-test/kotlin/net/corda/flows/djvm/whitelist/DeterministicWhitelistFlow.kt +++ b/node/src/integration-test/kotlin/net/corda/flows/djvm/whitelist/DeterministicWhitelistFlow.kt @@ -7,11 +7,9 @@ import net.corda.contracts.djvm.whitelist.WhitelistData import net.corda.core.contracts.Command import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowLogic -import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.StartableByRPC import net.corda.core.transactions.TransactionBuilder -@InitiatingFlow @StartableByRPC class DeterministicWhitelistFlow(private val data: WhitelistData) : FlowLogic() { @Suspendable diff --git a/node/src/integration-test/kotlin/net/corda/flows/fixup/CordappFixupFlow.kt b/node/src/integration-test/kotlin/net/corda/flows/fixup/CordappFixupFlow.kt index c6731ecd50..d35c556dc9 100644 --- a/node/src/integration-test/kotlin/net/corda/flows/fixup/CordappFixupFlow.kt +++ b/node/src/integration-test/kotlin/net/corda/flows/fixup/CordappFixupFlow.kt @@ -7,11 +7,9 @@ import net.corda.contracts.fixup.dependent.DependentData import net.corda.core.contracts.Command import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowLogic -import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.StartableByRPC import net.corda.core.transactions.TransactionBuilder -@InitiatingFlow @StartableByRPC class CordappFixupFlow(private val data: DependentData) : FlowLogic() { @Suspendable diff --git a/node/src/integration-test/kotlin/net/corda/flows/serialization/custom/CustomSerializerFlow.kt b/node/src/integration-test/kotlin/net/corda/flows/serialization/custom/CustomSerializerFlow.kt index e3491748e6..4435418bf9 100644 --- a/node/src/integration-test/kotlin/net/corda/flows/serialization/custom/CustomSerializerFlow.kt +++ b/node/src/integration-test/kotlin/net/corda/flows/serialization/custom/CustomSerializerFlow.kt @@ -7,11 +7,9 @@ import net.corda.contracts.serialization.custom.CustomSerializerContract.Purchas import net.corda.core.contracts.Command import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowLogic -import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.StartableByRPC import net.corda.core.transactions.TransactionBuilder -@InitiatingFlow @StartableByRPC class CustomSerializerFlow( private val purchase: Currantsy diff --git a/node/src/integration-test/kotlin/net/corda/flows/serialization/missing/MissingSerializerBuilderFlow.kt b/node/src/integration-test/kotlin/net/corda/flows/serialization/missing/MissingSerializerBuilderFlow.kt index 8353ee6bc5..b5ad46d01a 100644 --- a/node/src/integration-test/kotlin/net/corda/flows/serialization/missing/MissingSerializerBuilderFlow.kt +++ b/node/src/integration-test/kotlin/net/corda/flows/serialization/missing/MissingSerializerBuilderFlow.kt @@ -7,11 +7,9 @@ import net.corda.contracts.serialization.missing.MissingSerializerContract.Opera import net.corda.core.contracts.Command import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowLogic -import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.StartableByRPC import net.corda.core.transactions.TransactionBuilder -@InitiatingFlow @StartableByRPC class MissingSerializerBuilderFlow(private val value: Long) : FlowLogic() { @Suspendable diff --git a/node/src/integration-test/kotlin/net/corda/flows/serialization/missing/MissingSerializerFlow.kt b/node/src/integration-test/kotlin/net/corda/flows/serialization/missing/MissingSerializerFlow.kt index 8f38f85f72..6bef6b071f 100644 --- a/node/src/integration-test/kotlin/net/corda/flows/serialization/missing/MissingSerializerFlow.kt +++ b/node/src/integration-test/kotlin/net/corda/flows/serialization/missing/MissingSerializerFlow.kt @@ -12,14 +12,12 @@ import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SignableData import net.corda.core.crypto.SignatureMetadata import net.corda.core.flows.FlowLogic -import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.StartableByRPC import net.corda.core.internal.createComponentGroups import net.corda.core.internal.requiredContractClassName import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.WireTransaction -@InitiatingFlow @StartableByRPC class MissingSerializerFlow(private val value: Long) : FlowLogic() { @Suspendable diff --git a/node/src/integration-test/kotlin/net/corda/flows/serialization/whitelist/WhitelistFlow.kt b/node/src/integration-test/kotlin/net/corda/flows/serialization/whitelist/WhitelistFlow.kt new file mode 100644 index 0000000000..e623d7a00e --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/flows/serialization/whitelist/WhitelistFlow.kt @@ -0,0 +1,26 @@ +package net.corda.flows.serialization.whitelist + +import co.paralleluniverse.fibers.Suspendable +import net.corda.contracts.serialization.whitelist.WhitelistContract +import net.corda.contracts.serialization.whitelist.WhitelistContract.State +import net.corda.contracts.serialization.whitelist.WhitelistData +import net.corda.core.contracts.Command +import net.corda.core.crypto.SecureHash +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.StartableByRPC +import net.corda.core.transactions.TransactionBuilder + +@StartableByRPC +class WhitelistFlow(private val data: WhitelistData) : FlowLogic() { + @Suspendable + override fun call(): SecureHash { + val notary = serviceHub.networkMapCache.notaryIdentities[0] + val stx = serviceHub.signInitialTransaction( + TransactionBuilder(notary) + .addOutputState(State(ourIdentity, data)) + .addCommand(Command(WhitelistContract.Operate(), ourIdentity.owningKey)) + ) + stx.verify(serviceHub, checkSufficientSignatures = false) + return stx.id + } +} diff --git a/node/src/integration-test/kotlin/net/corda/node/ContractWithSerializationWhitelistTest.kt b/node/src/integration-test/kotlin/net/corda/node/ContractWithSerializationWhitelistTest.kt new file mode 100644 index 0000000000..e031a046ab --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/node/ContractWithSerializationWhitelistTest.kt @@ -0,0 +1,96 @@ +package net.corda.node + +import net.corda.client.rpc.CordaRPCClient +import net.corda.contracts.serialization.whitelist.WhitelistData +import net.corda.core.contracts.TransactionVerificationException.ContractRejection +import net.corda.core.messaging.startFlow +import net.corda.core.utilities.getOrThrow +import net.corda.flows.serialization.whitelist.WhitelistFlow +import net.corda.node.services.Permissions +import net.corda.testing.core.ALICE_NAME +import net.corda.testing.core.DUMMY_NOTARY_NAME +import net.corda.testing.driver.DriverParameters +import net.corda.testing.driver.driver +import net.corda.testing.driver.internal.incrementalPortAllocation +import net.corda.testing.node.NotarySpec +import net.corda.testing.node.User +import net.corda.testing.node.internal.cordappWithPackages +import org.assertj.core.api.Assertions.assertThat +import org.junit.BeforeClass +import org.junit.Test +import kotlin.test.assertFailsWith + +@Suppress("FunctionName") +class ContractWithSerializationWhitelistTest { + companion object { + const val DATA = 123456L + + @JvmField + val contractCordapp = cordappWithPackages("net.corda.contracts.serialization.whitelist").signed() + + @JvmField + val workflowCordapp = cordappWithPackages("net.corda.flows.serialization.whitelist").signed() + + fun parametersFor(runInProcess: Boolean): DriverParameters { + return DriverParameters( + portAllocation = incrementalPortAllocation(), + startNodesInProcess = runInProcess, + notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME, validating = true)), + cordappsForAllNodes = listOf(contractCordapp, workflowCordapp) + ) + } + + @BeforeClass + @JvmStatic + fun checkData() { + assertNotCordaSerializable() + } + } + + @Test + fun `test serialization whitelist out-of-process`() { + val user = User("u", "p", setOf(Permissions.all())) + driver(parametersFor(runInProcess = false)) { + val badData = WhitelistData(DATA) + val alice = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow() + val ex = assertFailsWith { + CordaRPCClient(hostAndPort = alice.rpcAddress) + .start(user.username, user.password) + .use { client -> + client.proxy.startFlow(::WhitelistFlow, badData) + .returnValue + .getOrThrow() + } + } + assertThat(ex) + .hasMessageContaining("WhitelistData $badData exceeds maximum value!") + } + } + + @Test + fun `test serialization whitelist in-process`() { + val user = User("u", "p", setOf(Permissions.all())) + driver(parametersFor(runInProcess = true)) { + val badData = WhitelistData(DATA) + val alice = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow() + val ex = assertFailsWith { + CordaRPCClient(hostAndPort = alice.rpcAddress) + .start(user.username, user.password) + .use { client -> + client.proxy.startFlow(::WhitelistFlow, badData) + .returnValue + .getOrThrow() + } + } + assertThat(ex) + .hasMessageContaining("WhitelistData $badData exceeds maximum value!") + } + } + +// @Test +// fun `test serialization whitelist in-process`() { +// assertFailsWith { +// driver(parametersFor(runInProcess = true)) {} +// } +// } +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt index 345876741e..ce2a6544fe 100644 --- a/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt @@ -219,7 +219,7 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths: // present in the CorDapp. val result = scanResult.getClassesWithSuperclass(NotaryService::class) + scanResult.getClassesWithSuperclass(SinglePartyNotaryService::class) - if(!result.isEmpty()) { + if (result.isNotEmpty()) { logger.info("Found notary service CorDapp implementations: " + result.joinToString(", ")) } return result.firstOrNull() @@ -270,10 +270,26 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths: private fun findWhitelists(cordappJarPath: RestrictedURL): List { val whitelists = URLClassLoader(arrayOf(cordappJarPath.url)).use { ServiceLoader.load(SerializationWhitelist::class.java, it).toList() + }.filter { + it.javaClass.name.startsWith(cordappJarPath.qualifiedNamePrefix) && it.javaClass.location == cordappJarPath.url } - return whitelists.filter { - it.javaClass.location == cordappJarPath.url && it.javaClass.name.startsWith(cordappJarPath.qualifiedNamePrefix) - } + DefaultWhitelist // Always add the DefaultWhitelist to the whitelist for an app. + + whitelists.filterNot { + it.javaClass.location == cordappJarPath.url + }.apply { + if (isNotEmpty()) { + throw NotCordappWhitelist("Whitelists ${showClasses(this)} not found within ${cordappJarPath.url}") + } + } + return whitelists + DefaultWhitelist // Always add the DefaultWhitelist to the whitelist for an app. + } + + private fun showClasses(items: Iterable): String { + return items.map { + it::class.java + }.map { + "${it.name} in ${it.protectionDomain.codeSource.location}" + }.toString() } private fun findSerializers(scanResult: RestrictedScanResult): List> { @@ -381,6 +397,13 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths: */ class MultipleCordappsForFlowException(message: String) : Exception(message) +/** + * Thrown when a [SerializationWhitelist] is loaded from outside the CorDapp. + * Most likely because you are testing with node-driver and "in-process" nodes. + * Try using "out-of-process" driver nodes instead. + */ +class NotCordappWhitelist(message: String) : Exception(message) + /** * Thrown if an exception occurs whilst parsing version identifiers within cordapp configuration */ diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt index cacaaac303..48c602d359 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt @@ -142,11 +142,7 @@ class DriverDSLImpl( private lateinit var _notaries: CordaFuture> override val notaryHandles: List get() = _notaries.getOrThrow() - override val cordappsClassLoader: ClassLoader? = if (!startNodesInProcess) { - createCordappsClassLoader(cordappsForAllNodes) - } else { - null - } + override val cordappsClassLoader: URLClassLoader? = createCordappsClassLoader(cordappsForAllNodes) interface Waitable { @Throws(InterruptedException::class) @@ -195,14 +191,15 @@ class DriverDSLImpl( } override fun shutdown() { - if (waitForAllNodesToFinish) { - state.locked { - processes.forEach { it.waitFor() } + cordappsClassLoader.use { _ -> + if (waitForAllNodesToFinish) { + state.locked { + processes.forEach { it.waitFor() } + } } + _shutdownManager?.shutdown() + _executorService?.shutdownNow() } - _shutdownManager?.shutdown() - _executorService?.shutdownNow() - (cordappsClassLoader as? AutoCloseable)?.close() } private fun establishRpc(config: NodeConfig, processDeathFuture: CordaFuture): CordaFuture { @@ -992,7 +989,7 @@ class DriverDSLImpl( return config } - private fun createCordappsClassLoader(cordapps: Collection?): ClassLoader? { + private fun createCordappsClassLoader(cordapps: Collection?): URLClassLoader? { if (cordapps == null || cordapps.isEmpty()) { return null }