mirror of
https://github.com/corda/corda.git
synced 2025-01-13 16:30:25 +00:00
Merge remote-tracking branch 'open-hc01/master' into colljos-os-hc01-merge-111217
This commit is contained in:
commit
8a6e9c52f3
@ -24,7 +24,7 @@ buildscript {
|
||||
ext.jackson_version = '2.9.2'
|
||||
ext.jetty_version = '9.4.7.v20170914'
|
||||
ext.jersey_version = '2.25'
|
||||
ext.jolokia_version = '2.0.0-M3'
|
||||
ext.jolokia_version = '1.3.7'
|
||||
ext.assertj_version = '3.8.0'
|
||||
ext.slf4j_version = '1.7.25'
|
||||
ext.log4j_version = '2.9.1'
|
||||
@ -50,6 +50,8 @@ buildscript {
|
||||
ext.crash_version = 'cce5a00f114343c1145c1d7756e1dd6df3ea984e'
|
||||
ext.jsr305_version = constants.getProperty("jsr305Version")
|
||||
ext.spring_jdbc_version ='5.0.0.RELEASE'
|
||||
ext.shiro_version = '1.4.0'
|
||||
ext.artifactory_plugin_version = constants.getProperty('artifactoryPluginVersion')
|
||||
|
||||
// Update 121 is required for ObjectInputFilter and at time of writing 131 was latest:
|
||||
ext.java8_minUpdateVersion = '131'
|
||||
@ -73,6 +75,7 @@ buildscript {
|
||||
classpath "org.ajoberstar:grgit:1.1.0"
|
||||
classpath "net.i2p.crypto:eddsa:$eddsa_version" // Needed for ServiceIdentityGenerator in the build environment.
|
||||
classpath "org.owasp:dependency-check-gradle:${dependency_checker_version}"
|
||||
classpath "org.jfrog.buildinfo:build-info-extractor-gradle:$artifactory_plugin_version"
|
||||
}
|
||||
}
|
||||
|
||||
@ -81,7 +84,6 @@ plugins {
|
||||
// but the DSL has some restrictions e.g can't be used on the allprojects section. So we should revisit this if there are improvements in Gradle.
|
||||
// Version 1.0.2 of this plugin uses capsule:1.0.1
|
||||
id "us.kirchmeier.capsule" version "1.0.2"
|
||||
id "com.jfrog.artifactory" version "4.4.18"
|
||||
}
|
||||
|
||||
ext {
|
||||
@ -93,6 +95,7 @@ apply plugin: 'com.github.ben-manes.versions'
|
||||
apply plugin: 'net.corda.plugins.publish-utils'
|
||||
apply plugin: 'net.corda.plugins.cordformation'
|
||||
apply plugin: 'maven-publish'
|
||||
apply plugin: 'com.jfrog.artifactory'
|
||||
|
||||
// We need the following three lines even though they're inside an allprojects {} block below because otherwise
|
||||
// IntelliJ gets confused when importing the project and ends up erasing and recreating the .idea directory, along
|
||||
|
@ -13,7 +13,7 @@ import net.corda.core.utilities.*
|
||||
import net.corda.node.services.messaging.RPCServerConfiguration
|
||||
import net.corda.nodeapi.RPCApi
|
||||
import net.corda.testing.*
|
||||
import net.corda.testing.driver.poll
|
||||
import net.corda.testing.internal.poll
|
||||
import net.corda.testing.internal.*
|
||||
import org.apache.activemq.artemis.api.core.SimpleString
|
||||
import org.junit.After
|
||||
@ -70,8 +70,8 @@ class RPCStabilityTests : IntegrationTest() {
|
||||
val executor = Executors.newScheduledThreadPool(1)
|
||||
fun startAndStop() {
|
||||
rpcDriver {
|
||||
val server = startRpcServer<RPCOps>(ops = DummyOps)
|
||||
startRpcClient<RPCOps>(server.get().broker.hostAndPort!!).get()
|
||||
val server = startRpcServer<RPCOps>(ops = DummyOps).get()
|
||||
startRpcClient<RPCOps>(server.broker.hostAndPort!!).get()
|
||||
}
|
||||
}
|
||||
repeat(5) {
|
||||
@ -238,6 +238,7 @@ class RPCStabilityTests : IntegrationTest() {
|
||||
override val protocolVersion = 0
|
||||
override fun ping() = "pong"
|
||||
}
|
||||
|
||||
val serverFollower = shutdownManager.follower()
|
||||
val serverPort = startRpcServer<ReconnectOps>(ops = ops).getOrThrow().broker.hostAndPort!!
|
||||
serverFollower.unfollow()
|
||||
@ -355,7 +356,7 @@ class RPCStabilityTests : IntegrationTest() {
|
||||
|
||||
}
|
||||
|
||||
fun RPCDriverExposedDSLInterface.pollUntilClientNumber(server: RpcServerHandle, expected: Int) {
|
||||
fun RPCDriverDSL.pollUntilClientNumber(server: RpcServerHandle, expected: Int) {
|
||||
pollUntilTrue("number of RPC clients to become $expected") {
|
||||
val clientAddresses = server.broker.serverControl.addressNames.filter { it.startsWith(RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX) }
|
||||
clientAddresses.size == expected
|
||||
|
@ -22,7 +22,10 @@ import net.corda.core.internal.ThreadBox
|
||||
import net.corda.core.messaging.RPCOps
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.utilities.*
|
||||
import net.corda.core.utilities.Try
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.debug
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.nodeapi.ArtemisConsumer
|
||||
import net.corda.nodeapi.ArtemisProducer
|
||||
import net.corda.nodeapi.RPCApi
|
||||
|
@ -7,7 +7,7 @@ import net.corda.core.messaging.RPCOps
|
||||
import net.corda.node.services.messaging.RPCServerConfiguration
|
||||
import net.corda.nodeapi.internal.config.User
|
||||
import net.corda.testing.SerializationEnvironmentRule
|
||||
import net.corda.testing.internal.RPCDriverExposedDSLInterface
|
||||
import net.corda.testing.internal.RPCDriverDSL
|
||||
import net.corda.testing.internal.rpcTestUser
|
||||
import net.corda.testing.internal.startInVmRpcClient
|
||||
import net.corda.testing.internal.startRpcClient
|
||||
@ -41,7 +41,7 @@ open class AbstractRPCTest {
|
||||
val createSession: () -> ClientSession
|
||||
)
|
||||
|
||||
inline fun <reified I : RPCOps> RPCDriverExposedDSLInterface.testProxy(
|
||||
inline fun <reified I : RPCOps> RPCDriverDSL.testProxy(
|
||||
ops: I,
|
||||
rpcUser: User = rpcTestUser,
|
||||
clientConfiguration: RPCClientConfiguration = RPCClientConfiguration.default,
|
||||
@ -55,9 +55,9 @@ open class AbstractRPCTest {
|
||||
}
|
||||
}
|
||||
RPCTestMode.Netty ->
|
||||
startRpcServer(ops = ops, rpcUser = rpcUser, configuration = serverConfiguration).flatMap { server ->
|
||||
startRpcClient<I>(server.broker.hostAndPort!!, rpcUser.username, rpcUser.password, clientConfiguration).map {
|
||||
TestProxy(it, { startArtemisSession(server.broker.hostAndPort!!, rpcUser.username, rpcUser.password) })
|
||||
startRpcServer(ops = ops, rpcUser = rpcUser, configuration = serverConfiguration).flatMap { (broker) ->
|
||||
startRpcClient<I>(broker.hostAndPort!!, rpcUser.username, rpcUser.password, clientConfiguration).map {
|
||||
TestProxy(it, { startArtemisSession(broker.hostAndPort!!, rpcUser.username, rpcUser.password) })
|
||||
}
|
||||
}
|
||||
}.get()
|
||||
|
@ -7,7 +7,7 @@ import net.corda.core.internal.concurrent.thenMatch
|
||||
import net.corda.core.messaging.RPCOps
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.node.services.messaging.rpcContext
|
||||
import net.corda.testing.internal.RPCDriverExposedDSLInterface
|
||||
import net.corda.testing.internal.RPCDriverDSL
|
||||
import net.corda.testing.internal.rpcDriver
|
||||
import net.corda.testing.internal.rpcTestUser
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
@ -26,7 +26,7 @@ import kotlin.test.assertTrue
|
||||
class ClientRPCInfrastructureTests : AbstractRPCTest() {
|
||||
// TODO: Test that timeouts work
|
||||
|
||||
private fun RPCDriverExposedDSLInterface.testProxy(): TestOps {
|
||||
private fun RPCDriverDSL.testProxy(): TestOps {
|
||||
return testProxy<TestOps>(TestOpsImpl()).ops
|
||||
}
|
||||
|
||||
|
@ -1,15 +1,15 @@
|
||||
package net.corda.client.rpc
|
||||
|
||||
import net.corda.client.rpc.internal.RPCClientConfiguration
|
||||
import net.corda.core.messaging.RPCOps
|
||||
import net.corda.core.utilities.millis
|
||||
import net.corda.core.crypto.random63BitValue
|
||||
import net.corda.core.internal.concurrent.fork
|
||||
import net.corda.core.internal.concurrent.transpose
|
||||
import net.corda.core.messaging.RPCOps
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.millis
|
||||
import net.corda.node.services.messaging.RPCServerConfiguration
|
||||
import net.corda.testing.internal.RPCDriverExposedDSLInterface
|
||||
import net.corda.testing.internal.RPCDriverDSL
|
||||
import net.corda.testing.internal.rpcDriver
|
||||
import net.corda.testing.internal.testThreadFactory
|
||||
import org.apache.activemq.artemis.utils.collections.ConcurrentHashSet
|
||||
@ -20,7 +20,10 @@ import org.junit.runners.Parameterized
|
||||
import rx.Observable
|
||||
import rx.subjects.UnicastSubject
|
||||
import java.util.*
|
||||
import java.util.concurrent.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.Executor
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
@RunWith(Parameterized::class)
|
||||
class RPCConcurrencyTests : AbstractRPCTest() {
|
||||
@ -84,7 +87,7 @@ class RPCConcurrencyTests : AbstractRPCTest() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun RPCDriverExposedDSLInterface.testProxy(): TestProxy<TestOps> {
|
||||
private fun RPCDriverDSL.testProxy(): TestProxy<TestOps> {
|
||||
return testProxy<TestOps>(
|
||||
TestOpsImpl(pool),
|
||||
clientConfiguration = RPCClientConfiguration.default.copy(
|
||||
|
@ -5,14 +5,14 @@ import net.corda.client.rpc.internal.RPCClientConfiguration
|
||||
import net.corda.core.messaging.RPCOps
|
||||
import net.corda.core.utilities.minutes
|
||||
import net.corda.core.utilities.seconds
|
||||
import net.corda.testing.internal.performance.div
|
||||
import net.corda.node.services.messaging.RPCServerConfiguration
|
||||
import net.corda.testing.internal.RPCDriverExposedDSLInterface
|
||||
import net.corda.testing.measure
|
||||
import net.corda.testing.internal.RPCDriverDSL
|
||||
import net.corda.testing.internal.performance.div
|
||||
import net.corda.testing.internal.performance.startPublishingFixedRateInjector
|
||||
import net.corda.testing.internal.performance.startReporter
|
||||
import net.corda.testing.internal.performance.startTightLoopInjector
|
||||
import net.corda.testing.internal.rpcDriver
|
||||
import net.corda.testing.measure
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
@ -42,7 +42,7 @@ class RPCPerformanceTests : AbstractRPCTest() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun RPCDriverExposedDSLInterface.testProxy(
|
||||
private fun RPCDriverDSL.testProxy(
|
||||
clientConfiguration: RPCClientConfiguration,
|
||||
serverConfiguration: RPCServerConfiguration
|
||||
): TestProxy<TestOps> {
|
||||
|
@ -1,24 +1,19 @@
|
||||
package net.corda.client.rpc
|
||||
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.messaging.RPCOps
|
||||
import net.corda.node.services.Permissions.Companion.invokeRpc
|
||||
import net.corda.node.services.messaging.rpcContext
|
||||
import net.corda.nodeapi.internal.config.User
|
||||
import net.corda.testing.internal.RPCDriverExposedDSLInterface
|
||||
import net.corda.testing.internal.RPCDriverDSL
|
||||
import net.corda.testing.internal.rpcDriver
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.Parameterized
|
||||
import kotlin.reflect.KVisibility
|
||||
import kotlin.reflect.full.declaredMemberFunctions
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
@RunWith(Parameterized::class)
|
||||
class RPCPermissionsTests : AbstractRPCTest() {
|
||||
companion object {
|
||||
const val DUMMY_FLOW = "StartFlow.net.corda.flows.DummyFlow"
|
||||
const val OTHER_FLOW = "StartFlow.net.corda.flows.OtherFlow"
|
||||
const val ALL_ALLOWED = "ALL"
|
||||
}
|
||||
|
||||
@ -26,18 +21,27 @@ class RPCPermissionsTests : AbstractRPCTest() {
|
||||
* RPC operation.
|
||||
*/
|
||||
interface TestOps : RPCOps {
|
||||
fun validatePermission(str: String)
|
||||
fun validatePermission(method: String, target: String? = null)
|
||||
}
|
||||
|
||||
class TestOpsImpl : TestOps {
|
||||
override val protocolVersion = 1
|
||||
override fun validatePermission(str: String) { rpcContext().requirePermission(str) }
|
||||
override fun validatePermission(method: String, target: String?) {
|
||||
val authorized = if (target == null) {
|
||||
rpcContext().isPermitted(method)
|
||||
} else {
|
||||
rpcContext().isPermitted(method, target)
|
||||
}
|
||||
if (!authorized) {
|
||||
throw PermissionException("RPC user not authorized")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an RPC proxy for the given user.
|
||||
*/
|
||||
private fun RPCDriverExposedDSLInterface.testProxyFor(rpcUser: User) = testProxy<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)
|
||||
|
||||
@ -46,9 +50,9 @@ class RPCPermissionsTests : AbstractRPCTest() {
|
||||
rpcDriver {
|
||||
val emptyUser = userOf("empty", emptySet())
|
||||
val proxy = testProxyFor(emptyUser)
|
||||
assertFailsWith(PermissionException::class,
|
||||
"User ${emptyUser.username} should not be allowed to use $DUMMY_FLOW.",
|
||||
{ proxy.validatePermission(DUMMY_FLOW) })
|
||||
assertNotAllowed {
|
||||
proxy.validatePermission("startFlowDynamic", "net.corda.flows.DummyFlow")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -57,7 +61,8 @@ class RPCPermissionsTests : AbstractRPCTest() {
|
||||
rpcDriver {
|
||||
val adminUser = userOf("admin", setOf(ALL_ALLOWED))
|
||||
val proxy = testProxyFor(adminUser)
|
||||
proxy.validatePermission(DUMMY_FLOW)
|
||||
proxy.validatePermission("startFlowDynamic", "net.corda.flows.DummyFlow")
|
||||
proxy.validatePermission("startTrackedFlowDynamic", "net.corda.flows.DummyFlow")
|
||||
}
|
||||
}
|
||||
|
||||
@ -66,7 +71,8 @@ class RPCPermissionsTests : AbstractRPCTest() {
|
||||
rpcDriver {
|
||||
val joeUser = userOf("joe", setOf(DUMMY_FLOW))
|
||||
val proxy = testProxyFor(joeUser)
|
||||
proxy.validatePermission(DUMMY_FLOW)
|
||||
proxy.validatePermission("startFlowDynamic", "net.corda.flows.DummyFlow")
|
||||
proxy.validatePermission("startTrackedFlowDynamic", "net.corda.flows.DummyFlow")
|
||||
}
|
||||
}
|
||||
|
||||
@ -75,36 +81,46 @@ class RPCPermissionsTests : AbstractRPCTest() {
|
||||
rpcDriver {
|
||||
val joeUser = userOf("joe", setOf(DUMMY_FLOW))
|
||||
val proxy = testProxyFor(joeUser)
|
||||
assertFailsWith(PermissionException::class,
|
||||
"User ${joeUser.username} should not be allowed to use $OTHER_FLOW",
|
||||
{ proxy.validatePermission(OTHER_FLOW) })
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `check ALL is implemented the correct way round`() {
|
||||
rpcDriver {
|
||||
val joeUser = userOf("joe", setOf(DUMMY_FLOW))
|
||||
val proxy = testProxyFor(joeUser)
|
||||
assertFailsWith(PermissionException::class,
|
||||
"Permission $ALL_ALLOWED should not do anything for User ${joeUser.username}",
|
||||
{ proxy.validatePermission(ALL_ALLOWED) })
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fine grained permissions are enforced`() {
|
||||
val allPermissions = CordaRPCOps::class.declaredMemberFunctions.filter { it.visibility == KVisibility.PUBLIC }.map { invokeRpc(it) }
|
||||
allPermissions.forEach { permission ->
|
||||
rpcDriver {
|
||||
val user = userOf("Mark", setOf(permission))
|
||||
val proxy = testProxyFor(user)
|
||||
|
||||
proxy.validatePermission(permission)
|
||||
(allPermissions - permission).forEach { notOwnedPermission ->
|
||||
assertFailsWith(PermissionException::class, { proxy.validatePermission(notOwnedPermission) })
|
||||
}
|
||||
assertNotAllowed {
|
||||
proxy.validatePermission("startFlowDynamic", "net.corda.flows.OtherFlow")
|
||||
}
|
||||
assertNotAllowed {
|
||||
proxy.validatePermission("startTrackedFlowDynamic", "net.corda.flows.OtherFlow")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `joe user is not allowed to call other RPC methods`() {
|
||||
rpcDriver {
|
||||
val joeUser = userOf("joe", setOf(DUMMY_FLOW))
|
||||
val proxy = testProxyFor(joeUser)
|
||||
assertNotAllowed {
|
||||
proxy.validatePermission("nodeInfo")
|
||||
}
|
||||
assertNotAllowed {
|
||||
proxy.validatePermission("networkMapFeed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `checking invokeRpc permissions entitlements`() {
|
||||
rpcDriver {
|
||||
val joeUser = userOf("joe", setOf("InvokeRpc.networkMapFeed"))
|
||||
val proxy = testProxyFor(joeUser)
|
||||
assertNotAllowed {
|
||||
proxy.validatePermission("nodeInfo")
|
||||
}
|
||||
assertNotAllowed {
|
||||
proxy.validatePermission("startTrackedFlowDynamic", "net.corda.flows.OtherFlow")
|
||||
}
|
||||
proxy.validatePermission("networkMapFeed")
|
||||
}
|
||||
}
|
||||
|
||||
private fun assertNotAllowed(action: () -> Unit) {
|
||||
|
||||
assertFailsWith(PermissionException::class, "User should not be allowed to perform this action.", action)
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!-- an all powerful policy file -->
|
||||
<restrict>
|
||||
<http>
|
||||
<method>post</method>
|
||||
@ -8,23 +8,10 @@
|
||||
|
||||
<commands>
|
||||
<command>read</command>
|
||||
<command>write</command>
|
||||
<command>exec</command>
|
||||
<command>list</command>
|
||||
<command>search</command>
|
||||
<command>version</command>
|
||||
</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>
|
||||
|
24
config/prod/jolokia-access.xml
Normal file
24
config/prod/jolokia-access.xml
Normal 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>
|
@ -5,3 +5,4 @@ guavaVersion=21.0
|
||||
bouncycastleVersion=1.57
|
||||
typesafeConfigVersion=1.3.1
|
||||
jsr305Version=3.0.2
|
||||
artifactoryPluginVersion=4.4.18
|
@ -1,13 +1,14 @@
|
||||
package net.corda.core.contracts
|
||||
|
||||
import com.nhaarman.mockito_kotlin.doReturn
|
||||
import com.nhaarman.mockito_kotlin.whenever
|
||||
import net.corda.core.cordapp.CordappProvider
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.internal.UpgradeCommand
|
||||
import net.corda.testing.ALICE
|
||||
import net.corda.testing.DUMMY_NOTARY
|
||||
import net.corda.core.node.ServicesForResolution
|
||||
import net.corda.testing.*
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
import net.corda.testing.contracts.DummyContractV2
|
||||
import net.corda.testing.node.MockServices
|
||||
import net.corda.testing.SerializationEnvironmentRule
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
@ -23,7 +24,9 @@ class DummyContractV2Tests {
|
||||
|
||||
@Test
|
||||
fun `upgrade from v1`() {
|
||||
val services = MockServices()
|
||||
val services = rigorousMock<ServicesForResolution>().also {
|
||||
doReturn(rigorousMock<CordappProvider>()).whenever(it).cordappProvider
|
||||
}
|
||||
val contractUpgrade = DummyContractV2()
|
||||
val v1State = TransactionState(DummyContract.SingleOwnerState(0, ALICE), DummyContract.PROGRAM_ID, DUMMY_NOTARY, constraint = AlwaysAcceptAttachmentConstraint)
|
||||
val v1Ref = StateRef(SecureHash.randomSHA256(), 0)
|
||||
|
@ -337,7 +337,7 @@ class CompositeKeyTests {
|
||||
val ca = X509Utilities.createSelfSignedCACertificate(caName, caKeyPair)
|
||||
|
||||
// Sign the composite key with the self sign CA.
|
||||
val compositeKeyCert = X509Utilities.createCertificate(CertificateType.IDENTITY, ca, caKeyPair, caName.copy(commonName = "CompositeKey"), compositeKey)
|
||||
val compositeKeyCert = X509Utilities.createCertificate(CertificateType.WELL_KNOWN_IDENTITY, ca, caKeyPair, caName.copy(commonName = "CompositeKey"), compositeKey)
|
||||
|
||||
// Store certificate to keystore.
|
||||
val keystorePath = tempFolder.root.toPath() / "keystore.jks"
|
||||
|
@ -129,7 +129,7 @@ class CollectSignaturesFlowTests {
|
||||
@Test
|
||||
fun `fails when not signed by initiator`() {
|
||||
val onePartyDummyContract = DummyContract.generateInitial(1337, notary, alice.ref(1))
|
||||
val miniCorpServices = MockServices(listOf("net.corda.testing.contracts"), MINI_CORP.name, MINI_CORP_KEY)
|
||||
val miniCorpServices = MockServices(listOf("net.corda.testing.contracts"), rigorousMock(), MINI_CORP.name, MINI_CORP_KEY)
|
||||
val ptx = miniCorpServices.signInitialTransaction(onePartyDummyContract)
|
||||
val flow = aliceNode.services.startFlow(CollectSignaturesFlow(ptx, emptySet()))
|
||||
mockNet.runNetwork()
|
||||
|
@ -20,14 +20,17 @@ import net.corda.node.internal.SecureCordaRPCOps
|
||||
import net.corda.node.internal.StartedNode
|
||||
import net.corda.node.services.Permissions.Companion.startFlow
|
||||
import net.corda.nodeapi.internal.config.User
|
||||
import net.corda.testing.*
|
||||
import net.corda.testing.ALICE_NAME
|
||||
import net.corda.testing.BOB_NAME
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
import net.corda.testing.contracts.DummyContractV2
|
||||
import net.corda.testing.internal.RPCDriverExposedDSLInterface
|
||||
import net.corda.testing.internal.RPCDriverDSL
|
||||
import net.corda.testing.internal.rpcDriver
|
||||
import net.corda.testing.internal.rpcTestUser
|
||||
import net.corda.testing.internal.startRpcClient
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import net.corda.testing.singleIdentity
|
||||
import net.corda.testing.startFlow
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
@ -120,7 +123,7 @@ class ContractUpgradeFlowTest {
|
||||
check(bobNode)
|
||||
}
|
||||
|
||||
private fun RPCDriverExposedDSLInterface.startProxy(node: StartedNode<*>, user: User): CordaRPCOps {
|
||||
private fun RPCDriverDSL.startProxy(node: StartedNode<*>, user: User): CordaRPCOps {
|
||||
return startRpcClient<CordaRPCOps>(
|
||||
rpcAddress = startRpcServer(
|
||||
rpcUser = user,
|
||||
|
@ -1,6 +1,7 @@
|
||||
package net.corda.core.serialization
|
||||
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.generateKeyPair
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
@ -49,9 +50,8 @@ class TransactionSerializationTests {
|
||||
val inputState = StateAndRef(TransactionState(TestCash.State(depositRef, 100.POUNDS, MEGA_CORP), TEST_CASH_PROGRAM_ID, DUMMY_NOTARY), fakeStateRef)
|
||||
val outputState = TransactionState(TestCash.State(depositRef, 600.POUNDS, MEGA_CORP), TEST_CASH_PROGRAM_ID, DUMMY_NOTARY)
|
||||
val changeState = TransactionState(TestCash.State(depositRef, 400.POUNDS, MEGA_CORP), TEST_CASH_PROGRAM_ID, DUMMY_NOTARY)
|
||||
|
||||
val megaCorpServices = MockServices(listOf("net.corda.core.serialization"), MEGA_CORP.name, MEGA_CORP_KEY)
|
||||
val notaryServices = MockServices(listOf("net.corda.core.serialization"), DUMMY_NOTARY.name, DUMMY_NOTARY_KEY)
|
||||
val megaCorpServices = MockServices(listOf("net.corda.core.serialization"), rigorousMock(), MEGA_CORP.name, MEGA_CORP_KEY)
|
||||
val notaryServices = MockServices(listOf("net.corda.core.serialization"), rigorousMock(), DUMMY_NOTARY.name, DUMMY_NOTARY_KEY)
|
||||
lateinit var tx: TransactionBuilder
|
||||
|
||||
@Before
|
||||
@ -88,14 +88,14 @@ class TransactionSerializationTests {
|
||||
assertFailsWith(IllegalArgumentException::class) {
|
||||
stx.copy(sigs = emptyList())
|
||||
}
|
||||
|
||||
val DUMMY_KEY_2 = generateKeyPair()
|
||||
// If the signature was replaced in transit, we don't like it.
|
||||
assertFailsWith(SignatureException::class) {
|
||||
val tx2 = TransactionBuilder(DUMMY_NOTARY).withItems(inputState, outputState, changeState,
|
||||
Command(TestCash.Commands.Move(), DUMMY_KEY_2.public))
|
||||
|
||||
val ptx2 = notaryServices.signInitialTransaction(tx2)
|
||||
val dummyServices = MockServices(DUMMY_KEY_2)
|
||||
val dummyServices = MockServices(rigorousMock(), MEGA_CORP.name, DUMMY_KEY_2)
|
||||
val stx2 = dummyServices.addSignature(ptx2)
|
||||
|
||||
stx.copy(sigs = stx2.sigs).verifyRequiredSignatures()
|
||||
|
@ -2,9 +2,11 @@ package net.corda.core.serialization
|
||||
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.generateKeyPair
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.services.UniquenessException
|
||||
import net.corda.core.node.services.UniquenessProvider
|
||||
import net.corda.testing.DUMMY_PARTY
|
||||
import net.corda.testing.SerializationEnvironmentRule
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
@ -19,7 +21,8 @@ class UniquenessExceptionSerializationTest {
|
||||
fun testSerializationRoundTrip() {
|
||||
val txhash = SecureHash.randomSHA256()
|
||||
val txHash2 = SecureHash.randomSHA256()
|
||||
val stateHistory: Map<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 instance = UniquenessException(conflict)
|
||||
|
||||
|
@ -15,6 +15,11 @@ import java.util.function.Predicate
|
||||
import kotlin.test.*
|
||||
|
||||
class CompatibleTransactionTests {
|
||||
private companion object {
|
||||
val DUMMY_KEY_1 = generateKeyPair()
|
||||
val DUMMY_KEY_2 = generateKeyPair()
|
||||
}
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val testSerialization = SerializationEnvironmentRule()
|
||||
|
@ -1,14 +1,15 @@
|
||||
package net.corda.core.transactions
|
||||
|
||||
import com.nhaarman.mockito_kotlin.doReturn
|
||||
import com.nhaarman.mockito_kotlin.whenever
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.generateKeyPair
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.testing.DUMMY_NOTARY
|
||||
import net.corda.testing.SerializationEnvironmentRule
|
||||
import net.corda.node.services.api.IdentityServiceInternal
|
||||
import net.corda.testing.*
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
import net.corda.testing.dummyCommand
|
||||
import net.corda.testing.node.MockServices
|
||||
import net.corda.testing.singleIdentity
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
@ -21,7 +22,10 @@ class LedgerTransactionQueryTests {
|
||||
@Rule
|
||||
@JvmField
|
||||
val testSerialization = SerializationEnvironmentRule()
|
||||
private val services: MockServices = MockServices()
|
||||
private val keyPair = generateKeyPair()
|
||||
private val services = MockServices(rigorousMock<IdentityServiceInternal>().also {
|
||||
doReturn(null).whenever(it).partyFromKey(keyPair.public)
|
||||
}, MEGA_CORP.name, keyPair)
|
||||
private val identity: Party = services.myInfo.singleIdentity()
|
||||
|
||||
@Before
|
||||
|
@ -16,6 +16,11 @@ import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertNotEquals
|
||||
|
||||
class TransactionTests {
|
||||
private companion object {
|
||||
val DUMMY_KEY_1 = generateKeyPair()
|
||||
val DUMMY_KEY_2 = generateKeyPair()
|
||||
}
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val testSerialization = SerializationEnvironmentRule()
|
||||
|
Binary file not shown.
@ -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>`,
|
||||
is required reading for this page.
|
||||
|
||||
Transaction states specify a constraint over the contract that will be used to verify it. For a transaction to be
|
||||
valid, the verify() function associated with each state must run successfully. However, for this to be secure, it is
|
||||
not sufficient to specify the verify() function by name as there may exist multiple different implementations with the
|
||||
same method signature and enclosing class. Contract constraints solve this problem by allowing a contract developer to
|
||||
constrain which verify() functions out of the universe of implementations can be used.
|
||||
(ie the universe is everything that matches the signature and contract constraints restricts this universe to a subset.)
|
||||
valid, the ``verify`` function associated with each state must run successfully. However, for this to be secure, it is
|
||||
not sufficient to specify the ``verify`` function by name as there may exist multiple different implementations with
|
||||
the same method signature and enclosing class. Contract constraints solve this problem by allowing a contract developer
|
||||
to constrain which ``verify`` functions out of the universe of implementations can be used (i.e. the universe is
|
||||
everything that matches the signature and contract constraints restricts this universe to a subset).
|
||||
|
||||
A typical constraint is the hash of the CorDapp JAR that contains the contract and states but will in future releases
|
||||
include constraints that require specific signers of the JAR, or both the signer and the hash. Constraints can be
|
||||
@ -20,12 +20,13 @@ constructs a ``TransactionState`` without specifying the constraint parameter a
|
||||
(``AutomaticHashConstraint``) is used. This default will be automatically resolved to a specific
|
||||
``HashAttachmentConstraint`` that contains the hash of the attachment which contains the contract of that
|
||||
``TransactionState``. This automatic resolution occurs when a ``TransactionBuilder`` is converted to a
|
||||
``WireTransaction``. This reduces the boilerplate involved in finding a specific hash constraint when building a transaction.
|
||||
``WireTransaction``. This reduces the boilerplate involved in finding a specific hash constraint when building a
|
||||
transaction.
|
||||
|
||||
It is possible to specify the constraint explicitly with any other class that implements the ``AttachmentConstraint``
|
||||
interface. To specify a hash manually the ``HashAttachmentConstraint`` can be used and to not provide any constraint
|
||||
the ``AlwaysAcceptAttachmentConstraint`` can be used - though this is intended for testing only. An example below
|
||||
shows how to construct a ``TransactionState`` with an explicitly specified hash constraint from within a flow;
|
||||
shows how to construct a ``TransactionState`` with an explicitly specified hash constraint from within a flow:
|
||||
|
||||
.. sourcecode:: java
|
||||
|
||||
@ -42,12 +43,11 @@ shows how to construct a ``TransactionState`` with an explicitly specified hash
|
||||
LedgerTransaction ltx = wtx.toLedgerTransaction(serviceHub)
|
||||
ltx.verify() // Verifies both the attachment constraints and contracts
|
||||
|
||||
|
||||
This mechanism exists both for integrity and security reasons. It is important not to verify against the wrong contract,
|
||||
which could happen if the wrong version of the contract is attached. More importantly when resolving transaction chains
|
||||
there will, in a future release, be attachments loaded from the network into the attachment sandbox that are used
|
||||
to verify the transaction chain. Ensuring the attachment used is the correct one ensures that the verification will
|
||||
not be tamperable by providing a fake contract.
|
||||
to verify the transaction chain. Ensuring the attachment used is the correct one ensures that the verification is
|
||||
tamper-proof by providing a fake contract.
|
||||
|
||||
CorDapps as attachments
|
||||
-----------------------
|
||||
@ -55,10 +55,10 @@ CorDapps as attachments
|
||||
CorDapp JARs (:doc:`cordapp-overview`) that are installed to the node and contain classes implementing the ``Contract``
|
||||
interface are automatically loaded into the ``AttachmentStorage`` of a node at startup.
|
||||
|
||||
After CorDapps are loaded into the attachment store the node creates a link between contract classes and the
|
||||
attachment that they were loaded from. This makes it possible to find the attachment for any given contract.
|
||||
This is how the automatic resolution of attachments is done by the ``TransactionBuilder`` and how, when verifying
|
||||
the constraints and contracts, attachments are associated with their respective contracts.
|
||||
After CorDapps are loaded into the attachment store the node creates a link between contract classes and the attachment
|
||||
that they were loaded from. This makes it possible to find the attachment for any given contract. This is how the
|
||||
automatic resolution of attachments is done by the ``TransactionBuilder`` and how, when verifying the constraints and
|
||||
contracts, attachments are associated with their respective contracts.
|
||||
|
||||
Implementations
|
||||
---------------
|
||||
@ -95,7 +95,7 @@ to specify JAR URLs in the case that the CorDapp(s) involved in testing already
|
||||
MockNetwork/MockNode
|
||||
********************
|
||||
|
||||
The most simple way to ensure that a vanilla instance of a MockNode generates the correct CorDapps is to use the
|
||||
The simplest way to ensure that a vanilla instance of a MockNode generates the correct CorDapps is to use the
|
||||
``cordappPackages`` constructor parameter (Kotlin) or the ``setCordappPackages`` method on ``MockNetworkParameters`` (Java)
|
||||
when creating the MockNetwork. This will cause the ``AbstractNode`` to use the named packages as sources for CorDapps. All files
|
||||
within those packages will be zipped into a JAR and added to the attachment store and loaded as CorDapps by the
|
@ -6,6 +6,9 @@ from the previous milestone release.
|
||||
|
||||
UNRELEASED
|
||||
----------
|
||||
* Exporting additional JMX metrics (artemis, hibernate statistics) and loading Jolokia agent at JVM startup when using
|
||||
DriverDSL and/or cordformation node runner.
|
||||
|
||||
* Removed confusing property database.initDatabase, enabling its guarded behaviour with the dev-mode.
|
||||
In devMode Hibernate will try to create or update database schemas, otherwise it will expect relevant schemas to be present
|
||||
in the database (pre configured via DDL scripts or equivalent), and validate these are correct.
|
||||
|
@ -4,6 +4,6 @@ Cheat sheet
|
||||
A "cheat sheet" summarizing the key Corda types. A PDF version is downloadable `here`_.
|
||||
|
||||
.. image:: resources/cheatsheet.jpg
|
||||
:width: 700px
|
||||
:width: 700px
|
||||
|
||||
.. _`here`: _static/corda-cheat-sheet.pdf
|
@ -9,6 +9,7 @@ The following are the core APIs that are used in the development of CorDapps:
|
||||
api-states
|
||||
api-persistence
|
||||
api-contracts
|
||||
api-contract-constraints
|
||||
api-vault-query
|
||||
api-transactions
|
||||
api-flows
|
||||
|
@ -68,21 +68,28 @@ path to the node's base directory.
|
||||
|
||||
.. note:: Longer term these keys will be managed in secure hardware devices.
|
||||
|
||||
:database: Database configuration:
|
||||
|
||||
:serverNameTablePrefix: Prefix string to apply to all the database tables. The default is no prefix.
|
||||
:transactionIsolationLevel: Transaction isolation level as defined by the ``TRANSACTION_`` constants in
|
||||
``java.sql.Connection``, but without the "TRANSACTION_" prefix. Defaults to REPEATABLE_READ.
|
||||
:exportHibernateJMXStatistics: Whether to export Hibernate JMX statistics (caution: expensive run-time overhead)
|
||||
|
||||
:dataSourceProperties: This section is used to configure the jdbc connection and database driver used for the nodes persistence.
|
||||
Currently the defaults in ``/node/src/main/resources/reference.conf`` are as shown in the first example. This is currently
|
||||
the only configuration that has been tested, although in the future full support for other storage layers will be validated.
|
||||
|
||||
:database: This section is used to configure JDBC and Hibernate related properties:
|
||||
|
||||
:initDatabase: Boolean on whether to initialise the database or just validate the schema. Defaults to true.
|
||||
|
||||
:schema: (optional) some database providers require a schema name when generating DDL and SQL statements.
|
||||
(the value is passed to Hibernate property 'hibernate.hbm2ddl.auto').
|
||||
:serverNameTablePrefix: Prefix string to apply to all the database tables. The default is no prefix.
|
||||
|
||||
:transactionIsolationLevel: Transaction isolation level as defined by the ``TRANSACTION_`` constants in
|
||||
``java.sql.Connection``, but without the "TRANSACTION_" prefix. Defaults to REPEATABLE_READ.
|
||||
|
||||
:serverNameTablePrefix: Prefix string to apply to all the database tables. The default is no prefix.
|
||||
:exportHibernateJMXStatistics: Whether to export Hibernate JMX statistics (caution: expensive run-time overhead)
|
||||
|
||||
:schema: (optional) some database providers require a schema name when generating DDL and SQL statements.
|
||||
(the value is passed to Hibernate property 'hibernate.hbm2ddl.auto').
|
||||
|
||||
:messagingServerAddress: The address of the ArtemisMQ broker instance. If not provided the node will run one locally.
|
||||
|
||||
@ -169,8 +176,7 @@ path to the node's base directory.
|
||||
Each should be a string. Only the JARs in the directories are added, not the directories themselves. This is useful
|
||||
for including JDBC drivers and the like. e.g. ``jarDirs = [ 'lib' ]``
|
||||
|
||||
:sshd: If provided, node will start internal SSH server which will provide a management shell. It uses the same credentials
|
||||
and permissions as RPC subsystem. It has one required parameter.
|
||||
:sshd: If provided, node will start internal SSH server which will provide a management shell. It uses the same credentials and permissions as RPC subsystem. It has one required parameter.
|
||||
|
||||
:port: The port to start SSH server on
|
||||
|
||||
@ -184,3 +190,6 @@ path to the node's base directory.
|
||||
:privateKeyFile: Path to the private key file for SSH authentication. The private key must not have a passphrase.
|
||||
:publicKeyFile: Path to the public key file for SSH authentication.
|
||||
:sshPort: Port to be used for SSH connection, default ``22``.
|
||||
|
||||
:exportJMXTo: If set to ``http``, will enable JMX metrics reporting via the Jolokia HTTP/JSON agent.
|
||||
Default Jolokia access url is http://127.0.0.1:7005/jolokia/
|
@ -10,6 +10,7 @@ Corda nodes
|
||||
corda-configuration-file
|
||||
clientrpc
|
||||
shell
|
||||
node-auth-config
|
||||
node-database
|
||||
node-administration
|
||||
out-of-process-verification
|
@ -115,19 +115,24 @@ is already correctly configured and this is for reference only;
|
||||
|
||||
Creating the CorDapp JAR
|
||||
------------------------
|
||||
The gradle ``jar`` task included in the CorDapp template build file will automatically build your CorDapp JAR correctly
|
||||
as long as your dependencies are set correctly.
|
||||
Once your dependencies are set correctly, you can build your CorDapp JAR using the gradle ``jar`` task:
|
||||
|
||||
* Unix/Mac OSX: ``./gradlew jar``
|
||||
|
||||
* Windows: ``gradlew.bat jar``
|
||||
|
||||
The CorDapp JAR will be output to the ``build/libs`` folder.
|
||||
|
||||
.. warning:: The hash of the generated CorDapp JAR is not deterministic, as it depends on variables such as the
|
||||
timestamp at creation. Nodes running the same CorDapp must therefore ensure they are using the exact same CorDapp
|
||||
jar, and not different versions of the JAR created from identical sources.
|
||||
JAR, and not different versions of the JAR created from identical sources.
|
||||
|
||||
The filename of the JAR must include a unique identifier to deduplicate it from other releases of the same CorDapp.
|
||||
This is typically done by appending the version string to the CorDapp's name. This unique identifier should not change
|
||||
once the JAR has been deployed on a node. If it does, make sure no one is relying on ``FlowContext.appName`` in their
|
||||
flows (see :doc:`versioning`).
|
||||
|
||||
Installing the CorDapp jar
|
||||
Installing the CorDapp JAR
|
||||
--------------------------
|
||||
|
||||
.. note:: Before installing a CorDapp, you must create one or more nodes to install it on. For instructions, please see
|
||||
@ -136,6 +141,3 @@ Installing the CorDapp jar
|
||||
At runtime, nodes will load any CorDapps present in their ``cordapps`` folder. Therefore in order to install a CorDapp on
|
||||
a node, the CorDapp JAR must be added to the ``<node_dir>/cordapps/`` folder, where ``node_dir`` is the folder in which
|
||||
the node's JAR and configuration files are stored.
|
||||
|
||||
The ``deployNodes`` gradle task, if correctly configured, will automatically place your CorDapp JAR as well as any
|
||||
dependent CorDapp JARs specified into the ``cordapps`` folder automatically.
|
@ -23,7 +23,22 @@ into the ``cordapps`` folder.
|
||||
|
||||
Node naming
|
||||
-----------
|
||||
A node's name must be a valid X.500 name that obeys the following additional constraints:
|
||||
A node's name must be a valid X.500 distinguished name. In order to be compatible with other implementations
|
||||
(particularly TLS implementations), we constrain the allowed X.500 attribute types to a subset of the minimum supported
|
||||
set for X.509 certificates (specified in RFC 3280), plus the locality attribute:
|
||||
|
||||
* Organization (O)
|
||||
* State (ST)
|
||||
* Locality (L)
|
||||
* Country (C)
|
||||
* Organizational-unit (OU)
|
||||
* Common name (CN) (only used for service identities)
|
||||
|
||||
The name must also obey the following constraints:
|
||||
|
||||
* The organisation, locality and country attributes are present
|
||||
|
||||
* The state, organisational-unit and common name attributes are optional
|
||||
|
||||
* The fields of the name have the following maximum character lengths:
|
||||
|
||||
@ -33,21 +48,22 @@ A node's name must be a valid X.500 name that obeys the following additional con
|
||||
* Locality: 64
|
||||
* State: 64
|
||||
|
||||
* The country code is a valid ISO 3166-1 two letter code in upper-case
|
||||
|
||||
* The organisation, locality and country attributes are present
|
||||
* The country attribute is a valid ISO 3166-1 two letter code in upper-case
|
||||
|
||||
* The organisation field of the name obeys the following constraints:
|
||||
|
||||
* Upper-case first letter
|
||||
* Has at least two letters
|
||||
* No leading or trailing whitespace
|
||||
* No double-spacing
|
||||
* Upper-case first letter
|
||||
* Does not contain the words "node" or "server"
|
||||
* Does not include the characters ',' or '=' or '$' or '"' or '\'' or '\\'
|
||||
* Does not include the following characters: ``,`` , ``=`` , ``$`` , ``"`` , ``'`` , ``\``
|
||||
* Is in NFKC normalization form
|
||||
* Only the latin, common and inherited unicode scripts are supported
|
||||
|
||||
* This is to avoid right-to-left issues, debugging issues when we can't pronounce names over the phone, and
|
||||
character confusability attacks
|
||||
|
||||
The Cordform task
|
||||
-----------------
|
||||
Corda provides a gradle plugin called ``Cordform`` that allows you to automatically generate and configure a set of
|
||||
|
@ -42,13 +42,3 @@ Nodes can provide several types of services:
|
||||
updates. Each notary service may be run on a single node, or across a cluster of nodes.
|
||||
* Zero or more **oracle services**. An oracle is a well-known service that signs transactions if they state a fact and
|
||||
that fact is considered to be true.
|
||||
|
||||
These components are illustrated in the following diagram:
|
||||
|
||||
.. image:: resources/cordaNetwork.png
|
||||
:scale: 25%
|
||||
:align: center
|
||||
|
||||
In this diagram, Corda infrastructure services are those upon which all participants depend, such as the network map
|
||||
and notary services. Corda services may be deployed by participants, third parties or a central network operator
|
||||
(such as R3). The diagram is not intended to imply that only a centralised model is supported.
|
@ -34,33 +34,6 @@ only shared with those who need to see them, and planned use of Intel SGX, it is
|
||||
privacy breaches. Confidential identities are used to ensure that even if a third party gets access to an unencrypted
|
||||
transaction, they cannot identify the participants without additional information.
|
||||
|
||||
Name
|
||||
----
|
||||
|
||||
Identity names are X.500 distinguished names with Corda-specific constraints applied. In order to be compatible with
|
||||
other implementations (particularly TLS implementations), we constrain the allowed X.500 attribute types to a subset of
|
||||
the minimum supported set for X.509 certificates (specified in RFC 3280), plus the locality attribute:
|
||||
|
||||
* organization (O)
|
||||
* state (ST)
|
||||
* locality (L)
|
||||
* country (C)
|
||||
* organizational-unit (OU)
|
||||
* common name (CN) - used only for service identities
|
||||
|
||||
The organisation, locality and country attributes are required, while state, organisational-unit and common name are
|
||||
optional. Attributes cannot be be present more than once in the name.
|
||||
|
||||
All of these attributes have the following set of constraints applied for security reasons:
|
||||
|
||||
- No blacklisted words (currently "node" and "server").
|
||||
- Restrict names to Latin scripts for now to avoid right-to-left issues, debugging issues when we can't pronounce names over the phone, and character confusability attacks.
|
||||
- No commas or equals signs.
|
||||
- No dollars or quote marks.
|
||||
|
||||
Additionally the "organisation" attribute must consist of at least three letters and starting with a capital letter,
|
||||
and "country code" is strictly restricted to valid ISO 3166-1 two letter codes.
|
||||
|
||||
Certificates
|
||||
------------
|
||||
|
||||
@ -83,5 +56,3 @@ to the main network map service, for operational reasons. Identities registered
|
||||
considered well known, and it is never appropriate to store confidential identities in a central directory without
|
||||
controls applied at the record level to ensure only those who require access to an identity can retrieve its
|
||||
certificate.
|
||||
|
||||
.. TODO: Revisit once design & use cases of private maps is further fleshed out
|
@ -16,7 +16,6 @@ This section should be read in order:
|
||||
key-concepts-identity
|
||||
key-concepts-states
|
||||
key-concepts-contracts
|
||||
key-concepts-contract-constraints
|
||||
key-concepts-transactions
|
||||
key-concepts-flows
|
||||
key-concepts-consensus
|
||||
|
@ -93,6 +93,8 @@ formats for accessing MBeans, and provides client libraries to work with that pr
|
||||
|
||||
Here are a few ways to build dashboards and extract monitoring data for a node:
|
||||
|
||||
* `hawtio <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
|
||||
scrape the statistics found there, then insert them into the Graphite monitoring tool on a regular basis. It runs
|
||||
in Docker and can be started with a single command.
|
||||
@ -105,6 +107,29 @@ Here are a few ways to build dashboards and extract monitoring data for a node:
|
||||
It can bridge any data input to any output using their plugin system, for example, Telegraf can
|
||||
be configured to collect data from Jolokia and write to DataDog web api.
|
||||
|
||||
The Node configuration parameter `exportJMXTo` should be set to ``http`` to ensure a Jolokia agent is instrumented with
|
||||
the JVM run-time.
|
||||
|
||||
The following JMX statistics are exported:
|
||||
|
||||
* Corda specific metrics: flow information (total started, finished, in-flight; flow duration by flow type), attachments (count)
|
||||
* Apache Artemis metrics: queue information for P2P and RPC services
|
||||
* JVM statistics: classloading, garbage collection, memory, runtime, threading, operating system
|
||||
* Hibernate statistics (only when node is started-up in `devMode` due to to expensive run-time costs)
|
||||
|
||||
When starting Corda nodes using Cordformation runner (see :doc:`running-a-node`), you should see a startup message similar to the following:
|
||||
**Jolokia: Agent started with URL http://127.0.0.1:7005/jolokia/**
|
||||
|
||||
When starting Corda nodes using the `DriverDSL`, you should see a startup message in the logs similar to the following:
|
||||
**Starting out-of-process Node USA Bank Corp, debug port is not enabled, jolokia monitoring port is 7005 {}**
|
||||
|
||||
Several Jolokia policy based security configuration files (``jolokia-access.xml``) are available for dev, test, and prod
|
||||
environments under ``/config/<env>``.
|
||||
|
||||
The following diagram illustrates Corda flow metrics visualized using `hawtio <https://hawt.io>`_ :
|
||||
|
||||
.. image:: resources/hawtio-jmx.png
|
||||
|
||||
Memory usage and tuning
|
||||
-----------------------
|
||||
|
||||
|
136
docs/source/node-auth-config.rst
Normal file
136
docs/source/node-auth-config.rst
Normal 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 |
BIN
docs/source/resources/hawtio-jmx.png
Normal file
BIN
docs/source/resources/hawtio-jmx.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 242 KiB |
@ -17,7 +17,6 @@ import net.corda.testing.contracts.VaultFiller
|
||||
import net.corda.testing.node.MockServices
|
||||
import net.corda.testing.node.MockServices.Companion.makeTestDatabaseAndMockServices
|
||||
import net.corda.testing.node.makeTestIdentityService
|
||||
import org.junit.Ignore
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
@ -224,19 +223,16 @@ class CommercialPaperTestsGeneric {
|
||||
private lateinit var aliceServices: MockServices
|
||||
private lateinit var aliceVaultService: VaultService
|
||||
private lateinit var alicesVault: Vault<ContractState>
|
||||
|
||||
private val notaryServices = MockServices(DUMMY_NOTARY_KEY)
|
||||
private val issuerServices = MockServices(DUMMY_CASH_ISSUER_KEY)
|
||||
|
||||
private val notaryServices = MockServices(rigorousMock(), MEGA_CORP.name, DUMMY_NOTARY_KEY)
|
||||
private val issuerServices = MockServices(listOf("net.corda.finance.contracts"), rigorousMock(), MEGA_CORP.name, DUMMY_CASH_ISSUER_KEY)
|
||||
private lateinit var moveTX: SignedTransaction
|
||||
|
||||
// @Test
|
||||
@Ignore
|
||||
fun `issue move and then redeem`() = withTestSerialization {
|
||||
@Test
|
||||
fun `issue move and then redeem`() {
|
||||
val aliceDatabaseAndServices = makeTestDatabaseAndMockServices(
|
||||
listOf(ALICE_KEY),
|
||||
makeTestIdentityService(listOf(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, DUMMY_CASH_ISSUER_IDENTITY, DUMMY_NOTARY_IDENTITY)),
|
||||
initialIdentityName = MEGA_CORP.name)
|
||||
listOf("net.corda.finance.contracts"),
|
||||
MEGA_CORP.name)
|
||||
val databaseAlice = aliceDatabaseAndServices.first
|
||||
aliceServices = aliceDatabaseAndServices.second
|
||||
aliceVaultService = aliceServices.vaultService
|
||||
@ -248,7 +244,8 @@ class CommercialPaperTestsGeneric {
|
||||
val bigCorpDatabaseAndServices = makeTestDatabaseAndMockServices(
|
||||
listOf(BIG_CORP_KEY),
|
||||
makeTestIdentityService(listOf(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, DUMMY_CASH_ISSUER_IDENTITY, DUMMY_NOTARY_IDENTITY)),
|
||||
initialIdentityName = MEGA_CORP.name)
|
||||
listOf("net.corda.finance.contracts"),
|
||||
MEGA_CORP.name)
|
||||
val databaseBigCorp = bigCorpDatabaseAndServices.first
|
||||
bigCorpServices = bigCorpDatabaseAndServices.second
|
||||
bigCorpVaultService = bigCorpServices.vaultService
|
||||
|
@ -1,5 +1,6 @@
|
||||
package net.corda.finance.contracts.asset
|
||||
|
||||
import com.nhaarman.mockito_kotlin.*
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.generateKeyPair
|
||||
@ -18,6 +19,7 @@ import net.corda.finance.utils.sumCash
|
||||
import net.corda.finance.utils.sumCashBy
|
||||
import net.corda.finance.utils.sumCashOrNull
|
||||
import net.corda.finance.utils.sumCashOrZero
|
||||
import net.corda.node.services.api.IdentityServiceInternal
|
||||
import net.corda.node.services.vault.NodeVaultService
|
||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||
import net.corda.testing.*
|
||||
@ -67,9 +69,11 @@ class CashTests {
|
||||
@Before
|
||||
fun setUp() {
|
||||
LogHelper.setLevel(NodeVaultService::class)
|
||||
megaCorpServices = MockServices(listOf("net.corda.finance.contracts.asset"), MEGA_CORP.name, MEGA_CORP_KEY)
|
||||
miniCorpServices = MockServices(listOf("net.corda.finance.contracts.asset"), MINI_CORP.name, MINI_CORP_KEY)
|
||||
val notaryServices = MockServices(listOf("net.corda.finance.contracts.asset"), DUMMY_NOTARY.name, DUMMY_NOTARY_KEY)
|
||||
megaCorpServices = MockServices(listOf("net.corda.finance.contracts.asset"), rigorousMock(), MEGA_CORP.name, MEGA_CORP_KEY)
|
||||
miniCorpServices = MockServices(listOf("net.corda.finance.contracts.asset"), rigorousMock<IdentityServiceInternal>().also {
|
||||
doNothing().whenever(it).justVerifyAndRegisterIdentity(argThat { name == MINI_CORP.name })
|
||||
}, MINI_CORP.name, MINI_CORP_KEY)
|
||||
val notaryServices = MockServices(listOf("net.corda.finance.contracts.asset"), rigorousMock(), DUMMY_NOTARY.name, DUMMY_NOTARY_KEY)
|
||||
val databaseAndServices = makeTestDatabaseAndMockServices(
|
||||
listOf(generateKeyPair()),
|
||||
makeTestIdentityService(listOf(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, DUMMY_CASH_ISSUER_IDENTITY, DUMMY_NOTARY_IDENTITY)),
|
||||
@ -502,10 +506,9 @@ class CashTests {
|
||||
|
||||
private fun makeSpend(services: ServiceHub, amount: Amount<Currency>, dest: AbstractParty): WireTransaction {
|
||||
val ourIdentity = services.myInfo.singleIdentityAndCert()
|
||||
val changeIdentity = services.keyManagementService.freshKeyAndCert(ourIdentity, false)
|
||||
val tx = TransactionBuilder(DUMMY_NOTARY)
|
||||
database.transaction {
|
||||
Cash.generateSpend(services, tx, amount, changeIdentity, dest)
|
||||
Cash.generateSpend(services, tx, amount, ourIdentity, dest)
|
||||
}
|
||||
return tx.toWireTransaction(services)
|
||||
}
|
||||
@ -601,11 +604,10 @@ class CashTests {
|
||||
|
||||
@Test
|
||||
fun generateSimpleSpendWithParties() {
|
||||
val changeIdentity = ourServices.keyManagementService.freshKeyAndCert(ourServices.myInfo.singleIdentityAndCert(), false)
|
||||
database.transaction {
|
||||
|
||||
val tx = TransactionBuilder(DUMMY_NOTARY)
|
||||
Cash.generateSpend(ourServices, tx, 80.DOLLARS, changeIdentity, ALICE, setOf(MINI_CORP))
|
||||
Cash.generateSpend(ourServices, tx, 80.DOLLARS, ourServices.myInfo.singleIdentityAndCert(), ALICE, setOf(MINI_CORP))
|
||||
|
||||
assertEquals(vaultStatesUnconsumed.elementAt(2).ref, tx.inputStates()[0])
|
||||
}
|
||||
@ -774,8 +776,9 @@ class CashTests {
|
||||
// Double spend.
|
||||
@Test
|
||||
fun chainCashDoubleSpendFailsWith() {
|
||||
val mockService = MockServices(listOf("net.corda.finance.contracts.asset"), MEGA_CORP.name, MEGA_CORP_KEY)
|
||||
|
||||
val mockService = MockServices(listOf("net.corda.finance.contracts.asset"), rigorousMock<IdentityServiceInternal>().also {
|
||||
doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY)
|
||||
}, MEGA_CORP.name, MEGA_CORP_KEY)
|
||||
ledger(mockService) {
|
||||
unverifiedTransaction {
|
||||
attachment(Cash.PROGRAM_ID)
|
||||
@ -813,12 +816,11 @@ class CashTests {
|
||||
fun multiSpend() {
|
||||
val tx = TransactionBuilder(DUMMY_NOTARY)
|
||||
database.transaction {
|
||||
val changeIdentity = ourServices.keyManagementService.freshKeyAndCert(ourServices.myInfo.singleIdentityAndCert(), false)
|
||||
val payments = listOf(
|
||||
PartyAndAmount(miniCorpAnonymised, 400.DOLLARS),
|
||||
PartyAndAmount(CHARLIE_ANONYMISED, 150.DOLLARS)
|
||||
)
|
||||
Cash.generateSpend(ourServices, tx, payments, changeIdentity)
|
||||
Cash.generateSpend(ourServices, tx, payments, ourServices.myInfo.singleIdentityAndCert())
|
||||
}
|
||||
val wtx = tx.toWireTransaction(ourServices)
|
||||
fun out(i: Int) = wtx.getOutput(i) as Cash.State
|
||||
|
@ -1,5 +1,7 @@
|
||||
package net.corda.finance.contracts.asset
|
||||
|
||||
import com.nhaarman.mockito_kotlin.doReturn
|
||||
import com.nhaarman.mockito_kotlin.whenever
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.NullKeys.NULL_PARTY
|
||||
import net.corda.core.crypto.SecureHash
|
||||
@ -15,6 +17,7 @@ import net.corda.finance.*
|
||||
import net.corda.finance.contracts.Commodity
|
||||
import net.corda.finance.contracts.NetType
|
||||
import net.corda.finance.contracts.asset.Obligation.Lifecycle
|
||||
import net.corda.node.services.api.IdentityServiceInternal
|
||||
import net.corda.testing.*
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
import net.corda.testing.contracts.DummyState
|
||||
@ -51,9 +54,13 @@ class ObligationTests {
|
||||
beneficiary = CHARLIE
|
||||
)
|
||||
private val outState = inState.copy(beneficiary = AnonymousParty(BOB_PUBKEY))
|
||||
private val miniCorpServices = MockServices(listOf("net.corda.finance.contracts.asset"), MINI_CORP.name, MINI_CORP_KEY)
|
||||
private val notaryServices = MockServices(DUMMY_NOTARY_KEY)
|
||||
private val mockService = MockServices(listOf("net.corda.finance.contracts.asset"))
|
||||
private val miniCorpServices = MockServices(listOf("net.corda.finance.contracts.asset"), rigorousMock(), MINI_CORP.name, MINI_CORP_KEY)
|
||||
private val notaryServices = MockServices(rigorousMock(), MEGA_CORP.name, DUMMY_NOTARY_KEY)
|
||||
private val mockService = MockServices(listOf("net.corda.finance.contracts.asset"), rigorousMock<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(
|
||||
group: LedgerDSL<TestTransactionDSLInterpreter, TestLedgerDSLInterpreter>
|
||||
|
@ -1,5 +1,6 @@
|
||||
apply plugin: 'java'
|
||||
apply plugin: 'net.corda.plugins.publish-utils'
|
||||
apply plugin: 'com.jfrog.artifactory'
|
||||
|
||||
description "Generates a summary of the artifact's public API"
|
||||
|
||||
|
@ -7,11 +7,14 @@ buildscript {
|
||||
file("$projectDir/../constants.properties").withInputStream { constants.load(it) }
|
||||
|
||||
// If you bump this version you must re-bootstrap the codebase. See the README for more information.
|
||||
ext.gradle_plugins_version = constants.getProperty("gradlePluginsVersion")
|
||||
ext.bouncycastle_version = constants.getProperty("bouncycastleVersion")
|
||||
ext.typesafe_config_version = constants.getProperty("typesafeConfigVersion")
|
||||
ext.jsr305_version = constants.getProperty("jsr305Version")
|
||||
ext.kotlin_version = constants.getProperty("kotlinVersion")
|
||||
ext {
|
||||
gradle_plugins_version = constants.getProperty("gradlePluginsVersion")
|
||||
bouncycastle_version = constants.getProperty("bouncycastleVersion")
|
||||
typesafe_config_version = constants.getProperty("typesafeConfigVersion")
|
||||
jsr305_version = constants.getProperty("jsr305Version")
|
||||
kotlin_version = constants.getProperty("kotlinVersion")
|
||||
artifactory_plugin_version = constants.getProperty('artifactoryPluginVersion')
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenLocal()
|
||||
@ -22,10 +25,12 @@ buildscript {
|
||||
classpath "net.corda.plugins:publish-utils:$gradle_plugins_version"
|
||||
classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.7.3'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath "org.jfrog.buildinfo:build-info-extractor-gradle:$artifactory_plugin_version"
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: 'net.corda.plugins.publish-utils'
|
||||
apply plugin: 'com.jfrog.artifactory'
|
||||
|
||||
allprojects {
|
||||
version gradle_plugins_version
|
||||
@ -54,3 +59,25 @@ bintrayConfig {
|
||||
email = 'dev@corda.net'
|
||||
}
|
||||
}
|
||||
|
||||
artifactory {
|
||||
publish {
|
||||
contextUrl = 'https://ci-artifactory.corda.r3cev.com/artifactory'
|
||||
repository {
|
||||
repoKey = 'corda-dev'
|
||||
username = 'teamcity'
|
||||
password = System.getenv('CORDA_ARTIFACTORY_PASSWORD')
|
||||
}
|
||||
|
||||
defaults {
|
||||
// Publish utils does not have a publish block because it would be circular for it to apply it's own
|
||||
// extensions to itself
|
||||
if(project.name == 'publish-utils') {
|
||||
publications('publishUtils')
|
||||
// Root project applies the plugin (for this block) but does not need to be published
|
||||
} else if(project != rootProject) {
|
||||
publications(project.extensions.publish.name())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: 'net.corda.plugins.publish-utils'
|
||||
apply plugin: 'com.jfrog.artifactory'
|
||||
|
||||
description 'Turns a project into a cordapp project that produces cordapp fat JARs'
|
||||
|
||||
|
@ -23,6 +23,13 @@ class Utils {
|
||||
project.configurations.single { it.name == "compile" }.extendsFrom(configuration)
|
||||
}
|
||||
}
|
||||
fun createRuntimeConfiguration(name: String, project: Project) {
|
||||
if(!project.configurations.any { it.name == name }) {
|
||||
val configuration = project.configurations.create(name)
|
||||
configuration.isTransitive = false
|
||||
project.configurations.single { it.name == "runtime" }.extendsFrom(configuration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
apply plugin: 'java'
|
||||
apply plugin: 'maven-publish'
|
||||
apply plugin: 'net.corda.plugins.publish-utils'
|
||||
apply plugin: 'com.jfrog.artifactory'
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
|
@ -10,6 +10,7 @@ buildscript {
|
||||
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: 'net.corda.plugins.publish-utils'
|
||||
apply plugin: 'com.jfrog.artifactory'
|
||||
|
||||
description 'A small gradle plugin for adding some basic Quasar tasks and configurations to reduce build.gradle bloat.'
|
||||
|
||||
|
@ -10,6 +10,8 @@ import java.io.File
|
||||
*/
|
||||
class Cordformation : Plugin<Project> {
|
||||
internal companion object {
|
||||
const val CORDFORMATION_TYPE = "cordformationInternal"
|
||||
|
||||
/**
|
||||
* Gets a resource file from this plugin's JAR file.
|
||||
*
|
||||
@ -31,5 +33,8 @@ class Cordformation : Plugin<Project> {
|
||||
|
||||
override fun apply(project: 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")
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
package net.corda.plugins
|
||||
|
||||
import com.typesafe.config.*
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import com.typesafe.config.ConfigRenderOptions
|
||||
import com.typesafe.config.ConfigValueFactory
|
||||
import net.corda.cordform.CordformNode
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import org.bouncycastle.asn1.x500.style.BCStyle
|
||||
@ -90,6 +92,7 @@ class Node(private val project: Project) : CordformNode() {
|
||||
if (config.hasPath("webAddress")) {
|
||||
installWebserverJar()
|
||||
}
|
||||
installAgentJar()
|
||||
installBuiltCordapp()
|
||||
installCordapps()
|
||||
installConfig()
|
||||
@ -177,6 +180,29 @@ class Node(private val project: Project) : CordformNode() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs the jolokia monitoring agent JAR to the node/drivers directory
|
||||
*/
|
||||
private fun installAgentJar() {
|
||||
val jolokiaVersion = project.rootProject.ext<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.
|
||||
*/
|
||||
|
@ -22,6 +22,11 @@ private object debugPortAlloc {
|
||||
internal fun next() = basePort++
|
||||
}
|
||||
|
||||
private object monitoringPortAlloc {
|
||||
private var basePort = 7005
|
||||
internal fun next() = basePort++
|
||||
}
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
val startedProcesses = mutableListOf<Process>()
|
||||
val headless = GraphicsEnvironment.isHeadless() || args.contains(HEADLESS_FLAG)
|
||||
@ -49,8 +54,9 @@ private abstract class JarType(private val jarName: String) {
|
||||
return null
|
||||
}
|
||||
val debugPort = debugPortAlloc.next()
|
||||
val monitoringPort = monitoringPortAlloc.next()
|
||||
println("Starting $jarName in $dir on debug port $debugPort")
|
||||
val process = (if (headless) ::HeadlessJavaCommand else ::TerminalWindowJavaCommand)(jarName, dir, debugPort, javaArgs, jvmArgs).start()
|
||||
val process = (if (headless) ::HeadlessJavaCommand else ::TerminalWindowJavaCommand)(jarName, dir, debugPort, monitoringPort, javaArgs, jvmArgs).start()
|
||||
if (os == OS.MACOS) Thread.sleep(1000)
|
||||
return process
|
||||
}
|
||||
@ -69,15 +75,23 @@ private abstract class JavaCommand(
|
||||
jarName: String,
|
||||
internal val dir: File,
|
||||
debugPort: Int?,
|
||||
monitoringPort: Int?,
|
||||
internal val nodeName: String,
|
||||
init: MutableList<String>.() -> Unit, args: 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 {
|
||||
add(getJavaPath())
|
||||
addAll(jvmArgs)
|
||||
add("-Dname=$nodeName")
|
||||
null != debugPort && add("-Dcapsule.jvm.args=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=$debugPort")
|
||||
null != monitoringPort && add("-Dcapsule.jvm.args=-javaagent:drivers/$jolokiaJar=port=$monitoringPort")
|
||||
add("-jar")
|
||||
add(jarName)
|
||||
init()
|
||||
@ -89,14 +103,14 @@ private abstract class JavaCommand(
|
||||
internal abstract fun getJavaPath(): String
|
||||
}
|
||||
|
||||
private class HeadlessJavaCommand(jarName: String, dir: File, debugPort: Int?, args: List<String>, jvmArgs: List<String>)
|
||||
: JavaCommand(jarName, dir, debugPort, dir.name, { add("--no-local-shell") }, args, jvmArgs) {
|
||||
private class HeadlessJavaCommand(jarName: String, dir: File, debugPort: Int?, monitoringPort: Int?, args: List<String>, jvmArgs: List<String>)
|
||||
: JavaCommand(jarName, dir, debugPort, monitoringPort, dir.name, { add("--no-local-shell") }, args, jvmArgs) {
|
||||
override fun processBuilder() = ProcessBuilder(command).redirectError(File("error.$nodeName.log")).inheritIO()
|
||||
override fun getJavaPath() = File(File(System.getProperty("java.home"), "bin"), "java").path
|
||||
}
|
||||
|
||||
private class TerminalWindowJavaCommand(jarName: String, dir: File, debugPort: Int?, args: List<String>, jvmArgs: List<String>)
|
||||
: JavaCommand(jarName, dir, debugPort, "${dir.name}-$jarName", {}, args, jvmArgs) {
|
||||
private class TerminalWindowJavaCommand(jarName: String, dir: File, debugPort: Int?, monitoringPort: Int?, args: List<String>, jvmArgs: List<String>)
|
||||
: JavaCommand(jarName, dir, debugPort, monitoringPort, "${dir.name}-$jarName", {}, args, jvmArgs) {
|
||||
override fun processBuilder() = ProcessBuilder(when (os) {
|
||||
OS.MACOS -> {
|
||||
listOf("osascript", "-e", """tell app "Terminal"
|
||||
|
@ -1,13 +1,17 @@
|
||||
apply plugin: 'groovy'
|
||||
apply plugin: 'maven-publish'
|
||||
apply plugin: 'com.jfrog.bintray'
|
||||
apply plugin: 'com.jfrog.artifactory'
|
||||
|
||||
// Used for bootstrapping project
|
||||
buildscript {
|
||||
Properties constants = new Properties()
|
||||
file("../../constants.properties").withInputStream { constants.load(it) }
|
||||
|
||||
ext.gradle_plugins_version = constants.getProperty("gradlePluginsVersion")
|
||||
ext {
|
||||
gradle_plugins_version = constants.getProperty("gradlePluginsVersion")
|
||||
artifactory_plugin_version = constants.getProperty('artifactoryPluginVersion')
|
||||
}
|
||||
|
||||
repositories {
|
||||
jcenter()
|
||||
@ -15,6 +19,7 @@ buildscript {
|
||||
|
||||
dependencies {
|
||||
classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.4'
|
||||
classpath "org.jfrog.buildinfo:build-info-extractor-gradle:$artifactory_plugin_version"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
apply plugin: 'groovy'
|
||||
apply plugin: 'maven-publish'
|
||||
apply plugin: 'net.corda.plugins.publish-utils'
|
||||
apply plugin: 'com.jfrog.artifactory'
|
||||
|
||||
description 'A small gradle plugin for adding some basic Quasar tasks and configurations to reduce build.gradle bloat.'
|
||||
|
||||
|
@ -80,6 +80,7 @@ private fun Config.getSingleValue(path: String, type: KType): Any? {
|
||||
URL::class -> URL(getString(path))
|
||||
CordaX500Name::class -> CordaX500Name.parse(getString(path))
|
||||
Properties::class -> getConfig(path).toProperties()
|
||||
Config::class -> getConfig(path)
|
||||
else -> if (typeClass.java.isEnum) {
|
||||
parseEnum(typeClass.java, getString(path))
|
||||
} else {
|
||||
|
@ -8,7 +8,6 @@ import java.security.KeyPair
|
||||
import java.security.PublicKey
|
||||
import java.security.cert.CertPath
|
||||
import java.security.cert.Certificate
|
||||
import java.security.cert.CertificateFactory
|
||||
|
||||
class KeyStoreWrapper(private val storePath: Path, private val storePassword: String) {
|
||||
private val keyStore = storePath.read { loadKeyStore(it, storePassword) }
|
||||
@ -18,7 +17,7 @@ class KeyStoreWrapper(private val storePath: Path, private val storePassword: St
|
||||
// Assume key password = store password.
|
||||
val clientCA = certificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA)
|
||||
// Create new keys and store in keystore.
|
||||
val cert = X509Utilities.createCertificate(CertificateType.IDENTITY, clientCA.certificate, clientCA.keyPair, serviceName, pubKey)
|
||||
val cert = X509Utilities.createCertificate(CertificateType.WELL_KNOWN_IDENTITY, clientCA.certificate, clientCA.keyPair, serviceName, pubKey)
|
||||
val certPath = X509CertificateFactory().delegate.generateCertPath(listOf(cert.cert) + clientCertPath)
|
||||
require(certPath.certificates.isNotEmpty()) { "Certificate path cannot be empty" }
|
||||
// TODO: X509Utilities.validateCertificateChain()
|
||||
|
@ -332,7 +332,7 @@ enum class CertificateType(val keyUsage: KeyUsage, vararg val purposes: KeyPurpo
|
||||
isCA = true
|
||||
),
|
||||
|
||||
CLIENT_CA(
|
||||
NODE_CA(
|
||||
KeyUsage(KeyUsage.digitalSignature or KeyUsage.keyCertSign or KeyUsage.cRLSign),
|
||||
KeyPurposeId.id_kp_serverAuth,
|
||||
KeyPurposeId.id_kp_clientAuth,
|
||||
@ -349,12 +349,20 @@ enum class CertificateType(val keyUsage: KeyUsage, vararg val purposes: KeyPurpo
|
||||
),
|
||||
|
||||
// TODO: Identity certs should have only limited depth (i.e. 1) CA signing capability, with tight name constraints
|
||||
IDENTITY(
|
||||
WELL_KNOWN_IDENTITY(
|
||||
KeyUsage(KeyUsage.digitalSignature or KeyUsage.keyCertSign),
|
||||
KeyPurposeId.id_kp_serverAuth,
|
||||
KeyPurposeId.id_kp_clientAuth,
|
||||
KeyPurposeId.anyExtendedKeyUsage,
|
||||
isCA = true
|
||||
),
|
||||
|
||||
CONFIDENTIAL_IDENTITY(
|
||||
KeyUsage(KeyUsage.digitalSignature),
|
||||
KeyPurposeId.id_kp_serverAuth,
|
||||
KeyPurposeId.id_kp_clientAuth,
|
||||
KeyPurposeId.anyExtendedKeyUsage,
|
||||
isCA = false
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -21,7 +21,8 @@ data class DatabaseConfig(
|
||||
val initialiseSchema: Boolean = true,
|
||||
val serverNameTablePrefix: String = "",
|
||||
val transactionIsolationLevel: TransactionIsolationLevel = TransactionIsolationLevel.REPEATABLE_READ,
|
||||
val schema: String? = null
|
||||
val schema: String? = null,
|
||||
val exportHibernateJMXStatistics: Boolean = false
|
||||
)
|
||||
|
||||
// This class forms part of the node config and so any changes to it must be handled with care
|
||||
|
@ -17,8 +17,10 @@ import org.hibernate.type.AbstractSingleColumnStandardBasicType
|
||||
import org.hibernate.type.descriptor.java.PrimitiveByteArrayTypeDescriptor
|
||||
import org.hibernate.type.descriptor.sql.BlobTypeDescriptor
|
||||
import org.hibernate.type.descriptor.sql.VarbinaryTypeDescriptor
|
||||
import java.lang.management.ManagementFactory
|
||||
import java.sql.Connection
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import javax.management.ObjectName
|
||||
import javax.persistence.AttributeConverter
|
||||
|
||||
class HibernateConfiguration(
|
||||
@ -65,9 +67,31 @@ class HibernateConfiguration(
|
||||
|
||||
val sessionFactory = buildSessionFactory(config, metadataSources, databaseConfig.serverNameTablePrefix)
|
||||
logger.info("Created session factory for schemas: $schemas")
|
||||
|
||||
// export Hibernate JMX statistics
|
||||
if (databaseConfig.exportHibernateJMXStatistics)
|
||||
initStatistics(sessionFactory)
|
||||
|
||||
return sessionFactory
|
||||
}
|
||||
|
||||
// NOTE: workaround suggested to overcome deprecation of StatisticsService (since Hibernate v4.0)
|
||||
// https://stackoverflow.com/questions/23606092/hibernate-upgrade-statisticsservice
|
||||
fun initStatistics(sessionFactory: SessionFactory) {
|
||||
val statsName = ObjectName("org.hibernate:type=statistics")
|
||||
val mbeanServer = ManagementFactory.getPlatformMBeanServer()
|
||||
|
||||
val statisticsMBean = DelegatingStatisticsService(sessionFactory.statistics)
|
||||
statisticsMBean.isStatisticsEnabled = true
|
||||
|
||||
try {
|
||||
mbeanServer.registerMBean(statisticsMBean, statsName)
|
||||
}
|
||||
catch (e: Exception) {
|
||||
logger.warn(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildSessionFactory(config: Configuration, metadataSources: MetadataSources, tablePrefix: String): SessionFactory {
|
||||
config.standardServiceRegistryBuilder.applySettings(config.properties)
|
||||
val metadata = metadataSources.getMetadataBuilder(config.standardServiceRegistryBuilder.build()).run {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -27,9 +27,9 @@ import java.util.*
|
||||
* transformation rules we create a mapping between those values and the values that exist on the
|
||||
* current class
|
||||
*
|
||||
* @property clazz The enum as it exists now, not as it did when it was serialized (either in the past
|
||||
* @property type The enum as it exists now, not as it did when it was serialized (either in the past
|
||||
* or future).
|
||||
* @property factory the [SerializerFactory] that is building this serialization object.
|
||||
* @param factory the [SerializerFactory] that is building this serialization object.
|
||||
* @property conversions A mapping between all potential enum constants that could've been assigned to
|
||||
* an instance of the enum as it existed at time of serialisation and those that exist now
|
||||
* @property ordinals Convenience mapping of constant to ordinality
|
||||
@ -57,7 +57,7 @@ class EnumEvolutionSerializer(
|
||||
* received AMQP header
|
||||
* @param new The Serializer object we built based on the current state of the enum class on our classpath
|
||||
* @param factory the [SerializerFactory] that is building this serialization object.
|
||||
* @param transformsFromBlob the transforms attached to the class in the AMQP header, i.e. the transforms
|
||||
* @param schemas the transforms attached to the class in the AMQP header, i.e. the transforms
|
||||
* known at serialization time
|
||||
*/
|
||||
fun make(old: RestrictedType,
|
||||
|
@ -32,17 +32,16 @@ sealed class PropertySerializer(val name: String, val readMethod: Method?, val r
|
||||
return if (isInterface) listOf(SerializerFactory.nameForType(resolvedType)) else emptyList()
|
||||
}
|
||||
|
||||
private fun generateDefault(): String? {
|
||||
if (isJVMPrimitive) {
|
||||
return when (resolvedType) {
|
||||
java.lang.Boolean.TYPE -> "false"
|
||||
java.lang.Character.TYPE -> "�"
|
||||
else -> "0"
|
||||
private fun generateDefault(): String? =
|
||||
if (isJVMPrimitive) {
|
||||
when (resolvedType) {
|
||||
java.lang.Boolean.TYPE -> "false"
|
||||
java.lang.Character.TYPE -> "�"
|
||||
else -> "0"
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateMandatory(): Boolean {
|
||||
return isJVMPrimitive || readMethod?.returnsNullable() == false
|
||||
|
@ -28,7 +28,7 @@ enum class TransformTypes(val build: (Annotation) -> Transform) : DescribedType
|
||||
Unknown({ UnknownTransform() }) {
|
||||
override fun getDescriptor(): Any = DESCRIPTOR
|
||||
override fun getDescribed(): Any = ordinal
|
||||
override fun validate(l : List<Transform>, constants: Map<String, Int>) { }
|
||||
override fun validate(list: List<Transform>, constants: Map<String, Int>) {}
|
||||
},
|
||||
EnumDefault({ a -> EnumDefaultSchemaTransform((a as CordaSerializationTransformEnumDefault).old, a.new) }) {
|
||||
override fun getDescriptor(): Any = DESCRIPTOR
|
||||
@ -39,11 +39,11 @@ enum class TransformTypes(val build: (Annotation) -> Transform) : DescribedType
|
||||
* 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.
|
||||
*
|
||||
* @param l The list of transforms representing new constants and the mapping from that constant to an
|
||||
* @param list The list of transforms representing new constants and the mapping from that constant to an
|
||||
* existing value
|
||||
* @param constants The list of enum constants on the type the transforms are being applied to
|
||||
*/
|
||||
override fun validate(list : List<Transform>, constants: Map<String, Int>) {
|
||||
override fun validate(list: List<Transform>, constants: Map<String, Int>) {
|
||||
uncheckedCast<List<Transform>, List<EnumDefaultSchemaTransform>>(list).forEach {
|
||||
if (!constants.contains(it.new)) {
|
||||
throw NotSerializableException("Unknown enum constant ${it.new}")
|
||||
@ -62,7 +62,7 @@ enum class TransformTypes(val build: (Annotation) -> Transform) : DescribedType
|
||||
if (constants[it.old]!! >= constants[it.new]!!) {
|
||||
throw NotSerializableException(
|
||||
"Enum extensions must default to older constants. ${it.new}[${constants[it.new]}] " +
|
||||
"defaults to ${it.old}[${constants[it.old]}] which is greater")
|
||||
"defaults to ${it.old}[${constants[it.old]}] which is greater")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -76,15 +76,16 @@ enum class TransformTypes(val build: (Annotation) -> Transform) : DescribedType
|
||||
* that is a constant is renamed to something that used to exist in the enum. We do this for both
|
||||
* the same constant (i.e. C -> D -> C) and multiple constants (C->D, B->C)
|
||||
*
|
||||
* @param l The list of transforms representing the renamed constants and the mapping between their new
|
||||
* @param list The list of transforms representing the renamed constants and the mapping between their new
|
||||
* and old values
|
||||
* @param constants The list of enum constants on the type the transforms are being applied to
|
||||
*/
|
||||
override fun validate(l : List<Transform>, constants: Map<String, Int>) {
|
||||
override fun validate(list: List<Transform>, constants: Map<String, Int>) {
|
||||
object : Any() {
|
||||
val from : MutableSet<String> = mutableSetOf()
|
||||
val to : MutableSet<String> = mutableSetOf() }.apply {
|
||||
@Suppress("UNCHECKED_CAST") (l as List<RenameSchemaTransform>).forEach { rename ->
|
||||
val from: MutableSet<String> = mutableSetOf()
|
||||
val to: MutableSet<String> = mutableSetOf()
|
||||
}.apply {
|
||||
@Suppress("UNCHECKED_CAST") (list as List<RenameSchemaTransform>).forEach { rename ->
|
||||
if (rename.to in this.to || rename.from in this.from) {
|
||||
throw NotSerializableException("Cyclic renames are not allowed (${rename.to})")
|
||||
}
|
||||
@ -104,7 +105,7 @@ enum class TransformTypes(val build: (Annotation) -> Transform) : DescribedType
|
||||
//}
|
||||
;
|
||||
|
||||
abstract fun validate(l: List<Transform>, constants: Map<String, Int>)
|
||||
abstract fun validate(list: List<Transform>, constants: Map<String, Int>)
|
||||
|
||||
companion object : DescribedTypeConstructor<TransformTypes> {
|
||||
val DESCRIPTOR = AMQPDescriptorRegistry.TRANSFORM_ELEMENT_KEY.amqpDescriptor
|
||||
|
@ -92,7 +92,7 @@ class UnknownTestTransform(val a: Int, val b: Int, val c: Int) : Transform() {
|
||||
companion object : DescribedTypeConstructor<UnknownTestTransform> {
|
||||
val typeName = "UnknownTest"
|
||||
|
||||
override fun newInstance(obj: Any?) : UnknownTestTransform {
|
||||
override fun newInstance(obj: Any?): UnknownTestTransform {
|
||||
val described = obj as List<*>
|
||||
return UnknownTestTransform(described[1] as Int, described[2] as Int, described[3] as Int)
|
||||
}
|
||||
@ -201,41 +201,41 @@ data class TransformsSchema(val types: Map<String, EnumMap<TransformTypes, Mutab
|
||||
* class loader and this dictates which classes we can and cannot see
|
||||
*/
|
||||
fun get(name: String, sf: SerializerFactory) = sf.transformsCache.computeIfAbsent(name) {
|
||||
val transforms = EnumMap<TransformTypes, MutableList<Transform>>(TransformTypes::class.java)
|
||||
try {
|
||||
val clazz = sf.classloader.loadClass(name)
|
||||
val transforms = EnumMap<TransformTypes, MutableList<Transform>>(TransformTypes::class.java)
|
||||
try {
|
||||
val clazz = sf.classloader.loadClass(name)
|
||||
|
||||
supportedTransforms.forEach { transform ->
|
||||
clazz.getAnnotation(transform.type)?.let { list ->
|
||||
transform.getAnnotations(list).forEach { annotation ->
|
||||
val t = transform.enum.build(annotation)
|
||||
supportedTransforms.forEach { transform ->
|
||||
clazz.getAnnotation(transform.type)?.let { list ->
|
||||
transform.getAnnotations(list).forEach { annotation ->
|
||||
val t = transform.enum.build(annotation)
|
||||
|
||||
// we're explicitly rejecting repeated annotations, whilst it's fine and we'd just
|
||||
// ignore them it feels like a good thing to alert the user to since this is
|
||||
// more than likely a typo in their code so best make it an actual error
|
||||
if (transforms.computeIfAbsent(transform.enum) { mutableListOf() }
|
||||
.filter { t == it }
|
||||
.isNotEmpty()) {
|
||||
throw NotSerializableException(
|
||||
"Repeated unique transformation annotation of type ${t.name}")
|
||||
}
|
||||
|
||||
transforms[transform.enum]!!.add(t)
|
||||
// we're explicitly rejecting repeated annotations, whilst it's fine and we'd just
|
||||
// ignore them it feels like a good thing to alert the user to since this is
|
||||
// more than likely a typo in their code so best make it an actual error
|
||||
if (transforms.computeIfAbsent(transform.enum) { mutableListOf() }
|
||||
.filter { t == it }
|
||||
.isNotEmpty()) {
|
||||
throw NotSerializableException(
|
||||
"Repeated unique transformation annotation of type ${t.name}")
|
||||
}
|
||||
|
||||
transform.enum.validate(
|
||||
transforms[transform.enum] ?: emptyList(),
|
||||
clazz.enumConstants.mapIndexed { i, s -> Pair(s.toString(), i) }.toMap())
|
||||
transforms[transform.enum]!!.add(t)
|
||||
}
|
||||
}
|
||||
} catch (_: ClassNotFoundException) {
|
||||
// if we can't load the class we'll end up caching an empty list which is fine as that
|
||||
// list, on lookup, won't be included in the schema because it's empty
|
||||
}
|
||||
|
||||
transforms
|
||||
transform.enum.validate(
|
||||
transforms[transform.enum] ?: emptyList(),
|
||||
clazz.enumConstants.mapIndexed { i, s -> Pair(s.toString(), i) }.toMap())
|
||||
}
|
||||
}
|
||||
} catch (_: ClassNotFoundException) {
|
||||
// if we can't load the class we'll end up caching an empty list which is fine as that
|
||||
// list, on lookup, won't be included in the schema because it's empty
|
||||
}
|
||||
|
||||
transforms
|
||||
}
|
||||
|
||||
private fun getAndAdd(
|
||||
type: String,
|
||||
sf: SerializerFactory,
|
||||
|
@ -2,7 +2,7 @@ package net.corda.nodeapi.internal.serialization
|
||||
|
||||
import net.corda.core.contracts.ContractAttachment
|
||||
import net.corda.core.serialization.*
|
||||
import net.corda.testing.SerializationEnvironmentRule
|
||||
import net.corda.testing.*
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
import net.corda.testing.node.MockServices
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
@ -22,9 +22,7 @@ class ContractAttachmentSerializerTest {
|
||||
private lateinit var factory: SerializationFactory
|
||||
private lateinit var context: SerializationContext
|
||||
private lateinit var contextWithToken: SerializationContext
|
||||
|
||||
private val mockServices = MockServices()
|
||||
|
||||
private val mockServices = MockServices(rigorousMock(), MEGA_CORP.name)
|
||||
@Before
|
||||
fun setup() {
|
||||
factory = testSerialization.env.serializationFactory
|
||||
|
@ -6,7 +6,6 @@ import org.assertj.core.api.Assertions
|
||||
import org.junit.Test
|
||||
import java.io.File
|
||||
import java.io.NotSerializableException
|
||||
import java.net.URI
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
@ -47,7 +46,7 @@ class EnumEvolvabilityTests {
|
||||
|
||||
@Test
|
||||
fun noAnnotation() {
|
||||
data class C (val n: NotAnnotated)
|
||||
data class C(val n: NotAnnotated)
|
||||
|
||||
val sf = testDefaultFactory()
|
||||
val bAndS = TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(C(NotAnnotated.A))
|
||||
@ -63,7 +62,7 @@ class EnumEvolvabilityTests {
|
||||
|
||||
@Test
|
||||
fun missingDefaults() {
|
||||
data class C (val m: MissingDefaults)
|
||||
data class C(val m: MissingDefaults)
|
||||
|
||||
val sf = testDefaultFactory()
|
||||
val bAndS = TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(C(MissingDefaults.A))
|
||||
@ -74,7 +73,7 @@ class EnumEvolvabilityTests {
|
||||
|
||||
@Test
|
||||
fun missingRenames() {
|
||||
data class C (val m: MissingRenames)
|
||||
data class C(val m: MissingRenames)
|
||||
|
||||
val sf = testDefaultFactory()
|
||||
val bAndS = TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(C(MissingRenames.A))
|
||||
@ -86,7 +85,7 @@ class EnumEvolvabilityTests {
|
||||
|
||||
@Test
|
||||
fun defaultAnnotationIsAddedToEnvelope() {
|
||||
data class C (val annotatedEnum: AnnotatedEnumOnce)
|
||||
data class C(val annotatedEnum: AnnotatedEnumOnce)
|
||||
|
||||
val sf = testDefaultFactory()
|
||||
val bAndS = TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(C(AnnotatedEnumOnce.D))
|
||||
@ -94,45 +93,45 @@ class EnumEvolvabilityTests {
|
||||
// only the enum is decorated so schema sizes should be different (2 objects, only one evolved)
|
||||
assertEquals(2, bAndS.schema.types.size)
|
||||
assertEquals(1, bAndS.transformsSchema.types.size)
|
||||
assertEquals (AnnotatedEnumOnce::class.java.name, bAndS.transformsSchema.types.keys.first())
|
||||
assertEquals(AnnotatedEnumOnce::class.java.name, bAndS.transformsSchema.types.keys.first())
|
||||
|
||||
val schema = bAndS.transformsSchema.types.values.first()
|
||||
|
||||
assertEquals(1, schema.size)
|
||||
assertTrue (schema.keys.contains(TransformTypes.EnumDefault))
|
||||
assertEquals (1, schema[TransformTypes.EnumDefault]!!.size)
|
||||
assertTrue (schema[TransformTypes.EnumDefault]!![0] is EnumDefaultSchemaTransform)
|
||||
assertEquals ("D", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).new)
|
||||
assertEquals ("A", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).old)
|
||||
assertTrue(schema.keys.contains(TransformTypes.EnumDefault))
|
||||
assertEquals(1, schema[TransformTypes.EnumDefault]!!.size)
|
||||
assertTrue(schema[TransformTypes.EnumDefault]!![0] is EnumDefaultSchemaTransform)
|
||||
assertEquals("D", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).new)
|
||||
assertEquals("A", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).old)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun doubleDefaultAnnotationIsAddedToEnvelope() {
|
||||
data class C (val annotatedEnum: AnnotatedEnumTwice)
|
||||
data class C(val annotatedEnum: AnnotatedEnumTwice)
|
||||
|
||||
val sf = testDefaultFactory()
|
||||
val bAndS = TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(C(AnnotatedEnumTwice.E))
|
||||
|
||||
assertEquals(2, bAndS.schema.types.size)
|
||||
assertEquals(1, bAndS.transformsSchema.types.size)
|
||||
assertEquals (AnnotatedEnumTwice::class.java.name, bAndS.transformsSchema.types.keys.first())
|
||||
assertEquals(AnnotatedEnumTwice::class.java.name, bAndS.transformsSchema.types.keys.first())
|
||||
|
||||
val schema = bAndS.transformsSchema.types.values.first()
|
||||
|
||||
assertEquals(1, schema.size)
|
||||
assertTrue (schema.keys.contains(TransformTypes.EnumDefault))
|
||||
assertEquals (2, schema[TransformTypes.EnumDefault]!!.size)
|
||||
assertTrue (schema[TransformTypes.EnumDefault]!![0] is EnumDefaultSchemaTransform)
|
||||
assertEquals ("E", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).new)
|
||||
assertEquals ("D", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).old)
|
||||
assertTrue (schema[TransformTypes.EnumDefault]!![1] is EnumDefaultSchemaTransform)
|
||||
assertEquals ("D", (schema[TransformTypes.EnumDefault]!![1] as EnumDefaultSchemaTransform).new)
|
||||
assertEquals ("A", (schema[TransformTypes.EnumDefault]!![1] as EnumDefaultSchemaTransform).old)
|
||||
assertTrue(schema.keys.contains(TransformTypes.EnumDefault))
|
||||
assertEquals(2, schema[TransformTypes.EnumDefault]!!.size)
|
||||
assertTrue(schema[TransformTypes.EnumDefault]!![0] is EnumDefaultSchemaTransform)
|
||||
assertEquals("E", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).new)
|
||||
assertEquals("D", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).old)
|
||||
assertTrue(schema[TransformTypes.EnumDefault]!![1] is EnumDefaultSchemaTransform)
|
||||
assertEquals("D", (schema[TransformTypes.EnumDefault]!![1] as EnumDefaultSchemaTransform).new)
|
||||
assertEquals("A", (schema[TransformTypes.EnumDefault]!![1] as EnumDefaultSchemaTransform).old)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun defaultAnnotationIsAddedToEnvelopeAndDeserialised() {
|
||||
data class C (val annotatedEnum: AnnotatedEnumOnce)
|
||||
data class C(val annotatedEnum: AnnotatedEnumOnce)
|
||||
|
||||
val sf = testDefaultFactory()
|
||||
val sb = TestSerializationOutput(VERBOSE, sf).serialize(C(AnnotatedEnumOnce.D))
|
||||
@ -152,11 +151,11 @@ class EnumEvolvabilityTests {
|
||||
|
||||
val schema = transforms[eName]
|
||||
|
||||
assertTrue (schema!!.keys.contains(TransformTypes.EnumDefault))
|
||||
assertEquals (1, schema[TransformTypes.EnumDefault]!!.size)
|
||||
assertTrue (schema[TransformTypes.EnumDefault]!![0] is EnumDefaultSchemaTransform)
|
||||
assertEquals ("D", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).new)
|
||||
assertEquals ("A", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).old)
|
||||
assertTrue(schema!!.keys.contains(TransformTypes.EnumDefault))
|
||||
assertEquals(1, schema[TransformTypes.EnumDefault]!!.size)
|
||||
assertTrue(schema[TransformTypes.EnumDefault]!![0] is EnumDefaultSchemaTransform)
|
||||
assertEquals("D", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).new)
|
||||
assertEquals("A", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).old)
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -174,9 +173,9 @@ class EnumEvolvabilityTests {
|
||||
|
||||
val transforms = db.envelope.transformsSchema.types
|
||||
|
||||
assertTrue (transforms.contains(AnnotatedEnumTwice::class.java.name))
|
||||
assertTrue (transforms[AnnotatedEnumTwice::class.java.name]!!.contains(TransformTypes.EnumDefault))
|
||||
assertEquals (2, transforms[AnnotatedEnumTwice::class.java.name]!![TransformTypes.EnumDefault]!!.size)
|
||||
assertTrue(transforms.contains(AnnotatedEnumTwice::class.java.name))
|
||||
assertTrue(transforms[AnnotatedEnumTwice::class.java.name]!!.contains(TransformTypes.EnumDefault))
|
||||
assertEquals(2, transforms[AnnotatedEnumTwice::class.java.name]!![TransformTypes.EnumDefault]!!.size)
|
||||
|
||||
val enumDefaults = transforms[AnnotatedEnumTwice::class.java.name]!![TransformTypes.EnumDefault]!!
|
||||
|
||||
@ -188,7 +187,7 @@ class EnumEvolvabilityTests {
|
||||
|
||||
@Test
|
||||
fun renameAnnotationIsAdded() {
|
||||
data class C (val annotatedEnum: RenameEnumOnce)
|
||||
data class C(val annotatedEnum: RenameEnumOnce)
|
||||
|
||||
val sf = testDefaultFactory()
|
||||
|
||||
@ -197,7 +196,7 @@ class EnumEvolvabilityTests {
|
||||
|
||||
assertEquals(2, bAndS.schema.types.size)
|
||||
assertEquals(1, bAndS.transformsSchema.types.size)
|
||||
assertEquals (RenameEnumOnce::class.java.name, bAndS.transformsSchema.types.keys.first())
|
||||
assertEquals(RenameEnumOnce::class.java.name, bAndS.transformsSchema.types.keys.first())
|
||||
|
||||
val serialisedSchema = bAndS.transformsSchema.types[RenameEnumOnce::class.java.name]!!
|
||||
|
||||
@ -212,7 +211,7 @@ class EnumEvolvabilityTests {
|
||||
|
||||
assertEquals(2, cAndS.envelope.schema.types.size)
|
||||
assertEquals(1, cAndS.envelope.transformsSchema.types.size)
|
||||
assertEquals (RenameEnumOnce::class.java.name, cAndS.envelope.transformsSchema.types.keys.first())
|
||||
assertEquals(RenameEnumOnce::class.java.name, cAndS.envelope.transformsSchema.types.keys.first())
|
||||
|
||||
val deserialisedSchema = cAndS.envelope.transformsSchema.types[RenameEnumOnce::class.java.name]!!
|
||||
|
||||
@ -232,7 +231,7 @@ class EnumEvolvabilityTests {
|
||||
|
||||
@Test
|
||||
fun doubleRenameAnnotationIsAdded() {
|
||||
data class C (val annotatedEnum: RenameEnumTwice)
|
||||
data class C(val annotatedEnum: RenameEnumTwice)
|
||||
|
||||
val sf = testDefaultFactory()
|
||||
|
||||
@ -241,7 +240,7 @@ class EnumEvolvabilityTests {
|
||||
|
||||
assertEquals(2, bAndS.schema.types.size)
|
||||
assertEquals(1, bAndS.transformsSchema.types.size)
|
||||
assertEquals (RenameEnumTwice::class.java.name, bAndS.transformsSchema.types.keys.first())
|
||||
assertEquals(RenameEnumTwice::class.java.name, bAndS.transformsSchema.types.keys.first())
|
||||
|
||||
val serialisedSchema = bAndS.transformsSchema.types[RenameEnumTwice::class.java.name]!!
|
||||
|
||||
@ -258,7 +257,7 @@ class EnumEvolvabilityTests {
|
||||
|
||||
assertEquals(2, cAndS.envelope.schema.types.size)
|
||||
assertEquals(1, cAndS.envelope.transformsSchema.types.size)
|
||||
assertEquals (RenameEnumTwice::class.java.name, cAndS.envelope.transformsSchema.types.keys.first())
|
||||
assertEquals(RenameEnumTwice::class.java.name, cAndS.envelope.transformsSchema.types.keys.first())
|
||||
|
||||
val deserialisedSchema = cAndS.envelope.transformsSchema.types[RenameEnumTwice::class.java.name]!!
|
||||
|
||||
@ -271,15 +270,15 @@ class EnumEvolvabilityTests {
|
||||
assertEquals("F", (deserialisedSchema[TransformTypes.Rename]!![1] as RenameSchemaTransform).to)
|
||||
}
|
||||
|
||||
@CordaSerializationTransformRename(from="A", to="X")
|
||||
@CordaSerializationTransformEnumDefault(old = "X", new="E")
|
||||
@CordaSerializationTransformRename(from = "A", to = "X")
|
||||
@CordaSerializationTransformEnumDefault(old = "X", new = "E")
|
||||
enum class RenameAndExtendEnum {
|
||||
X, B, C, D, E
|
||||
}
|
||||
|
||||
@Test
|
||||
fun bothAnnotationTypes() {
|
||||
data class C (val annotatedEnum: RenameAndExtendEnum)
|
||||
data class C(val annotatedEnum: RenameAndExtendEnum)
|
||||
|
||||
val sf = testDefaultFactory()
|
||||
|
||||
@ -288,15 +287,15 @@ class EnumEvolvabilityTests {
|
||||
|
||||
assertEquals(2, bAndS.schema.types.size)
|
||||
assertEquals(1, bAndS.transformsSchema.types.size)
|
||||
assertEquals (RenameAndExtendEnum::class.java.name, bAndS.transformsSchema.types.keys.first())
|
||||
assertEquals(RenameAndExtendEnum::class.java.name, bAndS.transformsSchema.types.keys.first())
|
||||
|
||||
val serialisedSchema = bAndS.transformsSchema.types[RenameAndExtendEnum::class.java.name]!!
|
||||
|
||||
// This time there should be two distinct transform types (all previous tests have had only
|
||||
// a single type
|
||||
assertEquals(2, serialisedSchema.size)
|
||||
assertTrue (serialisedSchema.containsKey(TransformTypes.Rename))
|
||||
assertTrue (serialisedSchema.containsKey(TransformTypes.EnumDefault))
|
||||
assertTrue(serialisedSchema.containsKey(TransformTypes.Rename))
|
||||
assertTrue(serialisedSchema.containsKey(TransformTypes.EnumDefault))
|
||||
|
||||
assertEquals(1, serialisedSchema[TransformTypes.Rename]!!.size)
|
||||
assertEquals("A", (serialisedSchema[TransformTypes.Rename]!![0] as RenameSchemaTransform).from)
|
||||
@ -307,7 +306,7 @@ class EnumEvolvabilityTests {
|
||||
assertEquals("X", (serialisedSchema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).old)
|
||||
}
|
||||
|
||||
@CordaSerializationTransformEnumDefaults (
|
||||
@CordaSerializationTransformEnumDefaults(
|
||||
CordaSerializationTransformEnumDefault("D", "A"),
|
||||
CordaSerializationTransformEnumDefault("D", "A"))
|
||||
enum class RepeatedAnnotation {
|
||||
@ -316,7 +315,7 @@ class EnumEvolvabilityTests {
|
||||
|
||||
@Test
|
||||
fun repeatedAnnotation() {
|
||||
data class C (val a: RepeatedAnnotation)
|
||||
data class C(val a: RepeatedAnnotation)
|
||||
|
||||
val sf = testDefaultFactory()
|
||||
|
||||
@ -330,40 +329,40 @@ class EnumEvolvabilityTests {
|
||||
A, B, C, D
|
||||
}
|
||||
|
||||
@CordaSerializationTransformEnumDefaults (
|
||||
@CordaSerializationTransformEnumDefaults(
|
||||
CordaSerializationTransformEnumDefault("D", "A"),
|
||||
CordaSerializationTransformEnumDefault("E", "A"))
|
||||
enum class E2 {
|
||||
A, B, C, D, E
|
||||
}
|
||||
|
||||
@CordaSerializationTransformEnumDefaults (CordaSerializationTransformEnumDefault("D", "A"))
|
||||
@CordaSerializationTransformEnumDefaults(CordaSerializationTransformEnumDefault("D", "A"))
|
||||
enum class E3 {
|
||||
A, B, C, D
|
||||
}
|
||||
|
||||
@Test
|
||||
fun multiEnums() {
|
||||
data class A (val a: E1, val b: E2)
|
||||
data class B (val a: E3, val b: A, val c: E1)
|
||||
data class C (val a: B, val b: E2, val c: E3)
|
||||
data class A(val a: E1, val b: E2)
|
||||
data class B(val a: E3, val b: A, val c: E1)
|
||||
data class C(val a: B, val b: E2, val c: E3)
|
||||
|
||||
val c = C(B(E3.A,A(E1.A,E2.B),E1.C),E2.B,E3.A)
|
||||
val c = C(B(E3.A, A(E1.A, E2.B), E1.C), E2.B, E3.A)
|
||||
|
||||
val sf = testDefaultFactory()
|
||||
|
||||
// Serialise the object
|
||||
val bAndS = TestSerializationOutput(VERBOSE, sf).serializeAndReturnSchema(c)
|
||||
|
||||
println (bAndS.transformsSchema)
|
||||
println(bAndS.transformsSchema)
|
||||
|
||||
// we have six types and three of those, the enums, should have transforms
|
||||
assertEquals(6, bAndS.schema.types.size)
|
||||
assertEquals(3, bAndS.transformsSchema.types.size)
|
||||
|
||||
assertTrue (E1::class.java.name in bAndS.transformsSchema.types)
|
||||
assertTrue (E2::class.java.name in bAndS.transformsSchema.types)
|
||||
assertTrue (E3::class.java.name in bAndS.transformsSchema.types)
|
||||
assertTrue(E1::class.java.name in bAndS.transformsSchema.types)
|
||||
assertTrue(E2::class.java.name in bAndS.transformsSchema.types)
|
||||
assertTrue(E3::class.java.name in bAndS.transformsSchema.types)
|
||||
|
||||
val e1S = bAndS.transformsSchema.types[E1::class.java.name]!!
|
||||
val e2S = bAndS.transformsSchema.types[E2::class.java.name]!!
|
||||
@ -404,7 +403,7 @@ class EnumEvolvabilityTests {
|
||||
assertTrue(sf.transformsCache.containsKey(C2::class.java.name))
|
||||
assertTrue(sf.transformsCache.containsKey(AnnotatedEnumOnce::class.java.name))
|
||||
|
||||
assertEquals (sb1.transformsSchema.types[AnnotatedEnumOnce::class.java.name],
|
||||
assertEquals(sb1.transformsSchema.types[AnnotatedEnumOnce::class.java.name],
|
||||
sb2.transformsSchema.types[AnnotatedEnumOnce::class.java.name])
|
||||
}
|
||||
|
||||
@ -447,7 +446,7 @@ class EnumEvolvabilityTests {
|
||||
//
|
||||
// And we're not at 3. However, we ban this rename
|
||||
//
|
||||
@CordaSerializationTransformRenames (
|
||||
@CordaSerializationTransformRenames(
|
||||
CordaSerializationTransformRename("D", "C"),
|
||||
CordaSerializationTransformRename("C", "D")
|
||||
)
|
||||
@ -455,7 +454,7 @@ class EnumEvolvabilityTests {
|
||||
|
||||
@Test
|
||||
fun rejectCyclicRename() {
|
||||
data class C (val e: RejectCyclicRename)
|
||||
data class C(val e: RejectCyclicRename)
|
||||
|
||||
val sf = testDefaultFactory()
|
||||
Assertions.assertThatThrownBy {
|
||||
@ -468,7 +467,7 @@ class EnumEvolvabilityTests {
|
||||
// unserailzble. However, in this case, it isn't a struct cycle, rather one element
|
||||
// is renamed to match what a different element used to be called
|
||||
//
|
||||
@CordaSerializationTransformRenames (
|
||||
@CordaSerializationTransformRenames(
|
||||
CordaSerializationTransformRename(from = "B", to = "C"),
|
||||
CordaSerializationTransformRename(from = "C", to = "D")
|
||||
)
|
||||
@ -476,7 +475,7 @@ class EnumEvolvabilityTests {
|
||||
|
||||
@Test
|
||||
fun rejectCyclicRenameAlt() {
|
||||
data class C (val e: RejectCyclicRenameAlt)
|
||||
data class C(val e: RejectCyclicRenameAlt)
|
||||
|
||||
val sf = testDefaultFactory()
|
||||
Assertions.assertThatThrownBy {
|
||||
@ -484,7 +483,7 @@ class EnumEvolvabilityTests {
|
||||
}.isInstanceOf(NotSerializableException::class.java)
|
||||
}
|
||||
|
||||
@CordaSerializationTransformRenames (
|
||||
@CordaSerializationTransformRenames(
|
||||
CordaSerializationTransformRename("G", "C"),
|
||||
CordaSerializationTransformRename("F", "G"),
|
||||
CordaSerializationTransformRename("E", "F"),
|
||||
@ -495,7 +494,7 @@ class EnumEvolvabilityTests {
|
||||
|
||||
@Test
|
||||
fun rejectCyclicRenameRedux() {
|
||||
data class C (val e: RejectCyclicRenameRedux)
|
||||
data class C(val e: RejectCyclicRenameRedux)
|
||||
|
||||
val sf = testDefaultFactory()
|
||||
Assertions.assertThatThrownBy {
|
||||
@ -503,12 +502,12 @@ class EnumEvolvabilityTests {
|
||||
}.isInstanceOf(NotSerializableException::class.java)
|
||||
}
|
||||
|
||||
@CordaSerializationTransformEnumDefault (new = "D", old = "X")
|
||||
@CordaSerializationTransformEnumDefault(new = "D", old = "X")
|
||||
enum class RejectBadDefault { A, B, C, D }
|
||||
|
||||
@Test
|
||||
fun rejectBadDefault() {
|
||||
data class C (val e: RejectBadDefault)
|
||||
data class C(val e: RejectBadDefault)
|
||||
|
||||
val sf = testDefaultFactory()
|
||||
Assertions.assertThatThrownBy {
|
||||
@ -516,12 +515,12 @@ class EnumEvolvabilityTests {
|
||||
}.isInstanceOf(NotSerializableException::class.java)
|
||||
}
|
||||
|
||||
@CordaSerializationTransformEnumDefault (new = "D", old = "D")
|
||||
@CordaSerializationTransformEnumDefault(new = "D", old = "D")
|
||||
enum class RejectBadDefaultToSelf { A, B, C, D }
|
||||
|
||||
@Test
|
||||
fun rejectBadDefaultToSelf() {
|
||||
data class C (val e: RejectBadDefaultToSelf)
|
||||
data class C(val e: RejectBadDefaultToSelf)
|
||||
|
||||
val sf = testDefaultFactory()
|
||||
Assertions.assertThatThrownBy {
|
||||
|
@ -1,12 +1,13 @@
|
||||
package net.corda.nodeapi.internal.serialization.amqp
|
||||
|
||||
import net.corda.core.serialization.*
|
||||
import net.corda.core.serialization.CordaSerializationTransformEnumDefault
|
||||
import net.corda.core.serialization.CordaSerializationTransformEnumDefaults
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.testing.common.internal.ProjectStructure.projectRootDir
|
||||
import org.assertj.core.api.Assertions
|
||||
import org.junit.Test
|
||||
import java.io.File
|
||||
import java.io.NotSerializableException
|
||||
import java.net.URI
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
// NOTE: To recreate the test files used by these tests uncomment the original test classes and comment
|
||||
@ -30,7 +31,7 @@ class EnumEvolveTests {
|
||||
val resource = "${javaClass.simpleName}.${testName()}"
|
||||
val sf = testDefaultFactory()
|
||||
|
||||
data class C (val e : DeserializeNewerSetToUnknown)
|
||||
data class C(val e: DeserializeNewerSetToUnknown)
|
||||
|
||||
// Uncomment to re-generate test files
|
||||
// File(URI("$localPath/$resource")).writeBytes(
|
||||
@ -40,7 +41,7 @@ class EnumEvolveTests {
|
||||
|
||||
val obj = DeserializationInput(sf).deserialize(SerializedBytes<C>(File(path.toURI()).readBytes()))
|
||||
|
||||
assertEquals (DeserializeNewerSetToUnknown.C, obj.e)
|
||||
assertEquals(DeserializeNewerSetToUnknown.C, obj.e)
|
||||
}
|
||||
|
||||
// Version of the class as it was serialised
|
||||
@ -78,9 +79,9 @@ class EnumEvolveTests {
|
||||
// of the evolution code
|
||||
val obj3 = DeserializationInput(sf).deserialize(SerializedBytes<C>(File(path3.toURI()).readBytes()))
|
||||
|
||||
assertEquals (DeserializeNewerSetToUnknown2.C, obj1.e)
|
||||
assertEquals (DeserializeNewerSetToUnknown2.C, obj2.e)
|
||||
assertEquals (DeserializeNewerSetToUnknown2.C, obj3.e)
|
||||
assertEquals(DeserializeNewerSetToUnknown2.C, obj1.e)
|
||||
assertEquals(DeserializeNewerSetToUnknown2.C, obj2.e)
|
||||
assertEquals(DeserializeNewerSetToUnknown2.C, obj3.e)
|
||||
}
|
||||
|
||||
|
||||
@ -149,7 +150,7 @@ class EnumEvolveTests {
|
||||
data class C(val e: DeserializeWithRename)
|
||||
|
||||
// Uncomment to re-generate test files, needs to be done in three stages
|
||||
val so = SerializationOutput(sf)
|
||||
// val so = SerializationOutput(sf)
|
||||
// First change
|
||||
// File(URI("$localPath/$resource.1.AA")).writeBytes(so.serialize(C(DeserializeWithRename.AA)).bytes)
|
||||
// File(URI("$localPath/$resource.1.B")).writeBytes(so.serialize(C(DeserializeWithRename.B)).bytes)
|
||||
@ -271,7 +272,7 @@ class EnumEvolveTests {
|
||||
data class C(val e: MultiOperations)
|
||||
|
||||
// Uncomment to re-generate test files, needs to be done in three stages
|
||||
val so = SerializationOutput(sf)
|
||||
// val so = SerializationOutput(sf)
|
||||
// First change
|
||||
// File(URI("$localPath/$resource.1.A")).writeBytes(so.serialize(C(MultiOperations.A)).bytes)
|
||||
// File(URI("$localPath/$resource.1.B")).writeBytes(so.serialize(C(MultiOperations.B)).bytes)
|
||||
@ -345,15 +346,15 @@ class EnumEvolveTests {
|
||||
Pair("$resource.5.G", MultiOperations.C))
|
||||
|
||||
fun load(l: List<Pair<String, MultiOperations>>) = l.map {
|
||||
Pair (DeserializationInput(sf).deserialize(SerializedBytes<C>(
|
||||
File(EvolvabilityTests::class.java.getResource(it.first).toURI()).readBytes())), it.second)
|
||||
Pair(DeserializationInput(sf).deserialize(SerializedBytes<C>(
|
||||
File(EvolvabilityTests::class.java.getResource(it.first).toURI()).readBytes())), it.second)
|
||||
}
|
||||
|
||||
load (stage1Resources).forEach { assertEquals(it.second, it.first.e) }
|
||||
load (stage2Resources).forEach { assertEquals(it.second, it.first.e) }
|
||||
load (stage3Resources).forEach { assertEquals(it.second, it.first.e) }
|
||||
load (stage4Resources).forEach { assertEquals(it.second, it.first.e) }
|
||||
load (stage5Resources).forEach { assertEquals(it.second, it.first.e) }
|
||||
load(stage1Resources).forEach { assertEquals(it.second, it.first.e) }
|
||||
load(stage2Resources).forEach { assertEquals(it.second, it.first.e) }
|
||||
load(stage3Resources).forEach { assertEquals(it.second, it.first.e) }
|
||||
load(stage4Resources).forEach { assertEquals(it.second, it.first.e) }
|
||||
load(stage5Resources).forEach { assertEquals(it.second, it.first.e) }
|
||||
}
|
||||
|
||||
@CordaSerializationTransformEnumDefault(old = "A", new = "F")
|
||||
@ -363,7 +364,7 @@ class EnumEvolveTests {
|
||||
fun badNewValue() {
|
||||
val sf = testDefaultFactory()
|
||||
|
||||
data class C (val e : BadNewValue)
|
||||
data class C(val e: BadNewValue)
|
||||
|
||||
Assertions.assertThatThrownBy {
|
||||
SerializationOutput(sf).serialize(C(BadNewValue.A))
|
||||
@ -374,13 +375,13 @@ class EnumEvolveTests {
|
||||
CordaSerializationTransformEnumDefault(new = "D", old = "E"),
|
||||
CordaSerializationTransformEnumDefault(new = "E", old = "A")
|
||||
)
|
||||
enum class OutOfOrder { A, B, C, D, E}
|
||||
enum class OutOfOrder { A, B, C, D, E }
|
||||
|
||||
@Test
|
||||
fun outOfOrder() {
|
||||
val sf = testDefaultFactory()
|
||||
|
||||
data class C (val e : OutOfOrder)
|
||||
data class C(val e: OutOfOrder)
|
||||
|
||||
Assertions.assertThatThrownBy {
|
||||
SerializationOutput(sf).serialize(C(OutOfOrder.A))
|
||||
|
@ -2,17 +2,14 @@ package net.corda.nodeapi.internal.serialization.amqp
|
||||
|
||||
import net.corda.core.serialization.ClassWhitelist
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import org.junit.Test
|
||||
import java.time.DayOfWeek
|
||||
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
|
||||
import java.io.File
|
||||
import java.io.NotSerializableException
|
||||
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import org.assertj.core.api.Assertions
|
||||
import org.junit.Test
|
||||
import java.io.File
|
||||
import java.io.NotSerializableException
|
||||
import java.time.DayOfWeek
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
|
||||
class EnumTests {
|
||||
enum class Bras {
|
||||
@ -42,8 +39,8 @@ class EnumTests {
|
||||
//}
|
||||
|
||||
// the new state, note in the test we serialised with value UNDERWIRE so the spacer
|
||||
// occuring after this won't have changed the ordinality of our serialised value
|
||||
// and thus should still be deserialisable
|
||||
// occurring after this won't have changed the ordinality of our serialised value
|
||||
// and thus should still be deserializable
|
||||
enum class OldBras2 {
|
||||
TSHIRT, UNDERWIRE, PUSHUP, SPACER, BRALETTE, SPACER2
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ import net.corda.testing.common.internal.ProjectStructure.projectRootDir
|
||||
import org.junit.Test
|
||||
import java.io.File
|
||||
import java.io.NotSerializableException
|
||||
import java.net.URI
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
// To regenerate any of the binary test files do the following
|
||||
@ -19,13 +18,14 @@ import kotlin.test.assertEquals
|
||||
// 5. Comment back out the generation code and uncomment the actual test
|
||||
class EvolvabilityTests {
|
||||
// When regenerating the test files this needs to be set to the file system location of the resource files
|
||||
@Suppress("UNUSED")
|
||||
var localPath = projectRootDir.toUri().resolve(
|
||||
"node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp")
|
||||
|
||||
@Test
|
||||
fun simpleOrderSwapSameType() {
|
||||
val sf = testDefaultFactory()
|
||||
val resource= "EvolvabilityTests.simpleOrderSwapSameType"
|
||||
val resource = "EvolvabilityTests.simpleOrderSwapSameType"
|
||||
|
||||
val A = 1
|
||||
val B = 2
|
||||
@ -91,7 +91,7 @@ class EvolvabilityTests {
|
||||
|
||||
assertEquals(A, deserializedC.a)
|
||||
assertEquals(null, deserializedC.b)
|
||||
}
|
||||
}
|
||||
|
||||
@Test(expected = NotSerializableException::class)
|
||||
fun addAdditionalParam() {
|
||||
@ -370,6 +370,7 @@ class EvolvabilityTests {
|
||||
|
||||
// Add a parameter to inner but keep outer unchanged
|
||||
data class Inner(val a: Int, val b: String?)
|
||||
|
||||
data class Outer(val a: Int, val b: Inner)
|
||||
|
||||
val path = EvolvabilityTests::class.java.getResource(resource)
|
||||
|
@ -44,6 +44,11 @@ sourceSets {
|
||||
// This prevents problems in IntelliJ with regard to duplicate source roots.
|
||||
processResources {
|
||||
from file("$rootDir/config/dev/log4j2.xml")
|
||||
from file("$rootDir/config/dev/jolokia-access.xml")
|
||||
}
|
||||
|
||||
processTestResources {
|
||||
from file("$rootDir/config/test/jolokia-access.xml")
|
||||
}
|
||||
|
||||
// To find potential version conflicts, run "gradle htmlDependencyReport" and then look in
|
||||
@ -163,6 +168,9 @@ dependencies {
|
||||
// FastClasspathScanner: classpath scanning
|
||||
compile 'io.github.lukehutch:fast-classpath-scanner:2.0.21'
|
||||
|
||||
// Apache Shiro: authentication, authorization and session management.
|
||||
compile "org.apache.shiro:shiro-core:${shiro_version}"
|
||||
|
||||
// Jsh: A SSH implementation for tunneling inbound traffic via a relay
|
||||
compile group: 'com.jcraft', name: 'jsch', version: '0.1.54'
|
||||
|
||||
@ -183,6 +191,9 @@ dependencies {
|
||||
testCompile "org.glassfish.jersey.core:jersey-server:${jersey_version}"
|
||||
testCompile "org.glassfish.jersey.containers:jersey-container-servlet-core:${jersey_version}"
|
||||
testCompile "org.glassfish.jersey.containers:jersey-container-jetty-http:${jersey_version}"
|
||||
|
||||
// Jolokia JVM monitoring agent
|
||||
runtime "org.jolokia:jolokia-jvm:${jolokia_version}:agent"
|
||||
}
|
||||
|
||||
task integrationTest(type: Test) {
|
||||
|
@ -42,7 +42,7 @@ task buildCordaJAR(type: FatCapsule, dependsOn: project(':node').compileJava) {
|
||||
|
||||
capsuleManifest {
|
||||
applicationVersion = corda_release_version
|
||||
appClassPath = ["jolokia-agent-war-${project.rootProject.ext.jolokia_version}.war"]
|
||||
appClassPath = ["jolokia-war-${project.rootProject.ext.jolokia_version}.war"]
|
||||
// See experimental/quasar-hook/README.md for how to generate.
|
||||
def quasarExcludeExpression = "x(antlr**;bftsmart**;ch**;co.paralleluniverse**;com.codahale**;com.esotericsoftware**;com.fasterxml**;com.google**;com.ibm**;com.intellij**;com.jcabi**;com.nhaarman**;com.opengamma**;com.typesafe**;com.zaxxer**;de.javakaffee**;groovy**;groovyjarjarantlr**;groovyjarjarasm**;io.atomix**;io.github**;io.netty**;jdk**;joptsimple**;junit**;kotlin**;net.bytebuddy**;net.i2p**;org.apache**;org.assertj**;org.bouncycastle**;org.codehaus**;org.crsh**;org.dom4j**;org.fusesource**;org.h2**;org.hamcrest**;org.hibernate**;org.jboss**;org.jcp**;org.joda**;org.junit**;org.mockito**;org.objectweb**;org.objenesis**;org.slf4j**;org.w3c**;org.xml**;org.yaml**;reflectasm**;rx**)"
|
||||
javaAgents = ["quasar-core-${quasar_version}-jdk8.jar=${quasarExcludeExpression}"]
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -19,6 +19,7 @@ import net.corda.testing.*
|
||||
import net.corda.testing.driver.NodeHandle
|
||||
import net.corda.testing.driver.PortAllocation
|
||||
import net.corda.testing.driver.driver
|
||||
import net.corda.testing.internal.InternalDriverDSL
|
||||
import net.corda.testing.internal.performance.div
|
||||
import net.corda.testing.internal.performance.startPublishingFixedRateInjector
|
||||
import net.corda.testing.internal.performance.startReporter
|
||||
@ -100,7 +101,7 @@ class NodePerformanceTests : IntegrationTest() {
|
||||
driver(startNodesInProcess = true) {
|
||||
val a = startNode(rpcUsers = listOf(User("A", "A", setOf(startFlow<EmptyFlow>())))).get()
|
||||
a as NodeHandle.InProcess
|
||||
val metricRegistry = startReporter(shutdownManager, a.node.services.monitoringService.metrics)
|
||||
val metricRegistry = startReporter((this as InternalDriverDSL).shutdownManager, a.node.services.monitoringService.metrics)
|
||||
a.rpcClientToNode().use("A", "A") { connection ->
|
||||
startPublishingFixedRateInjector(metricRegistry, 1, 5.minutes, 2000L / TimeUnit.SECONDS) {
|
||||
connection.proxy.startFlow(::EmptyFlow).returnValue.get()
|
||||
@ -133,7 +134,7 @@ class NodePerformanceTests : IntegrationTest() {
|
||||
portAllocation = PortAllocation.Incremental(20000)
|
||||
) {
|
||||
val notary = defaultNotaryNode.getOrThrow() as NodeHandle.InProcess
|
||||
val metricRegistry = startReporter(shutdownManager, notary.node.services.monitoringService.metrics)
|
||||
val metricRegistry = startReporter((this as InternalDriverDSL).shutdownManager, notary.node.services.monitoringService.metrics)
|
||||
notary.rpcClientToNode().use("A", "A") { connection ->
|
||||
println("ISSUING")
|
||||
val doneFutures = (1..100).toList().map {
|
||||
|
@ -23,6 +23,7 @@ import kotlin.test.fail
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.ClassRule
|
||||
import java.util.regex.Pattern
|
||||
import kotlin.reflect.jvm.jvmName
|
||||
|
||||
class SSHServerTest : IntegrationTest() {
|
||||
companion object {
|
||||
@ -120,7 +121,7 @@ class SSHServerTest : IntegrationTest() {
|
||||
channel.disconnect()
|
||||
session.disconnect()
|
||||
|
||||
assertThat(response).matches("(?s)User not permissioned with any of \\[[^]]*${flowNameEscaped}.*")
|
||||
assertThat(response).matches("(?s)User not authorized to perform RPC call .*")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -20,14 +20,15 @@ import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.node.internal.cordapp.CordappLoader
|
||||
import net.corda.node.internal.cordapp.CordappProviderImpl
|
||||
import net.corda.testing.*
|
||||
import net.corda.testing.DUMMY_BANK_A
|
||||
import net.corda.testing.DUMMY_NOTARY
|
||||
import net.corda.testing.IntegrationTest
|
||||
import net.corda.testing.driver.DriverDSLExposedInterface
|
||||
import net.corda.testing.driver.DriverDSL
|
||||
import net.corda.testing.driver.NodeHandle
|
||||
import net.corda.testing.driver.driver
|
||||
import net.corda.testing.node.MockAttachmentStorage
|
||||
import net.corda.testing.rigorousMock
|
||||
import net.corda.testing.withTestSerialization
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.ClassRule
|
||||
import org.junit.Test
|
||||
@ -57,16 +58,16 @@ class AttachmentLoadingTests : IntegrationTest() {
|
||||
Class.forName("net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Initiator", true, URLClassLoader(arrayOf(isolatedJAR)))
|
||||
.asSubclass(FlowLogic::class.java)
|
||||
|
||||
private fun DriverDSLExposedInterface.createTwoNodes(): List<NodeHandle> {
|
||||
private fun DriverDSL.createTwoNodes(): List<NodeHandle> {
|
||||
return listOf(
|
||||
startNode(providedName = bankAName),
|
||||
startNode(providedName = bankBName)
|
||||
).transpose().getOrThrow()
|
||||
}
|
||||
|
||||
private fun DriverDSLExposedInterface.installIsolatedCordappTo(nodeName: CordaX500Name) {
|
||||
private fun DriverDSL.installIsolatedCordappTo(nodeName: CordaX500Name) {
|
||||
// Copy the app jar to the first node. The second won't have it.
|
||||
val path = (baseDirectory(nodeName.toString()) / "cordapps").createDirectories() / "isolated.jar"
|
||||
val path = (baseDirectory(nodeName) / "cordapps").createDirectories() / "isolated.jar"
|
||||
logger.info("Installing isolated jar to $path")
|
||||
isolatedJAR.openStream().buffered().use { input ->
|
||||
Files.newOutputStream(path).buffered().use { output ->
|
||||
|
@ -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
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -19,7 +19,7 @@ import net.corda.node.services.messaging.ReceivedMessage
|
||||
import net.corda.node.services.messaging.send
|
||||
import net.corda.node.services.transactions.RaftValidatingNotaryService
|
||||
import net.corda.testing.*
|
||||
import net.corda.testing.driver.DriverDSLExposedInterface
|
||||
import net.corda.testing.driver.DriverDSL
|
||||
import net.corda.testing.driver.NodeHandle
|
||||
import net.corda.testing.driver.driver
|
||||
import net.corda.testing.node.ClusterSpec
|
||||
@ -113,13 +113,13 @@ class P2PMessagingTest : IntegrationTest() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun startDriverWithDistributedService(dsl: DriverDSLExposedInterface.(List<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)))) {
|
||||
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))
|
||||
.map { (it as NodeHandle.InProcess).node }
|
||||
.getOrThrow()
|
||||
|
@ -39,7 +39,7 @@ import net.corda.node.internal.cordapp.CordappProviderInternal
|
||||
import net.corda.node.services.ContractUpgradeHandler
|
||||
import net.corda.node.services.FinalityHandler
|
||||
import net.corda.node.services.NotaryChangeHandler
|
||||
import net.corda.node.services.RPCUserService
|
||||
import net.corda.node.internal.security.RPCSecurityManager
|
||||
import net.corda.node.services.api.*
|
||||
import net.corda.node.services.config.BFTSMaRtConfiguration
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
@ -47,6 +47,7 @@ import net.corda.node.services.config.NotaryConfig
|
||||
import net.corda.node.services.config.configureWithDevSSLCertificate
|
||||
import net.corda.node.services.events.NodeSchedulerService
|
||||
import net.corda.node.services.events.ScheduledActivityObserver
|
||||
import net.corda.node.services.api.IdentityServiceInternal
|
||||
import net.corda.node.services.identity.PersistentIdentityService
|
||||
import net.corda.node.services.keys.PersistentKeyManagementService
|
||||
import net.corda.node.services.messaging.MessagingService
|
||||
@ -138,7 +139,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
|
||||
protected val _nodeReadyFuture = openFuture<Unit>()
|
||||
protected val networkMapClient: NetworkMapClient? by lazy { configuration.compatibilityZoneURL?.let(::NetworkMapClient) }
|
||||
|
||||
lateinit var userService: RPCUserService get
|
||||
lateinit var securityManager: RPCSecurityManager get
|
||||
|
||||
/** Completes once the node has successfully registered with the network map service
|
||||
* or has loaded network map data from local database */
|
||||
@ -175,26 +176,33 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
|
||||
check(started == null) { "Node has already been started" }
|
||||
log.info("Generating nodeInfo ...")
|
||||
initCertificate()
|
||||
val (keyPairs, info) = initNodeInfo()
|
||||
val identityKeypair = keyPairs.first { it.public == info.legalIdentities.first().owningKey }
|
||||
val serialisedNodeInfo = info.serialize()
|
||||
val signature = identityKeypair.sign(serialisedNodeInfo)
|
||||
// TODO: Signed data might not be sufficient for multiple identities, as it only contains one signature.
|
||||
NodeInfoWatcher.saveToFile(configuration.baseDirectory, SignedData(serialisedNodeInfo, signature))
|
||||
val schemaService = NodeSchemaService(cordappLoader.cordappSchemas)
|
||||
val (identity, identityKeyPair) = obtainIdentity(notaryConfig = null)
|
||||
initialiseDatabasePersistence(schemaService, makeIdentityService(identity.certificate)) { database ->
|
||||
val persistentNetworkMapCache = PersistentNetworkMapCache(database)
|
||||
val (keyPairs, info) = initNodeInfo(persistentNetworkMapCache, identity, identityKeyPair)
|
||||
val identityKeypair = keyPairs.first { it.public == info.legalIdentities.first().owningKey }
|
||||
val serialisedNodeInfo = info.serialize()
|
||||
val signature = identityKeypair.sign(serialisedNodeInfo)
|
||||
// TODO: Signed data might not be sufficient for multiple identities, as it only contains one signature.
|
||||
NodeInfoWatcher.saveToFile(configuration.baseDirectory, SignedData(serialisedNodeInfo, signature))
|
||||
}
|
||||
}
|
||||
|
||||
open fun start(): StartedNode<AbstractNode> {
|
||||
check(started == null) { "Node has already been started" }
|
||||
log.info("Node starting up ...")
|
||||
initCertificate()
|
||||
val (keyPairs, info) = initNodeInfo()
|
||||
val schemaService = NodeSchemaService(cordappLoader.cordappSchemas)
|
||||
val identityService = makeIdentityService(info)
|
||||
val (identity, identityKeyPair) = obtainIdentity(notaryConfig = null)
|
||||
val identityService = makeIdentityService(identity.certificate)
|
||||
// Do all of this in a database transaction so anything that might need a connection has one.
|
||||
val (startedImpl, schedulerService) = initialiseDatabasePersistence(schemaService, identityService) { database ->
|
||||
val networkMapCache = NetworkMapCacheImpl(PersistentNetworkMapCache(database), identityService)
|
||||
val (keyPairs, info) = initNodeInfo(networkMapCache, identity, identityKeyPair)
|
||||
identityService.loadIdentities(info.legalIdentitiesAndCerts)
|
||||
val transactionStorage = makeTransactionStorage(database)
|
||||
val nodeServices = makeServices(keyPairs, schemaService, transactionStorage, database, info, identityService)
|
||||
val nodeServices = makeServices(keyPairs, schemaService, transactionStorage, database, info, identityService, networkMapCache)
|
||||
val mutualExclusionConfiguration = configuration.enterpriseConfiguration.mutualExclusionConfiguration
|
||||
if (mutualExclusionConfiguration.on) {
|
||||
RunOnceService(database, mutualExclusionConfiguration.machineName,
|
||||
@ -245,7 +253,6 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
|
||||
networkMapUpdater.subscribeToNetworkMap()
|
||||
|
||||
// If we successfully loaded network data from database, we set this future to Unit.
|
||||
services.networkMapCache.addNode(info)
|
||||
_nodeReadyFuture.captureLater(services.networkMapCache.nodeReady.map { Unit })
|
||||
|
||||
return startedImpl.apply {
|
||||
@ -266,11 +273,12 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
|
||||
protected abstract fun getRxIoScheduler(): Scheduler
|
||||
|
||||
open fun startShell(rpcOps: CordaRPCOps) {
|
||||
InteractiveShell.startShell(configuration, rpcOps, userService, _services.identityService, _services.database)
|
||||
InteractiveShell.startShell(configuration, rpcOps, securityManager, _services.identityService, _services.database)
|
||||
}
|
||||
|
||||
private fun initNodeInfo(): Pair<Set<KeyPair>, NodeInfo> {
|
||||
val (identity, identityKeyPair) = obtainIdentity(notaryConfig = null)
|
||||
private fun initNodeInfo(networkMapCache: NetworkMapCacheBaseInternal,
|
||||
identity: PartyAndCertificate,
|
||||
identityKeyPair: KeyPair): Pair<Set<KeyPair>, NodeInfo> {
|
||||
val keyPairs = mutableSetOf(identityKeyPair)
|
||||
|
||||
myNotaryIdentity = configuration.notary?.let {
|
||||
@ -278,12 +286,20 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
|
||||
keyPairs += notaryIdentityKeyPair
|
||||
notaryIdentity
|
||||
}
|
||||
val info = NodeInfo(
|
||||
|
||||
var info = NodeInfo(
|
||||
myAddresses(),
|
||||
listOf(identity, myNotaryIdentity).filterNotNull(),
|
||||
versionInfo.platformVersion,
|
||||
platformClock.instant().toEpochMilli()
|
||||
)
|
||||
// Check if we have already stored a version of 'our own' NodeInfo, this is to avoid regenerating it with
|
||||
// a different timestamp.
|
||||
networkMapCache.getNodesByLegalName(myLegalName).firstOrNull()?.let {
|
||||
if (info.copy(serial = it.serial) == it) {
|
||||
info = it
|
||||
}
|
||||
}
|
||||
return Pair(keyPairs, info)
|
||||
}
|
||||
|
||||
@ -506,7 +522,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
|
||||
* Builds node internal, advertised, and plugin services.
|
||||
* Returns a list of tokenizable services to be added to the serialisation context.
|
||||
*/
|
||||
private fun makeServices(keyPairs: Set<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()
|
||||
val metrics = MetricRegistry()
|
||||
attachments = NodeAttachmentService(metrics)
|
||||
@ -520,7 +536,8 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
|
||||
MonitoringService(metrics),
|
||||
cordappProvider,
|
||||
database,
|
||||
info)
|
||||
info,
|
||||
networkMapCache)
|
||||
network = makeMessagingService(database, info)
|
||||
val tokenizableServices = mutableListOf(attachments, network, services.vaultService,
|
||||
services.keyManagementService, services.identityService, platformClock,
|
||||
@ -559,6 +576,17 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
|
||||
"or if you don't have one yet, fill out the config file and run corda.jar --initial-registration. " +
|
||||
"Read more at: https://docs.corda.net/permissioning.html"
|
||||
}
|
||||
|
||||
// Check all cert path chain to the trusted root
|
||||
val sslKeystore = loadKeyStore(configuration.sslKeystore, configuration.keyStorePassword)
|
||||
val identitiesKeystore = loadKeyStore(configuration.nodeKeystore, configuration.keyStorePassword)
|
||||
val trustStore = loadKeyStore(configuration.trustStoreFile, configuration.trustStorePassword)
|
||||
val sslRoot = sslKeystore.getCertificateChain(X509Utilities.CORDA_CLIENT_TLS).last()
|
||||
val clientCARoot = identitiesKeystore.getCertificateChain(X509Utilities.CORDA_CLIENT_CA).last()
|
||||
val trustRoot = trustStore.getCertificate(X509Utilities.CORDA_ROOT_CA)
|
||||
|
||||
require(sslRoot == trustRoot) { "TLS certificate must chain to the trusted root." }
|
||||
require(clientCARoot == trustRoot) { "Client CA certificate must chain to the trusted root." }
|
||||
}
|
||||
|
||||
// Specific class so that MockNode can catch it.
|
||||
@ -600,7 +628,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun makeKeyManagementService(identityService: IdentityService, keyPairs: Set<KeyPair>): KeyManagementService {
|
||||
protected open fun makeKeyManagementService(identityService: IdentityServiceInternal, keyPairs: Set<KeyPair>): KeyManagementService {
|
||||
return PersistentKeyManagementService(identityService, keyPairs)
|
||||
}
|
||||
|
||||
@ -627,12 +655,12 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
|
||||
}
|
||||
}
|
||||
|
||||
private fun makeIdentityService(info: NodeInfo): PersistentIdentityService {
|
||||
private fun makeIdentityService(identityCert: X509Certificate): PersistentIdentityService {
|
||||
val trustStore = KeyStoreWrapper(configuration.trustStoreFile, configuration.trustStorePassword)
|
||||
val caKeyStore = KeyStoreWrapper(configuration.nodeKeystore, configuration.keyStorePassword)
|
||||
val trustRoot = trustStore.getX509Certificate(X509Utilities.CORDA_ROOT_CA)
|
||||
val clientCa = caKeyStore.certificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA)
|
||||
val caCertificates = arrayOf(info.legalIdentitiesAndCerts[0].certificate, clientCa.certificate.cert)
|
||||
val caCertificates = arrayOf(identityCert, clientCa.certificate.cert)
|
||||
return PersistentIdentityService(trustRoot, *caCertificates)
|
||||
}
|
||||
|
||||
@ -732,13 +760,13 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
|
||||
override val monitoringService: MonitoringService,
|
||||
override val cordappProvider: CordappProviderInternal,
|
||||
override val database: CordaPersistence,
|
||||
override val myInfo: NodeInfo
|
||||
override val myInfo: NodeInfo,
|
||||
override val networkMapCache: NetworkMapCacheInternal
|
||||
) : SingletonSerializeAsToken(), ServiceHubInternal, StateLoader by validatedTransactions {
|
||||
override val rpcFlows = ArrayList<Class<out FlowLogic<*>>>()
|
||||
override val stateMachineRecordedTransactionMapping = DBTransactionMappingStorage()
|
||||
override val auditService = DummyAuditService()
|
||||
override val transactionVerifierService by lazy { makeTransactionVerifierService() }
|
||||
override val networkMapCache by lazy { NetworkMapCacheImpl(PersistentNetworkMapCache(database), identityService) }
|
||||
override val vaultService by lazy { makeVaultService(keyManagementService, validatedTransactions, database.hibernateConfig) }
|
||||
override val contractUpgradeService by lazy { ContractUpgradeServiceImpl() }
|
||||
override val attachments: AttachmentStorage get() = this@AbstractNode.attachments
|
||||
|
@ -2,6 +2,7 @@ package net.corda.node.internal
|
||||
|
||||
import com.codahale.metrics.JmxReporter
|
||||
import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.context.AuthServiceId
|
||||
import net.corda.core.internal.concurrent.openFuture
|
||||
import net.corda.core.internal.concurrent.thenMatch
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
@ -16,11 +17,10 @@ import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.node.VersionInfo
|
||||
import net.corda.node.internal.cordapp.CordappLoader
|
||||
import net.corda.node.internal.security.RPCSecurityManagerImpl
|
||||
import net.corda.node.serialization.KryoServerSerializationScheme
|
||||
import net.corda.node.services.RPCUserServiceImpl
|
||||
import net.corda.node.services.api.SchemaService
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
import net.corda.node.services.config.VerifierType
|
||||
import net.corda.node.services.config.*
|
||||
import net.corda.node.services.messaging.*
|
||||
import net.corda.node.services.transactions.InMemoryTransactionVerifierService
|
||||
import net.corda.node.utilities.AddressUtils
|
||||
@ -133,7 +133,12 @@ open class Node(configuration: NodeConfiguration,
|
||||
private var shutdownHook: ShutdownHook? = null
|
||||
|
||||
override fun makeMessagingService(database: CordaPersistence, info: NodeInfo): MessagingService {
|
||||
userService = RPCUserServiceImpl(configuration.rpcUsers)
|
||||
// Construct security manager reading users data either from the 'security' config section
|
||||
// if present or from rpcUsers list if the former is missing from config.
|
||||
val securityManagerConfig = configuration.security?.authService ?:
|
||||
SecurityConfiguration.AuthService.fromUsers(configuration.rpcUsers)
|
||||
|
||||
securityManager = RPCSecurityManagerImpl(securityManagerConfig)
|
||||
|
||||
val serverAddress = configuration.messagingServerAddress ?: makeLocalMessageBroker()
|
||||
val advertisedAddress = info.addresses.single()
|
||||
@ -156,7 +161,7 @@ open class Node(configuration: NodeConfiguration,
|
||||
|
||||
private fun makeLocalMessageBroker(): NetworkHostAndPort {
|
||||
with(configuration) {
|
||||
messageBroker = ArtemisMessagingServer(this, p2pAddress.port, rpcAddress?.port, services.networkMapCache, userService)
|
||||
messageBroker = ArtemisMessagingServer(this, p2pAddress.port, rpcAddress?.port, services.networkMapCache, securityManager)
|
||||
return NetworkHostAndPort("localhost", p2pAddress.port)
|
||||
}
|
||||
}
|
||||
@ -212,7 +217,7 @@ open class Node(configuration: NodeConfiguration,
|
||||
// Start up the MQ clients.
|
||||
rpcMessagingClient.run {
|
||||
runOnStop += this::stop
|
||||
start(rpcOps, userService)
|
||||
start(rpcOps, securityManager)
|
||||
}
|
||||
verifierMessagingClient?.run {
|
||||
runOnStop += this::stop
|
||||
@ -225,10 +230,10 @@ open class Node(configuration: NodeConfiguration,
|
||||
}
|
||||
|
||||
/**
|
||||
* If the node is persisting to an embedded H2 database, then expose this via TCP with a JDBC URL of the form:
|
||||
* If the node is persisting to an embedded H2 database, then expose this via TCP with a DB URL of the form:
|
||||
* jdbc:h2:tcp://<host>:<port>/node
|
||||
* with username and password as per the DataSource connection details. The key element to enabling this support is to
|
||||
* ensure that you specify a JDBC connection URL of the form jdbc:h2:file: in the node config and that you include
|
||||
* ensure that you specify a DB connection URL of the form jdbc:h2:file: in the node config and that you include
|
||||
* the H2 option AUTO_SERVER_PORT set to the port you desire to use (0 will give a dynamically allocated port number)
|
||||
* but exclude the H2 option AUTO_SERVER=TRUE.
|
||||
* This is not using the H2 "automatic mixed mode" directly but leans on many of the underpinnings. For more details
|
||||
|
@ -81,13 +81,13 @@ open class NodeStartup(val args: Array<String>) {
|
||||
conf0
|
||||
}
|
||||
|
||||
banJavaSerialisation(conf)
|
||||
preNetworkRegistration(conf)
|
||||
if (shouldRegisterWithNetwork(cmdlineOptions, conf)) {
|
||||
banJavaSerialisation(conf)
|
||||
preNetworkRegistration(conf)
|
||||
if (shouldRegisterWithNetwork(cmdlineOptions, conf)) {
|
||||
registerWithNetwork(cmdlineOptions, conf)
|
||||
return true
|
||||
}
|
||||
logStartupInfo(versionInfo, cmdlineOptions, conf)
|
||||
logStartupInfo(versionInfo, cmdlineOptions, conf)
|
||||
|
||||
try {
|
||||
cmdlineOptions.baseDirectory.createDirectories()
|
||||
|
@ -1,5 +1,6 @@
|
||||
package net.corda.node.internal
|
||||
|
||||
import net.corda.client.rpc.PermissionException
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.FlowLogic
|
||||
@ -156,9 +157,12 @@ class RpcAuthorisationProxy(private val implementation: CordaRPCOps, private val
|
||||
private inline fun <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
|
||||
private inline fun <RESULT> guard(methodName: String, args: List<Any?>, action: () -> RESULT): RESULT {
|
||||
|
||||
context().requireEitherPermission(permissionsAllowing.invoke(methodName, args))
|
||||
return action()
|
||||
private inline fun <RESULT> guard(methodName: String, args: List<Class<*>>, action: () -> RESULT) : RESULT {
|
||||
if (!context().isPermitted(methodName, *(args.map { it.name }.toTypedArray()))) {
|
||||
throw PermissionException("User not authorized to perform RPC call $methodName with target $args")
|
||||
}
|
||||
else {
|
||||
return action()
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
||||
}
|
@ -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 = '*'
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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>()
|
||||
}
|
||||
}
|
@ -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 }
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -63,7 +63,7 @@ fun NodeConfiguration.configureWithDevSSLCertificate() = configureDevKeyAndTrust
|
||||
fun SSLConfiguration.configureDevKeyAndTrustStores(myLegalName: CordaX500Name) {
|
||||
certificatesDirectory.createDirectories()
|
||||
if (!trustStoreFile.exists()) {
|
||||
javaClass.classLoader.getResourceAsStream("net/corda/node/internal/certificates/cordatruststore.jks").copyTo(trustStoreFile)
|
||||
loadKeyStore(javaClass.classLoader.getResourceAsStream("net/corda/node/internal/certificates/cordatruststore.jks"), "trustpass").save(trustStoreFile, trustStorePassword)
|
||||
}
|
||||
if (!sslKeystore.exists() || !nodeKeystore.exists()) {
|
||||
val caKeyStore = loadKeyStore(javaClass.classLoader.getResourceAsStream("net/corda/node/internal/certificates/cordadevcakeys.jks"), "cordacadevpass")
|
||||
|
@ -1,6 +1,7 @@
|
||||
package net.corda.node.services.config
|
||||
|
||||
import com.typesafe.config.Config
|
||||
import net.corda.core.context.AuthServiceId
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.seconds
|
||||
@ -21,6 +22,7 @@ interface NodeConfiguration : NodeSSLConfiguration {
|
||||
val exportJMXto: String
|
||||
val dataSourceProperties: Properties
|
||||
val rpcUsers: List<User>
|
||||
val security: SecurityConfiguration?
|
||||
val devMode: Boolean
|
||||
val devModeOptions: DevModeOptions?
|
||||
val compatibilityZoneURL: URL?
|
||||
@ -95,6 +97,7 @@ data class NodeConfigurationImpl(
|
||||
override val dataSourceProperties: Properties,
|
||||
override val compatibilityZoneURL: URL? = null,
|
||||
override val rpcUsers: List<User>,
|
||||
override val security : SecurityConfiguration? = null,
|
||||
override val verifierType: VerifierType,
|
||||
// TODO typesafe config supports the notion of durations. Make use of that by mapping it to java.time.Duration.
|
||||
// Then rename this to messageRedeliveryDelay and make it of type Duration
|
||||
@ -117,8 +120,9 @@ data class NodeConfigurationImpl(
|
||||
// TODO See TODO above. Rename this to nodeInfoPollingFrequency and make it of type Duration
|
||||
override val additionalNodeInfoPollingFrequencyMsec: Long = 5.seconds.toMillis(),
|
||||
override val sshd: SSHDConfiguration? = null,
|
||||
override val database: DatabaseConfig = DatabaseConfig(initialiseSchema = devMode)
|
||||
override val database: DatabaseConfig = DatabaseConfig(initialiseSchema = devMode, exportHibernateJMXStatistics = devMode)
|
||||
) : NodeConfiguration {
|
||||
|
||||
override val exportJMXto: String get() = "http"
|
||||
|
||||
init {
|
||||
@ -126,6 +130,9 @@ data class NodeConfigurationImpl(
|
||||
require(!useTestClock || devMode) { "Cannot use test clock outside of dev mode" }
|
||||
require(devModeOptions == null || devMode) { "Cannot use devModeOptions outside of dev mode" }
|
||||
require(myLegalName.commonName == null) { "Common name must be null: $myLegalName" }
|
||||
require(security == null || rpcUsers.isEmpty()) {
|
||||
"Cannot specify both 'rpcUsers' and 'security' in configuration"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -154,6 +161,80 @@ data class CertChainPolicyConfig(val role: String, private val policy: CertChain
|
||||
}
|
||||
|
||||
data class SSHDConfiguration(val port: Int)
|
||||
|
||||
// Supported types of authentication/authorization data providers
|
||||
enum class AuthDataSourceType {
|
||||
// External RDBMS
|
||||
DB,
|
||||
|
||||
// Static dataset hard-coded in config
|
||||
INMEMORY
|
||||
}
|
||||
|
||||
// Password encryption scheme
|
||||
enum class PasswordEncryption {
|
||||
|
||||
// Password stored in clear
|
||||
NONE,
|
||||
|
||||
// Password salt-hashed using Apache Shiro flexible encryption format
|
||||
// [org.apache.shiro.crypto.hash.format.Shiro1CryptFormat]
|
||||
SHIRO_1_CRYPT
|
||||
}
|
||||
|
||||
// Subset of Node configuration related to security aspects
|
||||
data class SecurityConfiguration(val authService: SecurityConfiguration.AuthService) {
|
||||
|
||||
// Configure RPC/Shell users authentication/authorization service
|
||||
data class AuthService(val dataSource: AuthService.DataSource,
|
||||
val id: AuthServiceId = defaultAuthServiceId(dataSource.type),
|
||||
val options: AuthService.Options? = null) {
|
||||
|
||||
init {
|
||||
require(!(dataSource.type == AuthDataSourceType.INMEMORY &&
|
||||
options?.cache != null)) {
|
||||
"No cache supported for INMEMORY data provider"
|
||||
}
|
||||
}
|
||||
|
||||
// Optional components: cache
|
||||
data class Options(val cache: Options.Cache?) {
|
||||
|
||||
// Cache parameters
|
||||
data class Cache(val expiryTimeInSecs: Long, val capacity: Long)
|
||||
|
||||
}
|
||||
|
||||
// Provider of users credentials and permissions data
|
||||
data class DataSource(val type: AuthDataSourceType,
|
||||
val passwordEncryption: PasswordEncryption = PasswordEncryption.NONE,
|
||||
val connection: Properties? = null,
|
||||
val users: List<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,
|
||||
val remoteInboundPort: Int,
|
||||
val username: String,
|
||||
|
@ -5,11 +5,11 @@ import net.corda.core.crypto.toStringShort
|
||||
import net.corda.core.identity.*
|
||||
import net.corda.core.internal.cert
|
||||
import net.corda.core.internal.toX509CertHolder
|
||||
import net.corda.core.node.services.IdentityService
|
||||
import net.corda.core.node.services.UnknownAnonymousPartyException
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.trace
|
||||
import net.corda.node.services.api.IdentityServiceInternal
|
||||
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
|
||||
import org.bouncycastle.cert.X509CertificateHolder
|
||||
import java.security.InvalidAlgorithmParameterException
|
||||
@ -25,7 +25,7 @@ import javax.annotation.concurrent.ThreadSafe
|
||||
*/
|
||||
@ThreadSafe
|
||||
class InMemoryIdentityService(identities: Iterable<PartyAndCertificate>,
|
||||
trustRoot: X509CertificateHolder) : SingletonSerializeAsToken(), IdentityService {
|
||||
trustRoot: X509CertificateHolder) : SingletonSerializeAsToken(), IdentityServiceInternal {
|
||||
companion object {
|
||||
private val log = contextLogger()
|
||||
}
|
||||
@ -45,18 +45,17 @@ class InMemoryIdentityService(identities: Iterable<PartyAndCertificate>,
|
||||
principalToParties.putAll(identities.associateBy { it.name })
|
||||
}
|
||||
|
||||
// TODO: Check the certificate validation logic
|
||||
@Throws(CertificateExpiredException::class, CertificateNotYetValidException::class, InvalidAlgorithmParameterException::class)
|
||||
override fun verifyAndRegisterIdentity(identity: PartyAndCertificate): PartyAndCertificate? {
|
||||
// Validate the chain first, before we do anything clever with it
|
||||
try {
|
||||
identity.verify(trustAnchor)
|
||||
} catch (e: CertPathValidatorException) {
|
||||
log.error("Certificate validation failed for ${identity.name} against trusted root ${trustAnchor.trustedCert.subjectX500Principal}.")
|
||||
log.error("Certificate path :")
|
||||
log.warn("Certificate validation failed for ${identity.name} against trusted root ${trustAnchor.trustedCert.subjectX500Principal}.")
|
||||
log.warn("Certificate path :")
|
||||
identity.certPath.certificates.reversed().forEachIndexed { index, certificate ->
|
||||
val space = (0 until index).joinToString("") { " " }
|
||||
log.error("$space${certificate.toX509CertHolder().subject}")
|
||||
log.warn("$space${certificate.toX509CertHolder().subject}")
|
||||
}
|
||||
throw e
|
||||
}
|
||||
|
@ -6,12 +6,12 @@ import net.corda.core.crypto.toStringShort
|
||||
import net.corda.core.identity.*
|
||||
import net.corda.core.internal.cert
|
||||
import net.corda.core.internal.toX509CertHolder
|
||||
import net.corda.core.node.services.IdentityService
|
||||
import net.corda.core.node.services.UnknownAnonymousPartyException
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
import net.corda.core.utilities.MAX_HASH_HEX_SIZE
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.debug
|
||||
import net.corda.node.services.api.IdentityServiceInternal
|
||||
import net.corda.node.utilities.AppendOnlyPersistentMap
|
||||
import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX
|
||||
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
|
||||
@ -27,7 +27,7 @@ import javax.persistence.Lob
|
||||
|
||||
@ThreadSafe
|
||||
class PersistentIdentityService(override val trustRoot: X509Certificate,
|
||||
vararg caCertificates: X509Certificate) : SingletonSerializeAsToken(), IdentityService {
|
||||
vararg caCertificates: X509Certificate) : SingletonSerializeAsToken(), IdentityServiceInternal {
|
||||
constructor(trustRoot: X509CertificateHolder) : this(trustRoot.cert)
|
||||
|
||||
companion object {
|
||||
@ -110,17 +110,16 @@ class PersistentIdentityService(override val trustRoot: X509Certificate,
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Check the certificate validation logic
|
||||
@Throws(CertificateExpiredException::class, CertificateNotYetValidException::class, InvalidAlgorithmParameterException::class)
|
||||
override fun verifyAndRegisterIdentity(identity: PartyAndCertificate): PartyAndCertificate? {
|
||||
// Validate the chain first, before we do anything clever with it
|
||||
try {
|
||||
identity.verify(trustAnchor)
|
||||
} catch (e: CertPathValidatorException) {
|
||||
log.error(e.localizedMessage)
|
||||
log.error("Path = ")
|
||||
log.warn(e.localizedMessage)
|
||||
log.warn("Path = ")
|
||||
identity.certPath.certificates.reversed().forEach {
|
||||
log.error(it.toX509CertHolder().subject.toString())
|
||||
log.warn(it.toX509CertHolder().subject.toString())
|
||||
}
|
||||
throw e
|
||||
}
|
||||
|
@ -3,9 +3,9 @@ package net.corda.node.services.keys
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.identity.PartyAndCertificate
|
||||
import net.corda.core.internal.ThreadBox
|
||||
import net.corda.core.node.services.IdentityService
|
||||
import net.corda.core.node.services.KeyManagementService
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
import net.corda.node.services.api.IdentityServiceInternal
|
||||
import org.bouncycastle.operator.ContentSigner
|
||||
import java.security.KeyPair
|
||||
import java.security.PrivateKey
|
||||
@ -25,7 +25,7 @@ import javax.annotation.concurrent.ThreadSafe
|
||||
* etc.
|
||||
*/
|
||||
@ThreadSafe
|
||||
class E2ETestKeyManagementService(val identityService: IdentityService,
|
||||
class E2ETestKeyManagementService(val identityService: IdentityServiceInternal,
|
||||
initialKeys: Set<KeyPair>) : SingletonSerializeAsToken(), KeyManagementService {
|
||||
private class InnerState {
|
||||
val keys = HashMap<PublicKey, PrivateKey>()
|
||||
|
@ -4,8 +4,8 @@ import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.identity.PartyAndCertificate
|
||||
import net.corda.core.internal.cert
|
||||
import net.corda.core.internal.toX509CertHolder
|
||||
import net.corda.core.node.services.IdentityService
|
||||
import net.corda.core.utilities.days
|
||||
import net.corda.node.services.api.IdentityServiceInternal
|
||||
import net.corda.nodeapi.internal.crypto.CertificateType
|
||||
import net.corda.nodeapi.internal.crypto.ContentSignerBuilder
|
||||
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
|
||||
@ -28,18 +28,18 @@ import java.time.Duration
|
||||
* @param revocationEnabled whether to check revocation status of certificates in the certificate path.
|
||||
* @return X.509 certificate and path to the trust root.
|
||||
*/
|
||||
fun freshCertificate(identityService: IdentityService,
|
||||
fun freshCertificate(identityService: IdentityServiceInternal,
|
||||
subjectPublicKey: PublicKey,
|
||||
issuer: PartyAndCertificate,
|
||||
issuerSigner: ContentSigner,
|
||||
revocationEnabled: Boolean = false): PartyAndCertificate {
|
||||
val issuerCert = issuer.certificate.toX509CertHolder()
|
||||
val window = X509Utilities.getCertificateValidityWindow(Duration.ZERO, 3650.days, issuerCert)
|
||||
val ourCertificate = X509Utilities.createCertificate(CertificateType.IDENTITY, issuerCert.subject,
|
||||
val ourCertificate = X509Utilities.createCertificate(CertificateType.WELL_KNOWN_IDENTITY, issuerCert.subject,
|
||||
issuerSigner, issuer.name, subjectPublicKey, window)
|
||||
val ourCertPath = X509CertificateFactory().delegate.generateCertPath(listOf(ourCertificate.cert) + issuer.certPath.certificates)
|
||||
val anonymisedIdentity = PartyAndCertificate(ourCertPath)
|
||||
identityService.verifyAndRegisterIdentity(anonymisedIdentity)
|
||||
identityService.justVerifyAndRegisterIdentity(anonymisedIdentity)
|
||||
return anonymisedIdentity
|
||||
}
|
||||
|
||||
|
@ -2,10 +2,10 @@ package net.corda.node.services.keys
|
||||
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.identity.PartyAndCertificate
|
||||
import net.corda.core.node.services.IdentityService
|
||||
import net.corda.core.node.services.KeyManagementService
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
import net.corda.core.utilities.MAX_HASH_HEX_SIZE
|
||||
import net.corda.node.services.api.IdentityServiceInternal
|
||||
import net.corda.node.utilities.AppendOnlyPersistentMap
|
||||
import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX
|
||||
import org.bouncycastle.operator.ContentSigner
|
||||
@ -24,7 +24,7 @@ import javax.persistence.Lob
|
||||
*
|
||||
* This class needs database transactions to be in-flight during method calls and init.
|
||||
*/
|
||||
class PersistentKeyManagementService(val identityService: IdentityService,
|
||||
class PersistentKeyManagementService(val identityService: IdentityServiceInternal,
|
||||
initialKeys: Set<KeyPair>) : SingletonSerializeAsToken(), KeyManagementService {
|
||||
|
||||
@Entity
|
||||
|
@ -12,9 +12,13 @@ import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.node.services.NetworkMapCache
|
||||
import net.corda.core.node.services.NetworkMapCache.MapChange
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
import net.corda.core.utilities.*
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.debug
|
||||
import net.corda.core.utilities.parsePublicKeyBase58
|
||||
import net.corda.node.internal.Node
|
||||
import net.corda.node.services.RPCUserService
|
||||
import net.corda.node.internal.security.Password
|
||||
import net.corda.node.internal.security.RPCSecurityManager
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
import net.corda.node.services.messaging.NodeLoginModule.Companion.NODE_ROLE
|
||||
import net.corda.node.services.messaging.NodeLoginModule.Companion.PEER_ROLE
|
||||
@ -25,13 +29,13 @@ import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_CLIENT_TLS
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_ROOT_CA
|
||||
import net.corda.nodeapi.internal.crypto.loadKeyStore
|
||||
import net.corda.nodeapi.*
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.ArtemisPeerAddress
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.INTERNAL_PREFIX
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_USER
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NOTIFICATIONS_ADDRESS
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_QUEUE
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEERS_PREFIX
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEER_USER
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.ArtemisPeerAddress
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.NodeAddress
|
||||
import net.corda.nodeapi.internal.requireOnDefaultFileSystem
|
||||
import org.apache.activemq.artemis.api.core.SimpleString
|
||||
@ -97,7 +101,7 @@ class ArtemisMessagingServer(private val config: NodeConfiguration,
|
||||
private val p2pPort: Int,
|
||||
val rpcPort: Int?,
|
||||
val networkMapCache: NetworkMapCache,
|
||||
val userService: RPCUserService) : SingletonSerializeAsToken() {
|
||||
val securityManager: RPCSecurityManager) : SingletonSerializeAsToken() {
|
||||
companion object {
|
||||
private val log = contextLogger()
|
||||
/** 10 MiB maximum allowed file size for attachments, including message headers. TODO: acquire this value from Network Map when supported. */
|
||||
@ -211,7 +215,12 @@ class ArtemisMessagingServer(private val config: NodeConfiguration,
|
||||
addressFullMessagePolicy = AddressFullMessagePolicy.FAIL
|
||||
}
|
||||
)
|
||||
}.configureAddressSecurity()
|
||||
// JMX enablement
|
||||
if (config.exportJMXto.isNotEmpty()) {isJMXManagementEnabled = true
|
||||
isJMXUseBrokerName = true}
|
||||
|
||||
}.configureAddressSecurity()
|
||||
|
||||
|
||||
private fun queueConfig(name: String, address: String = name, filter: String? = null, durable: Boolean): CoreQueueConfiguration {
|
||||
return CoreQueueConfiguration().apply {
|
||||
@ -229,13 +238,11 @@ class ArtemisMessagingServer(private val config: NodeConfiguration,
|
||||
* 3. RPC users. These are only given sufficient access to perform RPC with us.
|
||||
* 4. Verifiers. These are given read access to the verification request queue and write access to the response queue.
|
||||
*/
|
||||
private fun ConfigurationImpl.configureAddressSecurity() : Pair<Configuration, LoginListener> {
|
||||
private fun ConfigurationImpl.configureAddressSecurity(): Pair<Configuration, LoginListener> {
|
||||
val nodeInternalRole = Role(NODE_ROLE, true, true, true, true, true, true, true, true)
|
||||
securityRoles["$INTERNAL_PREFIX#"] = setOf(nodeInternalRole) // Do not add any other roles here as it's only for the node
|
||||
securityRoles[P2P_QUEUE] = setOf(nodeInternalRole, restrictedRole(PEER_ROLE, send = true))
|
||||
securityRoles[RPCApi.RPC_SERVER_QUEUE_NAME] = setOf(nodeInternalRole, restrictedRole(RPC_ROLE, send = true))
|
||||
// TODO: remove the NODE_USER role below once the webserver doesn't need it anymore.
|
||||
securityRoles["${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.$NODE_USER.#"] = setOf(nodeInternalRole)
|
||||
// Each RPC user must have its own role and its own queue. This prevents users accessing each other's queues
|
||||
// and stealing RPC responses.
|
||||
val rolesAdderOnLogin = RolesAdderOnLogin { username ->
|
||||
@ -282,7 +289,7 @@ class ArtemisMessagingServer(private val config: NodeConfiguration,
|
||||
override fun getAppConfigurationEntry(name: String): Array<AppConfigurationEntry> {
|
||||
val options = mapOf(
|
||||
LoginListener::javaClass.name to loginListener,
|
||||
RPCUserService::class.java.name to userService,
|
||||
RPCSecurityManager::class.java.name to securityManager,
|
||||
NodeLoginModule.CERT_CHAIN_CHECKS_OPTION_NAME to certChecks)
|
||||
return arrayOf(AppConfigurationEntry(name, REQUIRED, options))
|
||||
}
|
||||
@ -557,7 +564,7 @@ class NodeLoginModule : LoginModule {
|
||||
private var loginSucceeded: Boolean = false
|
||||
private lateinit var subject: Subject
|
||||
private lateinit var callbackHandler: CallbackHandler
|
||||
private lateinit var userService: RPCUserService
|
||||
private lateinit var securityManager: RPCSecurityManager
|
||||
private lateinit var loginListener: LoginListener
|
||||
private lateinit var peerCertCheck: CertificateChainCheckPolicy.Check
|
||||
private lateinit var nodeCertCheck: CertificateChainCheckPolicy.Check
|
||||
@ -567,7 +574,7 @@ class NodeLoginModule : LoginModule {
|
||||
override fun initialize(subject: Subject, callbackHandler: CallbackHandler, sharedState: Map<String, *>, options: Map<String, *>) {
|
||||
this.subject = subject
|
||||
this.callbackHandler = callbackHandler
|
||||
userService = options[RPCUserService::class.java.name] as RPCUserService
|
||||
securityManager = options[RPCSecurityManager::class.java.name] as RPCSecurityManager
|
||||
loginListener = options[LoginListener::javaClass.name] as LoginListener
|
||||
val certChainChecks: Map<String, CertificateChainCheckPolicy.Check> = uncheckedCast(options[CERT_CHAIN_CHECKS_OPTION_NAME])
|
||||
peerCertCheck = certChainChecks[PEER_ROLE]!!
|
||||
@ -598,7 +605,7 @@ class NodeLoginModule : LoginModule {
|
||||
PEER_ROLE -> authenticatePeer(certificates)
|
||||
NODE_ROLE -> authenticateNode(certificates)
|
||||
VERIFIER_ROLE -> authenticateVerifier(certificates)
|
||||
RPC_ROLE -> authenticateRpcUser(password, username)
|
||||
RPC_ROLE -> authenticateRpcUser(username, Password(password))
|
||||
else -> throw FailedLoginException("Peer does not belong on our network")
|
||||
}
|
||||
principals += UserPrincipal(validatedUser)
|
||||
@ -629,13 +636,8 @@ class NodeLoginModule : LoginModule {
|
||||
return certificates.first().subjectDN.name
|
||||
}
|
||||
|
||||
private fun authenticateRpcUser(password: String, username: String): String {
|
||||
val rpcUser = userService.getUser(username) ?: throw FailedLoginException("User does not exist")
|
||||
if (password != rpcUser.password) {
|
||||
// TODO Switch to hashed passwords
|
||||
// TODO Retrieve client IP address to include in exception message
|
||||
throw FailedLoginException("Password for user $username does not match")
|
||||
}
|
||||
private fun authenticateRpcUser(username: String, password: Password): String {
|
||||
securityManager.authenticate(username, password)
|
||||
loginListener(username)
|
||||
principals += RolePrincipal(RPC_ROLE) // This enables the RPC client to send requests
|
||||
principals += RolePrincipal("${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.$username") // This enables the RPC client to receive responses
|
||||
|
@ -4,7 +4,7 @@ import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.messaging.RPCOps
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.node.services.RPCUserService
|
||||
import net.corda.node.internal.security.RPCSecurityManager
|
||||
import net.corda.nodeapi.internal.config.SSLConfiguration
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_USER
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities
|
||||
@ -16,10 +16,10 @@ class RPCMessagingClient(private val config: SSLConfiguration, serverAddress: Ne
|
||||
private val artemis = ArtemisMessagingClient(config, serverAddress)
|
||||
private var rpcServer: RPCServer? = null
|
||||
|
||||
fun start(rpcOps: RPCOps, userService: RPCUserService) = synchronized(this) {
|
||||
fun start(rpcOps: RPCOps, securityManager: RPCSecurityManager) = synchronized(this) {
|
||||
val locator = artemis.start().sessionFactory.serverLocator
|
||||
val myCert = loadKeyStore(config.sslKeystore, config.keyStorePassword).getX509Certificate(X509Utilities.CORDA_CLIENT_TLS)
|
||||
rpcServer = RPCServer(rpcOps, NODE_USER, NODE_USER, locator, userService, CordaX500Name.build(myCert.subjectX500Principal))
|
||||
rpcServer = RPCServer(rpcOps, NODE_USER, NODE_USER, locator, securityManager, CordaX500Name.build(myCert.subjectX500Principal))
|
||||
}
|
||||
|
||||
fun start2(serverControl: ActiveMQServerControl) = synchronized(this) {
|
||||
|
@ -26,11 +26,10 @@ import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.serialization.SerializationDefaults.RPC_SERVER_CONTEXT
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.utilities.*
|
||||
import net.corda.node.services.RPCUserService
|
||||
import net.corda.node.internal.security.AuthorizingSubject
|
||||
import net.corda.node.internal.security.RPCSecurityManager
|
||||
import net.corda.node.services.logging.pushToLoggingContext
|
||||
import net.corda.nodeapi.*
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_USER
|
||||
import net.corda.nodeapi.internal.config.User
|
||||
import org.apache.activemq.artemis.api.core.Message
|
||||
import org.apache.activemq.artemis.api.core.SimpleString
|
||||
import org.apache.activemq.artemis.api.core.client.ActiveMQClient.DEFAULT_ACK_BATCH_SIZE
|
||||
@ -85,7 +84,7 @@ class RPCServer(
|
||||
private val rpcServerUsername: String,
|
||||
private val rpcServerPassword: String,
|
||||
private val serverLocator: ServerLocator,
|
||||
private val userService: RPCUserService,
|
||||
private val securityManager: RPCSecurityManager,
|
||||
private val nodeLegalName: CordaX500Name,
|
||||
private val rpcConfiguration: RPCServerConfiguration = RPCServerConfiguration.default
|
||||
) {
|
||||
@ -213,6 +212,7 @@ class RPCServer(
|
||||
reaperScheduledFuture?.cancel(false)
|
||||
rpcExecutor?.shutdownNow()
|
||||
reaperExecutor?.shutdownNow()
|
||||
securityManager.close()
|
||||
sessionAndConsumers.forEach {
|
||||
it.sessionFactory.close()
|
||||
}
|
||||
@ -357,9 +357,6 @@ class RPCServer(
|
||||
observableMap.cleanUp()
|
||||
}
|
||||
|
||||
// TODO remove this User once webserver doesn't need it
|
||||
private val nodeUser = User(NODE_USER, NODE_USER, setOf())
|
||||
|
||||
private fun ClientMessage.context(sessionId: Trace.SessionId): RpcAuthContext {
|
||||
val trace = Trace.newInstance(sessionId = sessionId)
|
||||
val externalTrace = externalTrace()
|
||||
@ -368,19 +365,10 @@ class RPCServer(
|
||||
return RpcAuthContext(InvocationContext.rpc(rpcActor.first, trace, externalTrace, impersonatedActor), rpcActor.second)
|
||||
}
|
||||
|
||||
private fun actorFrom(message: ClientMessage): Pair<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 targetLegalIdentity = message.getStringProperty(RPCApi.RPC_TARGET_LEGAL_IDENTITY)?.let(CordaX500Name.Companion::parse) ?: nodeLegalName
|
||||
// TODO switch userService based on targetLegalIdentity
|
||||
val rpcUser = userService.getUser(validatedUser)
|
||||
return if (rpcUser != null) {
|
||||
Actor(Id(rpcUser.username), userService.id, targetLegalIdentity) to RpcPermissions(rpcUser.permissions)
|
||||
} else if (CordaX500Name.parse(validatedUser) == nodeLegalName) {
|
||||
// TODO remove this after Shell and WebServer will no longer need it
|
||||
Actor(Id(nodeUser.username), userService.id, targetLegalIdentity) to RpcPermissions(nodeUser.permissions)
|
||||
} else {
|
||||
throw IllegalArgumentException("Validated user '$validatedUser' is not an RPC user nor the NODE user")
|
||||
}
|
||||
return Pair(Actor(Id(validatedUser), securityManager.id, targetLegalIdentity), securityManager.buildSubject(validatedUser))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,30 +1,9 @@
|
||||
package net.corda.node.services.messaging
|
||||
|
||||
import net.corda.client.rpc.PermissionException
|
||||
import net.corda.core.context.InvocationContext
|
||||
import net.corda.node.services.Permissions
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent
|
||||
import net.corda.node.internal.security.AuthorizingSubject
|
||||
|
||||
data class RpcAuthContext(val invocation: InvocationContext, val grantedPermissions: RpcPermissions) {
|
||||
data class RpcAuthContext(val invocation: InvocationContext,
|
||||
private val authorizer: AuthorizingSubject)
|
||||
: AuthorizingSubject by authorizer
|
||||
|
||||
fun requirePermission(permission: String) = requireEitherPermission(setOf(permission))
|
||||
|
||||
fun requireEitherPermission(permissions: Set<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()
|
||||
}
|
@ -88,6 +88,7 @@ class NetworkMapUpdater(private val networkMapCache: NetworkMapCacheInternal,
|
||||
|
||||
// Only publish and write to disk if there are changes to the node info.
|
||||
val signedNodeInfo = signNodeInfo(newInfo)
|
||||
networkMapCache.addNode(newInfo)
|
||||
fileWatcher.saveToFile(signedNodeInfo)
|
||||
|
||||
if (networkMapClient != null) {
|
||||
|
@ -20,6 +20,7 @@ import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.node.services.api.NetworkMapCacheBaseInternal
|
||||
import net.corda.node.services.api.NetworkMapCacheInternal
|
||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||
@ -38,13 +39,22 @@ class NetworkMapCacheImpl(
|
||||
networkMapCacheBase: NetworkMapCacheBaseInternal,
|
||||
private val identityService: IdentityService
|
||||
) : NetworkMapCacheBaseInternal by networkMapCacheBase, NetworkMapCacheInternal {
|
||||
companion object {
|
||||
private val logger = loggerFor<NetworkMapCacheImpl>()
|
||||
}
|
||||
|
||||
init {
|
||||
networkMapCacheBase.allNodes.forEach { it.legalIdentitiesAndCerts.forEach { identityService.verifyAndRegisterIdentity(it) } }
|
||||
networkMapCacheBase.changed.subscribe { mapChange ->
|
||||
// TODO how should we handle network map removal
|
||||
if (mapChange is MapChange.Added) {
|
||||
mapChange.node.legalIdentitiesAndCerts.forEach {
|
||||
identityService.verifyAndRegisterIdentity(it)
|
||||
try {
|
||||
identityService.verifyAndRegisterIdentity(it)
|
||||
} catch (ignore: Exception) {
|
||||
// Log a warning to indicate node info is not added to the network map cache.
|
||||
logger.warn("Node info for :'${it.name}' is not added to the network map due to verification error.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,31 +4,30 @@ import net.corda.core.context.Actor
|
||||
import net.corda.core.context.InvocationContext
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.node.services.RPCUserService
|
||||
import net.corda.node.services.messaging.RpcPermissions
|
||||
import net.corda.node.internal.security.Password
|
||||
import net.corda.node.internal.security.RPCSecurityManager
|
||||
import net.corda.node.internal.security.tryAuthenticate
|
||||
import org.crsh.auth.AuthInfo
|
||||
import org.crsh.auth.AuthenticationPlugin
|
||||
import org.crsh.plugin.CRaSHPlugin
|
||||
|
||||
class CordaAuthenticationPlugin(val rpcOps:CordaRPCOps, val userService:RPCUserService, val nodeLegalName:CordaX500Name) : CRaSHPlugin<AuthenticationPlugin<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 getName(): String = "corda"
|
||||
|
||||
override fun authenticate(username: String?, credential: String?): AuthInfo {
|
||||
|
||||
if (username == null || credential == null) {
|
||||
return AuthInfo.UNSUCCESSFUL
|
||||
}
|
||||
|
||||
val user = userService.getUser(username)
|
||||
|
||||
if (user != null && user.password == credential) {
|
||||
val actor = Actor(Actor.Id(username), userService.id, nodeLegalName)
|
||||
return CordaSSHAuthInfo(true, makeRPCOpsWithContext(rpcOps, InvocationContext.rpc(actor), RpcPermissions(user.permissions)))
|
||||
val authorizingSubject = securityManager.tryAuthenticate(username, Password(credential))
|
||||
if (authorizingSubject != null) {
|
||||
val actor = Actor(Actor.Id(username), securityManager.id, nodeLegalName)
|
||||
return CordaSSHAuthInfo(true, makeRPCOpsWithContext(rpcOps, InvocationContext.rpc(actor), authorizingSubject))
|
||||
}
|
||||
|
||||
return AuthInfo.UNSUCCESSFUL;
|
||||
return AuthInfo.UNSUCCESSFUL
|
||||
}
|
||||
|
||||
override fun getCredentialType(): Class<String> = String::class.java
|
||||
|
@ -25,11 +25,11 @@ import net.corda.core.messaging.StateMachineUpdate
|
||||
import net.corda.core.node.services.IdentityService
|
||||
import net.corda.node.internal.Node
|
||||
import net.corda.node.internal.StartedNode
|
||||
import net.corda.node.services.RPCUserService
|
||||
import net.corda.node.internal.security.AdminSubject
|
||||
import net.corda.node.internal.security.RPCSecurityManager
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
import net.corda.node.services.messaging.CURRENT_RPC_CONTEXT
|
||||
import net.corda.node.services.messaging.RpcAuthContext
|
||||
import net.corda.node.services.messaging.RpcPermissions
|
||||
import net.corda.node.utilities.ANSIProgressRenderer
|
||||
import net.corda.node.utilities.StdoutANSIProgressRenderer
|
||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||
@ -82,19 +82,19 @@ object InteractiveShell {
|
||||
private lateinit var node: StartedNode<Node>
|
||||
@VisibleForTesting
|
||||
internal lateinit var database: CordaPersistence
|
||||
private lateinit var rpcOps:CordaRPCOps
|
||||
private lateinit var userService:RPCUserService
|
||||
private lateinit var identityService:IdentityService
|
||||
private var shell:Shell? = null
|
||||
private lateinit var rpcOps: CordaRPCOps
|
||||
private lateinit var securityManager: RPCSecurityManager
|
||||
private lateinit var identityService: IdentityService
|
||||
private var shell: Shell? = null
|
||||
private lateinit var nodeLegalName: CordaX500Name
|
||||
|
||||
/**
|
||||
* Starts an interactive shell connected to the local terminal. This shell gives administrator access to the node
|
||||
* internals.
|
||||
*/
|
||||
fun startShell(configuration:NodeConfiguration, cordaRPCOps: CordaRPCOps, userService: RPCUserService, identityService: IdentityService, database: CordaPersistence) {
|
||||
fun startShell(configuration: NodeConfiguration, cordaRPCOps: CordaRPCOps, securityManager: RPCSecurityManager, identityService: IdentityService, database: CordaPersistence) {
|
||||
this.rpcOps = cordaRPCOps
|
||||
this.userService = userService
|
||||
this.securityManager = securityManager
|
||||
this.identityService = identityService
|
||||
this.nodeLegalName = configuration.myLegalName
|
||||
this.database = database
|
||||
@ -123,14 +123,14 @@ object InteractiveShell {
|
||||
}
|
||||
}
|
||||
|
||||
fun runLocalShell(node:StartedNode<Node>) {
|
||||
fun runLocalShell(node: StartedNode<Node>) {
|
||||
val terminal = TerminalFactory.create()
|
||||
val consoleReader = ConsoleReader("Corda", FileInputStream(FileDescriptor.`in`), System.out, terminal)
|
||||
val jlineProcessor = JLineProcessor(terminal.isAnsiSupported, shell, consoleReader, System.out)
|
||||
InterruptHandler { jlineProcessor.interrupt() }.install()
|
||||
thread(name = "Command line shell processor", isDaemon = true) {
|
||||
// Give whoever has local shell access administrator access to the node.
|
||||
val context = RpcAuthContext(net.corda.core.context.InvocationContext.shell(), RpcPermissions.ALL)
|
||||
val context = RpcAuthContext(net.corda.core.context.InvocationContext.shell(), AdminSubject("SHELL_USER"))
|
||||
CURRENT_RPC_CONTEXT.set(context)
|
||||
Emoji.renderIfSupported {
|
||||
jlineProcessor.run()
|
||||
@ -169,7 +169,7 @@ object InteractiveShell {
|
||||
// Don't use the Java language plugin (we may not have tools.jar available at runtime), this
|
||||
// will cause any commands using JIT Java compilation to be suppressed. In CRaSH upstream that
|
||||
// is only the 'jmx' command.
|
||||
return super.getPlugins().filterNot { it is JavaLanguage } + CordaAuthenticationPlugin(rpcOps, userService, nodeLegalName)
|
||||
return super.getPlugins().filterNot { it is JavaLanguage } + CordaAuthenticationPlugin(rpcOps, securityManager, nodeLegalName)
|
||||
}
|
||||
}
|
||||
val attributes = mapOf(
|
||||
@ -180,7 +180,7 @@ object InteractiveShell {
|
||||
context.refresh()
|
||||
this.config = config
|
||||
start(context)
|
||||
return context.getPlugin(ShellFactory::class.java).create(null, CordaSSHAuthInfo(false, makeRPCOpsWithContext(rpcOps, net.corda.core.context.InvocationContext.shell(), RpcPermissions.ALL), StdoutANSIProgressRenderer))
|
||||
return context.getPlugin(ShellFactory::class.java).create(null, CordaSSHAuthInfo(false, makeRPCOpsWithContext(rpcOps, net.corda.core.context.InvocationContext.shell(), AdminSubject("SHELL_USER")), StdoutANSIProgressRenderer))
|
||||
}
|
||||
}
|
||||
|
||||
@ -248,7 +248,7 @@ object InteractiveShell {
|
||||
} catch (e: NoApplicableConstructor) {
|
||||
output.println("No matching constructor found:", Color.red)
|
||||
e.errors.forEach { output.println("- $it", Color.red) }
|
||||
} catch (e:PermissionException) {
|
||||
} catch (e: PermissionException) {
|
||||
output.println(e.message ?: "Access denied", Color.red)
|
||||
} finally {
|
||||
InputStreamDeserializer.closeAll()
|
||||
@ -271,9 +271,9 @@ object InteractiveShell {
|
||||
*/
|
||||
@Throws(NoApplicableConstructor::class)
|
||||
fun <T> runFlowFromString(invoke: (Class<out FlowLogic<T>>, Array<out Any?>) -> FlowProgressHandle<T>,
|
||||
inputData: String,
|
||||
clazz: Class<out FlowLogic<T>>,
|
||||
om: ObjectMapper = yamlInputMapper): FlowProgressHandle<T> {
|
||||
inputData: String,
|
||||
clazz: Class<out FlowLogic<T>>,
|
||||
om: ObjectMapper = yamlInputMapper): FlowProgressHandle<T> {
|
||||
// For each constructor, attempt to parse the input data as a method call. Use the first that succeeds,
|
||||
// and keep track of the reasons we failed so we can print them out if no constructors are usable.
|
||||
val parser = StringToMethodCallParser(clazz, om)
|
||||
|
@ -1,36 +1,39 @@
|
||||
package net.corda.node.shell
|
||||
|
||||
import net.corda.core.context.InvocationContext
|
||||
import net.corda.core.messaging.*
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.node.internal.security.AuthorizingSubject
|
||||
import net.corda.node.services.messaging.CURRENT_RPC_CONTEXT
|
||||
import net.corda.node.services.messaging.RpcAuthContext
|
||||
import net.corda.node.services.messaging.RpcPermissions
|
||||
import java.lang.reflect.InvocationTargetException
|
||||
import java.lang.reflect.Proxy
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.concurrent.Future
|
||||
|
||||
fun makeRPCOpsWithContext(cordaRPCOps: CordaRPCOps, invocationContext:InvocationContext, rpcPermissions: RpcPermissions) : CordaRPCOps {
|
||||
return Proxy.newProxyInstance(CordaRPCOps::class.java.classLoader, arrayOf(CordaRPCOps::class.java), { proxy, method, args ->
|
||||
RPCContextRunner(invocationContext, rpcPermissions) {
|
||||
try {
|
||||
method.invoke(cordaRPCOps, *(args ?: arrayOf()))
|
||||
} catch (e: InvocationTargetException) {
|
||||
// Unpack exception.
|
||||
throw e.targetException
|
||||
}
|
||||
}.get().getOrThrow()
|
||||
}) as CordaRPCOps
|
||||
fun makeRPCOpsWithContext(cordaRPCOps: CordaRPCOps, invocationContext:InvocationContext, authorizingSubject: AuthorizingSubject) : CordaRPCOps {
|
||||
|
||||
return Proxy.newProxyInstance(CordaRPCOps::class.java.classLoader, arrayOf(CordaRPCOps::class.java), { _, method, args ->
|
||||
RPCContextRunner(invocationContext, authorizingSubject) {
|
||||
try {
|
||||
method.invoke(cordaRPCOps, *(args ?: arrayOf()))
|
||||
} catch (e: InvocationTargetException) {
|
||||
// Unpack exception.
|
||||
throw e.targetException
|
||||
}
|
||||
}.get().getOrThrow()
|
||||
}) as CordaRPCOps
|
||||
}
|
||||
|
||||
private class RPCContextRunner<T>(val invocationContext:InvocationContext, val rpcPermissions: RpcPermissions, val block:() -> T) : Thread() {
|
||||
private class RPCContextRunner<T>(val invocationContext: InvocationContext, val authorizingSubject: AuthorizingSubject, val block:() -> T): Thread() {
|
||||
|
||||
private var result: CompletableFuture<T> = CompletableFuture()
|
||||
|
||||
override fun run() {
|
||||
CURRENT_RPC_CONTEXT.set(RpcAuthContext(invocationContext, rpcPermissions))
|
||||
CURRENT_RPC_CONTEXT.set(RpcAuthContext(invocationContext, authorizingSubject))
|
||||
try {
|
||||
result.complete(block())
|
||||
} catch (e:Throwable) {
|
||||
} catch (e: Throwable) {
|
||||
result.completeExceptionally(e)
|
||||
} finally {
|
||||
CURRENT_RPC_CONTEXT.remove()
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user