Merge pull request #170 from corda/colljos-os-hc01-merge-111217

Merge OS->Enterprise for HC01
This commit is contained in:
josecoll 2017-12-11 14:17:36 +00:00 committed by GitHub
commit 3dd524c6fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
154 changed files with 3510 additions and 1895 deletions

View File

@ -24,7 +24,7 @@ buildscript {
ext.jackson_version = '2.9.2' ext.jackson_version = '2.9.2'
ext.jetty_version = '9.4.7.v20170914' ext.jetty_version = '9.4.7.v20170914'
ext.jersey_version = '2.25' 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.assertj_version = '3.8.0'
ext.slf4j_version = '1.7.25' ext.slf4j_version = '1.7.25'
ext.log4j_version = '2.9.1' ext.log4j_version = '2.9.1'
@ -50,6 +50,8 @@ buildscript {
ext.crash_version = 'cce5a00f114343c1145c1d7756e1dd6df3ea984e' ext.crash_version = 'cce5a00f114343c1145c1d7756e1dd6df3ea984e'
ext.jsr305_version = constants.getProperty("jsr305Version") ext.jsr305_version = constants.getProperty("jsr305Version")
ext.spring_jdbc_version ='5.0.0.RELEASE' 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: // Update 121 is required for ObjectInputFilter and at time of writing 131 was latest:
ext.java8_minUpdateVersion = '131' ext.java8_minUpdateVersion = '131'
@ -73,6 +75,7 @@ buildscript {
classpath "org.ajoberstar:grgit:1.1.0" classpath "org.ajoberstar:grgit:1.1.0"
classpath "net.i2p.crypto:eddsa:$eddsa_version" // Needed for ServiceIdentityGenerator in the build environment. 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.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. // 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 // Version 1.0.2 of this plugin uses capsule:1.0.1
id "us.kirchmeier.capsule" version "1.0.2" id "us.kirchmeier.capsule" version "1.0.2"
id "com.jfrog.artifactory" version "4.4.18"
} }
ext { ext {
@ -93,6 +95,7 @@ apply plugin: 'com.github.ben-manes.versions'
apply plugin: 'net.corda.plugins.publish-utils' apply plugin: 'net.corda.plugins.publish-utils'
apply plugin: 'net.corda.plugins.cordformation' apply plugin: 'net.corda.plugins.cordformation'
apply plugin: 'maven-publish' 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 // 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 // IntelliJ gets confused when importing the project and ends up erasing and recreating the .idea directory, along

View File

@ -13,7 +13,7 @@ import net.corda.core.utilities.*
import net.corda.node.services.messaging.RPCServerConfiguration import net.corda.node.services.messaging.RPCServerConfiguration
import net.corda.nodeapi.RPCApi import net.corda.nodeapi.RPCApi
import net.corda.testing.* import net.corda.testing.*
import net.corda.testing.driver.poll import net.corda.testing.internal.poll
import net.corda.testing.internal.* import net.corda.testing.internal.*
import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.api.core.SimpleString
import org.junit.After import org.junit.After
@ -70,8 +70,8 @@ class RPCStabilityTests : IntegrationTest() {
val executor = Executors.newScheduledThreadPool(1) val executor = Executors.newScheduledThreadPool(1)
fun startAndStop() { fun startAndStop() {
rpcDriver { rpcDriver {
val server = startRpcServer<RPCOps>(ops = DummyOps) val server = startRpcServer<RPCOps>(ops = DummyOps).get()
startRpcClient<RPCOps>(server.get().broker.hostAndPort!!).get() startRpcClient<RPCOps>(server.broker.hostAndPort!!).get()
} }
} }
repeat(5) { repeat(5) {
@ -238,6 +238,7 @@ class RPCStabilityTests : IntegrationTest() {
override val protocolVersion = 0 override val protocolVersion = 0
override fun ping() = "pong" override fun ping() = "pong"
} }
val serverFollower = shutdownManager.follower() val serverFollower = shutdownManager.follower()
val serverPort = startRpcServer<ReconnectOps>(ops = ops).getOrThrow().broker.hostAndPort!! val serverPort = startRpcServer<ReconnectOps>(ops = ops).getOrThrow().broker.hostAndPort!!
serverFollower.unfollow() 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") { pollUntilTrue("number of RPC clients to become $expected") {
val clientAddresses = server.broker.serverControl.addressNames.filter { it.startsWith(RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX) } val clientAddresses = server.broker.serverControl.addressNames.filter { it.startsWith(RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX) }
clientAddresses.size == expected clientAddresses.size == expected

View File

@ -22,7 +22,10 @@ import net.corda.core.internal.ThreadBox
import net.corda.core.messaging.RPCOps import net.corda.core.messaging.RPCOps
import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializationContext
import net.corda.core.serialization.serialize 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.ArtemisConsumer
import net.corda.nodeapi.ArtemisProducer import net.corda.nodeapi.ArtemisProducer
import net.corda.nodeapi.RPCApi import net.corda.nodeapi.RPCApi

View File

@ -7,7 +7,7 @@ import net.corda.core.messaging.RPCOps
import net.corda.node.services.messaging.RPCServerConfiguration import net.corda.node.services.messaging.RPCServerConfiguration
import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.config.User
import net.corda.testing.SerializationEnvironmentRule 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.rpcTestUser
import net.corda.testing.internal.startInVmRpcClient import net.corda.testing.internal.startInVmRpcClient
import net.corda.testing.internal.startRpcClient import net.corda.testing.internal.startRpcClient
@ -41,7 +41,7 @@ open class AbstractRPCTest {
val createSession: () -> ClientSession val createSession: () -> ClientSession
) )
inline fun <reified I : RPCOps> RPCDriverExposedDSLInterface.testProxy( inline fun <reified I : RPCOps> RPCDriverDSL.testProxy(
ops: I, ops: I,
rpcUser: User = rpcTestUser, rpcUser: User = rpcTestUser,
clientConfiguration: RPCClientConfiguration = RPCClientConfiguration.default, clientConfiguration: RPCClientConfiguration = RPCClientConfiguration.default,
@ -55,9 +55,9 @@ open class AbstractRPCTest {
} }
} }
RPCTestMode.Netty -> RPCTestMode.Netty ->
startRpcServer(ops = ops, rpcUser = rpcUser, configuration = serverConfiguration).flatMap { server -> startRpcServer(ops = ops, rpcUser = rpcUser, configuration = serverConfiguration).flatMap { (broker) ->
startRpcClient<I>(server.broker.hostAndPort!!, rpcUser.username, rpcUser.password, clientConfiguration).map { startRpcClient<I>(broker.hostAndPort!!, rpcUser.username, rpcUser.password, clientConfiguration).map {
TestProxy(it, { startArtemisSession(server.broker.hostAndPort!!, rpcUser.username, rpcUser.password) }) TestProxy(it, { startArtemisSession(broker.hostAndPort!!, rpcUser.username, rpcUser.password) })
} }
} }
}.get() }.get()

View File

@ -7,7 +7,7 @@ import net.corda.core.internal.concurrent.thenMatch
import net.corda.core.messaging.RPCOps import net.corda.core.messaging.RPCOps
import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.getOrThrow
import net.corda.node.services.messaging.rpcContext 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.rpcDriver
import net.corda.testing.internal.rpcTestUser import net.corda.testing.internal.rpcTestUser
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
@ -26,7 +26,7 @@ import kotlin.test.assertTrue
class ClientRPCInfrastructureTests : AbstractRPCTest() { class ClientRPCInfrastructureTests : AbstractRPCTest() {
// TODO: Test that timeouts work // TODO: Test that timeouts work
private fun RPCDriverExposedDSLInterface.testProxy(): TestOps { private fun RPCDriverDSL.testProxy(): TestOps {
return testProxy<TestOps>(TestOpsImpl()).ops return testProxy<TestOps>(TestOpsImpl()).ops
} }

View File

@ -1,15 +1,15 @@
package net.corda.client.rpc package net.corda.client.rpc
import net.corda.client.rpc.internal.RPCClientConfiguration 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.crypto.random63BitValue
import net.corda.core.internal.concurrent.fork import net.corda.core.internal.concurrent.fork
import net.corda.core.internal.concurrent.transpose import net.corda.core.internal.concurrent.transpose
import net.corda.core.messaging.RPCOps
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.millis
import net.corda.node.services.messaging.RPCServerConfiguration 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.rpcDriver
import net.corda.testing.internal.testThreadFactory import net.corda.testing.internal.testThreadFactory
import org.apache.activemq.artemis.utils.collections.ConcurrentHashSet import org.apache.activemq.artemis.utils.collections.ConcurrentHashSet
@ -20,7 +20,10 @@ import org.junit.runners.Parameterized
import rx.Observable import rx.Observable
import rx.subjects.UnicastSubject import rx.subjects.UnicastSubject
import java.util.* 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) @RunWith(Parameterized::class)
class RPCConcurrencyTests : AbstractRPCTest() { class RPCConcurrencyTests : AbstractRPCTest() {
@ -84,7 +87,7 @@ class RPCConcurrencyTests : AbstractRPCTest() {
} }
} }
private fun RPCDriverExposedDSLInterface.testProxy(): TestProxy<TestOps> { private fun RPCDriverDSL.testProxy(): TestProxy<TestOps> {
return testProxy<TestOps>( return testProxy<TestOps>(
TestOpsImpl(pool), TestOpsImpl(pool),
clientConfiguration = RPCClientConfiguration.default.copy( clientConfiguration = RPCClientConfiguration.default.copy(

View File

@ -5,14 +5,14 @@ import net.corda.client.rpc.internal.RPCClientConfiguration
import net.corda.core.messaging.RPCOps import net.corda.core.messaging.RPCOps
import net.corda.core.utilities.minutes import net.corda.core.utilities.minutes
import net.corda.core.utilities.seconds import net.corda.core.utilities.seconds
import net.corda.testing.internal.performance.div
import net.corda.node.services.messaging.RPCServerConfiguration import net.corda.node.services.messaging.RPCServerConfiguration
import net.corda.testing.internal.RPCDriverExposedDSLInterface import net.corda.testing.internal.RPCDriverDSL
import net.corda.testing.measure import net.corda.testing.internal.performance.div
import net.corda.testing.internal.performance.startPublishingFixedRateInjector import net.corda.testing.internal.performance.startPublishingFixedRateInjector
import net.corda.testing.internal.performance.startReporter import net.corda.testing.internal.performance.startReporter
import net.corda.testing.internal.performance.startTightLoopInjector import net.corda.testing.internal.performance.startTightLoopInjector
import net.corda.testing.internal.rpcDriver import net.corda.testing.internal.rpcDriver
import net.corda.testing.measure
import org.junit.Ignore import org.junit.Ignore
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@ -42,7 +42,7 @@ class RPCPerformanceTests : AbstractRPCTest() {
} }
} }
private fun RPCDriverExposedDSLInterface.testProxy( private fun RPCDriverDSL.testProxy(
clientConfiguration: RPCClientConfiguration, clientConfiguration: RPCClientConfiguration,
serverConfiguration: RPCServerConfiguration serverConfiguration: RPCServerConfiguration
): TestProxy<TestOps> { ): TestProxy<TestOps> {

View File

@ -1,24 +1,19 @@
package net.corda.client.rpc package net.corda.client.rpc
import net.corda.core.messaging.CordaRPCOps
import net.corda.core.messaging.RPCOps import net.corda.core.messaging.RPCOps
import net.corda.node.services.Permissions.Companion.invokeRpc
import net.corda.node.services.messaging.rpcContext import net.corda.node.services.messaging.rpcContext
import net.corda.nodeapi.internal.config.User 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 net.corda.testing.internal.rpcDriver
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.junit.runners.Parameterized import org.junit.runners.Parameterized
import kotlin.reflect.KVisibility
import kotlin.reflect.full.declaredMemberFunctions
import kotlin.test.assertFailsWith import kotlin.test.assertFailsWith
@RunWith(Parameterized::class) @RunWith(Parameterized::class)
class RPCPermissionsTests : AbstractRPCTest() { class RPCPermissionsTests : AbstractRPCTest() {
companion object { companion object {
const val DUMMY_FLOW = "StartFlow.net.corda.flows.DummyFlow" const val DUMMY_FLOW = "StartFlow.net.corda.flows.DummyFlow"
const val OTHER_FLOW = "StartFlow.net.corda.flows.OtherFlow"
const val ALL_ALLOWED = "ALL" const val ALL_ALLOWED = "ALL"
} }
@ -26,18 +21,27 @@ class RPCPermissionsTests : AbstractRPCTest() {
* RPC operation. * RPC operation.
*/ */
interface TestOps : RPCOps { interface TestOps : RPCOps {
fun validatePermission(str: String) fun validatePermission(method: String, target: String? = null)
} }
class TestOpsImpl : TestOps { class TestOpsImpl : TestOps {
override val protocolVersion = 1 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. * Create an RPC proxy for the given user.
*/ */
private fun RPCDriverExposedDSLInterface.testProxyFor(rpcUser: User) = testProxy<TestOps>(TestOpsImpl(), rpcUser).ops private fun RPCDriverDSL.testProxyFor(rpcUser: User) = testProxy<TestOps>(TestOpsImpl(), rpcUser).ops
private fun userOf(name: String, permissions: Set<String>) = User(name, "password", permissions) private fun userOf(name: String, permissions: Set<String>) = User(name, "password", permissions)
@ -46,9 +50,9 @@ class RPCPermissionsTests : AbstractRPCTest() {
rpcDriver { rpcDriver {
val emptyUser = userOf("empty", emptySet()) val emptyUser = userOf("empty", emptySet())
val proxy = testProxyFor(emptyUser) val proxy = testProxyFor(emptyUser)
assertFailsWith(PermissionException::class, assertNotAllowed {
"User ${emptyUser.username} should not be allowed to use $DUMMY_FLOW.", proxy.validatePermission("startFlowDynamic", "net.corda.flows.DummyFlow")
{ proxy.validatePermission(DUMMY_FLOW) }) }
} }
} }
@ -57,7 +61,8 @@ class RPCPermissionsTests : AbstractRPCTest() {
rpcDriver { rpcDriver {
val adminUser = userOf("admin", setOf(ALL_ALLOWED)) val adminUser = userOf("admin", setOf(ALL_ALLOWED))
val proxy = testProxyFor(adminUser) 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 { rpcDriver {
val joeUser = userOf("joe", setOf(DUMMY_FLOW)) val joeUser = userOf("joe", setOf(DUMMY_FLOW))
val proxy = testProxyFor(joeUser) 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 { rpcDriver {
val joeUser = userOf("joe", setOf(DUMMY_FLOW)) val joeUser = userOf("joe", setOf(DUMMY_FLOW))
val proxy = testProxyFor(joeUser) val proxy = testProxyFor(joeUser)
assertFailsWith(PermissionException::class, assertNotAllowed {
"User ${joeUser.username} should not be allowed to use $OTHER_FLOW", proxy.validatePermission("startFlowDynamic", "net.corda.flows.OtherFlow")
{ proxy.validatePermission(OTHER_FLOW) }) }
} assertNotAllowed {
} proxy.validatePermission("startTrackedFlowDynamic", "net.corda.flows.OtherFlow")
@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) })
}
} }
} }
} }
@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)
}
} }

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- an all powerful policy file -->
<restrict> <restrict>
<http> <http>
<method>post</method> <method>post</method>
@ -8,23 +8,10 @@
<commands> <commands>
<command>read</command> <command>read</command>
<command>write</command>
<command>exec</command>
<command>list</command> <command>list</command>
<command>search</command>
<command>version</command>
</commands> </commands>
<!-- allow anyone to force a garbage collection -->
<allow>
<mbean>
<name>java.lang:type=Memory</name>
<operation>gc</operation>
</mbean>
</allow>
<!-- in case we ever end up using c3pio connection pooling, this example from the docs prevents the password being exported -->
<deny>
<mbean>
<name>com.mchange.v2.c3p0:type=PooledDataSource,*</name>
<attribute>properties</attribute>
</mbean>
</deny>
</restrict> </restrict>

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Jolokia agent and MBean access policy based security -->
<!-- TODO: review these settings before production deployment -->
<restrict>
<!-- IP based restrictions -->
<remote>
<!-- IP address, a host name, or a netmask given in CIDR format (e.g. "10.0.0.0/16" for all clients coming from the 10.0 network). -->
<host>127.0.0.1</host>
<host>localhost</host>
</remote>
<!-- commands for which access is granted: read, write, exec, list, search, version -->
<commands>
<command>version</command>
<command>read</command>
</commands>
<!-- MBean access and deny restrictions -->
<!-- HTTP method restrictions: get, post -->
<http>
<method>get</method>
</http>
<!-- Cross-Origin Resource Sharing (CORS) restrictions
(by default, allow cross origin access from any host)
-->
</restrict>

View File

@ -5,3 +5,4 @@ guavaVersion=21.0
bouncycastleVersion=1.57 bouncycastleVersion=1.57
typesafeConfigVersion=1.3.1 typesafeConfigVersion=1.3.1
jsr305Version=3.0.2 jsr305Version=3.0.2
artifactoryPluginVersion=4.4.18

View File

@ -1,13 +1,14 @@
package net.corda.core.contracts 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.crypto.SecureHash
import net.corda.core.internal.UpgradeCommand import net.corda.core.internal.UpgradeCommand
import net.corda.testing.ALICE import net.corda.core.node.ServicesForResolution
import net.corda.testing.DUMMY_NOTARY import net.corda.testing.*
import net.corda.testing.contracts.DummyContract import net.corda.testing.contracts.DummyContract
import net.corda.testing.contracts.DummyContractV2 import net.corda.testing.contracts.DummyContractV2
import net.corda.testing.node.MockServices
import net.corda.testing.SerializationEnvironmentRule
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
@ -23,7 +24,9 @@ class DummyContractV2Tests {
@Test @Test
fun `upgrade from v1`() { fun `upgrade from v1`() {
val services = MockServices() val services = rigorousMock<ServicesForResolution>().also {
doReturn(rigorousMock<CordappProvider>()).whenever(it).cordappProvider
}
val contractUpgrade = DummyContractV2() val contractUpgrade = DummyContractV2()
val v1State = TransactionState(DummyContract.SingleOwnerState(0, ALICE), DummyContract.PROGRAM_ID, DUMMY_NOTARY, constraint = AlwaysAcceptAttachmentConstraint) val v1State = TransactionState(DummyContract.SingleOwnerState(0, ALICE), DummyContract.PROGRAM_ID, DUMMY_NOTARY, constraint = AlwaysAcceptAttachmentConstraint)
val v1Ref = StateRef(SecureHash.randomSHA256(), 0) val v1Ref = StateRef(SecureHash.randomSHA256(), 0)

View File

@ -337,7 +337,7 @@ class CompositeKeyTests {
val ca = X509Utilities.createSelfSignedCACertificate(caName, caKeyPair) val ca = X509Utilities.createSelfSignedCACertificate(caName, caKeyPair)
// Sign the composite key with the self sign CA. // 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. // Store certificate to keystore.
val keystorePath = tempFolder.root.toPath() / "keystore.jks" val keystorePath = tempFolder.root.toPath() / "keystore.jks"

View File

@ -129,7 +129,7 @@ class CollectSignaturesFlowTests {
@Test @Test
fun `fails when not signed by initiator`() { fun `fails when not signed by initiator`() {
val onePartyDummyContract = DummyContract.generateInitial(1337, notary, alice.ref(1)) 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 ptx = miniCorpServices.signInitialTransaction(onePartyDummyContract)
val flow = aliceNode.services.startFlow(CollectSignaturesFlow(ptx, emptySet())) val flow = aliceNode.services.startFlow(CollectSignaturesFlow(ptx, emptySet()))
mockNet.runNetwork() mockNet.runNetwork()

View File

@ -20,14 +20,17 @@ import net.corda.node.internal.SecureCordaRPCOps
import net.corda.node.internal.StartedNode import net.corda.node.internal.StartedNode
import net.corda.node.services.Permissions.Companion.startFlow import net.corda.node.services.Permissions.Companion.startFlow
import net.corda.nodeapi.internal.config.User 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.DummyContract
import net.corda.testing.contracts.DummyContractV2 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.rpcDriver
import net.corda.testing.internal.rpcTestUser import net.corda.testing.internal.rpcTestUser
import net.corda.testing.internal.startRpcClient import net.corda.testing.internal.startRpcClient
import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNetwork
import net.corda.testing.singleIdentity
import net.corda.testing.startFlow
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
@ -120,7 +123,7 @@ class ContractUpgradeFlowTest {
check(bobNode) check(bobNode)
} }
private fun RPCDriverExposedDSLInterface.startProxy(node: StartedNode<*>, user: User): CordaRPCOps { private fun RPCDriverDSL.startProxy(node: StartedNode<*>, user: User): CordaRPCOps {
return startRpcClient<CordaRPCOps>( return startRpcClient<CordaRPCOps>(
rpcAddress = startRpcServer( rpcAddress = startRpcServer(
rpcUser = user, rpcUser = user,

View File

@ -1,6 +1,7 @@
package net.corda.core.serialization package net.corda.core.serialization
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.crypto.generateKeyPair
import net.corda.core.identity.AbstractParty import net.corda.core.identity.AbstractParty
import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.LedgerTransaction
import net.corda.core.transactions.TransactionBuilder 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 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 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 changeState = TransactionState(TestCash.State(depositRef, 400.POUNDS, MEGA_CORP), TEST_CASH_PROGRAM_ID, DUMMY_NOTARY)
val megaCorpServices = MockServices(listOf("net.corda.core.serialization"), rigorousMock(), MEGA_CORP.name, MEGA_CORP_KEY)
val megaCorpServices = MockServices(listOf("net.corda.core.serialization"), MEGA_CORP.name, MEGA_CORP_KEY) val notaryServices = MockServices(listOf("net.corda.core.serialization"), rigorousMock(), DUMMY_NOTARY.name, DUMMY_NOTARY_KEY)
val notaryServices = MockServices(listOf("net.corda.core.serialization"), DUMMY_NOTARY.name, DUMMY_NOTARY_KEY)
lateinit var tx: TransactionBuilder lateinit var tx: TransactionBuilder
@Before @Before
@ -88,14 +88,14 @@ class TransactionSerializationTests {
assertFailsWith(IllegalArgumentException::class) { assertFailsWith(IllegalArgumentException::class) {
stx.copy(sigs = emptyList()) stx.copy(sigs = emptyList())
} }
val DUMMY_KEY_2 = generateKeyPair()
// If the signature was replaced in transit, we don't like it. // If the signature was replaced in transit, we don't like it.
assertFailsWith(SignatureException::class) { assertFailsWith(SignatureException::class) {
val tx2 = TransactionBuilder(DUMMY_NOTARY).withItems(inputState, outputState, changeState, val tx2 = TransactionBuilder(DUMMY_NOTARY).withItems(inputState, outputState, changeState,
Command(TestCash.Commands.Move(), DUMMY_KEY_2.public)) Command(TestCash.Commands.Move(), DUMMY_KEY_2.public))
val ptx2 = notaryServices.signInitialTransaction(tx2) 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) val stx2 = dummyServices.addSignature(ptx2)
stx.copy(sigs = stx2.sigs).verifyRequiredSignatures() stx.copy(sigs = stx2.sigs).verifyRequiredSignatures()

View File

@ -2,9 +2,11 @@ package net.corda.core.serialization
import net.corda.core.contracts.StateRef import net.corda.core.contracts.StateRef
import net.corda.core.crypto.SecureHash 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.UniquenessException
import net.corda.core.node.services.UniquenessProvider import net.corda.core.node.services.UniquenessProvider
import net.corda.testing.DUMMY_PARTY
import net.corda.testing.SerializationEnvironmentRule import net.corda.testing.SerializationEnvironmentRule
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
@ -19,7 +21,8 @@ class UniquenessExceptionSerializationTest {
fun testSerializationRoundTrip() { fun testSerializationRoundTrip() {
val txhash = SecureHash.randomSHA256() val txhash = SecureHash.randomSHA256()
val txHash2 = SecureHash.randomSHA256() val txHash2 = SecureHash.randomSHA256()
val stateHistory: Map<StateRef, UniquenessProvider.ConsumingTx> = mapOf(StateRef(txhash, 0) to UniquenessProvider.ConsumingTx(txHash2, 1, DUMMY_PARTY)) val dummyParty = Party(CordaX500Name("Dummy", "Madrid", "ES"), generateKeyPair().public)
val stateHistory: Map<StateRef, UniquenessProvider.ConsumingTx> = mapOf(StateRef(txhash, 0) to UniquenessProvider.ConsumingTx(txHash2, 1, dummyParty))
val conflict = UniquenessProvider.Conflict(stateHistory) val conflict = UniquenessProvider.Conflict(stateHistory)
val instance = UniquenessException(conflict) val instance = UniquenessException(conflict)

View File

@ -15,6 +15,11 @@ import java.util.function.Predicate
import kotlin.test.* import kotlin.test.*
class CompatibleTransactionTests { class CompatibleTransactionTests {
private companion object {
val DUMMY_KEY_1 = generateKeyPair()
val DUMMY_KEY_2 = generateKeyPair()
}
@Rule @Rule
@JvmField @JvmField
val testSerialization = SerializationEnvironmentRule() val testSerialization = SerializationEnvironmentRule()

View File

@ -1,14 +1,15 @@
package net.corda.core.transactions 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.contracts.*
import net.corda.core.crypto.generateKeyPair
import net.corda.core.identity.AbstractParty import net.corda.core.identity.AbstractParty
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.testing.DUMMY_NOTARY import net.corda.node.services.api.IdentityServiceInternal
import net.corda.testing.SerializationEnvironmentRule import net.corda.testing.*
import net.corda.testing.contracts.DummyContract import net.corda.testing.contracts.DummyContract
import net.corda.testing.dummyCommand
import net.corda.testing.node.MockServices import net.corda.testing.node.MockServices
import net.corda.testing.singleIdentity
import org.junit.Before import org.junit.Before
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
@ -21,7 +22,10 @@ class LedgerTransactionQueryTests {
@Rule @Rule
@JvmField @JvmField
val testSerialization = SerializationEnvironmentRule() val testSerialization = SerializationEnvironmentRule()
private val services: MockServices = MockServices() private val keyPair = generateKeyPair()
private val services = MockServices(rigorousMock<IdentityServiceInternal>().also {
doReturn(null).whenever(it).partyFromKey(keyPair.public)
}, MEGA_CORP.name, keyPair)
private val identity: Party = services.myInfo.singleIdentity() private val identity: Party = services.myInfo.singleIdentity()
@Before @Before

View File

@ -16,6 +16,11 @@ import kotlin.test.assertFailsWith
import kotlin.test.assertNotEquals import kotlin.test.assertNotEquals
class TransactionTests { class TransactionTests {
private companion object {
val DUMMY_KEY_1 = generateKeyPair()
val DUMMY_KEY_2 = generateKeyPair()
}
@Rule @Rule
@JvmField @JvmField
val testSerialization = SerializationEnvironmentRule() val testSerialization = SerializationEnvironmentRule()

View File

@ -1,15 +1,15 @@
Contract Constraints API: Contract Constraints
==================== =========================
A basic understanding of contract key concepts, which can be found :doc:`here </key-concepts-contracts>`, A basic understanding of contract key concepts, which can be found :doc:`here </key-concepts-contracts>`,
is required reading for this page. 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 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 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 not sufficient to specify the ``verify`` function by name as there may exist multiple different implementations with
same method signature and enclosing class. Contract constraints solve this problem by allowing a contract developer to the same method signature and enclosing class. Contract constraints solve this problem by allowing a contract developer
constrain which verify() functions out of the universe of implementations can be used. to constrain which ``verify`` functions out of the universe of implementations can be used (i.e. the universe is
(ie the universe is everything that matches the signature and contract constraints restricts this universe to a subset.) 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 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 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 (``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 ``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 ``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`` 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 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 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 .. sourcecode:: java
@ -42,12 +43,11 @@ shows how to construct a ``TransactionState`` with an explicitly specified hash
LedgerTransaction ltx = wtx.toLedgerTransaction(serviceHub) LedgerTransaction ltx = wtx.toLedgerTransaction(serviceHub)
ltx.verify() // Verifies both the attachment constraints and contracts 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, 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 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 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 to verify the transaction chain. Ensuring the attachment used is the correct one ensures that the verification is
not be tamperable by providing a fake contract. tamper-proof by providing a fake contract.
CorDapps as attachments 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`` 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. 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 After CorDapps are loaded into the attachment store the node creates a link between contract classes and the attachment
attachment that they were loaded from. This makes it possible to find the attachment for any given contract. that they were loaded from. This makes it possible to find the attachment for any given contract. This is how the
This is how the automatic resolution of attachments is done by the ``TransactionBuilder`` and how, when verifying automatic resolution of attachments is done by the ``TransactionBuilder`` and how, when verifying the constraints and
the constraints and contracts, attachments are associated with their respective contracts. contracts, attachments are associated with their respective contracts.
Implementations Implementations
--------------- ---------------
@ -95,7 +95,7 @@ to specify JAR URLs in the case that the CorDapp(s) involved in testing already
MockNetwork/MockNode 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) ``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 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 within those packages will be zipped into a JAR and added to the attachment store and loaded as CorDapps by the

View File

@ -6,6 +6,9 @@ from the previous milestone release.
UNRELEASED 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. * 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 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. in the database (pre configured via DDL scripts or equivalent), and validate these are correct.

View File

@ -4,6 +4,6 @@ Cheat sheet
A "cheat sheet" summarizing the key Corda types. A PDF version is downloadable `here`_. A "cheat sheet" summarizing the key Corda types. A PDF version is downloadable `here`_.
.. image:: resources/cheatsheet.jpg .. image:: resources/cheatsheet.jpg
:width: 700px :width: 700px
.. _`here`: _static/corda-cheat-sheet.pdf .. _`here`: _static/corda-cheat-sheet.pdf

View File

@ -9,6 +9,7 @@ The following are the core APIs that are used in the development of CorDapps:
api-states api-states
api-persistence api-persistence
api-contracts api-contracts
api-contract-constraints
api-vault-query api-vault-query
api-transactions api-transactions
api-flows api-flows

View File

@ -68,21 +68,28 @@ path to the node's base directory.
.. note:: Longer term these keys will be managed in secure hardware devices. .. 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. :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 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. 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: :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. :serverNameTablePrefix: Prefix string to apply to all the database tables. The default is no prefix.
: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').
:transactionIsolationLevel: Transaction isolation level as defined by the ``TRANSACTION_`` constants in :transactionIsolationLevel: Transaction isolation level as defined by the ``TRANSACTION_`` constants in
``java.sql.Connection``, but without the "TRANSACTION_" prefix. Defaults to REPEATABLE_READ. ``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. :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 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' ]`` 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 :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.
and permissions as RPC subsystem. It has one required parameter.
:port: The port to start SSH server on :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. :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. :publicKeyFile: Path to the public key file for SSH authentication.
:sshPort: Port to be used for SSH connection, default ``22``. :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/

View File

@ -10,6 +10,7 @@ Corda nodes
corda-configuration-file corda-configuration-file
clientrpc clientrpc
shell shell
node-auth-config
node-database node-database
node-administration node-administration
out-of-process-verification out-of-process-verification

View File

@ -115,19 +115,24 @@ is already correctly configured and this is for reference only;
Creating the CorDapp JAR Creating the CorDapp JAR
------------------------ ------------------------
The gradle ``jar`` task included in the CorDapp template build file will automatically build your CorDapp JAR correctly Once your dependencies are set correctly, you can build your CorDapp JAR using the gradle ``jar`` task:
as long as your dependencies are set correctly.
* 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 .. 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 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. 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 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 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`). 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 .. 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 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 ``<node_dir>/cordapps/`` folder, where ``node_dir`` is the folder in which a node, the CorDapp JAR must be added to the ``<node_dir>/cordapps/`` folder, where ``node_dir`` is the folder in which
the node's JAR and configuration files are stored. 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.

View File

@ -23,7 +23,22 @@ into the ``cordapps`` folder.
Node naming 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: * 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 * Locality: 64
* State: 64 * State: 64
* The country code is a valid ISO 3166-1 two letter code in upper-case * The country attribute is a valid ISO 3166-1 two letter code in upper-case
* The organisation, locality and country attributes are present
* The organisation field of the name obeys the following constraints: * The organisation field of the name obeys the following constraints:
* Upper-case first letter
* Has at least two letters * Has at least two letters
* No leading or trailing whitespace * No leading or trailing whitespace
* No double-spacing * No double-spacing
* Upper-case first letter
* Does not contain the words "node" or "server" * 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 * Is in NFKC normalization form
* Only the latin, common and inherited unicode scripts are supported * 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 The Cordform task
----------------- -----------------
Corda provides a gradle plugin called ``Cordform`` that allows you to automatically generate and configure a set of Corda provides a gradle plugin called ``Cordform`` that allows you to automatically generate and configure a set of

View File

@ -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 * 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. 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 * 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. 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.

View File

@ -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 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. 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 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 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 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 controls applied at the record level to ensure only those who require access to an identity can retrieve its
certificate. certificate.
.. TODO: Revisit once design & use cases of private maps is further fleshed out

View File

@ -16,7 +16,6 @@ This section should be read in order:
key-concepts-identity key-concepts-identity
key-concepts-states key-concepts-states
key-concepts-contracts key-concepts-contracts
key-concepts-contract-constraints
key-concepts-transactions key-concepts-transactions
key-concepts-flows key-concepts-flows
key-concepts-consensus key-concepts-consensus

View File

@ -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: Here are a few ways to build dashboards and extract monitoring data for a node:
* `hawtio <https://hawt.io>`_ 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 <https://github.com/logzio/jmx2graphite>`_ is a tool that can be pointed to /monitoring/json and will * `JMX2Graphite <https://github.com/logzio/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 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. 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 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. 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/<env>``.
The following diagram illustrates Corda flow metrics visualized using `hawtio <https://hawt.io>`_ :
.. image:: resources/hawtio-jmx.png
Memory usage and tuning Memory usage and tuning
----------------------- -----------------------

View File

@ -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 = "<jdbc connection string>"
username = "<db username>"
password = "<db user password>"
driverClassName = "<JDBC driver>"
}
}
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 <https://shiro.apache.org/command-line-hasher.html>`_
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.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 668 KiB

After

Width:  |  Height:  |  Size: 670 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

View File

@ -17,7 +17,6 @@ import net.corda.testing.contracts.VaultFiller
import net.corda.testing.node.MockServices import net.corda.testing.node.MockServices
import net.corda.testing.node.MockServices.Companion.makeTestDatabaseAndMockServices import net.corda.testing.node.MockServices.Companion.makeTestDatabaseAndMockServices
import net.corda.testing.node.makeTestIdentityService import net.corda.testing.node.makeTestIdentityService
import org.junit.Ignore
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@ -224,19 +223,16 @@ class CommercialPaperTestsGeneric {
private lateinit var aliceServices: MockServices private lateinit var aliceServices: MockServices
private lateinit var aliceVaultService: VaultService private lateinit var aliceVaultService: VaultService
private lateinit var alicesVault: Vault<ContractState> private lateinit var alicesVault: Vault<ContractState>
private val notaryServices = MockServices(rigorousMock(), MEGA_CORP.name, DUMMY_NOTARY_KEY)
private val notaryServices = MockServices(DUMMY_NOTARY_KEY) private val issuerServices = MockServices(listOf("net.corda.finance.contracts"), rigorousMock(), MEGA_CORP.name, DUMMY_CASH_ISSUER_KEY)
private val issuerServices = MockServices(DUMMY_CASH_ISSUER_KEY)
private lateinit var moveTX: SignedTransaction private lateinit var moveTX: SignedTransaction
@Test
// @Test fun `issue move and then redeem`() {
@Ignore
fun `issue move and then redeem`() = withTestSerialization {
val aliceDatabaseAndServices = makeTestDatabaseAndMockServices( val aliceDatabaseAndServices = makeTestDatabaseAndMockServices(
listOf(ALICE_KEY), listOf(ALICE_KEY),
makeTestIdentityService(listOf(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, DUMMY_CASH_ISSUER_IDENTITY, DUMMY_NOTARY_IDENTITY)), 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 val databaseAlice = aliceDatabaseAndServices.first
aliceServices = aliceDatabaseAndServices.second aliceServices = aliceDatabaseAndServices.second
aliceVaultService = aliceServices.vaultService aliceVaultService = aliceServices.vaultService
@ -248,7 +244,8 @@ class CommercialPaperTestsGeneric {
val bigCorpDatabaseAndServices = makeTestDatabaseAndMockServices( val bigCorpDatabaseAndServices = makeTestDatabaseAndMockServices(
listOf(BIG_CORP_KEY), listOf(BIG_CORP_KEY),
makeTestIdentityService(listOf(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, DUMMY_CASH_ISSUER_IDENTITY, DUMMY_NOTARY_IDENTITY)), 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 val databaseBigCorp = bigCorpDatabaseAndServices.first
bigCorpServices = bigCorpDatabaseAndServices.second bigCorpServices = bigCorpDatabaseAndServices.second
bigCorpVaultService = bigCorpServices.vaultService bigCorpVaultService = bigCorpServices.vaultService

View File

@ -1,5 +1,6 @@
package net.corda.finance.contracts.asset package net.corda.finance.contracts.asset
import com.nhaarman.mockito_kotlin.*
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.generateKeyPair 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.sumCashBy
import net.corda.finance.utils.sumCashOrNull import net.corda.finance.utils.sumCashOrNull
import net.corda.finance.utils.sumCashOrZero import net.corda.finance.utils.sumCashOrZero
import net.corda.node.services.api.IdentityServiceInternal
import net.corda.node.services.vault.NodeVaultService import net.corda.node.services.vault.NodeVaultService
import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.testing.* import net.corda.testing.*
@ -67,9 +69,11 @@ class CashTests {
@Before @Before
fun setUp() { fun setUp() {
LogHelper.setLevel(NodeVaultService::class) LogHelper.setLevel(NodeVaultService::class)
megaCorpServices = MockServices(listOf("net.corda.finance.contracts.asset"), MEGA_CORP.name, MEGA_CORP_KEY) megaCorpServices = MockServices(listOf("net.corda.finance.contracts.asset"), rigorousMock(), MEGA_CORP.name, MEGA_CORP_KEY)
miniCorpServices = MockServices(listOf("net.corda.finance.contracts.asset"), MINI_CORP.name, MINI_CORP_KEY) miniCorpServices = MockServices(listOf("net.corda.finance.contracts.asset"), rigorousMock<IdentityServiceInternal>().also {
val notaryServices = MockServices(listOf("net.corda.finance.contracts.asset"), DUMMY_NOTARY.name, DUMMY_NOTARY_KEY) 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( val databaseAndServices = makeTestDatabaseAndMockServices(
listOf(generateKeyPair()), listOf(generateKeyPair()),
makeTestIdentityService(listOf(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, DUMMY_CASH_ISSUER_IDENTITY, DUMMY_NOTARY_IDENTITY)), 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<Currency>, dest: AbstractParty): WireTransaction { private fun makeSpend(services: ServiceHub, amount: Amount<Currency>, dest: AbstractParty): WireTransaction {
val ourIdentity = services.myInfo.singleIdentityAndCert() val ourIdentity = services.myInfo.singleIdentityAndCert()
val changeIdentity = services.keyManagementService.freshKeyAndCert(ourIdentity, false)
val tx = TransactionBuilder(DUMMY_NOTARY) val tx = TransactionBuilder(DUMMY_NOTARY)
database.transaction { database.transaction {
Cash.generateSpend(services, tx, amount, changeIdentity, dest) Cash.generateSpend(services, tx, amount, ourIdentity, dest)
} }
return tx.toWireTransaction(services) return tx.toWireTransaction(services)
} }
@ -601,11 +604,10 @@ class CashTests {
@Test @Test
fun generateSimpleSpendWithParties() { fun generateSimpleSpendWithParties() {
val changeIdentity = ourServices.keyManagementService.freshKeyAndCert(ourServices.myInfo.singleIdentityAndCert(), false)
database.transaction { database.transaction {
val tx = TransactionBuilder(DUMMY_NOTARY) 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]) assertEquals(vaultStatesUnconsumed.elementAt(2).ref, tx.inputStates()[0])
} }
@ -774,8 +776,9 @@ class CashTests {
// Double spend. // Double spend.
@Test @Test
fun chainCashDoubleSpendFailsWith() { 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<IdentityServiceInternal>().also {
doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY)
}, MEGA_CORP.name, MEGA_CORP_KEY)
ledger(mockService) { ledger(mockService) {
unverifiedTransaction { unverifiedTransaction {
attachment(Cash.PROGRAM_ID) attachment(Cash.PROGRAM_ID)
@ -813,12 +816,11 @@ class CashTests {
fun multiSpend() { fun multiSpend() {
val tx = TransactionBuilder(DUMMY_NOTARY) val tx = TransactionBuilder(DUMMY_NOTARY)
database.transaction { database.transaction {
val changeIdentity = ourServices.keyManagementService.freshKeyAndCert(ourServices.myInfo.singleIdentityAndCert(), false)
val payments = listOf( val payments = listOf(
PartyAndAmount(miniCorpAnonymised, 400.DOLLARS), PartyAndAmount(miniCorpAnonymised, 400.DOLLARS),
PartyAndAmount(CHARLIE_ANONYMISED, 150.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) val wtx = tx.toWireTransaction(ourServices)
fun out(i: Int) = wtx.getOutput(i) as Cash.State fun out(i: Int) = wtx.getOutput(i) as Cash.State

View File

@ -1,5 +1,7 @@
package net.corda.finance.contracts.asset 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.contracts.*
import net.corda.core.crypto.NullKeys.NULL_PARTY import net.corda.core.crypto.NullKeys.NULL_PARTY
import net.corda.core.crypto.SecureHash 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.Commodity
import net.corda.finance.contracts.NetType import net.corda.finance.contracts.NetType
import net.corda.finance.contracts.asset.Obligation.Lifecycle import net.corda.finance.contracts.asset.Obligation.Lifecycle
import net.corda.node.services.api.IdentityServiceInternal
import net.corda.testing.* import net.corda.testing.*
import net.corda.testing.contracts.DummyContract import net.corda.testing.contracts.DummyContract
import net.corda.testing.contracts.DummyState import net.corda.testing.contracts.DummyState
@ -51,9 +54,13 @@ class ObligationTests {
beneficiary = CHARLIE beneficiary = CHARLIE
) )
private val outState = inState.copy(beneficiary = AnonymousParty(BOB_PUBKEY)) 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 miniCorpServices = MockServices(listOf("net.corda.finance.contracts.asset"), rigorousMock(), MINI_CORP.name, MINI_CORP_KEY)
private val notaryServices = MockServices(DUMMY_NOTARY_KEY) private val notaryServices = MockServices(rigorousMock(), MEGA_CORP.name, DUMMY_NOTARY_KEY)
private val mockService = MockServices(listOf("net.corda.finance.contracts.asset")) private val mockService = MockServices(listOf("net.corda.finance.contracts.asset"), rigorousMock<IdentityServiceInternal>().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( private fun cashObligationTestRoots(
group: LedgerDSL<TestTransactionDSLInterpreter, TestLedgerDSLInterpreter> group: LedgerDSL<TestTransactionDSLInterpreter, TestLedgerDSLInterpreter>

View File

@ -1,5 +1,6 @@
apply plugin: 'java' apply plugin: 'java'
apply plugin: 'net.corda.plugins.publish-utils' apply plugin: 'net.corda.plugins.publish-utils'
apply plugin: 'com.jfrog.artifactory'
description "Generates a summary of the artifact's public API" description "Generates a summary of the artifact's public API"

View File

@ -7,11 +7,14 @@ buildscript {
file("$projectDir/../constants.properties").withInputStream { constants.load(it) } 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. // 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 {
ext.bouncycastle_version = constants.getProperty("bouncycastleVersion") gradle_plugins_version = constants.getProperty("gradlePluginsVersion")
ext.typesafe_config_version = constants.getProperty("typesafeConfigVersion") bouncycastle_version = constants.getProperty("bouncycastleVersion")
ext.jsr305_version = constants.getProperty("jsr305Version") typesafe_config_version = constants.getProperty("typesafeConfigVersion")
ext.kotlin_version = constants.getProperty("kotlinVersion") jsr305_version = constants.getProperty("jsr305Version")
kotlin_version = constants.getProperty("kotlinVersion")
artifactory_plugin_version = constants.getProperty('artifactoryPluginVersion')
}
repositories { repositories {
mavenLocal() mavenLocal()
@ -22,10 +25,12 @@ buildscript {
classpath "net.corda.plugins:publish-utils:$gradle_plugins_version" classpath "net.corda.plugins:publish-utils:$gradle_plugins_version"
classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.7.3' classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.7.3'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 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: 'net.corda.plugins.publish-utils'
apply plugin: 'com.jfrog.artifactory'
allprojects { allprojects {
version gradle_plugins_version version gradle_plugins_version
@ -54,3 +59,25 @@ bintrayConfig {
email = 'dev@corda.net' 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())
}
}
}
}

View File

@ -1,5 +1,6 @@
apply plugin: 'kotlin' apply plugin: 'kotlin'
apply plugin: 'net.corda.plugins.publish-utils' 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' description 'Turns a project into a cordapp project that produces cordapp fat JARs'

View File

@ -23,6 +23,13 @@ class Utils {
project.configurations.single { it.name == "compile" }.extendsFrom(configuration) 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)
}
}
} }
} }

View File

@ -1,6 +1,7 @@
apply plugin: 'java' apply plugin: 'java'
apply plugin: 'maven-publish' apply plugin: 'maven-publish'
apply plugin: 'net.corda.plugins.publish-utils' apply plugin: 'net.corda.plugins.publish-utils'
apply plugin: 'com.jfrog.artifactory'
repositories { repositories {
mavenCentral() mavenCentral()

View File

@ -10,6 +10,7 @@ buildscript {
apply plugin: 'kotlin' apply plugin: 'kotlin'
apply plugin: 'net.corda.plugins.publish-utils' 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.' description 'A small gradle plugin for adding some basic Quasar tasks and configurations to reduce build.gradle bloat.'

View File

@ -10,6 +10,8 @@ import java.io.File
*/ */
class Cordformation : Plugin<Project> { class Cordformation : Plugin<Project> {
internal companion object { internal companion object {
const val CORDFORMATION_TYPE = "cordformationInternal"
/** /**
* Gets a resource file from this plugin's JAR file. * Gets a resource file from this plugin's JAR file.
* *
@ -31,5 +33,8 @@ class Cordformation : Plugin<Project> {
override fun apply(project: Project) { override fun apply(project: Project) {
Utils.createCompileConfiguration("cordapp", project) Utils.createCompileConfiguration("cordapp", project)
Utils.createRuntimeConfiguration(CORDFORMATION_TYPE, project)
val jolokiaVersion = project.rootProject.ext<String>("jolokia_version")
project.dependencies.add(CORDFORMATION_TYPE, "org.jolokia:jolokia-jvm:$jolokiaVersion:agent")
} }
} }

View File

@ -1,6 +1,8 @@
package net.corda.plugins 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 net.corda.cordform.CordformNode
import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.asn1.x500.style.BCStyle import org.bouncycastle.asn1.x500.style.BCStyle
@ -90,6 +92,7 @@ class Node(private val project: Project) : CordformNode() {
if (config.hasPath("webAddress")) { if (config.hasPath("webAddress")) {
installWebserverJar() installWebserverJar()
} }
installAgentJar()
installBuiltCordapp() installBuiltCordapp()
installCordapps() installCordapps()
installConfig() 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<String>("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. * Installs the configuration file to this node's directory and detokenises it.
*/ */

View File

@ -22,6 +22,11 @@ private object debugPortAlloc {
internal fun next() = basePort++ internal fun next() = basePort++
} }
private object monitoringPortAlloc {
private var basePort = 7005
internal fun next() = basePort++
}
fun main(args: Array<String>) { fun main(args: Array<String>) {
val startedProcesses = mutableListOf<Process>() val startedProcesses = mutableListOf<Process>()
val headless = GraphicsEnvironment.isHeadless() || args.contains(HEADLESS_FLAG) val headless = GraphicsEnvironment.isHeadless() || args.contains(HEADLESS_FLAG)
@ -49,8 +54,9 @@ private abstract class JarType(private val jarName: String) {
return null return null
} }
val debugPort = debugPortAlloc.next() val debugPort = debugPortAlloc.next()
val monitoringPort = monitoringPortAlloc.next()
println("Starting $jarName in $dir on debug port $debugPort") 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) if (os == OS.MACOS) Thread.sleep(1000)
return process return process
} }
@ -69,15 +75,23 @@ private abstract class JavaCommand(
jarName: String, jarName: String,
internal val dir: File, internal val dir: File,
debugPort: Int?, debugPort: Int?,
monitoringPort: Int?,
internal val nodeName: String, internal val nodeName: String,
init: MutableList<String>.() -> Unit, args: List<String>, init: MutableList<String>.() -> Unit, args: List<String>,
jvmArgs: List<String> jvmArgs: List<String>
) { ) {
private val jolokiaJar by lazy {
File("$dir/drivers").listFiles { _, filename ->
filename.matches("jolokia-jvm-.*-agent\\.jar$".toRegex())
}.first().name
}
internal val command: List<String> = mutableListOf<String>().apply { internal val command: List<String> = mutableListOf<String>().apply {
add(getJavaPath()) add(getJavaPath())
addAll(jvmArgs) addAll(jvmArgs)
add("-Dname=$nodeName") add("-Dname=$nodeName")
null != debugPort && add("-Dcapsule.jvm.args=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=$debugPort") 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("-jar")
add(jarName) add(jarName)
init() init()
@ -89,14 +103,14 @@ private abstract class JavaCommand(
internal abstract fun getJavaPath(): String internal abstract fun getJavaPath(): String
} }
private class HeadlessJavaCommand(jarName: String, dir: File, debugPort: Int?, args: List<String>, jvmArgs: List<String>) private class HeadlessJavaCommand(jarName: String, dir: File, debugPort: Int?, monitoringPort: Int?, args: List<String>, jvmArgs: List<String>)
: JavaCommand(jarName, dir, debugPort, dir.name, { add("--no-local-shell") }, args, jvmArgs) { : 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 processBuilder() = ProcessBuilder(command).redirectError(File("error.$nodeName.log")).inheritIO()
override fun getJavaPath() = File(File(System.getProperty("java.home"), "bin"), "java").path override fun getJavaPath() = File(File(System.getProperty("java.home"), "bin"), "java").path
} }
private class TerminalWindowJavaCommand(jarName: String, dir: File, debugPort: Int?, args: List<String>, jvmArgs: List<String>) private class TerminalWindowJavaCommand(jarName: String, dir: File, debugPort: Int?, monitoringPort: Int?, args: List<String>, jvmArgs: List<String>)
: JavaCommand(jarName, dir, debugPort, "${dir.name}-$jarName", {}, args, jvmArgs) { : JavaCommand(jarName, dir, debugPort, monitoringPort, "${dir.name}-$jarName", {}, args, jvmArgs) {
override fun processBuilder() = ProcessBuilder(when (os) { override fun processBuilder() = ProcessBuilder(when (os) {
OS.MACOS -> { OS.MACOS -> {
listOf("osascript", "-e", """tell app "Terminal" listOf("osascript", "-e", """tell app "Terminal"

View File

@ -1,13 +1,17 @@
apply plugin: 'groovy' apply plugin: 'groovy'
apply plugin: 'maven-publish' apply plugin: 'maven-publish'
apply plugin: 'com.jfrog.bintray' apply plugin: 'com.jfrog.bintray'
apply plugin: 'com.jfrog.artifactory'
// Used for bootstrapping project // Used for bootstrapping project
buildscript { buildscript {
Properties constants = new Properties() Properties constants = new Properties()
file("../../constants.properties").withInputStream { constants.load(it) } 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 { repositories {
jcenter() jcenter()
@ -15,6 +19,7 @@ buildscript {
dependencies { dependencies {
classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.4' classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.4'
classpath "org.jfrog.buildinfo:build-info-extractor-gradle:$artifactory_plugin_version"
} }
} }

View File

@ -1,6 +1,7 @@
apply plugin: 'groovy' apply plugin: 'groovy'
apply plugin: 'maven-publish' apply plugin: 'maven-publish'
apply plugin: 'net.corda.plugins.publish-utils' 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.' description 'A small gradle plugin for adding some basic Quasar tasks and configurations to reduce build.gradle bloat.'

View File

@ -80,6 +80,7 @@ private fun Config.getSingleValue(path: String, type: KType): Any? {
URL::class -> URL(getString(path)) URL::class -> URL(getString(path))
CordaX500Name::class -> CordaX500Name.parse(getString(path)) CordaX500Name::class -> CordaX500Name.parse(getString(path))
Properties::class -> getConfig(path).toProperties() Properties::class -> getConfig(path).toProperties()
Config::class -> getConfig(path)
else -> if (typeClass.java.isEnum) { else -> if (typeClass.java.isEnum) {
parseEnum(typeClass.java, getString(path)) parseEnum(typeClass.java, getString(path))
} else { } else {

View File

@ -8,7 +8,6 @@ import java.security.KeyPair
import java.security.PublicKey import java.security.PublicKey
import java.security.cert.CertPath import java.security.cert.CertPath
import java.security.cert.Certificate import java.security.cert.Certificate
import java.security.cert.CertificateFactory
class KeyStoreWrapper(private val storePath: Path, private val storePassword: String) { class KeyStoreWrapper(private val storePath: Path, private val storePassword: String) {
private val keyStore = storePath.read { loadKeyStore(it, storePassword) } 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. // Assume key password = store password.
val clientCA = certificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA) val clientCA = certificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA)
// Create new keys and store in keystore. // 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) val certPath = X509CertificateFactory().delegate.generateCertPath(listOf(cert.cert) + clientCertPath)
require(certPath.certificates.isNotEmpty()) { "Certificate path cannot be empty" } require(certPath.certificates.isNotEmpty()) { "Certificate path cannot be empty" }
// TODO: X509Utilities.validateCertificateChain() // TODO: X509Utilities.validateCertificateChain()

View File

@ -332,7 +332,7 @@ enum class CertificateType(val keyUsage: KeyUsage, vararg val purposes: KeyPurpo
isCA = true isCA = true
), ),
CLIENT_CA( NODE_CA(
KeyUsage(KeyUsage.digitalSignature or KeyUsage.keyCertSign or KeyUsage.cRLSign), KeyUsage(KeyUsage.digitalSignature or KeyUsage.keyCertSign or KeyUsage.cRLSign),
KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_serverAuth,
KeyPurposeId.id_kp_clientAuth, 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 // 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), KeyUsage(KeyUsage.digitalSignature or KeyUsage.keyCertSign),
KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_serverAuth,
KeyPurposeId.id_kp_clientAuth, KeyPurposeId.id_kp_clientAuth,
KeyPurposeId.anyExtendedKeyUsage, KeyPurposeId.anyExtendedKeyUsage,
isCA = true isCA = true
),
CONFIDENTIAL_IDENTITY(
KeyUsage(KeyUsage.digitalSignature),
KeyPurposeId.id_kp_serverAuth,
KeyPurposeId.id_kp_clientAuth,
KeyPurposeId.anyExtendedKeyUsage,
isCA = false
) )
} }

View File

@ -21,7 +21,8 @@ data class DatabaseConfig(
val initialiseSchema: Boolean = true, val initialiseSchema: Boolean = true,
val serverNameTablePrefix: String = "", val serverNameTablePrefix: String = "",
val transactionIsolationLevel: TransactionIsolationLevel = TransactionIsolationLevel.REPEATABLE_READ, 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 // This class forms part of the node config and so any changes to it must be handled with care

View File

@ -17,8 +17,10 @@ import org.hibernate.type.AbstractSingleColumnStandardBasicType
import org.hibernate.type.descriptor.java.PrimitiveByteArrayTypeDescriptor import org.hibernate.type.descriptor.java.PrimitiveByteArrayTypeDescriptor
import org.hibernate.type.descriptor.sql.BlobTypeDescriptor import org.hibernate.type.descriptor.sql.BlobTypeDescriptor
import org.hibernate.type.descriptor.sql.VarbinaryTypeDescriptor import org.hibernate.type.descriptor.sql.VarbinaryTypeDescriptor
import java.lang.management.ManagementFactory
import java.sql.Connection import java.sql.Connection
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import javax.management.ObjectName
import javax.persistence.AttributeConverter import javax.persistence.AttributeConverter
class HibernateConfiguration( class HibernateConfiguration(
@ -65,9 +67,31 @@ class HibernateConfiguration(
val sessionFactory = buildSessionFactory(config, metadataSources, databaseConfig.serverNameTablePrefix) val sessionFactory = buildSessionFactory(config, metadataSources, databaseConfig.serverNameTablePrefix)
logger.info("Created session factory for schemas: $schemas") logger.info("Created session factory for schemas: $schemas")
// export Hibernate JMX statistics
if (databaseConfig.exportHibernateJMXStatistics)
initStatistics(sessionFactory)
return 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 { private fun buildSessionFactory(config: Configuration, metadataSources: MetadataSources, tablePrefix: String): SessionFactory {
config.standardServiceRegistryBuilder.applySettings(config.properties) config.standardServiceRegistryBuilder.applySettings(config.properties)
val metadata = metadataSources.getMetadataBuilder(config.standardServiceRegistryBuilder.build()).run { val metadata = metadataSources.getMetadataBuilder(config.standardServiceRegistryBuilder.build()).run {

View File

@ -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<String> {
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<String> {
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<String> {
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<String> {
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
}
}

View File

@ -27,9 +27,9 @@ import java.util.*
* transformation rules we create a mapping between those values and the values that exist on the * transformation rules we create a mapping between those values and the values that exist on the
* current class * 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). * 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 * @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 * 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 * @property ordinals Convenience mapping of constant to ordinality
@ -57,7 +57,7 @@ class EnumEvolutionSerializer(
* received AMQP header * received AMQP header
* @param new The Serializer object we built based on the current state of the enum class on our classpath * @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 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 * known at serialization time
*/ */
fun make(old: RestrictedType, fun make(old: RestrictedType,

View File

@ -32,17 +32,16 @@ sealed class PropertySerializer(val name: String, val readMethod: Method?, val r
return if (isInterface) listOf(SerializerFactory.nameForType(resolvedType)) else emptyList() return if (isInterface) listOf(SerializerFactory.nameForType(resolvedType)) else emptyList()
} }
private fun generateDefault(): String? { private fun generateDefault(): String? =
if (isJVMPrimitive) { if (isJVMPrimitive) {
return when (resolvedType) { when (resolvedType) {
java.lang.Boolean.TYPE -> "false" java.lang.Boolean.TYPE -> "false"
java.lang.Character.TYPE -> "&#0" java.lang.Character.TYPE -> "&#0"
else -> "0" else -> "0"
}
} else {
null
} }
} else {
return null
}
}
private fun generateMandatory(): Boolean { private fun generateMandatory(): Boolean {
return isJVMPrimitive || readMethod?.returnsNullable() == false return isJVMPrimitive || readMethod?.returnsNullable() == false

View File

@ -28,7 +28,7 @@ enum class TransformTypes(val build: (Annotation) -> Transform) : DescribedType
Unknown({ UnknownTransform() }) { Unknown({ UnknownTransform() }) {
override fun getDescriptor(): Any = DESCRIPTOR override fun getDescriptor(): Any = DESCRIPTOR
override fun getDescribed(): Any = ordinal override fun getDescribed(): Any = ordinal
override fun validate(l : List<Transform>, constants: Map<String, Int>) { } override fun validate(list: List<Transform>, constants: Map<String, Int>) {}
}, },
EnumDefault({ a -> EnumDefaultSchemaTransform((a as CordaSerializationTransformEnumDefault).old, a.new) }) { EnumDefault({ a -> EnumDefaultSchemaTransform((a as CordaSerializationTransformEnumDefault).old, a.new) }) {
override fun getDescriptor(): Any = DESCRIPTOR 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 * 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 * 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 * existing value
* @param constants The list of enum constants on the type the transforms are being applied to * @param constants The list of enum constants on the type the transforms are being applied to
*/ */
override fun validate(list : List<Transform>, constants: Map<String, Int>) { override fun validate(list: List<Transform>, constants: Map<String, Int>) {
uncheckedCast<List<Transform>, List<EnumDefaultSchemaTransform>>(list).forEach { uncheckedCast<List<Transform>, List<EnumDefaultSchemaTransform>>(list).forEach {
if (!constants.contains(it.new)) { if (!constants.contains(it.new)) {
throw NotSerializableException("Unknown enum constant ${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]!!) { if (constants[it.old]!! >= constants[it.new]!!) {
throw NotSerializableException( throw NotSerializableException(
"Enum extensions must default to older constants. ${it.new}[${constants[it.new]}] " + "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 * 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) * 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 * and old values
* @param constants The list of enum constants on the type the transforms are being applied to * @param constants The list of enum constants on the type the transforms are being applied to
*/ */
override fun validate(l : List<Transform>, constants: Map<String, Int>) { override fun validate(list: List<Transform>, constants: Map<String, Int>) {
object : Any() { object : Any() {
val from : MutableSet<String> = mutableSetOf() val from: MutableSet<String> = mutableSetOf()
val to : MutableSet<String> = mutableSetOf() }.apply { val to: MutableSet<String> = mutableSetOf()
@Suppress("UNCHECKED_CAST") (l as List<RenameSchemaTransform>).forEach { rename -> }.apply {
@Suppress("UNCHECKED_CAST") (list as List<RenameSchemaTransform>).forEach { rename ->
if (rename.to in this.to || rename.from in this.from) { if (rename.to in this.to || rename.from in this.from) {
throw NotSerializableException("Cyclic renames are not allowed (${rename.to})") 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<Transform>, constants: Map<String, Int>) abstract fun validate(list: List<Transform>, constants: Map<String, Int>)
companion object : DescribedTypeConstructor<TransformTypes> { companion object : DescribedTypeConstructor<TransformTypes> {
val DESCRIPTOR = AMQPDescriptorRegistry.TRANSFORM_ELEMENT_KEY.amqpDescriptor val DESCRIPTOR = AMQPDescriptorRegistry.TRANSFORM_ELEMENT_KEY.amqpDescriptor

View File

@ -92,7 +92,7 @@ class UnknownTestTransform(val a: Int, val b: Int, val c: Int) : Transform() {
companion object : DescribedTypeConstructor<UnknownTestTransform> { companion object : DescribedTypeConstructor<UnknownTestTransform> {
val typeName = "UnknownTest" val typeName = "UnknownTest"
override fun newInstance(obj: Any?) : UnknownTestTransform { override fun newInstance(obj: Any?): UnknownTestTransform {
val described = obj as List<*> val described = obj as List<*>
return UnknownTestTransform(described[1] as Int, described[2] as Int, described[3] as Int) return UnknownTestTransform(described[1] as Int, described[2] as Int, described[3] as Int)
} }
@ -201,41 +201,41 @@ data class TransformsSchema(val types: Map<String, EnumMap<TransformTypes, Mutab
* class loader and this dictates which classes we can and cannot see * class loader and this dictates which classes we can and cannot see
*/ */
fun get(name: String, sf: SerializerFactory) = sf.transformsCache.computeIfAbsent(name) { fun get(name: String, sf: SerializerFactory) = sf.transformsCache.computeIfAbsent(name) {
val transforms = EnumMap<TransformTypes, MutableList<Transform>>(TransformTypes::class.java) val transforms = EnumMap<TransformTypes, MutableList<Transform>>(TransformTypes::class.java)
try { try {
val clazz = sf.classloader.loadClass(name) val clazz = sf.classloader.loadClass(name)
supportedTransforms.forEach { transform -> supportedTransforms.forEach { transform ->
clazz.getAnnotation(transform.type)?.let { list -> clazz.getAnnotation(transform.type)?.let { list ->
transform.getAnnotations(list).forEach { annotation -> transform.getAnnotations(list).forEach { annotation ->
val t = transform.enum.build(annotation) val t = transform.enum.build(annotation)
// we're explicitly rejecting repeated annotations, whilst it's fine and we'd just // 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 // 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 // more than likely a typo in their code so best make it an actual error
if (transforms.computeIfAbsent(transform.enum) { mutableListOf() } if (transforms.computeIfAbsent(transform.enum) { mutableListOf() }
.filter { t == it } .filter { t == it }
.isNotEmpty()) { .isNotEmpty()) {
throw NotSerializableException( throw NotSerializableException(
"Repeated unique transformation annotation of type ${t.name}") "Repeated unique transformation annotation of type ${t.name}")
}
transforms[transform.enum]!!.add(t)
} }
transform.enum.validate( transforms[transform.enum]!!.add(t)
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 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( private fun getAndAdd(
type: String, type: String,
sf: SerializerFactory, sf: SerializerFactory,

View File

@ -2,7 +2,7 @@ package net.corda.nodeapi.internal.serialization
import net.corda.core.contracts.ContractAttachment import net.corda.core.contracts.ContractAttachment
import net.corda.core.serialization.* import net.corda.core.serialization.*
import net.corda.testing.SerializationEnvironmentRule import net.corda.testing.*
import net.corda.testing.contracts.DummyContract import net.corda.testing.contracts.DummyContract
import net.corda.testing.node.MockServices import net.corda.testing.node.MockServices
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
@ -22,9 +22,7 @@ class ContractAttachmentSerializerTest {
private lateinit var factory: SerializationFactory private lateinit var factory: SerializationFactory
private lateinit var context: SerializationContext private lateinit var context: SerializationContext
private lateinit var contextWithToken: SerializationContext private lateinit var contextWithToken: SerializationContext
private val mockServices = MockServices(rigorousMock(), MEGA_CORP.name)
private val mockServices = MockServices()
@Before @Before
fun setup() { fun setup() {
factory = testSerialization.env.serializationFactory factory = testSerialization.env.serializationFactory

View File

@ -6,7 +6,6 @@ import org.assertj.core.api.Assertions
import org.junit.Test import org.junit.Test
import java.io.File import java.io.File
import java.io.NotSerializableException import java.io.NotSerializableException
import java.net.URI
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertTrue import kotlin.test.assertTrue
@ -47,7 +46,7 @@ class EnumEvolvabilityTests {
@Test @Test
fun noAnnotation() { fun noAnnotation() {
data class C (val n: NotAnnotated) data class C(val n: NotAnnotated)
val sf = testDefaultFactory() val sf = testDefaultFactory()
val bAndS = TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(C(NotAnnotated.A)) val bAndS = TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(C(NotAnnotated.A))
@ -63,7 +62,7 @@ class EnumEvolvabilityTests {
@Test @Test
fun missingDefaults() { fun missingDefaults() {
data class C (val m: MissingDefaults) data class C(val m: MissingDefaults)
val sf = testDefaultFactory() val sf = testDefaultFactory()
val bAndS = TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(C(MissingDefaults.A)) val bAndS = TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(C(MissingDefaults.A))
@ -74,7 +73,7 @@ class EnumEvolvabilityTests {
@Test @Test
fun missingRenames() { fun missingRenames() {
data class C (val m: MissingRenames) data class C(val m: MissingRenames)
val sf = testDefaultFactory() val sf = testDefaultFactory()
val bAndS = TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(C(MissingRenames.A)) val bAndS = TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(C(MissingRenames.A))
@ -86,7 +85,7 @@ class EnumEvolvabilityTests {
@Test @Test
fun defaultAnnotationIsAddedToEnvelope() { fun defaultAnnotationIsAddedToEnvelope() {
data class C (val annotatedEnum: AnnotatedEnumOnce) data class C(val annotatedEnum: AnnotatedEnumOnce)
val sf = testDefaultFactory() val sf = testDefaultFactory()
val bAndS = TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(C(AnnotatedEnumOnce.D)) 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) // only the enum is decorated so schema sizes should be different (2 objects, only one evolved)
assertEquals(2, bAndS.schema.types.size) assertEquals(2, bAndS.schema.types.size)
assertEquals(1, bAndS.transformsSchema.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() val schema = bAndS.transformsSchema.types.values.first()
assertEquals(1, schema.size) assertEquals(1, schema.size)
assertTrue (schema.keys.contains(TransformTypes.EnumDefault)) assertTrue(schema.keys.contains(TransformTypes.EnumDefault))
assertEquals (1, schema[TransformTypes.EnumDefault]!!.size) assertEquals(1, schema[TransformTypes.EnumDefault]!!.size)
assertTrue (schema[TransformTypes.EnumDefault]!![0] is EnumDefaultSchemaTransform) assertTrue(schema[TransformTypes.EnumDefault]!![0] is EnumDefaultSchemaTransform)
assertEquals ("D", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).new) assertEquals("D", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).new)
assertEquals ("A", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).old) assertEquals("A", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).old)
} }
@Test @Test
fun doubleDefaultAnnotationIsAddedToEnvelope() { fun doubleDefaultAnnotationIsAddedToEnvelope() {
data class C (val annotatedEnum: AnnotatedEnumTwice) data class C(val annotatedEnum: AnnotatedEnumTwice)
val sf = testDefaultFactory() val sf = testDefaultFactory()
val bAndS = TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(C(AnnotatedEnumTwice.E)) val bAndS = TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(C(AnnotatedEnumTwice.E))
assertEquals(2, bAndS.schema.types.size) assertEquals(2, bAndS.schema.types.size)
assertEquals(1, bAndS.transformsSchema.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() val schema = bAndS.transformsSchema.types.values.first()
assertEquals(1, schema.size) assertEquals(1, schema.size)
assertTrue (schema.keys.contains(TransformTypes.EnumDefault)) assertTrue(schema.keys.contains(TransformTypes.EnumDefault))
assertEquals (2, schema[TransformTypes.EnumDefault]!!.size) assertEquals(2, schema[TransformTypes.EnumDefault]!!.size)
assertTrue (schema[TransformTypes.EnumDefault]!![0] is EnumDefaultSchemaTransform) assertTrue(schema[TransformTypes.EnumDefault]!![0] is EnumDefaultSchemaTransform)
assertEquals ("E", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).new) assertEquals("E", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).new)
assertEquals ("D", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).old) assertEquals("D", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).old)
assertTrue (schema[TransformTypes.EnumDefault]!![1] is EnumDefaultSchemaTransform) assertTrue(schema[TransformTypes.EnumDefault]!![1] is EnumDefaultSchemaTransform)
assertEquals ("D", (schema[TransformTypes.EnumDefault]!![1] as EnumDefaultSchemaTransform).new) assertEquals("D", (schema[TransformTypes.EnumDefault]!![1] as EnumDefaultSchemaTransform).new)
assertEquals ("A", (schema[TransformTypes.EnumDefault]!![1] as EnumDefaultSchemaTransform).old) assertEquals("A", (schema[TransformTypes.EnumDefault]!![1] as EnumDefaultSchemaTransform).old)
} }
@Test @Test
fun defaultAnnotationIsAddedToEnvelopeAndDeserialised() { fun defaultAnnotationIsAddedToEnvelopeAndDeserialised() {
data class C (val annotatedEnum: AnnotatedEnumOnce) data class C(val annotatedEnum: AnnotatedEnumOnce)
val sf = testDefaultFactory() val sf = testDefaultFactory()
val sb = TestSerializationOutput(VERBOSE, sf).serialize(C(AnnotatedEnumOnce.D)) val sb = TestSerializationOutput(VERBOSE, sf).serialize(C(AnnotatedEnumOnce.D))
@ -152,11 +151,11 @@ class EnumEvolvabilityTests {
val schema = transforms[eName] val schema = transforms[eName]
assertTrue (schema!!.keys.contains(TransformTypes.EnumDefault)) assertTrue(schema!!.keys.contains(TransformTypes.EnumDefault))
assertEquals (1, schema[TransformTypes.EnumDefault]!!.size) assertEquals(1, schema[TransformTypes.EnumDefault]!!.size)
assertTrue (schema[TransformTypes.EnumDefault]!![0] is EnumDefaultSchemaTransform) assertTrue(schema[TransformTypes.EnumDefault]!![0] is EnumDefaultSchemaTransform)
assertEquals ("D", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).new) assertEquals("D", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).new)
assertEquals ("A", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).old) assertEquals("A", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).old)
} }
@Test @Test
@ -174,9 +173,9 @@ class EnumEvolvabilityTests {
val transforms = db.envelope.transformsSchema.types val transforms = db.envelope.transformsSchema.types
assertTrue (transforms.contains(AnnotatedEnumTwice::class.java.name)) assertTrue(transforms.contains(AnnotatedEnumTwice::class.java.name))
assertTrue (transforms[AnnotatedEnumTwice::class.java.name]!!.contains(TransformTypes.EnumDefault)) assertTrue(transforms[AnnotatedEnumTwice::class.java.name]!!.contains(TransformTypes.EnumDefault))
assertEquals (2, transforms[AnnotatedEnumTwice::class.java.name]!![TransformTypes.EnumDefault]!!.size) assertEquals(2, transforms[AnnotatedEnumTwice::class.java.name]!![TransformTypes.EnumDefault]!!.size)
val enumDefaults = transforms[AnnotatedEnumTwice::class.java.name]!![TransformTypes.EnumDefault]!! val enumDefaults = transforms[AnnotatedEnumTwice::class.java.name]!![TransformTypes.EnumDefault]!!
@ -188,7 +187,7 @@ class EnumEvolvabilityTests {
@Test @Test
fun renameAnnotationIsAdded() { fun renameAnnotationIsAdded() {
data class C (val annotatedEnum: RenameEnumOnce) data class C(val annotatedEnum: RenameEnumOnce)
val sf = testDefaultFactory() val sf = testDefaultFactory()
@ -197,7 +196,7 @@ class EnumEvolvabilityTests {
assertEquals(2, bAndS.schema.types.size) assertEquals(2, bAndS.schema.types.size)
assertEquals(1, bAndS.transformsSchema.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]!! val serialisedSchema = bAndS.transformsSchema.types[RenameEnumOnce::class.java.name]!!
@ -212,7 +211,7 @@ class EnumEvolvabilityTests {
assertEquals(2, cAndS.envelope.schema.types.size) assertEquals(2, cAndS.envelope.schema.types.size)
assertEquals(1, cAndS.envelope.transformsSchema.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]!! val deserialisedSchema = cAndS.envelope.transformsSchema.types[RenameEnumOnce::class.java.name]!!
@ -232,7 +231,7 @@ class EnumEvolvabilityTests {
@Test @Test
fun doubleRenameAnnotationIsAdded() { fun doubleRenameAnnotationIsAdded() {
data class C (val annotatedEnum: RenameEnumTwice) data class C(val annotatedEnum: RenameEnumTwice)
val sf = testDefaultFactory() val sf = testDefaultFactory()
@ -241,7 +240,7 @@ class EnumEvolvabilityTests {
assertEquals(2, bAndS.schema.types.size) assertEquals(2, bAndS.schema.types.size)
assertEquals(1, bAndS.transformsSchema.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]!! val serialisedSchema = bAndS.transformsSchema.types[RenameEnumTwice::class.java.name]!!
@ -258,7 +257,7 @@ class EnumEvolvabilityTests {
assertEquals(2, cAndS.envelope.schema.types.size) assertEquals(2, cAndS.envelope.schema.types.size)
assertEquals(1, cAndS.envelope.transformsSchema.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]!! 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) assertEquals("F", (deserialisedSchema[TransformTypes.Rename]!![1] as RenameSchemaTransform).to)
} }
@CordaSerializationTransformRename(from="A", to="X") @CordaSerializationTransformRename(from = "A", to = "X")
@CordaSerializationTransformEnumDefault(old = "X", new="E") @CordaSerializationTransformEnumDefault(old = "X", new = "E")
enum class RenameAndExtendEnum { enum class RenameAndExtendEnum {
X, B, C, D, E X, B, C, D, E
} }
@Test @Test
fun bothAnnotationTypes() { fun bothAnnotationTypes() {
data class C (val annotatedEnum: RenameAndExtendEnum) data class C(val annotatedEnum: RenameAndExtendEnum)
val sf = testDefaultFactory() val sf = testDefaultFactory()
@ -288,15 +287,15 @@ class EnumEvolvabilityTests {
assertEquals(2, bAndS.schema.types.size) assertEquals(2, bAndS.schema.types.size)
assertEquals(1, bAndS.transformsSchema.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]!! val serialisedSchema = bAndS.transformsSchema.types[RenameAndExtendEnum::class.java.name]!!
// This time there should be two distinct transform types (all previous tests have had only // This time there should be two distinct transform types (all previous tests have had only
// a single type // a single type
assertEquals(2, serialisedSchema.size) assertEquals(2, serialisedSchema.size)
assertTrue (serialisedSchema.containsKey(TransformTypes.Rename)) assertTrue(serialisedSchema.containsKey(TransformTypes.Rename))
assertTrue (serialisedSchema.containsKey(TransformTypes.EnumDefault)) assertTrue(serialisedSchema.containsKey(TransformTypes.EnumDefault))
assertEquals(1, serialisedSchema[TransformTypes.Rename]!!.size) assertEquals(1, serialisedSchema[TransformTypes.Rename]!!.size)
assertEquals("A", (serialisedSchema[TransformTypes.Rename]!![0] as RenameSchemaTransform).from) assertEquals("A", (serialisedSchema[TransformTypes.Rename]!![0] as RenameSchemaTransform).from)
@ -307,7 +306,7 @@ class EnumEvolvabilityTests {
assertEquals("X", (serialisedSchema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).old) assertEquals("X", (serialisedSchema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).old)
} }
@CordaSerializationTransformEnumDefaults ( @CordaSerializationTransformEnumDefaults(
CordaSerializationTransformEnumDefault("D", "A"), CordaSerializationTransformEnumDefault("D", "A"),
CordaSerializationTransformEnumDefault("D", "A")) CordaSerializationTransformEnumDefault("D", "A"))
enum class RepeatedAnnotation { enum class RepeatedAnnotation {
@ -316,7 +315,7 @@ class EnumEvolvabilityTests {
@Test @Test
fun repeatedAnnotation() { fun repeatedAnnotation() {
data class C (val a: RepeatedAnnotation) data class C(val a: RepeatedAnnotation)
val sf = testDefaultFactory() val sf = testDefaultFactory()
@ -330,40 +329,40 @@ class EnumEvolvabilityTests {
A, B, C, D A, B, C, D
} }
@CordaSerializationTransformEnumDefaults ( @CordaSerializationTransformEnumDefaults(
CordaSerializationTransformEnumDefault("D", "A"), CordaSerializationTransformEnumDefault("D", "A"),
CordaSerializationTransformEnumDefault("E", "A")) CordaSerializationTransformEnumDefault("E", "A"))
enum class E2 { enum class E2 {
A, B, C, D, E A, B, C, D, E
} }
@CordaSerializationTransformEnumDefaults (CordaSerializationTransformEnumDefault("D", "A")) @CordaSerializationTransformEnumDefaults(CordaSerializationTransformEnumDefault("D", "A"))
enum class E3 { enum class E3 {
A, B, C, D A, B, C, D
} }
@Test @Test
fun multiEnums() { fun multiEnums() {
data class A (val a: E1, val b: E2) data class A(val a: E1, val b: E2)
data class B (val a: E3, val b: A, val c: E1) 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 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() val sf = testDefaultFactory()
// Serialise the object // Serialise the object
val bAndS = TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(c) 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 // we have six types and three of those, the enums, should have transforms
assertEquals(6, bAndS.schema.types.size) assertEquals(6, bAndS.schema.types.size)
assertEquals(3, bAndS.transformsSchema.types.size) assertEquals(3, bAndS.transformsSchema.types.size)
assertTrue (E1::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(E2::class.java.name in bAndS.transformsSchema.types)
assertTrue (E3::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 e1S = bAndS.transformsSchema.types[E1::class.java.name]!!
val e2S = bAndS.transformsSchema.types[E2::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(C2::class.java.name))
assertTrue(sf.transformsCache.containsKey(AnnotatedEnumOnce::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]) sb2.transformsSchema.types[AnnotatedEnumOnce::class.java.name])
} }
@ -447,7 +446,7 @@ class EnumEvolvabilityTests {
// //
// And we're not at 3. However, we ban this rename // And we're not at 3. However, we ban this rename
// //
@CordaSerializationTransformRenames ( @CordaSerializationTransformRenames(
CordaSerializationTransformRename("D", "C"), CordaSerializationTransformRename("D", "C"),
CordaSerializationTransformRename("C", "D") CordaSerializationTransformRename("C", "D")
) )
@ -455,7 +454,7 @@ class EnumEvolvabilityTests {
@Test @Test
fun rejectCyclicRename() { fun rejectCyclicRename() {
data class C (val e: RejectCyclicRename) data class C(val e: RejectCyclicRename)
val sf = testDefaultFactory() val sf = testDefaultFactory()
Assertions.assertThatThrownBy { Assertions.assertThatThrownBy {
@ -468,7 +467,7 @@ class EnumEvolvabilityTests {
// unserailzble. However, in this case, it isn't a struct cycle, rather one element // 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 // is renamed to match what a different element used to be called
// //
@CordaSerializationTransformRenames ( @CordaSerializationTransformRenames(
CordaSerializationTransformRename(from = "B", to = "C"), CordaSerializationTransformRename(from = "B", to = "C"),
CordaSerializationTransformRename(from = "C", to = "D") CordaSerializationTransformRename(from = "C", to = "D")
) )
@ -476,7 +475,7 @@ class EnumEvolvabilityTests {
@Test @Test
fun rejectCyclicRenameAlt() { fun rejectCyclicRenameAlt() {
data class C (val e: RejectCyclicRenameAlt) data class C(val e: RejectCyclicRenameAlt)
val sf = testDefaultFactory() val sf = testDefaultFactory()
Assertions.assertThatThrownBy { Assertions.assertThatThrownBy {
@ -484,7 +483,7 @@ class EnumEvolvabilityTests {
}.isInstanceOf(NotSerializableException::class.java) }.isInstanceOf(NotSerializableException::class.java)
} }
@CordaSerializationTransformRenames ( @CordaSerializationTransformRenames(
CordaSerializationTransformRename("G", "C"), CordaSerializationTransformRename("G", "C"),
CordaSerializationTransformRename("F", "G"), CordaSerializationTransformRename("F", "G"),
CordaSerializationTransformRename("E", "F"), CordaSerializationTransformRename("E", "F"),
@ -495,7 +494,7 @@ class EnumEvolvabilityTests {
@Test @Test
fun rejectCyclicRenameRedux() { fun rejectCyclicRenameRedux() {
data class C (val e: RejectCyclicRenameRedux) data class C(val e: RejectCyclicRenameRedux)
val sf = testDefaultFactory() val sf = testDefaultFactory()
Assertions.assertThatThrownBy { Assertions.assertThatThrownBy {
@ -503,12 +502,12 @@ class EnumEvolvabilityTests {
}.isInstanceOf(NotSerializableException::class.java) }.isInstanceOf(NotSerializableException::class.java)
} }
@CordaSerializationTransformEnumDefault (new = "D", old = "X") @CordaSerializationTransformEnumDefault(new = "D", old = "X")
enum class RejectBadDefault { A, B, C, D } enum class RejectBadDefault { A, B, C, D }
@Test @Test
fun rejectBadDefault() { fun rejectBadDefault() {
data class C (val e: RejectBadDefault) data class C(val e: RejectBadDefault)
val sf = testDefaultFactory() val sf = testDefaultFactory()
Assertions.assertThatThrownBy { Assertions.assertThatThrownBy {
@ -516,12 +515,12 @@ class EnumEvolvabilityTests {
}.isInstanceOf(NotSerializableException::class.java) }.isInstanceOf(NotSerializableException::class.java)
} }
@CordaSerializationTransformEnumDefault (new = "D", old = "D") @CordaSerializationTransformEnumDefault(new = "D", old = "D")
enum class RejectBadDefaultToSelf { A, B, C, D } enum class RejectBadDefaultToSelf { A, B, C, D }
@Test @Test
fun rejectBadDefaultToSelf() { fun rejectBadDefaultToSelf() {
data class C (val e: RejectBadDefaultToSelf) data class C(val e: RejectBadDefaultToSelf)
val sf = testDefaultFactory() val sf = testDefaultFactory()
Assertions.assertThatThrownBy { Assertions.assertThatThrownBy {

View File

@ -1,12 +1,13 @@
package net.corda.nodeapi.internal.serialization.amqp 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 net.corda.testing.common.internal.ProjectStructure.projectRootDir
import org.assertj.core.api.Assertions import org.assertj.core.api.Assertions
import org.junit.Test import org.junit.Test
import java.io.File import java.io.File
import java.io.NotSerializableException import java.io.NotSerializableException
import java.net.URI
import kotlin.test.assertEquals import kotlin.test.assertEquals
// NOTE: To recreate the test files used by these tests uncomment the original test classes and comment // 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 resource = "${javaClass.simpleName}.${testName()}"
val sf = testDefaultFactory() val sf = testDefaultFactory()
data class C (val e : DeserializeNewerSetToUnknown) data class C(val e: DeserializeNewerSetToUnknown)
// Uncomment to re-generate test files // Uncomment to re-generate test files
// File(URI("$localPath/$resource")).writeBytes( // File(URI("$localPath/$resource")).writeBytes(
@ -40,7 +41,7 @@ class EnumEvolveTests {
val obj = DeserializationInput(sf).deserialize(SerializedBytes<C>(File(path.toURI()).readBytes())) val obj = DeserializationInput(sf).deserialize(SerializedBytes<C>(File(path.toURI()).readBytes()))
assertEquals (DeserializeNewerSetToUnknown.C, obj.e) assertEquals(DeserializeNewerSetToUnknown.C, obj.e)
} }
// Version of the class as it was serialised // Version of the class as it was serialised
@ -78,9 +79,9 @@ class EnumEvolveTests {
// of the evolution code // of the evolution code
val obj3 = DeserializationInput(sf).deserialize(SerializedBytes<C>(File(path3.toURI()).readBytes())) val obj3 = DeserializationInput(sf).deserialize(SerializedBytes<C>(File(path3.toURI()).readBytes()))
assertEquals (DeserializeNewerSetToUnknown2.C, obj1.e) assertEquals(DeserializeNewerSetToUnknown2.C, obj1.e)
assertEquals (DeserializeNewerSetToUnknown2.C, obj2.e) assertEquals(DeserializeNewerSetToUnknown2.C, obj2.e)
assertEquals (DeserializeNewerSetToUnknown2.C, obj3.e) assertEquals(DeserializeNewerSetToUnknown2.C, obj3.e)
} }
@ -149,7 +150,7 @@ class EnumEvolveTests {
data class C(val e: DeserializeWithRename) data class C(val e: DeserializeWithRename)
// Uncomment to re-generate test files, needs to be done in three stages // Uncomment to re-generate test files, needs to be done in three stages
val so = SerializationOutput(sf) // val so = SerializationOutput(sf)
// First change // First change
// File(URI("$localPath/$resource.1.AA")).writeBytes(so.serialize(C(DeserializeWithRename.AA)).bytes) // 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) // 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) data class C(val e: MultiOperations)
// Uncomment to re-generate test files, needs to be done in three stages // Uncomment to re-generate test files, needs to be done in three stages
val so = SerializationOutput(sf) // val so = SerializationOutput(sf)
// First change // First change
// File(URI("$localPath/$resource.1.A")).writeBytes(so.serialize(C(MultiOperations.A)).bytes) // 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) // 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)) Pair("$resource.5.G", MultiOperations.C))
fun load(l: List<Pair<String, MultiOperations>>) = l.map { fun load(l: List<Pair<String, MultiOperations>>) = l.map {
Pair (DeserializationInput(sf).deserialize(SerializedBytes<C>( Pair(DeserializationInput(sf).deserialize(SerializedBytes<C>(
File(EvolvabilityTests::class.java.getResource(it.first).toURI()).readBytes())), it.second) File(EvolvabilityTests::class.java.getResource(it.first).toURI()).readBytes())), it.second)
} }
load (stage1Resources).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(stage2Resources).forEach { assertEquals(it.second, it.first.e) }
load (stage3Resources).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(stage4Resources).forEach { assertEquals(it.second, it.first.e) }
load (stage5Resources).forEach { assertEquals(it.second, it.first.e) } load(stage5Resources).forEach { assertEquals(it.second, it.first.e) }
} }
@CordaSerializationTransformEnumDefault(old = "A", new = "F") @CordaSerializationTransformEnumDefault(old = "A", new = "F")
@ -363,7 +364,7 @@ class EnumEvolveTests {
fun badNewValue() { fun badNewValue() {
val sf = testDefaultFactory() val sf = testDefaultFactory()
data class C (val e : BadNewValue) data class C(val e: BadNewValue)
Assertions.assertThatThrownBy { Assertions.assertThatThrownBy {
SerializationOutput(sf).serialize(C(BadNewValue.A)) SerializationOutput(sf).serialize(C(BadNewValue.A))
@ -374,13 +375,13 @@ class EnumEvolveTests {
CordaSerializationTransformEnumDefault(new = "D", old = "E"), CordaSerializationTransformEnumDefault(new = "D", old = "E"),
CordaSerializationTransformEnumDefault(new = "E", old = "A") CordaSerializationTransformEnumDefault(new = "E", old = "A")
) )
enum class OutOfOrder { A, B, C, D, E} enum class OutOfOrder { A, B, C, D, E }
@Test @Test
fun outOfOrder() { fun outOfOrder() {
val sf = testDefaultFactory() val sf = testDefaultFactory()
data class C (val e : OutOfOrder) data class C(val e: OutOfOrder)
Assertions.assertThatThrownBy { Assertions.assertThatThrownBy {
SerializationOutput(sf).serialize(C(OutOfOrder.A)) SerializationOutput(sf).serialize(C(OutOfOrder.A))

View File

@ -2,17 +2,14 @@ package net.corda.nodeapi.internal.serialization.amqp
import net.corda.core.serialization.ClassWhitelist import net.corda.core.serialization.ClassWhitelist
import net.corda.core.serialization.CordaSerializable 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 net.corda.core.serialization.SerializedBytes
import org.assertj.core.api.Assertions 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 { class EnumTests {
enum class Bras { enum class Bras {
@ -42,8 +39,8 @@ class EnumTests {
//} //}
// the new state, note in the test we serialised with value UNDERWIRE so the spacer // 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 // occurring after this won't have changed the ordinality of our serialised value
// and thus should still be deserialisable // and thus should still be deserializable
enum class OldBras2 { enum class OldBras2 {
TSHIRT, UNDERWIRE, PUSHUP, SPACER, BRALETTE, SPACER2 TSHIRT, UNDERWIRE, PUSHUP, SPACER, BRALETTE, SPACER2
} }

View File

@ -6,7 +6,6 @@ import net.corda.testing.common.internal.ProjectStructure.projectRootDir
import org.junit.Test import org.junit.Test
import java.io.File import java.io.File
import java.io.NotSerializableException import java.io.NotSerializableException
import java.net.URI
import kotlin.test.assertEquals import kotlin.test.assertEquals
// To regenerate any of the binary test files do the following // 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 // 5. Comment back out the generation code and uncomment the actual test
class EvolvabilityTests { class EvolvabilityTests {
// When regenerating the test files this needs to be set to the file system location of the resource files // 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( var localPath = projectRootDir.toUri().resolve(
"node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp") "node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp")
@Test @Test
fun simpleOrderSwapSameType() { fun simpleOrderSwapSameType() {
val sf = testDefaultFactory() val sf = testDefaultFactory()
val resource= "EvolvabilityTests.simpleOrderSwapSameType" val resource = "EvolvabilityTests.simpleOrderSwapSameType"
val A = 1 val A = 1
val B = 2 val B = 2
@ -91,7 +91,7 @@ class EvolvabilityTests {
assertEquals(A, deserializedC.a) assertEquals(A, deserializedC.a)
assertEquals(null, deserializedC.b) assertEquals(null, deserializedC.b)
} }
@Test(expected = NotSerializableException::class) @Test(expected = NotSerializableException::class)
fun addAdditionalParam() { fun addAdditionalParam() {
@ -370,6 +370,7 @@ class EvolvabilityTests {
// Add a parameter to inner but keep outer unchanged // Add a parameter to inner but keep outer unchanged
data class Inner(val a: Int, val b: String?) data class Inner(val a: Int, val b: String?)
data class Outer(val a: Int, val b: Inner) data class Outer(val a: Int, val b: Inner)
val path = EvolvabilityTests::class.java.getResource(resource) val path = EvolvabilityTests::class.java.getResource(resource)

View File

@ -44,6 +44,11 @@ sourceSets {
// This prevents problems in IntelliJ with regard to duplicate source roots. // This prevents problems in IntelliJ with regard to duplicate source roots.
processResources { processResources {
from file("$rootDir/config/dev/log4j2.xml") 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 // To find potential version conflicts, run "gradle htmlDependencyReport" and then look in
@ -163,6 +168,9 @@ dependencies {
// FastClasspathScanner: classpath scanning // FastClasspathScanner: classpath scanning
compile 'io.github.lukehutch:fast-classpath-scanner:2.0.21' 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 // Jsh: A SSH implementation for tunneling inbound traffic via a relay
compile group: 'com.jcraft', name: 'jsch', version: '0.1.54' 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.core:jersey-server:${jersey_version}"
testCompile "org.glassfish.jersey.containers:jersey-container-servlet-core:${jersey_version}" testCompile "org.glassfish.jersey.containers:jersey-container-servlet-core:${jersey_version}"
testCompile "org.glassfish.jersey.containers:jersey-container-jetty-http:${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) { task integrationTest(type: Test) {

View File

@ -42,7 +42,7 @@ task buildCordaJAR(type: FatCapsule, dependsOn: project(':node').compileJava) {
capsuleManifest { capsuleManifest {
applicationVersion = corda_release_version 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. // 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**)" 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}"] javaAgents = ["quasar-core-${quasar_version}-jdk8.jar=${quasarExcludeExpression}"]

View File

@ -42,7 +42,7 @@ class BootTests : IntegrationTest() {
fun `double node start doesn't write into log file`() { fun `double node start doesn't write into log file`() {
val logConfigFile = projectRootDir / "config" / "dev" / "log4j2.xml" val logConfigFile = projectRootDir / "config" / "dev" / "log4j2.xml"
assertThat(logConfigFile).isRegularFile() assertThat(logConfigFile).isRegularFile()
driver(isDebug = true, extraSystemProperties = mapOf("log4j.configurationFile" to logConfigFile.toString())) { driver(isDebug = true, systemProperties = mapOf("log4j.configurationFile" to logConfigFile.toString())) {
val alice = startNode(providedName = ALICE.name).get() val alice = startNode(providedName = ALICE.name).get()
val logFolder = alice.configuration.baseDirectory / NodeStartup.LOGS_DIRECTORY_NAME val logFolder = alice.configuration.baseDirectory / NodeStartup.LOGS_DIRECTORY_NAME
val logFile = logFolder.toFile().listFiles { _, name -> name.endsWith(".log") }.single() val logFile = logFolder.toFile().listFiles { _, name -> name.endsWith(".log") }.single()

View File

@ -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)
}
}
}
}

View File

@ -19,6 +19,7 @@ import net.corda.testing.*
import net.corda.testing.driver.NodeHandle import net.corda.testing.driver.NodeHandle
import net.corda.testing.driver.PortAllocation import net.corda.testing.driver.PortAllocation
import net.corda.testing.driver.driver 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.div
import net.corda.testing.internal.performance.startPublishingFixedRateInjector import net.corda.testing.internal.performance.startPublishingFixedRateInjector
import net.corda.testing.internal.performance.startReporter import net.corda.testing.internal.performance.startReporter
@ -100,7 +101,7 @@ class NodePerformanceTests : IntegrationTest() {
driver(startNodesInProcess = true) { driver(startNodesInProcess = true) {
val a = startNode(rpcUsers = listOf(User("A", "A", setOf(startFlow<EmptyFlow>())))).get() val a = startNode(rpcUsers = listOf(User("A", "A", setOf(startFlow<EmptyFlow>())))).get()
a as NodeHandle.InProcess 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 -> a.rpcClientToNode().use("A", "A") { connection ->
startPublishingFixedRateInjector(metricRegistry, 1, 5.minutes, 2000L / TimeUnit.SECONDS) { startPublishingFixedRateInjector(metricRegistry, 1, 5.minutes, 2000L / TimeUnit.SECONDS) {
connection.proxy.startFlow(::EmptyFlow).returnValue.get() connection.proxy.startFlow(::EmptyFlow).returnValue.get()
@ -114,7 +115,7 @@ class NodePerformanceTests : IntegrationTest() {
driver(startNodesInProcess = true, extraCordappPackagesToScan = listOf("net.corda.finance")) { driver(startNodesInProcess = true, extraCordappPackagesToScan = listOf("net.corda.finance")) {
val a = startNode(rpcUsers = listOf(User("A", "A", setOf(startFlow<CashIssueFlow>())))).get() val a = startNode(rpcUsers = listOf(User("A", "A", setOf(startFlow<CashIssueFlow>())))).get()
a as NodeHandle.InProcess 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 -> a.rpcClientToNode().use("A", "A") { connection ->
startPublishingFixedRateInjector(metricRegistry, 1, 5.minutes, 2000L / TimeUnit.SECONDS) { startPublishingFixedRateInjector(metricRegistry, 1, 5.minutes, 2000L / TimeUnit.SECONDS) {
connection.proxy.startFlow(::CashIssueFlow, 1.DOLLARS, OpaqueBytes.of(0), ALICE).returnValue.get() connection.proxy.startFlow(::CashIssueFlow, 1.DOLLARS, OpaqueBytes.of(0), ALICE).returnValue.get()
@ -133,7 +134,7 @@ class NodePerformanceTests : IntegrationTest() {
portAllocation = PortAllocation.Incremental(20000) portAllocation = PortAllocation.Incremental(20000)
) { ) {
val notary = defaultNotaryNode.getOrThrow() as NodeHandle.InProcess 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 -> notary.rpcClientToNode().use("A", "A") { connection ->
println("ISSUING") println("ISSUING")
val doneFutures = (1..100).toList().map { val doneFutures = (1..100).toList().map {
@ -158,7 +159,7 @@ class NodePerformanceTests : IntegrationTest() {
portAllocation = PortAllocation.Incremental(20000) portAllocation = PortAllocation.Incremental(20000)
) { ) {
val notary = defaultNotaryNode.getOrThrow() as NodeHandle.InProcess 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 -> notary.rpcClientToNode().use("A", "A") { connection ->
connection.proxy.startFlow(::CashIssueFlow, 1.DOLLARS, OpaqueBytes.of(0), defaultNotaryIdentity).returnValue.getOrThrow() connection.proxy.startFlow(::CashIssueFlow, 1.DOLLARS, OpaqueBytes.of(0), defaultNotaryIdentity).returnValue.getOrThrow()
connection.proxy.startFlow(::CashPaymentFlow, 1.DOLLARS, defaultNotaryIdentity).returnValue.getOrThrow() connection.proxy.startFlow(::CashPaymentFlow, 1.DOLLARS, defaultNotaryIdentity).returnValue.getOrThrow()

View File

@ -23,6 +23,7 @@ import kotlin.test.fail
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.junit.ClassRule import org.junit.ClassRule
import java.util.regex.Pattern import java.util.regex.Pattern
import kotlin.reflect.jvm.jvmName
class SSHServerTest : IntegrationTest() { class SSHServerTest : IntegrationTest() {
companion object { companion object {
@ -120,7 +121,7 @@ class SSHServerTest : IntegrationTest() {
channel.disconnect() channel.disconnect()
session.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 .*")
} }
} }

View File

@ -21,10 +21,7 @@ import net.corda.core.utilities.getOrThrow
import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.internal.cordapp.CordappLoader
import net.corda.node.internal.cordapp.CordappProviderImpl import net.corda.node.internal.cordapp.CordappProviderImpl
import net.corda.testing.* import net.corda.testing.*
import net.corda.testing.DUMMY_BANK_A import net.corda.testing.driver.DriverDSL
import net.corda.testing.DUMMY_NOTARY
import net.corda.testing.IntegrationTest
import net.corda.testing.driver.DriverDSLExposedInterface
import net.corda.testing.driver.NodeHandle import net.corda.testing.driver.NodeHandle
import net.corda.testing.driver.driver import net.corda.testing.driver.driver
import net.corda.testing.node.MockAttachmentStorage import net.corda.testing.node.MockAttachmentStorage
@ -57,16 +54,16 @@ class AttachmentLoadingTests : IntegrationTest() {
Class.forName("net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Initiator", true, URLClassLoader(arrayOf(isolatedJAR))) Class.forName("net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Initiator", true, URLClassLoader(arrayOf(isolatedJAR)))
.asSubclass(FlowLogic::class.java) .asSubclass(FlowLogic::class.java)
private fun DriverDSLExposedInterface.createTwoNodes(): List<NodeHandle> { private fun DriverDSL.createTwoNodes(): List<NodeHandle> {
return listOf( return listOf(
startNode(providedName = bankAName), startNode(providedName = bankAName),
startNode(providedName = bankBName) startNode(providedName = bankBName)
).transpose().getOrThrow() ).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. // 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") logger.info("Installing isolated jar to $path")
isolatedJAR.openStream().buffered().use { input -> isolatedJAR.openStream().buffered().use { input ->
Files.newOutputStream(path).buffered().use { output -> Files.newOutputStream(path).buffered().use { output ->

View File

@ -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<Node>
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<Unit>() {
@Suspendable
override fun call() = Unit
}
}
class UserAuthServiceEmbedded : UserAuthServiceTest() {
private val rpcUser = User("user", "foo", permissions = setOf(
Permissions.startFlow<DummyFlow>(),
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<DummyFlow>(),
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
override fun tearDown() {
db.close()
}
}
private data class UserAndRoles(val username: String, val password: String, val roles: List<String>)
private data class RoleAndPermissions(val role: String, val permissions: List<String>)
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<UserAndRoles> = emptyList(),
roleAndPermissions: List<RoleAndPermissions> = 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")
}
}
}
}

View File

@ -19,7 +19,7 @@ import net.corda.node.services.messaging.ReceivedMessage
import net.corda.node.services.messaging.send import net.corda.node.services.messaging.send
import net.corda.node.services.transactions.RaftValidatingNotaryService import net.corda.node.services.transactions.RaftValidatingNotaryService
import net.corda.testing.* 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.NodeHandle
import net.corda.testing.driver.driver import net.corda.testing.driver.driver
import net.corda.testing.node.ClusterSpec import net.corda.testing.node.ClusterSpec
@ -113,13 +113,13 @@ class P2PMessagingTest : IntegrationTest() {
} }
} }
private fun startDriverWithDistributedService(dsl: DriverDSLExposedInterface.(List<StartedNode<Node>>) -> Unit) { private fun startDriverWithDistributedService(dsl: DriverDSL.(List<StartedNode<Node>>) -> Unit) {
driver(startNodesInProcess = true, notarySpecs = listOf(NotarySpec(DISTRIBUTED_SERVICE_NAME, cluster = ClusterSpec.Raft(clusterSize = 2)))) { driver(startNodesInProcess = true, notarySpecs = listOf(NotarySpec(DISTRIBUTED_SERVICE_NAME, cluster = ClusterSpec.Raft(clusterSize = 2)))) {
dsl(defaultNotaryHandle.nodeHandles.getOrThrow().map { (it as NodeHandle.InProcess).node }) dsl(defaultNotaryHandle.nodeHandles.getOrThrow().map { (it as NodeHandle.InProcess).node })
} }
} }
private fun DriverDSLExposedInterface.startAlice(): StartedNode<Node> { private fun DriverDSL.startAlice(): StartedNode<Node> {
return startNode(providedName = ALICE.name, customOverrides = mapOf("messageRedeliveryDelaySeconds" to 1)) return startNode(providedName = ALICE.name, customOverrides = mapOf("messageRedeliveryDelaySeconds" to 1))
.map { (it as NodeHandle.InProcess).node } .map { (it as NodeHandle.InProcess).node }
.getOrThrow() .getOrThrow()

View File

@ -39,7 +39,7 @@ import net.corda.node.internal.cordapp.CordappProviderInternal
import net.corda.node.services.ContractUpgradeHandler import net.corda.node.services.ContractUpgradeHandler
import net.corda.node.services.FinalityHandler import net.corda.node.services.FinalityHandler
import net.corda.node.services.NotaryChangeHandler 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.api.*
import net.corda.node.services.config.BFTSMaRtConfiguration import net.corda.node.services.config.BFTSMaRtConfiguration
import net.corda.node.services.config.NodeConfiguration 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.config.configureWithDevSSLCertificate
import net.corda.node.services.events.NodeSchedulerService import net.corda.node.services.events.NodeSchedulerService
import net.corda.node.services.events.ScheduledActivityObserver 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.identity.PersistentIdentityService
import net.corda.node.services.keys.PersistentKeyManagementService import net.corda.node.services.keys.PersistentKeyManagementService
import net.corda.node.services.messaging.MessagingService import net.corda.node.services.messaging.MessagingService
@ -138,7 +139,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
protected val _nodeReadyFuture = openFuture<Unit>() protected val _nodeReadyFuture = openFuture<Unit>()
protected val networkMapClient: NetworkMapClient? by lazy { configuration.compatibilityZoneURL?.let(::NetworkMapClient) } 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 /** Completes once the node has successfully registered with the network map service
* or has loaded network map data from local database */ * 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" } check(started == null) { "Node has already been started" }
log.info("Generating nodeInfo ...") log.info("Generating nodeInfo ...")
initCertificate() initCertificate()
val (keyPairs, info) = initNodeInfo() val schemaService = NodeSchemaService(cordappLoader.cordappSchemas)
val identityKeypair = keyPairs.first { it.public == info.legalIdentities.first().owningKey } val (identity, identityKeyPair) = obtainIdentity(notaryConfig = null)
val serialisedNodeInfo = info.serialize() initialiseDatabasePersistence(schemaService, makeIdentityService(identity.certificate)) { database ->
val signature = identityKeypair.sign(serialisedNodeInfo) val persistentNetworkMapCache = PersistentNetworkMapCache(database)
// TODO: Signed data might not be sufficient for multiple identities, as it only contains one signature. val (keyPairs, info) = initNodeInfo(persistentNetworkMapCache, identity, identityKeyPair)
NodeInfoWatcher.saveToFile(configuration.baseDirectory, SignedData(serialisedNodeInfo, signature)) 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<AbstractNode> { open fun start(): StartedNode<AbstractNode> {
check(started == null) { "Node has already been started" } check(started == null) { "Node has already been started" }
log.info("Node starting up ...") log.info("Node starting up ...")
initCertificate() initCertificate()
val (keyPairs, info) = initNodeInfo()
val schemaService = NodeSchemaService(cordappLoader.cordappSchemas) 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. // 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 (startedImpl, schedulerService) = initialiseDatabasePersistence(schemaService, identityService) { database ->
val networkMapCache = NetworkMapCacheImpl(PersistentNetworkMapCache(database), identityService)
val (keyPairs, info) = initNodeInfo(networkMapCache, identity, identityKeyPair)
identityService.loadIdentities(info.legalIdentitiesAndCerts) identityService.loadIdentities(info.legalIdentitiesAndCerts)
val transactionStorage = makeTransactionStorage(database) 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 val mutualExclusionConfiguration = configuration.enterpriseConfiguration.mutualExclusionConfiguration
if (mutualExclusionConfiguration.on) { if (mutualExclusionConfiguration.on) {
RunOnceService(database, mutualExclusionConfiguration.machineName, RunOnceService(database, mutualExclusionConfiguration.machineName,
@ -245,7 +253,6 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
networkMapUpdater.subscribeToNetworkMap() networkMapUpdater.subscribeToNetworkMap()
// If we successfully loaded network data from database, we set this future to Unit. // 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 }) _nodeReadyFuture.captureLater(services.networkMapCache.nodeReady.map { Unit })
return startedImpl.apply { return startedImpl.apply {
@ -266,11 +273,12 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
protected abstract fun getRxIoScheduler(): Scheduler protected abstract fun getRxIoScheduler(): Scheduler
open fun startShell(rpcOps: CordaRPCOps) { 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<Set<KeyPair>, NodeInfo> { private fun initNodeInfo(networkMapCache: NetworkMapCacheBaseInternal,
val (identity, identityKeyPair) = obtainIdentity(notaryConfig = null) identity: PartyAndCertificate,
identityKeyPair: KeyPair): Pair<Set<KeyPair>, NodeInfo> {
val keyPairs = mutableSetOf(identityKeyPair) val keyPairs = mutableSetOf(identityKeyPair)
myNotaryIdentity = configuration.notary?.let { myNotaryIdentity = configuration.notary?.let {
@ -278,12 +286,20 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
keyPairs += notaryIdentityKeyPair keyPairs += notaryIdentityKeyPair
notaryIdentity notaryIdentity
} }
val info = NodeInfo(
var info = NodeInfo(
myAddresses(), myAddresses(),
listOf(identity, myNotaryIdentity).filterNotNull(), listOf(identity, myNotaryIdentity).filterNotNull(),
versionInfo.platformVersion, versionInfo.platformVersion,
platformClock.instant().toEpochMilli() 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) return Pair(keyPairs, info)
} }
@ -506,7 +522,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
* Builds node internal, advertised, and plugin services. * Builds node internal, advertised, and plugin services.
* Returns a list of tokenizable services to be added to the serialisation context. * Returns a list of tokenizable services to be added to the serialisation context.
*/ */
private fun makeServices(keyPairs: Set<KeyPair>, schemaService: SchemaService, transactionStorage: WritableTransactionStorage, database: CordaPersistence, info: NodeInfo, identityService: IdentityService): MutableList<Any> { private fun makeServices(keyPairs: Set<KeyPair>, schemaService: SchemaService, transactionStorage: WritableTransactionStorage, database: CordaPersistence, info: NodeInfo, identityService: IdentityServiceInternal, networkMapCache: NetworkMapCacheInternal): MutableList<Any> {
checkpointStorage = DBCheckpointStorage() checkpointStorage = DBCheckpointStorage()
val metrics = MetricRegistry() val metrics = MetricRegistry()
attachments = NodeAttachmentService(metrics) attachments = NodeAttachmentService(metrics)
@ -520,7 +536,8 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
MonitoringService(metrics), MonitoringService(metrics),
cordappProvider, cordappProvider,
database, database,
info) info,
networkMapCache)
network = makeMessagingService(database, info) network = makeMessagingService(database, info)
val tokenizableServices = mutableListOf(attachments, network, services.vaultService, val tokenizableServices = mutableListOf(attachments, network, services.vaultService,
services.keyManagementService, services.identityService, platformClock, 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. " + "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" "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. // 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<KeyPair>): KeyManagementService { protected open fun makeKeyManagementService(identityService: IdentityServiceInternal, keyPairs: Set<KeyPair>): KeyManagementService {
return PersistentKeyManagementService(identityService, keyPairs) 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 trustStore = KeyStoreWrapper(configuration.trustStoreFile, configuration.trustStorePassword)
val caKeyStore = KeyStoreWrapper(configuration.nodeKeystore, configuration.keyStorePassword) val caKeyStore = KeyStoreWrapper(configuration.nodeKeystore, configuration.keyStorePassword)
val trustRoot = trustStore.getX509Certificate(X509Utilities.CORDA_ROOT_CA) val trustRoot = trustStore.getX509Certificate(X509Utilities.CORDA_ROOT_CA)
val clientCa = caKeyStore.certificateAndKeyPair(X509Utilities.CORDA_CLIENT_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) return PersistentIdentityService(trustRoot, *caCertificates)
} }
@ -732,13 +760,13 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
override val monitoringService: MonitoringService, override val monitoringService: MonitoringService,
override val cordappProvider: CordappProviderInternal, override val cordappProvider: CordappProviderInternal,
override val database: CordaPersistence, override val database: CordaPersistence,
override val myInfo: NodeInfo override val myInfo: NodeInfo,
override val networkMapCache: NetworkMapCacheInternal
) : SingletonSerializeAsToken(), ServiceHubInternal, StateLoader by validatedTransactions { ) : SingletonSerializeAsToken(), ServiceHubInternal, StateLoader by validatedTransactions {
override val rpcFlows = ArrayList<Class<out FlowLogic<*>>>() override val rpcFlows = ArrayList<Class<out FlowLogic<*>>>()
override val stateMachineRecordedTransactionMapping = DBTransactionMappingStorage() override val stateMachineRecordedTransactionMapping = DBTransactionMappingStorage()
override val auditService = DummyAuditService() override val auditService = DummyAuditService()
override val transactionVerifierService by lazy { makeTransactionVerifierService() } 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 vaultService by lazy { makeVaultService(keyManagementService, validatedTransactions, database.hibernateConfig) }
override val contractUpgradeService by lazy { ContractUpgradeServiceImpl() } override val contractUpgradeService by lazy { ContractUpgradeServiceImpl() }
override val attachments: AttachmentStorage get() = this@AbstractNode.attachments override val attachments: AttachmentStorage get() = this@AbstractNode.attachments

View File

@ -2,6 +2,7 @@ package net.corda.node.internal
import com.codahale.metrics.JmxReporter import com.codahale.metrics.JmxReporter
import net.corda.core.concurrent.CordaFuture 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.openFuture
import net.corda.core.internal.concurrent.thenMatch import net.corda.core.internal.concurrent.thenMatch
import net.corda.core.internal.uncheckedCast 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.core.utilities.contextLogger
import net.corda.node.VersionInfo import net.corda.node.VersionInfo
import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.internal.cordapp.CordappLoader
import net.corda.node.internal.security.RPCSecurityManagerImpl
import net.corda.node.serialization.KryoServerSerializationScheme import net.corda.node.serialization.KryoServerSerializationScheme
import net.corda.node.services.RPCUserServiceImpl
import net.corda.node.services.api.SchemaService import net.corda.node.services.api.SchemaService
import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.*
import net.corda.node.services.config.VerifierType
import net.corda.node.services.messaging.* import net.corda.node.services.messaging.*
import net.corda.node.services.transactions.InMemoryTransactionVerifierService import net.corda.node.services.transactions.InMemoryTransactionVerifierService
import net.corda.node.utilities.AddressUtils import net.corda.node.utilities.AddressUtils
@ -133,7 +133,12 @@ open class Node(configuration: NodeConfiguration,
private var shutdownHook: ShutdownHook? = null private var shutdownHook: ShutdownHook? = null
override fun makeMessagingService(database: CordaPersistence, info: NodeInfo): MessagingService { 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 serverAddress = configuration.messagingServerAddress ?: makeLocalMessageBroker()
val advertisedAddress = info.addresses.single() val advertisedAddress = info.addresses.single()
@ -156,7 +161,7 @@ open class Node(configuration: NodeConfiguration,
private fun makeLocalMessageBroker(): NetworkHostAndPort { private fun makeLocalMessageBroker(): NetworkHostAndPort {
with(configuration) { 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) return NetworkHostAndPort("localhost", p2pAddress.port)
} }
} }
@ -212,7 +217,7 @@ open class Node(configuration: NodeConfiguration,
// Start up the MQ clients. // Start up the MQ clients.
rpcMessagingClient.run { rpcMessagingClient.run {
runOnStop += this::stop runOnStop += this::stop
start(rpcOps, userService) start(rpcOps, securityManager)
} }
verifierMessagingClient?.run { verifierMessagingClient?.run {
runOnStop += this::stop 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://<host>:<port>/node * jdbc:h2:tcp://<host>:<port>/node
* with username and password as per the DataSource connection details. The key element to enabling this support is to * 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) * 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. * 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 * This is not using the H2 "automatic mixed mode" directly but leans on many of the underpinnings. For more details

View File

@ -81,13 +81,13 @@ open class NodeStartup(val args: Array<String>) {
conf0 conf0
} }
banJavaSerialisation(conf) banJavaSerialisation(conf)
preNetworkRegistration(conf) preNetworkRegistration(conf)
if (shouldRegisterWithNetwork(cmdlineOptions, conf)) { if (shouldRegisterWithNetwork(cmdlineOptions, conf)) {
registerWithNetwork(cmdlineOptions, conf) registerWithNetwork(cmdlineOptions, conf)
return true return true
} }
logStartupInfo(versionInfo, cmdlineOptions, conf) logStartupInfo(versionInfo, cmdlineOptions, conf)
try { try {
cmdlineOptions.baseDirectory.createDirectories() cmdlineOptions.baseDirectory.createDirectories()

View File

@ -1,5 +1,6 @@
package net.corda.node.internal package net.corda.node.internal
import net.corda.client.rpc.PermissionException
import net.corda.core.contracts.ContractState import net.corda.core.contracts.ContractState
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
@ -156,9 +157,12 @@ class RpcAuthorisationProxy(private val implementation: CordaRPCOps, private val
private inline fun <RESULT> guard(methodName: String, action: () -> RESULT) = guard(methodName, emptyList(), action) private inline fun <RESULT> guard(methodName: String, action: () -> RESULT) = guard(methodName, emptyList(), action)
// TODO change to KFunction reference after Kotlin fixes https://youtrack.jetbrains.com/issue/KT-12140 // TODO change to KFunction reference after Kotlin fixes https://youtrack.jetbrains.com/issue/KT-12140
private inline fun <RESULT> guard(methodName: String, args: List<Any?>, action: () -> RESULT): RESULT { private inline fun <RESULT> guard(methodName: String, args: List<Class<*>>, action: () -> RESULT) : RESULT {
if (!context().isPermitted(methodName, *(args.map { it.name }.toTypedArray()))) {
context().requireEitherPermission(permissionsAllowing.invoke(methodName, args)) throw PermissionException("User not authorized to perform RPC call $methodName with target $args")
return action() }
else {
return action()
}
} }
} }

View File

@ -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
}

View File

@ -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 = '*'
}
}

View File

@ -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
}
}
}

View File

@ -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<RPCSecurityManagerImpl>()
/**
* Instantiate RPCSecurityManager initialised with users data from a list of [User]
*/
fun fromUserList(id: AuthServiceId, users: List<User>) =
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<String>, 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<User>,
realmId: String,
passwordEncryption: PasswordEncryption = PasswordEncryption.NONE) : AuthorizingRealm() {
private val authorizationInfoByUser: Map<String, AuthorizationInfo>
private val authenticationInfoByUser: Map<String, AuthenticationInfo>
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<String>()
stringPermissions = emptySet<String>()
}
}
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<K, V> = org.apache.shiro.cache.Cache<K, V>
/**
* Adapts a [com.google.common.cache.Cache] to a [org.apache.shiro.cache.Cache] implementation.
*/
private fun <K, V> Cache<K, V>.toShiroCache(name: String) = object : ShiroCache<K, V> {
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<String, ShiroCache<*, *>>()
override fun <K, V> getCache(name: String): ShiroCache<K, V> {
val result = instances[name] ?: buildCache<K, V>(name)
instances.putIfAbsent(name, result)
return result as ShiroCache<K, V>
}
private fun <K, V> buildCache(name: String) : ShiroCache<K, V> {
logger.info("Constructing cache '$name' with maximumSize=$maxSize, TTL=${timeToLiveSeconds}s")
return CacheBuilder.newBuilder()
.expireAfterWrite(timeToLiveSeconds, TimeUnit.SECONDS)
.maximumSize(maxSize)
.build<K, V>()
.toShiroCache(name)
}
companion object {
private val logger = loggerFor<GuavaCacheManager>()
}
}

View File

@ -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<User>
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<User>) : 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 }
}

View File

@ -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)
}
}

View File

@ -63,7 +63,7 @@ fun NodeConfiguration.configureWithDevSSLCertificate() = configureDevKeyAndTrust
fun SSLConfiguration.configureDevKeyAndTrustStores(myLegalName: CordaX500Name) { fun SSLConfiguration.configureDevKeyAndTrustStores(myLegalName: CordaX500Name) {
certificatesDirectory.createDirectories() certificatesDirectory.createDirectories()
if (!trustStoreFile.exists()) { 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()) { if (!sslKeystore.exists() || !nodeKeystore.exists()) {
val caKeyStore = loadKeyStore(javaClass.classLoader.getResourceAsStream("net/corda/node/internal/certificates/cordadevcakeys.jks"), "cordacadevpass") val caKeyStore = loadKeyStore(javaClass.classLoader.getResourceAsStream("net/corda/node/internal/certificates/cordadevcakeys.jks"), "cordacadevpass")

View File

@ -1,6 +1,7 @@
package net.corda.node.services.config package net.corda.node.services.config
import com.typesafe.config.Config import com.typesafe.config.Config
import net.corda.core.context.AuthServiceId
import net.corda.core.identity.CordaX500Name import net.corda.core.identity.CordaX500Name
import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.seconds import net.corda.core.utilities.seconds
@ -21,6 +22,7 @@ interface NodeConfiguration : NodeSSLConfiguration {
val exportJMXto: String val exportJMXto: String
val dataSourceProperties: Properties val dataSourceProperties: Properties
val rpcUsers: List<User> val rpcUsers: List<User>
val security: SecurityConfiguration?
val devMode: Boolean val devMode: Boolean
val devModeOptions: DevModeOptions? val devModeOptions: DevModeOptions?
val compatibilityZoneURL: URL? val compatibilityZoneURL: URL?
@ -95,6 +97,7 @@ data class NodeConfigurationImpl(
override val dataSourceProperties: Properties, override val dataSourceProperties: Properties,
override val compatibilityZoneURL: URL? = null, override val compatibilityZoneURL: URL? = null,
override val rpcUsers: List<User>, override val rpcUsers: List<User>,
override val security : SecurityConfiguration? = null,
override val verifierType: VerifierType, override val verifierType: VerifierType,
// TODO typesafe config supports the notion of durations. Make use of that by mapping it to java.time.Duration. // 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 // 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 // TODO See TODO above. Rename this to nodeInfoPollingFrequency and make it of type Duration
override val additionalNodeInfoPollingFrequencyMsec: Long = 5.seconds.toMillis(), override val additionalNodeInfoPollingFrequencyMsec: Long = 5.seconds.toMillis(),
override val sshd: SSHDConfiguration? = null, override val sshd: SSHDConfiguration? = null,
override val database: DatabaseConfig = DatabaseConfig(initialiseSchema = devMode) override val database: DatabaseConfig = DatabaseConfig(initialiseSchema = devMode, exportHibernateJMXStatistics = devMode)
) : NodeConfiguration { ) : NodeConfiguration {
override val exportJMXto: String get() = "http" override val exportJMXto: String get() = "http"
init { init {
@ -126,6 +130,9 @@ data class NodeConfigurationImpl(
require(!useTestClock || devMode) { "Cannot use test clock outside of dev mode" } require(!useTestClock || devMode) { "Cannot use test clock outside of dev mode" }
require(devModeOptions == null || devMode) { "Cannot use devModeOptions 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(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) 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<User>? = 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<User>) = AuthService(
dataSource = DataSource(
type = AuthDataSourceType.INMEMORY,
users = users,
passwordEncryption = PasswordEncryption.NONE),
id = AuthServiceId("NODE_CONFIG"))
}
}
}
data class RelayConfiguration(val relayHost: String, data class RelayConfiguration(val relayHost: String,
val remoteInboundPort: Int, val remoteInboundPort: Int,
val username: String, val username: String,

View File

@ -5,11 +5,11 @@ import net.corda.core.crypto.toStringShort
import net.corda.core.identity.* import net.corda.core.identity.*
import net.corda.core.internal.cert import net.corda.core.internal.cert
import net.corda.core.internal.toX509CertHolder import net.corda.core.internal.toX509CertHolder
import net.corda.core.node.services.IdentityService
import net.corda.core.node.services.UnknownAnonymousPartyException import net.corda.core.node.services.UnknownAnonymousPartyException
import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.utilities.contextLogger import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.trace import net.corda.core.utilities.trace
import net.corda.node.services.api.IdentityServiceInternal
import net.corda.nodeapi.internal.crypto.X509CertificateFactory import net.corda.nodeapi.internal.crypto.X509CertificateFactory
import org.bouncycastle.cert.X509CertificateHolder import org.bouncycastle.cert.X509CertificateHolder
import java.security.InvalidAlgorithmParameterException import java.security.InvalidAlgorithmParameterException
@ -25,7 +25,7 @@ import javax.annotation.concurrent.ThreadSafe
*/ */
@ThreadSafe @ThreadSafe
class InMemoryIdentityService(identities: Iterable<PartyAndCertificate>, class InMemoryIdentityService(identities: Iterable<PartyAndCertificate>,
trustRoot: X509CertificateHolder) : SingletonSerializeAsToken(), IdentityService { trustRoot: X509CertificateHolder) : SingletonSerializeAsToken(), IdentityServiceInternal {
companion object { companion object {
private val log = contextLogger() private val log = contextLogger()
} }
@ -45,18 +45,17 @@ class InMemoryIdentityService(identities: Iterable<PartyAndCertificate>,
principalToParties.putAll(identities.associateBy { it.name }) principalToParties.putAll(identities.associateBy { it.name })
} }
// TODO: Check the certificate validation logic
@Throws(CertificateExpiredException::class, CertificateNotYetValidException::class, InvalidAlgorithmParameterException::class) @Throws(CertificateExpiredException::class, CertificateNotYetValidException::class, InvalidAlgorithmParameterException::class)
override fun verifyAndRegisterIdentity(identity: PartyAndCertificate): PartyAndCertificate? { override fun verifyAndRegisterIdentity(identity: PartyAndCertificate): PartyAndCertificate? {
// Validate the chain first, before we do anything clever with it // Validate the chain first, before we do anything clever with it
try { try {
identity.verify(trustAnchor) identity.verify(trustAnchor)
} catch (e: CertPathValidatorException) { } catch (e: CertPathValidatorException) {
log.error("Certificate validation failed for ${identity.name} against trusted root ${trustAnchor.trustedCert.subjectX500Principal}.") log.warn("Certificate validation failed for ${identity.name} against trusted root ${trustAnchor.trustedCert.subjectX500Principal}.")
log.error("Certificate path :") log.warn("Certificate path :")
identity.certPath.certificates.reversed().forEachIndexed { index, certificate -> identity.certPath.certificates.reversed().forEachIndexed { index, certificate ->
val space = (0 until index).joinToString("") { " " } val space = (0 until index).joinToString("") { " " }
log.error("$space${certificate.toX509CertHolder().subject}") log.warn("$space${certificate.toX509CertHolder().subject}")
} }
throw e throw e
} }

View File

@ -6,12 +6,12 @@ import net.corda.core.crypto.toStringShort
import net.corda.core.identity.* import net.corda.core.identity.*
import net.corda.core.internal.cert import net.corda.core.internal.cert
import net.corda.core.internal.toX509CertHolder import net.corda.core.internal.toX509CertHolder
import net.corda.core.node.services.IdentityService
import net.corda.core.node.services.UnknownAnonymousPartyException import net.corda.core.node.services.UnknownAnonymousPartyException
import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.utilities.MAX_HASH_HEX_SIZE import net.corda.core.utilities.MAX_HASH_HEX_SIZE
import net.corda.core.utilities.contextLogger import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.debug import net.corda.core.utilities.debug
import net.corda.node.services.api.IdentityServiceInternal
import net.corda.node.utilities.AppendOnlyPersistentMap import net.corda.node.utilities.AppendOnlyPersistentMap
import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX
import net.corda.nodeapi.internal.crypto.X509CertificateFactory import net.corda.nodeapi.internal.crypto.X509CertificateFactory
@ -27,7 +27,7 @@ import javax.persistence.Lob
@ThreadSafe @ThreadSafe
class PersistentIdentityService(override val trustRoot: X509Certificate, class PersistentIdentityService(override val trustRoot: X509Certificate,
vararg caCertificates: X509Certificate) : SingletonSerializeAsToken(), IdentityService { vararg caCertificates: X509Certificate) : SingletonSerializeAsToken(), IdentityServiceInternal {
constructor(trustRoot: X509CertificateHolder) : this(trustRoot.cert) constructor(trustRoot: X509CertificateHolder) : this(trustRoot.cert)
companion object { 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) @Throws(CertificateExpiredException::class, CertificateNotYetValidException::class, InvalidAlgorithmParameterException::class)
override fun verifyAndRegisterIdentity(identity: PartyAndCertificate): PartyAndCertificate? { override fun verifyAndRegisterIdentity(identity: PartyAndCertificate): PartyAndCertificate? {
// Validate the chain first, before we do anything clever with it // Validate the chain first, before we do anything clever with it
try { try {
identity.verify(trustAnchor) identity.verify(trustAnchor)
} catch (e: CertPathValidatorException) { } catch (e: CertPathValidatorException) {
log.error(e.localizedMessage) log.warn(e.localizedMessage)
log.error("Path = ") log.warn("Path = ")
identity.certPath.certificates.reversed().forEach { identity.certPath.certificates.reversed().forEach {
log.error(it.toX509CertHolder().subject.toString()) log.warn(it.toX509CertHolder().subject.toString())
} }
throw e throw e
} }

View File

@ -3,9 +3,9 @@ package net.corda.node.services.keys
import net.corda.core.crypto.* import net.corda.core.crypto.*
import net.corda.core.identity.PartyAndCertificate import net.corda.core.identity.PartyAndCertificate
import net.corda.core.internal.ThreadBox import net.corda.core.internal.ThreadBox
import net.corda.core.node.services.IdentityService
import net.corda.core.node.services.KeyManagementService import net.corda.core.node.services.KeyManagementService
import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.node.services.api.IdentityServiceInternal
import org.bouncycastle.operator.ContentSigner import org.bouncycastle.operator.ContentSigner
import java.security.KeyPair import java.security.KeyPair
import java.security.PrivateKey import java.security.PrivateKey
@ -25,7 +25,7 @@ import javax.annotation.concurrent.ThreadSafe
* etc. * etc.
*/ */
@ThreadSafe @ThreadSafe
class E2ETestKeyManagementService(val identityService: IdentityService, class E2ETestKeyManagementService(val identityService: IdentityServiceInternal,
initialKeys: Set<KeyPair>) : SingletonSerializeAsToken(), KeyManagementService { initialKeys: Set<KeyPair>) : SingletonSerializeAsToken(), KeyManagementService {
private class InnerState { private class InnerState {
val keys = HashMap<PublicKey, PrivateKey>() val keys = HashMap<PublicKey, PrivateKey>()

View File

@ -4,8 +4,8 @@ import net.corda.core.crypto.Crypto
import net.corda.core.identity.PartyAndCertificate import net.corda.core.identity.PartyAndCertificate
import net.corda.core.internal.cert import net.corda.core.internal.cert
import net.corda.core.internal.toX509CertHolder import net.corda.core.internal.toX509CertHolder
import net.corda.core.node.services.IdentityService
import net.corda.core.utilities.days 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.CertificateType
import net.corda.nodeapi.internal.crypto.ContentSignerBuilder import net.corda.nodeapi.internal.crypto.ContentSignerBuilder
import net.corda.nodeapi.internal.crypto.X509CertificateFactory 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. * @param revocationEnabled whether to check revocation status of certificates in the certificate path.
* @return X.509 certificate and path to the trust root. * @return X.509 certificate and path to the trust root.
*/ */
fun freshCertificate(identityService: IdentityService, fun freshCertificate(identityService: IdentityServiceInternal,
subjectPublicKey: PublicKey, subjectPublicKey: PublicKey,
issuer: PartyAndCertificate, issuer: PartyAndCertificate,
issuerSigner: ContentSigner, issuerSigner: ContentSigner,
revocationEnabled: Boolean = false): PartyAndCertificate { revocationEnabled: Boolean = false): PartyAndCertificate {
val issuerCert = issuer.certificate.toX509CertHolder() val issuerCert = issuer.certificate.toX509CertHolder()
val window = X509Utilities.getCertificateValidityWindow(Duration.ZERO, 3650.days, issuerCert) 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) issuerSigner, issuer.name, subjectPublicKey, window)
val ourCertPath = X509CertificateFactory().delegate.generateCertPath(listOf(ourCertificate.cert) + issuer.certPath.certificates) val ourCertPath = X509CertificateFactory().delegate.generateCertPath(listOf(ourCertificate.cert) + issuer.certPath.certificates)
val anonymisedIdentity = PartyAndCertificate(ourCertPath) val anonymisedIdentity = PartyAndCertificate(ourCertPath)
identityService.verifyAndRegisterIdentity(anonymisedIdentity) identityService.justVerifyAndRegisterIdentity(anonymisedIdentity)
return anonymisedIdentity return anonymisedIdentity
} }

View File

@ -2,10 +2,10 @@ package net.corda.node.services.keys
import net.corda.core.crypto.* import net.corda.core.crypto.*
import net.corda.core.identity.PartyAndCertificate import net.corda.core.identity.PartyAndCertificate
import net.corda.core.node.services.IdentityService
import net.corda.core.node.services.KeyManagementService import net.corda.core.node.services.KeyManagementService
import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.utilities.MAX_HASH_HEX_SIZE 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.node.utilities.AppendOnlyPersistentMap
import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX
import org.bouncycastle.operator.ContentSigner 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. * 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<KeyPair>) : SingletonSerializeAsToken(), KeyManagementService { initialKeys: Set<KeyPair>) : SingletonSerializeAsToken(), KeyManagementService {
@Entity @Entity

View File

@ -12,9 +12,13 @@ import net.corda.core.node.NodeInfo
import net.corda.core.node.services.NetworkMapCache import net.corda.core.node.services.NetworkMapCache
import net.corda.core.node.services.NetworkMapCache.MapChange import net.corda.core.node.services.NetworkMapCache.MapChange
import net.corda.core.serialization.SingletonSerializeAsToken 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.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.config.NodeConfiguration
import net.corda.node.services.messaging.NodeLoginModule.Companion.NODE_ROLE import net.corda.node.services.messaging.NodeLoginModule.Companion.NODE_ROLE
import net.corda.node.services.messaging.NodeLoginModule.Companion.PEER_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.X509Utilities.CORDA_ROOT_CA
import net.corda.nodeapi.internal.crypto.loadKeyStore import net.corda.nodeapi.internal.crypto.loadKeyStore
import net.corda.nodeapi.* 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.INTERNAL_PREFIX
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_USER 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.NOTIFICATIONS_ADDRESS
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_QUEUE 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.PEERS_PREFIX
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEER_USER 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.ArtemisMessagingComponent.NodeAddress
import net.corda.nodeapi.internal.requireOnDefaultFileSystem import net.corda.nodeapi.internal.requireOnDefaultFileSystem
import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.api.core.SimpleString
@ -97,7 +101,7 @@ class ArtemisMessagingServer(private val config: NodeConfiguration,
private val p2pPort: Int, private val p2pPort: Int,
val rpcPort: Int?, val rpcPort: Int?,
val networkMapCache: NetworkMapCache, val networkMapCache: NetworkMapCache,
val userService: RPCUserService) : SingletonSerializeAsToken() { val securityManager: RPCSecurityManager) : SingletonSerializeAsToken() {
companion object { companion object {
private val log = contextLogger() private val log = contextLogger()
/** 10 MiB maximum allowed file size for attachments, including message headers. TODO: acquire this value from Network Map when supported. */ /** 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 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 { private fun queueConfig(name: String, address: String = name, filter: String? = null, durable: Boolean): CoreQueueConfiguration {
return CoreQueueConfiguration().apply { 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. * 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. * 4. Verifiers. These are given read access to the verification request queue and write access to the response queue.
*/ */
private fun ConfigurationImpl.configureAddressSecurity() : Pair<Configuration, LoginListener> { private fun ConfigurationImpl.configureAddressSecurity(): Pair<Configuration, LoginListener> {
val nodeInternalRole = Role(NODE_ROLE, true, true, true, true, true, true, true, true) 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["$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[P2P_QUEUE] = setOf(nodeInternalRole, restrictedRole(PEER_ROLE, send = true))
securityRoles[RPCApi.RPC_SERVER_QUEUE_NAME] = setOf(nodeInternalRole, restrictedRole(RPC_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 // Each RPC user must have its own role and its own queue. This prevents users accessing each other's queues
// and stealing RPC responses. // and stealing RPC responses.
val rolesAdderOnLogin = RolesAdderOnLogin { username -> val rolesAdderOnLogin = RolesAdderOnLogin { username ->
@ -282,7 +289,7 @@ class ArtemisMessagingServer(private val config: NodeConfiguration,
override fun getAppConfigurationEntry(name: String): Array<AppConfigurationEntry> { override fun getAppConfigurationEntry(name: String): Array<AppConfigurationEntry> {
val options = mapOf( val options = mapOf(
LoginListener::javaClass.name to loginListener, 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) NodeLoginModule.CERT_CHAIN_CHECKS_OPTION_NAME to certChecks)
return arrayOf(AppConfigurationEntry(name, REQUIRED, options)) return arrayOf(AppConfigurationEntry(name, REQUIRED, options))
} }
@ -557,7 +564,7 @@ class NodeLoginModule : LoginModule {
private var loginSucceeded: Boolean = false private var loginSucceeded: Boolean = false
private lateinit var subject: Subject private lateinit var subject: Subject
private lateinit var callbackHandler: CallbackHandler private lateinit var callbackHandler: CallbackHandler
private lateinit var userService: RPCUserService private lateinit var securityManager: RPCSecurityManager
private lateinit var loginListener: LoginListener private lateinit var loginListener: LoginListener
private lateinit var peerCertCheck: CertificateChainCheckPolicy.Check private lateinit var peerCertCheck: CertificateChainCheckPolicy.Check
private lateinit var nodeCertCheck: CertificateChainCheckPolicy.Check private lateinit var nodeCertCheck: CertificateChainCheckPolicy.Check
@ -567,7 +574,7 @@ class NodeLoginModule : LoginModule {
override fun initialize(subject: Subject, callbackHandler: CallbackHandler, sharedState: Map<String, *>, options: Map<String, *>) { override fun initialize(subject: Subject, callbackHandler: CallbackHandler, sharedState: Map<String, *>, options: Map<String, *>) {
this.subject = subject this.subject = subject
this.callbackHandler = callbackHandler 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 loginListener = options[LoginListener::javaClass.name] as LoginListener
val certChainChecks: Map<String, CertificateChainCheckPolicy.Check> = uncheckedCast(options[CERT_CHAIN_CHECKS_OPTION_NAME]) val certChainChecks: Map<String, CertificateChainCheckPolicy.Check> = uncheckedCast(options[CERT_CHAIN_CHECKS_OPTION_NAME])
peerCertCheck = certChainChecks[PEER_ROLE]!! peerCertCheck = certChainChecks[PEER_ROLE]!!
@ -598,7 +605,7 @@ class NodeLoginModule : LoginModule {
PEER_ROLE -> authenticatePeer(certificates) PEER_ROLE -> authenticatePeer(certificates)
NODE_ROLE -> authenticateNode(certificates) NODE_ROLE -> authenticateNode(certificates)
VERIFIER_ROLE -> authenticateVerifier(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") else -> throw FailedLoginException("Peer does not belong on our network")
} }
principals += UserPrincipal(validatedUser) principals += UserPrincipal(validatedUser)
@ -629,13 +636,8 @@ class NodeLoginModule : LoginModule {
return certificates.first().subjectDN.name return certificates.first().subjectDN.name
} }
private fun authenticateRpcUser(password: String, username: String): String { private fun authenticateRpcUser(username: String, password: Password): String {
val rpcUser = userService.getUser(username) ?: throw FailedLoginException("User does not exist") securityManager.authenticate(username, password)
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")
}
loginListener(username) loginListener(username)
principals += RolePrincipal(RPC_ROLE) // This enables the RPC client to send requests 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 principals += RolePrincipal("${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.$username") // This enables the RPC client to receive responses

View File

@ -4,7 +4,7 @@ import net.corda.core.identity.CordaX500Name
import net.corda.core.messaging.RPCOps import net.corda.core.messaging.RPCOps
import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.utilities.NetworkHostAndPort 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.config.SSLConfiguration
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_USER import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_USER
import net.corda.nodeapi.internal.crypto.X509Utilities 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 val artemis = ArtemisMessagingClient(config, serverAddress)
private var rpcServer: RPCServer? = null 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 locator = artemis.start().sessionFactory.serverLocator
val myCert = loadKeyStore(config.sslKeystore, config.keyStorePassword).getX509Certificate(X509Utilities.CORDA_CLIENT_TLS) 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) { fun start2(serverControl: ActiveMQServerControl) = synchronized(this) {

View File

@ -26,11 +26,10 @@ import net.corda.core.serialization.SerializationContext
import net.corda.core.serialization.SerializationDefaults.RPC_SERVER_CONTEXT import net.corda.core.serialization.SerializationDefaults.RPC_SERVER_CONTEXT
import net.corda.core.serialization.deserialize import net.corda.core.serialization.deserialize
import net.corda.core.utilities.* 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.node.services.logging.pushToLoggingContext
import net.corda.nodeapi.* 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.Message
import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.api.core.SimpleString
import org.apache.activemq.artemis.api.core.client.ActiveMQClient.DEFAULT_ACK_BATCH_SIZE 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 rpcServerUsername: String,
private val rpcServerPassword: String, private val rpcServerPassword: String,
private val serverLocator: ServerLocator, private val serverLocator: ServerLocator,
private val userService: RPCUserService, private val securityManager: RPCSecurityManager,
private val nodeLegalName: CordaX500Name, private val nodeLegalName: CordaX500Name,
private val rpcConfiguration: RPCServerConfiguration = RPCServerConfiguration.default private val rpcConfiguration: RPCServerConfiguration = RPCServerConfiguration.default
) { ) {
@ -213,6 +212,7 @@ class RPCServer(
reaperScheduledFuture?.cancel(false) reaperScheduledFuture?.cancel(false)
rpcExecutor?.shutdownNow() rpcExecutor?.shutdownNow()
reaperExecutor?.shutdownNow() reaperExecutor?.shutdownNow()
securityManager.close()
sessionAndConsumers.forEach { sessionAndConsumers.forEach {
it.sessionFactory.close() it.sessionFactory.close()
} }
@ -357,9 +357,6 @@ class RPCServer(
observableMap.cleanUp() 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 { private fun ClientMessage.context(sessionId: Trace.SessionId): RpcAuthContext {
val trace = Trace.newInstance(sessionId = sessionId) val trace = Trace.newInstance(sessionId = sessionId)
val externalTrace = externalTrace() val externalTrace = externalTrace()
@ -368,19 +365,10 @@ class RPCServer(
return RpcAuthContext(InvocationContext.rpc(rpcActor.first, trace, externalTrace, impersonatedActor), rpcActor.second) return RpcAuthContext(InvocationContext.rpc(rpcActor.first, trace, externalTrace, impersonatedActor), rpcActor.second)
} }
private fun actorFrom(message: ClientMessage): Pair<Actor, RpcPermissions> { private fun actorFrom(message: ClientMessage): Pair<Actor, AuthorizingSubject> {
val validatedUser = message.getStringProperty(Message.HDR_VALIDATED_USER) ?: throw IllegalArgumentException("Missing validated user from the Artemis message") 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 val targetLegalIdentity = message.getStringProperty(RPCApi.RPC_TARGET_LEGAL_IDENTITY)?.let(CordaX500Name.Companion::parse) ?: nodeLegalName
// TODO switch userService based on targetLegalIdentity return Pair(Actor(Id(validatedUser), securityManager.id, targetLegalIdentity), securityManager.buildSubject(validatedUser))
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")
}
} }
} }

View File

@ -1,30 +1,9 @@
package net.corda.node.services.messaging package net.corda.node.services.messaging
import net.corda.client.rpc.PermissionException
import net.corda.core.context.InvocationContext import net.corda.core.context.InvocationContext
import net.corda.node.services.Permissions import net.corda.node.internal.security.AuthorizingSubject
import net.corda.nodeapi.internal.ArtemisMessagingComponent
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<String>): 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<String> = emptySet()) {
companion object {
val NONE = RpcPermissions()
val ALL = RpcPermissions(setOf("ALL"))
}
fun coverAny(permissions: Set<String>) = !values.intersect(permissions + Permissions.all()).isEmpty()
}

View File

@ -88,6 +88,7 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal,
// Only publish and write to disk if there are changes to the node info. // Only publish and write to disk if there are changes to the node info.
val signedNodeInfo = signNodeInfo(newInfo) val signedNodeInfo = signNodeInfo(newInfo)
networkMapCache.addNode(newInfo)
fileWatcher.saveToFile(signedNodeInfo) fileWatcher.saveToFile(signedNodeInfo)
if (networkMapClient != null) { if (networkMapClient != null) {

View File

@ -20,6 +20,7 @@ import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.serialization.serialize import net.corda.core.serialization.serialize
import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.contextLogger 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.NetworkMapCacheBaseInternal
import net.corda.node.services.api.NetworkMapCacheInternal import net.corda.node.services.api.NetworkMapCacheInternal
import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.CordaPersistence
@ -38,13 +39,22 @@ class NetworkMapCacheImpl(
networkMapCacheBase: NetworkMapCacheBaseInternal, networkMapCacheBase: NetworkMapCacheBaseInternal,
private val identityService: IdentityService private val identityService: IdentityService
) : NetworkMapCacheBaseInternal by networkMapCacheBase, NetworkMapCacheInternal { ) : NetworkMapCacheBaseInternal by networkMapCacheBase, NetworkMapCacheInternal {
companion object {
private val logger = loggerFor<NetworkMapCacheImpl>()
}
init { init {
networkMapCacheBase.allNodes.forEach { it.legalIdentitiesAndCerts.forEach { identityService.verifyAndRegisterIdentity(it) } } networkMapCacheBase.allNodes.forEach { it.legalIdentitiesAndCerts.forEach { identityService.verifyAndRegisterIdentity(it) } }
networkMapCacheBase.changed.subscribe { mapChange -> networkMapCacheBase.changed.subscribe { mapChange ->
// TODO how should we handle network map removal // TODO how should we handle network map removal
if (mapChange is MapChange.Added) { if (mapChange is MapChange.Added) {
mapChange.node.legalIdentitiesAndCerts.forEach { 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.")
}
} }
} }
} }

View File

@ -4,31 +4,30 @@ import net.corda.core.context.Actor
import net.corda.core.context.InvocationContext import net.corda.core.context.InvocationContext
import net.corda.core.identity.CordaX500Name import net.corda.core.identity.CordaX500Name
import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.CordaRPCOps
import net.corda.node.services.RPCUserService import net.corda.node.internal.security.Password
import net.corda.node.services.messaging.RpcPermissions import net.corda.node.internal.security.RPCSecurityManager
import net.corda.node.internal.security.tryAuthenticate
import org.crsh.auth.AuthInfo import org.crsh.auth.AuthInfo
import org.crsh.auth.AuthenticationPlugin import org.crsh.auth.AuthenticationPlugin
import org.crsh.plugin.CRaSHPlugin import org.crsh.plugin.CRaSHPlugin
class CordaAuthenticationPlugin(val rpcOps:CordaRPCOps, val userService:RPCUserService, val nodeLegalName:CordaX500Name) : CRaSHPlugin<AuthenticationPlugin<String>>(), AuthenticationPlugin<String> { class CordaAuthenticationPlugin(private val rpcOps: CordaRPCOps, private val securityManager: RPCSecurityManager, private val nodeLegalName: CordaX500Name) : CRaSHPlugin<AuthenticationPlugin<String>>(), AuthenticationPlugin<String> {
override fun getImplementation(): AuthenticationPlugin<String> = this override fun getImplementation(): AuthenticationPlugin<String> = this
override fun getName(): String = "corda" override fun getName(): String = "corda"
override fun authenticate(username: String?, credential: String?): AuthInfo { override fun authenticate(username: String?, credential: String?): AuthInfo {
if (username == null || credential == null) { if (username == null || credential == null) {
return AuthInfo.UNSUCCESSFUL return AuthInfo.UNSUCCESSFUL
} }
val authorizingSubject = securityManager.tryAuthenticate(username, Password(credential))
val user = userService.getUser(username) if (authorizingSubject != null) {
val actor = Actor(Actor.Id(username), securityManager.id, nodeLegalName)
if (user != null && user.password == credential) { return CordaSSHAuthInfo(true, makeRPCOpsWithContext(rpcOps, InvocationContext.rpc(actor), authorizingSubject))
val actor = Actor(Actor.Id(username), userService.id, nodeLegalName)
return CordaSSHAuthInfo(true, makeRPCOpsWithContext(rpcOps, InvocationContext.rpc(actor), RpcPermissions(user.permissions)))
} }
return AuthInfo.UNSUCCESSFUL
return AuthInfo.UNSUCCESSFUL;
} }
override fun getCredentialType(): Class<String> = String::class.java override fun getCredentialType(): Class<String> = String::class.java

View File

@ -25,11 +25,11 @@ import net.corda.core.messaging.StateMachineUpdate
import net.corda.core.node.services.IdentityService import net.corda.core.node.services.IdentityService
import net.corda.node.internal.Node import net.corda.node.internal.Node
import net.corda.node.internal.StartedNode 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.config.NodeConfiguration
import net.corda.node.services.messaging.CURRENT_RPC_CONTEXT import net.corda.node.services.messaging.CURRENT_RPC_CONTEXT
import net.corda.node.services.messaging.RpcAuthContext 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.ANSIProgressRenderer
import net.corda.node.utilities.StdoutANSIProgressRenderer import net.corda.node.utilities.StdoutANSIProgressRenderer
import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.CordaPersistence
@ -82,19 +82,19 @@ object InteractiveShell {
private lateinit var node: StartedNode<Node> private lateinit var node: StartedNode<Node>
@VisibleForTesting @VisibleForTesting
internal lateinit var database: CordaPersistence internal lateinit var database: CordaPersistence
private lateinit var rpcOps:CordaRPCOps private lateinit var rpcOps: CordaRPCOps
private lateinit var userService:RPCUserService private lateinit var securityManager: RPCSecurityManager
private lateinit var identityService:IdentityService private lateinit var identityService: IdentityService
private var shell:Shell? = null private var shell: Shell? = null
private lateinit var nodeLegalName: CordaX500Name private lateinit var nodeLegalName: CordaX500Name
/** /**
* Starts an interactive shell connected to the local terminal. This shell gives administrator access to the node * Starts an interactive shell connected to the local terminal. This shell gives administrator access to the node
* internals. * 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.rpcOps = cordaRPCOps
this.userService = userService this.securityManager = securityManager
this.identityService = identityService this.identityService = identityService
this.nodeLegalName = configuration.myLegalName this.nodeLegalName = configuration.myLegalName
this.database = database this.database = database
@ -123,14 +123,14 @@ object InteractiveShell {
} }
} }
fun runLocalShell(node:StartedNode<Node>) { fun runLocalShell(node: StartedNode<Node>) {
val terminal = TerminalFactory.create() val terminal = TerminalFactory.create()
val consoleReader = ConsoleReader("Corda", FileInputStream(FileDescriptor.`in`), System.out, terminal) val consoleReader = ConsoleReader("Corda", FileInputStream(FileDescriptor.`in`), System.out, terminal)
val jlineProcessor = JLineProcessor(terminal.isAnsiSupported, shell, consoleReader, System.out) val jlineProcessor = JLineProcessor(terminal.isAnsiSupported, shell, consoleReader, System.out)
InterruptHandler { jlineProcessor.interrupt() }.install() InterruptHandler { jlineProcessor.interrupt() }.install()
thread(name = "Command line shell processor", isDaemon = true) { thread(name = "Command line shell processor", isDaemon = true) {
// Give whoever has local shell access administrator access to the node. // 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) CURRENT_RPC_CONTEXT.set(context)
Emoji.renderIfSupported { Emoji.renderIfSupported {
jlineProcessor.run() 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 // 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 // will cause any commands using JIT Java compilation to be suppressed. In CRaSH upstream that
// is only the 'jmx' command. // 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( val attributes = mapOf(
@ -180,7 +180,7 @@ object InteractiveShell {
context.refresh() context.refresh()
this.config = config this.config = config
start(context) 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) { } catch (e: NoApplicableConstructor) {
output.println("No matching constructor found:", Color.red) output.println("No matching constructor found:", Color.red)
e.errors.forEach { output.println("- $it", Color.red) } e.errors.forEach { output.println("- $it", Color.red) }
} catch (e:PermissionException) { } catch (e: PermissionException) {
output.println(e.message ?: "Access denied", Color.red) output.println(e.message ?: "Access denied", Color.red)
} finally { } finally {
InputStreamDeserializer.closeAll() InputStreamDeserializer.closeAll()
@ -271,9 +271,9 @@ object InteractiveShell {
*/ */
@Throws(NoApplicableConstructor::class) @Throws(NoApplicableConstructor::class)
fun <T> runFlowFromString(invoke: (Class<out FlowLogic<T>>, Array<out Any?>) -> FlowProgressHandle<T>, fun <T> runFlowFromString(invoke: (Class<out FlowLogic<T>>, Array<out Any?>) -> FlowProgressHandle<T>,
inputData: String, inputData: String,
clazz: Class<out FlowLogic<T>>, clazz: Class<out FlowLogic<T>>,
om: ObjectMapper = yamlInputMapper): FlowProgressHandle<T> { om: ObjectMapper = yamlInputMapper): FlowProgressHandle<T> {
// For each constructor, attempt to parse the input data as a method call. Use the first that succeeds, // 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. // and keep track of the reasons we failed so we can print them out if no constructors are usable.
val parser = StringToMethodCallParser(clazz, om) val parser = StringToMethodCallParser(clazz, om)

Some files were not shown because too many files have changed in this diff Show More