diff --git a/build.gradle b/build.gradle index 8bad2e7344..781fedb2c0 100644 --- a/build.gradle +++ b/build.gradle @@ -24,7 +24,7 @@ buildscript { ext.jackson_version = '2.9.2' ext.jetty_version = '9.4.7.v20170914' ext.jersey_version = '2.25' - ext.jolokia_version = '2.0.0-M3' + ext.jolokia_version = '1.3.7' ext.assertj_version = '3.8.0' ext.slf4j_version = '1.7.25' ext.log4j_version = '2.9.1' @@ -50,6 +50,8 @@ buildscript { ext.crash_version = 'cce5a00f114343c1145c1d7756e1dd6df3ea984e' ext.jsr305_version = constants.getProperty("jsr305Version") ext.spring_jdbc_version ='5.0.0.RELEASE' + ext.shiro_version = '1.4.0' + ext.artifactory_plugin_version = constants.getProperty('artifactoryPluginVersion') // Update 121 is required for ObjectInputFilter and at time of writing 131 was latest: ext.java8_minUpdateVersion = '131' @@ -73,6 +75,7 @@ buildscript { classpath "org.ajoberstar:grgit:1.1.0" classpath "net.i2p.crypto:eddsa:$eddsa_version" // Needed for ServiceIdentityGenerator in the build environment. classpath "org.owasp:dependency-check-gradle:${dependency_checker_version}" + classpath "org.jfrog.buildinfo:build-info-extractor-gradle:$artifactory_plugin_version" } } @@ -81,7 +84,6 @@ plugins { // but the DSL has some restrictions e.g can't be used on the allprojects section. So we should revisit this if there are improvements in Gradle. // Version 1.0.2 of this plugin uses capsule:1.0.1 id "us.kirchmeier.capsule" version "1.0.2" - id "com.jfrog.artifactory" version "4.4.18" } ext { @@ -93,6 +95,7 @@ apply plugin: 'com.github.ben-manes.versions' apply plugin: 'net.corda.plugins.publish-utils' apply plugin: 'net.corda.plugins.cordformation' apply plugin: 'maven-publish' +apply plugin: 'com.jfrog.artifactory' // We need the following three lines even though they're inside an allprojects {} block below because otherwise // IntelliJ gets confused when importing the project and ends up erasing and recreating the .idea directory, along diff --git a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt index 48908ef7b1..1c5f1d6d42 100644 --- a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt +++ b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt @@ -13,7 +13,7 @@ import net.corda.core.utilities.* import net.corda.node.services.messaging.RPCServerConfiguration import net.corda.nodeapi.RPCApi import net.corda.testing.* -import net.corda.testing.driver.poll +import net.corda.testing.internal.poll import net.corda.testing.internal.* import org.apache.activemq.artemis.api.core.SimpleString import org.junit.After @@ -70,8 +70,8 @@ class RPCStabilityTests : IntegrationTest() { val executor = Executors.newScheduledThreadPool(1) fun startAndStop() { rpcDriver { - val server = startRpcServer(ops = DummyOps) - startRpcClient(server.get().broker.hostAndPort!!).get() + val server = startRpcServer(ops = DummyOps).get() + startRpcClient(server.broker.hostAndPort!!).get() } } repeat(5) { @@ -238,6 +238,7 @@ class RPCStabilityTests : IntegrationTest() { override val protocolVersion = 0 override fun ping() = "pong" } + val serverFollower = shutdownManager.follower() val serverPort = startRpcServer(ops = ops).getOrThrow().broker.hostAndPort!! serverFollower.unfollow() @@ -355,7 +356,7 @@ class RPCStabilityTests : IntegrationTest() { } -fun RPCDriverExposedDSLInterface.pollUntilClientNumber(server: RpcServerHandle, expected: Int) { +fun RPCDriverDSL.pollUntilClientNumber(server: RpcServerHandle, expected: Int) { pollUntilTrue("number of RPC clients to become $expected") { val clientAddresses = server.broker.serverControl.addressNames.filter { it.startsWith(RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX) } clientAddresses.size == expected diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt index a85a6342b2..9de4fd6afb 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt @@ -22,7 +22,10 @@ import net.corda.core.internal.ThreadBox import net.corda.core.messaging.RPCOps import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.serialize -import net.corda.core.utilities.* +import net.corda.core.utilities.Try +import net.corda.core.utilities.contextLogger +import net.corda.core.utilities.debug +import net.corda.core.utilities.getOrThrow import net.corda.nodeapi.ArtemisConsumer import net.corda.nodeapi.ArtemisProducer import net.corda.nodeapi.RPCApi diff --git a/client/rpc/src/test/kotlin/net/corda/client/rpc/AbstractRPCTest.kt b/client/rpc/src/test/kotlin/net/corda/client/rpc/AbstractRPCTest.kt index 546415e247..053c8b85c1 100644 --- a/client/rpc/src/test/kotlin/net/corda/client/rpc/AbstractRPCTest.kt +++ b/client/rpc/src/test/kotlin/net/corda/client/rpc/AbstractRPCTest.kt @@ -7,7 +7,7 @@ import net.corda.core.messaging.RPCOps import net.corda.node.services.messaging.RPCServerConfiguration import net.corda.nodeapi.internal.config.User import net.corda.testing.SerializationEnvironmentRule -import net.corda.testing.internal.RPCDriverExposedDSLInterface +import net.corda.testing.internal.RPCDriverDSL import net.corda.testing.internal.rpcTestUser import net.corda.testing.internal.startInVmRpcClient import net.corda.testing.internal.startRpcClient @@ -41,7 +41,7 @@ open class AbstractRPCTest { val createSession: () -> ClientSession ) - inline fun RPCDriverExposedDSLInterface.testProxy( + inline fun RPCDriverDSL.testProxy( ops: I, rpcUser: User = rpcTestUser, clientConfiguration: RPCClientConfiguration = RPCClientConfiguration.default, @@ -55,9 +55,9 @@ open class AbstractRPCTest { } } RPCTestMode.Netty -> - startRpcServer(ops = ops, rpcUser = rpcUser, configuration = serverConfiguration).flatMap { server -> - startRpcClient(server.broker.hostAndPort!!, rpcUser.username, rpcUser.password, clientConfiguration).map { - TestProxy(it, { startArtemisSession(server.broker.hostAndPort!!, rpcUser.username, rpcUser.password) }) + startRpcServer(ops = ops, rpcUser = rpcUser, configuration = serverConfiguration).flatMap { (broker) -> + startRpcClient(broker.hostAndPort!!, rpcUser.username, rpcUser.password, clientConfiguration).map { + TestProxy(it, { startArtemisSession(broker.hostAndPort!!, rpcUser.username, rpcUser.password) }) } } }.get() diff --git a/client/rpc/src/test/kotlin/net/corda/client/rpc/ClientRPCInfrastructureTests.kt b/client/rpc/src/test/kotlin/net/corda/client/rpc/ClientRPCInfrastructureTests.kt index a5bf6c9b7d..eee72cf1b5 100644 --- a/client/rpc/src/test/kotlin/net/corda/client/rpc/ClientRPCInfrastructureTests.kt +++ b/client/rpc/src/test/kotlin/net/corda/client/rpc/ClientRPCInfrastructureTests.kt @@ -7,7 +7,7 @@ import net.corda.core.internal.concurrent.thenMatch import net.corda.core.messaging.RPCOps import net.corda.core.utilities.getOrThrow import net.corda.node.services.messaging.rpcContext -import net.corda.testing.internal.RPCDriverExposedDSLInterface +import net.corda.testing.internal.RPCDriverDSL import net.corda.testing.internal.rpcDriver import net.corda.testing.internal.rpcTestUser import org.assertj.core.api.Assertions.assertThat @@ -26,7 +26,7 @@ import kotlin.test.assertTrue class ClientRPCInfrastructureTests : AbstractRPCTest() { // TODO: Test that timeouts work - private fun RPCDriverExposedDSLInterface.testProxy(): TestOps { + private fun RPCDriverDSL.testProxy(): TestOps { return testProxy(TestOpsImpl()).ops } diff --git a/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCConcurrencyTests.kt b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCConcurrencyTests.kt index 7ba003f910..6d80a39398 100644 --- a/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCConcurrencyTests.kt +++ b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCConcurrencyTests.kt @@ -1,15 +1,15 @@ package net.corda.client.rpc import net.corda.client.rpc.internal.RPCClientConfiguration -import net.corda.core.messaging.RPCOps -import net.corda.core.utilities.millis import net.corda.core.crypto.random63BitValue import net.corda.core.internal.concurrent.fork import net.corda.core.internal.concurrent.transpose +import net.corda.core.messaging.RPCOps import net.corda.core.serialization.CordaSerializable import net.corda.core.utilities.getOrThrow +import net.corda.core.utilities.millis import net.corda.node.services.messaging.RPCServerConfiguration -import net.corda.testing.internal.RPCDriverExposedDSLInterface +import net.corda.testing.internal.RPCDriverDSL import net.corda.testing.internal.rpcDriver import net.corda.testing.internal.testThreadFactory import org.apache.activemq.artemis.utils.collections.ConcurrentHashSet @@ -20,7 +20,10 @@ import org.junit.runners.Parameterized import rx.Observable import rx.subjects.UnicastSubject import java.util.* -import java.util.concurrent.* +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.CountDownLatch +import java.util.concurrent.Executor +import java.util.concurrent.Executors @RunWith(Parameterized::class) class RPCConcurrencyTests : AbstractRPCTest() { @@ -84,7 +87,7 @@ class RPCConcurrencyTests : AbstractRPCTest() { } } - private fun RPCDriverExposedDSLInterface.testProxy(): TestProxy { + private fun RPCDriverDSL.testProxy(): TestProxy { return testProxy( TestOpsImpl(pool), clientConfiguration = RPCClientConfiguration.default.copy( diff --git a/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPerformanceTests.kt b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPerformanceTests.kt index ede114cf7d..90a01ec645 100644 --- a/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPerformanceTests.kt +++ b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPerformanceTests.kt @@ -5,14 +5,14 @@ import net.corda.client.rpc.internal.RPCClientConfiguration import net.corda.core.messaging.RPCOps import net.corda.core.utilities.minutes import net.corda.core.utilities.seconds -import net.corda.testing.internal.performance.div import net.corda.node.services.messaging.RPCServerConfiguration -import net.corda.testing.internal.RPCDriverExposedDSLInterface -import net.corda.testing.measure +import net.corda.testing.internal.RPCDriverDSL +import net.corda.testing.internal.performance.div import net.corda.testing.internal.performance.startPublishingFixedRateInjector import net.corda.testing.internal.performance.startReporter import net.corda.testing.internal.performance.startTightLoopInjector import net.corda.testing.internal.rpcDriver +import net.corda.testing.measure import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith @@ -42,7 +42,7 @@ class RPCPerformanceTests : AbstractRPCTest() { } } - private fun RPCDriverExposedDSLInterface.testProxy( + private fun RPCDriverDSL.testProxy( clientConfiguration: RPCClientConfiguration, serverConfiguration: RPCServerConfiguration ): TestProxy { diff --git a/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPermissionsTests.kt b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPermissionsTests.kt index 6e7de32087..0607541ba9 100644 --- a/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPermissionsTests.kt +++ b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPermissionsTests.kt @@ -1,24 +1,19 @@ package net.corda.client.rpc -import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.RPCOps -import net.corda.node.services.Permissions.Companion.invokeRpc import net.corda.node.services.messaging.rpcContext import net.corda.nodeapi.internal.config.User -import net.corda.testing.internal.RPCDriverExposedDSLInterface +import net.corda.testing.internal.RPCDriverDSL import net.corda.testing.internal.rpcDriver import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized -import kotlin.reflect.KVisibility -import kotlin.reflect.full.declaredMemberFunctions import kotlin.test.assertFailsWith @RunWith(Parameterized::class) class RPCPermissionsTests : AbstractRPCTest() { companion object { const val DUMMY_FLOW = "StartFlow.net.corda.flows.DummyFlow" - const val OTHER_FLOW = "StartFlow.net.corda.flows.OtherFlow" const val ALL_ALLOWED = "ALL" } @@ -26,18 +21,27 @@ class RPCPermissionsTests : AbstractRPCTest() { * RPC operation. */ interface TestOps : RPCOps { - fun validatePermission(str: String) + fun validatePermission(method: String, target: String? = null) } class TestOpsImpl : TestOps { override val protocolVersion = 1 - override fun validatePermission(str: String) { rpcContext().requirePermission(str) } + override fun validatePermission(method: String, target: String?) { + val authorized = if (target == null) { + rpcContext().isPermitted(method) + } else { + rpcContext().isPermitted(method, target) + } + if (!authorized) { + throw PermissionException("RPC user not authorized") + } + } } /** * Create an RPC proxy for the given user. */ - private fun RPCDriverExposedDSLInterface.testProxyFor(rpcUser: User) = testProxy(TestOpsImpl(), rpcUser).ops + private fun RPCDriverDSL.testProxyFor(rpcUser: User) = testProxy(TestOpsImpl(), rpcUser).ops private fun userOf(name: String, permissions: Set) = User(name, "password", permissions) @@ -46,9 +50,9 @@ class RPCPermissionsTests : AbstractRPCTest() { rpcDriver { val emptyUser = userOf("empty", emptySet()) val proxy = testProxyFor(emptyUser) - assertFailsWith(PermissionException::class, - "User ${emptyUser.username} should not be allowed to use $DUMMY_FLOW.", - { proxy.validatePermission(DUMMY_FLOW) }) + assertNotAllowed { + proxy.validatePermission("startFlowDynamic", "net.corda.flows.DummyFlow") + } } } @@ -57,7 +61,8 @@ class RPCPermissionsTests : AbstractRPCTest() { rpcDriver { val adminUser = userOf("admin", setOf(ALL_ALLOWED)) val proxy = testProxyFor(adminUser) - proxy.validatePermission(DUMMY_FLOW) + proxy.validatePermission("startFlowDynamic", "net.corda.flows.DummyFlow") + proxy.validatePermission("startTrackedFlowDynamic", "net.corda.flows.DummyFlow") } } @@ -66,7 +71,8 @@ class RPCPermissionsTests : AbstractRPCTest() { rpcDriver { val joeUser = userOf("joe", setOf(DUMMY_FLOW)) val proxy = testProxyFor(joeUser) - proxy.validatePermission(DUMMY_FLOW) + proxy.validatePermission("startFlowDynamic", "net.corda.flows.DummyFlow") + proxy.validatePermission("startTrackedFlowDynamic", "net.corda.flows.DummyFlow") } } @@ -75,36 +81,46 @@ class RPCPermissionsTests : AbstractRPCTest() { rpcDriver { val joeUser = userOf("joe", setOf(DUMMY_FLOW)) val proxy = testProxyFor(joeUser) - assertFailsWith(PermissionException::class, - "User ${joeUser.username} should not be allowed to use $OTHER_FLOW", - { proxy.validatePermission(OTHER_FLOW) }) - } - } - - @Test - fun `check ALL is implemented the correct way round`() { - rpcDriver { - val joeUser = userOf("joe", setOf(DUMMY_FLOW)) - val proxy = testProxyFor(joeUser) - assertFailsWith(PermissionException::class, - "Permission $ALL_ALLOWED should not do anything for User ${joeUser.username}", - { proxy.validatePermission(ALL_ALLOWED) }) - } - } - - @Test - fun `fine grained permissions are enforced`() { - val allPermissions = CordaRPCOps::class.declaredMemberFunctions.filter { it.visibility == KVisibility.PUBLIC }.map { invokeRpc(it) } - allPermissions.forEach { permission -> - rpcDriver { - val user = userOf("Mark", setOf(permission)) - val proxy = testProxyFor(user) - - proxy.validatePermission(permission) - (allPermissions - permission).forEach { notOwnedPermission -> - assertFailsWith(PermissionException::class, { proxy.validatePermission(notOwnedPermission) }) - } + assertNotAllowed { + proxy.validatePermission("startFlowDynamic", "net.corda.flows.OtherFlow") + } + assertNotAllowed { + proxy.validatePermission("startTrackedFlowDynamic", "net.corda.flows.OtherFlow") } } } + + @Test + fun `joe user is not allowed to call other RPC methods`() { + rpcDriver { + val joeUser = userOf("joe", setOf(DUMMY_FLOW)) + val proxy = testProxyFor(joeUser) + assertNotAllowed { + proxy.validatePermission("nodeInfo") + } + assertNotAllowed { + proxy.validatePermission("networkMapFeed") + } + } + } + + @Test + fun `checking invokeRpc permissions entitlements`() { + rpcDriver { + val joeUser = userOf("joe", setOf("InvokeRpc.networkMapFeed")) + val proxy = testProxyFor(joeUser) + assertNotAllowed { + proxy.validatePermission("nodeInfo") + } + assertNotAllowed { + proxy.validatePermission("startTrackedFlowDynamic", "net.corda.flows.OtherFlow") + } + proxy.validatePermission("networkMapFeed") + } + } + + private fun assertNotAllowed(action: () -> Unit) { + + assertFailsWith(PermissionException::class, "User should not be allowed to perform this action.", action) + } } diff --git a/config/dev/jolokia-access.xml b/config/dev/jolokia-access.xml index 9b4acde879..41ff27a44e 100644 --- a/config/dev/jolokia-access.xml +++ b/config/dev/jolokia-access.xml @@ -1,5 +1,5 @@ - + post @@ -8,23 +8,10 @@ read + write + exec list + search + version - - - - - java.lang:type=Memory - gc - - - - - - - com.mchange.v2.c3p0:type=PooledDataSource,* - properties - - - diff --git a/config/prod/jolokia-access.xml b/config/prod/jolokia-access.xml new file mode 100644 index 0000000000..c17fd09320 --- /dev/null +++ b/config/prod/jolokia-access.xml @@ -0,0 +1,24 @@ + + + + + + + + 127.0.0.1 + localhost + + + + version + read + + + + + get + + + \ No newline at end of file diff --git a/constants.properties b/constants.properties index cb32304d74..71cec9ca52 100644 --- a/constants.properties +++ b/constants.properties @@ -5,3 +5,4 @@ guavaVersion=21.0 bouncycastleVersion=1.57 typesafeConfigVersion=1.3.1 jsr305Version=3.0.2 +artifactoryPluginVersion=4.4.18 \ No newline at end of file diff --git a/core/src/test/kotlin/net/corda/core/contracts/DummyContractV2Tests.kt b/core/src/test/kotlin/net/corda/core/contracts/DummyContractV2Tests.kt index 361fb6e876..29ad386359 100644 --- a/core/src/test/kotlin/net/corda/core/contracts/DummyContractV2Tests.kt +++ b/core/src/test/kotlin/net/corda/core/contracts/DummyContractV2Tests.kt @@ -1,13 +1,14 @@ package net.corda.core.contracts +import com.nhaarman.mockito_kotlin.doReturn +import com.nhaarman.mockito_kotlin.whenever +import net.corda.core.cordapp.CordappProvider import net.corda.core.crypto.SecureHash import net.corda.core.internal.UpgradeCommand -import net.corda.testing.ALICE -import net.corda.testing.DUMMY_NOTARY +import net.corda.core.node.ServicesForResolution +import net.corda.testing.* import net.corda.testing.contracts.DummyContract import net.corda.testing.contracts.DummyContractV2 -import net.corda.testing.node.MockServices -import net.corda.testing.SerializationEnvironmentRule import org.junit.Rule import org.junit.Test import kotlin.test.assertEquals @@ -23,7 +24,9 @@ class DummyContractV2Tests { @Test fun `upgrade from v1`() { - val services = MockServices() + val services = rigorousMock().also { + doReturn(rigorousMock()).whenever(it).cordappProvider + } val contractUpgrade = DummyContractV2() val v1State = TransactionState(DummyContract.SingleOwnerState(0, ALICE), DummyContract.PROGRAM_ID, DUMMY_NOTARY, constraint = AlwaysAcceptAttachmentConstraint) val v1Ref = StateRef(SecureHash.randomSHA256(), 0) diff --git a/core/src/test/kotlin/net/corda/core/crypto/CompositeKeyTests.kt b/core/src/test/kotlin/net/corda/core/crypto/CompositeKeyTests.kt index 88724674f1..12ea44c72b 100644 --- a/core/src/test/kotlin/net/corda/core/crypto/CompositeKeyTests.kt +++ b/core/src/test/kotlin/net/corda/core/crypto/CompositeKeyTests.kt @@ -337,7 +337,7 @@ class CompositeKeyTests { val ca = X509Utilities.createSelfSignedCACertificate(caName, caKeyPair) // Sign the composite key with the self sign CA. - val compositeKeyCert = X509Utilities.createCertificate(CertificateType.IDENTITY, ca, caKeyPair, caName.copy(commonName = "CompositeKey"), compositeKey) + val compositeKeyCert = X509Utilities.createCertificate(CertificateType.WELL_KNOWN_IDENTITY, ca, caKeyPair, caName.copy(commonName = "CompositeKey"), compositeKey) // Store certificate to keystore. val keystorePath = tempFolder.root.toPath() / "keystore.jks" diff --git a/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt b/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt index 0c2a00331b..d38e0fa841 100644 --- a/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt @@ -129,7 +129,7 @@ class CollectSignaturesFlowTests { @Test fun `fails when not signed by initiator`() { val onePartyDummyContract = DummyContract.generateInitial(1337, notary, alice.ref(1)) - val miniCorpServices = MockServices(listOf("net.corda.testing.contracts"), MINI_CORP.name, MINI_CORP_KEY) + val miniCorpServices = MockServices(listOf("net.corda.testing.contracts"), rigorousMock(), MINI_CORP.name, MINI_CORP_KEY) val ptx = miniCorpServices.signInitialTransaction(onePartyDummyContract) val flow = aliceNode.services.startFlow(CollectSignaturesFlow(ptx, emptySet())) mockNet.runNetwork() diff --git a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt index a072d9ec34..9e51829bdc 100644 --- a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt +++ b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt @@ -20,14 +20,17 @@ import net.corda.node.internal.SecureCordaRPCOps import net.corda.node.internal.StartedNode import net.corda.node.services.Permissions.Companion.startFlow import net.corda.nodeapi.internal.config.User -import net.corda.testing.* +import net.corda.testing.ALICE_NAME +import net.corda.testing.BOB_NAME import net.corda.testing.contracts.DummyContract import net.corda.testing.contracts.DummyContractV2 -import net.corda.testing.internal.RPCDriverExposedDSLInterface +import net.corda.testing.internal.RPCDriverDSL import net.corda.testing.internal.rpcDriver import net.corda.testing.internal.rpcTestUser import net.corda.testing.internal.startRpcClient import net.corda.testing.node.MockNetwork +import net.corda.testing.singleIdentity +import net.corda.testing.startFlow import org.junit.After import org.junit.Before import org.junit.Test @@ -120,7 +123,7 @@ class ContractUpgradeFlowTest { check(bobNode) } - private fun RPCDriverExposedDSLInterface.startProxy(node: StartedNode<*>, user: User): CordaRPCOps { + private fun RPCDriverDSL.startProxy(node: StartedNode<*>, user: User): CordaRPCOps { return startRpcClient( rpcAddress = startRpcServer( rpcUser = user, diff --git a/core/src/test/kotlin/net/corda/core/serialization/TransactionSerializationTests.kt b/core/src/test/kotlin/net/corda/core/serialization/TransactionSerializationTests.kt index efa87a441f..bfdc34d30a 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/TransactionSerializationTests.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/TransactionSerializationTests.kt @@ -1,6 +1,7 @@ package net.corda.core.serialization import net.corda.core.contracts.* +import net.corda.core.crypto.generateKeyPair import net.corda.core.identity.AbstractParty import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.TransactionBuilder @@ -49,9 +50,8 @@ class TransactionSerializationTests { val inputState = StateAndRef(TransactionState(TestCash.State(depositRef, 100.POUNDS, MEGA_CORP), TEST_CASH_PROGRAM_ID, DUMMY_NOTARY), fakeStateRef) val outputState = TransactionState(TestCash.State(depositRef, 600.POUNDS, MEGA_CORP), TEST_CASH_PROGRAM_ID, DUMMY_NOTARY) val changeState = TransactionState(TestCash.State(depositRef, 400.POUNDS, MEGA_CORP), TEST_CASH_PROGRAM_ID, DUMMY_NOTARY) - - val megaCorpServices = MockServices(listOf("net.corda.core.serialization"), MEGA_CORP.name, MEGA_CORP_KEY) - val notaryServices = MockServices(listOf("net.corda.core.serialization"), DUMMY_NOTARY.name, DUMMY_NOTARY_KEY) + val megaCorpServices = MockServices(listOf("net.corda.core.serialization"), rigorousMock(), MEGA_CORP.name, MEGA_CORP_KEY) + val notaryServices = MockServices(listOf("net.corda.core.serialization"), rigorousMock(), DUMMY_NOTARY.name, DUMMY_NOTARY_KEY) lateinit var tx: TransactionBuilder @Before @@ -88,14 +88,14 @@ class TransactionSerializationTests { assertFailsWith(IllegalArgumentException::class) { stx.copy(sigs = emptyList()) } - + val DUMMY_KEY_2 = generateKeyPair() // If the signature was replaced in transit, we don't like it. assertFailsWith(SignatureException::class) { val tx2 = TransactionBuilder(DUMMY_NOTARY).withItems(inputState, outputState, changeState, Command(TestCash.Commands.Move(), DUMMY_KEY_2.public)) val ptx2 = notaryServices.signInitialTransaction(tx2) - val dummyServices = MockServices(DUMMY_KEY_2) + val dummyServices = MockServices(rigorousMock(), MEGA_CORP.name, DUMMY_KEY_2) val stx2 = dummyServices.addSignature(ptx2) stx.copy(sigs = stx2.sigs).verifyRequiredSignatures() diff --git a/core/src/test/kotlin/net/corda/core/serialization/UniquenessExceptionSerializationTest.kt b/core/src/test/kotlin/net/corda/core/serialization/UniquenessExceptionSerializationTest.kt index 8c431b4e20..d0da8a2bcb 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/UniquenessExceptionSerializationTest.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/UniquenessExceptionSerializationTest.kt @@ -2,9 +2,11 @@ package net.corda.core.serialization import net.corda.core.contracts.StateRef import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.generateKeyPair +import net.corda.core.identity.CordaX500Name +import net.corda.core.identity.Party import net.corda.core.node.services.UniquenessException import net.corda.core.node.services.UniquenessProvider -import net.corda.testing.DUMMY_PARTY import net.corda.testing.SerializationEnvironmentRule import org.junit.Rule import org.junit.Test @@ -19,7 +21,8 @@ class UniquenessExceptionSerializationTest { fun testSerializationRoundTrip() { val txhash = SecureHash.randomSHA256() val txHash2 = SecureHash.randomSHA256() - val stateHistory: Map = mapOf(StateRef(txhash, 0) to UniquenessProvider.ConsumingTx(txHash2, 1, DUMMY_PARTY)) + val dummyParty = Party(CordaX500Name("Dummy", "Madrid", "ES"), generateKeyPair().public) + val stateHistory: Map = mapOf(StateRef(txhash, 0) to UniquenessProvider.ConsumingTx(txHash2, 1, dummyParty)) val conflict = UniquenessProvider.Conflict(stateHistory) val instance = UniquenessException(conflict) diff --git a/core/src/test/kotlin/net/corda/core/transactions/CompatibleTransactionTests.kt b/core/src/test/kotlin/net/corda/core/transactions/CompatibleTransactionTests.kt index 7464126983..5461d280a1 100644 --- a/core/src/test/kotlin/net/corda/core/transactions/CompatibleTransactionTests.kt +++ b/core/src/test/kotlin/net/corda/core/transactions/CompatibleTransactionTests.kt @@ -15,6 +15,11 @@ import java.util.function.Predicate import kotlin.test.* class CompatibleTransactionTests { + private companion object { + val DUMMY_KEY_1 = generateKeyPair() + val DUMMY_KEY_2 = generateKeyPair() + } + @Rule @JvmField val testSerialization = SerializationEnvironmentRule() diff --git a/core/src/test/kotlin/net/corda/core/transactions/LedgerTransactionQueryTests.kt b/core/src/test/kotlin/net/corda/core/transactions/LedgerTransactionQueryTests.kt index eba531f13a..d9a8e9bc10 100644 --- a/core/src/test/kotlin/net/corda/core/transactions/LedgerTransactionQueryTests.kt +++ b/core/src/test/kotlin/net/corda/core/transactions/LedgerTransactionQueryTests.kt @@ -1,14 +1,15 @@ package net.corda.core.transactions +import com.nhaarman.mockito_kotlin.doReturn +import com.nhaarman.mockito_kotlin.whenever import net.corda.core.contracts.* +import net.corda.core.crypto.generateKeyPair import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party -import net.corda.testing.DUMMY_NOTARY -import net.corda.testing.SerializationEnvironmentRule +import net.corda.node.services.api.IdentityServiceInternal +import net.corda.testing.* import net.corda.testing.contracts.DummyContract -import net.corda.testing.dummyCommand import net.corda.testing.node.MockServices -import net.corda.testing.singleIdentity import org.junit.Before import org.junit.Rule import org.junit.Test @@ -21,7 +22,10 @@ class LedgerTransactionQueryTests { @Rule @JvmField val testSerialization = SerializationEnvironmentRule() - private val services: MockServices = MockServices() + private val keyPair = generateKeyPair() + private val services = MockServices(rigorousMock().also { + doReturn(null).whenever(it).partyFromKey(keyPair.public) + }, MEGA_CORP.name, keyPair) private val identity: Party = services.myInfo.singleIdentity() @Before diff --git a/core/src/test/kotlin/net/corda/core/transactions/TransactionTests.kt b/core/src/test/kotlin/net/corda/core/transactions/TransactionTests.kt index 17cc1bb769..c5a4976876 100644 --- a/core/src/test/kotlin/net/corda/core/transactions/TransactionTests.kt +++ b/core/src/test/kotlin/net/corda/core/transactions/TransactionTests.kt @@ -16,6 +16,11 @@ import kotlin.test.assertFailsWith import kotlin.test.assertNotEquals class TransactionTests { + private companion object { + val DUMMY_KEY_1 = generateKeyPair() + val DUMMY_KEY_2 = generateKeyPair() + } + @Rule @JvmField val testSerialization = SerializationEnvironmentRule() diff --git a/docs/source/_static/corda-cheat-sheet.pdf b/docs/source/_static/corda-cheat-sheet.pdf index b7c3b7d1cb..382a2d8005 100644 Binary files a/docs/source/_static/corda-cheat-sheet.pdf and b/docs/source/_static/corda-cheat-sheet.pdf differ diff --git a/docs/source/key-concepts-contract-constraints.rst b/docs/source/api-contract-constraints.rst similarity index 83% rename from docs/source/key-concepts-contract-constraints.rst rename to docs/source/api-contract-constraints.rst index 35db806193..6f7fad2ff4 100644 --- a/docs/source/key-concepts-contract-constraints.rst +++ b/docs/source/api-contract-constraints.rst @@ -1,15 +1,15 @@ -Contract Constraints -==================== +API: Contract Constraints +========================= A basic understanding of contract key concepts, which can be found :doc:`here `, is required reading for this page. Transaction states specify a constraint over the contract that will be used to verify it. For a transaction to be -valid, the verify() function associated with each state must run successfully. However, for this to be secure, it is -not sufficient to specify the verify() function by name as there may exist multiple different implementations with the -same method signature and enclosing class. Contract constraints solve this problem by allowing a contract developer to -constrain which verify() functions out of the universe of implementations can be used. -(ie the universe is everything that matches the signature and contract constraints restricts this universe to a subset.) +valid, the ``verify`` function associated with each state must run successfully. However, for this to be secure, it is +not sufficient to specify the ``verify`` function by name as there may exist multiple different implementations with +the same method signature and enclosing class. Contract constraints solve this problem by allowing a contract developer +to constrain which ``verify`` functions out of the universe of implementations can be used (i.e. the universe is +everything that matches the signature and contract constraints restricts this universe to a subset). A typical constraint is the hash of the CorDapp JAR that contains the contract and states but will in future releases include constraints that require specific signers of the JAR, or both the signer and the hash. Constraints can be @@ -20,12 +20,13 @@ constructs a ``TransactionState`` without specifying the constraint parameter a (``AutomaticHashConstraint``) is used. This default will be automatically resolved to a specific ``HashAttachmentConstraint`` that contains the hash of the attachment which contains the contract of that ``TransactionState``. This automatic resolution occurs when a ``TransactionBuilder`` is converted to a -``WireTransaction``. This reduces the boilerplate involved in finding a specific hash constraint when building a transaction. +``WireTransaction``. This reduces the boilerplate involved in finding a specific hash constraint when building a +transaction. It is possible to specify the constraint explicitly with any other class that implements the ``AttachmentConstraint`` interface. To specify a hash manually the ``HashAttachmentConstraint`` can be used and to not provide any constraint the ``AlwaysAcceptAttachmentConstraint`` can be used - though this is intended for testing only. An example below -shows how to construct a ``TransactionState`` with an explicitly specified hash constraint from within a flow; +shows how to construct a ``TransactionState`` with an explicitly specified hash constraint from within a flow: .. sourcecode:: java @@ -42,12 +43,11 @@ shows how to construct a ``TransactionState`` with an explicitly specified hash LedgerTransaction ltx = wtx.toLedgerTransaction(serviceHub) ltx.verify() // Verifies both the attachment constraints and contracts - This mechanism exists both for integrity and security reasons. It is important not to verify against the wrong contract, which could happen if the wrong version of the contract is attached. More importantly when resolving transaction chains there will, in a future release, be attachments loaded from the network into the attachment sandbox that are used -to verify the transaction chain. Ensuring the attachment used is the correct one ensures that the verification will -not be tamperable by providing a fake contract. +to verify the transaction chain. Ensuring the attachment used is the correct one ensures that the verification is +tamper-proof by providing a fake contract. CorDapps as attachments ----------------------- @@ -55,10 +55,10 @@ CorDapps as attachments CorDapp JARs (:doc:`cordapp-overview`) that are installed to the node and contain classes implementing the ``Contract`` interface are automatically loaded into the ``AttachmentStorage`` of a node at startup. -After CorDapps are loaded into the attachment store the node creates a link between contract classes and the -attachment that they were loaded from. This makes it possible to find the attachment for any given contract. -This is how the automatic resolution of attachments is done by the ``TransactionBuilder`` and how, when verifying -the constraints and contracts, attachments are associated with their respective contracts. +After CorDapps are loaded into the attachment store the node creates a link between contract classes and the attachment +that they were loaded from. This makes it possible to find the attachment for any given contract. This is how the +automatic resolution of attachments is done by the ``TransactionBuilder`` and how, when verifying the constraints and +contracts, attachments are associated with their respective contracts. Implementations --------------- @@ -95,7 +95,7 @@ to specify JAR URLs in the case that the CorDapp(s) involved in testing already MockNetwork/MockNode ******************** -The most simple way to ensure that a vanilla instance of a MockNode generates the correct CorDapps is to use the +The simplest way to ensure that a vanilla instance of a MockNode generates the correct CorDapps is to use the ``cordappPackages`` constructor parameter (Kotlin) or the ``setCordappPackages`` method on ``MockNetworkParameters`` (Java) when creating the MockNetwork. This will cause the ``AbstractNode`` to use the named packages as sources for CorDapps. All files within those packages will be zipped into a JAR and added to the attachment store and loaded as CorDapps by the diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 635ea69c5a..d17183d8d8 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -6,6 +6,9 @@ from the previous milestone release. UNRELEASED ---------- +* Exporting additional JMX metrics (artemis, hibernate statistics) and loading Jolokia agent at JVM startup when using + DriverDSL and/or cordformation node runner. + * Removed confusing property database.initDatabase, enabling its guarded behaviour with the dev-mode. In devMode Hibernate will try to create or update database schemas, otherwise it will expect relevant schemas to be present in the database (pre configured via DDL scripts or equivalent), and validate these are correct. diff --git a/docs/source/cheat-sheet.rst b/docs/source/cheat-sheet.rst index 9a69483f52..991a55a70d 100644 --- a/docs/source/cheat-sheet.rst +++ b/docs/source/cheat-sheet.rst @@ -4,6 +4,6 @@ Cheat sheet A "cheat sheet" summarizing the key Corda types. A PDF version is downloadable `here`_. .. image:: resources/cheatsheet.jpg -:width: 700px + :width: 700px .. _`here`: _static/corda-cheat-sheet.pdf \ No newline at end of file diff --git a/docs/source/corda-api.rst b/docs/source/corda-api.rst index bb973b0f05..21040ea968 100644 --- a/docs/source/corda-api.rst +++ b/docs/source/corda-api.rst @@ -9,6 +9,7 @@ The following are the core APIs that are used in the development of CorDapps: api-states api-persistence api-contracts + api-contract-constraints api-vault-query api-transactions api-flows diff --git a/docs/source/corda-configuration-file.rst b/docs/source/corda-configuration-file.rst index c7654fb3f6..3a8a7ebfcd 100644 --- a/docs/source/corda-configuration-file.rst +++ b/docs/source/corda-configuration-file.rst @@ -68,21 +68,28 @@ path to the node's base directory. .. note:: Longer term these keys will be managed in secure hardware devices. +:database: Database configuration: + + :serverNameTablePrefix: Prefix string to apply to all the database tables. The default is no prefix. + :transactionIsolationLevel: Transaction isolation level as defined by the ``TRANSACTION_`` constants in + ``java.sql.Connection``, but without the "TRANSACTION_" prefix. Defaults to REPEATABLE_READ. + :exportHibernateJMXStatistics: Whether to export Hibernate JMX statistics (caution: expensive run-time overhead) + :dataSourceProperties: This section is used to configure the jdbc connection and database driver used for the nodes persistence. Currently the defaults in ``/node/src/main/resources/reference.conf`` are as shown in the first example. This is currently the only configuration that has been tested, although in the future full support for other storage layers will be validated. :database: This section is used to configure JDBC and Hibernate related properties: - :initDatabase: Boolean on whether to initialise the database or just validate the schema. Defaults to true. - - :schema: (optional) some database providers require a schema name when generating DDL and SQL statements. - (the value is passed to Hibernate property 'hibernate.hbm2ddl.auto'). + :serverNameTablePrefix: Prefix string to apply to all the database tables. The default is no prefix. :transactionIsolationLevel: Transaction isolation level as defined by the ``TRANSACTION_`` constants in ``java.sql.Connection``, but without the "TRANSACTION_" prefix. Defaults to REPEATABLE_READ. - :serverNameTablePrefix: Prefix string to apply to all the database tables. The default is no prefix. + :exportHibernateJMXStatistics: Whether to export Hibernate JMX statistics (caution: expensive run-time overhead) + + :schema: (optional) some database providers require a schema name when generating DDL and SQL statements. + (the value is passed to Hibernate property 'hibernate.hbm2ddl.auto'). :messagingServerAddress: The address of the ArtemisMQ broker instance. If not provided the node will run one locally. @@ -169,8 +176,7 @@ path to the node's base directory. Each should be a string. Only the JARs in the directories are added, not the directories themselves. This is useful for including JDBC drivers and the like. e.g. ``jarDirs = [ 'lib' ]`` -:sshd: If provided, node will start internal SSH server which will provide a management shell. It uses the same credentials - and permissions as RPC subsystem. It has one required parameter. +:sshd: If provided, node will start internal SSH server which will provide a management shell. It uses the same credentials and permissions as RPC subsystem. It has one required parameter. :port: The port to start SSH server on @@ -184,3 +190,6 @@ path to the node's base directory. :privateKeyFile: Path to the private key file for SSH authentication. The private key must not have a passphrase. :publicKeyFile: Path to the public key file for SSH authentication. :sshPort: Port to be used for SSH connection, default ``22``. + +:exportJMXTo: If set to ``http``, will enable JMX metrics reporting via the Jolokia HTTP/JSON agent. + Default Jolokia access url is http://127.0.0.1:7005/jolokia/ \ No newline at end of file diff --git a/docs/source/corda-nodes-index.rst b/docs/source/corda-nodes-index.rst index c1dfa0b508..061902b1aa 100644 --- a/docs/source/corda-nodes-index.rst +++ b/docs/source/corda-nodes-index.rst @@ -10,6 +10,7 @@ Corda nodes corda-configuration-file clientrpc shell + node-auth-config node-database node-administration out-of-process-verification \ No newline at end of file diff --git a/docs/source/cordapp-build-systems.rst b/docs/source/cordapp-build-systems.rst index bfbe08662c..f2561429c0 100644 --- a/docs/source/cordapp-build-systems.rst +++ b/docs/source/cordapp-build-systems.rst @@ -115,19 +115,24 @@ is already correctly configured and this is for reference only; Creating the CorDapp JAR ------------------------ -The gradle ``jar`` task included in the CorDapp template build file will automatically build your CorDapp JAR correctly -as long as your dependencies are set correctly. +Once your dependencies are set correctly, you can build your CorDapp JAR using the gradle ``jar`` task: + +* Unix/Mac OSX: ``./gradlew jar`` + +* Windows: ``gradlew.bat jar`` + +The CorDapp JAR will be output to the ``build/libs`` folder. .. warning:: The hash of the generated CorDapp JAR is not deterministic, as it depends on variables such as the timestamp at creation. Nodes running the same CorDapp must therefore ensure they are using the exact same CorDapp - jar, and not different versions of the JAR created from identical sources. + JAR, and not different versions of the JAR created from identical sources. The filename of the JAR must include a unique identifier to deduplicate it from other releases of the same CorDapp. This is typically done by appending the version string to the CorDapp's name. This unique identifier should not change once the JAR has been deployed on a node. If it does, make sure no one is relying on ``FlowContext.appName`` in their flows (see :doc:`versioning`). -Installing the CorDapp jar +Installing the CorDapp JAR -------------------------- .. note:: Before installing a CorDapp, you must create one or more nodes to install it on. For instructions, please see @@ -135,7 +140,4 @@ Installing the CorDapp jar At runtime, nodes will load any CorDapps present in their ``cordapps`` folder. Therefore in order to install a CorDapp on a node, the CorDapp JAR must be added to the ``/cordapps/`` folder, where ``node_dir`` is the folder in which -the node's JAR and configuration files are stored. - -The ``deployNodes`` gradle task, if correctly configured, will automatically place your CorDapp JAR as well as any -dependent CorDapp JARs specified into the ``cordapps`` folder automatically. \ No newline at end of file +the node's JAR and configuration files are stored. \ No newline at end of file diff --git a/docs/source/generating-a-node.rst b/docs/source/generating-a-node.rst index afbde7127c..614c8084ec 100644 --- a/docs/source/generating-a-node.rst +++ b/docs/source/generating-a-node.rst @@ -23,7 +23,22 @@ into the ``cordapps`` folder. Node naming ----------- -A node's name must be a valid X.500 name that obeys the following additional constraints: +A node's name must be a valid X.500 distinguished name. In order to be compatible with other implementations +(particularly TLS implementations), we constrain the allowed X.500 attribute types to a subset of the minimum supported +set for X.509 certificates (specified in RFC 3280), plus the locality attribute: + +* Organization (O) +* State (ST) +* Locality (L) +* Country (C) +* Organizational-unit (OU) +* Common name (CN) (only used for service identities) + +The name must also obey the following constraints: + +* The organisation, locality and country attributes are present + + * The state, organisational-unit and common name attributes are optional * The fields of the name have the following maximum character lengths: @@ -33,21 +48,22 @@ A node's name must be a valid X.500 name that obeys the following additional con * Locality: 64 * State: 64 -* The country code is a valid ISO 3166-1 two letter code in upper-case - -* The organisation, locality and country attributes are present +* The country attribute is a valid ISO 3166-1 two letter code in upper-case * The organisation field of the name obeys the following constraints: + * Upper-case first letter * Has at least two letters * No leading or trailing whitespace * No double-spacing - * Upper-case first letter * Does not contain the words "node" or "server" - * Does not include the characters ',' or '=' or '$' or '"' or '\'' or '\\' + * Does not include the following characters: ``,`` , ``=`` , ``$`` , ``"`` , ``'`` , ``\`` * Is in NFKC normalization form * Only the latin, common and inherited unicode scripts are supported + * This is to avoid right-to-left issues, debugging issues when we can't pronounce names over the phone, and + character confusability attacks + The Cordform task ----------------- Corda provides a gradle plugin called ``Cordform`` that allows you to automatically generate and configure a set of diff --git a/docs/source/key-concepts-ecosystem.rst b/docs/source/key-concepts-ecosystem.rst index 2e2e900f64..872fb25b57 100644 --- a/docs/source/key-concepts-ecosystem.rst +++ b/docs/source/key-concepts-ecosystem.rst @@ -41,14 +41,4 @@ Nodes can provide several types of services: * One or more pluggable **notary services**. Notaries guarantee the uniqueness, and possibility the validity, of ledger updates. Each notary service may be run on a single node, or across a cluster of nodes. * Zero or more **oracle services**. An oracle is a well-known service that signs transactions if they state a fact and - that fact is considered to be true. - -These components are illustrated in the following diagram: - -.. image:: resources/cordaNetwork.png - :scale: 25% - :align: center - -In this diagram, Corda infrastructure services are those upon which all participants depend, such as the network map -and notary services. Corda services may be deployed by participants, third parties or a central network operator -(such as R3). The diagram is not intended to imply that only a centralised model is supported. \ No newline at end of file + that fact is considered to be true. \ No newline at end of file diff --git a/docs/source/key-concepts-identity.rst b/docs/source/key-concepts-identity.rst index de01ee8295..678c19273e 100644 --- a/docs/source/key-concepts-identity.rst +++ b/docs/source/key-concepts-identity.rst @@ -34,33 +34,6 @@ only shared with those who need to see them, and planned use of Intel SGX, it is privacy breaches. Confidential identities are used to ensure that even if a third party gets access to an unencrypted transaction, they cannot identify the participants without additional information. -Name ----- - -Identity names are X.500 distinguished names with Corda-specific constraints applied. In order to be compatible with -other implementations (particularly TLS implementations), we constrain the allowed X.500 attribute types to a subset of -the minimum supported set for X.509 certificates (specified in RFC 3280), plus the locality attribute: - -* organization (O) -* state (ST) -* locality (L) -* country (C) -* organizational-unit (OU) -* common name (CN) - used only for service identities - -The organisation, locality and country attributes are required, while state, organisational-unit and common name are -optional. Attributes cannot be be present more than once in the name. - -All of these attributes have the following set of constraints applied for security reasons: - - - No blacklisted words (currently "node" and "server"). - - Restrict names to Latin scripts for now to avoid right-to-left issues, debugging issues when we can't pronounce names over the phone, and character confusability attacks. - - No commas or equals signs. - - No dollars or quote marks. - -Additionally the "organisation" attribute must consist of at least three letters and starting with a capital letter, -and "country code" is strictly restricted to valid ISO 3166-1 two letter codes. - Certificates ------------ @@ -82,6 +55,4 @@ business sensitive details of transactions). In some cases nodes may also use pr to the main network map service, for operational reasons. Identities registered with such network maps must be considered well known, and it is never appropriate to store confidential identities in a central directory without controls applied at the record level to ensure only those who require access to an identity can retrieve its -certificate. - -.. TODO: Revisit once design & use cases of private maps is further fleshed out \ No newline at end of file +certificate. \ No newline at end of file diff --git a/docs/source/key-concepts.rst b/docs/source/key-concepts.rst index a252e1a853..902225e2b0 100644 --- a/docs/source/key-concepts.rst +++ b/docs/source/key-concepts.rst @@ -16,7 +16,6 @@ This section should be read in order: key-concepts-identity key-concepts-states key-concepts-contracts - key-concepts-contract-constraints key-concepts-transactions key-concepts-flows key-concepts-consensus diff --git a/docs/source/node-administration.rst b/docs/source/node-administration.rst index 7a677c34d2..fe9b9231fd 100644 --- a/docs/source/node-administration.rst +++ b/docs/source/node-administration.rst @@ -93,6 +93,8 @@ formats for accessing MBeans, and provides client libraries to work with that pr Here are a few ways to build dashboards and extract monitoring data for a node: +* `hawtio `_ is a web based console that connects directly to JVM's that have been instrumented with a + jolokia agent. This tool provides a nice JMX dashboard very similar to the traditional JVisualVM / JConsole MBbeans original. * `JMX2Graphite `_ is a tool that can be pointed to /monitoring/json and will scrape the statistics found there, then insert them into the Graphite monitoring tool on a regular basis. It runs in Docker and can be started with a single command. @@ -105,6 +107,29 @@ Here are a few ways to build dashboards and extract monitoring data for a node: It can bridge any data input to any output using their plugin system, for example, Telegraf can be configured to collect data from Jolokia and write to DataDog web api. +The Node configuration parameter `exportJMXTo` should be set to ``http`` to ensure a Jolokia agent is instrumented with +the JVM run-time. + +The following JMX statistics are exported: + +* Corda specific metrics: flow information (total started, finished, in-flight; flow duration by flow type), attachments (count) +* Apache Artemis metrics: queue information for P2P and RPC services +* JVM statistics: classloading, garbage collection, memory, runtime, threading, operating system +* Hibernate statistics (only when node is started-up in `devMode` due to to expensive run-time costs) + +When starting Corda nodes using Cordformation runner (see :doc:`running-a-node`), you should see a startup message similar to the following: +**Jolokia: Agent started with URL http://127.0.0.1:7005/jolokia/** + +When starting Corda nodes using the `DriverDSL`, you should see a startup message in the logs similar to the following: +**Starting out-of-process Node USA Bank Corp, debug port is not enabled, jolokia monitoring port is 7005 {}** + +Several Jolokia policy based security configuration files (``jolokia-access.xml``) are available for dev, test, and prod +environments under ``/config/``. + +The following diagram illustrates Corda flow metrics visualized using `hawtio `_ : + +.. image:: resources/hawtio-jmx.png + Memory usage and tuning ----------------------- diff --git a/docs/source/node-auth-config.rst b/docs/source/node-auth-config.rst new file mode 100644 index 0000000000..5f9c54c3e4 --- /dev/null +++ b/docs/source/node-auth-config.rst @@ -0,0 +1,136 @@ +Access security settings +======================== + +Access to node functionalities via SSH or RPC is protected by an authentication and authorisation policy. + +The field ``security`` in ``node.conf`` exposes various sub-fields related to authentication/authorisation specifying: + + * The data source providing credentials and permissions for users (e.g.: a remote RDBMS) + * An optional password encryption method. + * An optional caching of users data from Node side. + +.. warning:: Specifying both ``rpcUsers`` and ``security`` fields in ``node.conf`` is considered an illegal setting and + rejected by the node at startup since ``rpcUsers`` is effectively deprecated in favour of ``security.authService``. + +**Example 1:** connect to remote RDBMS for credentials/permissions, with encrypted user passwords and +caching on node-side: + +.. container:: codeset + + .. sourcecode:: groovy + + security = { + authService = { + dataSource = { + type = "DB", + passwordEncryption = "SHIRO_1_CRYPT", + connection = { + jdbcUrl = "" + username = "" + password = "" + driverClassName = "" + } + } + options = { + cache = { + expiryTimeSecs = 120 + capacity = 10000 + } + } + } + } + +**Example 2:** list of user credentials and permissions hard-coded in ``node.conf`` + +.. container:: codeset + + .. sourcecode:: groovy + + security = { + authService = { + dataSource = { + type = "INMEMORY", + users =[ + { + username = "user1" + password = "password" + permissions = [ + "StartFlow.net.corda.flows.ExampleFlow1", + "StartFlow.net.corda.flows.ExampleFlow2", + ... + ] + }, + ... + ] + } + } + } + +Let us look in more details at the structure of ``security.authService``: + +Authentication/authorisation data +--------------------------------- + +The ``dataSource`` field defines the data provider supplying credentials and permissions for users. The ``type`` +subfield identify the type of data provider, currently supported one are: + + * **INMEMORY:** a list of user credentials and permissions hard-coded in configuration in the ``users`` field + (see example 2 above) + + * **DB:** An external RDBMS accessed via the JDBC connection described by ``connection``. The current implementation + expect the database to store data according to the following schema: + + - Table ``users`` containing columns ``username`` and ``password``. + The ``username`` column *must have unique values*. + - Table ``user_roles`` containing columns ``username`` and ``role_name`` associating a user to a set of *roles* + - Table ``roles_permissions`` containing columns ``role_name`` and ``permission`` associating a role to a set of + permission strings + + Note in particular how in the DB case permissions are assigned to _roles_ rather than individual users. + Also, there is no prescription on the SQL type of the columns (although in our tests we defined ``username`` and + ``role_name`` of SQL type ``VARCHAR`` and ``password`` of ``TEXT`` type) and it is allowed to put additional columns + besides the one expected by the implementation. + +Password encryption +------------------- + +Storing passwords in plain text is discouraged in production systems aiming for high security requirements. We support +reading passwords stored using the Apache Shiro fully reversible Modular Crypt Format, specified in the documentation +of ``org.apache.shiro.crypto.hash.format.Shiro1CryptFormat``. + +Password are assumed in plain format by default. To specify an encryption it is necessary to use the field: + +.. container:: codeset + + .. sourcecode:: groovy + + passwordEncryption = SHIRO_1_CRYPT + +Hash encrypted password based on the Shiro1CryptFormat can be produced with the `Apache Shiro Hasher tool `_ + +Cache +----- + +Adding a cache layer on top of an external provider of users credentials and permissions can significantly benefit +performances in some cases, with the disadvantage of introducing a latency in the propagation of changes to the data. + +Caching of users data is disabled by default, it can be enabled by defining the ``options.cache`` field, like seen in +the examples above: + +.. container:: codeset + + .. sourcecode:: groovy + + options = { + cache = { + expiryTimeSecs = 120 + capacity = 10000 + } + } + +This will enable an in-memory cache with maximum capacity (number of entries) and maximum life time of entries given by +respectively the values set by the ``capacity`` and ``expiryTimeSecs`` fields. + + + + diff --git a/docs/source/resources/cheatsheet.jpg b/docs/source/resources/cheatsheet.jpg index 056b6db836..a2cc924688 100644 Binary files a/docs/source/resources/cheatsheet.jpg and b/docs/source/resources/cheatsheet.jpg differ diff --git a/docs/source/resources/cordaNetwork.png b/docs/source/resources/cordaNetwork.png deleted file mode 100644 index 907a0172ba..0000000000 Binary files a/docs/source/resources/cordaNetwork.png and /dev/null differ diff --git a/docs/source/resources/hawtio-jmx.png b/docs/source/resources/hawtio-jmx.png new file mode 100644 index 0000000000..42d4504830 Binary files /dev/null and b/docs/source/resources/hawtio-jmx.png differ diff --git a/finance/src/test/kotlin/net/corda/finance/contracts/CommercialPaperTests.kt b/finance/src/test/kotlin/net/corda/finance/contracts/CommercialPaperTests.kt index 25d65f3dab..fc02955c6a 100644 --- a/finance/src/test/kotlin/net/corda/finance/contracts/CommercialPaperTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/contracts/CommercialPaperTests.kt @@ -17,7 +17,6 @@ import net.corda.testing.contracts.VaultFiller import net.corda.testing.node.MockServices import net.corda.testing.node.MockServices.Companion.makeTestDatabaseAndMockServices import net.corda.testing.node.makeTestIdentityService -import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -224,19 +223,16 @@ class CommercialPaperTestsGeneric { private lateinit var aliceServices: MockServices private lateinit var aliceVaultService: VaultService private lateinit var alicesVault: Vault - - private val notaryServices = MockServices(DUMMY_NOTARY_KEY) - private val issuerServices = MockServices(DUMMY_CASH_ISSUER_KEY) - + private val notaryServices = MockServices(rigorousMock(), MEGA_CORP.name, DUMMY_NOTARY_KEY) + private val issuerServices = MockServices(listOf("net.corda.finance.contracts"), rigorousMock(), MEGA_CORP.name, DUMMY_CASH_ISSUER_KEY) private lateinit var moveTX: SignedTransaction - - // @Test - @Ignore - fun `issue move and then redeem`() = withTestSerialization { + @Test + fun `issue move and then redeem`() { val aliceDatabaseAndServices = makeTestDatabaseAndMockServices( listOf(ALICE_KEY), makeTestIdentityService(listOf(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, DUMMY_CASH_ISSUER_IDENTITY, DUMMY_NOTARY_IDENTITY)), - initialIdentityName = MEGA_CORP.name) + listOf("net.corda.finance.contracts"), + MEGA_CORP.name) val databaseAlice = aliceDatabaseAndServices.first aliceServices = aliceDatabaseAndServices.second aliceVaultService = aliceServices.vaultService @@ -248,7 +244,8 @@ class CommercialPaperTestsGeneric { val bigCorpDatabaseAndServices = makeTestDatabaseAndMockServices( listOf(BIG_CORP_KEY), makeTestIdentityService(listOf(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, DUMMY_CASH_ISSUER_IDENTITY, DUMMY_NOTARY_IDENTITY)), - initialIdentityName = MEGA_CORP.name) + listOf("net.corda.finance.contracts"), + MEGA_CORP.name) val databaseBigCorp = bigCorpDatabaseAndServices.first bigCorpServices = bigCorpDatabaseAndServices.second bigCorpVaultService = bigCorpServices.vaultService diff --git a/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt b/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt index bfec19b57d..bb027e880c 100644 --- a/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt @@ -1,5 +1,6 @@ package net.corda.finance.contracts.asset +import com.nhaarman.mockito_kotlin.* import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash import net.corda.core.crypto.generateKeyPair @@ -18,6 +19,7 @@ import net.corda.finance.utils.sumCash import net.corda.finance.utils.sumCashBy import net.corda.finance.utils.sumCashOrNull import net.corda.finance.utils.sumCashOrZero +import net.corda.node.services.api.IdentityServiceInternal import net.corda.node.services.vault.NodeVaultService import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.testing.* @@ -67,9 +69,11 @@ class CashTests { @Before fun setUp() { LogHelper.setLevel(NodeVaultService::class) - megaCorpServices = MockServices(listOf("net.corda.finance.contracts.asset"), MEGA_CORP.name, MEGA_CORP_KEY) - miniCorpServices = MockServices(listOf("net.corda.finance.contracts.asset"), MINI_CORP.name, MINI_CORP_KEY) - val notaryServices = MockServices(listOf("net.corda.finance.contracts.asset"), DUMMY_NOTARY.name, DUMMY_NOTARY_KEY) + megaCorpServices = MockServices(listOf("net.corda.finance.contracts.asset"), rigorousMock(), MEGA_CORP.name, MEGA_CORP_KEY) + miniCorpServices = MockServices(listOf("net.corda.finance.contracts.asset"), rigorousMock().also { + doNothing().whenever(it).justVerifyAndRegisterIdentity(argThat { name == MINI_CORP.name }) + }, MINI_CORP.name, MINI_CORP_KEY) + val notaryServices = MockServices(listOf("net.corda.finance.contracts.asset"), rigorousMock(), DUMMY_NOTARY.name, DUMMY_NOTARY_KEY) val databaseAndServices = makeTestDatabaseAndMockServices( listOf(generateKeyPair()), makeTestIdentityService(listOf(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, DUMMY_CASH_ISSUER_IDENTITY, DUMMY_NOTARY_IDENTITY)), @@ -502,10 +506,9 @@ class CashTests { private fun makeSpend(services: ServiceHub, amount: Amount, dest: AbstractParty): WireTransaction { val ourIdentity = services.myInfo.singleIdentityAndCert() - val changeIdentity = services.keyManagementService.freshKeyAndCert(ourIdentity, false) val tx = TransactionBuilder(DUMMY_NOTARY) database.transaction { - Cash.generateSpend(services, tx, amount, changeIdentity, dest) + Cash.generateSpend(services, tx, amount, ourIdentity, dest) } return tx.toWireTransaction(services) } @@ -601,11 +604,10 @@ class CashTests { @Test fun generateSimpleSpendWithParties() { - val changeIdentity = ourServices.keyManagementService.freshKeyAndCert(ourServices.myInfo.singleIdentityAndCert(), false) database.transaction { val tx = TransactionBuilder(DUMMY_NOTARY) - Cash.generateSpend(ourServices, tx, 80.DOLLARS, changeIdentity, ALICE, setOf(MINI_CORP)) + Cash.generateSpend(ourServices, tx, 80.DOLLARS, ourServices.myInfo.singleIdentityAndCert(), ALICE, setOf(MINI_CORP)) assertEquals(vaultStatesUnconsumed.elementAt(2).ref, tx.inputStates()[0]) } @@ -774,8 +776,9 @@ class CashTests { // Double spend. @Test fun chainCashDoubleSpendFailsWith() { - val mockService = MockServices(listOf("net.corda.finance.contracts.asset"), MEGA_CORP.name, MEGA_CORP_KEY) - + val mockService = MockServices(listOf("net.corda.finance.contracts.asset"), rigorousMock().also { + doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY) + }, MEGA_CORP.name, MEGA_CORP_KEY) ledger(mockService) { unverifiedTransaction { attachment(Cash.PROGRAM_ID) @@ -813,12 +816,11 @@ class CashTests { fun multiSpend() { val tx = TransactionBuilder(DUMMY_NOTARY) database.transaction { - val changeIdentity = ourServices.keyManagementService.freshKeyAndCert(ourServices.myInfo.singleIdentityAndCert(), false) val payments = listOf( PartyAndAmount(miniCorpAnonymised, 400.DOLLARS), PartyAndAmount(CHARLIE_ANONYMISED, 150.DOLLARS) ) - Cash.generateSpend(ourServices, tx, payments, changeIdentity) + Cash.generateSpend(ourServices, tx, payments, ourServices.myInfo.singleIdentityAndCert()) } val wtx = tx.toWireTransaction(ourServices) fun out(i: Int) = wtx.getOutput(i) as Cash.State diff --git a/finance/src/test/kotlin/net/corda/finance/contracts/asset/ObligationTests.kt b/finance/src/test/kotlin/net/corda/finance/contracts/asset/ObligationTests.kt index 22e7db887f..207955b0dd 100644 --- a/finance/src/test/kotlin/net/corda/finance/contracts/asset/ObligationTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/contracts/asset/ObligationTests.kt @@ -1,5 +1,7 @@ package net.corda.finance.contracts.asset +import com.nhaarman.mockito_kotlin.doReturn +import com.nhaarman.mockito_kotlin.whenever import net.corda.core.contracts.* import net.corda.core.crypto.NullKeys.NULL_PARTY import net.corda.core.crypto.SecureHash @@ -15,6 +17,7 @@ import net.corda.finance.* import net.corda.finance.contracts.Commodity import net.corda.finance.contracts.NetType import net.corda.finance.contracts.asset.Obligation.Lifecycle +import net.corda.node.services.api.IdentityServiceInternal import net.corda.testing.* import net.corda.testing.contracts.DummyContract import net.corda.testing.contracts.DummyState @@ -51,9 +54,13 @@ class ObligationTests { beneficiary = CHARLIE ) private val outState = inState.copy(beneficiary = AnonymousParty(BOB_PUBKEY)) - private val miniCorpServices = MockServices(listOf("net.corda.finance.contracts.asset"), MINI_CORP.name, MINI_CORP_KEY) - private val notaryServices = MockServices(DUMMY_NOTARY_KEY) - private val mockService = MockServices(listOf("net.corda.finance.contracts.asset")) + private val miniCorpServices = MockServices(listOf("net.corda.finance.contracts.asset"), rigorousMock(), MINI_CORP.name, MINI_CORP_KEY) + private val notaryServices = MockServices(rigorousMock(), MEGA_CORP.name, DUMMY_NOTARY_KEY) + private val mockService = MockServices(listOf("net.corda.finance.contracts.asset"), rigorousMock().also { + doReturn(null).whenever(it).partyFromKey(ALICE_PUBKEY) + doReturn(null).whenever(it).partyFromKey(BOB_PUBKEY) + doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY) + }, MEGA_CORP.name) private fun cashObligationTestRoots( group: LedgerDSL diff --git a/gradle-plugins/api-scanner/build.gradle b/gradle-plugins/api-scanner/build.gradle index 85f7b4d0f9..c178472d58 100644 --- a/gradle-plugins/api-scanner/build.gradle +++ b/gradle-plugins/api-scanner/build.gradle @@ -1,5 +1,6 @@ apply plugin: 'java' apply plugin: 'net.corda.plugins.publish-utils' +apply plugin: 'com.jfrog.artifactory' description "Generates a summary of the artifact's public API" diff --git a/gradle-plugins/build.gradle b/gradle-plugins/build.gradle index bfab124a56..1e04813546 100644 --- a/gradle-plugins/build.gradle +++ b/gradle-plugins/build.gradle @@ -7,11 +7,14 @@ buildscript { file("$projectDir/../constants.properties").withInputStream { constants.load(it) } // If you bump this version you must re-bootstrap the codebase. See the README for more information. - ext.gradle_plugins_version = constants.getProperty("gradlePluginsVersion") - ext.bouncycastle_version = constants.getProperty("bouncycastleVersion") - ext.typesafe_config_version = constants.getProperty("typesafeConfigVersion") - ext.jsr305_version = constants.getProperty("jsr305Version") - ext.kotlin_version = constants.getProperty("kotlinVersion") + ext { + gradle_plugins_version = constants.getProperty("gradlePluginsVersion") + bouncycastle_version = constants.getProperty("bouncycastleVersion") + typesafe_config_version = constants.getProperty("typesafeConfigVersion") + jsr305_version = constants.getProperty("jsr305Version") + kotlin_version = constants.getProperty("kotlinVersion") + artifactory_plugin_version = constants.getProperty('artifactoryPluginVersion') + } repositories { mavenLocal() @@ -22,10 +25,12 @@ buildscript { classpath "net.corda.plugins:publish-utils:$gradle_plugins_version" classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.7.3' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath "org.jfrog.buildinfo:build-info-extractor-gradle:$artifactory_plugin_version" } } apply plugin: 'net.corda.plugins.publish-utils' +apply plugin: 'com.jfrog.artifactory' allprojects { version gradle_plugins_version @@ -54,3 +59,25 @@ bintrayConfig { email = 'dev@corda.net' } } + +artifactory { + publish { + contextUrl = 'https://ci-artifactory.corda.r3cev.com/artifactory' + repository { + repoKey = 'corda-dev' + username = 'teamcity' + password = System.getenv('CORDA_ARTIFACTORY_PASSWORD') + } + + defaults { + // Publish utils does not have a publish block because it would be circular for it to apply it's own + // extensions to itself + if(project.name == 'publish-utils') { + publications('publishUtils') + // Root project applies the plugin (for this block) but does not need to be published + } else if(project != rootProject) { + publications(project.extensions.publish.name()) + } + } + } +} \ No newline at end of file diff --git a/gradle-plugins/cordapp/build.gradle b/gradle-plugins/cordapp/build.gradle index dc284faca1..3d1ecb6b53 100644 --- a/gradle-plugins/cordapp/build.gradle +++ b/gradle-plugins/cordapp/build.gradle @@ -1,5 +1,6 @@ apply plugin: 'kotlin' apply plugin: 'net.corda.plugins.publish-utils' +apply plugin: 'com.jfrog.artifactory' description 'Turns a project into a cordapp project that produces cordapp fat JARs' diff --git a/gradle-plugins/cordapp/src/main/kotlin/net/corda/plugins/Utils.kt b/gradle-plugins/cordapp/src/main/kotlin/net/corda/plugins/Utils.kt index d7200ba9f8..7572cd9876 100644 --- a/gradle-plugins/cordapp/src/main/kotlin/net/corda/plugins/Utils.kt +++ b/gradle-plugins/cordapp/src/main/kotlin/net/corda/plugins/Utils.kt @@ -23,6 +23,13 @@ class Utils { project.configurations.single { it.name == "compile" }.extendsFrom(configuration) } } + fun createRuntimeConfiguration(name: String, project: Project) { + if(!project.configurations.any { it.name == name }) { + val configuration = project.configurations.create(name) + configuration.isTransitive = false + project.configurations.single { it.name == "runtime" }.extendsFrom(configuration) + } + } } } \ No newline at end of file diff --git a/gradle-plugins/cordform-common/build.gradle b/gradle-plugins/cordform-common/build.gradle index 2423bcd773..3b9ab84805 100644 --- a/gradle-plugins/cordform-common/build.gradle +++ b/gradle-plugins/cordform-common/build.gradle @@ -1,6 +1,7 @@ apply plugin: 'java' apply plugin: 'maven-publish' apply plugin: 'net.corda.plugins.publish-utils' +apply plugin: 'com.jfrog.artifactory' repositories { mavenCentral() diff --git a/gradle-plugins/cordformation/build.gradle b/gradle-plugins/cordformation/build.gradle index 94eb8f3b7b..6c22f78a9e 100644 --- a/gradle-plugins/cordformation/build.gradle +++ b/gradle-plugins/cordformation/build.gradle @@ -10,6 +10,7 @@ buildscript { apply plugin: 'kotlin' apply plugin: 'net.corda.plugins.publish-utils' +apply plugin: 'com.jfrog.artifactory' description 'A small gradle plugin for adding some basic Quasar tasks and configurations to reduce build.gradle bloat.' diff --git a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordformation.kt b/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordformation.kt index c28c8b645e..4b722a7f03 100644 --- a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordformation.kt +++ b/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordformation.kt @@ -10,6 +10,8 @@ import java.io.File */ class Cordformation : Plugin { internal companion object { + const val CORDFORMATION_TYPE = "cordformationInternal" + /** * Gets a resource file from this plugin's JAR file. * @@ -31,5 +33,8 @@ class Cordformation : Plugin { override fun apply(project: Project) { Utils.createCompileConfiguration("cordapp", project) + Utils.createRuntimeConfiguration(CORDFORMATION_TYPE, project) + val jolokiaVersion = project.rootProject.ext("jolokia_version") + project.dependencies.add(CORDFORMATION_TYPE, "org.jolokia:jolokia-jvm:$jolokiaVersion:agent") } } diff --git a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Node.kt b/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Node.kt index c58b9c713c..a009df7c4d 100644 --- a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Node.kt +++ b/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Node.kt @@ -1,6 +1,8 @@ package net.corda.plugins -import com.typesafe.config.* +import com.typesafe.config.ConfigFactory +import com.typesafe.config.ConfigRenderOptions +import com.typesafe.config.ConfigValueFactory import net.corda.cordform.CordformNode import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x500.style.BCStyle @@ -90,6 +92,7 @@ class Node(private val project: Project) : CordformNode() { if (config.hasPath("webAddress")) { installWebserverJar() } + installAgentJar() installBuiltCordapp() installCordapps() installConfig() @@ -177,6 +180,29 @@ class Node(private val project: Project) : CordformNode() { } } + /** + * Installs the jolokia monitoring agent JAR to the node/drivers directory + */ + private fun installAgentJar() { + val jolokiaVersion = project.rootProject.ext("jolokia_version") + val agentJar = project.configuration("runtime").files { + (it.group == "org.jolokia") && + (it.name == "jolokia-jvm") && + (it.version == jolokiaVersion) + // TODO: revisit when classifier attribute is added. eg && (it.classifier = "agent") + }.first() // should always be the jolokia agent fat jar: eg. jolokia-jvm-1.3.7-agent.jar + project.logger.info("Jolokia agent jar: $agentJar") + if (agentJar.isFile) { + val driversDir = File(nodeDir, "drivers") + project.copy { + it.apply { + from(agentJar) + into(driversDir) + } + } + } + } + /** * Installs the configuration file to this node's directory and detokenises it. */ diff --git a/gradle-plugins/cordformation/src/noderunner/kotlin/net/corda/plugins/NodeRunner.kt b/gradle-plugins/cordformation/src/noderunner/kotlin/net/corda/plugins/NodeRunner.kt index 04b01654f3..94953584ab 100644 --- a/gradle-plugins/cordformation/src/noderunner/kotlin/net/corda/plugins/NodeRunner.kt +++ b/gradle-plugins/cordformation/src/noderunner/kotlin/net/corda/plugins/NodeRunner.kt @@ -22,6 +22,11 @@ private object debugPortAlloc { internal fun next() = basePort++ } +private object monitoringPortAlloc { + private var basePort = 7005 + internal fun next() = basePort++ +} + fun main(args: Array) { val startedProcesses = mutableListOf() val headless = GraphicsEnvironment.isHeadless() || args.contains(HEADLESS_FLAG) @@ -49,8 +54,9 @@ private abstract class JarType(private val jarName: String) { return null } val debugPort = debugPortAlloc.next() + val monitoringPort = monitoringPortAlloc.next() println("Starting $jarName in $dir on debug port $debugPort") - val process = (if (headless) ::HeadlessJavaCommand else ::TerminalWindowJavaCommand)(jarName, dir, debugPort, javaArgs, jvmArgs).start() + val process = (if (headless) ::HeadlessJavaCommand else ::TerminalWindowJavaCommand)(jarName, dir, debugPort, monitoringPort, javaArgs, jvmArgs).start() if (os == OS.MACOS) Thread.sleep(1000) return process } @@ -69,15 +75,23 @@ private abstract class JavaCommand( jarName: String, internal val dir: File, debugPort: Int?, + monitoringPort: Int?, internal val nodeName: String, init: MutableList.() -> Unit, args: List, jvmArgs: List ) { + private val jolokiaJar by lazy { + File("$dir/drivers").listFiles { _, filename -> + filename.matches("jolokia-jvm-.*-agent\\.jar$".toRegex()) + }.first().name + } + internal val command: List = mutableListOf().apply { add(getJavaPath()) addAll(jvmArgs) add("-Dname=$nodeName") null != debugPort && add("-Dcapsule.jvm.args=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=$debugPort") + null != monitoringPort && add("-Dcapsule.jvm.args=-javaagent:drivers/$jolokiaJar=port=$monitoringPort") add("-jar") add(jarName) init() @@ -89,14 +103,14 @@ private abstract class JavaCommand( internal abstract fun getJavaPath(): String } -private class HeadlessJavaCommand(jarName: String, dir: File, debugPort: Int?, args: List, jvmArgs: List) - : JavaCommand(jarName, dir, debugPort, dir.name, { add("--no-local-shell") }, args, jvmArgs) { +private class HeadlessJavaCommand(jarName: String, dir: File, debugPort: Int?, monitoringPort: Int?, args: List, jvmArgs: List) + : JavaCommand(jarName, dir, debugPort, monitoringPort, dir.name, { add("--no-local-shell") }, args, jvmArgs) { override fun processBuilder() = ProcessBuilder(command).redirectError(File("error.$nodeName.log")).inheritIO() override fun getJavaPath() = File(File(System.getProperty("java.home"), "bin"), "java").path } -private class TerminalWindowJavaCommand(jarName: String, dir: File, debugPort: Int?, args: List, jvmArgs: List) - : JavaCommand(jarName, dir, debugPort, "${dir.name}-$jarName", {}, args, jvmArgs) { +private class TerminalWindowJavaCommand(jarName: String, dir: File, debugPort: Int?, monitoringPort: Int?, args: List, jvmArgs: List) + : JavaCommand(jarName, dir, debugPort, monitoringPort, "${dir.name}-$jarName", {}, args, jvmArgs) { override fun processBuilder() = ProcessBuilder(when (os) { OS.MACOS -> { listOf("osascript", "-e", """tell app "Terminal" diff --git a/gradle-plugins/publish-utils/build.gradle b/gradle-plugins/publish-utils/build.gradle index fa302b9dc3..da9c659498 100644 --- a/gradle-plugins/publish-utils/build.gradle +++ b/gradle-plugins/publish-utils/build.gradle @@ -1,13 +1,17 @@ apply plugin: 'groovy' apply plugin: 'maven-publish' apply plugin: 'com.jfrog.bintray' +apply plugin: 'com.jfrog.artifactory' // Used for bootstrapping project buildscript { Properties constants = new Properties() file("../../constants.properties").withInputStream { constants.load(it) } - ext.gradle_plugins_version = constants.getProperty("gradlePluginsVersion") + ext { + gradle_plugins_version = constants.getProperty("gradlePluginsVersion") + artifactory_plugin_version = constants.getProperty('artifactoryPluginVersion') + } repositories { jcenter() @@ -15,6 +19,7 @@ buildscript { dependencies { classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.4' + classpath "org.jfrog.buildinfo:build-info-extractor-gradle:$artifactory_plugin_version" } } diff --git a/gradle-plugins/quasar-utils/build.gradle b/gradle-plugins/quasar-utils/build.gradle index 7829d47d75..8f9eca30f2 100644 --- a/gradle-plugins/quasar-utils/build.gradle +++ b/gradle-plugins/quasar-utils/build.gradle @@ -1,6 +1,7 @@ apply plugin: 'groovy' apply plugin: 'maven-publish' apply plugin: 'net.corda.plugins.publish-utils' +apply plugin: 'com.jfrog.artifactory' description 'A small gradle plugin for adding some basic Quasar tasks and configurations to reduce build.gradle bloat.' diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/ConfigUtilities.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/ConfigUtilities.kt index 33a5e237ff..53a9ab7ce2 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/ConfigUtilities.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/ConfigUtilities.kt @@ -80,6 +80,7 @@ private fun Config.getSingleValue(path: String, type: KType): Any? { URL::class -> URL(getString(path)) CordaX500Name::class -> CordaX500Name.parse(getString(path)) Properties::class -> getConfig(path).toProperties() + Config::class -> getConfig(path) else -> if (typeClass.java.isEnum) { parseEnum(typeClass.java, getString(path)) } else { diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/KeyStoreWrapper.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/KeyStoreWrapper.kt index 3e0d526a58..2552e38cbb 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/KeyStoreWrapper.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/KeyStoreWrapper.kt @@ -8,7 +8,6 @@ import java.security.KeyPair import java.security.PublicKey import java.security.cert.CertPath import java.security.cert.Certificate -import java.security.cert.CertificateFactory class KeyStoreWrapper(private val storePath: Path, private val storePassword: String) { private val keyStore = storePath.read { loadKeyStore(it, storePassword) } @@ -18,7 +17,7 @@ class KeyStoreWrapper(private val storePath: Path, private val storePassword: St // Assume key password = store password. val clientCA = certificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA) // Create new keys and store in keystore. - val cert = X509Utilities.createCertificate(CertificateType.IDENTITY, clientCA.certificate, clientCA.keyPair, serviceName, pubKey) + val cert = X509Utilities.createCertificate(CertificateType.WELL_KNOWN_IDENTITY, clientCA.certificate, clientCA.keyPair, serviceName, pubKey) val certPath = X509CertificateFactory().delegate.generateCertPath(listOf(cert.cert) + clientCertPath) require(certPath.certificates.isNotEmpty()) { "Certificate path cannot be empty" } // TODO: X509Utilities.validateCertificateChain() diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt index def4ee9879..b739a4bc5c 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt @@ -332,7 +332,7 @@ enum class CertificateType(val keyUsage: KeyUsage, vararg val purposes: KeyPurpo isCA = true ), - CLIENT_CA( + NODE_CA( KeyUsage(KeyUsage.digitalSignature or KeyUsage.keyCertSign or KeyUsage.cRLSign), KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth, @@ -349,12 +349,20 @@ enum class CertificateType(val keyUsage: KeyUsage, vararg val purposes: KeyPurpo ), // TODO: Identity certs should have only limited depth (i.e. 1) CA signing capability, with tight name constraints - IDENTITY( + WELL_KNOWN_IDENTITY( KeyUsage(KeyUsage.digitalSignature or KeyUsage.keyCertSign), KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth, KeyPurposeId.anyExtendedKeyUsage, isCA = true + ), + + CONFIDENTIAL_IDENTITY( + KeyUsage(KeyUsage.digitalSignature), + KeyPurposeId.id_kp_serverAuth, + KeyPurposeId.id_kp_clientAuth, + KeyPurposeId.anyExtendedKeyUsage, + isCA = false ) } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/CordaPersistence.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/CordaPersistence.kt index f05849212d..e029620dce 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/CordaPersistence.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/CordaPersistence.kt @@ -21,7 +21,8 @@ data class DatabaseConfig( val initialiseSchema: Boolean = true, val serverNameTablePrefix: String = "", val transactionIsolationLevel: TransactionIsolationLevel = TransactionIsolationLevel.REPEATABLE_READ, - val schema: String? = null + val schema: String? = null, + val exportHibernateJMXStatistics: Boolean = false ) // This class forms part of the node config and so any changes to it must be handled with care diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/HibernateConfiguration.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/HibernateConfiguration.kt index 7b603ae5b1..fc37580e61 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/HibernateConfiguration.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/HibernateConfiguration.kt @@ -17,8 +17,10 @@ import org.hibernate.type.AbstractSingleColumnStandardBasicType import org.hibernate.type.descriptor.java.PrimitiveByteArrayTypeDescriptor import org.hibernate.type.descriptor.sql.BlobTypeDescriptor import org.hibernate.type.descriptor.sql.VarbinaryTypeDescriptor +import java.lang.management.ManagementFactory import java.sql.Connection import java.util.concurrent.ConcurrentHashMap +import javax.management.ObjectName import javax.persistence.AttributeConverter class HibernateConfiguration( @@ -65,9 +67,31 @@ class HibernateConfiguration( val sessionFactory = buildSessionFactory(config, metadataSources, databaseConfig.serverNameTablePrefix) logger.info("Created session factory for schemas: $schemas") + + // export Hibernate JMX statistics + if (databaseConfig.exportHibernateJMXStatistics) + initStatistics(sessionFactory) + return sessionFactory } + // NOTE: workaround suggested to overcome deprecation of StatisticsService (since Hibernate v4.0) + // https://stackoverflow.com/questions/23606092/hibernate-upgrade-statisticsservice + fun initStatistics(sessionFactory: SessionFactory) { + val statsName = ObjectName("org.hibernate:type=statistics") + val mbeanServer = ManagementFactory.getPlatformMBeanServer() + + val statisticsMBean = DelegatingStatisticsService(sessionFactory.statistics) + statisticsMBean.isStatisticsEnabled = true + + try { + mbeanServer.registerMBean(statisticsMBean, statsName) + } + catch (e: Exception) { + logger.warn(e.message) + } + } + private fun buildSessionFactory(config: Configuration, metadataSources: MetadataSources, tablePrefix: String): SessionFactory { config.standardServiceRegistryBuilder.applySettings(config.properties) val metadata = metadataSources.getMetadataBuilder(config.standardServiceRegistryBuilder.build()).run { diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/HibernateStatistics.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/HibernateStatistics.kt new file mode 100644 index 0000000000..2c08d3e77f --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/HibernateStatistics.kt @@ -0,0 +1,227 @@ +package net.corda.nodeapi.internal.persistence + +import javax.management.MXBean + +import org.hibernate.stat.Statistics +import org.hibernate.stat.SecondLevelCacheStatistics +import org.hibernate.stat.QueryStatistics +import org.hibernate.stat.NaturalIdCacheStatistics +import org.hibernate.stat.EntityStatistics +import org.hibernate.stat.CollectionStatistics + +/** + * Exposes Hibernate [Statistics] contract as JMX resource. + */ +@MXBean +interface StatisticsService : Statistics + +/** + * Implements the MXBean interface by delegating through the actual [Statistics] implementation retrieved from the + * session factory. + */ +class DelegatingStatisticsService(private val delegate: Statistics) : StatisticsService { + + override fun clear() { + delegate.clear() + } + + override fun getCloseStatementCount(): Long { + return delegate.closeStatementCount + } + + override fun getCollectionFetchCount(): Long { + return delegate.collectionFetchCount + } + + override fun getCollectionLoadCount(): Long { + return delegate.collectionLoadCount + } + + override fun getCollectionRecreateCount(): Long { + return delegate.collectionRecreateCount + } + + override fun getCollectionRemoveCount(): Long { + return delegate.collectionRemoveCount + } + + override fun getCollectionRoleNames(): Array { + return delegate.collectionRoleNames + } + + override fun getCollectionStatistics(arg0: String): CollectionStatistics { + return delegate.getCollectionStatistics(arg0) + } + + override fun getCollectionUpdateCount(): Long { + return delegate.collectionUpdateCount + } + + override fun getConnectCount(): Long { + return delegate.connectCount + } + + override fun getEntityDeleteCount(): Long { + return delegate.entityDeleteCount + } + + override fun getEntityFetchCount(): Long { + return delegate.entityFetchCount + } + + override fun getEntityInsertCount(): Long { + return delegate.entityInsertCount + } + + override fun getEntityLoadCount(): Long { + return delegate.entityLoadCount + } + + override fun getEntityNames(): Array { + return delegate.entityNames + } + + override fun getEntityStatistics(arg0: String): EntityStatistics { + return delegate.getEntityStatistics(arg0) + } + + override fun getEntityUpdateCount(): Long { + return delegate.entityUpdateCount + } + + override fun getFlushCount(): Long { + return delegate.flushCount + } + + override fun getNaturalIdCacheHitCount(): Long { + return delegate.naturalIdCacheHitCount + } + + override fun getNaturalIdCacheMissCount(): Long { + return delegate.naturalIdCacheMissCount + } + + override fun getNaturalIdCachePutCount(): Long { + return delegate.naturalIdCachePutCount + } + + override fun getNaturalIdCacheStatistics(arg0: String): NaturalIdCacheStatistics { + return delegate.getNaturalIdCacheStatistics(arg0) + } + + override fun getNaturalIdQueryExecutionCount(): Long { + return delegate.naturalIdQueryExecutionCount + } + + override fun getNaturalIdQueryExecutionMaxTime(): Long { + return delegate.naturalIdQueryExecutionMaxTime + } + + override fun getNaturalIdQueryExecutionMaxTimeRegion(): String { + return delegate.naturalIdQueryExecutionMaxTimeRegion + } + + override fun getOptimisticFailureCount(): Long { + return delegate.optimisticFailureCount + } + + override fun getPrepareStatementCount(): Long { + return delegate.prepareStatementCount + } + + override fun getQueries(): Array { + return delegate.queries + } + + override fun getQueryCacheHitCount(): Long { + return delegate.queryCacheHitCount + } + + override fun getQueryCacheMissCount(): Long { + return delegate.queryCacheMissCount + } + + override fun getQueryCachePutCount(): Long { + return delegate.queryCachePutCount + } + + override fun getQueryExecutionCount(): Long { + return delegate.queryExecutionCount + } + + override fun getQueryExecutionMaxTime(): Long { + return delegate.queryExecutionMaxTime + } + + override fun getQueryExecutionMaxTimeQueryString(): String { + return delegate.queryExecutionMaxTimeQueryString + } + + override fun getQueryStatistics(arg0: String): QueryStatistics { + return delegate.getQueryStatistics(arg0) + } + + override fun getSecondLevelCacheHitCount(): Long { + return delegate.secondLevelCacheHitCount + } + + override fun getSecondLevelCacheMissCount(): Long { + return delegate.secondLevelCacheMissCount + } + + override fun getSecondLevelCachePutCount(): Long { + return delegate.secondLevelCachePutCount + } + + override fun getSecondLevelCacheRegionNames(): Array { + return delegate.secondLevelCacheRegionNames + } + + override fun getSecondLevelCacheStatistics(arg0: String): SecondLevelCacheStatistics { + return delegate.getSecondLevelCacheStatistics(arg0) + } + + override fun getSessionCloseCount(): Long { + return delegate.sessionCloseCount + } + + override fun getSessionOpenCount(): Long { + return delegate.sessionOpenCount + } + + override fun getStartTime(): Long { + return delegate.startTime + } + + override fun getSuccessfulTransactionCount(): Long { + return delegate.successfulTransactionCount + } + + override fun getTransactionCount(): Long { + return delegate.transactionCount + } + + override fun getUpdateTimestampsCacheHitCount(): Long { + return delegate.updateTimestampsCacheHitCount + } + + override fun getUpdateTimestampsCacheMissCount(): Long { + return delegate.updateTimestampsCacheMissCount + } + + override fun getUpdateTimestampsCachePutCount(): Long { + return delegate.updateTimestampsCachePutCount + } + + override fun isStatisticsEnabled(): Boolean { + return delegate.isStatisticsEnabled + } + + override fun logSummary() { + delegate.logSummary() + } + + override fun setStatisticsEnabled(arg0: Boolean) { + delegate.isStatisticsEnabled = arg0 + } +} \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolutionSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolutionSerializer.kt index ec8a61c793..88639d466b 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolutionSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolutionSerializer.kt @@ -27,9 +27,9 @@ import java.util.* * transformation rules we create a mapping between those values and the values that exist on the * current class * - * @property clazz The enum as it exists now, not as it did when it was serialized (either in the past + * @property type The enum as it exists now, not as it did when it was serialized (either in the past * or future). - * @property factory the [SerializerFactory] that is building this serialization object. + * @param factory the [SerializerFactory] that is building this serialization object. * @property conversions A mapping between all potential enum constants that could've been assigned to * an instance of the enum as it existed at time of serialisation and those that exist now * @property ordinals Convenience mapping of constant to ordinality @@ -57,7 +57,7 @@ class EnumEvolutionSerializer( * received AMQP header * @param new The Serializer object we built based on the current state of the enum class on our classpath * @param factory the [SerializerFactory] that is building this serialization object. - * @param transformsFromBlob the transforms attached to the class in the AMQP header, i.e. the transforms + * @param schemas the transforms attached to the class in the AMQP header, i.e. the transforms * known at serialization time */ fun make(old: RestrictedType, diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/PropertySerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/PropertySerializer.kt index 230a3eb4a8..d296a8fd91 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/PropertySerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/PropertySerializer.kt @@ -32,17 +32,16 @@ sealed class PropertySerializer(val name: String, val readMethod: Method?, val r return if (isInterface) listOf(SerializerFactory.nameForType(resolvedType)) else emptyList() } - private fun generateDefault(): String? { - if (isJVMPrimitive) { - return when (resolvedType) { - java.lang.Boolean.TYPE -> "false" - java.lang.Character.TYPE -> "�" - else -> "0" + private fun generateDefault(): String? = + if (isJVMPrimitive) { + when (resolvedType) { + java.lang.Boolean.TYPE -> "false" + java.lang.Character.TYPE -> "�" + else -> "0" + } + } else { + null } - } else { - return null - } - } private fun generateMandatory(): Boolean { return isJVMPrimitive || readMethod?.returnsNullable() == false diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/TansformTypes.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/TansformTypes.kt index 0003a48ba2..09c9be7746 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/TansformTypes.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/TansformTypes.kt @@ -28,7 +28,7 @@ enum class TransformTypes(val build: (Annotation) -> Transform) : DescribedType Unknown({ UnknownTransform() }) { override fun getDescriptor(): Any = DESCRIPTOR override fun getDescribed(): Any = ordinal - override fun validate(l : List, constants: Map) { } + override fun validate(list: List, constants: Map) {} }, EnumDefault({ a -> EnumDefaultSchemaTransform((a as CordaSerializationTransformEnumDefault).old, a.new) }) { override fun getDescriptor(): Any = DESCRIPTOR @@ -37,13 +37,13 @@ enum class TransformTypes(val build: (Annotation) -> Transform) : DescribedType /** * Validates a list of constant additions to an enumerated type. To be valid a default (the value * that should be used when we cannot use the new value) must refer to a constant that exists in the - * enum class as it exists now and it cannot refer to itself. + * enum class as it exists now and it cannot refer to itself. * - * @param l The list of transforms representing new constants and the mapping from that constant to an + * @param list The list of transforms representing new constants and the mapping from that constant to an * existing value * @param constants The list of enum constants on the type the transforms are being applied to */ - override fun validate(list : List, constants: Map) { + override fun validate(list: List, constants: Map) { uncheckedCast, List>(list).forEach { if (!constants.contains(it.new)) { throw NotSerializableException("Unknown enum constant ${it.new}") @@ -62,7 +62,7 @@ enum class TransformTypes(val build: (Annotation) -> Transform) : DescribedType if (constants[it.old]!! >= constants[it.new]!!) { throw NotSerializableException( "Enum extensions must default to older constants. ${it.new}[${constants[it.new]}] " + - "defaults to ${it.old}[${constants[it.old]}] which is greater") + "defaults to ${it.old}[${constants[it.old]}] which is greater") } } } @@ -76,15 +76,16 @@ enum class TransformTypes(val build: (Annotation) -> Transform) : DescribedType * that is a constant is renamed to something that used to exist in the enum. We do this for both * the same constant (i.e. C -> D -> C) and multiple constants (C->D, B->C) * - * @param l The list of transforms representing the renamed constants and the mapping between their new + * @param list The list of transforms representing the renamed constants and the mapping between their new * and old values * @param constants The list of enum constants on the type the transforms are being applied to */ - override fun validate(l : List, constants: Map) { + override fun validate(list: List, constants: Map) { object : Any() { - val from : MutableSet = mutableSetOf() - val to : MutableSet = mutableSetOf() }.apply { - @Suppress("UNCHECKED_CAST") (l as List).forEach { rename -> + val from: MutableSet = mutableSetOf() + val to: MutableSet = mutableSetOf() + }.apply { + @Suppress("UNCHECKED_CAST") (list as List).forEach { rename -> if (rename.to in this.to || rename.from in this.from) { throw NotSerializableException("Cyclic renames are not allowed (${rename.to})") } @@ -104,7 +105,7 @@ enum class TransformTypes(val build: (Annotation) -> Transform) : DescribedType //} ; - abstract fun validate(l: List, constants: Map) + abstract fun validate(list: List, constants: Map) companion object : DescribedTypeConstructor { val DESCRIPTOR = AMQPDescriptorRegistry.TRANSFORM_ELEMENT_KEY.amqpDescriptor diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/TransformsSchema.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/TransformsSchema.kt index 378675b84e..1d01f91f4f 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/TransformsSchema.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/TransformsSchema.kt @@ -92,7 +92,7 @@ class UnknownTestTransform(val a: Int, val b: Int, val c: Int) : Transform() { companion object : DescribedTypeConstructor { val typeName = "UnknownTest" - override fun newInstance(obj: Any?) : UnknownTestTransform { + override fun newInstance(obj: Any?): UnknownTestTransform { val described = obj as List<*> return UnknownTestTransform(described[1] as Int, described[2] as Int, described[3] as Int) } @@ -201,41 +201,41 @@ data class TransformsSchema(val types: Map>(TransformTypes::class.java) - try { - val clazz = sf.classloader.loadClass(name) + val transforms = EnumMap>(TransformTypes::class.java) + try { + val clazz = sf.classloader.loadClass(name) - supportedTransforms.forEach { transform -> - clazz.getAnnotation(transform.type)?.let { list -> - transform.getAnnotations(list).forEach { annotation -> - val t = transform.enum.build(annotation) + supportedTransforms.forEach { transform -> + clazz.getAnnotation(transform.type)?.let { list -> + transform.getAnnotations(list).forEach { annotation -> + val t = transform.enum.build(annotation) - // we're explicitly rejecting repeated annotations, whilst it's fine and we'd just - // ignore them it feels like a good thing to alert the user to since this is - // more than likely a typo in their code so best make it an actual error - if (transforms.computeIfAbsent(transform.enum) { mutableListOf() } - .filter { t == it } - .isNotEmpty()) { - throw NotSerializableException( - "Repeated unique transformation annotation of type ${t.name}") - } - - transforms[transform.enum]!!.add(t) + // we're explicitly rejecting repeated annotations, whilst it's fine and we'd just + // ignore them it feels like a good thing to alert the user to since this is + // more than likely a typo in their code so best make it an actual error + if (transforms.computeIfAbsent(transform.enum) { mutableListOf() } + .filter { t == it } + .isNotEmpty()) { + throw NotSerializableException( + "Repeated unique transformation annotation of type ${t.name}") } - transform.enum.validate( - transforms[transform.enum] ?: emptyList(), - clazz.enumConstants.mapIndexed { i, s -> Pair(s.toString(), i) }.toMap()) + transforms[transform.enum]!!.add(t) } - } - } catch (_: ClassNotFoundException) { - // if we can't load the class we'll end up caching an empty list which is fine as that - // list, on lookup, won't be included in the schema because it's empty - } - transforms + transform.enum.validate( + transforms[transform.enum] ?: emptyList(), + clazz.enumConstants.mapIndexed { i, s -> Pair(s.toString(), i) }.toMap()) + } + } + } catch (_: ClassNotFoundException) { + // if we can't load the class we'll end up caching an empty list which is fine as that + // list, on lookup, won't be included in the schema because it's empty } + transforms + } + private fun getAndAdd( type: String, sf: SerializerFactory, diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/ContractAttachmentSerializerTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/ContractAttachmentSerializerTest.kt index 0f9d2dd116..c542330ebd 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/ContractAttachmentSerializerTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/ContractAttachmentSerializerTest.kt @@ -2,7 +2,7 @@ package net.corda.nodeapi.internal.serialization import net.corda.core.contracts.ContractAttachment import net.corda.core.serialization.* -import net.corda.testing.SerializationEnvironmentRule +import net.corda.testing.* import net.corda.testing.contracts.DummyContract import net.corda.testing.node.MockServices import org.assertj.core.api.Assertions.assertThat @@ -22,9 +22,7 @@ class ContractAttachmentSerializerTest { private lateinit var factory: SerializationFactory private lateinit var context: SerializationContext private lateinit var contextWithToken: SerializationContext - - private val mockServices = MockServices() - + private val mockServices = MockServices(rigorousMock(), MEGA_CORP.name) @Before fun setup() { factory = testSerialization.env.serializationFactory diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolvabilityTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolvabilityTests.kt index 969db89a54..d18b970cfa 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolvabilityTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolvabilityTests.kt @@ -6,7 +6,6 @@ import org.assertj.core.api.Assertions import org.junit.Test import java.io.File import java.io.NotSerializableException -import java.net.URI import kotlin.test.assertEquals import kotlin.test.assertTrue @@ -47,7 +46,7 @@ class EnumEvolvabilityTests { @Test fun noAnnotation() { - data class C (val n: NotAnnotated) + data class C(val n: NotAnnotated) val sf = testDefaultFactory() val bAndS = TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(C(NotAnnotated.A)) @@ -63,7 +62,7 @@ class EnumEvolvabilityTests { @Test fun missingDefaults() { - data class C (val m: MissingDefaults) + data class C(val m: MissingDefaults) val sf = testDefaultFactory() val bAndS = TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(C(MissingDefaults.A)) @@ -74,7 +73,7 @@ class EnumEvolvabilityTests { @Test fun missingRenames() { - data class C (val m: MissingRenames) + data class C(val m: MissingRenames) val sf = testDefaultFactory() val bAndS = TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(C(MissingRenames.A)) @@ -86,7 +85,7 @@ class EnumEvolvabilityTests { @Test fun defaultAnnotationIsAddedToEnvelope() { - data class C (val annotatedEnum: AnnotatedEnumOnce) + data class C(val annotatedEnum: AnnotatedEnumOnce) val sf = testDefaultFactory() val bAndS = TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(C(AnnotatedEnumOnce.D)) @@ -94,45 +93,45 @@ class EnumEvolvabilityTests { // only the enum is decorated so schema sizes should be different (2 objects, only one evolved) assertEquals(2, bAndS.schema.types.size) assertEquals(1, bAndS.transformsSchema.types.size) - assertEquals (AnnotatedEnumOnce::class.java.name, bAndS.transformsSchema.types.keys.first()) + assertEquals(AnnotatedEnumOnce::class.java.name, bAndS.transformsSchema.types.keys.first()) val schema = bAndS.transformsSchema.types.values.first() assertEquals(1, schema.size) - assertTrue (schema.keys.contains(TransformTypes.EnumDefault)) - assertEquals (1, schema[TransformTypes.EnumDefault]!!.size) - assertTrue (schema[TransformTypes.EnumDefault]!![0] is EnumDefaultSchemaTransform) - assertEquals ("D", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).new) - assertEquals ("A", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).old) + assertTrue(schema.keys.contains(TransformTypes.EnumDefault)) + assertEquals(1, schema[TransformTypes.EnumDefault]!!.size) + assertTrue(schema[TransformTypes.EnumDefault]!![0] is EnumDefaultSchemaTransform) + assertEquals("D", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).new) + assertEquals("A", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).old) } @Test fun doubleDefaultAnnotationIsAddedToEnvelope() { - data class C (val annotatedEnum: AnnotatedEnumTwice) + data class C(val annotatedEnum: AnnotatedEnumTwice) val sf = testDefaultFactory() val bAndS = TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(C(AnnotatedEnumTwice.E)) assertEquals(2, bAndS.schema.types.size) assertEquals(1, bAndS.transformsSchema.types.size) - assertEquals (AnnotatedEnumTwice::class.java.name, bAndS.transformsSchema.types.keys.first()) + assertEquals(AnnotatedEnumTwice::class.java.name, bAndS.transformsSchema.types.keys.first()) val schema = bAndS.transformsSchema.types.values.first() assertEquals(1, schema.size) - assertTrue (schema.keys.contains(TransformTypes.EnumDefault)) - assertEquals (2, schema[TransformTypes.EnumDefault]!!.size) - assertTrue (schema[TransformTypes.EnumDefault]!![0] is EnumDefaultSchemaTransform) - assertEquals ("E", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).new) - assertEquals ("D", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).old) - assertTrue (schema[TransformTypes.EnumDefault]!![1] is EnumDefaultSchemaTransform) - assertEquals ("D", (schema[TransformTypes.EnumDefault]!![1] as EnumDefaultSchemaTransform).new) - assertEquals ("A", (schema[TransformTypes.EnumDefault]!![1] as EnumDefaultSchemaTransform).old) + assertTrue(schema.keys.contains(TransformTypes.EnumDefault)) + assertEquals(2, schema[TransformTypes.EnumDefault]!!.size) + assertTrue(schema[TransformTypes.EnumDefault]!![0] is EnumDefaultSchemaTransform) + assertEquals("E", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).new) + assertEquals("D", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).old) + assertTrue(schema[TransformTypes.EnumDefault]!![1] is EnumDefaultSchemaTransform) + assertEquals("D", (schema[TransformTypes.EnumDefault]!![1] as EnumDefaultSchemaTransform).new) + assertEquals("A", (schema[TransformTypes.EnumDefault]!![1] as EnumDefaultSchemaTransform).old) } @Test fun defaultAnnotationIsAddedToEnvelopeAndDeserialised() { - data class C (val annotatedEnum: AnnotatedEnumOnce) + data class C(val annotatedEnum: AnnotatedEnumOnce) val sf = testDefaultFactory() val sb = TestSerializationOutput(VERBOSE, sf).serialize(C(AnnotatedEnumOnce.D)) @@ -152,11 +151,11 @@ class EnumEvolvabilityTests { val schema = transforms[eName] - assertTrue (schema!!.keys.contains(TransformTypes.EnumDefault)) - assertEquals (1, schema[TransformTypes.EnumDefault]!!.size) - assertTrue (schema[TransformTypes.EnumDefault]!![0] is EnumDefaultSchemaTransform) - assertEquals ("D", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).new) - assertEquals ("A", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).old) + assertTrue(schema!!.keys.contains(TransformTypes.EnumDefault)) + assertEquals(1, schema[TransformTypes.EnumDefault]!!.size) + assertTrue(schema[TransformTypes.EnumDefault]!![0] is EnumDefaultSchemaTransform) + assertEquals("D", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).new) + assertEquals("A", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).old) } @Test @@ -174,9 +173,9 @@ class EnumEvolvabilityTests { val transforms = db.envelope.transformsSchema.types - assertTrue (transforms.contains(AnnotatedEnumTwice::class.java.name)) - assertTrue (transforms[AnnotatedEnumTwice::class.java.name]!!.contains(TransformTypes.EnumDefault)) - assertEquals (2, transforms[AnnotatedEnumTwice::class.java.name]!![TransformTypes.EnumDefault]!!.size) + assertTrue(transforms.contains(AnnotatedEnumTwice::class.java.name)) + assertTrue(transforms[AnnotatedEnumTwice::class.java.name]!!.contains(TransformTypes.EnumDefault)) + assertEquals(2, transforms[AnnotatedEnumTwice::class.java.name]!![TransformTypes.EnumDefault]!!.size) val enumDefaults = transforms[AnnotatedEnumTwice::class.java.name]!![TransformTypes.EnumDefault]!! @@ -188,7 +187,7 @@ class EnumEvolvabilityTests { @Test fun renameAnnotationIsAdded() { - data class C (val annotatedEnum: RenameEnumOnce) + data class C(val annotatedEnum: RenameEnumOnce) val sf = testDefaultFactory() @@ -197,7 +196,7 @@ class EnumEvolvabilityTests { assertEquals(2, bAndS.schema.types.size) assertEquals(1, bAndS.transformsSchema.types.size) - assertEquals (RenameEnumOnce::class.java.name, bAndS.transformsSchema.types.keys.first()) + assertEquals(RenameEnumOnce::class.java.name, bAndS.transformsSchema.types.keys.first()) val serialisedSchema = bAndS.transformsSchema.types[RenameEnumOnce::class.java.name]!! @@ -212,7 +211,7 @@ class EnumEvolvabilityTests { assertEquals(2, cAndS.envelope.schema.types.size) assertEquals(1, cAndS.envelope.transformsSchema.types.size) - assertEquals (RenameEnumOnce::class.java.name, cAndS.envelope.transformsSchema.types.keys.first()) + assertEquals(RenameEnumOnce::class.java.name, cAndS.envelope.transformsSchema.types.keys.first()) val deserialisedSchema = cAndS.envelope.transformsSchema.types[RenameEnumOnce::class.java.name]!! @@ -232,7 +231,7 @@ class EnumEvolvabilityTests { @Test fun doubleRenameAnnotationIsAdded() { - data class C (val annotatedEnum: RenameEnumTwice) + data class C(val annotatedEnum: RenameEnumTwice) val sf = testDefaultFactory() @@ -241,7 +240,7 @@ class EnumEvolvabilityTests { assertEquals(2, bAndS.schema.types.size) assertEquals(1, bAndS.transformsSchema.types.size) - assertEquals (RenameEnumTwice::class.java.name, bAndS.transformsSchema.types.keys.first()) + assertEquals(RenameEnumTwice::class.java.name, bAndS.transformsSchema.types.keys.first()) val serialisedSchema = bAndS.transformsSchema.types[RenameEnumTwice::class.java.name]!! @@ -258,7 +257,7 @@ class EnumEvolvabilityTests { assertEquals(2, cAndS.envelope.schema.types.size) assertEquals(1, cAndS.envelope.transformsSchema.types.size) - assertEquals (RenameEnumTwice::class.java.name, cAndS.envelope.transformsSchema.types.keys.first()) + assertEquals(RenameEnumTwice::class.java.name, cAndS.envelope.transformsSchema.types.keys.first()) val deserialisedSchema = cAndS.envelope.transformsSchema.types[RenameEnumTwice::class.java.name]!! @@ -271,15 +270,15 @@ class EnumEvolvabilityTests { assertEquals("F", (deserialisedSchema[TransformTypes.Rename]!![1] as RenameSchemaTransform).to) } - @CordaSerializationTransformRename(from="A", to="X") - @CordaSerializationTransformEnumDefault(old = "X", new="E") + @CordaSerializationTransformRename(from = "A", to = "X") + @CordaSerializationTransformEnumDefault(old = "X", new = "E") enum class RenameAndExtendEnum { X, B, C, D, E } @Test fun bothAnnotationTypes() { - data class C (val annotatedEnum: RenameAndExtendEnum) + data class C(val annotatedEnum: RenameAndExtendEnum) val sf = testDefaultFactory() @@ -288,15 +287,15 @@ class EnumEvolvabilityTests { assertEquals(2, bAndS.schema.types.size) assertEquals(1, bAndS.transformsSchema.types.size) - assertEquals (RenameAndExtendEnum::class.java.name, bAndS.transformsSchema.types.keys.first()) + assertEquals(RenameAndExtendEnum::class.java.name, bAndS.transformsSchema.types.keys.first()) val serialisedSchema = bAndS.transformsSchema.types[RenameAndExtendEnum::class.java.name]!! // This time there should be two distinct transform types (all previous tests have had only // a single type assertEquals(2, serialisedSchema.size) - assertTrue (serialisedSchema.containsKey(TransformTypes.Rename)) - assertTrue (serialisedSchema.containsKey(TransformTypes.EnumDefault)) + assertTrue(serialisedSchema.containsKey(TransformTypes.Rename)) + assertTrue(serialisedSchema.containsKey(TransformTypes.EnumDefault)) assertEquals(1, serialisedSchema[TransformTypes.Rename]!!.size) assertEquals("A", (serialisedSchema[TransformTypes.Rename]!![0] as RenameSchemaTransform).from) @@ -307,7 +306,7 @@ class EnumEvolvabilityTests { assertEquals("X", (serialisedSchema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).old) } - @CordaSerializationTransformEnumDefaults ( + @CordaSerializationTransformEnumDefaults( CordaSerializationTransformEnumDefault("D", "A"), CordaSerializationTransformEnumDefault("D", "A")) enum class RepeatedAnnotation { @@ -316,7 +315,7 @@ class EnumEvolvabilityTests { @Test fun repeatedAnnotation() { - data class C (val a: RepeatedAnnotation) + data class C(val a: RepeatedAnnotation) val sf = testDefaultFactory() @@ -330,40 +329,40 @@ class EnumEvolvabilityTests { A, B, C, D } - @CordaSerializationTransformEnumDefaults ( + @CordaSerializationTransformEnumDefaults( CordaSerializationTransformEnumDefault("D", "A"), CordaSerializationTransformEnumDefault("E", "A")) enum class E2 { A, B, C, D, E } - @CordaSerializationTransformEnumDefaults (CordaSerializationTransformEnumDefault("D", "A")) + @CordaSerializationTransformEnumDefaults(CordaSerializationTransformEnumDefault("D", "A")) enum class E3 { A, B, C, D } @Test fun multiEnums() { - data class A (val a: E1, val b: E2) - data class B (val a: E3, val b: A, val c: E1) - data class C (val a: B, val b: E2, val c: E3) + data class A(val a: E1, val b: E2) + data class B(val a: E3, val b: A, val c: E1) + data class C(val a: B, val b: E2, val c: E3) - val c = C(B(E3.A,A(E1.A,E2.B),E1.C),E2.B,E3.A) + val c = C(B(E3.A, A(E1.A, E2.B), E1.C), E2.B, E3.A) val sf = testDefaultFactory() // Serialise the object val bAndS = TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(c) - println (bAndS.transformsSchema) + println(bAndS.transformsSchema) // we have six types and three of those, the enums, should have transforms assertEquals(6, bAndS.schema.types.size) assertEquals(3, bAndS.transformsSchema.types.size) - assertTrue (E1::class.java.name in bAndS.transformsSchema.types) - assertTrue (E2::class.java.name in bAndS.transformsSchema.types) - assertTrue (E3::class.java.name in bAndS.transformsSchema.types) + assertTrue(E1::class.java.name in bAndS.transformsSchema.types) + assertTrue(E2::class.java.name in bAndS.transformsSchema.types) + assertTrue(E3::class.java.name in bAndS.transformsSchema.types) val e1S = bAndS.transformsSchema.types[E1::class.java.name]!! val e2S = bAndS.transformsSchema.types[E2::class.java.name]!! @@ -404,7 +403,7 @@ class EnumEvolvabilityTests { assertTrue(sf.transformsCache.containsKey(C2::class.java.name)) assertTrue(sf.transformsCache.containsKey(AnnotatedEnumOnce::class.java.name)) - assertEquals (sb1.transformsSchema.types[AnnotatedEnumOnce::class.java.name], + assertEquals(sb1.transformsSchema.types[AnnotatedEnumOnce::class.java.name], sb2.transformsSchema.types[AnnotatedEnumOnce::class.java.name]) } @@ -447,7 +446,7 @@ class EnumEvolvabilityTests { // // And we're not at 3. However, we ban this rename // - @CordaSerializationTransformRenames ( + @CordaSerializationTransformRenames( CordaSerializationTransformRename("D", "C"), CordaSerializationTransformRename("C", "D") ) @@ -455,7 +454,7 @@ class EnumEvolvabilityTests { @Test fun rejectCyclicRename() { - data class C (val e: RejectCyclicRename) + data class C(val e: RejectCyclicRename) val sf = testDefaultFactory() Assertions.assertThatThrownBy { @@ -468,7 +467,7 @@ class EnumEvolvabilityTests { // unserailzble. However, in this case, it isn't a struct cycle, rather one element // is renamed to match what a different element used to be called // - @CordaSerializationTransformRenames ( + @CordaSerializationTransformRenames( CordaSerializationTransformRename(from = "B", to = "C"), CordaSerializationTransformRename(from = "C", to = "D") ) @@ -476,7 +475,7 @@ class EnumEvolvabilityTests { @Test fun rejectCyclicRenameAlt() { - data class C (val e: RejectCyclicRenameAlt) + data class C(val e: RejectCyclicRenameAlt) val sf = testDefaultFactory() Assertions.assertThatThrownBy { @@ -484,7 +483,7 @@ class EnumEvolvabilityTests { }.isInstanceOf(NotSerializableException::class.java) } - @CordaSerializationTransformRenames ( + @CordaSerializationTransformRenames( CordaSerializationTransformRename("G", "C"), CordaSerializationTransformRename("F", "G"), CordaSerializationTransformRename("E", "F"), @@ -495,7 +494,7 @@ class EnumEvolvabilityTests { @Test fun rejectCyclicRenameRedux() { - data class C (val e: RejectCyclicRenameRedux) + data class C(val e: RejectCyclicRenameRedux) val sf = testDefaultFactory() Assertions.assertThatThrownBy { @@ -503,12 +502,12 @@ class EnumEvolvabilityTests { }.isInstanceOf(NotSerializableException::class.java) } - @CordaSerializationTransformEnumDefault (new = "D", old = "X") + @CordaSerializationTransformEnumDefault(new = "D", old = "X") enum class RejectBadDefault { A, B, C, D } @Test fun rejectBadDefault() { - data class C (val e: RejectBadDefault) + data class C(val e: RejectBadDefault) val sf = testDefaultFactory() Assertions.assertThatThrownBy { @@ -516,12 +515,12 @@ class EnumEvolvabilityTests { }.isInstanceOf(NotSerializableException::class.java) } - @CordaSerializationTransformEnumDefault (new = "D", old = "D") + @CordaSerializationTransformEnumDefault(new = "D", old = "D") enum class RejectBadDefaultToSelf { A, B, C, D } @Test fun rejectBadDefaultToSelf() { - data class C (val e: RejectBadDefaultToSelf) + data class C(val e: RejectBadDefaultToSelf) val sf = testDefaultFactory() Assertions.assertThatThrownBy { diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.kt index b2939d7fa9..bdcde9bf64 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolveTests.kt @@ -1,12 +1,13 @@ package net.corda.nodeapi.internal.serialization.amqp -import net.corda.core.serialization.* +import net.corda.core.serialization.CordaSerializationTransformEnumDefault +import net.corda.core.serialization.CordaSerializationTransformEnumDefaults +import net.corda.core.serialization.SerializedBytes import net.corda.testing.common.internal.ProjectStructure.projectRootDir import org.assertj.core.api.Assertions import org.junit.Test import java.io.File import java.io.NotSerializableException -import java.net.URI import kotlin.test.assertEquals // NOTE: To recreate the test files used by these tests uncomment the original test classes and comment @@ -30,7 +31,7 @@ class EnumEvolveTests { val resource = "${javaClass.simpleName}.${testName()}" val sf = testDefaultFactory() - data class C (val e : DeserializeNewerSetToUnknown) + data class C(val e: DeserializeNewerSetToUnknown) // Uncomment to re-generate test files // File(URI("$localPath/$resource")).writeBytes( @@ -40,7 +41,7 @@ class EnumEvolveTests { val obj = DeserializationInput(sf).deserialize(SerializedBytes(File(path.toURI()).readBytes())) - assertEquals (DeserializeNewerSetToUnknown.C, obj.e) + assertEquals(DeserializeNewerSetToUnknown.C, obj.e) } // Version of the class as it was serialised @@ -78,9 +79,9 @@ class EnumEvolveTests { // of the evolution code val obj3 = DeserializationInput(sf).deserialize(SerializedBytes(File(path3.toURI()).readBytes())) - assertEquals (DeserializeNewerSetToUnknown2.C, obj1.e) - assertEquals (DeserializeNewerSetToUnknown2.C, obj2.e) - assertEquals (DeserializeNewerSetToUnknown2.C, obj3.e) + assertEquals(DeserializeNewerSetToUnknown2.C, obj1.e) + assertEquals(DeserializeNewerSetToUnknown2.C, obj2.e) + assertEquals(DeserializeNewerSetToUnknown2.C, obj3.e) } @@ -149,7 +150,7 @@ class EnumEvolveTests { data class C(val e: DeserializeWithRename) // Uncomment to re-generate test files, needs to be done in three stages - val so = SerializationOutput(sf) + // val so = SerializationOutput(sf) // First change // File(URI("$localPath/$resource.1.AA")).writeBytes(so.serialize(C(DeserializeWithRename.AA)).bytes) // File(URI("$localPath/$resource.1.B")).writeBytes(so.serialize(C(DeserializeWithRename.B)).bytes) @@ -271,7 +272,7 @@ class EnumEvolveTests { data class C(val e: MultiOperations) // Uncomment to re-generate test files, needs to be done in three stages - val so = SerializationOutput(sf) + // val so = SerializationOutput(sf) // First change // File(URI("$localPath/$resource.1.A")).writeBytes(so.serialize(C(MultiOperations.A)).bytes) // File(URI("$localPath/$resource.1.B")).writeBytes(so.serialize(C(MultiOperations.B)).bytes) @@ -345,15 +346,15 @@ class EnumEvolveTests { Pair("$resource.5.G", MultiOperations.C)) fun load(l: List>) = l.map { - Pair (DeserializationInput(sf).deserialize(SerializedBytes( - File(EvolvabilityTests::class.java.getResource(it.first).toURI()).readBytes())), it.second) + Pair(DeserializationInput(sf).deserialize(SerializedBytes( + File(EvolvabilityTests::class.java.getResource(it.first).toURI()).readBytes())), it.second) } - load (stage1Resources).forEach { assertEquals(it.second, it.first.e) } - load (stage2Resources).forEach { assertEquals(it.second, it.first.e) } - load (stage3Resources).forEach { assertEquals(it.second, it.first.e) } - load (stage4Resources).forEach { assertEquals(it.second, it.first.e) } - load (stage5Resources).forEach { assertEquals(it.second, it.first.e) } + load(stage1Resources).forEach { assertEquals(it.second, it.first.e) } + load(stage2Resources).forEach { assertEquals(it.second, it.first.e) } + load(stage3Resources).forEach { assertEquals(it.second, it.first.e) } + load(stage4Resources).forEach { assertEquals(it.second, it.first.e) } + load(stage5Resources).forEach { assertEquals(it.second, it.first.e) } } @CordaSerializationTransformEnumDefault(old = "A", new = "F") @@ -363,7 +364,7 @@ class EnumEvolveTests { fun badNewValue() { val sf = testDefaultFactory() - data class C (val e : BadNewValue) + data class C(val e: BadNewValue) Assertions.assertThatThrownBy { SerializationOutput(sf).serialize(C(BadNewValue.A)) @@ -374,13 +375,13 @@ class EnumEvolveTests { CordaSerializationTransformEnumDefault(new = "D", old = "E"), CordaSerializationTransformEnumDefault(new = "E", old = "A") ) - enum class OutOfOrder { A, B, C, D, E} + enum class OutOfOrder { A, B, C, D, E } @Test fun outOfOrder() { val sf = testDefaultFactory() - data class C (val e : OutOfOrder) + data class C(val e: OutOfOrder) Assertions.assertThatThrownBy { SerializationOutput(sf).serialize(C(OutOfOrder.A)) diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumTests.kt index 1f20ac013f..60805c994f 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumTests.kt @@ -2,17 +2,14 @@ package net.corda.nodeapi.internal.serialization.amqp import net.corda.core.serialization.ClassWhitelist import net.corda.core.serialization.CordaSerializable -import org.junit.Test -import java.time.DayOfWeek - -import kotlin.test.assertEquals -import kotlin.test.assertNotNull - -import java.io.File -import java.io.NotSerializableException - import net.corda.core.serialization.SerializedBytes import org.assertj.core.api.Assertions +import org.junit.Test +import java.io.File +import java.io.NotSerializableException +import java.time.DayOfWeek +import kotlin.test.assertEquals +import kotlin.test.assertNotNull class EnumTests { enum class Bras { @@ -42,8 +39,8 @@ class EnumTests { //} // the new state, note in the test we serialised with value UNDERWIRE so the spacer - // occuring after this won't have changed the ordinality of our serialised value - // and thus should still be deserialisable + // occurring after this won't have changed the ordinality of our serialised value + // and thus should still be deserializable enum class OldBras2 { TSHIRT, UNDERWIRE, PUSHUP, SPACER, BRALETTE, SPACER2 } diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.kt index f092be08a0..ec15502c75 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.kt @@ -6,7 +6,6 @@ import net.corda.testing.common.internal.ProjectStructure.projectRootDir import org.junit.Test import java.io.File import java.io.NotSerializableException -import java.net.URI import kotlin.test.assertEquals // To regenerate any of the binary test files do the following @@ -19,13 +18,14 @@ import kotlin.test.assertEquals // 5. Comment back out the generation code and uncomment the actual test class EvolvabilityTests { // When regenerating the test files this needs to be set to the file system location of the resource files + @Suppress("UNUSED") var localPath = projectRootDir.toUri().resolve( "node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp") @Test fun simpleOrderSwapSameType() { val sf = testDefaultFactory() - val resource= "EvolvabilityTests.simpleOrderSwapSameType" + val resource = "EvolvabilityTests.simpleOrderSwapSameType" val A = 1 val B = 2 @@ -91,7 +91,7 @@ class EvolvabilityTests { assertEquals(A, deserializedC.a) assertEquals(null, deserializedC.b) - } + } @Test(expected = NotSerializableException::class) fun addAdditionalParam() { @@ -370,6 +370,7 @@ class EvolvabilityTests { // Add a parameter to inner but keep outer unchanged data class Inner(val a: Int, val b: String?) + data class Outer(val a: Int, val b: Inner) val path = EvolvabilityTests::class.java.getResource(resource) diff --git a/node/build.gradle b/node/build.gradle index ed39421af3..f6cbaf4eb6 100644 --- a/node/build.gradle +++ b/node/build.gradle @@ -44,6 +44,11 @@ sourceSets { // This prevents problems in IntelliJ with regard to duplicate source roots. processResources { from file("$rootDir/config/dev/log4j2.xml") + from file("$rootDir/config/dev/jolokia-access.xml") +} + +processTestResources { + from file("$rootDir/config/test/jolokia-access.xml") } // To find potential version conflicts, run "gradle htmlDependencyReport" and then look in @@ -163,6 +168,9 @@ dependencies { // FastClasspathScanner: classpath scanning compile 'io.github.lukehutch:fast-classpath-scanner:2.0.21' + // Apache Shiro: authentication, authorization and session management. + compile "org.apache.shiro:shiro-core:${shiro_version}" + // Jsh: A SSH implementation for tunneling inbound traffic via a relay compile group: 'com.jcraft', name: 'jsch', version: '0.1.54' @@ -183,6 +191,9 @@ dependencies { testCompile "org.glassfish.jersey.core:jersey-server:${jersey_version}" testCompile "org.glassfish.jersey.containers:jersey-container-servlet-core:${jersey_version}" testCompile "org.glassfish.jersey.containers:jersey-container-jetty-http:${jersey_version}" + + // Jolokia JVM monitoring agent + runtime "org.jolokia:jolokia-jvm:${jolokia_version}:agent" } task integrationTest(type: Test) { diff --git a/node/capsule/build.gradle b/node/capsule/build.gradle index ee486fbe86..d5c8e7906b 100644 --- a/node/capsule/build.gradle +++ b/node/capsule/build.gradle @@ -42,7 +42,7 @@ task buildCordaJAR(type: FatCapsule, dependsOn: project(':node').compileJava) { capsuleManifest { applicationVersion = corda_release_version - appClassPath = ["jolokia-agent-war-${project.rootProject.ext.jolokia_version}.war"] + appClassPath = ["jolokia-war-${project.rootProject.ext.jolokia_version}.war"] // See experimental/quasar-hook/README.md for how to generate. def quasarExcludeExpression = "x(antlr**;bftsmart**;ch**;co.paralleluniverse**;com.codahale**;com.esotericsoftware**;com.fasterxml**;com.google**;com.ibm**;com.intellij**;com.jcabi**;com.nhaarman**;com.opengamma**;com.typesafe**;com.zaxxer**;de.javakaffee**;groovy**;groovyjarjarantlr**;groovyjarjarasm**;io.atomix**;io.github**;io.netty**;jdk**;joptsimple**;junit**;kotlin**;net.bytebuddy**;net.i2p**;org.apache**;org.assertj**;org.bouncycastle**;org.codehaus**;org.crsh**;org.dom4j**;org.fusesource**;org.h2**;org.hamcrest**;org.hibernate**;org.jboss**;org.jcp**;org.joda**;org.junit**;org.mockito**;org.objectweb**;org.objenesis**;org.slf4j**;org.w3c**;org.xml**;org.yaml**;reflectasm**;rx**)" javaAgents = ["quasar-core-${quasar_version}-jdk8.jar=${quasarExcludeExpression}"] diff --git a/node/src/integration-test/kotlin/net/corda/node/NodeKeystoreCheckTest.kt b/node/src/integration-test/kotlin/net/corda/node/NodeKeystoreCheckTest.kt new file mode 100644 index 0000000000..ca1a3e9792 --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/node/NodeKeystoreCheckTest.kt @@ -0,0 +1,63 @@ +package net.corda.node + +import net.corda.core.crypto.Crypto +import net.corda.core.identity.CordaX500Name +import net.corda.core.internal.cert +import net.corda.core.internal.div +import net.corda.core.utilities.getOrThrow +import net.corda.node.services.config.configureDevKeyAndTrustStores +import net.corda.nodeapi.internal.config.SSLConfiguration +import net.corda.nodeapi.internal.crypto.* +import net.corda.testing.ALICE_NAME +import net.corda.testing.driver.driver +import org.junit.Test +import java.nio.file.Path +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue + +class NodeKeystoreCheckTest { + @Test + fun `node should throw exception if cert path doesn't chain to the trust root`() { + driver(startNodesInProcess = true) { + // This will fail because there are no keystore configured. + assertFailsWith(IllegalArgumentException::class) { + startNode(customOverrides = mapOf("devMode" to false)).getOrThrow() + }.apply { + assertTrue(message?.startsWith("Identity certificate not found. ") ?: false) + } + + // Create keystores + val keystorePassword = "password" + val config = object : SSLConfiguration { + override val keyStorePassword: String = keystorePassword + override val trustStorePassword: String = keystorePassword + override val certificatesDirectory: Path = baseDirectory(ALICE_NAME) / "certificates" + } + config.configureDevKeyAndTrustStores(ALICE_NAME) + + // This should pass with correct keystore. + val node = startNode(providedName = ALICE_NAME, customOverrides = mapOf("devMode" to false, + "keyStorePassword" to keystorePassword, + "trustStorePassword" to keystorePassword)).get() + node.stop() + + // Fiddle with node keystore. + val keystore = loadKeyStore(config.nodeKeystore, config.keyStorePassword) + + // Self signed root + val badRootKeyPair = Crypto.generateKeyPair() + val badRoot = X509Utilities.createSelfSignedCACertificate(CordaX500Name("Bad Root", "Lodnon", "GB"), badRootKeyPair) + val nodeCA = keystore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA, config.keyStorePassword) + val badNodeCACert = X509Utilities.createCertificate(CertificateType.NODE_CA, badRoot, badRootKeyPair, ALICE_NAME, nodeCA.keyPair.public) + keystore.setKeyEntry(X509Utilities.CORDA_CLIENT_CA, nodeCA.keyPair.private, config.keyStorePassword.toCharArray(), arrayOf(badNodeCACert.cert, badRoot.cert)) + keystore.save(config.nodeKeystore, config.keyStorePassword) + + assertFailsWith(IllegalArgumentException::class) { + startNode(providedName = ALICE_NAME, customOverrides = mapOf("devMode" to false)).getOrThrow() + }.apply { + assertEquals("Client CA certificate must chain to the trusted root.", message) + } + } + } +} diff --git a/node/src/integration-test/kotlin/net/corda/node/NodePerformanceTests.kt b/node/src/integration-test/kotlin/net/corda/node/NodePerformanceTests.kt index 0e507160cd..91e7e22be1 100644 --- a/node/src/integration-test/kotlin/net/corda/node/NodePerformanceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/NodePerformanceTests.kt @@ -19,6 +19,7 @@ import net.corda.testing.* import net.corda.testing.driver.NodeHandle import net.corda.testing.driver.PortAllocation import net.corda.testing.driver.driver +import net.corda.testing.internal.InternalDriverDSL import net.corda.testing.internal.performance.div import net.corda.testing.internal.performance.startPublishingFixedRateInjector import net.corda.testing.internal.performance.startReporter @@ -100,7 +101,7 @@ class NodePerformanceTests : IntegrationTest() { driver(startNodesInProcess = true) { val a = startNode(rpcUsers = listOf(User("A", "A", setOf(startFlow())))).get() a as NodeHandle.InProcess - val metricRegistry = startReporter(shutdownManager, a.node.services.monitoringService.metrics) + val metricRegistry = startReporter((this as InternalDriverDSL).shutdownManager, a.node.services.monitoringService.metrics) a.rpcClientToNode().use("A", "A") { connection -> startPublishingFixedRateInjector(metricRegistry, 1, 5.minutes, 2000L / TimeUnit.SECONDS) { connection.proxy.startFlow(::EmptyFlow).returnValue.get() @@ -133,7 +134,7 @@ class NodePerformanceTests : IntegrationTest() { portAllocation = PortAllocation.Incremental(20000) ) { val notary = defaultNotaryNode.getOrThrow() as NodeHandle.InProcess - val metricRegistry = startReporter(shutdownManager, notary.node.services.monitoringService.metrics) + val metricRegistry = startReporter((this as InternalDriverDSL).shutdownManager, notary.node.services.monitoringService.metrics) notary.rpcClientToNode().use("A", "A") { connection -> println("ISSUING") val doneFutures = (1..100).toList().map { diff --git a/node/src/integration-test/kotlin/net/corda/node/SSHServerTest.kt b/node/src/integration-test/kotlin/net/corda/node/SSHServerTest.kt index 1b0f596928..853af7cab9 100644 --- a/node/src/integration-test/kotlin/net/corda/node/SSHServerTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/SSHServerTest.kt @@ -23,6 +23,7 @@ import kotlin.test.fail import org.assertj.core.api.Assertions.assertThat import org.junit.ClassRule import java.util.regex.Pattern +import kotlin.reflect.jvm.jvmName class SSHServerTest : IntegrationTest() { companion object { @@ -120,7 +121,7 @@ class SSHServerTest : IntegrationTest() { channel.disconnect() session.disconnect() - assertThat(response).matches("(?s)User not permissioned with any of \\[[^]]*${flowNameEscaped}.*") + assertThat(response).matches("(?s)User not authorized to perform RPC call .*") } } diff --git a/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt index 327cc9175a..0c34ecca2a 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt @@ -20,14 +20,15 @@ import net.corda.core.utilities.contextLogger import net.corda.core.utilities.getOrThrow import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.internal.cordapp.CordappProviderImpl -import net.corda.testing.* import net.corda.testing.DUMMY_BANK_A import net.corda.testing.DUMMY_NOTARY import net.corda.testing.IntegrationTest -import net.corda.testing.driver.DriverDSLExposedInterface +import net.corda.testing.driver.DriverDSL import net.corda.testing.driver.NodeHandle import net.corda.testing.driver.driver import net.corda.testing.node.MockAttachmentStorage +import net.corda.testing.rigorousMock +import net.corda.testing.withTestSerialization import org.junit.Assert.assertEquals import org.junit.ClassRule import org.junit.Test @@ -57,16 +58,16 @@ class AttachmentLoadingTests : IntegrationTest() { Class.forName("net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Initiator", true, URLClassLoader(arrayOf(isolatedJAR))) .asSubclass(FlowLogic::class.java) - private fun DriverDSLExposedInterface.createTwoNodes(): List { + private fun DriverDSL.createTwoNodes(): List { return listOf( startNode(providedName = bankAName), startNode(providedName = bankBName) ).transpose().getOrThrow() } - private fun DriverDSLExposedInterface.installIsolatedCordappTo(nodeName: CordaX500Name) { + private fun DriverDSL.installIsolatedCordappTo(nodeName: CordaX500Name) { // Copy the app jar to the first node. The second won't have it. - val path = (baseDirectory(nodeName.toString()) / "cordapps").createDirectories() / "isolated.jar" + val path = (baseDirectory(nodeName) / "cordapps").createDirectories() / "isolated.jar" logger.info("Installing isolated jar to $path") isolatedJAR.openStream().buffered().use { input -> Files.newOutputStream(path).buffered().use { output -> diff --git a/node/src/integration-test/kotlin/net/corda/node/services/UserAuthServiceTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/UserAuthServiceTests.kt new file mode 100644 index 0000000000..75b86c7d94 --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/node/services/UserAuthServiceTests.kt @@ -0,0 +1,303 @@ +package net.corda.node.services + +import co.paralleluniverse.fibers.Suspendable +import net.corda.client.rpc.CordaRPCClient +import net.corda.client.rpc.PermissionException +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.InitiatingFlow +import net.corda.core.flows.StartableByRPC +import net.corda.core.messaging.CordaRPCOps +import net.corda.core.messaging.startFlow +import net.corda.finance.flows.CashIssueFlow +import net.corda.node.internal.Node +import net.corda.node.internal.StartedNode +import net.corda.node.services.config.PasswordEncryption +import net.corda.node.services.config.SecurityConfiguration +import net.corda.node.services.config.AuthDataSourceType +import net.corda.nodeapi.internal.config.User +import net.corda.nodeapi.internal.config.toConfig +import net.corda.testing.internal.NodeBasedTest +import net.corda.testing.* +import org.apache.activemq.artemis.api.core.ActiveMQSecurityException +import org.junit.After +import org.junit.Before +import org.junit.Test +import java.sql.DriverManager +import java.sql.Statement +import java.util.* +import kotlin.test.assertFailsWith + +abstract class UserAuthServiceTest : NodeBasedTest() { + + protected lateinit var node: StartedNode + protected lateinit var client: CordaRPCClient + + @Test + fun `login with correct credentials`() { + client.start("user", "foo") + } + + @Test + fun `login with wrong credentials`() { + client.start("user", "foo") + assertFailsWith( + ActiveMQSecurityException::class, + "Login with incorrect password should fail") { + client.start("user", "bar") + } + assertFailsWith( + ActiveMQSecurityException::class, + "Login with unknown username should fail") { + client.start("X", "foo") + } + } + + @Test + fun `check flow permissions are respected`() { + client.start("user", "foo").use { + val proxy = it.proxy + proxy.startFlowDynamic(DummyFlow::class.java) + proxy.startTrackedFlowDynamic(DummyFlow::class.java) + proxy.startFlow(::DummyFlow) + assertFailsWith( + PermissionException::class, + "This user should not be authorized to start flow `CashIssueFlow`") { + proxy.startFlowDynamic(CashIssueFlow::class.java) + } + assertFailsWith( + PermissionException::class, + "This user should not be authorized to start flow `CashIssueFlow`") { + proxy.startTrackedFlowDynamic(CashIssueFlow::class.java) + } + } + } + + @Test + fun `check permissions on RPC calls are respected`() { + client.start("user", "foo").use { + val proxy = it.proxy + proxy.stateMachinesFeed() + assertFailsWith( + PermissionException::class, + "This user should not be authorized to call 'nodeInfo'") { + proxy.nodeInfo() + } + } + } + + @StartableByRPC + @InitiatingFlow + class DummyFlow : FlowLogic() { + @Suspendable + override fun call() = Unit + } +} + +class UserAuthServiceEmbedded : UserAuthServiceTest() { + + private val rpcUser = User("user", "foo", permissions = setOf( + Permissions.startFlow(), + Permissions.invokeRpc("vaultQueryBy"), + Permissions.invokeRpc(CordaRPCOps::stateMachinesFeed), + Permissions.invokeRpc("vaultQueryByCriteria"))) + + @Before + fun setup() { + val securityConfig = SecurityConfiguration( + authService = SecurityConfiguration.AuthService.fromUsers(listOf(rpcUser))) + + val configOverrides = mapOf("security" to securityConfig.toConfig().root().unwrapped()) + node = startNode(ALICE_NAME, rpcUsers = emptyList(), configOverrides = configOverrides) + client = CordaRPCClient(node.internals.configuration.rpcAddress!!) + } +} + +class UserAuthServiceTestsJDBC : UserAuthServiceTest() { + + private val db = UsersDB( + name = "SecurityDataSourceTestDB", + users = listOf(UserAndRoles(username = "user", + password = "foo", + roles = listOf("default"))), + roleAndPermissions = listOf( + RoleAndPermissions( + role = "default", + permissions = listOf( + Permissions.startFlow(), + Permissions.invokeRpc("vaultQueryBy"), + Permissions.invokeRpc(CordaRPCOps::stateMachinesFeed), + Permissions.invokeRpc("vaultQueryByCriteria"))), + RoleAndPermissions( + role = "admin", + permissions = listOf("ALL") + ))) + + @Before + fun setup() { + val securityConfig = SecurityConfiguration( + authService = SecurityConfiguration.AuthService( + dataSource = SecurityConfiguration.AuthService.DataSource( + type = AuthDataSourceType.DB, + passwordEncryption = PasswordEncryption.NONE, + connection = Properties().apply { + setProperty("jdbcUrl", db.jdbcUrl) + setProperty("username", "") + setProperty("password", "") + setProperty("driverClassName", "org.h2.Driver") + } + ) + ) + ) + + val configOverrides = mapOf("security" to securityConfig.toConfig().root().unwrapped()) + node = startNode(ALICE_NAME, rpcUsers = emptyList(), configOverrides = configOverrides) + client = CordaRPCClient(node.internals.configuration.rpcAddress!!) + } + + @Test + fun `Add new users on-the-fly`() { + assertFailsWith( + ActiveMQSecurityException::class, + "Login with incorrect password should fail") { + client.start("user2", "bar") + } + + db.insert(UserAndRoles( + username = "user2", + password = "bar", + roles = listOf("default"))) + + client.start("user2", "bar") + } + + @Test + fun `Modify user permissions during RPC session`() { + db.insert(UserAndRoles( + username = "user3", + password = "bar", + roles = emptyList())) + + + client.start("user3", "bar").use { + val proxy = it.proxy + assertFailsWith( + PermissionException::class, + "This user should not be authorized to call 'nodeInfo'") { + proxy.stateMachinesFeed() + } + db.addRoleToUser("user3", "default") + proxy.stateMachinesFeed() + } + } + + @Test + fun `Revoke user permissions during RPC session`() { + db.insert(UserAndRoles( + username = "user4", + password = "test", + roles = listOf("default"))) + + client.start("user4", "test").use { + val proxy = it.proxy + proxy.stateMachinesFeed() + db.deleteUser("user4") + assertFailsWith( + PermissionException::class, + "This user should not be authorized to call 'nodeInfo'") { + proxy.stateMachinesFeed() + } + } + } + + @After + fun tearDown() { + db.close() + } +} + +private data class UserAndRoles(val username: String, val password: String, val roles: List) +private data class RoleAndPermissions(val role: String, val permissions: List) + +private class UsersDB : AutoCloseable { + + val jdbcUrl: String + + companion object { + val DB_CREATE_SCHEMA = """ + CREATE TABLE users (username VARCHAR(256), password TEXT); + CREATE TABLE user_roles (username VARCHAR(256), role_name VARCHAR(256)); + CREATE TABLE roles_permissions (role_name VARCHAR(256), permission TEXT); + """ + } + + fun insert(user: UserAndRoles) { + session { + it.execute("INSERT INTO users VALUES ('${user.username}', '${user.password}')") + for (role in user.roles) { + it.execute("INSERT INTO user_roles VALUES ('${user.username}', '${role}')") + } + } + } + + fun insert(roleAndPermissions: RoleAndPermissions) { + val (role, permissions) = roleAndPermissions + session { + for (permission in permissions) { + it.execute("INSERT INTO roles_permissions VALUES ('$role', '$permission')") + } + } + } + + fun addRoleToUser(username: String, role: String) { + session { + it.execute("INSERT INTO user_roles VALUES ('$username', '$role')") + } + } + + fun deleteRole(role: String) { + session { + it.execute("DELETE FROM role_permissions WHERE role_name = '$role'") + } + } + + fun deleteUser(username: String) { + session { + it.execute("DELETE FROM users WHERE username = '$username'") + it.execute("DELETE FROM user_roles WHERE username = '$username'") + } + } + + inline private fun session(statement: (Statement) -> Unit) { + DriverManager.getConnection(jdbcUrl).use { + it.autoCommit = false + it.createStatement().use(statement) + it.commit() + } + } + + constructor(name: String, + users: List = emptyList(), + roleAndPermissions: List = emptyList()) { + + jdbcUrl = "jdbc:h2:mem:${name};DB_CLOSE_DELAY=-1" + + session { + it.execute(DB_CREATE_SCHEMA) + } + + require(users.map { it.username }.toSet().size == users.size) { + "Duplicate username in input" + } + + users.forEach { insert(it) } + roleAndPermissions.forEach { insert(it) } + } + + override fun close() { + DriverManager.getConnection(jdbcUrl).use { + it.createStatement().use { + it.execute("DROP ALL OBJECTS") + } + } + } +} \ No newline at end of file diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMessagingTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMessagingTest.kt index dbbeaba860..83e4ad4afa 100644 --- a/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMessagingTest.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMessagingTest.kt @@ -19,7 +19,7 @@ import net.corda.node.services.messaging.ReceivedMessage import net.corda.node.services.messaging.send import net.corda.node.services.transactions.RaftValidatingNotaryService import net.corda.testing.* -import net.corda.testing.driver.DriverDSLExposedInterface +import net.corda.testing.driver.DriverDSL import net.corda.testing.driver.NodeHandle import net.corda.testing.driver.driver import net.corda.testing.node.ClusterSpec @@ -113,13 +113,13 @@ class P2PMessagingTest : IntegrationTest() { } } - private fun startDriverWithDistributedService(dsl: DriverDSLExposedInterface.(List>) -> Unit) { + private fun startDriverWithDistributedService(dsl: DriverDSL.(List>) -> Unit) { driver(startNodesInProcess = true, notarySpecs = listOf(NotarySpec(DISTRIBUTED_SERVICE_NAME, cluster = ClusterSpec.Raft(clusterSize = 2)))) { dsl(defaultNotaryHandle.nodeHandles.getOrThrow().map { (it as NodeHandle.InProcess).node }) } } - private fun DriverDSLExposedInterface.startAlice(): StartedNode { + private fun DriverDSL.startAlice(): StartedNode { return startNode(providedName = ALICE.name, customOverrides = mapOf("messageRedeliveryDelaySeconds" to 1)) .map { (it as NodeHandle.InProcess).node } .getOrThrow() 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 dfc206b118..acfd0b17fb 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -39,7 +39,7 @@ import net.corda.node.internal.cordapp.CordappProviderInternal import net.corda.node.services.ContractUpgradeHandler import net.corda.node.services.FinalityHandler import net.corda.node.services.NotaryChangeHandler -import net.corda.node.services.RPCUserService +import net.corda.node.internal.security.RPCSecurityManager import net.corda.node.services.api.* import net.corda.node.services.config.BFTSMaRtConfiguration import net.corda.node.services.config.NodeConfiguration @@ -47,6 +47,7 @@ import net.corda.node.services.config.NotaryConfig import net.corda.node.services.config.configureWithDevSSLCertificate import net.corda.node.services.events.NodeSchedulerService import net.corda.node.services.events.ScheduledActivityObserver +import net.corda.node.services.api.IdentityServiceInternal import net.corda.node.services.identity.PersistentIdentityService import net.corda.node.services.keys.PersistentKeyManagementService import net.corda.node.services.messaging.MessagingService @@ -138,7 +139,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, protected val _nodeReadyFuture = openFuture() protected val networkMapClient: NetworkMapClient? by lazy { configuration.compatibilityZoneURL?.let(::NetworkMapClient) } - lateinit var userService: RPCUserService get + lateinit var securityManager: RPCSecurityManager get /** Completes once the node has successfully registered with the network map service * or has loaded network map data from local database */ @@ -175,26 +176,33 @@ abstract class AbstractNode(val configuration: NodeConfiguration, check(started == null) { "Node has already been started" } log.info("Generating nodeInfo ...") initCertificate() - val (keyPairs, info) = initNodeInfo() - val identityKeypair = keyPairs.first { it.public == info.legalIdentities.first().owningKey } - val serialisedNodeInfo = info.serialize() - val signature = identityKeypair.sign(serialisedNodeInfo) - // TODO: Signed data might not be sufficient for multiple identities, as it only contains one signature. - NodeInfoWatcher.saveToFile(configuration.baseDirectory, SignedData(serialisedNodeInfo, signature)) + val schemaService = NodeSchemaService(cordappLoader.cordappSchemas) + val (identity, identityKeyPair) = obtainIdentity(notaryConfig = null) + initialiseDatabasePersistence(schemaService, makeIdentityService(identity.certificate)) { database -> + val persistentNetworkMapCache = PersistentNetworkMapCache(database) + val (keyPairs, info) = initNodeInfo(persistentNetworkMapCache, identity, identityKeyPair) + val identityKeypair = keyPairs.first { it.public == info.legalIdentities.first().owningKey } + val serialisedNodeInfo = info.serialize() + val signature = identityKeypair.sign(serialisedNodeInfo) + // TODO: Signed data might not be sufficient for multiple identities, as it only contains one signature. + NodeInfoWatcher.saveToFile(configuration.baseDirectory, SignedData(serialisedNodeInfo, signature)) + } } open fun start(): StartedNode { check(started == null) { "Node has already been started" } log.info("Node starting up ...") initCertificate() - val (keyPairs, info) = initNodeInfo() val schemaService = NodeSchemaService(cordappLoader.cordappSchemas) - val identityService = makeIdentityService(info) + val (identity, identityKeyPair) = obtainIdentity(notaryConfig = null) + val identityService = makeIdentityService(identity.certificate) // Do all of this in a database transaction so anything that might need a connection has one. val (startedImpl, schedulerService) = initialiseDatabasePersistence(schemaService, identityService) { database -> + val networkMapCache = NetworkMapCacheImpl(PersistentNetworkMapCache(database), identityService) + val (keyPairs, info) = initNodeInfo(networkMapCache, identity, identityKeyPair) identityService.loadIdentities(info.legalIdentitiesAndCerts) val transactionStorage = makeTransactionStorage(database) - val nodeServices = makeServices(keyPairs, schemaService, transactionStorage, database, info, identityService) + val nodeServices = makeServices(keyPairs, schemaService, transactionStorage, database, info, identityService, networkMapCache) val mutualExclusionConfiguration = configuration.enterpriseConfiguration.mutualExclusionConfiguration if (mutualExclusionConfiguration.on) { RunOnceService(database, mutualExclusionConfiguration.machineName, @@ -245,7 +253,6 @@ abstract class AbstractNode(val configuration: NodeConfiguration, networkMapUpdater.subscribeToNetworkMap() // If we successfully loaded network data from database, we set this future to Unit. - services.networkMapCache.addNode(info) _nodeReadyFuture.captureLater(services.networkMapCache.nodeReady.map { Unit }) return startedImpl.apply { @@ -266,11 +273,12 @@ abstract class AbstractNode(val configuration: NodeConfiguration, protected abstract fun getRxIoScheduler(): Scheduler open fun startShell(rpcOps: CordaRPCOps) { - InteractiveShell.startShell(configuration, rpcOps, userService, _services.identityService, _services.database) + InteractiveShell.startShell(configuration, rpcOps, securityManager, _services.identityService, _services.database) } - private fun initNodeInfo(): Pair, NodeInfo> { - val (identity, identityKeyPair) = obtainIdentity(notaryConfig = null) + private fun initNodeInfo(networkMapCache: NetworkMapCacheBaseInternal, + identity: PartyAndCertificate, + identityKeyPair: KeyPair): Pair, NodeInfo> { val keyPairs = mutableSetOf(identityKeyPair) myNotaryIdentity = configuration.notary?.let { @@ -278,12 +286,20 @@ abstract class AbstractNode(val configuration: NodeConfiguration, keyPairs += notaryIdentityKeyPair notaryIdentity } - val info = NodeInfo( + + var info = NodeInfo( myAddresses(), listOf(identity, myNotaryIdentity).filterNotNull(), versionInfo.platformVersion, platformClock.instant().toEpochMilli() ) + // Check if we have already stored a version of 'our own' NodeInfo, this is to avoid regenerating it with + // a different timestamp. + networkMapCache.getNodesByLegalName(myLegalName).firstOrNull()?.let { + if (info.copy(serial = it.serial) == it) { + info = it + } + } return Pair(keyPairs, info) } @@ -506,7 +522,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, * Builds node internal, advertised, and plugin services. * Returns a list of tokenizable services to be added to the serialisation context. */ - private fun makeServices(keyPairs: Set, schemaService: SchemaService, transactionStorage: WritableTransactionStorage, database: CordaPersistence, info: NodeInfo, identityService: IdentityService): MutableList { + private fun makeServices(keyPairs: Set, schemaService: SchemaService, transactionStorage: WritableTransactionStorage, database: CordaPersistence, info: NodeInfo, identityService: IdentityServiceInternal, networkMapCache: NetworkMapCacheInternal): MutableList { checkpointStorage = DBCheckpointStorage() val metrics = MetricRegistry() attachments = NodeAttachmentService(metrics) @@ -520,7 +536,8 @@ abstract class AbstractNode(val configuration: NodeConfiguration, MonitoringService(metrics), cordappProvider, database, - info) + info, + networkMapCache) network = makeMessagingService(database, info) val tokenizableServices = mutableListOf(attachments, network, services.vaultService, services.keyManagementService, services.identityService, platformClock, @@ -559,6 +576,17 @@ abstract class AbstractNode(val configuration: NodeConfiguration, "or if you don't have one yet, fill out the config file and run corda.jar --initial-registration. " + "Read more at: https://docs.corda.net/permissioning.html" } + + // Check all cert path chain to the trusted root + val sslKeystore = loadKeyStore(configuration.sslKeystore, configuration.keyStorePassword) + val identitiesKeystore = loadKeyStore(configuration.nodeKeystore, configuration.keyStorePassword) + val trustStore = loadKeyStore(configuration.trustStoreFile, configuration.trustStorePassword) + val sslRoot = sslKeystore.getCertificateChain(X509Utilities.CORDA_CLIENT_TLS).last() + val clientCARoot = identitiesKeystore.getCertificateChain(X509Utilities.CORDA_CLIENT_CA).last() + val trustRoot = trustStore.getCertificate(X509Utilities.CORDA_ROOT_CA) + + require(sslRoot == trustRoot) { "TLS certificate must chain to the trusted root." } + require(clientCARoot == trustRoot) { "Client CA certificate must chain to the trusted root." } } // Specific class so that MockNode can catch it. @@ -600,7 +628,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, } } - protected open fun makeKeyManagementService(identityService: IdentityService, keyPairs: Set): KeyManagementService { + protected open fun makeKeyManagementService(identityService: IdentityServiceInternal, keyPairs: Set): KeyManagementService { return PersistentKeyManagementService(identityService, keyPairs) } @@ -627,12 +655,12 @@ abstract class AbstractNode(val configuration: NodeConfiguration, } } - private fun makeIdentityService(info: NodeInfo): PersistentIdentityService { + private fun makeIdentityService(identityCert: X509Certificate): PersistentIdentityService { val trustStore = KeyStoreWrapper(configuration.trustStoreFile, configuration.trustStorePassword) val caKeyStore = KeyStoreWrapper(configuration.nodeKeystore, configuration.keyStorePassword) val trustRoot = trustStore.getX509Certificate(X509Utilities.CORDA_ROOT_CA) val clientCa = caKeyStore.certificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA) - val caCertificates = arrayOf(info.legalIdentitiesAndCerts[0].certificate, clientCa.certificate.cert) + val caCertificates = arrayOf(identityCert, clientCa.certificate.cert) return PersistentIdentityService(trustRoot, *caCertificates) } @@ -732,13 +760,13 @@ abstract class AbstractNode(val configuration: NodeConfiguration, override val monitoringService: MonitoringService, override val cordappProvider: CordappProviderInternal, override val database: CordaPersistence, - override val myInfo: NodeInfo + override val myInfo: NodeInfo, + override val networkMapCache: NetworkMapCacheInternal ) : SingletonSerializeAsToken(), ServiceHubInternal, StateLoader by validatedTransactions { override val rpcFlows = ArrayList>>() override val stateMachineRecordedTransactionMapping = DBTransactionMappingStorage() override val auditService = DummyAuditService() override val transactionVerifierService by lazy { makeTransactionVerifierService() } - override val networkMapCache by lazy { NetworkMapCacheImpl(PersistentNetworkMapCache(database), identityService) } override val vaultService by lazy { makeVaultService(keyManagementService, validatedTransactions, database.hibernateConfig) } override val contractUpgradeService by lazy { ContractUpgradeServiceImpl() } override val attachments: AttachmentStorage get() = this@AbstractNode.attachments 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 15682453b8..0396b0ef8b 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -2,6 +2,7 @@ package net.corda.node.internal import com.codahale.metrics.JmxReporter import net.corda.core.concurrent.CordaFuture +import net.corda.core.context.AuthServiceId import net.corda.core.internal.concurrent.openFuture import net.corda.core.internal.concurrent.thenMatch import net.corda.core.internal.uncheckedCast @@ -16,11 +17,10 @@ import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.contextLogger import net.corda.node.VersionInfo import net.corda.node.internal.cordapp.CordappLoader +import net.corda.node.internal.security.RPCSecurityManagerImpl import net.corda.node.serialization.KryoServerSerializationScheme -import net.corda.node.services.RPCUserServiceImpl import net.corda.node.services.api.SchemaService -import net.corda.node.services.config.NodeConfiguration -import net.corda.node.services.config.VerifierType +import net.corda.node.services.config.* import net.corda.node.services.messaging.* import net.corda.node.services.transactions.InMemoryTransactionVerifierService import net.corda.node.utilities.AddressUtils @@ -133,7 +133,12 @@ open class Node(configuration: NodeConfiguration, private var shutdownHook: ShutdownHook? = null override fun makeMessagingService(database: CordaPersistence, info: NodeInfo): MessagingService { - userService = RPCUserServiceImpl(configuration.rpcUsers) + // Construct security manager reading users data either from the 'security' config section + // if present or from rpcUsers list if the former is missing from config. + val securityManagerConfig = configuration.security?.authService ?: + SecurityConfiguration.AuthService.fromUsers(configuration.rpcUsers) + + securityManager = RPCSecurityManagerImpl(securityManagerConfig) val serverAddress = configuration.messagingServerAddress ?: makeLocalMessageBroker() val advertisedAddress = info.addresses.single() @@ -156,7 +161,7 @@ open class Node(configuration: NodeConfiguration, private fun makeLocalMessageBroker(): NetworkHostAndPort { with(configuration) { - messageBroker = ArtemisMessagingServer(this, p2pAddress.port, rpcAddress?.port, services.networkMapCache, userService) + messageBroker = ArtemisMessagingServer(this, p2pAddress.port, rpcAddress?.port, services.networkMapCache, securityManager) return NetworkHostAndPort("localhost", p2pAddress.port) } } @@ -212,7 +217,7 @@ open class Node(configuration: NodeConfiguration, // Start up the MQ clients. rpcMessagingClient.run { runOnStop += this::stop - start(rpcOps, userService) + start(rpcOps, securityManager) } verifierMessagingClient?.run { runOnStop += this::stop @@ -225,10 +230,10 @@ open class Node(configuration: NodeConfiguration, } /** - * If the node is persisting to an embedded H2 database, then expose this via TCP with a JDBC URL of the form: + * If the node is persisting to an embedded H2 database, then expose this via TCP with a DB URL of the form: * jdbc:h2:tcp://:/node * with username and password as per the DataSource connection details. The key element to enabling this support is to - * ensure that you specify a JDBC connection URL of the form jdbc:h2:file: in the node config and that you include + * ensure that you specify a DB connection URL of the form jdbc:h2:file: in the node config and that you include * the H2 option AUTO_SERVER_PORT set to the port you desire to use (0 will give a dynamically allocated port number) * but exclude the H2 option AUTO_SERVER=TRUE. * This is not using the H2 "automatic mixed mode" directly but leans on many of the underpinnings. For more details 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 ebb245c955..14803be925 100644 --- a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt +++ b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt @@ -81,13 +81,13 @@ open class NodeStartup(val args: Array) { conf0 } - banJavaSerialisation(conf) - preNetworkRegistration(conf) - if (shouldRegisterWithNetwork(cmdlineOptions, conf)) { + banJavaSerialisation(conf) + preNetworkRegistration(conf) + if (shouldRegisterWithNetwork(cmdlineOptions, conf)) { registerWithNetwork(cmdlineOptions, conf) return true } - logStartupInfo(versionInfo, cmdlineOptions, conf) + logStartupInfo(versionInfo, cmdlineOptions, conf) try { cmdlineOptions.baseDirectory.createDirectories() diff --git a/node/src/main/kotlin/net/corda/node/internal/RpcAuthorisationProxy.kt b/node/src/main/kotlin/net/corda/node/internal/RpcAuthorisationProxy.kt index 7a1e66f6ea..d2f0f8afc1 100644 --- a/node/src/main/kotlin/net/corda/node/internal/RpcAuthorisationProxy.kt +++ b/node/src/main/kotlin/net/corda/node/internal/RpcAuthorisationProxy.kt @@ -1,5 +1,6 @@ package net.corda.node.internal +import net.corda.client.rpc.PermissionException import net.corda.core.contracts.ContractState import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowLogic @@ -156,9 +157,12 @@ class RpcAuthorisationProxy(private val implementation: CordaRPCOps, private val private inline fun guard(methodName: String, action: () -> RESULT) = guard(methodName, emptyList(), action) // TODO change to KFunction reference after Kotlin fixes https://youtrack.jetbrains.com/issue/KT-12140 - private inline fun guard(methodName: String, args: List, action: () -> RESULT): RESULT { - - context().requireEitherPermission(permissionsAllowing.invoke(methodName, args)) - return action() + private inline fun guard(methodName: String, args: List>, action: () -> RESULT) : RESULT { + if (!context().isPermitted(methodName, *(args.map { it.name }.toTypedArray()))) { + throw PermissionException("User not authorized to perform RPC call $methodName with target $args") + } + else { + return action() + } } } \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/internal/security/AuthorizingSubject.kt b/node/src/main/kotlin/net/corda/node/internal/security/AuthorizingSubject.kt new file mode 100644 index 0000000000..d831582747 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/internal/security/AuthorizingSubject.kt @@ -0,0 +1,28 @@ +package net.corda.node.internal.security + +/** + * Provides permission checking for the subject identified by the given [principal]. + */ +interface AuthorizingSubject { + + /** + * Identity of underlying subject + */ + val principal: String + + /** + * Determines if the underlying subject is entitled to perform a certain action, + * (e.g. an RPC invocation) represented by an [action] string followed by an + * optional list of arguments. + */ + fun isPermitted(action : String, vararg arguments : String) : Boolean +} + +/** + * An implementation of [AuthorizingSubject] permitting all actions + */ +class AdminSubject(override val principal : String) : AuthorizingSubject { + + override fun isPermitted(action: String, vararg arguments: String) = true + +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/internal/security/Password.kt b/node/src/main/kotlin/net/corda/node/internal/security/Password.kt new file mode 100644 index 0000000000..105addc8c1 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/internal/security/Password.kt @@ -0,0 +1,43 @@ +package net.corda.node.internal.security + +import java.util.* + +class Password(valueRaw: CharArray) : AutoCloseable { + + constructor(value: String) : this(value.toCharArray()) + + private val internalValue = valueRaw.copyOf() + + val value: CharArray + get() = internalValue.copyOf() + + val valueAsString: String + get() = internalValue.joinToString("") + + override fun close() { + internalValue.indices.forEach { index -> + internalValue[index] = MASK + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Password + + if (!Arrays.equals(internalValue, other.internalValue)) return false + + return true + } + + override fun hashCode(): Int { + return Arrays.hashCode(internalValue) + } + + override fun toString(): String = (0..5).map { MASK }.joinToString("") + + private companion object { + private const val MASK = '*' + } +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/internal/security/RPCSecurityManager.kt b/node/src/main/kotlin/net/corda/node/internal/security/RPCSecurityManager.kt new file mode 100644 index 0000000000..dafa069833 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/internal/security/RPCSecurityManager.kt @@ -0,0 +1,41 @@ +package net.corda.node.internal.security + +import net.corda.core.context.AuthServiceId +import org.apache.shiro.authc.AuthenticationException +import javax.security.auth.login.FailedLoginException + +/** + * Manage security of RPC users, providing logic for user authentication and authorization. + */ +interface RPCSecurityManager : AutoCloseable { + /** + * An identifier associated to this security service + */ + val id: AuthServiceId + + /** + * Perform user authentication from principal and password. Return an [AuthorizingSubject] containing + * the permissions of the user identified by the given [principal] if authentication via password succeeds, + * otherwise a [FailedLoginException] is thrown. + */ + fun authenticate(principal: String, password: Password): AuthorizingSubject + + /** + * Construct an [AuthorizingSubject] instance con permissions of the user associated to + * the given principal. Throws an exception if the principal cannot be resolved to a known user. + */ + fun buildSubject(principal: String): AuthorizingSubject +} + +/** + * Non-throwing version of authenticate, returning null instead of throwing in case of authentication failure + */ +fun RPCSecurityManager.tryAuthenticate(principal: String, password: Password): AuthorizingSubject? { + password.use { + return try { + authenticate(principal, password) + } catch (e: AuthenticationException) { + null + } + } +} diff --git a/node/src/main/kotlin/net/corda/node/internal/security/RPCSecurityManagerImpl.kt b/node/src/main/kotlin/net/corda/node/internal/security/RPCSecurityManagerImpl.kt new file mode 100644 index 0000000000..add690620e --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/internal/security/RPCSecurityManagerImpl.kt @@ -0,0 +1,308 @@ +package net.corda.node.internal.security + +import com.google.common.cache.CacheBuilder +import com.google.common.cache.Cache +import com.google.common.primitives.Ints +import com.zaxxer.hikari.HikariConfig +import com.zaxxer.hikari.HikariDataSource +import net.corda.core.context.AuthServiceId +import net.corda.core.utilities.loggerFor +import net.corda.node.services.config.PasswordEncryption +import net.corda.node.services.config.SecurityConfiguration +import net.corda.node.services.config.AuthDataSourceType +import net.corda.nodeapi.internal.config.User +import org.apache.shiro.authc.* +import org.apache.shiro.authc.credential.PasswordMatcher +import org.apache.shiro.authc.credential.SimpleCredentialsMatcher +import org.apache.shiro.authz.AuthorizationInfo +import org.apache.shiro.authz.Permission +import org.apache.shiro.authz.SimpleAuthorizationInfo +import org.apache.shiro.authz.permission.DomainPermission +import org.apache.shiro.authz.permission.PermissionResolver +import org.apache.shiro.cache.CacheManager +import org.apache.shiro.mgt.DefaultSecurityManager +import org.apache.shiro.realm.AuthorizingRealm +import org.apache.shiro.realm.jdbc.JdbcRealm +import org.apache.shiro.subject.PrincipalCollection +import org.apache.shiro.subject.SimplePrincipalCollection +import javax.security.auth.login.FailedLoginException +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.TimeUnit +private typealias AuthServiceConfig = SecurityConfiguration.AuthService + +/** + * Default implementation of [RPCSecurityManager] adapting + * [org.apache.shiro.mgt.SecurityManager] + */ +class RPCSecurityManagerImpl(config: AuthServiceConfig) : RPCSecurityManager { + + override val id = config.id + private val manager: DefaultSecurityManager + + init { + manager = buildImpl(config) + } + + override fun close() { + manager.destroy() + } + + @Throws(FailedLoginException::class) + override fun authenticate(principal: String, password: Password): AuthorizingSubject { + password.use { + val authToken = UsernamePasswordToken(principal, it.value) + try { + manager.authenticate(authToken) + } catch (authcException: AuthenticationException) { + throw FailedLoginException(authcException.toString()) + } + return ShiroAuthorizingSubject( + subjectId = SimplePrincipalCollection(principal, id.value), + manager = manager) + } + } + + override fun buildSubject(principal: String): AuthorizingSubject = + ShiroAuthorizingSubject( + subjectId = SimplePrincipalCollection(principal, id.value), + manager = manager) + + + companion object { + + private val logger = loggerFor() + + /** + * Instantiate RPCSecurityManager initialised with users data from a list of [User] + */ + fun fromUserList(id: AuthServiceId, users: List) = + RPCSecurityManagerImpl( + AuthServiceConfig.fromUsers(users).copy(id = id)) + + // Build internal Shiro securityManager instance + private fun buildImpl(config: AuthServiceConfig): DefaultSecurityManager { + val realm = when (config.dataSource.type) { + AuthDataSourceType.DB -> { + logger.info("Constructing DB-backed security data source: ${config.dataSource.connection}") + NodeJdbcRealm(config.dataSource) + } + AuthDataSourceType.INMEMORY -> { + logger.info("Constructing realm from list of users in config ${config.dataSource.users!!}") + InMemoryRealm(config.dataSource.users, config.id.value, config.dataSource.passwordEncryption) + } + } + return DefaultSecurityManager(realm).also { + // Setup optional cache layer if configured + it.cacheManager = config.options?.cache?.let { + GuavaCacheManager( + timeToLiveSeconds = it.expiryTimeInSecs, + maxSize = it.capacity) + } + } + } + } +} + +/** + * Provide a representation of RPC permissions based on Apache Shiro permissions framework. + * A permission represents a set of actions: for example, the set of all RPC invocations, or the set + * of RPC invocations acting on a given class of Flows in input. A permission `implies` another one if + * its set of actions contains the set of actions in the other one. In Apache Shiro, permissions are + * represented by instances of the [Permission] interface which offers a single method: [implies], to + * test if the 'x implies y' binary predicate is satisfied. + */ +private class RPCPermission : DomainPermission { + + /** + * Helper constructor directly setting actions and target field + * + * @param methods Set of allowed RPC methods + * @param target An optional "target" type on which methods act + */ + constructor(methods: Set, target: String? = null) : super(methods, target?.let { setOf(it) }) + + + /** + * Default constructor instantiate an "ALL" permission + */ + constructor() : super() +} + +/** + * A [org.apache.shiro.authz.permission.PermissionResolver] implementation for RPC permissions. + * Provides a method to construct an [RPCPermission] instance from its string representation + * in the form used by a Node admin. + * + * Currently valid permission strings have the forms: + * + * - `ALL`: allowing all type of RPC calls + * + * - `InvokeRpc.$RPCMethodName`: allowing to call a given RPC method without restrictions on its arguments. + * + * - `StartFlow.$FlowClassName`: allowing to call a `startFlow*` RPC method targeting a Flow instance + * of a given class + * + */ +private object RPCPermissionResolver : PermissionResolver { + + private val SEPARATOR = '.' + private val ACTION_START_FLOW = "startflow" + private val ACTION_INVOKE_RPC = "invokerpc" + private val ACTION_ALL = "all" + + private val FLOW_RPC_CALLS = setOf("startFlowDynamic", "startTrackedFlowDynamic") + + override fun resolvePermission(representation: String): Permission { + + val action = representation.substringBefore(SEPARATOR).toLowerCase() + when (action) { + ACTION_INVOKE_RPC -> { + val rpcCall = representation.substringAfter(SEPARATOR) + require(representation.count { it == SEPARATOR } == 1) { + "Malformed permission string" + } + return RPCPermission(setOf(rpcCall)) + } + ACTION_START_FLOW -> { + val targetFlow = representation.substringAfter(SEPARATOR) + require(targetFlow.isNotEmpty()) { + "Missing target flow after StartFlow" + } + return RPCPermission(FLOW_RPC_CALLS, targetFlow) + } + ACTION_ALL -> { + // Leaving empty set of targets and actions to match everything + return RPCPermission() + } + else -> throw IllegalArgumentException("Unkwnow permission action specifier: $action") + } + } +} + +private class ShiroAuthorizingSubject( + private val subjectId: PrincipalCollection, + private val manager: DefaultSecurityManager) : AuthorizingSubject { + + override val principal get() = subjectId.primaryPrincipal.toString() + + override fun isPermitted(action: String, vararg arguments: String) = + manager.isPermitted(subjectId, RPCPermission(setOf(action), arguments.firstOrNull())) +} + +private fun buildCredentialMatcher(type: PasswordEncryption) = when (type) { + PasswordEncryption.NONE -> SimpleCredentialsMatcher() + PasswordEncryption.SHIRO_1_CRYPT -> PasswordMatcher() +} + +private class InMemoryRealm(users: List, + realmId: String, + passwordEncryption: PasswordEncryption = PasswordEncryption.NONE) : AuthorizingRealm() { + + private val authorizationInfoByUser: Map + private val authenticationInfoByUser: Map + + init { + permissionResolver = RPCPermissionResolver + users.forEach { + require(it.username.matches("\\w+".toRegex())) { + "Username ${it.username} contains invalid characters" + } + } + val resolvePermission = { s: String -> permissionResolver.resolvePermission(s) } + authorizationInfoByUser = users.associate { + it.username to SimpleAuthorizationInfo().apply { + objectPermissions = it.permissions.map { resolvePermission(it) }.toSet() + roles = emptySet() + stringPermissions = emptySet() + } + } + authenticationInfoByUser = users.associate { + it.username to SimpleAuthenticationInfo().apply { + credentials = it.password + principals = SimplePrincipalCollection(it.username, realmId) + } + } + credentialsMatcher = buildCredentialMatcher(passwordEncryption) + } + + // Methods from AuthorizingRealm interface used by Shiro to query + // for authentication/authorization data for a given user + override fun doGetAuthenticationInfo(token: AuthenticationToken) = + authenticationInfoByUser[token.principal as String] + + override fun doGetAuthorizationInfo(principals: PrincipalCollection) = + authorizationInfoByUser[principals.primaryPrincipal as String] +} + +private class NodeJdbcRealm(config: SecurityConfiguration.AuthService.DataSource) : JdbcRealm() { + + init { + credentialsMatcher = buildCredentialMatcher(config.passwordEncryption) + setPermissionsLookupEnabled(true) + dataSource = HikariDataSource(HikariConfig(config.connection!!)) + permissionResolver = RPCPermissionResolver + } +} + +private typealias ShiroCache = org.apache.shiro.cache.Cache + +/** + * Adapts a [com.google.common.cache.Cache] to a [org.apache.shiro.cache.Cache] implementation. + */ +private fun Cache.toShiroCache(name: String) = object : ShiroCache { + + val name = name + private val impl = this@toShiroCache + + override operator fun get(key: K) = impl.getIfPresent(key) + + override fun put(key: K, value: V): V? { + val lastValue = get(key) + impl.put(key, value) + return lastValue + } + + override fun remove(key: K): V? { + val lastValue = get(key) + impl.invalidate(key) + return lastValue + } + + override fun clear() { + impl.invalidateAll() + } + + override fun size() = Ints.checkedCast(impl.size()) + override fun keys() = impl.asMap().keys + override fun values() = impl.asMap().values + override fun toString() = "Guava cache adapter [$impl]" +} + +/** + * Implementation of [org.apache.shiro.cache.CacheManager] based on + * cache implementation in [com.google.common.cache] + */ +private class GuavaCacheManager(val maxSize: Long, + val timeToLiveSeconds: Long) : CacheManager { + + private val instances = ConcurrentHashMap>() + + override fun getCache(name: String): ShiroCache { + val result = instances[name] ?: buildCache(name) + instances.putIfAbsent(name, result) + return result as ShiroCache + } + + private fun buildCache(name: String) : ShiroCache { + logger.info("Constructing cache '$name' with maximumSize=$maxSize, TTL=${timeToLiveSeconds}s") + return CacheBuilder.newBuilder() + .expireAfterWrite(timeToLiveSeconds, TimeUnit.SECONDS) + .maximumSize(maxSize) + .build() + .toShiroCache(name) + } + + companion object { + private val logger = loggerFor() + } +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/RPCUserService.kt b/node/src/main/kotlin/net/corda/node/services/RPCUserService.kt deleted file mode 100644 index 3a7bbdcb9f..0000000000 --- a/node/src/main/kotlin/net/corda/node/services/RPCUserService.kt +++ /dev/null @@ -1,33 +0,0 @@ -package net.corda.node.services - -import net.corda.core.context.AuthServiceId -import net.corda.nodeapi.internal.config.User - -/** - * Service for retrieving [User] objects representing RPC users who are authorised to use the RPC system. A [User] - * contains their login username and password along with a set of permissions for RPC services they are allowed access - * to. These permissions are represented as [String]s to allow RPC implementations to add their own permissioning. - */ -interface RPCUserService { - - fun getUser(username: String): User? - val users: List - - val id: AuthServiceId -} - -// TODO Store passwords as salted hashes -// TODO Or ditch this and consider something like Apache Shiro -// TODO Need access to permission checks from inside flows and at other point during audit checking. -class RPCUserServiceImpl(override val users: List) : RPCUserService { - - override val id: AuthServiceId = AuthServiceId("NODE_FILE_CONFIGURATION") - - init { - users.forEach { - require(it.username.matches("\\w+".toRegex())) { "Username ${it.username} contains invalid characters" } - } - } - - override fun getUser(username: String): User? = users.find { it.username == username } -} diff --git a/node/src/main/kotlin/net/corda/node/services/api/IdentityServiceInternal.kt b/node/src/main/kotlin/net/corda/node/services/api/IdentityServiceInternal.kt new file mode 100644 index 0000000000..5694a68940 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/services/api/IdentityServiceInternal.kt @@ -0,0 +1,11 @@ +package net.corda.node.services.api + +import net.corda.core.identity.PartyAndCertificate +import net.corda.core.node.services.IdentityService + +interface IdentityServiceInternal : IdentityService { + /** This method exists so it can be mocked with doNothing, rather than having to make up a possibly invalid return value. */ + fun justVerifyAndRegisterIdentity(identity: PartyAndCertificate) { + verifyAndRegisterIdentity(identity) + } +} diff --git a/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt b/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt index eb47c8cb19..6c195810c2 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt @@ -63,7 +63,7 @@ fun NodeConfiguration.configureWithDevSSLCertificate() = configureDevKeyAndTrust fun SSLConfiguration.configureDevKeyAndTrustStores(myLegalName: CordaX500Name) { certificatesDirectory.createDirectories() if (!trustStoreFile.exists()) { - javaClass.classLoader.getResourceAsStream("net/corda/node/internal/certificates/cordatruststore.jks").copyTo(trustStoreFile) + loadKeyStore(javaClass.classLoader.getResourceAsStream("net/corda/node/internal/certificates/cordatruststore.jks"), "trustpass").save(trustStoreFile, trustStorePassword) } if (!sslKeystore.exists() || !nodeKeystore.exists()) { val caKeyStore = loadKeyStore(javaClass.classLoader.getResourceAsStream("net/corda/node/internal/certificates/cordadevcakeys.jks"), "cordacadevpass") diff --git a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt index b84b3e781b..49576ac15d 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt @@ -1,6 +1,7 @@ package net.corda.node.services.config import com.typesafe.config.Config +import net.corda.core.context.AuthServiceId import net.corda.core.identity.CordaX500Name import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.seconds @@ -21,6 +22,7 @@ interface NodeConfiguration : NodeSSLConfiguration { val exportJMXto: String val dataSourceProperties: Properties val rpcUsers: List + val security: SecurityConfiguration? val devMode: Boolean val devModeOptions: DevModeOptions? val compatibilityZoneURL: URL? @@ -95,6 +97,7 @@ data class NodeConfigurationImpl( override val dataSourceProperties: Properties, override val compatibilityZoneURL: URL? = null, override val rpcUsers: List, + override val security : SecurityConfiguration? = null, override val verifierType: VerifierType, // TODO typesafe config supports the notion of durations. Make use of that by mapping it to java.time.Duration. // Then rename this to messageRedeliveryDelay and make it of type Duration @@ -117,8 +120,9 @@ data class NodeConfigurationImpl( // TODO See TODO above. Rename this to nodeInfoPollingFrequency and make it of type Duration override val additionalNodeInfoPollingFrequencyMsec: Long = 5.seconds.toMillis(), override val sshd: SSHDConfiguration? = null, - override val database: DatabaseConfig = DatabaseConfig(initialiseSchema = devMode) + override val database: DatabaseConfig = DatabaseConfig(initialiseSchema = devMode, exportHibernateJMXStatistics = devMode) ) : NodeConfiguration { + override val exportJMXto: String get() = "http" init { @@ -126,6 +130,9 @@ data class NodeConfigurationImpl( require(!useTestClock || devMode) { "Cannot use test clock outside of dev mode" } require(devModeOptions == null || devMode) { "Cannot use devModeOptions outside of dev mode" } require(myLegalName.commonName == null) { "Common name must be null: $myLegalName" } + require(security == null || rpcUsers.isEmpty()) { + "Cannot specify both 'rpcUsers' and 'security' in configuration" + } } } @@ -154,6 +161,80 @@ data class CertChainPolicyConfig(val role: String, private val policy: CertChain } data class SSHDConfiguration(val port: Int) + +// Supported types of authentication/authorization data providers +enum class AuthDataSourceType { + // External RDBMS + DB, + + // Static dataset hard-coded in config + INMEMORY +} + +// Password encryption scheme +enum class PasswordEncryption { + + // Password stored in clear + NONE, + + // Password salt-hashed using Apache Shiro flexible encryption format + // [org.apache.shiro.crypto.hash.format.Shiro1CryptFormat] + SHIRO_1_CRYPT +} + +// Subset of Node configuration related to security aspects +data class SecurityConfiguration(val authService: SecurityConfiguration.AuthService) { + + // Configure RPC/Shell users authentication/authorization service + data class AuthService(val dataSource: AuthService.DataSource, + val id: AuthServiceId = defaultAuthServiceId(dataSource.type), + val options: AuthService.Options? = null) { + + init { + require(!(dataSource.type == AuthDataSourceType.INMEMORY && + options?.cache != null)) { + "No cache supported for INMEMORY data provider" + } + } + + // Optional components: cache + data class Options(val cache: Options.Cache?) { + + // Cache parameters + data class Cache(val expiryTimeInSecs: Long, val capacity: Long) + + } + + // Provider of users credentials and permissions data + data class DataSource(val type: AuthDataSourceType, + val passwordEncryption: PasswordEncryption = PasswordEncryption.NONE, + val connection: Properties? = null, + val users: List? = null) { + init { + when (type) { + AuthDataSourceType.INMEMORY -> require(users != null && connection == null) + AuthDataSourceType.DB -> require(users == null && connection != null) + } + } + } + + companion object { + // If unspecified, we assign an AuthServiceId by default based on the + // underlying data provider + fun defaultAuthServiceId(type: AuthDataSourceType) = when (type) { + AuthDataSourceType.INMEMORY -> AuthServiceId("NODE_CONFIG") + AuthDataSourceType.DB -> AuthServiceId("REMOTE_DATABASE") + } + + fun fromUsers(users: List) = AuthService( + dataSource = DataSource( + type = AuthDataSourceType.INMEMORY, + users = users, + passwordEncryption = PasswordEncryption.NONE), + id = AuthServiceId("NODE_CONFIG")) + } + } +} data class RelayConfiguration(val relayHost: String, val remoteInboundPort: Int, val username: String, diff --git a/node/src/main/kotlin/net/corda/node/services/identity/InMemoryIdentityService.kt b/node/src/main/kotlin/net/corda/node/services/identity/InMemoryIdentityService.kt index 97e1c03adc..e3f3cc233f 100644 --- a/node/src/main/kotlin/net/corda/node/services/identity/InMemoryIdentityService.kt +++ b/node/src/main/kotlin/net/corda/node/services/identity/InMemoryIdentityService.kt @@ -5,11 +5,11 @@ import net.corda.core.crypto.toStringShort import net.corda.core.identity.* import net.corda.core.internal.cert import net.corda.core.internal.toX509CertHolder -import net.corda.core.node.services.IdentityService import net.corda.core.node.services.UnknownAnonymousPartyException import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.utilities.contextLogger import net.corda.core.utilities.trace +import net.corda.node.services.api.IdentityServiceInternal import net.corda.nodeapi.internal.crypto.X509CertificateFactory import org.bouncycastle.cert.X509CertificateHolder import java.security.InvalidAlgorithmParameterException @@ -25,7 +25,7 @@ import javax.annotation.concurrent.ThreadSafe */ @ThreadSafe class InMemoryIdentityService(identities: Iterable, - trustRoot: X509CertificateHolder) : SingletonSerializeAsToken(), IdentityService { + trustRoot: X509CertificateHolder) : SingletonSerializeAsToken(), IdentityServiceInternal { companion object { private val log = contextLogger() } @@ -45,18 +45,17 @@ class InMemoryIdentityService(identities: Iterable, principalToParties.putAll(identities.associateBy { it.name }) } - // TODO: Check the certificate validation logic @Throws(CertificateExpiredException::class, CertificateNotYetValidException::class, InvalidAlgorithmParameterException::class) override fun verifyAndRegisterIdentity(identity: PartyAndCertificate): PartyAndCertificate? { // Validate the chain first, before we do anything clever with it try { identity.verify(trustAnchor) } catch (e: CertPathValidatorException) { - log.error("Certificate validation failed for ${identity.name} against trusted root ${trustAnchor.trustedCert.subjectX500Principal}.") - log.error("Certificate path :") + log.warn("Certificate validation failed for ${identity.name} against trusted root ${trustAnchor.trustedCert.subjectX500Principal}.") + log.warn("Certificate path :") identity.certPath.certificates.reversed().forEachIndexed { index, certificate -> val space = (0 until index).joinToString("") { " " } - log.error("$space${certificate.toX509CertHolder().subject}") + log.warn("$space${certificate.toX509CertHolder().subject}") } throw e } diff --git a/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt b/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt index 644204f351..471a1f51f5 100644 --- a/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt +++ b/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt @@ -6,12 +6,12 @@ import net.corda.core.crypto.toStringShort import net.corda.core.identity.* import net.corda.core.internal.cert import net.corda.core.internal.toX509CertHolder -import net.corda.core.node.services.IdentityService import net.corda.core.node.services.UnknownAnonymousPartyException import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.utilities.MAX_HASH_HEX_SIZE import net.corda.core.utilities.contextLogger import net.corda.core.utilities.debug +import net.corda.node.services.api.IdentityServiceInternal import net.corda.node.utilities.AppendOnlyPersistentMap import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX import net.corda.nodeapi.internal.crypto.X509CertificateFactory @@ -27,7 +27,7 @@ import javax.persistence.Lob @ThreadSafe class PersistentIdentityService(override val trustRoot: X509Certificate, - vararg caCertificates: X509Certificate) : SingletonSerializeAsToken(), IdentityService { + vararg caCertificates: X509Certificate) : SingletonSerializeAsToken(), IdentityServiceInternal { constructor(trustRoot: X509CertificateHolder) : this(trustRoot.cert) companion object { @@ -110,17 +110,16 @@ class PersistentIdentityService(override val trustRoot: X509Certificate, } } - // TODO: Check the certificate validation logic @Throws(CertificateExpiredException::class, CertificateNotYetValidException::class, InvalidAlgorithmParameterException::class) override fun verifyAndRegisterIdentity(identity: PartyAndCertificate): PartyAndCertificate? { // Validate the chain first, before we do anything clever with it try { identity.verify(trustAnchor) } catch (e: CertPathValidatorException) { - log.error(e.localizedMessage) - log.error("Path = ") + log.warn(e.localizedMessage) + log.warn("Path = ") identity.certPath.certificates.reversed().forEach { - log.error(it.toX509CertHolder().subject.toString()) + log.warn(it.toX509CertHolder().subject.toString()) } throw e } diff --git a/node/src/main/kotlin/net/corda/node/services/keys/E2ETestKeyManagementService.kt b/node/src/main/kotlin/net/corda/node/services/keys/E2ETestKeyManagementService.kt index 0c0eb61778..34b1c94e46 100644 --- a/node/src/main/kotlin/net/corda/node/services/keys/E2ETestKeyManagementService.kt +++ b/node/src/main/kotlin/net/corda/node/services/keys/E2ETestKeyManagementService.kt @@ -3,9 +3,9 @@ package net.corda.node.services.keys import net.corda.core.crypto.* import net.corda.core.identity.PartyAndCertificate import net.corda.core.internal.ThreadBox -import net.corda.core.node.services.IdentityService import net.corda.core.node.services.KeyManagementService import net.corda.core.serialization.SingletonSerializeAsToken +import net.corda.node.services.api.IdentityServiceInternal import org.bouncycastle.operator.ContentSigner import java.security.KeyPair import java.security.PrivateKey @@ -25,7 +25,7 @@ import javax.annotation.concurrent.ThreadSafe * etc. */ @ThreadSafe -class E2ETestKeyManagementService(val identityService: IdentityService, +class E2ETestKeyManagementService(val identityService: IdentityServiceInternal, initialKeys: Set) : SingletonSerializeAsToken(), KeyManagementService { private class InnerState { val keys = HashMap() diff --git a/node/src/main/kotlin/net/corda/node/services/keys/KMSUtils.kt b/node/src/main/kotlin/net/corda/node/services/keys/KMSUtils.kt index 6f520f37f1..619bb8c9cd 100644 --- a/node/src/main/kotlin/net/corda/node/services/keys/KMSUtils.kt +++ b/node/src/main/kotlin/net/corda/node/services/keys/KMSUtils.kt @@ -4,8 +4,8 @@ import net.corda.core.crypto.Crypto import net.corda.core.identity.PartyAndCertificate import net.corda.core.internal.cert import net.corda.core.internal.toX509CertHolder -import net.corda.core.node.services.IdentityService import net.corda.core.utilities.days +import net.corda.node.services.api.IdentityServiceInternal import net.corda.nodeapi.internal.crypto.CertificateType import net.corda.nodeapi.internal.crypto.ContentSignerBuilder import net.corda.nodeapi.internal.crypto.X509CertificateFactory @@ -28,18 +28,18 @@ import java.time.Duration * @param revocationEnabled whether to check revocation status of certificates in the certificate path. * @return X.509 certificate and path to the trust root. */ -fun freshCertificate(identityService: IdentityService, +fun freshCertificate(identityService: IdentityServiceInternal, subjectPublicKey: PublicKey, issuer: PartyAndCertificate, issuerSigner: ContentSigner, revocationEnabled: Boolean = false): PartyAndCertificate { val issuerCert = issuer.certificate.toX509CertHolder() val window = X509Utilities.getCertificateValidityWindow(Duration.ZERO, 3650.days, issuerCert) - val ourCertificate = X509Utilities.createCertificate(CertificateType.IDENTITY, issuerCert.subject, + val ourCertificate = X509Utilities.createCertificate(CertificateType.WELL_KNOWN_IDENTITY, issuerCert.subject, issuerSigner, issuer.name, subjectPublicKey, window) val ourCertPath = X509CertificateFactory().delegate.generateCertPath(listOf(ourCertificate.cert) + issuer.certPath.certificates) val anonymisedIdentity = PartyAndCertificate(ourCertPath) - identityService.verifyAndRegisterIdentity(anonymisedIdentity) + identityService.justVerifyAndRegisterIdentity(anonymisedIdentity) return anonymisedIdentity } diff --git a/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt b/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt index cf9f19abdf..aa8b372742 100644 --- a/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt +++ b/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt @@ -2,10 +2,10 @@ package net.corda.node.services.keys import net.corda.core.crypto.* import net.corda.core.identity.PartyAndCertificate -import net.corda.core.node.services.IdentityService import net.corda.core.node.services.KeyManagementService import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.utilities.MAX_HASH_HEX_SIZE +import net.corda.node.services.api.IdentityServiceInternal import net.corda.node.utilities.AppendOnlyPersistentMap import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX import org.bouncycastle.operator.ContentSigner @@ -24,7 +24,7 @@ import javax.persistence.Lob * * This class needs database transactions to be in-flight during method calls and init. */ -class PersistentKeyManagementService(val identityService: IdentityService, +class PersistentKeyManagementService(val identityService: IdentityServiceInternal, initialKeys: Set) : SingletonSerializeAsToken(), KeyManagementService { @Entity diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt b/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt index 2b7e496b32..5634ca74ed 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt @@ -12,9 +12,13 @@ import net.corda.core.node.NodeInfo import net.corda.core.node.services.NetworkMapCache import net.corda.core.node.services.NetworkMapCache.MapChange import net.corda.core.serialization.SingletonSerializeAsToken -import net.corda.core.utilities.* +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.core.utilities.contextLogger +import net.corda.core.utilities.debug +import net.corda.core.utilities.parsePublicKeyBase58 import net.corda.node.internal.Node -import net.corda.node.services.RPCUserService +import net.corda.node.internal.security.Password +import net.corda.node.internal.security.RPCSecurityManager import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.messaging.NodeLoginModule.Companion.NODE_ROLE import net.corda.node.services.messaging.NodeLoginModule.Companion.PEER_ROLE @@ -25,13 +29,13 @@ import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_CLIENT_TLS import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_ROOT_CA import net.corda.nodeapi.internal.crypto.loadKeyStore import net.corda.nodeapi.* +import net.corda.nodeapi.internal.ArtemisMessagingComponent.ArtemisPeerAddress import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.INTERNAL_PREFIX import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_USER import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NOTIFICATIONS_ADDRESS import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_QUEUE import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEERS_PREFIX import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEER_USER -import net.corda.nodeapi.internal.ArtemisMessagingComponent.ArtemisPeerAddress import net.corda.nodeapi.internal.ArtemisMessagingComponent.NodeAddress import net.corda.nodeapi.internal.requireOnDefaultFileSystem import org.apache.activemq.artemis.api.core.SimpleString @@ -97,7 +101,7 @@ class ArtemisMessagingServer(private val config: NodeConfiguration, private val p2pPort: Int, val rpcPort: Int?, val networkMapCache: NetworkMapCache, - val userService: RPCUserService) : SingletonSerializeAsToken() { + val securityManager: RPCSecurityManager) : SingletonSerializeAsToken() { companion object { private val log = contextLogger() /** 10 MiB maximum allowed file size for attachments, including message headers. TODO: acquire this value from Network Map when supported. */ @@ -211,7 +215,12 @@ class ArtemisMessagingServer(private val config: NodeConfiguration, addressFullMessagePolicy = AddressFullMessagePolicy.FAIL } ) - }.configureAddressSecurity() + // JMX enablement + if (config.exportJMXto.isNotEmpty()) {isJMXManagementEnabled = true + isJMXUseBrokerName = true} + + }.configureAddressSecurity() + private fun queueConfig(name: String, address: String = name, filter: String? = null, durable: Boolean): CoreQueueConfiguration { return CoreQueueConfiguration().apply { @@ -229,13 +238,11 @@ class ArtemisMessagingServer(private val config: NodeConfiguration, * 3. RPC users. These are only given sufficient access to perform RPC with us. * 4. Verifiers. These are given read access to the verification request queue and write access to the response queue. */ - private fun ConfigurationImpl.configureAddressSecurity() : Pair { + private fun ConfigurationImpl.configureAddressSecurity(): Pair { val nodeInternalRole = Role(NODE_ROLE, true, true, true, true, true, true, true, true) securityRoles["$INTERNAL_PREFIX#"] = setOf(nodeInternalRole) // Do not add any other roles here as it's only for the node securityRoles[P2P_QUEUE] = setOf(nodeInternalRole, restrictedRole(PEER_ROLE, send = true)) securityRoles[RPCApi.RPC_SERVER_QUEUE_NAME] = setOf(nodeInternalRole, restrictedRole(RPC_ROLE, send = true)) - // TODO: remove the NODE_USER role below once the webserver doesn't need it anymore. - securityRoles["${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.$NODE_USER.#"] = setOf(nodeInternalRole) // Each RPC user must have its own role and its own queue. This prevents users accessing each other's queues // and stealing RPC responses. val rolesAdderOnLogin = RolesAdderOnLogin { username -> @@ -282,7 +289,7 @@ class ArtemisMessagingServer(private val config: NodeConfiguration, override fun getAppConfigurationEntry(name: String): Array { val options = mapOf( LoginListener::javaClass.name to loginListener, - RPCUserService::class.java.name to userService, + RPCSecurityManager::class.java.name to securityManager, NodeLoginModule.CERT_CHAIN_CHECKS_OPTION_NAME to certChecks) return arrayOf(AppConfigurationEntry(name, REQUIRED, options)) } @@ -557,7 +564,7 @@ class NodeLoginModule : LoginModule { private var loginSucceeded: Boolean = false private lateinit var subject: Subject private lateinit var callbackHandler: CallbackHandler - private lateinit var userService: RPCUserService + private lateinit var securityManager: RPCSecurityManager private lateinit var loginListener: LoginListener private lateinit var peerCertCheck: CertificateChainCheckPolicy.Check private lateinit var nodeCertCheck: CertificateChainCheckPolicy.Check @@ -567,7 +574,7 @@ class NodeLoginModule : LoginModule { override fun initialize(subject: Subject, callbackHandler: CallbackHandler, sharedState: Map, options: Map) { this.subject = subject this.callbackHandler = callbackHandler - userService = options[RPCUserService::class.java.name] as RPCUserService + securityManager = options[RPCSecurityManager::class.java.name] as RPCSecurityManager loginListener = options[LoginListener::javaClass.name] as LoginListener val certChainChecks: Map = uncheckedCast(options[CERT_CHAIN_CHECKS_OPTION_NAME]) peerCertCheck = certChainChecks[PEER_ROLE]!! @@ -598,7 +605,7 @@ class NodeLoginModule : LoginModule { PEER_ROLE -> authenticatePeer(certificates) NODE_ROLE -> authenticateNode(certificates) VERIFIER_ROLE -> authenticateVerifier(certificates) - RPC_ROLE -> authenticateRpcUser(password, username) + RPC_ROLE -> authenticateRpcUser(username, Password(password)) else -> throw FailedLoginException("Peer does not belong on our network") } principals += UserPrincipal(validatedUser) @@ -629,13 +636,8 @@ class NodeLoginModule : LoginModule { return certificates.first().subjectDN.name } - private fun authenticateRpcUser(password: String, username: String): String { - val rpcUser = userService.getUser(username) ?: throw FailedLoginException("User does not exist") - if (password != rpcUser.password) { - // TODO Switch to hashed passwords - // TODO Retrieve client IP address to include in exception message - throw FailedLoginException("Password for user $username does not match") - } + private fun authenticateRpcUser(username: String, password: Password): String { + securityManager.authenticate(username, password) loginListener(username) principals += RolePrincipal(RPC_ROLE) // This enables the RPC client to send requests principals += RolePrincipal("${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.$username") // This enables the RPC client to receive responses diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/RPCMessagingClient.kt b/node/src/main/kotlin/net/corda/node/services/messaging/RPCMessagingClient.kt index 0d7bd5a314..3d1ee3e3d6 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/RPCMessagingClient.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/RPCMessagingClient.kt @@ -4,7 +4,7 @@ import net.corda.core.identity.CordaX500Name import net.corda.core.messaging.RPCOps import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.utilities.NetworkHostAndPort -import net.corda.node.services.RPCUserService +import net.corda.node.internal.security.RPCSecurityManager import net.corda.nodeapi.internal.config.SSLConfiguration import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_USER import net.corda.nodeapi.internal.crypto.X509Utilities @@ -16,10 +16,10 @@ class RPCMessagingClient(private val config: SSLConfiguration, serverAddress: Ne private val artemis = ArtemisMessagingClient(config, serverAddress) private var rpcServer: RPCServer? = null - fun start(rpcOps: RPCOps, userService: RPCUserService) = synchronized(this) { + fun start(rpcOps: RPCOps, securityManager: RPCSecurityManager) = synchronized(this) { val locator = artemis.start().sessionFactory.serverLocator val myCert = loadKeyStore(config.sslKeystore, config.keyStorePassword).getX509Certificate(X509Utilities.CORDA_CLIENT_TLS) - rpcServer = RPCServer(rpcOps, NODE_USER, NODE_USER, locator, userService, CordaX500Name.build(myCert.subjectX500Principal)) + rpcServer = RPCServer(rpcOps, NODE_USER, NODE_USER, locator, securityManager, CordaX500Name.build(myCert.subjectX500Principal)) } fun start2(serverControl: ActiveMQServerControl) = synchronized(this) { diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt b/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt index 2fe7a4b13d..0e06674aa0 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt @@ -26,11 +26,10 @@ import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializationDefaults.RPC_SERVER_CONTEXT import net.corda.core.serialization.deserialize import net.corda.core.utilities.* -import net.corda.node.services.RPCUserService +import net.corda.node.internal.security.AuthorizingSubject +import net.corda.node.internal.security.RPCSecurityManager import net.corda.node.services.logging.pushToLoggingContext import net.corda.nodeapi.* -import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_USER -import net.corda.nodeapi.internal.config.User import org.apache.activemq.artemis.api.core.Message import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.api.core.client.ActiveMQClient.DEFAULT_ACK_BATCH_SIZE @@ -85,7 +84,7 @@ class RPCServer( private val rpcServerUsername: String, private val rpcServerPassword: String, private val serverLocator: ServerLocator, - private val userService: RPCUserService, + private val securityManager: RPCSecurityManager, private val nodeLegalName: CordaX500Name, private val rpcConfiguration: RPCServerConfiguration = RPCServerConfiguration.default ) { @@ -213,6 +212,7 @@ class RPCServer( reaperScheduledFuture?.cancel(false) rpcExecutor?.shutdownNow() reaperExecutor?.shutdownNow() + securityManager.close() sessionAndConsumers.forEach { it.sessionFactory.close() } @@ -357,9 +357,6 @@ class RPCServer( observableMap.cleanUp() } - // TODO remove this User once webserver doesn't need it - private val nodeUser = User(NODE_USER, NODE_USER, setOf()) - private fun ClientMessage.context(sessionId: Trace.SessionId): RpcAuthContext { val trace = Trace.newInstance(sessionId = sessionId) val externalTrace = externalTrace() @@ -368,19 +365,10 @@ class RPCServer( return RpcAuthContext(InvocationContext.rpc(rpcActor.first, trace, externalTrace, impersonatedActor), rpcActor.second) } - private fun actorFrom(message: ClientMessage): Pair { + private fun actorFrom(message: ClientMessage): Pair { val validatedUser = message.getStringProperty(Message.HDR_VALIDATED_USER) ?: throw IllegalArgumentException("Missing validated user from the Artemis message") val targetLegalIdentity = message.getStringProperty(RPCApi.RPC_TARGET_LEGAL_IDENTITY)?.let(CordaX500Name.Companion::parse) ?: nodeLegalName - // TODO switch userService based on targetLegalIdentity - val rpcUser = userService.getUser(validatedUser) - return if (rpcUser != null) { - Actor(Id(rpcUser.username), userService.id, targetLegalIdentity) to RpcPermissions(rpcUser.permissions) - } else if (CordaX500Name.parse(validatedUser) == nodeLegalName) { - // TODO remove this after Shell and WebServer will no longer need it - Actor(Id(nodeUser.username), userService.id, targetLegalIdentity) to RpcPermissions(nodeUser.permissions) - } else { - throw IllegalArgumentException("Validated user '$validatedUser' is not an RPC user nor the NODE user") - } + return Pair(Actor(Id(validatedUser), securityManager.id, targetLegalIdentity), securityManager.buildSubject(validatedUser)) } } diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/RpcAuthContext.kt b/node/src/main/kotlin/net/corda/node/services/messaging/RpcAuthContext.kt index 58cd73de22..dfa50b16d7 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/RpcAuthContext.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/RpcAuthContext.kt @@ -1,30 +1,9 @@ package net.corda.node.services.messaging -import net.corda.client.rpc.PermissionException import net.corda.core.context.InvocationContext -import net.corda.node.services.Permissions -import net.corda.nodeapi.internal.ArtemisMessagingComponent +import net.corda.node.internal.security.AuthorizingSubject -data class RpcAuthContext(val invocation: InvocationContext, val grantedPermissions: RpcPermissions) { +data class RpcAuthContext(val invocation: InvocationContext, + private val authorizer: AuthorizingSubject) + : AuthorizingSubject by authorizer - fun requirePermission(permission: String) = requireEitherPermission(setOf(permission)) - - fun requireEitherPermission(permissions: Set): RpcAuthContext { - - // TODO remove the NODE_USER condition once webserver and shell won't need it anymore - if (invocation.principal().name != ArtemisMessagingComponent.NODE_USER && !grantedPermissions.coverAny(permissions)) { - throw PermissionException("User not permissioned with any of $permissions, permissions are ${this.grantedPermissions}.") - } - return this - } -} - -data class RpcPermissions(private val values: Set = emptySet()) { - - companion object { - val NONE = RpcPermissions() - val ALL = RpcPermissions(setOf("ALL")) - } - - fun coverAny(permissions: Set) = !values.intersect(permissions + Permissions.all()).isEmpty() -} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/network/NetworkMapClient.kt b/node/src/main/kotlin/net/corda/node/services/network/NetworkMapClient.kt index 02f4310061..9563bb9d3d 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/NetworkMapClient.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/NetworkMapClient.kt @@ -88,6 +88,7 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal, // Only publish and write to disk if there are changes to the node info. val signedNodeInfo = signNodeInfo(newInfo) + networkMapCache.addNode(newInfo) fileWatcher.saveToFile(signedNodeInfo) if (networkMapClient != null) { 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 367e596f63..c68f1ece30 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 @@ -20,6 +20,7 @@ import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.serialize import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.contextLogger +import net.corda.core.utilities.loggerFor import net.corda.node.services.api.NetworkMapCacheBaseInternal import net.corda.node.services.api.NetworkMapCacheInternal import net.corda.nodeapi.internal.persistence.CordaPersistence @@ -38,13 +39,22 @@ class NetworkMapCacheImpl( networkMapCacheBase: NetworkMapCacheBaseInternal, private val identityService: IdentityService ) : NetworkMapCacheBaseInternal by networkMapCacheBase, NetworkMapCacheInternal { + companion object { + private val logger = loggerFor() + } + init { networkMapCacheBase.allNodes.forEach { it.legalIdentitiesAndCerts.forEach { identityService.verifyAndRegisterIdentity(it) } } networkMapCacheBase.changed.subscribe { mapChange -> // TODO how should we handle network map removal if (mapChange is MapChange.Added) { mapChange.node.legalIdentitiesAndCerts.forEach { - identityService.verifyAndRegisterIdentity(it) + try { + identityService.verifyAndRegisterIdentity(it) + } catch (ignore: Exception) { + // Log a warning to indicate node info is not added to the network map cache. + logger.warn("Node info for :'${it.name}' is not added to the network map due to verification error.") + } } } } diff --git a/node/src/main/kotlin/net/corda/node/shell/CordaAuthenticationPlugin.kt b/node/src/main/kotlin/net/corda/node/shell/CordaAuthenticationPlugin.kt index 61fbf90f56..7dbdc8e52f 100644 --- a/node/src/main/kotlin/net/corda/node/shell/CordaAuthenticationPlugin.kt +++ b/node/src/main/kotlin/net/corda/node/shell/CordaAuthenticationPlugin.kt @@ -4,31 +4,30 @@ import net.corda.core.context.Actor import net.corda.core.context.InvocationContext import net.corda.core.identity.CordaX500Name import net.corda.core.messaging.CordaRPCOps -import net.corda.node.services.RPCUserService -import net.corda.node.services.messaging.RpcPermissions +import net.corda.node.internal.security.Password +import net.corda.node.internal.security.RPCSecurityManager +import net.corda.node.internal.security.tryAuthenticate import org.crsh.auth.AuthInfo import org.crsh.auth.AuthenticationPlugin import org.crsh.plugin.CRaSHPlugin -class CordaAuthenticationPlugin(val rpcOps:CordaRPCOps, val userService:RPCUserService, val nodeLegalName:CordaX500Name) : CRaSHPlugin>(), AuthenticationPlugin { +class CordaAuthenticationPlugin(private val rpcOps: CordaRPCOps, private val securityManager: RPCSecurityManager, private val nodeLegalName: CordaX500Name) : CRaSHPlugin>(), AuthenticationPlugin { override fun getImplementation(): AuthenticationPlugin = this override fun getName(): String = "corda" override fun authenticate(username: String?, credential: String?): AuthInfo { + if (username == null || credential == null) { return AuthInfo.UNSUCCESSFUL } - - val user = userService.getUser(username) - - if (user != null && user.password == credential) { - val actor = Actor(Actor.Id(username), userService.id, nodeLegalName) - return CordaSSHAuthInfo(true, makeRPCOpsWithContext(rpcOps, InvocationContext.rpc(actor), RpcPermissions(user.permissions))) + val authorizingSubject = securityManager.tryAuthenticate(username, Password(credential)) + if (authorizingSubject != null) { + val actor = Actor(Actor.Id(username), securityManager.id, nodeLegalName) + return CordaSSHAuthInfo(true, makeRPCOpsWithContext(rpcOps, InvocationContext.rpc(actor), authorizingSubject)) } - - return AuthInfo.UNSUCCESSFUL; + return AuthInfo.UNSUCCESSFUL } override fun getCredentialType(): Class = String::class.java diff --git a/node/src/main/kotlin/net/corda/node/shell/InteractiveShell.kt b/node/src/main/kotlin/net/corda/node/shell/InteractiveShell.kt index 3772e15290..ea6779f983 100644 --- a/node/src/main/kotlin/net/corda/node/shell/InteractiveShell.kt +++ b/node/src/main/kotlin/net/corda/node/shell/InteractiveShell.kt @@ -25,11 +25,11 @@ import net.corda.core.messaging.StateMachineUpdate import net.corda.core.node.services.IdentityService import net.corda.node.internal.Node import net.corda.node.internal.StartedNode -import net.corda.node.services.RPCUserService +import net.corda.node.internal.security.AdminSubject +import net.corda.node.internal.security.RPCSecurityManager import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.messaging.CURRENT_RPC_CONTEXT import net.corda.node.services.messaging.RpcAuthContext -import net.corda.node.services.messaging.RpcPermissions import net.corda.node.utilities.ANSIProgressRenderer import net.corda.node.utilities.StdoutANSIProgressRenderer import net.corda.nodeapi.internal.persistence.CordaPersistence @@ -82,19 +82,19 @@ object InteractiveShell { private lateinit var node: StartedNode @VisibleForTesting internal lateinit var database: CordaPersistence - private lateinit var rpcOps:CordaRPCOps - private lateinit var userService:RPCUserService - private lateinit var identityService:IdentityService - private var shell:Shell? = null + private lateinit var rpcOps: CordaRPCOps + private lateinit var securityManager: RPCSecurityManager + private lateinit var identityService: IdentityService + private var shell: Shell? = null private lateinit var nodeLegalName: CordaX500Name /** * Starts an interactive shell connected to the local terminal. This shell gives administrator access to the node * internals. */ - fun startShell(configuration:NodeConfiguration, cordaRPCOps: CordaRPCOps, userService: RPCUserService, identityService: IdentityService, database: CordaPersistence) { + fun startShell(configuration: NodeConfiguration, cordaRPCOps: CordaRPCOps, securityManager: RPCSecurityManager, identityService: IdentityService, database: CordaPersistence) { this.rpcOps = cordaRPCOps - this.userService = userService + this.securityManager = securityManager this.identityService = identityService this.nodeLegalName = configuration.myLegalName this.database = database @@ -123,14 +123,14 @@ object InteractiveShell { } } - fun runLocalShell(node:StartedNode) { + fun runLocalShell(node: StartedNode) { val terminal = TerminalFactory.create() val consoleReader = ConsoleReader("Corda", FileInputStream(FileDescriptor.`in`), System.out, terminal) val jlineProcessor = JLineProcessor(terminal.isAnsiSupported, shell, consoleReader, System.out) InterruptHandler { jlineProcessor.interrupt() }.install() thread(name = "Command line shell processor", isDaemon = true) { // Give whoever has local shell access administrator access to the node. - val context = RpcAuthContext(net.corda.core.context.InvocationContext.shell(), RpcPermissions.ALL) + val context = RpcAuthContext(net.corda.core.context.InvocationContext.shell(), AdminSubject("SHELL_USER")) CURRENT_RPC_CONTEXT.set(context) Emoji.renderIfSupported { jlineProcessor.run() @@ -169,7 +169,7 @@ object InteractiveShell { // Don't use the Java language plugin (we may not have tools.jar available at runtime), this // will cause any commands using JIT Java compilation to be suppressed. In CRaSH upstream that // is only the 'jmx' command. - return super.getPlugins().filterNot { it is JavaLanguage } + CordaAuthenticationPlugin(rpcOps, userService, nodeLegalName) + return super.getPlugins().filterNot { it is JavaLanguage } + CordaAuthenticationPlugin(rpcOps, securityManager, nodeLegalName) } } val attributes = mapOf( @@ -180,7 +180,7 @@ object InteractiveShell { context.refresh() this.config = config start(context) - return context.getPlugin(ShellFactory::class.java).create(null, CordaSSHAuthInfo(false, makeRPCOpsWithContext(rpcOps, net.corda.core.context.InvocationContext.shell(), RpcPermissions.ALL), StdoutANSIProgressRenderer)) + return context.getPlugin(ShellFactory::class.java).create(null, CordaSSHAuthInfo(false, makeRPCOpsWithContext(rpcOps, net.corda.core.context.InvocationContext.shell(), AdminSubject("SHELL_USER")), StdoutANSIProgressRenderer)) } } @@ -248,7 +248,7 @@ object InteractiveShell { } catch (e: NoApplicableConstructor) { output.println("No matching constructor found:", Color.red) e.errors.forEach { output.println("- $it", Color.red) } - } catch (e:PermissionException) { + } catch (e: PermissionException) { output.println(e.message ?: "Access denied", Color.red) } finally { InputStreamDeserializer.closeAll() @@ -271,9 +271,9 @@ object InteractiveShell { */ @Throws(NoApplicableConstructor::class) fun runFlowFromString(invoke: (Class>, Array) -> FlowProgressHandle, - inputData: String, - clazz: Class>, - om: ObjectMapper = yamlInputMapper): FlowProgressHandle { + inputData: String, + clazz: Class>, + om: ObjectMapper = yamlInputMapper): FlowProgressHandle { // For each constructor, attempt to parse the input data as a method call. Use the first that succeeds, // and keep track of the reasons we failed so we can print them out if no constructors are usable. val parser = StringToMethodCallParser(clazz, om) diff --git a/node/src/main/kotlin/net/corda/node/shell/RPCOpsWithContext.kt b/node/src/main/kotlin/net/corda/node/shell/RPCOpsWithContext.kt index afb163fed0..01446bd58d 100644 --- a/node/src/main/kotlin/net/corda/node/shell/RPCOpsWithContext.kt +++ b/node/src/main/kotlin/net/corda/node/shell/RPCOpsWithContext.kt @@ -1,36 +1,39 @@ package net.corda.node.shell import net.corda.core.context.InvocationContext -import net.corda.core.messaging.* +import net.corda.core.messaging.CordaRPCOps import net.corda.core.utilities.getOrThrow +import net.corda.node.internal.security.AuthorizingSubject import net.corda.node.services.messaging.CURRENT_RPC_CONTEXT import net.corda.node.services.messaging.RpcAuthContext -import net.corda.node.services.messaging.RpcPermissions import java.lang.reflect.InvocationTargetException import java.lang.reflect.Proxy import java.util.concurrent.CompletableFuture import java.util.concurrent.Future -fun makeRPCOpsWithContext(cordaRPCOps: CordaRPCOps, invocationContext:InvocationContext, rpcPermissions: RpcPermissions) : CordaRPCOps { - return Proxy.newProxyInstance(CordaRPCOps::class.java.classLoader, arrayOf(CordaRPCOps::class.java), { proxy, method, args -> - RPCContextRunner(invocationContext, rpcPermissions) { - try { - method.invoke(cordaRPCOps, *(args ?: arrayOf())) - } catch (e: InvocationTargetException) { - // Unpack exception. - throw e.targetException - } - }.get().getOrThrow() - }) as CordaRPCOps +fun makeRPCOpsWithContext(cordaRPCOps: CordaRPCOps, invocationContext:InvocationContext, authorizingSubject: AuthorizingSubject) : CordaRPCOps { + + return Proxy.newProxyInstance(CordaRPCOps::class.java.classLoader, arrayOf(CordaRPCOps::class.java), { _, method, args -> + RPCContextRunner(invocationContext, authorizingSubject) { + try { + method.invoke(cordaRPCOps, *(args ?: arrayOf())) + } catch (e: InvocationTargetException) { + // Unpack exception. + throw e.targetException + } + }.get().getOrThrow() + }) as CordaRPCOps } -private class RPCContextRunner(val invocationContext:InvocationContext, val rpcPermissions: RpcPermissions, val block:() -> T) : Thread() { +private class RPCContextRunner(val invocationContext: InvocationContext, val authorizingSubject: AuthorizingSubject, val block:() -> T): Thread() { + private var result: CompletableFuture = CompletableFuture() + override fun run() { - CURRENT_RPC_CONTEXT.set(RpcAuthContext(invocationContext, rpcPermissions)) + CURRENT_RPC_CONTEXT.set(RpcAuthContext(invocationContext, authorizingSubject)) try { result.complete(block()) - } catch (e:Throwable) { + } catch (e: Throwable) { result.completeExceptionally(e) } finally { CURRENT_RPC_CONTEXT.remove() diff --git a/node/src/main/kotlin/net/corda/node/utilities/ServiceIdentityGenerator.kt b/node/src/main/kotlin/net/corda/node/utilities/ServiceIdentityGenerator.kt index a0aad8c6d2..4bb48e34c9 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/ServiceIdentityGenerator.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/ServiceIdentityGenerator.kt @@ -35,8 +35,8 @@ object ServiceIdentityGenerator { val rootCert = caKeyStore.getCertificate(X509Utilities.CORDA_ROOT_CA) keyPairs.zip(dirs) { keyPair, dir -> - val serviceKeyCert = X509Utilities.createCertificate(CertificateType.CLIENT_CA, issuer.certificate, issuer.keyPair, serviceName, keyPair.public) - val compositeKeyCert = X509Utilities.createCertificate(CertificateType.CLIENT_CA, issuer.certificate, issuer.keyPair, serviceName, notaryKey) + val serviceKeyCert = X509Utilities.createCertificate(CertificateType.NODE_CA, issuer.certificate, issuer.keyPair, serviceName, keyPair.public) + val compositeKeyCert = X509Utilities.createCertificate(CertificateType.NODE_CA, issuer.certificate, issuer.keyPair, serviceName, notaryKey) val certPath = (dir / "certificates").createDirectories() / "distributedService.jks" val keystore = loadOrCreateKeyStore(certPath, "cordacadevpass") val serviceId = serviceName.commonName diff --git a/node/src/main/resources/reference.conf b/node/src/main/resources/reference.conf index a14b06d28a..6205b0d073 100644 --- a/node/src/main/resources/reference.conf +++ b/node/src/main/resources/reference.conf @@ -11,6 +11,7 @@ dataSourceProperties = { } database = { transactionIsolationLevel = "REPEATABLE_READ" + exportHibernateJMXStatistics = "false" } devMode = true useHTTPS = false diff --git a/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java b/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java index 838370c38c..48aa58a07f 100644 --- a/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java +++ b/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java @@ -4,9 +4,9 @@ import com.google.common.collect.ImmutableSet; import kotlin.Pair; import kotlin.Triple; import net.corda.core.contracts.*; +import net.corda.core.crypto.CryptoUtils; import net.corda.core.identity.AbstractParty; import net.corda.core.messaging.DataFeed; -import net.corda.core.node.services.IdentityService; import net.corda.core.node.services.Vault; import net.corda.core.node.services.VaultQueryException; import net.corda.core.node.services.VaultService; @@ -14,11 +14,11 @@ import net.corda.core.node.services.vault.*; import net.corda.core.node.services.vault.QueryCriteria.LinearStateQueryCriteria; import net.corda.core.node.services.vault.QueryCriteria.VaultCustomQueryCriteria; import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria; -import net.corda.core.utilities.EncodingUtils; import net.corda.core.utilities.OpaqueBytes; import net.corda.finance.contracts.DealState; import net.corda.finance.contracts.asset.Cash; import net.corda.finance.schemas.CashSchemaV1; +import net.corda.node.services.api.IdentityServiceInternal; import net.corda.nodeapi.internal.persistence.CordaPersistence; import net.corda.nodeapi.internal.persistence.DatabaseTransaction; import net.corda.testing.SerializationEnvironmentRule; @@ -61,13 +61,13 @@ public class VaultQueryJavaTests { @Before public void setUp() throws CertificateException, InvalidAlgorithmParameterException { List cordappPackages = Arrays.asList("net.corda.testing.contracts", "net.corda.finance.contracts.asset", CashSchemaV1.class.getPackage().getName()); - IdentityService identitySvc = makeTestIdentityService(Arrays.asList(getMEGA_CORP_IDENTITY(), getDUMMY_CASH_ISSUER_IDENTITY(), getDUMMY_NOTARY_IDENTITY())); + IdentityServiceInternal identitySvc = makeTestIdentityService(Arrays.asList(getMEGA_CORP_IDENTITY(), getDUMMY_CASH_ISSUER_IDENTITY(), getDUMMY_NOTARY_IDENTITY())); Pair databaseAndServices = makeTestDatabaseAndMockServices( Arrays.asList(getMEGA_CORP_KEY(), getDUMMY_NOTARY_KEY()), identitySvc, cordappPackages, getMEGA_CORP().getName()); - issuerServices = new MockServices(cordappPackages, getDUMMY_CASH_ISSUER_NAME(), getDUMMY_CASH_ISSUER_KEY(), getBOC_KEY()); + issuerServices = new MockServices(cordappPackages, rigorousMock(IdentityServiceInternal.class), getDUMMY_CASH_ISSUER_NAME(), getDUMMY_CASH_ISSUER_KEY(), getBOC_KEY()); database = databaseAndServices.getFirst(); MockServices services = databaseAndServices.getSecond(); vaultFiller = new VaultFiller(services, getDUMMY_NOTARY(), getDUMMY_NOTARY_KEY()); @@ -449,30 +449,28 @@ public class VaultQueryJavaTests { // DOCSTART VaultJavaQueryExample23 Field pennies = CashSchemaV1.PersistentCashState.class.getDeclaredField("pennies"); Field currency = CashSchemaV1.PersistentCashState.class.getDeclaredField("currency"); - Field issuerParty = CashSchemaV1.PersistentCashState.class.getDeclaredField("issuerParty"); - - QueryCriteria sumCriteria = new VaultCustomQueryCriteria(Builder.sum(pennies, Arrays.asList(issuerParty, currency), Sort.Direction.DESC)); - + Field issuerPartyHash = CashSchemaV1.PersistentCashState.class.getDeclaredField("issuerPartyHash"); + QueryCriteria sumCriteria = new VaultCustomQueryCriteria(Builder.sum(pennies, Arrays.asList(issuerPartyHash, currency), Sort.Direction.DESC)); Vault.Page results = vaultService.queryBy(Cash.State.class, sumCriteria); // DOCEND VaultJavaQueryExample23 assertThat(results.getOtherResults()).hasSize(12); assertThat(results.getOtherResults().get(0)).isEqualTo(400L); - assertThat(results.getOtherResults().get(1)).isEqualTo(EncodingUtils.toBase58String(getBOC_PUBKEY())); + assertThat(results.getOtherResults().get(1)).isEqualTo(CryptoUtils.toStringShort(getBOC_PUBKEY())); assertThat(results.getOtherResults().get(2)).isEqualTo("GBP"); assertThat(results.getOtherResults().get(3)).isEqualTo(300L); - assertThat(results.getOtherResults().get(4)).isEqualTo(EncodingUtils.toBase58String(getDUMMY_CASH_ISSUER().getParty().getOwningKey())); + assertThat(results.getOtherResults().get(4)).isEqualTo(CryptoUtils.toStringShort(getDUMMY_CASH_ISSUER().getParty().getOwningKey())); assertThat(results.getOtherResults().get(5)).isEqualTo("GBP"); assertThat(results.getOtherResults().get(6)).isEqualTo(200L); - assertThat(results.getOtherResults().get(7)).isEqualTo(EncodingUtils.toBase58String(getBOC_PUBKEY())); + assertThat(results.getOtherResults().get(7)).isEqualTo(CryptoUtils.toStringShort(getBOC_PUBKEY())); assertThat(results.getOtherResults().get(8)).isEqualTo("USD"); assertThat(results.getOtherResults().get(9)).isEqualTo(100L); - assertThat(results.getOtherResults().get(10)).isEqualTo(EncodingUtils.toBase58String(getDUMMY_CASH_ISSUER().getParty().getOwningKey())); + assertThat(results.getOtherResults().get(10)).isEqualTo(CryptoUtils.toStringShort(getDUMMY_CASH_ISSUER().getParty().getOwningKey())); assertThat(results.getOtherResults().get(11)).isEqualTo("USD"); } catch (NoSuchFieldException e) { - e.printStackTrace(); + throw new RuntimeException(e); } return tx; }); diff --git a/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt b/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt index 5ddff0c2d5..a579538eab 100644 --- a/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt +++ b/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt @@ -2,6 +2,7 @@ package net.corda.node import co.paralleluniverse.fibers.Suspendable import net.corda.client.rpc.PermissionException +import net.corda.core.context.AuthServiceId import net.corda.core.context.InvocationContext import net.corda.core.contracts.Amount import net.corda.core.contracts.ContractState @@ -26,11 +27,12 @@ import net.corda.finance.flows.CashIssueFlow import net.corda.finance.flows.CashPaymentFlow import net.corda.node.internal.SecureCordaRPCOps import net.corda.node.internal.StartedNode +import net.corda.node.internal.security.RPCSecurityManagerImpl import net.corda.node.services.Permissions.Companion.invokeRpc import net.corda.node.services.Permissions.Companion.startFlow import net.corda.node.services.messaging.CURRENT_RPC_CONTEXT import net.corda.node.services.messaging.RpcAuthContext -import net.corda.node.services.messaging.RpcPermissions +import net.corda.nodeapi.internal.config.User import net.corda.testing.* import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNetwork.MockNode @@ -48,6 +50,15 @@ import kotlin.test.assertFalse import kotlin.test.assertNull import kotlin.test.assertTrue +// Mock an AuthorizingSubject instance sticking to a fixed set of permissions +private fun buildSubject(principal: String, permissionStrings: Set) = + RPCSecurityManagerImpl.fromUserList( + id = AuthServiceId("TEST"), + users = listOf(User(username = principal, + password = "", + permissions = permissionStrings))) + .buildSubject(principal) + class CordaRPCOpsImplTest { private companion object { val testJar = "net/corda/node/testing/test.jar" @@ -67,7 +78,7 @@ class CordaRPCOpsImplTest { mockNet = MockNetwork(cordappPackages = listOf("net.corda.finance.contracts.asset")) aliceNode = mockNet.createNode(MockNodeParameters(legalName = ALICE_NAME)) rpc = SecureCordaRPCOps(aliceNode.services, aliceNode.smm, aliceNode.database, aliceNode.services) - CURRENT_RPC_CONTEXT.set(RpcAuthContext(InvocationContext.rpc(testActor()), RpcPermissions.NONE)) + CURRENT_RPC_CONTEXT.set(RpcAuthContext(InvocationContext.rpc(testActor()), buildSubject("TEST_USER", emptySet()))) mockNet.runNetwork() withPermissions(invokeRpc(CordaRPCOps::notaryIdentities)) { @@ -301,7 +312,8 @@ class CordaRPCOpsImplTest { val previous = CURRENT_RPC_CONTEXT.get() try { - CURRENT_RPC_CONTEXT.set(previous.copy(grantedPermissions = RpcPermissions(permissions.toSet()))) + CURRENT_RPC_CONTEXT.set(previous.copy(authorizer = + buildSubject(previous.principal, permissions.toSet()))) action.invoke() } finally { CURRENT_RPC_CONTEXT.set(previous) diff --git a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt index 8fdb10a1cb..c6fa6c2aac 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt @@ -1,6 +1,8 @@ package net.corda.node.messaging import co.paralleluniverse.fibers.Suspendable +import com.nhaarman.mockito_kotlin.doReturn +import com.nhaarman.mockito_kotlin.whenever import net.corda.core.concurrent.CordaFuture import net.corda.core.contracts.* import net.corda.core.crypto.* @@ -34,6 +36,7 @@ import net.corda.finance.flows.TwoPartyTradeFlow.Buyer import net.corda.finance.flows.TwoPartyTradeFlow.Seller import net.corda.node.internal.StartedNode import net.corda.node.services.api.WritableTransactionStorage +import net.corda.node.services.api.IdentityServiceInternal import net.corda.node.services.persistence.DBTransactionStorage import net.corda.node.services.persistence.checkpoints import net.corda.nodeapi.internal.persistence.CordaPersistence @@ -90,13 +93,15 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) { // we run in the unit test thread exclusively to speed things up, ensure deterministic results and // allow interruption half way through. mockNet = MockNetwork(threadPerNode = true, cordappPackages = cordappPackages) - ledger(MockServices(cordappPackages)) { + val ledgerIdentityService = rigorousMock() + ledger(MockServices(cordappPackages, ledgerIdentityService, MEGA_CORP.name)) { val notaryNode = mockNet.defaultNotaryNode val aliceNode = mockNet.createPartyNode(ALICE_NAME) val bobNode = mockNet.createPartyNode(BOB_NAME) val bankNode = mockNet.createPartyNode(BOC_NAME) val alice = aliceNode.info.singleIdentity() val bank = bankNode.info.singleIdentity() + doReturn(null).whenever(ledgerIdentityService).partyFromKey(bank.owningKey) val bob = bobNode.info.singleIdentity() val notary = mockNet.defaultNotaryIdentity val cashIssuer = bank.ref(1) @@ -140,13 +145,15 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) { @Test(expected = InsufficientBalanceException::class) fun `trade cash for commercial paper fails using soft locking`() { mockNet = MockNetwork(threadPerNode = true, cordappPackages = cordappPackages) - ledger(MockServices(cordappPackages)) { + val ledgerIdentityService = rigorousMock() + ledger(MockServices(cordappPackages, ledgerIdentityService, MEGA_CORP.name)) { val notaryNode = mockNet.defaultNotaryNode val aliceNode = mockNet.createPartyNode(ALICE_NAME) val bobNode = mockNet.createPartyNode(BOB_NAME) val bankNode = mockNet.createPartyNode(BOC_NAME) val alice = aliceNode.info.singleIdentity() val bank = bankNode.info.singleIdentity() + doReturn(null).whenever(ledgerIdentityService).partyFromKey(bank.owningKey) val bob = bobNode.info.singleIdentity() val issuer = bank.ref(1) val notary = mockNet.defaultNotaryIdentity @@ -196,7 +203,8 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) { @Test fun `shutdown and restore`() { mockNet = MockNetwork(cordappPackages = cordappPackages) - ledger(MockServices(cordappPackages)) { + val ledgerIdentityService = rigorousMock() + ledger(MockServices(cordappPackages, ledgerIdentityService, MEGA_CORP.name)) { val notaryNode = mockNet.defaultNotaryNode val aliceNode = mockNet.createPartyNode(ALICE_NAME) var bobNode = mockNet.createPartyNode(BOB_NAME) @@ -210,6 +218,7 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) { val notary = mockNet.defaultNotaryIdentity val alice = aliceNode.info.singleIdentity() val bank = bankNode.info.singleIdentity() + doReturn(null).whenever(ledgerIdentityService).partyFromKey(bank.owningKey) val bob = bobNode.info.singleIdentity() val issuer = bank.ref(1, 2, 3) @@ -491,16 +500,18 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) { @Test fun `dependency with error on buyer side`() { mockNet = MockNetwork(cordappPackages = cordappPackages) - ledger(MockServices(cordappPackages)) { - runWithError(true, false, "at least one cash input") + val ledgerIdentityService = rigorousMock() + ledger(MockServices(cordappPackages, ledgerIdentityService, MEGA_CORP.name)) { + runWithError(ledgerIdentityService, true, false, "at least one cash input") } } @Test fun `dependency with error on seller side`() { mockNet = MockNetwork(cordappPackages = cordappPackages) - ledger(MockServices(cordappPackages)) { - runWithError(false, true, "Issuances have a time-window") + val ledgerIdentityService = rigorousMock() + ledger(MockServices(cordappPackages, ledgerIdentityService, MEGA_CORP.name)) { + runWithError(ledgerIdentityService, false, true, "Issuances have a time-window") } } @@ -562,6 +573,7 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) { data class TestTx(val notaryIdentity: Party, val price: Amount, val anonymous: Boolean) private fun LedgerDSL.runWithError( + ledgerIdentityService: IdentityServiceInternal, bobError: Boolean, aliceError: Boolean, expectedMessageSubstring: String @@ -575,6 +587,7 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) { val alice = aliceNode.info.singleIdentity() val bob = bobNode.info.singleIdentity() val bank = bankNode.info.singleIdentity() + doReturn(null).whenever(ledgerIdentityService).partyFromKey(bank.owningKey) val issuer = bank.ref(1, 2, 3) val bobsBadCash = bobNode.database.transaction { diff --git a/node/src/test/kotlin/net/corda/node/services/RPCUserServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/RPCSecurityManagerTest.kt similarity index 68% rename from node/src/test/kotlin/net/corda/node/services/RPCUserServiceTest.kt rename to node/src/test/kotlin/net/corda/node/services/RPCSecurityManagerTest.kt index 0f54c85d1c..2866d41dc4 100644 --- a/node/src/test/kotlin/net/corda/node/services/RPCUserServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/RPCSecurityManagerTest.kt @@ -1,11 +1,12 @@ package net.corda.node.services - +import net.corda.core.context.AuthServiceId +import net.corda.node.internal.security.RPCSecurityManagerImpl import net.corda.nodeapi.internal.config.User import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.Test -class RPCUserServiceTest { +class RPCSecurityManagerTest { @Test fun `Artemis special characters not permitted in RPC usernames`() { @@ -15,6 +16,6 @@ class RPCUserServiceTest { } private fun configWithRPCUsername(username: String) { - RPCUserServiceImpl(listOf(User(username, "password", setOf()))) + RPCSecurityManagerImpl.fromUserList(users = listOf(User(username, "password", setOf())), id = AuthServiceId("TEST")) } } \ No newline at end of file diff --git a/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt index 857f9ded5c..2fc758926a 100644 --- a/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt @@ -4,6 +4,7 @@ import co.paralleluniverse.fibers.Suspendable import com.codahale.metrics.MetricRegistry import com.nhaarman.mockito_kotlin.* import net.corda.core.contracts.* +import net.corda.core.crypto.generateKeyPair import net.corda.core.crypto.newSecureRandom import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogicRef @@ -21,7 +22,6 @@ import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.internal.cordapp.CordappProviderImpl import net.corda.node.services.api.MonitoringService import net.corda.node.services.api.ServiceHubInternal -import net.corda.node.services.identity.InMemoryIdentityService import net.corda.node.services.network.NetworkMapCacheImpl import net.corda.node.services.persistence.DBCheckpointStorage import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl @@ -51,6 +51,7 @@ import kotlin.test.assertTrue class NodeSchedulerServiceTest : SingletonSerializeAsToken() { companion object { + private val DUMMY_IDENTITY_1 = getTestPartyAndCertificate(Party(CordaX500Name("Dummy", "Madrid", "ES"), generateKeyPair().public)) private val myInfo = NodeInfo(listOf(MOCK_HOST_AND_PORT), listOf(DUMMY_IDENTITY_1), 1, serial = 1L) } @@ -293,7 +294,7 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() { database.transaction { apply { val freshKey = kms.freshKey() - val state = TestState(FlowLogicRefFactoryImpl.createForRPC(TestFlowLogic::class.java, increment), instant, myInfo.chooseIdentity()) + val state = TestState(FlowLogicRefFactoryImpl.createForRPC(TestFlowLogic::class.java, increment), instant, DUMMY_IDENTITY_1.party) val builder = TransactionBuilder(null).apply { addOutputState(state, DummyContract.PROGRAM_ID, DUMMY_NOTARY) addCommand(Command(), freshKey) diff --git a/node/src/test/kotlin/net/corda/node/services/identity/InMemoryIdentityServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/identity/InMemoryIdentityServiceTests.kt index 9ed1ed2a4a..782c0fdf53 100644 --- a/node/src/test/kotlin/net/corda/node/services/identity/InMemoryIdentityServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/identity/InMemoryIdentityServiceTests.kt @@ -9,7 +9,6 @@ import net.corda.core.identity.PartyAndCertificate import net.corda.core.internal.cert import net.corda.core.internal.toX509CertHolder import net.corda.core.node.services.UnknownAnonymousPartyException -import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair import net.corda.nodeapi.internal.crypto.CertificateType import net.corda.nodeapi.internal.crypto.X509CertificateFactory import net.corda.nodeapi.internal.crypto.X509Utilities @@ -108,8 +107,8 @@ class InMemoryIdentityServiceTests { */ @Test fun `get anonymous identity by key`() { - val (alice, aliceTxIdentity) = createParty(ALICE.name, DEV_CA) - val (_, bobTxIdentity) = createParty(ALICE.name, DEV_CA) + val (alice, aliceTxIdentity) = createConfidentialIdentity(ALICE.name) + val (_, bobTxIdentity) = createConfidentialIdentity(ALICE.name) // Now we have identities, construct the service and let it know about both val service = createService(alice) @@ -131,8 +130,8 @@ class InMemoryIdentityServiceTests { @Test fun `assert ownership`() { withTestSerialization { - val (alice, anonymousAlice) = createParty(ALICE.name, DEV_CA) - val (bob, anonymousBob) = createParty(BOB.name, DEV_CA) + val (alice, anonymousAlice) = createConfidentialIdentity(ALICE.name) + val (bob, anonymousBob) = createConfidentialIdentity(BOB.name) // Now we have identities, construct the service and let it know about both val service = createService(alice, bob) @@ -157,11 +156,11 @@ class InMemoryIdentityServiceTests { } } - private fun createParty(x500Name: CordaX500Name, ca: CertificateAndKeyPair): Pair { + private fun createConfidentialIdentity(x500Name: CordaX500Name): Pair { val issuerKeyPair = generateKeyPair() - val issuer = getTestPartyAndCertificate(x500Name, issuerKeyPair.public, ca) + val issuer = getTestPartyAndCertificate(x500Name, issuerKeyPair.public) val txKey = Crypto.generateKeyPair() - val txCert = X509Utilities.createCertificate(CertificateType.IDENTITY, issuer.certificate.toX509CertHolder(), issuerKeyPair, x500Name, txKey.public) + val txCert = X509Utilities.createCertificate(CertificateType.CONFIDENTIAL_IDENTITY, issuer.certificate.toX509CertHolder(), issuerKeyPair, x500Name, txKey.public) val txCertPath = X509CertificateFactory().delegate.generateCertPath(listOf(txCert.cert) + issuer.certPath.certificates) return Pair(issuer, PartyAndCertificate(txCertPath)) } diff --git a/node/src/test/kotlin/net/corda/node/services/identity/PersistentIdentityServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/identity/PersistentIdentityServiceTests.kt index 0650e74486..e3894fce27 100644 --- a/node/src/test/kotlin/net/corda/node/services/identity/PersistentIdentityServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/identity/PersistentIdentityServiceTests.kt @@ -11,7 +11,6 @@ import net.corda.core.internal.toX509CertHolder import net.corda.core.node.services.IdentityService import net.corda.core.node.services.UnknownAnonymousPartyException import net.corda.node.internal.configureDatabase -import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair import net.corda.nodeapi.internal.crypto.CertificateType import net.corda.nodeapi.internal.crypto.X509CertificateFactory import net.corda.nodeapi.internal.crypto.X509Utilities @@ -149,8 +148,8 @@ class PersistentIdentityServiceTests { */ @Test fun `get anonymous identity by key`() { - val (alice, aliceTxIdentity) = createParty(ALICE.name, DEV_CA) - val (_, bobTxIdentity) = createParty(ALICE.name, DEV_CA) + val (alice, aliceTxIdentity) = createConfidentialIdentity(ALICE.name) + val (_, bobTxIdentity) = createConfidentialIdentity(ALICE.name) // Now we have identities, construct the service and let it know about both database.transaction { @@ -182,8 +181,8 @@ class PersistentIdentityServiceTests { @Test fun `assert ownership`() { withTestSerialization { - val (alice, anonymousAlice) = createParty(ALICE.name, DEV_CA) - val (bob, anonymousBob) = createParty(BOB.name, DEV_CA) + val (alice, anonymousAlice) = createConfidentialIdentity(ALICE.name) + val (bob, anonymousBob) = createConfidentialIdentity(BOB.name) database.transaction { // Now we have identities, construct the service and let it know about both @@ -219,8 +218,8 @@ class PersistentIdentityServiceTests { @Test fun `Test Persistence`() { - val (alice, anonymousAlice) = createParty(ALICE.name, DEV_CA) - val (bob, anonymousBob) = createParty(BOB.name, DEV_CA) + val (alice, anonymousAlice) = createConfidentialIdentity(ALICE.name) + val (bob, anonymousBob) = createConfidentialIdentity(BOB.name) database.transaction { // Register well known identities @@ -252,11 +251,11 @@ class PersistentIdentityServiceTests { assertEquals(anonymousBob, bobReload!!) } - private fun createParty(x500Name: CordaX500Name, ca: CertificateAndKeyPair): Pair { + private fun createConfidentialIdentity(x500Name: CordaX500Name): Pair { val issuerKeyPair = generateKeyPair() - val issuer = getTestPartyAndCertificate(x500Name, issuerKeyPair.public, ca) + val issuer = getTestPartyAndCertificate(x500Name, issuerKeyPair.public) val txKey = Crypto.generateKeyPair() - val txCert = X509Utilities.createCertificate(CertificateType.IDENTITY, issuer.certificate.toX509CertHolder(), issuerKeyPair, x500Name, txKey.public) + val txCert = X509Utilities.createCertificate(CertificateType.CONFIDENTIAL_IDENTITY, issuer.certificate.toX509CertHolder(), issuerKeyPair, x500Name, txKey.public) val txCertPath = X509CertificateFactory().delegate.generateCertPath(listOf(txCert.cert) + issuer.certPath.certificates) return Pair(issuer, PartyAndCertificate(txCertPath)) } diff --git a/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt b/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt index f8c1d0b5ed..07ed77e3b7 100644 --- a/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt @@ -1,13 +1,14 @@ package net.corda.node.services.messaging +import net.corda.core.context.AuthServiceId import net.corda.core.crypto.generateKeyPair import net.corda.core.concurrent.CordaFuture import com.codahale.metrics.MetricRegistry import net.corda.core.crypto.generateKeyPair import net.corda.core.internal.concurrent.openFuture import net.corda.core.utilities.NetworkHostAndPort -import net.corda.node.services.RPCUserService -import net.corda.node.services.RPCUserServiceImpl +import net.corda.node.internal.security.RPCSecurityManager +import net.corda.node.internal.security.RPCSecurityManagerImpl import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.configureWithDevSSLCertificate import net.corda.node.services.network.NetworkMapCacheImpl @@ -54,7 +55,7 @@ class ArtemisMessagingTests { private lateinit var config: NodeConfiguration private lateinit var database: CordaPersistence - private lateinit var userService: RPCUserService + private lateinit var securityManager: RPCSecurityManager private var messagingClient: P2PMessagingClient? = null private var messagingServer: ArtemisMessagingServer? = null @@ -62,7 +63,7 @@ class ArtemisMessagingTests { @Before fun setUp() { - userService = RPCUserServiceImpl(emptyList()) + securityManager = RPCSecurityManagerImpl.fromUserList(users = emptyList(), id = AuthServiceId("TEST")) config = testNodeConfiguration( baseDirectory = temporaryFolder.root.toPath(), myLegalName = ALICE.name) @@ -174,7 +175,7 @@ class ArtemisMessagingTests { } private fun createMessagingServer(local: Int = serverPort, rpc: Int = rpcPort): ArtemisMessagingServer { - return ArtemisMessagingServer(config, local, rpc, networkMapCache, userService).apply { + return ArtemisMessagingServer(config, local, rpc, networkMapCache, securityManager).apply { config.configureWithDevSSLCertificate() messagingServer = this } diff --git a/node/src/test/kotlin/net/corda/node/services/network/NetworkMapCacheTest.kt b/node/src/test/kotlin/net/corda/node/services/network/NetworkMapCacheTest.kt index 861742ff2a..0597bfbd40 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/NetworkMapCacheTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/NetworkMapCacheTest.kt @@ -3,7 +3,6 @@ package net.corda.node.services.network import net.corda.core.node.services.NetworkMapCache import net.corda.testing.ALICE_NAME import net.corda.testing.BOB_NAME -import net.corda.testing.DUMMY_NOTARY import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNodeParameters import net.corda.testing.singleIdentity diff --git a/node/src/test/kotlin/net/corda/node/services/network/TestNodeInfoFactory.kt b/node/src/test/kotlin/net/corda/node/services/network/TestNodeInfoFactory.kt index 13ac5c5657..0b70adbc57 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/TestNodeInfoFactory.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/TestNodeInfoFactory.kt @@ -26,7 +26,7 @@ object TestNodeInfoFactory { fun createNodeInfo(organisation: String): SignedData { val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val clientCert = X509Utilities.createCertificate(CertificateType.CLIENT_CA, intermediateCACert, intermediateCAKey, CordaX500Name(organisation = organisation, locality = "London", country = "GB"), keyPair.public) + val clientCert = X509Utilities.createCertificate(CertificateType.NODE_CA, intermediateCACert, intermediateCAKey, CordaX500Name(organisation = organisation, locality = "London", country = "GB"), keyPair.public) val certPath = buildCertPath(clientCert.toX509Certificate(), intermediateCACert.toX509Certificate(), rootCACert.toX509Certificate()) val nodeInfo = NodeInfo(listOf(NetworkHostAndPort("my.$organisation.com", 1234)), listOf(PartyAndCertificate(certPath)), 1, serial = 1L) return sign(keyPair, nodeInfo) diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt b/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt index 3d627eaf24..98c06829b8 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt @@ -1,8 +1,6 @@ package net.corda.node.services.persistence -import com.nhaarman.mockito_kotlin.any -import com.nhaarman.mockito_kotlin.doReturn -import com.nhaarman.mockito_kotlin.whenever +import com.nhaarman.mockito_kotlin.* import net.corda.core.contracts.Amount import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.StateRef @@ -35,6 +33,7 @@ import net.corda.node.services.schema.HibernateObserver import net.corda.node.services.schema.NodeSchemaService import net.corda.node.services.vault.VaultSchemaV1 import net.corda.node.internal.configureDatabase +import net.corda.node.services.api.IdentityServiceInternal import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.nodeapi.internal.persistence.HibernateConfiguration @@ -85,9 +84,9 @@ class HibernateConfigurationTest { @Before fun setUp() { val cordappPackages = listOf("net.corda.testing.contracts", "net.corda.finance.contracts.asset") - bankServices = MockServices(cordappPackages, BOC.name, BOC_KEY) - issuerServices = MockServices(cordappPackages, DUMMY_CASH_ISSUER_NAME, DUMMY_CASH_ISSUER_KEY) - notaryServices = MockServices(cordappPackages, DUMMY_NOTARY.name, DUMMY_NOTARY_KEY) + bankServices = MockServices(cordappPackages, rigorousMock(), BOC.name, BOC_KEY) + issuerServices = MockServices(cordappPackages, rigorousMock(), DUMMY_CASH_ISSUER_NAME, DUMMY_CASH_ISSUER_KEY) + notaryServices = MockServices(cordappPackages, rigorousMock(), DUMMY_NOTARY.name, DUMMY_NOTARY_KEY) notary = notaryServices.myInfo.singleIdentity() val dataSourceProps = makeTestDataSourceProperties() val identityService = rigorousMock().also { mock -> @@ -102,7 +101,9 @@ class HibernateConfigurationTest { database.transaction { hibernateConfig = database.hibernateConfig // `consumeCash` expects we can self-notarise transactions - services = object : MockServices(cordappPackages, BOB_NAME, generateKeyPair(), DUMMY_NOTARY_KEY) { + services = object : MockServices(cordappPackages, rigorousMock().also { + doNothing().whenever(it).justVerifyAndRegisterIdentity(argThat { name == BOB_NAME }) + }, BOB_NAME, generateKeyPair(), DUMMY_NOTARY_KEY) { override val vaultService = makeVaultService(database.hibernateConfig, schemaService) override fun recordTransactions(statesToRecord: StatesToRecord, txs: Iterable) { for (stx in txs) { @@ -867,7 +868,7 @@ class HibernateConfigurationTest { } /** - * Test invoking SQL query using JDBC connection (session) + * Test invoking SQL query using DB connection (session) */ @Test fun `test calling an arbitrary JDBC native query`() { diff --git a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt index 94fae003e2..8002741b5c 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt @@ -1,6 +1,9 @@ package net.corda.node.services.vault import co.paralleluniverse.fibers.Suspendable +import com.nhaarman.mockito_kotlin.argThat +import com.nhaarman.mockito_kotlin.doNothing +import com.nhaarman.mockito_kotlin.whenever import net.corda.core.contracts.Amount import net.corda.core.contracts.Issued import net.corda.core.contracts.StateAndRef @@ -34,6 +37,7 @@ import net.corda.finance.contracts.asset.DUMMY_CASH_ISSUER_NAME import net.corda.finance.contracts.getCashBalance import net.corda.finance.schemas.CashSchemaV1 import net.corda.finance.utils.sumCash +import net.corda.node.services.api.IdentityServiceInternal import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.testing.* import net.corda.testing.contracts.VaultFiller @@ -83,9 +87,8 @@ class NodeVaultServiceTest { vaultFiller = VaultFiller(services, DUMMY_NOTARY, DUMMY_NOTARY_KEY) // This is safe because MockServices only ever have a single identity identity = services.myInfo.singleIdentityAndCert() - issuerServices = MockServices(cordappPackages, DUMMY_CASH_ISSUER_NAME, DUMMY_CASH_ISSUER_KEY) - bocServices = MockServices(cordappPackages, BOC_NAME, BOC_KEY) - + issuerServices = MockServices(cordappPackages, rigorousMock(), DUMMY_CASH_ISSUER_NAME, DUMMY_CASH_ISSUER_KEY) + bocServices = MockServices(cordappPackages, rigorousMock(), BOC_NAME, BOC_KEY) services.identityService.verifyAndRegisterIdentity(DUMMY_CASH_ISSUER_IDENTITY) services.identityService.verifyAndRegisterIdentity(BOC_IDENTITY) } @@ -125,7 +128,7 @@ class NodeVaultServiceTest { assertThat(w1).hasSize(3) val originalVault = vaultService - val services2 = object : MockServices() { + val services2 = object : MockServices(rigorousMock(), MEGA_CORP.name) { override val vaultService: NodeVaultService get() = originalVault override fun recordTransactions(statesToRecord: StatesToRecord, txs: Iterable) { for (stx in txs) { @@ -468,7 +471,7 @@ class NodeVaultServiceTest { @Test fun addNoteToTransaction() { - val megaCorpServices = MockServices(cordappPackages, MEGA_CORP.name, MEGA_CORP_KEY) + val megaCorpServices = MockServices(cordappPackages, rigorousMock(), MEGA_CORP.name, MEGA_CORP_KEY) database.transaction { val freshKey = identity.owningKey @@ -528,6 +531,7 @@ class NodeVaultServiceTest { val identity = services.myInfo.singleIdentityAndCert() val anonymousIdentity = services.keyManagementService.freshKeyAndCert(identity, false) + // We use a random key pair to pay to here, as we don't actually use the cash once sent val thirdPartyIdentity = AnonymousParty(generateKeyPair().public) val amount = Amount(1000, Issued(BOC.ref(1), GBP)) @@ -547,8 +551,7 @@ class NodeVaultServiceTest { database.transaction { val moveBuilder = TransactionBuilder(notary).apply { - val changeIdentity = services.keyManagementService.freshKeyAndCert(identity, false) - Cash.generateSpend(services, this, Amount(1000, GBP), changeIdentity, thirdPartyIdentity) + Cash.generateSpend(services, this, Amount(1000, GBP), identity, thirdPartyIdentity) } val moveTx = moveBuilder.toWireTransaction(services) vaultService.notify(StatesToRecord.ONLY_RELEVANT, moveTx) @@ -575,7 +578,9 @@ class NodeVaultServiceTest { val identity = services.myInfo.singleIdentityAndCert() assertEquals(services.identityService.partyFromKey(identity.owningKey), identity.party) val anonymousIdentity = services.keyManagementService.freshKeyAndCert(identity, false) - val thirdPartyServices = MockServices() + val thirdPartyServices = MockServices(rigorousMock().also { + doNothing().whenever(it).justVerifyAndRegisterIdentity(argThat { name == MEGA_CORP.name }) + }, MEGA_CORP.name) val thirdPartyIdentity = thirdPartyServices.keyManagementService.freshKeyAndCert(thirdPartyServices.myInfo.singleIdentityAndCert(), false) val amount = Amount(1000, Issued(BOC.ref(1), GBP)) @@ -605,7 +610,7 @@ class NodeVaultServiceTest { // Move cash val moveTxBuilder = database.transaction { TransactionBuilder(newNotary).apply { - Cash.generateSpend(services, this, Amount(amount.quantity, GBP), anonymousIdentity, thirdPartyIdentity.party.anonymise()) + Cash.generateSpend(services, this, Amount(amount.quantity, GBP), identity, thirdPartyIdentity.party.anonymise()) } } val moveTx = moveTxBuilder.toWireTransaction(services) diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt index c9e23d69f5..f6367e728d 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt @@ -89,7 +89,7 @@ open class VaultQueryTests { services = databaseAndServices.second vaultFiller = VaultFiller(services, DUMMY_NOTARY, DUMMY_NOTARY_KEY) vaultFillerCashNotary = VaultFiller(services, DUMMY_NOTARY, DUMMY_NOTARY_KEY, CASH_NOTARY) - notaryServices = MockServices(cordappPackages, DUMMY_NOTARY.name, DUMMY_NOTARY_KEY, DUMMY_CASH_ISSUER_KEY, BOC_KEY, MEGA_CORP_KEY) + notaryServices = MockServices(cordappPackages, rigorousMock(), DUMMY_NOTARY.name, DUMMY_NOTARY_KEY, DUMMY_CASH_ISSUER_KEY, BOC_KEY, MEGA_CORP_KEY) identitySvc = services.identityService // Register all of the identities we're going to use (notaryServices.myInfo.legalIdentitiesAndCerts + BOC_IDENTITY + CASH_NOTARY_IDENTITY + MINI_CORP_IDENTITY + MEGA_CORP_IDENTITY).forEach { identity -> @@ -1324,15 +1324,15 @@ open class VaultQueryTests { fun `unconsumed fungible assets for selected issuer parties`() { // GBP issuer val gbpCashIssuerName = CordaX500Name(organisation = "British Pounds Cash Issuer", locality = "London", country = "GB") - val gbpCashIssuerServices = MockServices(cordappPackages, gbpCashIssuerName, generateKeyPair()) + val gbpCashIssuerServices = MockServices(cordappPackages, rigorousMock(), gbpCashIssuerName, generateKeyPair()) val gbpCashIssuer = gbpCashIssuerServices.myInfo.singleIdentityAndCert() // USD issuer val usdCashIssuerName = CordaX500Name(organisation = "US Dollars Cash Issuer", locality = "New York", country = "US") - val usdCashIssuerServices = MockServices(cordappPackages, usdCashIssuerName, generateKeyPair()) + val usdCashIssuerServices = MockServices(cordappPackages, rigorousMock(), usdCashIssuerName, generateKeyPair()) val usdCashIssuer = usdCashIssuerServices.myInfo.singleIdentityAndCert() // CHF issuer val chfCashIssuerName = CordaX500Name(organisation = "Swiss Francs Cash Issuer", locality = "Zurich", country = "CH") - val chfCashIssuerServices = MockServices(cordappPackages, chfCashIssuerName, generateKeyPair()) + val chfCashIssuerServices = MockServices(cordappPackages, rigorousMock(), chfCashIssuerName, generateKeyPair()) val chfCashIssuer = chfCashIssuerServices.myInfo.singleIdentityAndCert() listOf(gbpCashIssuer, usdCashIssuer, chfCashIssuer).forEach { identity -> services.identityService.verifyAndRegisterIdentity(identity) @@ -2037,7 +2037,7 @@ open class VaultQueryTests { * USE CASE demonstrations (outside of mainline Corda) * * 1) Template / Tutorial CorDapp service using Vault API Custom Query to access attributes of IOU State - * 2) Template / Tutorial Flow using a JDBC session to execute a custom query + * 2) Template / Tutorial Flow using a DB session to execute a custom query * 3) Template / Tutorial CorDapp service query extension executing Named Queries via JPA * 4) Advanced pagination queries using Spring Data (and/or Hibernate/JPQL) */ diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt index 733082b183..451a82d9de 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt @@ -69,8 +69,8 @@ class VaultWithCashTest { database = databaseAndServices.first services = databaseAndServices.second vaultFiller = VaultFiller(services, DUMMY_NOTARY, DUMMY_NOTARY_KEY) - issuerServices = MockServices(cordappPackages, DUMMY_CASH_ISSUER_NAME, DUMMY_CASH_ISSUER_KEY, MEGA_CORP_KEY) - notaryServices = MockServices(cordappPackages, DUMMY_NOTARY.name, DUMMY_NOTARY_KEY) + issuerServices = MockServices(cordappPackages, rigorousMock(), DUMMY_CASH_ISSUER_NAME, DUMMY_CASH_ISSUER_KEY, MEGA_CORP_KEY) + notaryServices = MockServices(cordappPackages, rigorousMock(), DUMMY_NOTARY.name, DUMMY_NOTARY_KEY) notary = notaryServices.myInfo.legalIdentitiesAndCerts.single().party } @@ -101,7 +101,7 @@ class VaultWithCashTest { @Test fun `issue and spend total correctly and irrelevant ignored`() { - val megaCorpServices = MockServices(cordappPackages, MEGA_CORP.name, MEGA_CORP_KEY) + val megaCorpServices = MockServices(cordappPackages, rigorousMock(), MEGA_CORP.name, MEGA_CORP_KEY) val freshKey = services.keyManagementService.freshKey() val usefulTX = diff --git a/node/src/test/kotlin/net/corda/node/utilities/TLSAuthenticationTests.kt b/node/src/test/kotlin/net/corda/node/utilities/TLSAuthenticationTests.kt index 837686aee6..a493f4d885 100644 --- a/node/src/test/kotlin/net/corda/node/utilities/TLSAuthenticationTests.kt +++ b/node/src/test/kotlin/net/corda/node/utilities/TLSAuthenticationTests.kt @@ -242,7 +242,7 @@ class TLSAuthenticationTests { // Client 1 keys, certs and SSLKeyStore. val client1CAKeyPair = Crypto.generateKeyPair(client1CAScheme) val client1CACert = X509Utilities.createCertificate( - CertificateType.CLIENT_CA, + CertificateType.NODE_CA, intermediateCACert, intermediateCAKeyPair, CLIENT_1_X500, @@ -269,7 +269,7 @@ class TLSAuthenticationTests { // Client 2 keys, certs and SSLKeyStore. val client2CAKeyPair = Crypto.generateKeyPair(client2CAScheme) val client2CACert = X509Utilities.createCertificate( - CertificateType.CLIENT_CA, + CertificateType.NODE_CA, intermediateCACert, intermediateCAKeyPair, CLIENT_2_X500, diff --git a/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkisRegistrationHelperTest.kt b/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperTest.kt similarity index 100% rename from node/src/test/kotlin/net/corda/node/utilities/registration/NetworkisRegistrationHelperTest.kt rename to node/src/test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperTest.kt diff --git a/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt b/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt index 17313ec688..e048bc791e 100644 --- a/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt +++ b/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt @@ -25,7 +25,7 @@ import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.getOrThrow import net.corda.testing.DUMMY_BANK_B import net.corda.testing.DUMMY_NOTARY -import net.corda.testing.driver.poll +import net.corda.testing.internal.poll import java.io.InputStream import java.net.HttpURLConnection import java.net.URL diff --git a/samples/irs-demo/cordapp/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt b/samples/irs-demo/cordapp/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt index 97729cadf0..ad45bbfbc6 100644 --- a/samples/irs-demo/cordapp/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt +++ b/samples/irs-demo/cordapp/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt @@ -19,11 +19,8 @@ import net.corda.node.internal.configureDatabase import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.testing.* -import net.corda.testing.node.MockNetwork -import net.corda.testing.node.MockNodeParameters -import net.corda.testing.node.MockServices +import net.corda.testing.node.* import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties -import net.corda.testing.node.createMockCordaService import org.junit.After import org.junit.Assert.* import org.junit.Before @@ -50,7 +47,7 @@ class NodeInterestRatesTest { private val DUMMY_CASH_ISSUER_KEY = generateKeyPair() private val DUMMY_CASH_ISSUER = Party(CordaX500Name(organisation = "Cash issuer", locality = "London", country = "GB"), DUMMY_CASH_ISSUER_KEY.public) - private val services = MockServices(listOf("net.corda.finance.contracts.asset"), DUMMY_CASH_ISSUER.name, DUMMY_CASH_ISSUER_KEY, MEGA_CORP_KEY) + private val services = MockServices(listOf("net.corda.finance.contracts.asset"), rigorousMock(), DUMMY_CASH_ISSUER.name, DUMMY_CASH_ISSUER_KEY, MEGA_CORP_KEY) // This is safe because MockServices only ever have a single identity private val identity = services.myInfo.singleIdentity() diff --git a/samples/irs-demo/cordapp/src/test/kotlin/net/corda/irs/contract/IRSTests.kt b/samples/irs-demo/cordapp/src/test/kotlin/net/corda/irs/contract/IRSTests.kt index c200bd7c88..9dd3180bd8 100644 --- a/samples/irs-demo/cordapp/src/test/kotlin/net/corda/irs/contract/IRSTests.kt +++ b/samples/irs-demo/cordapp/src/test/kotlin/net/corda/irs/contract/IRSTests.kt @@ -1,7 +1,12 @@ package net.corda.irs.contract +import com.nhaarman.mockito_kotlin.doReturn +import com.nhaarman.mockito_kotlin.whenever import net.corda.core.contracts.Amount import net.corda.core.contracts.UniqueIdentifier +import net.corda.core.crypto.generateKeyPair +import net.corda.core.identity.CordaX500Name +import net.corda.core.identity.Party import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.seconds @@ -18,6 +23,7 @@ import net.corda.finance.contracts.FixOf import net.corda.finance.contracts.Frequency import net.corda.finance.contracts.PaymentRule import net.corda.finance.contracts.Tenor +import net.corda.node.services.api.IdentityServiceInternal import net.corda.testing.* import net.corda.testing.node.MockServices import org.junit.Rule @@ -27,6 +33,7 @@ import java.time.LocalDate import java.util.* import kotlin.test.assertEquals +private val DUMMY_PARTY = Party(CordaX500Name("Dummy", "Madrid", "ES"), generateKeyPair().public) fun createDummyIRS(irsSelect: Int): InterestRateSwap.State { return when (irsSelect) { 1 -> { @@ -212,10 +219,9 @@ class IRSTests { @Rule @JvmField val testSerialization = SerializationEnvironmentRule() - private val megaCorpServices = MockServices(listOf("net.corda.irs.contract"), MEGA_CORP.name, MEGA_CORP_KEY) - private val miniCorpServices = MockServices(listOf("net.corda.irs.contract"), MINI_CORP.name, MINI_CORP_KEY) - private val notaryServices = MockServices(listOf("net.corda.irs.contract"), DUMMY_NOTARY.name, DUMMY_NOTARY_KEY) - + private val megaCorpServices = MockServices(listOf("net.corda.irs.contract"), rigorousMock(), MEGA_CORP.name, MEGA_CORP_KEY) + private val miniCorpServices = MockServices(listOf("net.corda.irs.contract"), rigorousMock(), MINI_CORP.name, MINI_CORP_KEY) + private val notaryServices = MockServices(listOf("net.corda.irs.contract"), rigorousMock(), DUMMY_NOTARY.name, DUMMY_NOTARY_KEY) @Test fun ok() { trade().verifies() @@ -311,7 +317,11 @@ class IRSTests { */ @Test fun generateIRSandFixSome() { - val services = MockServices(listOf("net.corda.irs.contract")) + val services = MockServices(listOf("net.corda.irs.contract"), rigorousMock().also { + listOf(MEGA_CORP, MINI_CORP).forEach { party -> + doReturn(party).whenever(it).partyFromKey(party.owningKey) + } + }, MEGA_CORP.name) var previousTXN = generateIRSTxn(1) previousTXN.toLedgerTransaction(services).verify() services.recordTransactions(previousTXN) diff --git a/samples/irs-demo/src/integration-test/kotlin/net/corda/test/spring/SpringDriver.kt b/samples/irs-demo/src/integration-test/kotlin/net/corda/test/spring/SpringDriver.kt index 7a5cf1bdb0..ba6d7811da 100644 --- a/samples/irs-demo/src/integration-test/kotlin/net/corda/test/spring/SpringDriver.kt +++ b/samples/irs-demo/src/integration-test/kotlin/net/corda/test/spring/SpringDriver.kt @@ -3,8 +3,11 @@ package net.corda.test.spring import net.corda.core.concurrent.CordaFuture import net.corda.core.internal.concurrent.map import net.corda.core.utilities.contextLogger -import net.corda.testing.driver.* -import net.corda.testing.internal.ProcessUtilities +import net.corda.testing.driver.DriverParameters +import net.corda.testing.driver.NodeHandle +import net.corda.testing.driver.PortAllocation +import net.corda.testing.driver.WebserverHandle +import net.corda.testing.internal.* import net.corda.testing.node.NotarySpec import okhttp3.OkHttpClient import okhttp3.Request @@ -14,22 +17,6 @@ import java.nio.file.Path import java.nio.file.Paths import java.util.concurrent.TimeUnit -interface SpringDriverExposedDSLInterface : DriverDSLExposedInterface { - - /** - * Starts a Spring Boot application, passes the RPC connection data as parameters the process. - * Returns future which will complete after (and if) the server passes healthcheck. - * @param clazz Class with main method which is expected to run Spring application - * @param handle Corda Node handle this webapp is expected to connect to - * @param checkUrl URL path to use for server readiness check - uses [okhttp3.Response.isSuccessful] as qualifier - * - * TODO: Rather then expecting a given clazz to contain main method which start Spring app our own simple class can do this - */ - fun startSpringBootWebapp(clazz: Class<*>, handle: NodeHandle, checkUrl: String): CordaFuture -} - -interface SpringDriverInternalDSLInterface : DriverDSLInternalInterface, SpringDriverExposedDSLInterface - fun springDriver( defaultParameters: DriverParameters = DriverParameters(), isDebug: Boolean = defaultParameters.isDebug, @@ -42,29 +29,40 @@ fun springDriver( startNodesInProcess: Boolean = defaultParameters.startNodesInProcess, notarySpecs: List, extraCordappPackagesToScan: List = defaultParameters.extraCordappPackagesToScan, - dsl: SpringDriverExposedDSLInterface.() -> A -) = genericDriver( - defaultParameters = defaultParameters, - isDebug = isDebug, - driverDirectory = driverDirectory, - portAllocation = portAllocation, - debugPortAllocation = debugPortAllocation, - systemProperties = systemProperties, - useTestClock = useTestClock, - initialiseSerialization = initialiseSerialization, - startNodesInProcess = startNodesInProcess, - extraCordappPackagesToScan = extraCordappPackagesToScan, - notarySpecs = notarySpecs, - driverDslWrapper = { driverDSL:DriverDSL -> SpringBootDriverDSL(driverDSL) }, - coerce = { it }, dsl = dsl -) + dsl: SpringBootDriverDSL.() -> A +): A { + return genericDriver( + defaultParameters = defaultParameters, + isDebug = isDebug, + driverDirectory = driverDirectory, + portAllocation = portAllocation, + debugPortAllocation = debugPortAllocation, + systemProperties = systemProperties, + useTestClock = useTestClock, + initialiseSerialization = initialiseSerialization, + startNodesInProcess = startNodesInProcess, + extraCordappPackagesToScan = extraCordappPackagesToScan, + notarySpecs = notarySpecs, + driverDslWrapper = { driverDSL: DriverDSLImpl -> SpringBootDriverDSL(driverDSL) }, + coerce = { it }, dsl = dsl + ) +} -data class SpringBootDriverDSL(private val driverDSL: DriverDSL) : DriverDSLInternalInterface by driverDSL, SpringDriverInternalDSLInterface { +data class SpringBootDriverDSL(private val driverDSL: DriverDSLImpl) : InternalDriverDSL by driverDSL { companion object { private val log = contextLogger() } - override fun startSpringBootWebapp(clazz: Class<*>, handle: NodeHandle, checkUrl: String): CordaFuture { + /** + * Starts a Spring Boot application, passes the RPC connection data as parameters the process. + * Returns future which will complete after (and if) the server passes healthcheck. + * @param clazz Class with main method which is expected to run Spring application + * @param handle Corda Node handle this webapp is expected to connect to + * @param checkUrl URL path to use for server readiness check - uses [okhttp3.Response.isSuccessful] as qualifier + * + * TODO: Rather then expecting a given clazz to contain main method which start Spring app our own simple class can do this + */ + fun startSpringBootWebapp(clazz: Class<*>, handle: NodeHandle, checkUrl: String): CordaFuture { val debugPort = if (driverDSL.isDebug) driverDSL.debugPortAllocation.nextPort() else null val process = startApplication(handle, debugPort, clazz) driverDSL.shutdownManager.registerProcessShutdown(process) diff --git a/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt b/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt index 1534641e79..1c4d862fa2 100644 --- a/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt +++ b/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt @@ -16,7 +16,7 @@ import net.corda.testing.chooseIdentity import net.corda.testing.* import net.corda.testing.driver.NodeHandle import net.corda.testing.driver.driver -import net.corda.testing.driver.poll +import net.corda.testing.internal.poll import net.corda.traderdemo.flow.BuyerFlow import net.corda.traderdemo.flow.CommercialPaperIssueFlow import net.corda.traderdemo.flow.SellerFlow diff --git a/samples/trader-demo/src/test/kotlin/net/corda/traderdemo/TransactionGraphSearchTests.kt b/samples/trader-demo/src/test/kotlin/net/corda/traderdemo/TransactionGraphSearchTests.kt index 0a893c54a6..dea21d50d3 100644 --- a/samples/trader-demo/src/test/kotlin/net/corda/traderdemo/TransactionGraphSearchTests.kt +++ b/samples/trader-demo/src/test/kotlin/net/corda/traderdemo/TransactionGraphSearchTests.kt @@ -36,9 +36,8 @@ class TransactionGraphSearchTests { * @param signer signer for the two transactions and their commands. */ fun buildTransactions(command: CommandData): GraphTransactionStorage { - val megaCorpServices = MockServices(listOf("net.corda.testing.contracts"), MEGA_CORP.name, MEGA_CORP_KEY) - val notaryServices = MockServices(listOf("net.corda.testing.contracts"), DUMMY_NOTARY.name, DUMMY_NOTARY_KEY) - + val megaCorpServices = MockServices(listOf("net.corda.testing.contracts"), rigorousMock(), MEGA_CORP.name, MEGA_CORP_KEY) + val notaryServices = MockServices(listOf("net.corda.testing.contracts"), rigorousMock(), DUMMY_NOTARY.name, DUMMY_NOTARY_KEY) val originBuilder = TransactionBuilder(DUMMY_NOTARY) .addOutputState(DummyState(random31BitValue()), DummyContract.PROGRAM_ID) .addCommand(command, MEGA_CORP_PUBKEY) diff --git a/testing/node-driver/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt b/testing/node-driver/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt index 2e8e748794..ed78ae8527 100644 --- a/testing/node-driver/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt +++ b/testing/node-driver/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt @@ -4,12 +4,17 @@ import net.corda.core.concurrent.CordaFuture import net.corda.core.internal.div import net.corda.core.internal.list import net.corda.core.internal.readLines +import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.getOrThrow import net.corda.node.internal.NodeStartup import net.corda.testing.* import net.corda.testing.common.internal.ProjectStructure.projectRootDir +import net.corda.testing.http.HttpApi +import net.corda.testing.internal.addressMustBeBound +import net.corda.testing.internal.addressMustNotBeBound import net.corda.testing.node.NotarySpec import org.assertj.core.api.Assertions.assertThat +import org.json.simple.JSONObject import org.junit.ClassRule import org.junit.Test import java.util.concurrent.Executors @@ -67,6 +72,20 @@ class DriverTests : IntegrationTest() { } } + @Test + fun `monitoring mode enables jolokia exporting of JMX metrics via HTTP JSON`() { + driver(jmxPolicy = JmxPolicy(true)) { + // start another node so we gain access to node JMX metrics + startNode(providedName = DUMMY_REGULATOR.name).getOrThrow() + + val webAddress = NetworkHostAndPort("localhost", 7006) + // request access to some JMX metrics via Jolokia HTTP/JSON + val api = HttpApi.fromHostAndPort(webAddress, "/jolokia/") + val versionAsJson = api.getJson("/jolokia/version/") + assertThat(versionAsJson.getValue("status")).isEqualTo(200) + } + } + @Test fun `started node, which is not waited for in the driver, is shutdown when the driver exits`() { // First check that the process-id file is created by the node on startup, so that we can be sure our check that @@ -77,7 +96,7 @@ class DriverTests : IntegrationTest() { } val baseDirectory = driver(notarySpecs = listOf(NotarySpec(DUMMY_NOTARY.name))) { - (this as DriverDSL).baseDirectory(DUMMY_NOTARY.name) + baseDirectory(DUMMY_NOTARY.name) } assertThat(baseDirectory / "process-id").doesNotExist() } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/NodeTestUtils.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/NodeTestUtils.kt index e96340e1a8..846305f3aa 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/NodeTestUtils.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/NodeTestUtils.kt @@ -13,13 +13,13 @@ import net.corda.core.identity.CordaX500Name import net.corda.core.internal.FlowStateMachine import net.corda.core.node.ServiceHub import net.corda.core.transactions.TransactionBuilder -import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.seconds import net.corda.node.services.api.StartedNodeServices import net.corda.node.services.config.* import net.corda.nodeapi.internal.config.User import net.corda.testing.node.MockServices import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties +import net.corda.testing.node.makeTestIdentityService import net.corda.testing.node.MockServices.Companion.makeTestDatabaseProperties import java.nio.file.Path @@ -29,7 +29,7 @@ import java.nio.file.Path */ @JvmOverloads fun ledger( - services: ServiceHub = MockServices(), + services: ServiceHub = MockServices(makeTestIdentityService(listOf(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, DUMMY_CASH_ISSUER_IDENTITY, DUMMY_NOTARY_IDENTITY)), MEGA_CORP.name), dsl: LedgerDSL.() -> Unit ): LedgerDSL { return LedgerDSL(TestLedgerDSLInterpreter(services)).also { dsl(it) } @@ -45,7 +45,7 @@ fun transaction( transactionBuilder: TransactionBuilder = TransactionBuilder(notary = DUMMY_NOTARY), cordappPackages: List = emptyList(), dsl: TransactionDSL.() -> EnforceVerifyOrFail -) = ledger(services = MockServices(cordappPackages)) { +) = ledger(services = MockServices(cordappPackages, makeTestIdentityService(listOf(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, DUMMY_CASH_ISSUER_IDENTITY, DUMMY_NOTARY_IDENTITY)), MEGA_CORP.name)) { dsl(TransactionDSL(TestTransactionDSLInterpreter(this.interpreter, transactionBuilder))) } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt index 51d0fa7266..3914445caa 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt @@ -2,203 +2,35 @@ package net.corda.testing.driver -import com.google.common.util.concurrent.ThreadFactoryBuilder -import com.typesafe.config.Config -import com.typesafe.config.ConfigRenderOptions import net.corda.client.rpc.CordaRPCClient -import net.corda.cordform.CordformContext -import net.corda.cordform.CordformNode -import net.corda.core.CordaException import net.corda.core.concurrent.CordaFuture -import net.corda.core.concurrent.firstOf import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party -import net.corda.core.internal.* -import net.corda.core.internal.concurrent.* import net.corda.core.messaging.CordaRPCOps import net.corda.core.node.NodeInfo -import net.corda.core.node.services.NetworkMapCache -import net.corda.core.node.services.NotaryService -import net.corda.core.toFuture -import net.corda.core.utilities.* +import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.internal.Node -import net.corda.node.internal.NodeStartup import net.corda.node.internal.StartedNode -import net.corda.node.services.Permissions.Companion.invokeRpc -import net.corda.node.services.config.* -import net.corda.node.utilities.ServiceIdentityGenerator -import net.corda.nodeapi.NodeInfoFilesCopier +import net.corda.node.services.config.NodeConfiguration +import net.corda.node.services.config.VerifierType import net.corda.nodeapi.internal.config.User -import net.corda.nodeapi.internal.config.toConfig -import net.corda.nodeapi.internal.addShutdownHook -import net.corda.testing.* +import net.corda.testing.DUMMY_NOTARY import net.corda.testing.internal.InProcessNode -import net.corda.testing.internal.ProcessUtilities -import net.corda.testing.node.ClusterSpec -import net.corda.testing.node.MockServices.Companion.MOCK_VERSION_INFO +import net.corda.testing.internal.DriverDSLImpl +import net.corda.testing.internal.genericDriver +import net.corda.testing.internal.getTimestampAsDirectoryName import net.corda.testing.node.NotarySpec -import okhttp3.OkHttpClient -import okhttp3.Request -import org.slf4j.Logger -import rx.Observable -import rx.observables.ConnectableObservable -import java.net.* +import java.net.InetSocketAddress +import java.net.ServerSocket import java.nio.file.Path import java.nio.file.Paths -import java.nio.file.StandardCopyOption.REPLACE_EXISTING -import java.time.Duration -import java.time.Instant -import java.time.ZoneOffset.UTC -import java.time.format.DateTimeFormatter -import java.util.* -import java.util.concurrent.Executors -import java.util.concurrent.ScheduledExecutorService -import java.util.concurrent.TimeUnit.MILLISECONDS -import java.util.concurrent.TimeUnit.SECONDS import java.util.concurrent.atomic.AtomicInteger -import kotlin.concurrent.thread - -/** - * This file defines a small "Driver" DSL for starting up nodes that is only intended for development, demos and tests. - * - * The process the driver is run in behaves as an Artemis client and starts up other processes. - * - * TODO this file is getting way too big, it should be split into several files. - */ -private val log: Logger = loggerFor() - -private val DEFAULT_POLL_INTERVAL = 500.millis - -private const val DEFAULT_WARN_COUNT = 120 - -/** - * A sub-set of permissions that grant most of the essential operations used in the unit/integration tests as well as - * in demo application like NodeExplorer. - */ -private val DRIVER_REQUIRED_PERMISSIONS = setOf( - invokeRpc(CordaRPCOps::nodeInfo), - invokeRpc(CordaRPCOps::networkMapFeed), - invokeRpc(CordaRPCOps::networkMapSnapshot), - invokeRpc(CordaRPCOps::notaryIdentities), - invokeRpc(CordaRPCOps::stateMachinesFeed), - invokeRpc(CordaRPCOps::stateMachineRecordedTransactionMappingFeed), - invokeRpc(CordaRPCOps::nodeInfoFromParty), - invokeRpc(CordaRPCOps::internalVerifiedTransactionsFeed), - invokeRpc("vaultQueryBy"), - invokeRpc("vaultTrackBy"), - invokeRpc(CordaRPCOps::registeredFlows) -) /** * Object ecapsulating a notary started automatically by the driver. */ data class NotaryHandle(val identity: Party, val validating: Boolean, val nodeHandles: CordaFuture>) -/** - * This is the interface that's exposed to DSL users. - */ -interface DriverDSLExposedInterface : CordformContext { - /** Returns a list of [NotaryHandle]s matching the list of [NotarySpec]s passed into [driver]. */ - val notaryHandles: List - - /** - * Returns the [NotaryHandle] for the single notary on the network. Throws if there are none or more than one. - * @see notaryHandles - */ - val defaultNotaryHandle: NotaryHandle get() { - return when (notaryHandles.size) { - 0 -> throw IllegalStateException("There are no notaries defined on the network") - 1 -> notaryHandles[0] - else -> throw IllegalStateException("There is more than one notary defined on the network") - } - } - - /** - * Returns the identity of the single notary on the network. Throws if there are none or more than one. - * @see defaultNotaryHandle - */ - val defaultNotaryIdentity: Party get() = defaultNotaryHandle.identity - - /** - * Returns a [CordaFuture] on the [NodeHandle] for the single-node notary on the network. Throws if there - * are no notaries or more than one, or if the notary is a distributed cluster. - * @see defaultNotaryHandle - * @see notaryHandles - */ - val defaultNotaryNode: CordaFuture get() { - return defaultNotaryHandle.nodeHandles.map { - it.singleOrNull() ?: throw IllegalStateException("Default notary is not a single node") - } - } - - /** - * Start a node. - * - * @param defaultParameters The default parameters for the node. Allows the node to be configured in builder style - * when called from Java code. - * @param providedName Optional name of the node, which will be its legal name in [Party]. Defaults to something - * random. Note that this must be unique as the driver uses it as a primary key! - * @param verifierType The type of transaction verifier to use. See: [VerifierType] - * @param rpcUsers List of users who are authorised to use the RPC system. Defaults to empty list. - * @param startInSameProcess Determines if the node should be started inside the same process the Driver is running - * in. If null the Driver-level value will be used. - * @return A [CordaFuture] on the [NodeHandle] to the node. The future will complete when the node is available. - */ - fun startNode( - defaultParameters: NodeParameters = NodeParameters(), - providedName: CordaX500Name? = defaultParameters.providedName, - rpcUsers: List = defaultParameters.rpcUsers, - verifierType: VerifierType = defaultParameters.verifierType, - customOverrides: Map = defaultParameters.customOverrides, - startInSameProcess: Boolean? = defaultParameters.startInSameProcess, - maximumHeapSize: String = defaultParameters.maximumHeapSize, - logLevel: String? = defaultParameters.logLevel): CordaFuture - - /** - * Helper function for starting a [Node] with custom parameters from Java. - * - * @param parameters The default parameters for the driver. - * @return [NodeHandle] that will be available sometime in the future. - */ - fun startNode(parameters: NodeParameters): CordaFuture = startNode(defaultParameters = parameters) - - /** Call [startWebserver] with a default maximumHeapSize. */ - fun startWebserver(handle: NodeHandle): CordaFuture = startWebserver(handle, "200m") - - /** - * Starts a web server for a node - * @param handle The handle for the node that this webserver connects to via RPC. - * @param maximumHeapSize Argument for JVM -Xmx option e.g. "200m". - */ - fun startWebserver(handle: NodeHandle, maximumHeapSize: String): CordaFuture - - /** - * Polls a function until it returns a non-null value. Note that there is no timeout on the polling. - * - * @param pollName A description of what is being polled. - * @param pollInterval The interval of polling. - * @param warnCount The number of polls after the Driver gives a warning. - * @param check The function being polled. - * @return A future that completes with the non-null value [check] has returned. - */ - fun pollUntilNonNull(pollName: String, pollInterval: Duration = DEFAULT_POLL_INTERVAL, warnCount: Int = DEFAULT_WARN_COUNT, check: () -> A?): CordaFuture - - /** - * Polls the given function until it returns true. - * @see pollUntilNonNull - */ - fun pollUntilTrue(pollName: String, pollInterval: Duration = DEFAULT_POLL_INTERVAL, warnCount: Int = DEFAULT_WARN_COUNT, check: () -> Boolean): CordaFuture { - return pollUntilNonNull(pollName, pollInterval, warnCount) { if (check()) Unit else null } - } - - val shutdownManager: ShutdownManager -} - -interface DriverDSLInternalInterface : DriverDSLExposedInterface { - fun start() - fun shutdown() -} - sealed class NodeHandle { abstract val nodeInfo: NodeInfo /** @@ -298,6 +130,10 @@ data class NodeParameters( fun ssetLogLevel(logLevel: String?) = copy(logLevel = logLevel) } +data class JmxPolicy(val startJmxHttpServer: Boolean = false, + val jmxHttpServerPortAllocation: PortAllocation? = + if (startJmxHttpServer) PortAllocation.Incremental(7005) else null) + /** * [driver] allows one to start up nodes like this: * driver { @@ -322,9 +158,12 @@ data class NodeParameters( * @param systemProperties A Map of extra system properties which will be given to each new node. Defaults to empty. * @param useTestClock If true the test clock will be used in Node. * @param startNodesInProcess Provides the default behaviour of whether new nodes should start inside this process or - * not. Note that this may be overridden in [DriverDSLExposedInterface.startNode]. + * not. Note that this may be overridden in [DriverDSL.startNode]. * @param notarySpecs The notaries advertised for this network. These nodes will be started automatically and will be - * available from [DriverDSLExposedInterface.notaryHandles]. Defaults to a simple validating notary. + * available from [DriverDSL.notaryHandles]. Defaults to a simple validating notary. + * @param jmxPolicy Used to specify whether to expose JMX metrics via Jolokia HHTP/JSON. Defines two attributes: + * startJmxHttpServer: indicates whether the spawned nodes should start with a Jolokia JMX agent to enable remote JMX monitoring using HTTP/JSON. + * jmxHttpServerPortAllocation: the port allocation strategy to use for remote Jolokia/JMX monitoring over HTTP. Defaults to incremental. * @param dsl The dsl itself. * @return The value returned in the [dsl] closure. */ @@ -338,13 +177,14 @@ fun driver( useTestClock: Boolean = defaultParameters.useTestClock, initialiseSerialization: Boolean = defaultParameters.initialiseSerialization, startNodesInProcess: Boolean = defaultParameters.startNodesInProcess, - waitForAllNodesToFinish: Boolean = defaultParameters.waitForNodesToFinish, + waitForAllNodesToFinish: Boolean = defaultParameters.waitForAllNodesToFinish, notarySpecs: List = defaultParameters.notarySpecs, extraCordappPackagesToScan: List = defaultParameters.extraCordappPackagesToScan, - dsl: DriverDSLExposedInterface.() -> A + jmxPolicy: JmxPolicy = JmxPolicy(), + dsl: DriverDSL.() -> A ): A { return genericDriver( - driverDsl = DriverDSL( + driverDsl = DriverDSLImpl( portAllocation = portAllocation, debugPortAllocation = debugPortAllocation, extraSystemProperties = extraSystemProperties, @@ -354,7 +194,8 @@ fun driver( startNodesInProcess = startNodesInProcess, waitForNodesToFinish = waitForAllNodesToFinish, notarySpecs = notarySpecs, - extraCordappPackagesToScan = extraCordappPackagesToScan + extraCordappPackagesToScan = extraCordappPackagesToScan, + jmxPolicy = jmxPolicy ), coerce = { it }, dsl = dsl, @@ -371,7 +212,7 @@ fun driver( */ fun driver( parameters: DriverParameters, - dsl: DriverDSLExposedInterface.() -> A + dsl: DriverDSL.() -> A ): A { return driver(defaultParameters = parameters, dsl = dsl) } @@ -387,9 +228,11 @@ data class DriverParameters( val useTestClock: Boolean = false, val initialiseSerialization: Boolean = true, val startNodesInProcess: Boolean = false, - val waitForNodesToFinish: Boolean = false, + val waitForAllNodesToFinish: Boolean = false, val notarySpecs: List = listOf(NotarySpec(DUMMY_NOTARY.name)), - val extraCordappPackagesToScan: List = emptyList() + val extraCordappPackagesToScan: List = emptyList(), + val jmxPolicy: JmxPolicy = JmxPolicy() + ) { fun setIsDebug(isDebug: Boolean) = copy(isDebug = isDebug) fun setDriverDirectory(driverDirectory: Path) = copy(driverDirectory = driverDirectory) @@ -399,643 +242,8 @@ data class DriverParameters( fun setUseTestClock(useTestClock: Boolean) = copy(useTestClock = useTestClock) fun setInitialiseSerialization(initialiseSerialization: Boolean) = copy(initialiseSerialization = initialiseSerialization) fun setStartNodesInProcess(startNodesInProcess: Boolean) = copy(startNodesInProcess = startNodesInProcess) - fun setTerminateNodesOnShutdown(terminateNodesOnShutdown: Boolean) = copy(waitForNodesToFinish = terminateNodesOnShutdown) + fun setWaitForAllNodesToFinish(waitForAllNodesToFinish: Boolean) = copy(waitForAllNodesToFinish = waitForAllNodesToFinish) fun setExtraCordappPackagesToScan(extraCordappPackagesToScan: List) = copy(extraCordappPackagesToScan = extraCordappPackagesToScan) fun setNotarySpecs(notarySpecs: List) = copy(notarySpecs = notarySpecs) + fun setJmxPolicy(jmxPolicy: JmxPolicy) = copy(jmxPolicy = jmxPolicy) } - -/** - * This is a helper method to allow extending of the DSL, along the lines of - * interface SomeOtherExposedDSLInterface : DriverDSLExposedInterface - * interface SomeOtherInternalDSLInterface : DriverDSLInternalInterface, SomeOtherExposedDSLInterface - * class SomeOtherDSL(val driverDSL : DriverDSL) : DriverDSLInternalInterface by driverDSL, SomeOtherInternalDSLInterface - * - * @param coerce We need this explicit coercion witness because we can't put an extra DI : D bound in a `where` clause. - */ -fun genericDriver( - driverDsl: D, - initialiseSerialization: Boolean = true, - coerce: (D) -> DI, - dsl: DI.() -> A -): A { - val serializationEnv = setGlobalSerialization(initialiseSerialization) - val shutdownHook = addShutdownHook(driverDsl::shutdown) - try { - driverDsl.start() - return dsl(coerce(driverDsl)) - } catch (exception: Throwable) { - log.error("Driver shutting down because of exception", exception) - throw exception - } finally { - driverDsl.shutdown() - shutdownHook.cancel() - serializationEnv.unset() - } -} - -/** - * This is a helper method to allow extending of the DSL, along the lines of - * interface SomeOtherExposedDSLInterface : DriverDSLExposedInterface - * interface SomeOtherInternalDSLInterface : DriverDSLInternalInterface, SomeOtherExposedDSLInterface - * class SomeOtherDSL(val driverDSL : DriverDSL) : DriverDSLInternalInterface by driverDSL, SomeOtherInternalDSLInterface - * - * @param coerce We need this explicit coercion witness because we can't put an extra DI : D bound in a `where` clause. - */ -fun genericDriver( - defaultParameters: DriverParameters = DriverParameters(), - isDebug: Boolean = defaultParameters.isDebug, - driverDirectory: Path = defaultParameters.driverDirectory, - portAllocation: PortAllocation = defaultParameters.portAllocation, - debugPortAllocation: PortAllocation = defaultParameters.debugPortAllocation, - systemProperties: Map = defaultParameters.extraSystemProperties, - useTestClock: Boolean = defaultParameters.useTestClock, - initialiseSerialization: Boolean = defaultParameters.initialiseSerialization, - waitForNodesToFinish: Boolean = defaultParameters.waitForNodesToFinish, - startNodesInProcess: Boolean = defaultParameters.startNodesInProcess, - notarySpecs: List, - extraCordappPackagesToScan: List = defaultParameters.extraCordappPackagesToScan, - driverDslWrapper: (DriverDSL) -> D, - coerce: (D) -> DI, dsl: DI.() -> A -): A { - val serializationEnv = setGlobalSerialization(initialiseSerialization) - val driverDsl = driverDslWrapper( - DriverDSL( - portAllocation = portAllocation, - debugPortAllocation = debugPortAllocation, - extraSystemProperties = systemProperties, - driverDirectory = driverDirectory.toAbsolutePath(), - useTestClock = useTestClock, - isDebug = isDebug, - startNodesInProcess = startNodesInProcess, - waitForNodesToFinish = waitForNodesToFinish, - extraCordappPackagesToScan = extraCordappPackagesToScan, - notarySpecs = notarySpecs - ) - ) - val shutdownHook = addShutdownHook(driverDsl::shutdown) - try { - driverDsl.start() - return dsl(coerce(driverDsl)) - } catch (exception: Throwable) { - log.error("Driver shutting down because of exception", exception) - throw exception - } finally { - driverDsl.shutdown() - shutdownHook.cancel() - serializationEnv.unset() - } -} - -fun getTimestampAsDirectoryName(): String { - return DateTimeFormatter.ofPattern("yyyyMMddHHmmss").withZone(UTC).format(Instant.now()) -} - -class ListenProcessDeathException(hostAndPort: NetworkHostAndPort, listenProcess: Process) : - CordaException("The process that was expected to listen on $hostAndPort has died with status: ${listenProcess.exitValue()}") - -/** - * @throws ListenProcessDeathException if [listenProcess] dies before the check succeeds, i.e. the check can't succeed as intended. - */ -fun addressMustBeBound(executorService: ScheduledExecutorService, hostAndPort: NetworkHostAndPort, listenProcess: Process? = null) { - addressMustBeBoundFuture(executorService, hostAndPort, listenProcess).getOrThrow() -} - -fun addressMustBeBoundFuture(executorService: ScheduledExecutorService, hostAndPort: NetworkHostAndPort, listenProcess: Process? = null): CordaFuture { - return poll(executorService, "address $hostAndPort to bind") { - if (listenProcess != null && !listenProcess.isAlive) { - throw ListenProcessDeathException(hostAndPort, listenProcess) - } - try { - Socket(hostAndPort.host, hostAndPort.port).close() - Unit - } catch (_exception: SocketException) { - null - } - } -} - -/* - * The default timeout value of 40 seconds have been chosen based on previous node shutdown time estimate. - * It's been observed that nodes can take up to 30 seconds to shut down, so just to stay on the safe side the 60 seconds - * timeout has been chosen. - */ -fun addressMustNotBeBound(executorService: ScheduledExecutorService, hostAndPort: NetworkHostAndPort, timeout: Duration = 40.seconds) { - addressMustNotBeBoundFuture(executorService, hostAndPort).getOrThrow(timeout) -} - -fun addressMustNotBeBoundFuture(executorService: ScheduledExecutorService, hostAndPort: NetworkHostAndPort): CordaFuture { - return poll(executorService, "address $hostAndPort to unbind") { - try { - Socket(hostAndPort.host, hostAndPort.port).close() - null - } catch (_exception: SocketException) { - Unit - } - } -} - -fun poll( - executorService: ScheduledExecutorService, - pollName: String, - pollInterval: Duration = 500.millis, - warnCount: Int = 120, - check: () -> A? -): CordaFuture { - val resultFuture = openFuture() - val task = object : Runnable { - var counter = -1 - override fun run() { - if (resultFuture.isCancelled) return // Give up, caller can no longer get the result. - if (++counter == warnCount) { - log.warn("Been polling $pollName for ${(pollInterval * warnCount.toLong()).seconds} seconds...") - } - try { - val checkResult = check() - if (checkResult != null) { - resultFuture.set(checkResult) - } else { - executorService.schedule(this, pollInterval.toMillis(), MILLISECONDS) - } - } catch (t: Throwable) { - resultFuture.setException(t) - } - } - } - executorService.submit(task) // The check may be expensive, so always run it in the background even the first time. - return resultFuture -} - -class DriverDSL( - val portAllocation: PortAllocation, - val debugPortAllocation: PortAllocation, - val extraSystemProperties: Map, - val driverDirectory: Path, - val useTestClock: Boolean, - val isDebug: Boolean, - val startNodesInProcess: Boolean, - val waitForNodesToFinish: Boolean, - extraCordappPackagesToScan: List, - val notarySpecs: List -) : DriverDSLInternalInterface { - private var _executorService: ScheduledExecutorService? = null - val executorService get() = _executorService!! - private var _shutdownManager: ShutdownManager? = null - override val shutdownManager get() = _shutdownManager!! - val systemProperties by lazy { System.getProperties().toList().map { it.first.toString() to it.second.toString() }.toMap() + extraSystemProperties } - private val cordappPackages = extraCordappPackagesToScan + getCallerPackage() - // TODO: this object will copy NodeInfo files from started nodes to other nodes additional-node-infos/ - // This uses the FileSystem and adds a delay (~5 seconds) given by the time we wait before polling the file system. - // Investigate whether we can avoid that. - private val nodeInfoFilesCopier = NodeInfoFilesCopier() - // Map from a nodes legal name to an observable emitting the number of nodes in its network map. - private val countObservables = mutableMapOf>() - private lateinit var _notaries: List - override val notaryHandles: List get() = _notaries - - class State { - val processes = ArrayList() - } - - private val state = ThreadBox(State()) - - //TODO: remove this once we can bundle quasar properly. - private val quasarJarPath: String by lazy { - val cl = ClassLoader.getSystemClassLoader() - val urls = (cl as URLClassLoader).urLs - val quasarPattern = ".*quasar.*\\.jar$".toRegex() - val quasarFileUrl = urls.first { quasarPattern.matches(it.path) } - Paths.get(quasarFileUrl.toURI()).toString() - } - - override fun shutdown() { - if (waitForNodesToFinish) { - state.locked { - processes.forEach { it.waitFor() } - } - } - _shutdownManager?.shutdown() - _executorService?.shutdownNow() - } - - private fun establishRpc(config: NodeConfiguration, processDeathFuture: CordaFuture): CordaFuture { - val rpcAddress = config.rpcAddress!! - val client = CordaRPCClient(rpcAddress) - val connectionFuture = poll(executorService, "RPC connection") { - try { - client.start(config.rpcUsers[0].username, config.rpcUsers[0].password) - } catch (e: Exception) { - if (processDeathFuture.isDone) throw e - log.error("Exception $e, Retrying RPC connection at $rpcAddress") - null - } - } - return firstOf(connectionFuture, processDeathFuture) { - if (it == processDeathFuture) { - throw ListenProcessDeathException(rpcAddress, processDeathFuture.getOrThrow()) - } - val connection = connectionFuture.getOrThrow() - shutdownManager.registerShutdown(connection::forceClose) - connection.proxy - } - } - - override fun startNode( - defaultParameters: NodeParameters, - providedName: CordaX500Name?, - rpcUsers: List, - verifierType: VerifierType, - customOverrides: Map, - startInSameProcess: Boolean?, - maximumHeapSize: String, - logLevel: String? - ): CordaFuture { - val p2pAddress = portAllocation.nextHostAndPort() - val rpcAddress = portAllocation.nextHostAndPort() - val webAddress = portAllocation.nextHostAndPort() - // TODO: Derive name from the full picked name, don't just wrap the common name - val name = providedName ?: CordaX500Name(organisation = "${oneOf(names).organisation}-${p2pAddress.port}", locality = "London", country = "GB") - val users = rpcUsers.map { it.copy(permissions = it.permissions + DRIVER_REQUIRED_PERMISSIONS) } - val config = ConfigHelper.loadConfig( - baseDirectory = baseDirectory(name), - allowMissingConfig = true, - configOverrides = configOf( - "myLegalName" to name.toString(), - "p2pAddress" to p2pAddress.toString(), - "rpcAddress" to rpcAddress.toString(), - "webAddress" to webAddress.toString(), - "useTestClock" to useTestClock, - "rpcUsers" to if (users.isEmpty()) defaultRpcUserList else users.map { it.toConfig().root().unwrapped() }, - "verifierType" to verifierType.name - ) + customOverrides - ) - return startNodeInternal(config, webAddress, startInSameProcess, maximumHeapSize) - } - - internal fun startCordformNode(cordform: CordformNode): CordaFuture { - val name = CordaX500Name.parse(cordform.name) - // TODO We shouldn't have to allocate an RPC or web address if they're not specified. We're having to do this because of startNodeInternal - val rpcAddress = if (cordform.rpcAddress == null) mapOf("rpcAddress" to portAllocation.nextHostAndPort().toString()) else emptyMap() - val webAddress = cordform.webAddress?.let { NetworkHostAndPort.parse(it) } ?: portAllocation.nextHostAndPort() - val notary = if (cordform.notary != null) mapOf("notary" to cordform.notary) else emptyMap() - val rpcUsers = cordform.rpcUsers - val config = ConfigHelper.loadConfig( - baseDirectory = baseDirectory(name), - allowMissingConfig = true, - configOverrides = cordform.config + rpcAddress + notary + mapOf( - "rpcUsers" to if (rpcUsers.isEmpty()) defaultRpcUserList else rpcUsers - ) - ) - return startNodeInternal(config, webAddress, null, "200m") - } - - private fun queryWebserver(handle: NodeHandle, process: Process): WebserverHandle { - val protocol = if (handle.configuration.useHTTPS) "https://" else "http://" - val url = URL("$protocol${handle.webAddress}/api/status") - val client = OkHttpClient.Builder().connectTimeout(5, SECONDS).readTimeout(60, SECONDS).build() - - while (process.isAlive) try { - val response = client.newCall(Request.Builder().url(url).build()).execute() - if (response.isSuccessful && (response.body().string() == "started")) { - return WebserverHandle(handle.webAddress, process) - } - } catch (e: ConnectException) { - log.debug("Retrying webserver info at ${handle.webAddress}") - } - - throw IllegalStateException("Webserver at ${handle.webAddress} has died") - } - - override fun startWebserver(handle: NodeHandle, maximumHeapSize: String): CordaFuture { - val debugPort = if (isDebug) debugPortAllocation.nextPort() else null - val process = DriverDSL.startWebserver(handle, debugPort, maximumHeapSize) - shutdownManager.registerProcessShutdown(process) - val webReadyFuture = addressMustBeBoundFuture(executorService, handle.webAddress, process) - return webReadyFuture.map { queryWebserver(handle, process) } - } - - override fun start() { - _executorService = Executors.newScheduledThreadPool(2, ThreadFactoryBuilder().setNameFormat("driver-pool-thread-%d").build()) - _shutdownManager = ShutdownManager(executorService) - shutdownManager.registerShutdown { nodeInfoFilesCopier.close() } - val notaryInfos = generateNotaryIdentities() - val nodeHandles = startNotaries() - _notaries = notaryInfos.zip(nodeHandles) { (identity, validating), nodes -> NotaryHandle(identity, validating, nodes) } - } - - private fun generateNotaryIdentities(): List> { - return notarySpecs.map { spec -> - val identity = if (spec.cluster == null) { - ServiceIdentityGenerator.generateToDisk( - dirs = listOf(baseDirectory(spec.name)), - serviceName = spec.name.copy(commonName = NotaryService.constructId(validating = spec.validating)) - ) - } else { - ServiceIdentityGenerator.generateToDisk( - dirs = generateNodeNames(spec).map { baseDirectory(it) }, - serviceName = spec.name - ) - } - Pair(identity, spec.validating) - } - } - - private fun generateNodeNames(spec: NotarySpec): List { - return (0 until spec.cluster!!.clusterSize).map { spec.name.copy(commonName = null, organisation = "${spec.name.organisation}-$it") } - } - - private fun startNotaries(): List>> { - return notarySpecs.map { - when { - it.cluster == null -> startSingleNotary(it) - it.cluster is ClusterSpec.Raft -> startRaftNotaryCluster(it) - else -> throw IllegalArgumentException("BFT-SMaRt not supported") - } - } - } - - // TODO This mapping is done is several places including the gradle plugin. In general we need a better way of - // generating the configs for the nodes, probably making use of Any.toConfig() - private fun NotaryConfig.toConfigMap(): Map = mapOf("notary" to toConfig().root().unwrapped()) - - private fun startSingleNotary(spec: NotarySpec): CordaFuture> { - return startNode( - providedName = spec.name, - rpcUsers = spec.rpcUsers, - verifierType = spec.verifierType, - customOverrides = NotaryConfig(spec.validating).toConfigMap() - ).map { listOf(it) } - } - - private fun startRaftNotaryCluster(spec: NotarySpec): CordaFuture> { - fun notaryConfig(nodeAddress: NetworkHostAndPort, clusterAddress: NetworkHostAndPort? = null): Map { - val clusterAddresses = if (clusterAddress != null) listOf(clusterAddress) else emptyList() - val config = NotaryConfig( - validating = spec.validating, - raft = RaftConfig(nodeAddress = nodeAddress, clusterAddresses = clusterAddresses)) - return config.toConfigMap() - } - - val nodeNames = generateNodeNames(spec) - val clusterAddress = portAllocation.nextHostAndPort() - - // Start the first node that will bootstrap the cluster - val firstNodeFuture = startNode( - providedName = nodeNames[0], - rpcUsers = spec.rpcUsers, - verifierType = spec.verifierType, - customOverrides = notaryConfig(clusterAddress) - ) - - // All other nodes will join the cluster - val restNodeFutures = nodeNames.drop(1).map { - val nodeAddress = portAllocation.nextHostAndPort() - startNode( - providedName = it, - rpcUsers = spec.rpcUsers, - verifierType = spec.verifierType, - customOverrides = notaryConfig(nodeAddress, clusterAddress) - ) - } - - return firstNodeFuture.flatMap { first -> - restNodeFutures.transpose().map { rest -> listOf(first) + rest } - } - } - - fun baseDirectory(nodeName: CordaX500Name): Path { - val nodeDirectoryName = nodeName.organisation.filter { !it.isWhitespace() } - return driverDirectory / nodeDirectoryName - } - - override fun baseDirectory(nodeName: String): Path = baseDirectory(CordaX500Name.parse(nodeName)) - - /** - * @param initial number of nodes currently in the network map of a running node. - * @param networkMapCacheChangeObservable an observable returning the updates to the node network map. - * @return a [ConnectableObservable] which emits a new [Int] every time the number of registered nodes changes - * the initial value emitted is always [initial] - */ - private fun nodeCountObservable(initial: Int, networkMapCacheChangeObservable: Observable): - ConnectableObservable { - val count = AtomicInteger(initial) - return networkMapCacheChangeObservable.map { it -> - when (it) { - is NetworkMapCache.MapChange.Added -> count.incrementAndGet() - is NetworkMapCache.MapChange.Removed -> count.decrementAndGet() - is NetworkMapCache.MapChange.Modified -> count.get() - } - }.startWith(initial).replay() - } - - /** - * @param rpc the [CordaRPCOps] of a newly started node. - * @return a [CordaFuture] which resolves when every node started by driver has in its network map a number of nodes - * equal to the number of running nodes. The future will yield the number of connected nodes. - */ - private fun allNodesConnected(rpc: CordaRPCOps): CordaFuture { - val (snapshot, updates) = rpc.networkMapFeed() - val counterObservable = nodeCountObservable(snapshot.size, updates) - countObservables[rpc.nodeInfo().legalIdentities[0].name] = counterObservable - /* TODO: this might not always be the exact number of nodes one has to wait for, - * for example in the following sequence - * 1 start 3 nodes in order, A, B, C. - * 2 before the future returned by this function resolves, kill B - * At that point this future won't ever resolve as it will wait for nodes to know 3 other nodes. - */ - val requiredNodes = countObservables.size - - // This is an observable which yield the minimum number of nodes in each node network map. - val smallestSeenNetworkMapSize = Observable.combineLatest(countObservables.values.toList()) { args: Array -> - args.map { it as Int }.min() ?: 0 - } - val future = smallestSeenNetworkMapSize.filter { it >= requiredNodes }.toFuture() - counterObservable.connect() - return future - } - - private fun startNodeInternal(config: Config, - webAddress: NetworkHostAndPort, - startInProcess: Boolean?, - maximumHeapSize: String): CordaFuture { - val configuration = config.parseAsNodeConfiguration() - val baseDirectory = configuration.baseDirectory.createDirectories() - nodeInfoFilesCopier.addConfig(baseDirectory) - val onNodeExit: () -> Unit = { - nodeInfoFilesCopier.removeConfig(baseDirectory) - countObservables.remove(configuration.myLegalName) - } - if (startInProcess ?: startNodesInProcess) { - val nodeAndThreadFuture = startInProcessNode(executorService, configuration, config, cordappPackages) - shutdownManager.registerShutdown( - nodeAndThreadFuture.map { (node, thread) -> - { - node.dispose() - thread.interrupt() - } - } - ) - return nodeAndThreadFuture.flatMap { (node, thread) -> - establishRpc(configuration, openFuture()).flatMap { rpc -> - allNodesConnected(rpc).map { - NodeHandle.InProcess(rpc.nodeInfo(), rpc, configuration, webAddress, node, thread, onNodeExit) - } - } - } - } else { - val debugPort = if (isDebug) debugPortAllocation.nextPort() else null - val process = startOutOfProcessNode(configuration, config, quasarJarPath, debugPort, systemProperties, cordappPackages, maximumHeapSize) - if (waitForNodesToFinish) { - state.locked { - processes += process - } - } else { - shutdownManager.registerProcessShutdown(process) - } - val p2pReadyFuture = addressMustBeBoundFuture(executorService, configuration.p2pAddress, process) - return p2pReadyFuture.flatMap { - val processDeathFuture = poll(executorService, "process death") { - if (process.isAlive) null else process - } - establishRpc(configuration, processDeathFuture).flatMap { rpc -> - // Check for all nodes to have all other nodes in background in case RPC is failing over: - val networkMapFuture = executorService.fork { allNodesConnected(rpc) }.flatMap { it } - firstOf(processDeathFuture, networkMapFuture) { - if (it == processDeathFuture) { - throw ListenProcessDeathException(configuration.p2pAddress, process) - } - processDeathFuture.cancel(false) - log.info("Node handle is ready. NodeInfo: ${rpc.nodeInfo()}, WebAddress: $webAddress") - NodeHandle.OutOfProcess(rpc.nodeInfo(), rpc, configuration, webAddress, debugPort, process, - onNodeExit) - } - } - } - } - } - - override fun pollUntilNonNull(pollName: String, pollInterval: Duration, warnCount: Int, check: () -> A?): CordaFuture { - val pollFuture = poll(executorService, pollName, pollInterval, warnCount, check) - shutdownManager.registerShutdown { pollFuture.cancel(true) } - return pollFuture - } - - companion object { - private val defaultRpcUserList = listOf(User("default", "default", setOf("ALL")).toConfig().root().unwrapped()) - - private val names = arrayOf( - ALICE.name, - BOB.name, - DUMMY_BANK_A.name - ) - - private fun oneOf(array: Array) = array[Random().nextInt(array.size)] - - private fun startInProcessNode( - executorService: ScheduledExecutorService, - nodeConf: NodeConfiguration, - config: Config, - cordappPackages: List - ): CordaFuture, Thread>> { - return executorService.fork { - log.info("Starting in-process Node ${nodeConf.myLegalName.organisation}") - // Write node.conf - writeConfig(nodeConf.baseDirectory, "node.conf", config) - // TODO pass the version in? - val node = InProcessNode(nodeConf, MOCK_VERSION_INFO, cordappPackages).start() - val nodeThread = thread(name = nodeConf.myLegalName.organisation) { - node.internals.run() - } - node to nodeThread - }.flatMap { - nodeAndThread -> addressMustBeBoundFuture(executorService, nodeConf.p2pAddress).map { nodeAndThread } - } - } - - private fun startOutOfProcessNode( - nodeConf: NodeConfiguration, - config: Config, - quasarJarPath: String, - debugPort: Int?, - overriddenSystemProperties: Map, - cordappPackages: List, - maximumHeapSize: String, - logLevel: String? = null - ): Process { - log.info("Starting out-of-process Node ${nodeConf.myLegalName.organisation}, debug port is " + (debugPort ?: "not enabled")) - // Write node.conf - writeConfig(nodeConf.baseDirectory, "node.conf", config) - - val systemProperties = overriddenSystemProperties + mapOf( - "name" to nodeConf.myLegalName, - "visualvm.display.name" to "corda-${nodeConf.myLegalName}", - Node.scanPackagesSystemProperty to cordappPackages.joinToString(Node.scanPackagesSeparator), - "java.io.tmpdir" to System.getProperty("java.io.tmpdir"), // Inherit from parent process - "user.dir" to nodeConf.baseDirectory, //Enterprise only - "log4j2.debug" to if(debugPort != null) "true" else "false" - ) - // See experimental/quasar-hook/README.md for how to generate. - val excludePattern = "x(antlr**;bftsmart**;ch**;co.paralleluniverse**;com.codahale**;com.esotericsoftware**;" + - "com.fasterxml**;com.google**;com.ibm**;com.intellij**;com.jcabi**;com.nhaarman**;com.opengamma**;" + - "com.typesafe**;com.zaxxer**;de.javakaffee**;groovy**;groovyjarjarantlr**;groovyjarjarasm**;io.atomix**;" + - "io.github**;io.netty**;jdk**;joptsimple**;junit**;kotlin**;net.bytebuddy**;net.i2p**;org.apache**;" + - "org.assertj**;org.bouncycastle**;org.codehaus**;org.crsh**;org.dom4j**;org.fusesource**;org.h2**;" + - "org.hamcrest**;org.hibernate**;org.jboss**;org.jcp**;org.joda**;org.junit**;org.mockito**;org.objectweb**;" + - "org.objenesis**;org.slf4j**;org.w3c**;org.xml**;org.yaml**;reflectasm**;rx**)" - val extraJvmArguments = systemProperties.removeResolvedClasspath().map { "-D${it.key}=${it.value}" } + - "-javaagent:$quasarJarPath=$excludePattern" - val loggingLevel = logLevel ?:if (debugPort == null) "INFO" else "DEBUG" - - return ProcessUtilities.startCordaProcess( - className = "net.corda.node.Corda", // cannot directly get class for this, so just use string - arguments = listOf( - "--base-directory=${nodeConf.baseDirectory}", - "--logging-level=$loggingLevel", - "--no-local-shell" - ), - jdwpPort = debugPort, - extraJvmArguments = extraJvmArguments, - errorLogPath = nodeConf.baseDirectory / NodeStartup.LOGS_DIRECTORY_NAME / "error.log", - workingDirectory = nodeConf.baseDirectory, - maximumHeapSize = maximumHeapSize - ) - } - - private fun startWebserver(handle: NodeHandle, debugPort: Int?, maximumHeapSize: String): Process { - val className = "net.corda.webserver.WebServer" - return ProcessUtilities.startCordaProcess( - className = className, // cannot directly get class for this, so just use string - arguments = listOf("--base-directory", handle.configuration.baseDirectory.toString()), - jdwpPort = debugPort, - extraJvmArguments = listOf( - "-Dname=node-${handle.configuration.p2pAddress}-webserver", - "-Djava.io.tmpdir=${System.getProperty("java.io.tmpdir")}" // Inherit from parent process - ), - errorLogPath = Paths.get("error.$className.log"), - workingDirectory = null, - maximumHeapSize = maximumHeapSize - ) - } - - private fun getCallerPackage(): String { - return Exception() - .stackTrace - .first { it.fileName != "Driver.kt" } - .let { Class.forName(it.className).`package`?.name } - ?: throw IllegalStateException("Function instantiating driver must be defined in a package.") - } - - /** - * We have an alternative way of specifying classpath for spawned process: by using "-cp" option. So duplicating the setting of this - * rather long string is un-necessary and can be harmful on Windows. - */ - private fun Map.removeResolvedClasspath(): Map { - return filterNot { it.key == "java.class.path" } - } - } -} - -fun writeConfig(path: Path, filename: String, config: Config) { - val configString = config.root().render(ConfigRenderOptions.defaults()) - configString.byteInputStream().copyTo(path / filename, REPLACE_EXISTING) -} - diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/DriverDSL.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/DriverDSL.kt new file mode 100644 index 0000000000..42a394b51d --- /dev/null +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/DriverDSL.kt @@ -0,0 +1,92 @@ +package net.corda.testing.driver + +import net.corda.core.concurrent.CordaFuture +import net.corda.core.identity.CordaX500Name +import net.corda.core.identity.Party +import net.corda.core.internal.concurrent.map +import net.corda.node.internal.Node +import net.corda.node.services.config.VerifierType +import net.corda.nodeapi.internal.config.User +import net.corda.testing.node.NotarySpec +import java.nio.file.Path + +interface DriverDSL { + /** Returns a list of [NotaryHandle]s matching the list of [NotarySpec]s passed into [driver]. */ + val notaryHandles: List + + /** + * Returns the [NotaryHandle] for the single notary on the network. Throws if there are none or more than one. + * @see notaryHandles + */ + val defaultNotaryHandle: NotaryHandle get() { + return when (notaryHandles.size) { + 0 -> throw IllegalStateException("There are no notaries defined on the network") + 1 -> notaryHandles[0] + else -> throw IllegalStateException("There is more than one notary defined on the network") + } + } + + /** + * Returns the identity of the single notary on the network. Throws if there are none or more than one. + * @see defaultNotaryHandle + */ + val defaultNotaryIdentity: Party get() = defaultNotaryHandle.identity + + /** + * Returns a [CordaFuture] on the [NodeHandle] for the single-node notary on the network. Throws if there + * are no notaries or more than one, or if the notary is a distributed cluster. + * @see defaultNotaryHandle + * @see notaryHandles + */ + val defaultNotaryNode: CordaFuture get() { + return defaultNotaryHandle.nodeHandles.map { + it.singleOrNull() ?: throw IllegalStateException("Default notary is not a single node") + } + } + + /** + * Start a node. + * + * @param defaultParameters The default parameters for the node. Allows the node to be configured in builder style + * when called from Java code. + * @param providedName Optional name of the node, which will be its legal name in [Party]. Defaults to something + * random. Note that this must be unique as the driver uses it as a primary key! + * @param verifierType The type of transaction verifier to use. See: [VerifierType] + * @param rpcUsers List of users who are authorised to use the RPC system. Defaults to empty list. + * @param startInSameProcess Determines if the node should be started inside the same process the Driver is running + * in. If null the Driver-level value will be used. + * @return A [CordaFuture] on the [NodeHandle] to the node. The future will complete when the node is available. + */ + fun startNode( + defaultParameters: NodeParameters = NodeParameters(), + providedName: CordaX500Name? = defaultParameters.providedName, + rpcUsers: List = defaultParameters.rpcUsers, + verifierType: VerifierType = defaultParameters.verifierType, + customOverrides: Map = defaultParameters.customOverrides, + startInSameProcess: Boolean? = defaultParameters.startInSameProcess, + maximumHeapSize: String = defaultParameters.maximumHeapSize): CordaFuture + + /** + * Helper function for starting a [Node] with custom parameters from Java. + * + * @param parameters The default parameters for the driver. + * @return [NodeHandle] that will be available sometime in the future. + */ + fun startNode(parameters: NodeParameters): CordaFuture = startNode(defaultParameters = parameters) + + /** Call [startWebserver] with a default maximumHeapSize. */ + fun startWebserver(handle: NodeHandle): CordaFuture = startWebserver(handle, "200m") + + /** + * Starts a web server for a node + * @param handle The handle for the node that this webserver connects to via RPC. + * @param maximumHeapSize Argument for JVM -Xmx option e.g. "200m". + */ + fun startWebserver(handle: NodeHandle, maximumHeapSize: String): CordaFuture + + /** + * Returns the base directory for a node with the given [CordaX500Name]. This method is useful if the base directory + * is needed before the node is started. + */ + fun baseDirectory(nodeName: CordaX500Name): Path +} diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/internal/DriverDSLImpl.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/internal/DriverDSLImpl.kt new file mode 100644 index 0000000000..a4046764d6 --- /dev/null +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/internal/DriverDSLImpl.kt @@ -0,0 +1,704 @@ +package net.corda.testing.internal + +import com.google.common.util.concurrent.ThreadFactoryBuilder +import com.typesafe.config.Config +import com.typesafe.config.ConfigRenderOptions +import net.corda.client.rpc.CordaRPCClient +import net.corda.cordform.CordformContext +import net.corda.cordform.CordformNode +import net.corda.core.concurrent.CordaFuture +import net.corda.core.concurrent.firstOf +import net.corda.core.identity.CordaX500Name +import net.corda.core.identity.Party +import net.corda.core.internal.ThreadBox +import net.corda.core.internal.concurrent.* +import net.corda.core.internal.copyTo +import net.corda.core.internal.createDirectories +import net.corda.core.internal.div +import net.corda.core.messaging.CordaRPCOps +import net.corda.core.node.services.NetworkMapCache +import net.corda.core.node.services.NotaryService +import net.corda.core.toFuture +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.core.utilities.contextLogger +import net.corda.core.utilities.getOrThrow +import net.corda.core.utilities.millis +import net.corda.node.internal.Node +import net.corda.node.internal.NodeStartup +import net.corda.node.internal.StartedNode +import net.corda.node.services.Permissions +import net.corda.node.services.config.* +import net.corda.node.utilities.ServiceIdentityGenerator +import net.corda.nodeapi.NodeInfoFilesCopier +import net.corda.nodeapi.internal.addShutdownHook +import net.corda.nodeapi.internal.config.User +import net.corda.nodeapi.internal.config.toConfig +import net.corda.testing.ALICE +import net.corda.testing.BOB +import net.corda.testing.DUMMY_BANK_A +import net.corda.testing.driver.* +import net.corda.testing.node.ClusterSpec +import net.corda.testing.node.MockServices.Companion.MOCK_VERSION_INFO +import net.corda.testing.node.NotarySpec +import net.corda.testing.setGlobalSerialization +import okhttp3.OkHttpClient +import okhttp3.Request +import rx.Observable +import rx.observables.ConnectableObservable +import java.net.ConnectException +import java.net.URL +import java.net.URLClassLoader +import java.nio.file.Path +import java.nio.file.Paths +import java.nio.file.StandardCopyOption +import java.time.Duration +import java.time.Instant +import java.time.ZoneOffset +import java.time.format.DateTimeFormatter +import java.util.* +import java.util.concurrent.Executors +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicInteger +import kotlin.concurrent.thread + +class DriverDSLImpl( + val portAllocation: PortAllocation, + val debugPortAllocation: PortAllocation, + val systemProperties: Map, + val driverDirectory: Path, + val useTestClock: Boolean, + val isDebug: Boolean, + val startNodesInProcess: Boolean, + val waitForNodesToFinish: Boolean, + extraCordappPackagesToScan: List, + val jmxPolicy: JmxPolicy, + val notarySpecs: List +) : InternalDriverDSL { + private var _executorService: ScheduledExecutorService? = null + val executorService get() = _executorService!! + private var _shutdownManager: ShutdownManager? = null + override val shutdownManager get() = _shutdownManager!! + private val cordappPackages = extraCordappPackagesToScan + getCallerPackage() + // TODO: this object will copy NodeInfo files from started nodes to other nodes additional-node-infos/ + // This uses the FileSystem and adds a delay (~5 seconds) given by the time we wait before polling the file system. + // Investigate whether we can avoid that. + private val nodeInfoFilesCopier = NodeInfoFilesCopier() + // Map from a nodes legal name to an observable emitting the number of nodes in its network map. + private val countObservables = mutableMapOf>() + private lateinit var _notaries: List + override val notaryHandles: List get() = _notaries + + class State { + val processes = ArrayList() + } + + private val state = ThreadBox(State()) + + //TODO: remove this once we can bundle quasar properly. + private val quasarJarPath: String by lazy { + resolveJar(".*quasar.*\\.jar$") + } + + private val jolokiaJarPath: String by lazy { + resolveJar(".*jolokia-jvm-.*-agent\\.jar$") + } + + private fun resolveJar(jarNamePattern: String): String { + return try { + val cl = ClassLoader.getSystemClassLoader() + val urls = (cl as URLClassLoader).urLs + val jarPattern = jarNamePattern.toRegex() + val jarFileUrl = urls.first { jarPattern.matches(it.path) } + Paths.get(jarFileUrl.toURI()).toString() + } + catch(e: Exception) { + log.warn("Unable to locate JAR `$jarNamePattern` on classpath: ${e.message}", e) + throw e + } + } + + override fun shutdown() { + if (waitForNodesToFinish) { + state.locked { + processes.forEach { it.waitFor() } + } + } + _shutdownManager?.shutdown() + _executorService?.shutdownNow() + } + + private fun establishRpc(config: NodeConfiguration, processDeathFuture: CordaFuture): CordaFuture { + val rpcAddress = config.rpcAddress!! + val client = CordaRPCClient(rpcAddress) + val connectionFuture = poll(executorService, "RPC connection") { + try { + client.start(config.rpcUsers[0].username, config.rpcUsers[0].password) + } catch (e: Exception) { + if (processDeathFuture.isDone) throw e + log.error("Exception $e, Retrying RPC connection at $rpcAddress") + null + } + } + return firstOf(connectionFuture, processDeathFuture) { + if (it == processDeathFuture) { + throw ListenProcessDeathException(rpcAddress, processDeathFuture.getOrThrow()) + } + val connection = connectionFuture.getOrThrow() + shutdownManager.registerShutdown(connection::close) + connection.proxy + } + } + + override fun startNode( + defaultParameters: NodeParameters, + providedName: CordaX500Name?, + rpcUsers: List, + verifierType: VerifierType, + customOverrides: Map, + startInSameProcess: Boolean?, + maximumHeapSize: String + ): CordaFuture { + val p2pAddress = portAllocation.nextHostAndPort() + val rpcAddress = portAllocation.nextHostAndPort() + val webAddress = portAllocation.nextHostAndPort() + // TODO: Derive name from the full picked name, don't just wrap the common name + val name = providedName ?: CordaX500Name(organisation = "${oneOf(names).organisation}-${p2pAddress.port}", locality = "London", country = "GB") + val users = rpcUsers.map { it.copy(permissions = it.permissions + DRIVER_REQUIRED_PERMISSIONS) } + val config = ConfigHelper.loadConfig( + baseDirectory = baseDirectory(name), + allowMissingConfig = true, + configOverrides = configOf( + "myLegalName" to name.toString(), + "p2pAddress" to p2pAddress.toString(), + "rpcAddress" to rpcAddress.toString(), + "webAddress" to webAddress.toString(), + "useTestClock" to useTestClock, + "rpcUsers" to if (users.isEmpty()) defaultRpcUserList else users.map { it.toConfig().root().unwrapped() }, + "verifierType" to verifierType.name + ) + customOverrides + ) + return startNodeInternal(config, webAddress, startInSameProcess, maximumHeapSize) + } + + internal fun startCordformNode(cordform: CordformNode): CordaFuture { + val name = CordaX500Name.parse(cordform.name) + // TODO We shouldn't have to allocate an RPC or web address if they're not specified. We're having to do this because of startNodeInternal + val rpcAddress = if (cordform.rpcAddress == null) mapOf("rpcAddress" to portAllocation.nextHostAndPort().toString()) else emptyMap() + val webAddress = cordform.webAddress?.let { NetworkHostAndPort.parse(it) } ?: portAllocation.nextHostAndPort() + val notary = if (cordform.notary != null) mapOf("notary" to cordform.notary) else emptyMap() + val rpcUsers = cordform.rpcUsers + val config = ConfigHelper.loadConfig( + baseDirectory = baseDirectory(name), + allowMissingConfig = true, + configOverrides = cordform.config + rpcAddress + notary + mapOf( + "rpcUsers" to if (rpcUsers.isEmpty()) defaultRpcUserList else rpcUsers + ) + ) + return startNodeInternal(config, webAddress, null, "200m") + } + + private fun queryWebserver(handle: NodeHandle, process: Process): WebserverHandle { + val protocol = if (handle.configuration.useHTTPS) "https://" else "http://" + val url = URL("$protocol${handle.webAddress}/api/status") + val client = OkHttpClient.Builder().connectTimeout(5, TimeUnit.SECONDS).readTimeout(60, TimeUnit.SECONDS).build() + + while (process.isAlive) try { + val response = client.newCall(Request.Builder().url(url).build()).execute() + if (response.isSuccessful && (response.body().string() == "started")) { + return WebserverHandle(handle.webAddress, process) + } + } catch (e: ConnectException) { + log.debug("Retrying webserver info at ${handle.webAddress}") + } + + throw IllegalStateException("Webserver at ${handle.webAddress} has died") + } + + override fun startWebserver(handle: NodeHandle, maximumHeapSize: String): CordaFuture { + val debugPort = if (isDebug) debugPortAllocation.nextPort() else null + val process = startWebserver(handle, debugPort, maximumHeapSize) + shutdownManager.registerProcessShutdown(process) + val webReadyFuture = addressMustBeBoundFuture(executorService, handle.webAddress, process) + return webReadyFuture.map { queryWebserver(handle, process) } + } + + override fun start() { + _executorService = Executors.newScheduledThreadPool(2, ThreadFactoryBuilder().setNameFormat("driver-pool-thread-%d").build()) + _shutdownManager = ShutdownManager(executorService) + shutdownManager.registerShutdown { nodeInfoFilesCopier.close() } + val notaryInfos = generateNotaryIdentities() + val nodeHandles = startNotaries() + _notaries = notaryInfos.zip(nodeHandles) { (identity, validating), nodes -> NotaryHandle(identity, validating, nodes) } + } + + private fun generateNotaryIdentities(): List> { + return notarySpecs.map { spec -> + val identity = if (spec.cluster == null) { + ServiceIdentityGenerator.generateToDisk( + dirs = listOf(baseDirectory(spec.name)), + serviceName = spec.name.copy(commonName = NotaryService.constructId(validating = spec.validating)) + ) + } else { + ServiceIdentityGenerator.generateToDisk( + dirs = generateNodeNames(spec).map { baseDirectory(it) }, + serviceName = spec.name + ) + } + Pair(identity, spec.validating) + } + } + + private fun generateNodeNames(spec: NotarySpec): List { + return (0 until spec.cluster!!.clusterSize).map { spec.name.copy(commonName = null, organisation = "${spec.name.organisation}-$it") } + } + + private fun startNotaries(): List>> { + return notarySpecs.map { + when { + it.cluster == null -> startSingleNotary(it) + it.cluster is ClusterSpec.Raft -> startRaftNotaryCluster(it) + else -> throw IllegalArgumentException("BFT-SMaRt not supported") + } + } + } + + // TODO This mapping is done is several places including the gradle plugin. In general we need a better way of + // generating the configs for the nodes, probably making use of Any.toConfig() + private fun NotaryConfig.toConfigMap(): Map = mapOf("notary" to toConfig().root().unwrapped()) + + private fun startSingleNotary(spec: NotarySpec): CordaFuture> { + return startNode( + providedName = spec.name, + rpcUsers = spec.rpcUsers, + verifierType = spec.verifierType, + customOverrides = NotaryConfig(spec.validating).toConfigMap() + ).map { listOf(it) } + } + + private fun startRaftNotaryCluster(spec: NotarySpec): CordaFuture> { + fun notaryConfig(nodeAddress: NetworkHostAndPort, clusterAddress: NetworkHostAndPort? = null): Map { + val clusterAddresses = if (clusterAddress != null) listOf(clusterAddress) else emptyList() + val config = NotaryConfig( + validating = spec.validating, + raft = RaftConfig(nodeAddress = nodeAddress, clusterAddresses = clusterAddresses)) + return config.toConfigMap() + } + + val nodeNames = generateNodeNames(spec) + val clusterAddress = portAllocation.nextHostAndPort() + + // Start the first node that will bootstrap the cluster + val firstNodeFuture = startNode( + providedName = nodeNames[0], + rpcUsers = spec.rpcUsers, + verifierType = spec.verifierType, + customOverrides = notaryConfig(clusterAddress) + mapOf( + "database.serverNameTablePrefix" to nodeNames[0].toString().replace(Regex("[^0-9A-Za-z]+"), "") + ) + ) + + // All other nodes will join the cluster + val restNodeFutures = nodeNames.drop(1).map { + val nodeAddress = portAllocation.nextHostAndPort() + startNode( + providedName = it, + rpcUsers = spec.rpcUsers, + verifierType = spec.verifierType, + customOverrides = notaryConfig(nodeAddress, clusterAddress) + mapOf( + "database.serverNameTablePrefix" to it.toString().replace(Regex("[^0-9A-Za-z]+"), "") + ) + ) + } + + return firstNodeFuture.flatMap { first -> + restNodeFutures.transpose().map { rest -> listOf(first) + rest } + } + } + + override fun baseDirectory(nodeName: CordaX500Name): Path { + val nodeDirectoryName = nodeName.organisation.filter { !it.isWhitespace() } + return driverDirectory / nodeDirectoryName + } + + /** + * @param initial number of nodes currently in the network map of a running node. + * @param networkMapCacheChangeObservable an observable returning the updates to the node network map. + * @return a [ConnectableObservable] which emits a new [Int] every time the number of registered nodes changes + * the initial value emitted is always [initial] + */ + private fun nodeCountObservable(initial: Int, networkMapCacheChangeObservable: Observable): + ConnectableObservable { + val count = AtomicInteger(initial) + return networkMapCacheChangeObservable.map { it -> + when (it) { + is NetworkMapCache.MapChange.Added -> count.incrementAndGet() + is NetworkMapCache.MapChange.Removed -> count.decrementAndGet() + is NetworkMapCache.MapChange.Modified -> count.get() + } + }.startWith(initial).replay() + } + + /** + * @param rpc the [CordaRPCOps] of a newly started node. + * @return a [CordaFuture] which resolves when every node started by driver has in its network map a number of nodes + * equal to the number of running nodes. The future will yield the number of connected nodes. + */ + private fun allNodesConnected(rpc: CordaRPCOps): CordaFuture { + val (snapshot, updates) = rpc.networkMapFeed() + val counterObservable = nodeCountObservable(snapshot.size, updates) + countObservables[rpc.nodeInfo().legalIdentities[0].name] = counterObservable + /* TODO: this might not always be the exact number of nodes one has to wait for, + * for example in the following sequence + * 1 start 3 nodes in order, A, B, C. + * 2 before the future returned by this function resolves, kill B + * At that point this future won't ever resolve as it will wait for nodes to know 3 other nodes. + */ + val requiredNodes = countObservables.size + + // This is an observable which yield the minimum number of nodes in each node network map. + val smallestSeenNetworkMapSize = Observable.combineLatest(countObservables.values.toList()) { args: Array -> + args.map { it as Int }.min() ?: 0 + } + val future = smallestSeenNetworkMapSize.filter { it >= requiredNodes }.toFuture() + counterObservable.connect() + return future + } + + private fun startNodeInternal(config: Config, + webAddress: NetworkHostAndPort, + startInProcess: Boolean?, + maximumHeapSize: String): CordaFuture { + val configuration = config.parseAsNodeConfiguration() + val baseDirectory = configuration.baseDirectory.createDirectories() + nodeInfoFilesCopier.addConfig(baseDirectory) + val onNodeExit: () -> Unit = { + nodeInfoFilesCopier.removeConfig(baseDirectory) + countObservables.remove(configuration.myLegalName) + } + if (startInProcess ?: startNodesInProcess) { + val nodeAndThreadFuture = startInProcessNode(executorService, configuration, config, cordappPackages) + shutdownManager.registerShutdown( + nodeAndThreadFuture.map { (node, thread) -> + { + node.dispose() + thread.interrupt() + } + } + ) + return nodeAndThreadFuture.flatMap { (node, thread) -> + establishRpc(configuration, openFuture()).flatMap { rpc -> + allNodesConnected(rpc).map { + NodeHandle.InProcess(rpc.nodeInfo(), rpc, configuration, webAddress, node, thread, onNodeExit) + } + } + } + } else { + val debugPort = if (isDebug) debugPortAllocation.nextPort() else null + val monitorPort = if (jmxPolicy.startJmxHttpServer) jmxPolicy.jmxHttpServerPortAllocation?.nextPort() else null + val process = startOutOfProcessNode(configuration, config, quasarJarPath, debugPort, jolokiaJarPath, monitorPort, systemProperties, cordappPackages, maximumHeapSize) + if (waitForNodesToFinish) { + state.locked { + processes += process + } + } else { + shutdownManager.registerProcessShutdown(process) + } + val p2pReadyFuture = addressMustBeBoundFuture(executorService, configuration.p2pAddress, process) + return p2pReadyFuture.flatMap { + val processDeathFuture = poll(executorService, "process death") { + if (process.isAlive) null else process + } + establishRpc(configuration, processDeathFuture).flatMap { rpc -> + // Check for all nodes to have all other nodes in background in case RPC is failing over: + val networkMapFuture = executorService.fork { allNodesConnected(rpc) }.flatMap { it } + firstOf(processDeathFuture, networkMapFuture) { + if (it == processDeathFuture) { + throw ListenProcessDeathException(configuration.p2pAddress, process) + } + processDeathFuture.cancel(false) + log.info("Node handle is ready. NodeInfo: ${rpc.nodeInfo()}, WebAddress: $webAddress") + NodeHandle.OutOfProcess(rpc.nodeInfo(), rpc, configuration, webAddress, debugPort, process, + onNodeExit) + } + } + } + } + } + + override fun pollUntilNonNull(pollName: String, pollInterval: Duration, warnCount: Int, check: () -> A?): CordaFuture { + val pollFuture = poll(executorService, pollName, pollInterval, warnCount, check) + shutdownManager.registerShutdown { pollFuture.cancel(true) } + return pollFuture + } + + companion object { + internal val log = contextLogger() + + private val defaultRpcUserList = listOf(User("default", "default", setOf("ALL")).toConfig().root().unwrapped()) + + private val names = arrayOf( + ALICE.name, + BOB.name, + DUMMY_BANK_A.name + ) + + /** + * A sub-set of permissions that grant most of the essential operations used in the unit/integration tests as well as + * in demo application like NodeExplorer. + */ + private val DRIVER_REQUIRED_PERMISSIONS = setOf( + Permissions.invokeRpc(CordaRPCOps::nodeInfo), + Permissions.invokeRpc(CordaRPCOps::networkMapFeed), + Permissions.invokeRpc(CordaRPCOps::networkMapSnapshot), + Permissions.invokeRpc(CordaRPCOps::notaryIdentities), + Permissions.invokeRpc(CordaRPCOps::stateMachinesFeed), + Permissions.invokeRpc(CordaRPCOps::stateMachineRecordedTransactionMappingFeed), + Permissions.invokeRpc(CordaRPCOps::nodeInfoFromParty), + Permissions.invokeRpc(CordaRPCOps::internalVerifiedTransactionsFeed), + Permissions.invokeRpc("vaultQueryBy"), + Permissions.invokeRpc("vaultTrackBy"), + Permissions.invokeRpc(CordaRPCOps::registeredFlows) + ) + + private fun oneOf(array: Array) = array[Random().nextInt(array.size)] + + private fun startInProcessNode( + executorService: ScheduledExecutorService, + nodeConf: NodeConfiguration, + config: Config, + cordappPackages: List + ): CordaFuture, Thread>> { + return executorService.fork { + log.info("Starting in-process Node ${nodeConf.myLegalName.organisation}") + // Write node.conf + writeConfig(nodeConf.baseDirectory, "node.conf", config) + // TODO pass the version in? + val node = InProcessNode(nodeConf, MOCK_VERSION_INFO, cordappPackages).start() + val nodeThread = thread(name = nodeConf.myLegalName.organisation) { + node.internals.run() + } + node to nodeThread + }.flatMap { + nodeAndThread -> addressMustBeBoundFuture(executorService, nodeConf.p2pAddress).map { nodeAndThread } + } + } + + private fun startOutOfProcessNode( + nodeConf: NodeConfiguration, + config: Config, + quasarJarPath: String, + debugPort: Int?, + jolokiaJarPath: String, + monitorPort: Int?, + overriddenSystemProperties: Map, + cordappPackages: List, + maximumHeapSize: String + ): Process { + log.info("Starting out-of-process Node ${nodeConf.myLegalName.organisation}, debug port is " + (debugPort ?: "not enabled") + ", jolokia monitoring port is " + (monitorPort ?: "not enabled")) + // Write node.conf + writeConfig(nodeConf.baseDirectory, "node.conf", config) + + val systemProperties = overriddenSystemProperties + mapOf( + "name" to nodeConf.myLegalName, + "visualvm.display.name" to "corda-${nodeConf.myLegalName}", + Node.scanPackagesSystemProperty to cordappPackages.joinToString(Node.scanPackagesSeparator), + "java.io.tmpdir" to System.getProperty("java.io.tmpdir"), // Inherit from parent process + "log4j2.debug" to if(debugPort != null) "true" else "false" + ) + // See experimental/quasar-hook/README.md for how to generate. + val excludePattern = "x(antlr**;bftsmart**;ch**;co.paralleluniverse**;com.codahale**;com.esotericsoftware**;" + + "com.fasterxml**;com.google**;com.ibm**;com.intellij**;com.jcabi**;com.nhaarman**;com.opengamma**;" + + "com.typesafe**;com.zaxxer**;de.javakaffee**;groovy**;groovyjarjarantlr**;groovyjarjarasm**;io.atomix**;" + + "io.github**;io.netty**;jdk**;joptsimple**;junit**;kotlin**;net.bytebuddy**;net.i2p**;org.apache**;" + + "org.assertj**;org.bouncycastle**;org.codehaus**;org.crsh**;org.dom4j**;org.fusesource**;org.h2**;" + + "org.hamcrest**;org.hibernate**;org.jboss**;org.jcp**;org.joda**;org.junit**;org.mockito**;org.objectweb**;" + + "org.objenesis**;org.slf4j**;org.w3c**;org.xml**;org.yaml**;reflectasm**;rx**)" + val extraJvmArguments = systemProperties.removeResolvedClasspath().map { "-D${it.key}=${it.value}" } + + "-javaagent:$quasarJarPath=$excludePattern" + val jolokiaAgent = monitorPort?.let { "-javaagent:$jolokiaJarPath=port=$monitorPort,host=localhost" } + val loggingLevel = if (debugPort == null) "INFO" else "DEBUG" + + return ProcessUtilities.startCordaProcess( + className = "net.corda.node.Corda", // cannot directly get class for this, so just use string + arguments = listOf( + "--base-directory=${nodeConf.baseDirectory}", + "--logging-level=$loggingLevel", + "--no-local-shell" + ), + jdwpPort = debugPort, + extraJvmArguments = extraJvmArguments + listOfNotNull(jolokiaAgent), + errorLogPath = nodeConf.baseDirectory / NodeStartup.LOGS_DIRECTORY_NAME / "error.log", + workingDirectory = nodeConf.baseDirectory, + maximumHeapSize = maximumHeapSize + ) + } + + private fun startWebserver(handle: NodeHandle, debugPort: Int?, maximumHeapSize: String): Process { + val className = "net.corda.webserver.WebServer" + return ProcessUtilities.startCordaProcess( + className = className, // cannot directly get class for this, so just use string + arguments = listOf("--base-directory", handle.configuration.baseDirectory.toString()), + jdwpPort = debugPort, + extraJvmArguments = listOf( + "-Dname=node-${handle.configuration.p2pAddress}-webserver", + "-Djava.io.tmpdir=${System.getProperty("java.io.tmpdir")}" // Inherit from parent process + ), + errorLogPath = Paths.get("error.$className.log"), + workingDirectory = null, + maximumHeapSize = maximumHeapSize + ) + } + + /** + * Get the package of the caller to the driver so that it can be added to the list of packages the nodes will scan. + * This makes the driver automatically pick the CorDapp module that it's run from. + * + * This returns List rather than String? to make it easier to bolt onto extraCordappPackagesToScan. + */ + private fun getCallerPackage(): List { + val stackTrace = Throwable().stackTrace + val index = stackTrace.indexOfLast { it.className == "net.corda.testing.driver.Driver" } + // In this case we're dealing with the the RPCDriver or one of it's cousins which are internal and we don't care about them + if (index == -1) return emptyList() + val callerPackage = Class.forName(stackTrace[index + 1].className).`package` ?: + throw IllegalStateException("Function instantiating driver must be defined in a package.") + return listOf(callerPackage.name) + } + + /** + * We have an alternative way of specifying classpath for spawned process: by using "-cp" option. So duplicating the setting of this + * rather long string is un-necessary and can be harmful on Windows. + */ + private fun Map.removeResolvedClasspath(): Map { + return filterNot { it.key == "java.class.path" } + } + } +} + +interface InternalDriverDSL : DriverDSL, CordformContext { + private companion object { + private val DEFAULT_POLL_INTERVAL = 500.millis + private const val DEFAULT_WARN_COUNT = 120 + } + + val shutdownManager: ShutdownManager + + override fun baseDirectory(nodeName: String): Path = baseDirectory(CordaX500Name.parse(nodeName)) + + /** + * Polls a function until it returns a non-null value. Note that there is no timeout on the polling. + * + * @param pollName A description of what is being polled. + * @param pollInterval The interval of polling. + * @param warnCount The number of polls after the Driver gives a warning. + * @param check The function being polled. + * @return A future that completes with the non-null value [check] has returned. + */ + fun pollUntilNonNull(pollName: String, pollInterval: Duration = DEFAULT_POLL_INTERVAL, warnCount: Int = DEFAULT_WARN_COUNT, check: () -> A?): CordaFuture + + /** + * Polls the given function until it returns true. + * @see pollUntilNonNull + */ + fun pollUntilTrue(pollName: String, pollInterval: Duration = DEFAULT_POLL_INTERVAL, warnCount: Int = DEFAULT_WARN_COUNT, check: () -> Boolean): CordaFuture { + return pollUntilNonNull(pollName, pollInterval, warnCount) { if (check()) Unit else null } + } + + fun start() + + fun shutdown() +} + +/** + * This is a helper method to allow extending of the DSL, along the lines of + * interface SomeOtherExposedDSLInterface : DriverDSL + * interface SomeOtherInternalDSLInterface : InternalDriverDSL, SomeOtherExposedDSLInterface + * class SomeOtherDSL(val driverDSL : DriverDSLImpl) : InternalDriverDSL by driverDSL, SomeOtherInternalDSLInterface + * + * @param coerce We need this explicit coercion witness because we can't put an extra DI : D bound in a `where` clause. + */ +fun genericDriver( + driverDsl: D, + initialiseSerialization: Boolean = true, + coerce: (D) -> DI, + dsl: DI.() -> A +): A { + val serializationEnv = setGlobalSerialization(initialiseSerialization) + val shutdownHook = addShutdownHook(driverDsl::shutdown) + try { + driverDsl.start() + return dsl(coerce(driverDsl)) + } catch (exception: Throwable) { + DriverDSLImpl.log.error("Driver shutting down because of exception", exception) + throw exception + } finally { + driverDsl.shutdown() + shutdownHook.cancel() + serializationEnv.unset() + } +} + +/** + * This is a helper method to allow extending of the DSL, along the lines of + * interface SomeOtherExposedDSLInterface : DriverDSL + * interface SomeOtherInternalDSLInterface : InternalDriverDSL, SomeOtherExposedDSLInterface + * class SomeOtherDSL(val driverDSL : DriverDSLImpl) : InternalDriverDSL by driverDSL, SomeOtherInternalDSLInterface + * + * @param coerce We need this explicit coercion witness because we can't put an extra DI : D bound in a `where` clause. + */ +fun genericDriver( + defaultParameters: DriverParameters = DriverParameters(), + isDebug: Boolean = defaultParameters.isDebug, + driverDirectory: Path = defaultParameters.driverDirectory, + portAllocation: PortAllocation = defaultParameters.portAllocation, + debugPortAllocation: PortAllocation = defaultParameters.debugPortAllocation, + systemProperties: Map = defaultParameters.systemProperties, + useTestClock: Boolean = defaultParameters.useTestClock, + initialiseSerialization: Boolean = defaultParameters.initialiseSerialization, + waitForNodesToFinish: Boolean = defaultParameters.waitForAllNodesToFinish, + startNodesInProcess: Boolean = defaultParameters.startNodesInProcess, + notarySpecs: List, + extraCordappPackagesToScan: List = defaultParameters.extraCordappPackagesToScan, + jmxPolicy: JmxPolicy = JmxPolicy(), + driverDslWrapper: (DriverDSLImpl) -> D, + coerce: (D) -> DI, dsl: DI.() -> A +): A { + val serializationEnv = setGlobalSerialization(initialiseSerialization) + val driverDsl = driverDslWrapper( + DriverDSLImpl( + portAllocation = portAllocation, + debugPortAllocation = debugPortAllocation, + systemProperties = systemProperties, + driverDirectory = driverDirectory.toAbsolutePath(), + useTestClock = useTestClock, + isDebug = isDebug, + startNodesInProcess = startNodesInProcess, + waitForNodesToFinish = waitForNodesToFinish, + extraCordappPackagesToScan = extraCordappPackagesToScan, + jmxPolicy = jmxPolicy, + notarySpecs = notarySpecs + ) + ) + val shutdownHook = addShutdownHook(driverDsl::shutdown) + try { + driverDsl.start() + return dsl(coerce(driverDsl)) + } catch (exception: Throwable) { + DriverDSLImpl.log.error("Driver shutting down because of exception", exception) + throw exception + } finally { + driverDsl.shutdown() + shutdownHook.cancel() + serializationEnv.unset() + } +} + +fun getTimestampAsDirectoryName(): String { + return DateTimeFormatter.ofPattern("yyyyMMddHHmmss").withZone(ZoneOffset.UTC).format(Instant.now()) +} + +fun writeConfig(path: Path, filename: String, config: Config) { + val configString = config.root().render(ConfigRenderOptions.defaults()) + configString.byteInputStream().copyTo(path / filename, StandardCopyOption.REPLACE_EXISTING) +} diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt new file mode 100644 index 0000000000..b24db80a3f --- /dev/null +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt @@ -0,0 +1,93 @@ +package net.corda.testing.internal + +import net.corda.core.CordaException +import net.corda.core.concurrent.CordaFuture +import net.corda.core.internal.concurrent.openFuture +import net.corda.core.internal.times +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.core.utilities.getOrThrow +import net.corda.core.utilities.millis +import net.corda.core.utilities.seconds +import org.slf4j.LoggerFactory +import java.net.Socket +import java.net.SocketException +import java.time.Duration +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.TimeUnit + +private val log = LoggerFactory.getLogger("net.corda.testing.internal.InternalTestUtils") + +/** + * @throws ListenProcessDeathException if [listenProcess] dies before the check succeeds, i.e. the check can't succeed as intended. + */ +fun addressMustBeBound(executorService: ScheduledExecutorService, hostAndPort: NetworkHostAndPort, listenProcess: Process? = null) { + addressMustBeBoundFuture(executorService, hostAndPort, listenProcess).getOrThrow() +} + +fun addressMustBeBoundFuture(executorService: ScheduledExecutorService, hostAndPort: NetworkHostAndPort, listenProcess: Process? = null): CordaFuture { + return poll(executorService, "address $hostAndPort to bind") { + if (listenProcess != null && !listenProcess.isAlive) { + throw ListenProcessDeathException(hostAndPort, listenProcess) + } + try { + Socket(hostAndPort.host, hostAndPort.port).close() + Unit + } catch (_exception: SocketException) { + null + } + } +} + +/* + * The default timeout value of 40 seconds have been chosen based on previous node shutdown time estimate. + * It's been observed that nodes can take up to 30 seconds to shut down, so just to stay on the safe side the 60 seconds + * timeout has been chosen. + */ +fun addressMustNotBeBound(executorService: ScheduledExecutorService, hostAndPort: NetworkHostAndPort, timeout: Duration = 40.seconds) { + addressMustNotBeBoundFuture(executorService, hostAndPort).getOrThrow(timeout) +} + +fun addressMustNotBeBoundFuture(executorService: ScheduledExecutorService, hostAndPort: NetworkHostAndPort): CordaFuture { + return poll(executorService, "address $hostAndPort to unbind") { + try { + Socket(hostAndPort.host, hostAndPort.port).close() + null + } catch (_exception: SocketException) { + Unit + } + } +} + +fun poll( + executorService: ScheduledExecutorService, + pollName: String, + pollInterval: Duration = 500.millis, + warnCount: Int = 120, + check: () -> A? +): CordaFuture { + val resultFuture = openFuture() + val task = object : Runnable { + var counter = -1 + override fun run() { + if (resultFuture.isCancelled) return // Give up, caller can no longer get the result. + if (++counter == warnCount) { + log.warn("Been polling $pollName for ${(pollInterval * warnCount.toLong()).seconds} seconds...") + } + try { + val checkResult = check() + if (checkResult != null) { + resultFuture.set(checkResult) + } else { + executorService.schedule(this, pollInterval.toMillis(), TimeUnit.MILLISECONDS) + } + } catch (t: Throwable) { + resultFuture.setException(t) + } + } + } + executorService.submit(task) // The check may be expensive, so always run it in the background even the first time. + return resultFuture +} + +class ListenProcessDeathException(hostAndPort: NetworkHostAndPort, listenProcess: Process) : + CordaException("The process that was expected to listen on $hostAndPort has died with status: ${listenProcess.exitValue()}") diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/internal/NodeBasedTest.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/internal/NodeBasedTest.kt index 62f083a746..21a0bfe288 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/internal/NodeBasedTest.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/internal/NodeBasedTest.kt @@ -12,14 +12,9 @@ import net.corda.node.internal.Node import net.corda.node.internal.StartedNode import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.services.config.* -import net.corda.node.services.config.ConfigHelper -import net.corda.node.services.config.configOf -import net.corda.node.services.config.parseAsNodeConfiguration -import net.corda.node.services.config.plus import net.corda.nodeapi.internal.config.User import net.corda.testing.IntegrationTest import net.corda.testing.SerializationEnvironmentRule -import net.corda.testing.driver.addressMustNotBeBoundFuture import net.corda.testing.getFreeLocalPorts import net.corda.testing.node.MockServices.Companion.MOCK_VERSION_INFO import org.apache.logging.log4j.Level diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/internal/RPCDriver.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/internal/RPCDriver.kt index 33867e5ac4..1e14047cc0 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/internal/RPCDriver.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/internal/RPCDriver.kt @@ -16,7 +16,7 @@ import net.corda.core.internal.div import net.corda.core.internal.uncheckedCast import net.corda.core.messaging.RPCOps import net.corda.core.utilities.NetworkHostAndPort -import net.corda.node.services.RPCUserService +import net.corda.node.internal.security.RPCSecurityManagerImpl import net.corda.node.services.messaging.ArtemisMessagingServer import net.corda.node.services.messaging.RPCServer import net.corda.node.services.messaging.RPCServerConfiguration @@ -52,159 +52,25 @@ import java.nio.file.Path import java.nio.file.Paths import java.util.* -interface RPCDriverExposedDSLInterface : DriverDSLExposedInterface { - /** - * Starts an In-VM RPC server. Note that only a single one may be started. - * - * @param rpcUser The single user who can access the server through RPC, and their permissions. - * @param nodeLegalName The legal name of the node to check against to authenticate a super user. - * @param configuration The RPC server configuration. - * @param ops The server-side implementation of the RPC interface. - */ - fun startInVmRpcServer( - rpcUser: User = rpcTestUser, - nodeLegalName: CordaX500Name = fakeNodeLegalName, - maxFileSize: Int = ArtemisMessagingServer.MAX_FILE_SIZE, - maxBufferedBytesPerClient: Long = 10L * ArtemisMessagingServer.MAX_FILE_SIZE, - configuration: RPCServerConfiguration = RPCServerConfiguration.default, - ops: I - ): CordaFuture - - /** - * Starts an In-VM RPC client. - * - * @param rpcOpsClass The [Class] of the RPC interface. - * @param username The username to authenticate with. - * @param password The password to authenticate with. - * @param configuration The RPC client configuration. - */ - fun startInVmRpcClient( - rpcOpsClass: Class, - username: String = rpcTestUser.username, - password: String = rpcTestUser.password, - configuration: RPCClientConfiguration = RPCClientConfiguration.default - ): CordaFuture - - /** - * Starts an In-VM Artemis session connecting to the RPC server. - * - * @param username The username to authenticate with. - * @param password The password to authenticate with. - */ - fun startInVmArtemisSession( - username: String = rpcTestUser.username, - password: String = rpcTestUser.password - ): ClientSession - - /** - * Starts a Netty RPC server. - * - * @param serverName The name of the server, to be used for the folder created for Artemis files. - * @param rpcUser The single user who can access the server through RPC, and their permissions. - * @param nodeLegalName The legal name of the node to check against to authenticate a super user. - * @param configuration The RPC server configuration. - * @param ops The server-side implementation of the RPC interface. - */ - fun startRpcServer( - serverName: String = "driver-rpc-server-${random63BitValue()}", - rpcUser: User = rpcTestUser, - nodeLegalName: CordaX500Name = fakeNodeLegalName, - maxFileSize: Int = ArtemisMessagingServer.MAX_FILE_SIZE, - maxBufferedBytesPerClient: Long = 10L * ArtemisMessagingServer.MAX_FILE_SIZE, - configuration: RPCServerConfiguration = RPCServerConfiguration.default, - customPort: NetworkHostAndPort? = null, - ops: I - ): CordaFuture - - /** - * Starts a Netty RPC client. - * - * @param rpcOpsClass The [Class] of the RPC interface. - * @param rpcAddress The address of the RPC server to connect to. - * @param username The username to authenticate with. - * @param password The password to authenticate with. - * @param configuration The RPC client configuration. - */ - fun startRpcClient( - rpcOpsClass: Class, - rpcAddress: NetworkHostAndPort, - username: String = rpcTestUser.username, - password: String = rpcTestUser.password, - configuration: RPCClientConfiguration = RPCClientConfiguration.default - ): CordaFuture - - /** - * Starts a Netty RPC client in a new JVM process that calls random RPCs with random arguments. - * - * @param rpcOpsClass The [Class] of the RPC interface. - * @param rpcAddress The address of the RPC server to connect to. - * @param username The username to authenticate with. - * @param password The password to authenticate with. - */ - fun startRandomRpcClient( - rpcOpsClass: Class, - rpcAddress: NetworkHostAndPort, - username: String = rpcTestUser.username, - password: String = rpcTestUser.password - ): CordaFuture - - /** - * Starts a Netty Artemis session connecting to an RPC server. - * - * @param rpcAddress The address of the RPC server. - * @param username The username to authenticate with. - * @param password The password to authenticate with. - */ - fun startArtemisSession( - rpcAddress: NetworkHostAndPort, - username: String = rpcTestUser.username, - password: String = rpcTestUser.password - ): ClientSession - - fun startRpcBroker( - serverName: String = "driver-rpc-server-${random63BitValue()}", - rpcUser: User = rpcTestUser, - maxFileSize: Int = ArtemisMessagingServer.MAX_FILE_SIZE, - maxBufferedBytesPerClient: Long = 10L * ArtemisMessagingServer.MAX_FILE_SIZE, - customPort: NetworkHostAndPort? = null - ): CordaFuture - - fun startInVmRpcBroker( - rpcUser: User = rpcTestUser, - maxFileSize: Int = ArtemisMessagingServer.MAX_FILE_SIZE, - maxBufferedBytesPerClient: Long = 10L * ArtemisMessagingServer.MAX_FILE_SIZE - ): CordaFuture - - fun startRpcServerWithBrokerRunning( - rpcUser: User = rpcTestUser, - nodeLegalName: CordaX500Name = fakeNodeLegalName, - configuration: RPCServerConfiguration = RPCServerConfiguration.default, - ops: I, - brokerHandle: RpcBrokerHandle - ): RpcServerHandle -} - -inline fun RPCDriverExposedDSLInterface.startInVmRpcClient( +inline fun RPCDriverDSL.startInVmRpcClient( username: String = rpcTestUser.username, password: String = rpcTestUser.password, configuration: RPCClientConfiguration = RPCClientConfiguration.default ) = startInVmRpcClient(I::class.java, username, password, configuration) -inline fun RPCDriverExposedDSLInterface.startRandomRpcClient( +inline fun RPCDriverDSL.startRandomRpcClient( hostAndPort: NetworkHostAndPort, username: String = rpcTestUser.username, password: String = rpcTestUser.password ) = startRandomRpcClient(I::class.java, hostAndPort, username, password) -inline fun RPCDriverExposedDSLInterface.startRpcClient( +inline fun RPCDriverDSL.startRpcClient( rpcAddress: NetworkHostAndPort, username: String = rpcTestUser.username, password: String = rpcTestUser.password, configuration: RPCClientConfiguration = RPCClientConfiguration.default ) = startRpcClient(I::class.java, rpcAddress, username, password, configuration) -interface RPCDriverInternalDSLInterface : DriverDSLInternalInterface, RPCDriverExposedDSLInterface - data class RpcBrokerHandle( val hostAndPort: NetworkHostAndPort?, /** null if this is an InVM broker */ @@ -223,6 +89,7 @@ val fakeNodeLegalName = CordaX500Name(organisation = "Not:a:real:name", locality // Use a global pool so that we can run RPC tests in parallel private val globalPortAllocation = PortAllocation.Incremental(10000) private val globalDebugPortAllocation = PortAllocation.Incremental(5005) +private val globalMonitorPortAllocation = PortAllocation.Incremental(7005) fun rpcDriver( isDebug: Boolean = false, driverDirectory: Path = Paths.get("build", getTimestampAsDirectoryName()), @@ -235,10 +102,12 @@ fun rpcDriver( extraCordappPackagesToScan: List = emptyList(), notarySpecs: List = emptyList(), externalTrace: Trace? = null, - dsl: RPCDriverExposedDSLInterface.() -> A -) = genericDriver( + jmxPolicy: JmxPolicy = JmxPolicy(), + dsl: RPCDriverDSL.() -> A +) : A { + return genericDriver( driverDsl = RPCDriverDSL( - DriverDSL( + DriverDSLImpl( portAllocation = portAllocation, debugPortAllocation = debugPortAllocation, extraSystemProperties = systemProperties, @@ -248,13 +117,14 @@ fun rpcDriver( startNodesInProcess = startNodesInProcess, waitForNodesToFinish = waitForNodesToFinish, extraCordappPackagesToScan = extraCordappPackagesToScan, - notarySpecs = notarySpecs + notarySpecs = notarySpecs, + jmxPolicy = jmxPolicy ), externalTrace ), coerce = { it }, dsl = dsl, initialiseSerialization = false -) +)} private class SingleUserSecurityManager(val rpcUser: User) : ActiveMQSecurityManager3 { override fun validateUser(user: String?, password: String?) = isValid(user, password) @@ -276,8 +146,8 @@ private class SingleUserSecurityManager(val rpcUser: User) : ActiveMQSecurityMan } data class RPCDriverDSL( - private val driverDSL: DriverDSL, private val externalTrace: Trace? -) : DriverDSLInternalInterface by driverDSL, RPCDriverInternalDSLInterface { + private val driverDSL: DriverDSLImpl, private val externalTrace: Trace? +) : InternalDriverDSL by driverDSL { private companion object { val notificationAddress = "notifications" @@ -340,12 +210,20 @@ data class RPCDriverDSL( } } - override fun startInVmRpcServer( - rpcUser: User, - nodeLegalName: CordaX500Name, - maxFileSize: Int, - maxBufferedBytesPerClient: Long, - configuration: RPCServerConfiguration, + /** + * Starts an In-VM RPC server. Note that only a single one may be started. + * + * @param rpcUser The single user who can access the server through RPC, and their permissions. + * @param nodeLegalName The legal name of the node to check against to authenticate a super user. + * @param configuration The RPC server configuration. + * @param ops The server-side implementation of the RPC interface. + */ + fun startInVmRpcServer( + rpcUser: User = rpcTestUser, + nodeLegalName: CordaX500Name = fakeNodeLegalName, + maxFileSize: Int = ArtemisMessagingServer.MAX_FILE_SIZE, + maxBufferedBytesPerClient: Long = 10L * ArtemisMessagingServer.MAX_FILE_SIZE, + configuration: RPCServerConfiguration = RPCServerConfiguration.default, ops: I ): CordaFuture { return startInVmRpcBroker(rpcUser, maxFileSize, maxBufferedBytesPerClient).map { broker -> @@ -353,7 +231,20 @@ data class RPCDriverDSL( } } - override fun startInVmRpcClient(rpcOpsClass: Class, username: String, password: String, configuration: RPCClientConfiguration): CordaFuture { + /** + * Starts an In-VM RPC client. + * + * @param rpcOpsClass The [Class] of the RPC interface. + * @param username The username to authenticate with. + * @param password The password to authenticate with. + * @param configuration The RPC client configuration. + */ + fun startInVmRpcClient( + rpcOpsClass: Class, + username: String = rpcTestUser.username, + password: String = rpcTestUser.password, + configuration: RPCClientConfiguration = RPCClientConfiguration.default + ): CordaFuture { return driverDSL.executorService.fork { val client = RPCClient(inVmClientTransportConfiguration, configuration) val connection = client.start(rpcOpsClass, username, password, externalTrace) @@ -364,7 +255,16 @@ data class RPCDriverDSL( } } - override fun startInVmArtemisSession(username: String, password: String): ClientSession { + /** + * Starts an In-VM Artemis session connecting to the RPC server. + * + * @param username The username to authenticate with. + * @param password The password to authenticate with. + */ + fun startInVmArtemisSession( + username: String = rpcTestUser.username, + password: String = rpcTestUser.password + ): ClientSession { val locator = ActiveMQClient.createServerLocatorWithoutHA(inVmClientTransportConfiguration) val sessionFactory = locator.createSessionFactory() val session = sessionFactory.createSession(username, password, false, true, true, locator.isPreAcknowledge, DEFAULT_ACK_BATCH_SIZE) @@ -376,14 +276,23 @@ data class RPCDriverDSL( return session } - override fun startRpcServer( - serverName: String, - rpcUser: User, - nodeLegalName: CordaX500Name, - maxFileSize: Int, - maxBufferedBytesPerClient: Long, - configuration: RPCServerConfiguration, - customPort: NetworkHostAndPort?, + /** + * Starts a Netty RPC server. + * + * @param serverName The name of the server, to be used for the folder created for Artemis files. + * @param rpcUser The single user who can access the server through RPC, and their permissions. + * @param nodeLegalName The legal name of the node to check against to authenticate a super user. + * @param configuration The RPC server configuration. + * @param ops The server-side implementation of the RPC interface. + */ + fun startRpcServer( + serverName: String = "driver-rpc-server-${random63BitValue()}", + rpcUser: User = rpcTestUser, + nodeLegalName: CordaX500Name = fakeNodeLegalName, + maxFileSize: Int = ArtemisMessagingServer.MAX_FILE_SIZE, + maxBufferedBytesPerClient: Long = 10L * ArtemisMessagingServer.MAX_FILE_SIZE, + configuration: RPCServerConfiguration = RPCServerConfiguration.default, + customPort: NetworkHostAndPort? = null, ops: I ): CordaFuture { return startRpcBroker(serverName, rpcUser, maxFileSize, maxBufferedBytesPerClient, customPort).map { broker -> @@ -391,12 +300,21 @@ data class RPCDriverDSL( } } - override fun startRpcClient( + /** + * Starts a Netty RPC client. + * + * @param rpcOpsClass The [Class] of the RPC interface. + * @param rpcAddress The address of the RPC server to connect to. + * @param username The username to authenticate with. + * @param password The password to authenticate with. + * @param configuration The RPC client configuration. + */ + fun startRpcClient( rpcOpsClass: Class, rpcAddress: NetworkHostAndPort, - username: String, - password: String, - configuration: RPCClientConfiguration + username: String = rpcTestUser.username, + password: String = rpcTestUser.password, + configuration: RPCClientConfiguration = RPCClientConfiguration.default ): CordaFuture { return driverDSL.executorService.fork { val client = RPCClient(ArtemisTcpTransport.tcpTransport(ConnectionDirection.Outbound(), rpcAddress, null), configuration) @@ -408,13 +326,37 @@ data class RPCDriverDSL( } } - override fun startRandomRpcClient(rpcOpsClass: Class, rpcAddress: NetworkHostAndPort, username: String, password: String): CordaFuture { + /** + * Starts a Netty RPC client in a new JVM process that calls random RPCs with random arguments. + * + * @param rpcOpsClass The [Class] of the RPC interface. + * @param rpcAddress The address of the RPC server to connect to. + * @param username The username to authenticate with. + * @param password The password to authenticate with. + */ + fun startRandomRpcClient( + rpcOpsClass: Class, + rpcAddress: NetworkHostAndPort, + username: String = rpcTestUser.username, + password: String = rpcTestUser.password + ): CordaFuture { val process = ProcessUtilities.startJavaProcess(listOf(rpcOpsClass.name, rpcAddress.toString(), username, password)) driverDSL.shutdownManager.registerProcessShutdown(process) return doneFuture(process) } - override fun startArtemisSession(rpcAddress: NetworkHostAndPort, username: String, password: String): ClientSession { + /** + * Starts a Netty Artemis session connecting to an RPC server. + * + * @param rpcAddress The address of the RPC server. + * @param username The username to authenticate with. + * @param password The password to authenticate with. + */ + fun startArtemisSession( + rpcAddress: NetworkHostAndPort, + username: String = rpcTestUser.username, + password: String = rpcTestUser.password + ): ClientSession { val locator = ActiveMQClient.createServerLocatorWithoutHA(createNettyClientTransportConfiguration(rpcAddress)) val sessionFactory = locator.createSessionFactory() val session = sessionFactory.createSession(username, password, false, true, true, false, DEFAULT_ACK_BATCH_SIZE) @@ -427,12 +369,12 @@ data class RPCDriverDSL( return session } - override fun startRpcBroker( - serverName: String, - rpcUser: User, - maxFileSize: Int, - maxBufferedBytesPerClient: Long, - customPort: NetworkHostAndPort? + fun startRpcBroker( + serverName: String = "driver-rpc-server-${random63BitValue()}", + rpcUser: User = rpcTestUser, + maxFileSize: Int = ArtemisMessagingServer.MAX_FILE_SIZE, + maxBufferedBytesPerClient: Long = 10L * ArtemisMessagingServer.MAX_FILE_SIZE, + customPort: NetworkHostAndPort? = null ): CordaFuture { val hostAndPort = customPort ?: driverDSL.portAllocation.nextHostAndPort() addressMustNotBeBound(driverDSL.executorService, hostAndPort) @@ -452,7 +394,11 @@ data class RPCDriverDSL( } } - override fun startInVmRpcBroker(rpcUser: User, maxFileSize: Int, maxBufferedBytesPerClient: Long): CordaFuture { + fun startInVmRpcBroker( + rpcUser: User = rpcTestUser, + maxFileSize: Int = ArtemisMessagingServer.MAX_FILE_SIZE, + maxBufferedBytesPerClient: Long = 10L * ArtemisMessagingServer.MAX_FILE_SIZE + ): CordaFuture { return driverDSL.executorService.fork { val artemisConfig = createInVmRpcServerArtemisConfig(maxFileSize, maxBufferedBytesPerClient) val server = EmbeddedActiveMQ() @@ -471,10 +417,10 @@ data class RPCDriverDSL( } } - override fun startRpcServerWithBrokerRunning( - rpcUser: User, - nodeLegalName: CordaX500Name, - configuration: RPCServerConfiguration, + fun startRpcServerWithBrokerRunning( + rpcUser: User = rpcTestUser, + nodeLegalName: CordaX500Name = fakeNodeLegalName, + configuration: RPCServerConfiguration = RPCServerConfiguration.default, ops: I, brokerHandle: RpcBrokerHandle ): RpcServerHandle { @@ -482,17 +428,13 @@ data class RPCDriverDSL( minLargeMessageSize = ArtemisMessagingServer.MAX_FILE_SIZE isUseGlobalPools = false } - val userService = object : RPCUserService { - override fun getUser(username: String): User? = if (username == rpcUser.username) rpcUser else null - override val users: List get() = listOf(rpcUser) - override val id: AuthServiceId = AuthServiceId("RPC_DRIVER") - } + val rpcSecurityManager = RPCSecurityManagerImpl.fromUserList(users = listOf(rpcUser), id = AuthServiceId("TEST_SECURITY_MANAGER")) val rpcServer = RPCServer( ops, rpcUser.username, rpcUser.password, locator, - userService, + rpcSecurityManager, nodeLegalName, configuration ) diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/ShutdownManager.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/internal/ShutdownManager.kt similarity index 99% rename from testing/node-driver/src/main/kotlin/net/corda/testing/driver/ShutdownManager.kt rename to testing/node-driver/src/main/kotlin/net/corda/testing/internal/ShutdownManager.kt index 0e8752aeb6..022569b958 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/ShutdownManager.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/internal/ShutdownManager.kt @@ -1,4 +1,4 @@ -package net.corda.testing.driver +package net.corda.testing.internal import net.corda.core.concurrent.CordaFuture import net.corda.core.internal.ThreadBox diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/internal/demorun/DemoRunner.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/internal/demorun/DemoRunner.kt index 5155a31850..60dfe3262d 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/internal/demorun/DemoRunner.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/internal/demorun/DemoRunner.kt @@ -8,7 +8,8 @@ import net.corda.core.internal.concurrent.flatMap import net.corda.core.internal.concurrent.transpose import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.getOrThrow -import net.corda.testing.driver.DriverDSL +import net.corda.testing.driver.JmxPolicy +import net.corda.testing.internal.DriverDSLImpl import net.corda.testing.driver.PortAllocation import net.corda.testing.driver.driver @@ -41,6 +42,7 @@ private fun CordformDefinition.runNodes(waitForAllNodesToFinish: Boolean, block: .max()!! driver( isDebug = true, + jmxPolicy = JmxPolicy(true), driverDirectory = nodesDirectory, extraCordappPackagesToScan = cordappPackages, // Notaries are manually specified in Cordform so we don't want the driver automatically starting any @@ -49,8 +51,8 @@ private fun CordformDefinition.runNodes(waitForAllNodesToFinish: Boolean, block: portAllocation = PortAllocation.Incremental(maxPort + 1), waitForAllNodesToFinish = waitForAllNodesToFinish ) { + this as DriverDSLImpl // access internal API setup(this) - this as DriverDSL // startCordformNode is an internal API nodes.map { val startedNode = startCordformNode(it) if (it.webAddress != null) { diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/internal/performance/Injectors.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/internal/performance/Injectors.kt index 83ef6fc585..b4b0f9f33d 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/internal/performance/Injectors.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/internal/performance/Injectors.kt @@ -3,7 +3,7 @@ package net.corda.testing.internal.performance import com.codahale.metrics.Gauge import com.codahale.metrics.MetricRegistry import com.google.common.base.Stopwatch -import net.corda.testing.driver.ShutdownManager +import net.corda.testing.internal.ShutdownManager import java.time.Duration import java.util.* import java.util.concurrent.CountDownLatch diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/internal/performance/Reporter.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/internal/performance/Reporter.kt index 9cac82d1af..5446165087 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/internal/performance/Reporter.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/internal/performance/Reporter.kt @@ -3,7 +3,7 @@ package net.corda.testing.internal.performance import com.codahale.metrics.ConsoleReporter import com.codahale.metrics.JmxReporter import com.codahale.metrics.MetricRegistry -import net.corda.testing.driver.ShutdownManager +import net.corda.testing.internal.ShutdownManager import java.util.concurrent.TimeUnit import javax.management.ObjectName import kotlin.concurrent.thread diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt index 5c1221f816..a7f247d98b 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNode.kt @@ -29,6 +29,7 @@ import net.corda.node.services.api.SchemaService import net.corda.node.services.config.BFTSMaRtConfiguration import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.NotaryConfig +import net.corda.node.services.api.IdentityServiceInternal import net.corda.node.services.keys.E2ETestKeyManagementService import net.corda.node.services.messaging.MessagingService import net.corda.node.services.transactions.BFTNonValidatingNotaryService @@ -275,7 +276,7 @@ class MockNetwork(defaultParameters: MockNetworkParameters = MockNetworkParamete network = messagingServiceSpy } - override fun makeKeyManagementService(identityService: IdentityService, keyPairs: Set): KeyManagementService { + override fun makeKeyManagementService(identityService: IdentityServiceInternal, keyPairs: Set): KeyManagementService { return E2ETestKeyManagementService(identityService, keyPairs) } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt index f645788216..1696cd9a5e 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt @@ -34,6 +34,8 @@ import net.corda.node.services.schema.HibernateObserver import net.corda.node.services.schema.NodeSchemaService import net.corda.node.services.transactions.InMemoryTransactionVerifierService import net.corda.node.services.vault.NodeVaultService +import net.corda.node.internal.configureDatabase +import net.corda.node.services.api.IdentityServiceInternal import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.nodeapi.internal.persistence.HibernateConfiguration @@ -60,10 +62,11 @@ fun makeTestIdentityService(identities: Iterable = emptySet * A singleton utility that only provides a mock identity, key and storage service. However, this is sufficient for * building chains of transactions and verifying them. It isn't sufficient for testing flows however. */ -open class MockServices( +open class MockServices private constructor( cordappLoader: CordappLoader, override val validatedTransactions: WritableTransactionStorage, - private val initialIdentityName: CordaX500Name = MEGA_CORP.name, + override val identityService: IdentityServiceInternal, + private val initialIdentityName: CordaX500Name, vararg val keys: KeyPair ) : ServiceHub, StateLoader by validatedTransactions { companion object { @@ -140,13 +143,13 @@ open class MockServices( /** * Makes database and mock services appropriate for unit tests. * @param keys a list of [KeyPair] instances to be used by [MockServices]. - * @param identityService an instance of [IdentityService], see [makeTestIdentityService]. + * @param identityService an instance of [IdentityServiceInternal], see [makeTestIdentityService]. * @param initialIdentityName the name of the first (typically sole) identity the services will represent. * @return a pair where the first element is the instance of [CordaPersistence] and the second is [MockServices]. */ @JvmStatic fun makeTestDatabaseAndMockServices(keys: List, - identityService: IdentityService, + identityService: IdentityServiceInternal, cordappPackages: List = emptyList(), initialIdentityName: CordaX500Name): Pair { val cordappLoader = CordappLoader.createWithTestPackages(cordappPackages) @@ -154,8 +157,7 @@ open class MockServices( val schemaService = NodeSchemaService(cordappLoader.cordappSchemas) val database = configureDatabase(dataSourceProps, makeTestDatabaseProperties(initialIdentityName.organisation), identityService, schemaService) val mockService = database.transaction { - object : MockServices(cordappLoader, initialIdentityName = initialIdentityName, keys = *(keys.toTypedArray())) { - override val identityService get() = identityService + object : MockServices(cordappLoader, identityService, initialIdentityName, *(keys.toTypedArray())) { override val vaultService: VaultServiceInternal = makeVaultService(database.hibernateConfig, schemaService) override fun recordTransactions(statesToRecord: StatesToRecord, txs: Iterable) { @@ -171,10 +173,10 @@ open class MockServices( } } - constructor(cordappLoader: CordappLoader, initialIdentityName: CordaX500Name = MEGA_CORP.name, vararg keys: KeyPair) : this(cordappLoader, MockTransactionStorage(), initialIdentityName = initialIdentityName, keys = *keys) - constructor(cordappPackages: List, initialIdentityName: CordaX500Name = MEGA_CORP.name, vararg keys: KeyPair) : this(CordappLoader.createWithTestPackages(cordappPackages), initialIdentityName = initialIdentityName, keys = *keys) - constructor(vararg keys: KeyPair) : this(emptyList(), MEGA_CORP.name, *keys) - constructor() : this(generateKeyPair()) + private constructor(cordappLoader: CordappLoader, identityService: IdentityServiceInternal, initialIdentityName: CordaX500Name, vararg keys: KeyPair) : this(cordappLoader, MockTransactionStorage(), identityService, initialIdentityName, *keys) + constructor(cordappPackages: List, identityService: IdentityServiceInternal, initialIdentityName: CordaX500Name, vararg keys: KeyPair) : this(CordappLoader.createWithTestPackages(cordappPackages), identityService, initialIdentityName, *keys) + constructor(identityService: IdentityServiceInternal, initialIdentityName: CordaX500Name, vararg keys: KeyPair) : this(emptyList(), identityService, initialIdentityName, *keys) + constructor(identityService: IdentityServiceInternal, initialIdentityName: CordaX500Name) : this(identityService, initialIdentityName, generateKeyPair()) val key: KeyPair get() = keys.first() @@ -185,7 +187,6 @@ open class MockServices( } final override val attachments = MockAttachmentStorage() - override val identityService: IdentityService = makeTestIdentityService(listOf(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, DUMMY_CASH_ISSUER_IDENTITY, DUMMY_NOTARY_IDENTITY)) override val keyManagementService: KeyManagementService by lazy { MockKeyManagementService(identityService, *keys) } override val vaultService: VaultService get() = throw UnsupportedOperationException() @@ -217,7 +218,7 @@ open class MockServices( override fun jdbcSession(): Connection = throw UnsupportedOperationException() } -class MockKeyManagementService(val identityService: IdentityService, +class MockKeyManagementService(val identityService: IdentityServiceInternal, vararg initialKeys: KeyPair) : SingletonSerializeAsToken(), KeyManagementService { private val keyStore: MutableMap = initialKeys.associateByTo(HashMap(), { it.public }, { it.private }) diff --git a/testing/node-driver/src/test/java/net/corda/node/internal/security/PasswordTest.kt b/testing/node-driver/src/test/java/net/corda/node/internal/security/PasswordTest.kt new file mode 100644 index 0000000000..ccdf750343 --- /dev/null +++ b/testing/node-driver/src/test/java/net/corda/node/internal/security/PasswordTest.kt @@ -0,0 +1,96 @@ +package net.corda.node.internal.security + +import org.hamcrest.CoreMatchers.containsString +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.core.IsEqual.equalTo +import org.hamcrest.core.IsNot.not +import org.junit.Test + +internal class PasswordTest { + + @Test + fun immutability() { + + val charArray = "dadada".toCharArray() + val password = Password(charArray) + assertThat(password.value, equalTo(charArray)) + + charArray[0] = 'm' + assertThat(password.value, not(equalTo(charArray))) + + val value = password.value + value[1] = 'e' + assertThat(password.value, not(equalTo(value))) + } + + @Test + fun constructor_and_getters() { + + val value = "dadada" + + assertThat(Password(value.toCharArray()).value, equalTo(value.toCharArray())) + assertThat(Password(value.toCharArray()).valueAsString, equalTo(value)) + + assertThat(Password(value).value, equalTo(value.toCharArray())) + assertThat(Password(value).valueAsString, equalTo(value)) + } + + @Test + fun equals() { + + val passwordValue1 = Password("value1") + val passwordValue2 = Password("value2") + val passwordValue12 = Password("value1") + + assertThat(passwordValue1, equalTo(passwordValue1)) + + assertThat(passwordValue1, not(equalTo(passwordValue2))) + assertThat(passwordValue2, not(equalTo(passwordValue1))) + + assertThat(passwordValue1, equalTo(passwordValue12)) + assertThat(passwordValue12, equalTo(passwordValue1)) + } + + @Test + fun hashcode() { + + val passwordValue1 = Password("value1") + val passwordValue2 = Password("value2") + val passwordValue12 = Password("value1") + + assertThat(passwordValue1.hashCode(), equalTo(passwordValue1.hashCode())) + + // not strictly required by hashCode() contract, but desirable + assertThat(passwordValue1.hashCode(), not(equalTo(passwordValue2.hashCode()))) + assertThat(passwordValue2.hashCode(), not(equalTo(passwordValue1.hashCode()))) + + assertThat(passwordValue1.hashCode(), equalTo(passwordValue12.hashCode())) + assertThat(passwordValue12.hashCode(), equalTo(passwordValue1.hashCode())) + } + + @Test + fun close() { + + val value = "ipjd1@pijmps112112" + val password = Password(value) + + password.use { + val readValue = it.valueAsString + assertThat(readValue, equalTo(value)) + } + + val readValue = password.valueAsString + assertThat(readValue, not(equalTo(value))) + } + + @Test + fun toString_is_masked() { + + val value = "ipjd1@pijmps112112" + val password = Password(value) + + val toString = password.toString() + + assertThat(toString, not(containsString(value))) + } +} \ No newline at end of file diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt index df5d85661c..da67d7a289 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt @@ -4,12 +4,14 @@ package net.corda.testing import net.corda.core.contracts.StateRef +import net.corda.core.crypto.Crypto import net.corda.core.crypto.SecureHash import net.corda.core.crypto.generateKeyPair import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate import net.corda.core.internal.cert +import net.corda.core.internal.x500Name import net.corda.core.node.NodeInfo import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.OpaqueBytes @@ -22,6 +24,10 @@ import net.corda.nodeapi.internal.crypto.CertificateType import net.corda.nodeapi.internal.crypto.X509CertificateFactory import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.nodeapi.internal.serialization.amqp.AMQP_ENABLED +import org.bouncycastle.asn1.x509.GeneralName +import org.bouncycastle.asn1.x509.GeneralSubtree +import org.bouncycastle.asn1.x509.NameConstraints +import org.bouncycastle.cert.X509CertificateHolder import org.mockito.Mockito.mock import org.mockito.internal.stubbing.answers.ThrowsException import java.lang.reflect.Modifier @@ -128,18 +134,27 @@ fun configureTestSSL(legalName: CordaX500Name = MEGA_CORP.name): SSLConfiguratio configureDevKeyAndTrustStores(legalName) } } +fun getTestPartyAndCertificate(party: Party): PartyAndCertificate { + val trustRoot: X509CertificateHolder = DEV_TRUST_ROOT + val intermediate: CertificateAndKeyPair = DEV_CA -fun getTestPartyAndCertificate(party: Party, trustRoot: CertificateAndKeyPair = DEV_CA): PartyAndCertificate { - val certHolder = X509Utilities.createCertificate(CertificateType.IDENTITY, trustRoot.certificate, trustRoot.keyPair, party.name, party.owningKey) - val certPath = X509CertificateFactory().delegate.generateCertPath(listOf(certHolder.cert, trustRoot.certificate.cert)) + val nodeCaName = party.name.copy(commonName = X509Utilities.CORDA_CLIENT_CA_CN) + val nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, party.name.x500Name))), arrayOf()) + val issuerKeyPair = Crypto.generateKeyPair(Crypto.ECDSA_SECP256K1_SHA256) + val issuerCertificate = X509Utilities.createCertificate(CertificateType.NODE_CA, intermediate.certificate, intermediate.keyPair, nodeCaName, issuerKeyPair.public, + nameConstraints = nameConstraints) + + val certHolder = X509Utilities.createCertificate(CertificateType.WELL_KNOWN_IDENTITY, issuerCertificate, issuerKeyPair, party.name, party.owningKey) + val pathElements = listOf(certHolder, issuerCertificate, intermediate.certificate, trustRoot) + val certPath = X509CertificateFactory().delegate.generateCertPath(pathElements.map(X509CertificateHolder::cert)) return PartyAndCertificate(certPath) } /** * Build a test party with a nonsense certificate authority for testing purposes. */ -fun getTestPartyAndCertificate(name: CordaX500Name, publicKey: PublicKey, trustRoot: CertificateAndKeyPair = DEV_CA): PartyAndCertificate { - return getTestPartyAndCertificate(Party(name, publicKey), trustRoot) +fun getTestPartyAndCertificate(name: CordaX500Name, publicKey: PublicKey): PartyAndCertificate { + return getTestPartyAndCertificate(Party(name, publicKey)) } @Suppress("unused") diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/TestConstants.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/TestConstants.kt index 96f452e74c..aa8a88aea6 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/TestConstants.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/TestConstants.kt @@ -4,28 +4,27 @@ package net.corda.testing import net.corda.core.contracts.Command import net.corda.core.contracts.TypeOnlyCommandData +import net.corda.core.crypto.Crypto import net.corda.core.crypto.entropyToKeyPair import net.corda.core.crypto.generateKeyPair import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate import net.corda.core.internal.toX509CertHolder -import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair -import net.corda.nodeapi.internal.crypto.X509Utilities -import net.corda.nodeapi.internal.crypto.getCertificateAndKeyPair -import net.corda.nodeapi.internal.crypto.loadKeyStore +import net.corda.core.internal.x500Name +import net.corda.nodeapi.internal.crypto.* +import org.bouncycastle.asn1.x509.GeneralName +import org.bouncycastle.asn1.x509.GeneralSubtree +import org.bouncycastle.asn1.x509.NameConstraints import org.bouncycastle.cert.X509CertificateHolder import java.math.BigInteger import java.security.KeyPair import java.security.PublicKey +import java.security.Security import java.time.Instant // A dummy time at which we will be pretending test transactions are created. val TEST_TX_TIME: Instant get() = Instant.parse("2015-04-17T12:00:00.00Z") - -val DUMMY_KEY_1: KeyPair by lazy { generateKeyPair() } -val DUMMY_KEY_2: KeyPair by lazy { generateKeyPair() } - val DUMMY_NOTARY_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(20)) } /** Dummy notary identity for tests and simulations */ val DUMMY_NOTARY_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(DUMMY_NOTARY) @@ -79,6 +78,3 @@ val DEV_TRUST_ROOT: X509CertificateHolder by lazy { fun dummyCommand(vararg signers: PublicKey = arrayOf(generateKeyPair().public)) = Command(DummyCommandData, signers.toList()) object DummyCommandData : TypeOnlyCommandData() - -val DUMMY_IDENTITY_1: PartyAndCertificate get() = getTestPartyAndCertificate(DUMMY_PARTY) -val DUMMY_PARTY: Party get() = Party(CordaX500Name(organisation = "Dummy", locality = "Madrid", country = "ES"), DUMMY_KEY_1.public) diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/X500NameUtils.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/X500NameUtils.kt deleted file mode 100644 index 1af6357b7e..0000000000 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/X500NameUtils.kt +++ /dev/null @@ -1,29 +0,0 @@ -@file:JvmName("X500NameUtils") - -package net.corda.testing - -import org.bouncycastle.asn1.x500.X500Name -import org.bouncycastle.asn1.x500.X500NameBuilder -import org.bouncycastle.asn1.x500.style.BCStyle - -/** - * Generate a distinguished name from the provided X500 . - * - * @param O organisation name. - * @param L locality. - * @param C county. - * @param CN common name. - * @param OU organisation unit. - * @param ST state. - */ -@JvmOverloads -fun getX500Name(O: String, L: String, C: String, CN: String? = null, OU: String? = null, ST: String? = null): X500Name { - return X500NameBuilder(BCStyle.INSTANCE).apply { - addRDN(BCStyle.C, C) - ST?.let { addRDN(BCStyle.ST, it) } - addRDN(BCStyle.L, L) - addRDN(BCStyle.O, O) - OU?.let { addRDN(BCStyle.OU, it) } - CN?.let { addRDN(BCStyle.CN, it) } - }.build() -} \ No newline at end of file diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt index 17a8b7dd69..bf3ef7944d 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt @@ -26,6 +26,7 @@ import net.corda.node.services.Permissions.Companion.startFlow import net.corda.nodeapi.internal.config.User import net.corda.testing.ALICE import net.corda.testing.BOB +import net.corda.testing.driver.JmxPolicy import net.corda.testing.driver.NodeHandle import net.corda.testing.driver.PortAllocation import net.corda.testing.driver.driver @@ -65,14 +66,14 @@ class ExplorerSimulation(private val options: OptionSet) { fun startDemoNodes() { val portAllocation = PortAllocation.Incremental(20000) driver(portAllocation = portAllocation, extraCordappPackagesToScan = listOf("net.corda.finance", IOUFlow::class.java.`package`.name), - isDebug = true, waitForAllNodesToFinish = true) { + isDebug = true, waitForAllNodesToFinish = true, jmxPolicy = JmxPolicy(true)) { // TODO : Supported flow should be exposed somehow from the node instead of set of ServiceInfo. - val alice = startNode(providedName = ALICE.name, rpcUsers = listOf(user)) + val alice = startNode(providedName = ALICE.name, rpcUsers = listOf(user), customOverrides = mapOf("devMode" to "true")) val bob = startNode(providedName = BOB.name, rpcUsers = listOf(user)) val ukBankName = CordaX500Name(organisation = "UK Bank Plc", locality = "London", country = "GB") val usaBankName = CordaX500Name(organisation = "USA Bank Corp", locality = "New York", country = "US") val issuerGBP = startNode(providedName = ukBankName, rpcUsers = listOf(manager), - customOverrides = mapOf("issuableCurrencies" to listOf("GBP"))) + customOverrides = mapOf("issuableCurrencies" to listOf("GBP"), "" to "true")) val issuerUSD = startNode(providedName = usaBankName, rpcUsers = listOf(manager), customOverrides = mapOf("issuableCurrencies" to listOf("USD"))) diff --git a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/NotaryTest.kt b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/NotaryTest.kt index b96f6f4f40..5c8900ebb3 100644 --- a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/NotaryTest.kt +++ b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/NotaryTest.kt @@ -10,8 +10,10 @@ import net.corda.finance.contracts.asset.DUMMY_CASH_ISSUER import net.corda.finance.contracts.asset.DUMMY_CASH_ISSUER_KEY import net.corda.loadtest.LoadTest import net.corda.loadtest.NodeConnection +import net.corda.testing.* import net.corda.testing.contracts.DummyContract import net.corda.testing.node.MockServices +import net.corda.testing.node.makeTestIdentityService import org.slf4j.LoggerFactory private val log = LoggerFactory.getLogger("NotaryTest") @@ -21,7 +23,7 @@ data class NotariseCommand(val issueTx: SignedTransaction, val moveTx: SignedTra val dummyNotarisationTest = LoadTest( "Notarising dummy transactions", generate = { _, _ -> - val issuerServices = MockServices(DUMMY_CASH_ISSUER_KEY) + val issuerServices = MockServices(makeTestIdentityService(listOf(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, DUMMY_CASH_ISSUER_IDENTITY, DUMMY_NOTARY_IDENTITY)), MEGA_CORP.name, DUMMY_CASH_ISSUER_KEY) val generateTx = Generator.pickOne(simpleNodes).flatMap { node -> Generator.int().map { val issueBuilder = DummyContract.generateInitial(it, notary.info.legalIdentities[1], DUMMY_CASH_ISSUER) // TODO notary choice diff --git a/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt b/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt index 7d1b771830..37270bb054 100644 --- a/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt +++ b/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt @@ -16,14 +16,17 @@ import net.corda.core.transactions.LedgerTransaction import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.contextLogger import net.corda.node.services.config.configureDevKeyAndTrustStores -import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_USER import net.corda.nodeapi.ArtemisTcpTransport import net.corda.nodeapi.ConnectionDirection import net.corda.nodeapi.VerifierApi import net.corda.nodeapi.internal.config.NodeSSLConfiguration import net.corda.nodeapi.internal.config.SSLConfiguration -import net.corda.testing.driver.* -import net.corda.testing.internal.ProcessUtilities +import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_USER +import net.corda.testing.driver.JmxPolicy +import net.corda.testing.driver.NodeHandle +import net.corda.testing.driver.PortAllocation +import net.corda.testing.driver.driver +import net.corda.testing.internal.* import net.corda.testing.node.NotarySpec import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.api.core.client.ActiveMQClient @@ -43,34 +46,6 @@ import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ScheduledExecutorService import java.util.concurrent.atomic.AtomicInteger -/** - * This file defines an extension to [DriverDSL] that allows starting of verifier processes and - * lightweight verification requestors. - */ -interface VerifierExposedDSLInterface : DriverDSLExposedInterface { - /** Starts a lightweight verification requestor that implements the Node's Verifier API */ - fun startVerificationRequestor(name: CordaX500Name): CordaFuture - - /** Starts an out of process verifier connected to [address] */ - fun startVerifier(address: NetworkHostAndPort): CordaFuture - - /** - * Waits until [number] verifiers are listening for verification requests coming from the Node. Check - * [VerificationRequestorHandle.waitUntilNumberOfVerifiers] for an equivalent for requestors. - */ - fun NodeHandle.waitUntilNumberOfVerifiers(number: Int) -} - -/** Starts a verifier connecting to the specified node */ -fun VerifierExposedDSLInterface.startVerifier(nodeHandle: NodeHandle) = - startVerifier(nodeHandle.configuration.p2pAddress) - -/** Starts a verifier connecting to the specified requestor */ -fun VerifierExposedDSLInterface.startVerifier(verificationRequestorHandle: VerificationRequestorHandle) = - startVerifier(verificationRequestorHandle.p2pAddress) - -interface VerifierInternalDSLInterface : DriverDSLInternalInterface, VerifierExposedDSLInterface - /** * Behaves the same as [driver] and adds verifier-related functionality. */ @@ -85,10 +60,11 @@ fun verifierDriver( waitForNodesToFinish: Boolean = false, extraCordappPackagesToScan: List = emptyList(), notarySpecs: List = emptyList(), - dsl: VerifierExposedDSLInterface.() -> A + jmxPolicy: JmxPolicy = JmxPolicy(), + dsl: VerifierDriverDSL.() -> A ) = genericDriver( driverDsl = VerifierDriverDSL( - DriverDSL( + DriverDSLImpl( portAllocation = portAllocation, debugPortAllocation = debugPortAllocation, extraSystemProperties = extraSystemProperties, @@ -98,7 +74,8 @@ fun verifierDriver( startNodesInProcess = startNodesInProcess, waitForNodesToFinish = waitForNodesToFinish, extraCordappPackagesToScan = extraCordappPackagesToScan, - notarySpecs = notarySpecs + notarySpecs = notarySpecs, + jmxPolicy = jmxPolicy ) ), coerce = { it }, @@ -143,10 +120,8 @@ data class VerificationRequestorHandle( } -data class VerifierDriverDSL( - val driverDSL: DriverDSL -) : DriverDSLInternalInterface by driverDSL, VerifierInternalDSLInterface { - val verifierCount = AtomicInteger(0) +data class VerifierDriverDSL(private val driverDSL: DriverDSLImpl) : InternalDriverDSL by driverDSL { + private val verifierCount = AtomicInteger(0) companion object { private val log = contextLogger() @@ -183,7 +158,8 @@ data class VerifierDriverDSL( } } - override fun startVerificationRequestor(name: CordaX500Name): CordaFuture { + /** Starts a lightweight verification requestor that implements the Node's Verifier API */ + fun startVerificationRequestor(name: CordaX500Name): CordaFuture { val hostAndPort = driverDSL.portAllocation.nextHostAndPort() return driverDSL.executorService.fork { startVerificationRequestorInternal(name, hostAndPort) @@ -255,7 +231,8 @@ data class VerifierDriverDSL( ) } - override fun startVerifier(address: NetworkHostAndPort): CordaFuture { + /** Starts an out of process verifier connected to [address] */ + fun startVerifier(address: NetworkHostAndPort): CordaFuture { log.info("Starting verifier connecting to address $address") val id = verifierCount.andIncrement val jdwpPort = if (driverDSL.isDebug) driverDSL.debugPortAllocation.nextPort() else null @@ -270,6 +247,16 @@ data class VerifierDriverDSL( return doneFuture(VerifierHandle(process)) } + /** Starts a verifier connecting to the specified node */ + fun startVerifier(nodeHandle: NodeHandle): CordaFuture { + return startVerifier(nodeHandle.configuration.p2pAddress) + } + + /** Starts a verifier connecting to the specified requestor */ + fun startVerifier(verificationRequestorHandle: VerificationRequestorHandle): CordaFuture { + return startVerifier(verificationRequestorHandle.p2pAddress) + } + private fun NodeHandle.connectToNode(closure: (ClientSession) -> A): A { val transport = ArtemisTcpTransport.tcpTransport(ConnectionDirection.Outbound(), configuration.p2pAddress, configuration) val locator = ActiveMQClient.createServerLocatorWithoutHA(transport) @@ -280,7 +267,11 @@ data class VerifierDriverDSL( } } - override fun NodeHandle.waitUntilNumberOfVerifiers(number: Int) { + /** + * Waits until [number] verifiers are listening for verification requests coming from the Node. Check + * [VerificationRequestorHandle.waitUntilNumberOfVerifiers] for an equivalent for requestors. + */ + fun NodeHandle.waitUntilNumberOfVerifiers(number: Int) { connectToNode { session -> poll(driverDSL.executorService, "$number verifiers to come online") { if (session.queueQuery(SimpleString(VerifierApi.VERIFICATION_REQUESTS_QUEUE_NAME)).consumerCount >= number) { diff --git a/webserver/build.gradle b/webserver/build.gradle index 166a8fe3aa..e0f66a360c 100644 --- a/webserver/build.gradle +++ b/webserver/build.gradle @@ -38,7 +38,7 @@ dependencies { compile "org.eclipse.jetty:jetty-servlet:$jetty_version" compile "org.eclipse.jetty:jetty-webapp:$jetty_version" compile "javax.servlet:javax.servlet-api:3.1.0" - compile "org.jolokia:jolokia-agent-war:$jolokia_version" + compile "org.jolokia:jolokia-war:$jolokia_version" compile "commons-fileupload:commons-fileupload:$fileupload_version" // Log4J: logging framework (with SLF4J bindings) diff --git a/webserver/src/integration-test/kotlin/net/corda/webserver/WebserverDriverTests.kt b/webserver/src/integration-test/kotlin/net/corda/webserver/WebserverDriverTests.kt index c55595d634..1af27a5759 100644 --- a/webserver/src/integration-test/kotlin/net/corda/webserver/WebserverDriverTests.kt +++ b/webserver/src/integration-test/kotlin/net/corda/webserver/WebserverDriverTests.kt @@ -4,8 +4,8 @@ import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.getOrThrow import net.corda.testing.* import net.corda.testing.driver.WebserverHandle -import net.corda.testing.driver.addressMustBeBound -import net.corda.testing.driver.addressMustNotBeBound +import net.corda.testing.internal.addressMustBeBound +import net.corda.testing.internal.addressMustNotBeBound import net.corda.testing.driver.driver import org.junit.ClassRule import org.junit.Test diff --git a/webserver/src/main/kotlin/net/corda/webserver/internal/NodeWebServer.kt b/webserver/src/main/kotlin/net/corda/webserver/internal/NodeWebServer.kt index 46bdf5a7ef..e5a2ce254f 100644 --- a/webserver/src/main/kotlin/net/corda/webserver/internal/NodeWebServer.kt +++ b/webserver/src/main/kotlin/net/corda/webserver/internal/NodeWebServer.kt @@ -58,7 +58,7 @@ class NodeWebServer(val config: WebServerConfig) { // Export JMX monitoring statistics and data over REST/JSON. if (config.exportJMXto.split(',').contains("http")) { val classpath = System.getProperty("java.class.path").split(System.getProperty("path.separator")) - val warpath = classpath.firstOrNull { it.contains("jolokia-agent-war-2") && it.endsWith(".war") } + val warpath = classpath.firstOrNull { it.contains("jolokia-war") && it.endsWith(".war") } if (warpath != null) { handlerCollection.addHandler(WebAppContext().apply { // Find the jolokia WAR file on the classpath.