From f52be47b482f470182b85d11bfac534042f67fbc Mon Sep 17 00:00:00 2001 From: Thomas Schroeter Date: Tue, 16 Jan 2018 10:28:09 +0000 Subject: [PATCH 01/20] Retry obtaining DB connection --- .../node/services/MySQLNotaryServiceTests.kt | 3 ++- .../node/services/config/NodeConfiguration.kt | 11 ++++++++- .../transactions/MySQLNotaryService.kt | 13 +++++----- .../transactions/MySQLUniquenessProvider.kt | 24 ++++++++++++++++--- 4 files changed, 40 insertions(+), 11 deletions(-) diff --git a/node/src/integration-test/kotlin/net/corda/node/services/MySQLNotaryServiceTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/MySQLNotaryServiceTests.kt index a7fd1c2ca5..bd5b3e1963 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/MySQLNotaryServiceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/MySQLNotaryServiceTests.kt @@ -13,6 +13,7 @@ import net.corda.core.identity.Party import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.getOrThrow import net.corda.node.internal.StartedNode +import net.corda.node.services.config.MySQLConfiguration import net.corda.node.services.config.NotaryConfig import net.corda.nodeapi.internal.DevIdentityGenerator import net.corda.nodeapi.internal.network.NetworkParametersCopier @@ -131,7 +132,7 @@ class MySQLNotaryServiceTests : IntegrationTest() { legalName = notaryName, entropyRoot = BigInteger.valueOf(60L), configOverrides = { - val notaryConfig = NotaryConfig(validating = false, mysql = dataStoreProperties) + val notaryConfig = NotaryConfig(validating = false, mysql = MySQLConfiguration(dataStoreProperties)) doReturn(notaryConfig).whenever(it).notary } ) diff --git a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt index ed6053ce05..4bc2f509e7 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt @@ -58,7 +58,7 @@ data class NotaryConfig(val validating: Boolean, val raft: RaftConfig? = null, val bftSMaRt: BFTSMaRtConfiguration? = null, val custom: Boolean = false, - val mysql: Properties? = null + val mysql: MySQLConfiguration? = null ) { init { require(raft == null || bftSMaRt == null || !custom || mysql == null) { @@ -68,6 +68,15 @@ data class NotaryConfig(val validating: Boolean, val isClusterConfig: Boolean get() = raft != null || bftSMaRt != null } +data class MySQLConfiguration( + val dataSource: Properties, + val connectionRetries: Int = 0 +) { + init { + require(connectionRetries >= 0) { "connectionRetries cannot be negative" } + } +} + data class RaftConfig(val nodeAddress: NetworkHostAndPort, val clusterAddresses: List) /** @param exposeRaces for testing only, so its default is not in reference.conf but here. */ diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/MySQLNotaryService.kt b/node/src/main/kotlin/net/corda/node/services/transactions/MySQLNotaryService.kt index adfe3e9744..f32dac307b 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/MySQLNotaryService.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/MySQLNotaryService.kt @@ -5,6 +5,7 @@ import net.corda.core.flows.FlowSession import net.corda.core.node.services.TimeWindowChecker import net.corda.core.node.services.TrustedAuthorityNotaryService import net.corda.node.services.api.ServiceHubInternal +import net.corda.node.services.config.MySQLConfiguration import java.security.PublicKey import java.util.* @@ -12,14 +13,14 @@ import java.util.* abstract class MySQLNotaryService( final override val services: ServiceHubInternal, override val notaryIdentityKey: PublicKey, - dataSourceProperties: Properties, + configuration: MySQLConfiguration, /** Database table will be automatically created in dev mode */ val devMode: Boolean) : TrustedAuthorityNotaryService() { override val timeWindowChecker = TimeWindowChecker(services.clock) override val uniquenessProvider = MySQLUniquenessProvider( services.monitoringService.metrics, - dataSourceProperties + configuration ) override fun start() { @@ -33,14 +34,14 @@ abstract class MySQLNotaryService( class MySQLNonValidatingNotaryService(services: ServiceHubInternal, notaryIdentityKey: PublicKey, - dataSourceProperties: Properties, - devMode: Boolean = false) : MySQLNotaryService(services, notaryIdentityKey, dataSourceProperties, devMode) { + configuration: MySQLConfiguration, + devMode: Boolean = false) : MySQLNotaryService(services, notaryIdentityKey, configuration, devMode) { override fun createServiceFlow(otherPartySession: FlowSession): FlowLogic = NonValidatingNotaryFlow(otherPartySession, this) } class MySQLValidatingNotaryService(services: ServiceHubInternal, notaryIdentityKey: PublicKey, - dataSourceProperties: Properties, - devMode: Boolean = false) : MySQLNotaryService(services, notaryIdentityKey, dataSourceProperties, devMode) { + configuration: MySQLConfiguration, + devMode: Boolean = false) : MySQLNotaryService(services, notaryIdentityKey, configuration, devMode) { override fun createServiceFlow(otherPartySession: FlowSession): FlowLogic = ValidatingNotaryFlow(otherPartySession, this) } \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/MySQLUniquenessProvider.kt b/node/src/main/kotlin/net/corda/node/services/transactions/MySQLUniquenessProvider.kt index 33fe84d79c..5b8e2be796 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/MySQLUniquenessProvider.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/MySQLUniquenessProvider.kt @@ -14,9 +14,11 @@ import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.core.utilities.loggerFor +import net.corda.node.services.config.MySQLConfiguration import java.security.PublicKey import java.sql.BatchUpdateException import java.sql.Connection +import java.sql.SQLTransientConnectionException import java.util.* /** @@ -27,7 +29,7 @@ import java.util.* */ class MySQLUniquenessProvider( metrics: MetricRegistry, - dataSourceProperties: Properties + configuration: MySQLConfiguration ) : UniquenessProvider, SingletonSerializeAsToken() { companion object { private val log = loggerFor() @@ -57,13 +59,29 @@ class MySQLUniquenessProvider( * This is a useful heath metric. */ private val rollbackCounter = metrics.counter("$metricPrefix.Rollback") + /** Incremented when we can not obtain a DB connection. */ + private val connectionExceptionCounter = metrics.counter("$metricPrefix.ConnectionException") /** Track double spend attempts. Note that this will also include notarisation retries. */ private val conflictCounter = metrics.counter("$metricPrefix.Conflicts") - val dataSource = HikariDataSource(HikariConfig(dataSourceProperties)) + val dataSource = HikariDataSource(HikariConfig(configuration.dataSource)) + private val connectionRetries = configuration.connectionRetries private val connection: Connection - get() = dataSource.connection + get() = getConnection() + + private fun getConnection(nRetries: Int = 0): Connection = + try { + dataSource.connection + } catch (e: SQLTransientConnectionException) { + if (nRetries == connectionRetries) { + log.warn("Couldn't obtain connection with {} retries, giving up, {}", nRetries, e) + throw e + } + log.warn("Connection exception, retrying", nRetries+1) + connectionExceptionCounter.inc() + getConnection(nRetries + 1) + } fun createTable() { log.debug("Attempting to create DB table if it does not yet exist: $createTableStatement") From 0638f8c539a47435cede6793cbb2fda6c25b44ba Mon Sep 17 00:00:00 2001 From: Joel Dudley Date: Wed, 17 Jan 2018 15:21:00 +0000 Subject: [PATCH 02/20] Adds Java code samples for versioning docs. --- docs/source/upgrading-cordapps.rst | 315 +++++++++++++++++++++++------ 1 file changed, 256 insertions(+), 59 deletions(-) diff --git a/docs/source/upgrading-cordapps.rst b/docs/source/upgrading-cordapps.rst index f7f8a6b038..893884bb78 100644 --- a/docs/source/upgrading-cordapps.rst +++ b/docs/source/upgrading-cordapps.rst @@ -1,3 +1,9 @@ +.. highlight:: kotlin +.. raw:: html + + + + Upgrading a CorDapp (outside of platform version upgrades) ========================================================== @@ -127,17 +133,39 @@ The ``InitiatingFlow`` version number is included in the flow session handshake the flow running on the other side. In particular, it has a ``flowVersion`` property which can be used to programmatically evolve flows across versions. For example: -.. sourcecode:: kotlin +.. container:: codeset - @Suspendable - override fun call() { - val otherFlowVersion = otherSession.getCounterpartyFlowInfo().flowVersion - val receivedString = if (otherFlowVersion == 1) { - receive(otherParty).unwrap { it.toString() } - } else { - receive(otherParty).unwrap { it } + .. sourcecode:: kotlin + + @Suspendable + override fun call() { + val otherFlowVersion = otherSession.getCounterpartyFlowInfo().flowVersion + val receivedString = if (otherFlowVersion == 1) { + otherSession.receive().unwrap { it.toString() } + } else { + otherSession.receive().unwrap { it } + } + } + + .. sourcecode:: java + + @Suspendable + @Override public Void call() throws FlowException { + int otherFlowVersion = otherSession.getCounterpartyFlowInfo().getFlowVersion(); + String receivedString; + + if (otherFlowVersion == 1) { + receivedString = otherSession.receive(Integer.class).unwrap(integer -> { + return integer.toString(); + }); + } else { + receivedString = otherSession.receive(String.class).unwrap(string -> { + return string; + }); + } + + return null; } - } This code shows a flow that in its first version expected to receive an Int, but in subsequent versions was modified to expect a String. This flow is still able to communicate with parties that are running the older CorDapp containing @@ -147,30 +175,73 @@ How do I deal with interface changes to inlined subflows? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Here is an example of an in-lined subflow: -.. sourcecode:: kotlin +.. container:: codeset - @StartableByRPC - @InitiatingFlow - class FlowA(val recipient: Party) : FlowLogic() { - @Suspendable - override fun call() { - subFlow(FlowB(recipient)) + .. sourcecode:: kotlin + + @StartableByRPC + @InitiatingFlow + class FlowA(val recipient: Party) : FlowLogic() { + @Suspendable + override fun call() { + subFlow(FlowB(recipient)) + } } - } - @InitiatedBy(FlowA::class) - class FlowC(val otherSession: FlowSession) : FlowLogic() { - // Omitted. - } - - // Note: No annotations. This is used as an inlined subflow. - class FlowB(val recipient: Party) : FlowLogic() { - @Suspendable - override fun call() { - val message = "I'm an inlined subflow, so I inherit the @InitiatingFlow's session ID and type." - initiateFlow(recipient).send(message) + @InitiatedBy(FlowA::class) + class FlowC(val otherSession: FlowSession) : FlowLogic() { + // Omitted. + } + + // Note: No annotations. This is used as an inlined subflow. + class FlowB(val recipient: Party) : FlowLogic() { + @Suspendable + override fun call() { + val message = "I'm an inlined subflow, so I inherit the @InitiatingFlow's session ID and type." + initiateFlow(recipient).send(message) + } + } + + .. sourcecode:: java + + @StartableByRPC + @InitiatingFlow + class FlowA extends FlowLogic { + private final Party recipient; + + public FlowA(Party recipient) { + this.recipient = recipient; + } + + @Suspendable + @Override public Void call() throws FlowException { + subFlow(new FlowB(recipient)); + + return null; + } + } + + @InitiatedBy(FlowA.class) + class FlowC extends FlowLogic { + // Omitted. + } + + // Note: No annotations. This is used as an inlined subflow. + class FlowB extends FlowLogic { + private final Party recipient; + + public FlowB(Party recipient) { + this.recipient = recipient; + } + + @Suspendable + @Override public Void call() { + String message = "I'm an inlined subflow, so I inherit the @InitiatingFlow's session ID and type."; + initiateFlow(recipient).send(message); + + return null; + } } - } Inlined subflows are treated as being the flow that invoked them when initiating a new flow session with a counterparty. Suppose flow ``A`` calls inlined subflow B, which, in turn, initiates a session with a counterparty. The ``FlowLogic`` @@ -361,34 +432,138 @@ for details. For backwards compatible changes such as adding columns, the procedure for upgrading a state schema is to extend the existing object relational mapper. For example, we can update: -.. sourcecode:: kotlin +.. container:: codeset - object ObligationSchemaV1 : MappedSchema(Obligation::class.java, 1, listOf(ObligationEntity::class.java)) { - @Entity @Table(name = "obligations") - class ObligationEntity(obligation: Obligation) : PersistentState() { - @Column var currency: String = obligation.amount.token.toString() - @Column var amount: Long = obligation.amount.quantity - @Column @Lob var lender: ByteArray = obligation.lender.owningKey.encoded - @Column @Lob var borrower: ByteArray = obligation.borrower.owningKey.encoded - @Column var linear_id: String = obligation.linearId.id.toString() + .. sourcecode:: kotlin + + object ObligationSchemaV1 : MappedSchema(Obligation::class.java, 1, listOf(ObligationEntity::class.java)) { + @Entity @Table(name = "obligations") + class ObligationEntity(obligation: Obligation) : PersistentState() { + @Column var currency: String = obligation.amount.token.toString() + @Column var amount: Long = obligation.amount.quantity + @Column @Lob var lender: ByteArray = obligation.lender.owningKey.encoded + @Column @Lob var borrower: ByteArray = obligation.borrower.owningKey.encoded + @Column var linear_id: String = obligation.linearId.id.toString() + } + } + + .. sourcecode:: java + + public class ObligationSchemaV1 extends MappedSchema { + public IOUSchemaV1() { + super(Obligation.class, 1, ImmutableList.of(ObligationEntity.class)); + } + + @Entity + @Table(name = "obligations") + public static class ObligationEntity extends PersistentState { + @Column(name = "currency") private final String currency; + @Column(name = "amount") private final Long amount; + @Column(name = "lender") @Lob private final Byte[] lender; + @Column(name = "borrower") @Lob private final Byte[] borrower; + @Column(name = "linear_id") private final UUID linearId; + + + public ObligationEntity(String currency, Long amount, Byte[] lender, Byte[] borrower, UUID linearId) { + this.currency = currency; + this.amount = amount; + this.lender = lender; + this.borrower = borrower; + this.linearId = linearId; + } + + public String getCurrency() { + return currency; + } + + public Long getAmount() { + return amount; + } + + public ByteArray getLender() { + return lender; + } + + public ByteArray getBorrower() { + return borrower; + } + + public UUID getId() { + return linearId; + } + } } - } To: -.. sourcecode:: kotlin +.. container:: codeset - object ObligationSchemaV1 : MappedSchema(Obligation::class.java, 1, listOf(ObligationEntity::class.java)) { - @Entity @Table(name = "obligations") - class ObligationEntity(obligation: Obligation) : PersistentState() { - @Column var currency: String = obligation.amount.token.toString() - @Column var amount: Long = obligation.amount.quantity - @Column @Lob var lender: ByteArray = obligation.lender.owningKey.encoded - @Column @Lob var borrower: ByteArray = obligation.borrower.owningKey.encoded - @Column var linear_id: String = obligation.linearId.id.toString() - @Column var defaulted: Bool = obligation.amount.inDefault // NEW COLUNM! + .. sourcecode:: kotlin + + object ObligationSchemaV1 : MappedSchema(Obligation::class.java, 1, listOf(ObligationEntity::class.java)) { + @Entity @Table(name = "obligations") + class ObligationEntity(obligation: Obligation) : PersistentState() { + @Column var currency: String = obligation.amount.token.toString() + @Column var amount: Long = obligation.amount.quantity + @Column @Lob var lender: ByteArray = obligation.lender.owningKey.encoded + @Column @Lob var borrower: ByteArray = obligation.borrower.owningKey.encoded + @Column var linear_id: String = obligation.linearId.id.toString() + @Column var defaulted: Bool = obligation.amount.inDefault // NEW COLUMN! + } + } + + .. sourcecode:: java + + public class ObligationSchemaV1 extends MappedSchema { + public IOUSchemaV1() { + super(Obligation.class, 1, ImmutableList.of(ObligationEntity.class)); + } + + @Entity + @Table(name = "obligations") + public static class ObligationEntity extends PersistentState { + @Column(name = "currency") private final String currency; + @Column(name = "amount") private final Long amount; + @Column(name = "lender") @Lob private final Byte[] lender; + @Column(name = "borrower") @Lob private final Byte[] borrower; + @Column(name = "linear_id") private final UUID linearId; + @Column(name = "defaulted") private final Boolean defaulted; // NEW COLUMN! + + + public ObligationEntity(String currency, Long amount, Byte[] lender, Byte[] borrower, UUID linearId, Boolean defaulted) { + this.currency = currency; + this.amount = amount; + this.lender = lender; + this.borrower = borrower; + this.linearId = linearId; + this.defaulted = defaulted; + } + + public String getCurrency() { + return currency; + } + + public Long getAmount() { + return amount; + } + + public ByteArray getLender() { + return lender; + } + + public ByteArray getBorrower() { + return borrower; + } + + public UUID getId() { + return linearId; + } + + public Boolean isDefaulted() { + return defaulted; + } + } } - } Thus adding a new column with a default value. @@ -397,21 +572,43 @@ used, as changes to the state are required. To make a backwards-incompatible cha because a property was removed from a state object), the procedure is to define another object relational mapper, then add it to the ``supportedSchemas`` property of your ``QueryableState``, like so: -.. sourcecode:: kotlin +.. container:: codeset - override fun supportedSchemas(): Iterable = listOf(ExampleSchemaV1, ExampleSchemaV2) + .. sourcecode:: kotlin + + override fun supportedSchemas(): Iterable = listOf(ExampleSchemaV1, ExampleSchemaV2) + + .. sourcecode:: java + + @Override public Iterable supportedSchemas() { + return ImmutableList.of(new ExampleSchemaV1(), new ExampleSchemaV2()); + } Then, in ``generateMappedObject``, add support for the new schema: -.. sourcecode:: kotlin +.. container:: codeset - override fun generateMappedObject(schema: MappedSchema): PersistentState { - return when (schema) { - is DummyLinearStateSchemaV1 -> // Omitted. - is DummyLinearStateSchemaV2 -> // Omitted. - else -> throw IllegalArgumentException("Unrecognised schema $schema") + .. sourcecode:: kotlin + + override fun generateMappedObject(schema: MappedSchema): PersistentState { + return when (schema) { + is DummyLinearStateSchemaV1 -> // Omitted. + is DummyLinearStateSchemaV2 -> // Omitted. + else -> throw IllegalArgumentException("Unrecognised schema $schema") + } + } + + .. sourcecode:: java + + @Override public PersistentState generateMappedObject(MappedSchema schema) { + if (schema instanceof DummyLinearStateSchemaV1) { + // Omitted. + } else if (schema instanceof DummyLinearStateSchemaV2) { + // Omitted. + } else { + throw new IllegalArgumentException("Unrecognised schema $schema"); + } } - } With this approach, whenever the state object is stored in the vault, a representation of it will be stored in two separate database tables where possible - one for each supported schema. \ No newline at end of file From d0b62db0d2dce31958ef0611d8e076845819aca8 Mon Sep 17 00:00:00 2001 From: josecoll Date: Wed, 17 Jan 2018 16:02:10 +0000 Subject: [PATCH 03/20] Backported fix. (#2378) --- .../src/main/kotlin/net/corda/plugins/Cordformation.kt | 3 ++- .../cordformation/src/main/kotlin/net/corda/plugins/Node.kt | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordformation.kt b/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordformation.kt index 26878fa357..43444ca270 100644 --- a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordformation.kt +++ b/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordformation.kt @@ -55,7 +55,8 @@ class Cordformation : Plugin { override fun apply(project: Project) { Utils.createCompileConfiguration("cordapp", project) Utils.createRuntimeConfiguration(CORDFORMATION_TYPE, project) - val jolokiaVersion = project.rootProject.ext("jolokia_version") + // TODO: improve how we re-use existing declared external variables from root gradle.build + val jolokiaVersion = try { project.rootProject.ext("jolokia_version") } catch (e: Exception) { "1.3.7" } project.dependencies.add(CORDFORMATION_TYPE, "org.jolokia:jolokia-jvm:$jolokiaVersion:agent") } } diff --git a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Node.kt b/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Node.kt index e5397ad5aa..1f0cdc9701 100644 --- a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Node.kt +++ b/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Node.kt @@ -127,7 +127,8 @@ class Node(private val project: Project) : CordformNode() { * Installs the jolokia monitoring agent JAR to the node/drivers directory */ private fun installAgentJar() { - val jolokiaVersion = project.rootProject.ext("jolokia_version") + // TODO: improve how we re-use existing declared external variables from root gradle.build + val jolokiaVersion = try { project.rootProject.ext("jolokia_version") } catch (e: Exception) { "1.3.7" } val agentJar = project.configuration("runtime").files { (it.group == "org.jolokia") && (it.name == "jolokia-jvm") && From 6edf95506bb2bd96ebffade9b9dd82d468cd4252 Mon Sep 17 00:00:00 2001 From: Matthew Nesbit Date: Wed, 17 Jan 2018 16:06:31 +0000 Subject: [PATCH 04/20] Rename the inbox (#2360) Add changelog entry Address review comments Alternate solution to service queues Fixup after merge --- .../rpc/internal/RPCClientProxyHandler.kt | 4 +- docs/source/changelog.rst | 7 +++ docs/source/messaging.rst | 6 +-- .../internal/ArtemisMessagingComponent.kt | 33 ++++++++++-- .../net/corda/node/amqp/AMQPBridgeTest.kt | 25 ++++----- .../net/corda/node/amqp/ProtonWrapperTests.kt | 12 +++-- .../services/messaging/MQSecurityTest.kt | 14 ++--- .../kotlin/net/corda/node/internal/Node.kt | 4 ++ .../node/services/api/ServiceHubInternal.kt | 4 ++ .../services/messaging/AMQPBridgeManager.kt | 5 +- .../messaging/ArtemisMessagingServer.kt | 13 ++--- .../services/messaging/CoreBridgeManager.kt | 15 ++++-- .../services/messaging/P2PMessagingClient.kt | 51 +++++++++++++++---- .../messaging/VerifierMessagingClient.kt | 8 +-- .../network/PersistentNetworkMapCache.kt | 18 ++++++- .../messaging/ArtemisMessagingTests.kt | 1 + 16 files changed, 157 insertions(+), 63 deletions(-) diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt index 9de4fd6afb..813569dc89 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt @@ -29,7 +29,7 @@ import net.corda.core.utilities.getOrThrow import net.corda.nodeapi.ArtemisConsumer import net.corda.nodeapi.ArtemisProducer import net.corda.nodeapi.RPCApi -import org.apache.activemq.artemis.api.config.ActiveMQDefaultConfiguration +import org.apache.activemq.artemis.api.core.RoutingType import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.api.core.client.ActiveMQClient.DEFAULT_ACK_BATCH_SIZE import org.apache.activemq.artemis.api.core.client.ClientMessage @@ -194,7 +194,7 @@ class RPCClientProxyHandler( TimeUnit.MILLISECONDS ) sessionAndProducerPool.run { - it.session.createTemporaryQueue(clientAddress, ActiveMQDefaultConfiguration.getDefaultRoutingType(), clientAddress) + it.session.createTemporaryQueue(clientAddress, RoutingType.ANYCAST, clientAddress) } val sessionFactory = serverLocator.createSessionFactory() val session = sessionFactory.createSession(rpcUsername, rpcPassword, false, true, true, false, DEFAULT_ACK_BATCH_SIZE) diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index ac9093745b..4119f08ce6 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -141,6 +141,13 @@ UNRELEASED * Peer-to-peer communications is now via AMQP 1.0 as default. Although the legacy Artemis CORE bridging can still be used by setting the ``useAMQPBridges`` configuration property to false. +* The Artemis topics used for peer-to-peer communication have been changed to be more consistent with future cryptographic + agility and to open up the future possibility of sharing brokers between nodes. This is a breaking wire level change + as it means that nodes after this change will not be able to communicate correctly with nodes running the previous version. + Also, any pending enqueued messages in the Artemis message store will not be delivered correctly to their original target. + However, assuming a clean reset of the artemis data and that the nodes are consistent versions, + data persisted via the AMQP serializer will be forward compatible. + .. _changelog_v1: Release 1.0 diff --git a/docs/source/messaging.rst b/docs/source/messaging.rst index 55f48418d3..6dd5b33cc2 100644 --- a/docs/source/messaging.rst +++ b/docs/source/messaging.rst @@ -46,7 +46,7 @@ Message queues The node makes use of various queues for its operation. The more important ones are described below. Others are used for maintenance and other minor purposes. -:``p2p.inbound``: +:``p2p.inbound.$identity``: The node listens for messages sent from other peer nodes on this queue. Only clients who are authenticated to be nodes on the same network are given permission to send. Messages which are routed internally are also sent to this queue (e.g. two flows on the same node communicating with each other). @@ -54,7 +54,7 @@ for maintenance and other minor purposes. :``internal.peers.$identity``: These are a set of private queues only available to the node which it uses to route messages destined to other peers. The queue name ends in the base 58 encoding of the peer's identity key. There is at most one queue per peer. The broker - creates a bridge from this queue to the peer's ``p2p.inbound`` queue, using the network map service to lookup the + creates a bridge from this queue to the peer's ``p2p.inbound.$identity`` queue, using the network map service to lookup the peer's network address. :``internal.services.$identity``: @@ -86,7 +86,7 @@ Clients attempting to connect to the node's broker fall in one of four groups: #. Anyone connecting with the username ``SystemUsers/Peer`` is treated as a peer on the same Corda network as the node. Their TLS root CA must be the same as the node's root CA - the root CA is the doorman of the network and having the same root CA implies we've been let in by the same doorman. If they are part of the same network then they are only given permission - to send to our ``p2p.inbound`` queue, otherwise they are rejected. + to send to our ``p2p.inbound.$identity`` queue, otherwise they are rejected. #. Every other username is treated as a RPC user and authenticated against the node's list of valid RPC users. If that is successful then they are only given sufficient permission to perform RPC, otherwise they are rejected. diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisMessagingComponent.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisMessagingComponent.kt index 7c11cf4bb7..906205b765 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisMessagingComponent.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisMessagingComponent.kt @@ -1,11 +1,12 @@ package net.corda.nodeapi.internal +import net.corda.core.crypto.toStringShort +import net.corda.core.identity.Party import net.corda.core.messaging.MessageRecipientGroup import net.corda.core.messaging.MessageRecipients import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.serialization.CordaSerializable import net.corda.core.utilities.NetworkHostAndPort -import net.corda.core.utilities.toBase58String import java.security.PublicKey /** @@ -23,7 +24,7 @@ class ArtemisMessagingComponent { const val PEER_USER = "SystemUsers/Peer" const val INTERNAL_PREFIX = "internal." const val PEERS_PREFIX = "${INTERNAL_PREFIX}peers." //TODO Come up with better name for common peers/services queue - const val P2P_QUEUE = "p2p.inbound" + const val P2P_PREFIX = "p2p.inbound." const val NOTIFICATIONS_ADDRESS = "${INTERNAL_PREFIX}activemq.notifications" } @@ -49,7 +50,7 @@ class ArtemisMessagingComponent { @CordaSerializable data class NodeAddress(override val queueName: String, override val hostAndPort: NetworkHostAndPort) : ArtemisPeerAddress { constructor(peerIdentity: PublicKey, hostAndPort: NetworkHostAndPort) : - this("$PEERS_PREFIX${peerIdentity.toBase58String()}", hostAndPort) + this("$PEERS_PREFIX${peerIdentity.toStringShort()}", hostAndPort) } /** @@ -62,6 +63,30 @@ class ArtemisMessagingComponent { * @param identity The service identity's owning key. */ data class ServiceAddress(val identity: PublicKey) : ArtemisAddress, MessageRecipientGroup { - override val queueName: String = "$PEERS_PREFIX${identity.toBase58String()}" + override val queueName: String = "$PEERS_PREFIX${identity.toStringShort()}" } + + /** + * [RemoteInboxAddress] implements [SingleMessageRecipient]. It represents the non-local address of a remote inbox. + * @param identity The Node public identity + */ + data class RemoteInboxAddress(val identity: PublicKey) : ArtemisAddress, SingleMessageRecipient { + constructor(party: Party) : this(party.owningKey) + + companion object { + /** + * When transferring a message from the local holding queue to the remote inbox queue + * this method provides a simple translation of the address string. + * The topics are distinct so that proper segregation of internal + * and external access permissions can be made. + */ + fun translateLocalQueueToInboxAddress(address: String): String { + require(address.startsWith(PEERS_PREFIX)) { "Failed to map address: $address to a remote topic as it is not in the $PEERS_PREFIX namespace" } + return P2P_PREFIX + address.substring(PEERS_PREFIX.length) + } + } + + override val queueName: String = "$P2P_PREFIX${identity.toStringShort()}" + } + } diff --git a/node/src/integration-test/kotlin/net/corda/node/amqp/AMQPBridgeTest.kt b/node/src/integration-test/kotlin/net/corda/node/amqp/AMQPBridgeTest.kt index 4c2f0ee881..127eb0718b 100644 --- a/node/src/integration-test/kotlin/net/corda/node/amqp/AMQPBridgeTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/amqp/AMQPBridgeTest.kt @@ -3,18 +3,18 @@ package net.corda.node.amqp import com.nhaarman.mockito_kotlin.any import com.nhaarman.mockito_kotlin.doReturn import com.nhaarman.mockito_kotlin.whenever +import net.corda.core.crypto.toStringShort import net.corda.core.internal.div import net.corda.core.node.NodeInfo import net.corda.core.node.services.NetworkMapCache import net.corda.core.utilities.NetworkHostAndPort -import net.corda.core.utilities.toBase58String import net.corda.node.internal.protonwrapper.netty.AMQPServer import net.corda.node.internal.security.RPCSecurityManager +import net.corda.node.services.api.NetworkMapCacheInternal import net.corda.node.services.config.* import net.corda.node.services.messaging.ArtemisMessagingClient import net.corda.node.services.messaging.ArtemisMessagingServer import net.corda.nodeapi.internal.ArtemisMessagingComponent -import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_QUEUE import net.corda.nodeapi.internal.crypto.loadKeyStore import net.corda.testing.* import net.corda.testing.internal.rigorousMock @@ -50,7 +50,7 @@ class AMQPBridgeTest { @Test fun `test acked and nacked messages`() { // Create local queue - val sourceQueueName = "internal.peers." + BOB.publicKey.toBase58String() + val sourceQueueName = "internal.peers." + BOB.publicKey.toStringShort() val (artemisServer, artemisClient) = createArtemis(sourceQueueName) // Pre-populate local queue with 3 messages @@ -130,11 +130,13 @@ class AMQPBridgeTest { @Test fun `Test legacy bridge still works`() { // Create local queue - val sourceQueueName = "internal.peers." + ALICE.publicKey.toBase58String() + val sourceQueueName = "internal.peers." + BOB.publicKey.toStringShort() val (artemisLegacyServer, artemisLegacyClient) = createLegacyArtemis(sourceQueueName) val (artemisServer, artemisClient) = createArtemis(null) + val inbox = ArtemisMessagingComponent.RemoteInboxAddress(BOB.party).queueName + artemisClient.started!!.session.createQueue(inbox, RoutingType.ANYCAST, inbox, true) val artemis = artemisLegacyClient.started!! for (i in 0 until 3) { @@ -147,8 +149,7 @@ class AMQPBridgeTest { artemis.producer.send(sourceQueueName, artemisMessage) } - - val subs = artemisClient.started!!.session.createConsumer(P2P_QUEUE) + val subs = artemisClient.started!!.session.createConsumer(inbox) for (i in 0 until 3) { val msg = subs.receive() val messageBody = ByteArray(msg.bodySize).apply { msg.bodyBuffer.readBytes(this) } @@ -174,9 +175,9 @@ class AMQPBridgeTest { doReturn(true).whenever(it).useAMQPBridges } artemisConfig.configureWithDevSSLCertificate() - val networkMap = rigorousMock().also { + val networkMap = rigorousMock().also { doReturn(Observable.never()).whenever(it).changed - doReturn(listOf(NodeInfo(listOf(amqpAddress), listOf(BOB.identity), 1, 1L))).whenever(it).getNodesByLegalIdentityKey(any()) + doReturn(listOf(NodeInfo(listOf(amqpAddress), listOf(BOB.identity), 1, 1L))).whenever(it).getNodesByOwningKeyIndex(any()) } val userService = rigorousMock() val artemisServer = ArtemisMessagingServer(artemisConfig, artemisPort, null, networkMap, userService, MAX_MESSAGE_SIZE) @@ -186,7 +187,7 @@ class AMQPBridgeTest { val artemis = artemisClient.started!! if (sourceQueueName != null) { // Local queue for outgoing messages - artemis.session.createQueue(sourceQueueName, RoutingType.MULTICAST, sourceQueueName, true) + artemis.session.createQueue(sourceQueueName, RoutingType.ANYCAST, sourceQueueName, true) } return Pair(artemisServer, artemisClient) } @@ -203,9 +204,9 @@ class AMQPBridgeTest { doReturn(ActiveMqServerConfiguration(BridgeConfiguration(0, 0, 0.0))).whenever(it).activeMQServer } artemisConfig.configureWithDevSSLCertificate() - val networkMap = rigorousMock().also { + val networkMap = rigorousMock().also { doReturn(Observable.never()).whenever(it).changed - doReturn(listOf(NodeInfo(listOf(artemisAddress), listOf(ALICE.identity), 1, 1L))).whenever(it).getNodesByLegalIdentityKey(any()) + doReturn(listOf(NodeInfo(listOf(artemisAddress), listOf(ALICE.identity), 1, 1L))).whenever(it).getNodesByOwningKeyIndex(any()) } val userService = rigorousMock() val artemisServer = ArtemisMessagingServer(artemisConfig, artemisPort2, null, networkMap, userService, MAX_MESSAGE_SIZE) @@ -214,7 +215,7 @@ class AMQPBridgeTest { artemisClient.start() val artemis = artemisClient.started!! // Local queue for outgoing messages - artemis.session.createQueue(sourceQueueName, RoutingType.MULTICAST, sourceQueueName, true) + artemis.session.createQueue(sourceQueueName, RoutingType.ANYCAST, sourceQueueName, true) return Pair(artemisServer, artemisClient) } diff --git a/node/src/integration-test/kotlin/net/corda/node/amqp/ProtonWrapperTests.kt b/node/src/integration-test/kotlin/net/corda/node/amqp/ProtonWrapperTests.kt index 88c299dd41..6c06fa607c 100644 --- a/node/src/integration-test/kotlin/net/corda/node/amqp/ProtonWrapperTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/amqp/ProtonWrapperTests.kt @@ -13,11 +13,13 @@ import net.corda.node.internal.protonwrapper.messages.MessageStatus import net.corda.node.internal.protonwrapper.netty.AMQPClient import net.corda.node.internal.protonwrapper.netty.AMQPServer import net.corda.node.internal.security.RPCSecurityManager +import net.corda.node.services.api.NetworkMapCacheInternal import net.corda.node.services.config.CertChainPolicyConfig import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.configureWithDevSSLCertificate import net.corda.node.services.messaging.ArtemisMessagingClient import net.corda.node.services.messaging.ArtemisMessagingServer +import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_PREFIX import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEER_USER import net.corda.nodeapi.internal.crypto.loadKeyStore import net.corda.testing.* @@ -48,7 +50,7 @@ class ProtonWrapperTests { amqpServer.start() val receiveSubs = amqpServer.onReceive.subscribe { assertEquals(BOB_NAME.toString(), it.sourceLegalName) - assertEquals("p2p.inbound", it.topic) + assertEquals(P2P_PREFIX + "Test", it.topic) assertEquals("Test", String(it.payload)) it.complete(true) } @@ -64,7 +66,7 @@ class ProtonWrapperTests { assertEquals(true, clientConnect.connected) assertEquals(ALICE_NAME, CordaX500Name.build(clientConnect.remoteCert!!.subjectX500Principal)) val msg = amqpClient.createMessage("Test".toByteArray(), - "p2p.inbound", + P2P_PREFIX + "Test", ALICE_NAME.toString(), emptyMap()) amqpClient.write(msg) @@ -151,8 +153,8 @@ class ProtonWrapperTests { assertEquals(true, clientConnected.get().connected) assertEquals(CHARLIE_NAME, CordaX500Name.build(clientConnected.get().remoteCert!!.subjectX500Principal)) val artemis = artemisClient.started!! - val sendAddress = "p2p.inbound" - artemis.session.createQueue(sendAddress, RoutingType.MULTICAST, "queue", true) + val sendAddress = P2P_PREFIX + "Test" + artemis.session.createQueue(sendAddress, RoutingType.ANYCAST, "queue", true) val consumer = artemis.session.createConsumer("queue") val testData = "Test".toByteArray() val testProperty = mutableMapOf() @@ -230,7 +232,7 @@ class ProtonWrapperTests { } artemisConfig.configureWithDevSSLCertificate() - val networkMap = rigorousMock().also { + val networkMap = rigorousMock().also { doReturn(never()).whenever(it).changed } val userService = rigorousMock() diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt index d08866c2f2..1ac529b462 100644 --- a/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt @@ -5,6 +5,7 @@ import net.corda.client.rpc.CordaRPCClient import net.corda.client.rpc.CordaRPCConnection import net.corda.core.crypto.generateKeyPair import net.corda.core.crypto.random63BitValue +import net.corda.core.crypto.toStringShort import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowSession import net.corda.core.flows.InitiatedBy @@ -14,14 +15,13 @@ import net.corda.core.identity.Party import net.corda.core.messaging.CordaRPCOps import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.getOrThrow -import net.corda.core.utilities.toBase58String import net.corda.core.utilities.unwrap import net.corda.node.internal.Node import net.corda.node.internal.StartedNode import net.corda.nodeapi.RPCApi import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.INTERNAL_PREFIX import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NOTIFICATIONS_ADDRESS -import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_QUEUE +import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_PREFIX import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEERS_PREFIX import net.corda.nodeapi.internal.config.SSLConfiguration import net.corda.testing.ALICE_NAME @@ -71,30 +71,30 @@ abstract class MQSecurityTest : NodeBasedTest() { @Test fun `consume message from P2P queue`() { - assertConsumeAttackFails(P2P_QUEUE) + assertConsumeAttackFails("$P2P_PREFIX${alice.info.chooseIdentity().owningKey.toStringShort()}") } @Test fun `consume message from peer queue`() { val bobParty = startBobAndCommunicateWithAlice() - assertConsumeAttackFails("$PEERS_PREFIX${bobParty.owningKey.toBase58String()}") + assertConsumeAttackFails("$PEERS_PREFIX${bobParty.owningKey.toStringShort()}") } @Test fun `send message to address of peer which has been communicated with`() { val bobParty = startBobAndCommunicateWithAlice() - assertSendAttackFails("$PEERS_PREFIX${bobParty.owningKey.toBase58String()}") + assertSendAttackFails("$PEERS_PREFIX${bobParty.owningKey.toStringShort()}") } @Test fun `create queue for peer which has not been communicated with`() { val bob = startNode(BOB_NAME) - assertAllQueueCreationAttacksFail("$PEERS_PREFIX${bob.info.chooseIdentity().owningKey.toBase58String()}") + assertAllQueueCreationAttacksFail("$PEERS_PREFIX${bob.info.chooseIdentity().owningKey.toStringShort()}") } @Test fun `create queue for unknown peer`() { - val invalidPeerQueue = "$PEERS_PREFIX${generateKeyPair().public.toBase58String()}" + val invalidPeerQueue = "$PEERS_PREFIX${generateKeyPair().public.toStringShort()}" assertAllQueueCreationAttacksFail(invalidPeerQueue) } diff --git a/node/src/main/kotlin/net/corda/node/internal/Node.kt b/node/src/main/kotlin/net/corda/node/internal/Node.kt index 0ac13ee6d8..21a16a1367 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -40,6 +40,7 @@ import org.slf4j.Logger import org.slf4j.LoggerFactory import rx.Scheduler import rx.schedulers.Schedulers +import java.security.PublicKey import java.time.Clock import java.util.concurrent.atomic.AtomicInteger import javax.management.ObjectName @@ -166,11 +167,14 @@ open class Node(configuration: NodeConfiguration, VerifierType.OutOfProcess -> VerifierMessagingClient(configuration, serverAddress, services.monitoringService.metrics, networkParameters.maxMessageSize) VerifierType.InMemory -> null } + require(info.legalIdentities.size in 1..2) { "Currently nodes must have a primary address and optionally one serviced address" } + val serviceIdentity: PublicKey? = if (info.legalIdentities.size == 1) null else info.legalIdentities[1].owningKey return P2PMessagingClient( configuration, versionInfo, serverAddress, info.legalIdentities[0].owningKey, + serviceIdentity, serverThread, database, advertisedAddress, diff --git a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt index 7a4f2f669d..e0094054a1 100644 --- a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt +++ b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt @@ -29,6 +29,10 @@ interface NetworkMapCacheBaseInternal : NetworkMapCacheBase { fun getNodeByHash(nodeHash: SecureHash): NodeInfo? + /** Find nodes from the [PublicKey] toShortString representation. + * This is used for Artemis bridge lookup process. */ + fun getNodesByOwningKeyIndex(identityKeyIndex: String): List + /** Adds a node to the local cache (generally only used for adding ourselves). */ fun addNode(node: NodeInfo) diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/AMQPBridgeManager.kt b/node/src/main/kotlin/net/corda/node/services/messaging/AMQPBridgeManager.kt index 8b285bf999..93b1137a00 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/AMQPBridgeManager.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/AMQPBridgeManager.kt @@ -12,8 +12,8 @@ import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.messaging.AMQPBridgeManager.AMQPBridge.Companion.getBridgeName import net.corda.nodeapi.internal.ArtemisMessagingComponent import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_USER -import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_QUEUE import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEER_USER +import net.corda.nodeapi.internal.ArtemisMessagingComponent.RemoteInboxAddress.Companion.translateLocalQueueToInboxAddress import net.corda.nodeapi.internal.crypto.loadKeyStore import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.api.core.client.ActiveMQClient.DEFAULT_ACK_BATCH_SIZE @@ -132,7 +132,8 @@ internal class AMQPBridgeManager(val config: NodeConfiguration, val p2pAddress: properties[key.toString()] = value } log.debug { "Bridged Send to ${legalNames.first()} uuid: ${artemisMessage.getObjectProperty("_AMQ_DUPL_ID")}" } - val sendableMessage = amqpClient.createMessage(data, P2P_QUEUE, + val peerInbox = translateLocalQueueToInboxAddress(queueName) + val sendableMessage = amqpClient.createMessage(data, peerInbox, legalNames.first().toString(), properties) sendableMessage.onComplete.then { diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt b/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt index 285d4ce94c..7eacea9520 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt @@ -8,16 +8,15 @@ import net.corda.core.internal.div import net.corda.core.internal.noneOrSingle import net.corda.core.internal.uncheckedCast 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.NetworkHostAndPort import net.corda.core.utilities.contextLogger import net.corda.core.utilities.debug -import net.corda.core.utilities.parsePublicKeyBase58 import net.corda.node.internal.Node import net.corda.node.internal.security.Password import net.corda.node.internal.security.RPCSecurityManager +import net.corda.node.services.api.NetworkMapCacheInternal 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 @@ -31,7 +30,7 @@ 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.P2P_PREFIX import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEERS_PREFIX import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEER_USER import net.corda.nodeapi.internal.ArtemisMessagingComponent.NodeAddress @@ -94,7 +93,7 @@ import javax.security.cert.CertificateException class ArtemisMessagingServer(private val config: NodeConfiguration, private val p2pPort: Int, val rpcPort: Int?, - val networkMapCache: NetworkMapCache, + val networkMapCache: NetworkMapCacheInternal, val securityManager: RPCSecurityManager, val maxMessageSize: Int) : SingletonSerializeAsToken() { companion object { @@ -191,7 +190,6 @@ class ArtemisMessagingServer(private val config: NodeConfiguration, // by having its password be an unknown securely random 128-bit value. clusterPassword = BigInteger(128, newSecureRandom()).toString(16) queueConfigurations = listOf( - queueConfig(P2P_QUEUE, durable = true), // Create an RPC queue: this will service locally connected clients only (not via a bridge) and those // clients must have authenticated. We could use a single consumer for everything and perhaps we should, // but these queues are not worth persisting. @@ -243,7 +241,7 @@ class ArtemisMessagingServer(private val config: NodeConfiguration, private fun ConfigurationImpl.configureAddressSecurity(): Pair { val nodeInternalRole = Role(NODE_ROLE, true, true, true, true, true, true, true, true) securityRoles["$INTERNAL_PREFIX#"] = setOf(nodeInternalRole) // Do not add any other roles here as it's only for the node - securityRoles[P2P_QUEUE] = setOf(nodeInternalRole, restrictedRole(PEER_ROLE, send = true)) + securityRoles["$P2P_PREFIX#"] = setOf(nodeInternalRole, restrictedRole(PEER_ROLE, send = true)) securityRoles[RPCApi.RPC_SERVER_QUEUE_NAME] = setOf(nodeInternalRole, restrictedRole(RPC_ROLE, send = true)) // Each RPC user must have its own role and its own queue. This prevents users accessing each other's queues // and stealing RPC responses. @@ -309,8 +307,7 @@ class ArtemisMessagingServer(private val config: NodeConfiguration, if (queueName.startsWith(PEERS_PREFIX)) { try { - val identity = parsePublicKeyBase58(queueName.substring(PEERS_PREFIX.length)) - val nodeInfos = networkMapCache.getNodesByLegalIdentityKey(identity) + val nodeInfos = networkMapCache.getNodesByOwningKeyIndex(queueName.substring(PEERS_PREFIX.length)) if (nodeInfos.isNotEmpty()) { nodeInfos.forEach { deployBridgeToPeer(it) } } else { diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/CoreBridgeManager.kt b/node/src/main/kotlin/net/corda/node/services/messaging/CoreBridgeManager.kt index 4161d89836..b821d13cff 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/CoreBridgeManager.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/CoreBridgeManager.kt @@ -10,15 +10,17 @@ import net.corda.node.services.config.NodeConfiguration import net.corda.nodeapi.ArtemisTcpTransport import net.corda.nodeapi.ConnectionDirection import net.corda.nodeapi.internal.ArtemisMessagingComponent -import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_QUEUE import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEER_USER +import net.corda.nodeapi.internal.ArtemisMessagingComponent.RemoteInboxAddress.Companion.translateLocalQueueToInboxAddress import net.corda.nodeapi.internal.crypto.X509Utilities +import org.apache.activemq.artemis.api.core.Message import org.apache.activemq.artemis.core.config.BridgeConfiguration import org.apache.activemq.artemis.core.remoting.impl.netty.NettyConnection import org.apache.activemq.artemis.core.remoting.impl.netty.NettyConnector import org.apache.activemq.artemis.core.remoting.impl.netty.NettyConnectorFactory import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants import org.apache.activemq.artemis.core.server.ActiveMQServer +import org.apache.activemq.artemis.core.server.cluster.Transformer import org.apache.activemq.artemis.spi.core.remoting.* import org.apache.activemq.artemis.utils.ConfigurationHelper import java.time.Duration @@ -46,7 +48,7 @@ internal class CoreBridgeManager(val config: NodeConfiguration, val activeMQServ /** - * All nodes are expected to have a public facing address called [ArtemisMessagingComponent.P2P_QUEUE] for receiving + * All nodes are expected to have a public facing address called p2p.inbound.$identity for receiving * messages from other nodes. When we want to send a message to a node we send it to our internal address/queue for it, * as defined by ArtemisAddress.queueName. A bridge is then created to forward messages from this queue to the node's * P2P address. @@ -64,7 +66,6 @@ internal class CoreBridgeManager(val config: NodeConfiguration, val activeMQServ activeMQServer.deployBridge(BridgeConfiguration().apply { name = getBridgeName(queueName, target) this.queueName = queueName - forwardingAddress = P2P_QUEUE staticConnectors = listOf(target.toString()) confirmationWindowSize = 100000 // a guess isUseDuplicateDetection = true // Enable the bridge's automatic deduplication logic @@ -77,6 +78,7 @@ internal class CoreBridgeManager(val config: NodeConfiguration, val activeMQServ // our TLS certificate. user = PEER_USER password = PEER_USER + transformerClassName = InboxTopicTransformer::class.java.name }) } @@ -99,6 +101,13 @@ internal class CoreBridgeManager(val config: NodeConfiguration, val activeMQServ } } +class InboxTopicTransformer : Transformer { + override fun transform(message: Message): Message { + message.address = translateLocalQueueToInboxAddress(message.address) + return message + } +} + class VerifyingNettyConnectorFactory : NettyConnectorFactory() { override fun createConnector(configuration: MutableMap, handler: BufferHandler?, diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/P2PMessagingClient.kt b/node/src/main/kotlin/net/corda/node/services/messaging/P2PMessagingClient.kt index 74c183fd5c..766e67b780 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/P2PMessagingClient.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/P2PMessagingClient.kt @@ -21,7 +21,6 @@ import net.corda.node.utilities.AffinityExecutor import net.corda.node.utilities.AppendOnlyPersistentMap import net.corda.node.utilities.PersistentMap import net.corda.nodeapi.internal.ArtemisMessagingComponent.* -import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_QUEUE import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX import org.apache.activemq.artemis.api.core.ActiveMQObjectClosedException @@ -55,21 +54,28 @@ import javax.persistence.Lob * invoke methods on the provided implementation. There is more documentation on this in the docsite and the * CordaRPCClient class. * - * @param serverAddress The address of the broker instance to connect to (might be running in the same process). - * @param myIdentity The public key to be used as the ArtemisMQ address and queue name for the node. - * @param nodeExecutor An executor to run received message tasks upon. - * @param advertisedAddress The node address for inbound connections, advertised to the network map service and peers. - * If not provided, will default to [serverAddress]. + * @param config The configuration of the node, which is used for controlling the message redelivery options. + * @param versionInfo All messages from the node carry the version info and received messages are checked against this for compatibility. + * @param serverAddress The host and port of the Artemis broker. + * @param myIdentity The primary identity of the node, which defines the messaging address for externally received messages. + * It is also used to construct the myAddress field, which is ultimately advertised in the network map. + * @param serviceIdentity An optional second identity if the node is also part of a group address, for example a notary. + * @param nodeExecutor The received messages are marshalled onto the server executor to prevent Netty buffers leaking during fiber suspends. + * @param database The nodes database, which is used to deduplicate messages. + * @param advertisedAddress The externally advertised version of the Artemis broker address used to construct myAddress and included + * in the network map data. + * @param maxMessageSize A bound applied to the message size. */ @ThreadSafe class P2PMessagingClient(config: NodeConfiguration, private val versionInfo: VersionInfo, serverAddress: NetworkHostAndPort, private val myIdentity: PublicKey, + private val serviceIdentity: PublicKey?, private val nodeExecutor: AffinityExecutor.ServiceAffinityExecutor, private val database: CordaPersistence, advertisedAddress: NetworkHostAndPort = serverAddress, - private val maxMessageSize: Int + maxMessageSize: Int ) : SingletonSerializeAsToken(), MessagingService { companion object { private val log = contextLogger() @@ -128,6 +134,7 @@ class P2PMessagingClient(config: NodeConfiguration, private class InnerState { var running = false var p2pConsumer: ClientConsumer? = null + var serviceConsumer: ClientConsumer? = null } private val messagesToRedeliver = database.transaction { @@ -183,8 +190,23 @@ class P2PMessagingClient(config: NodeConfiguration, fun start() { state.locked { val session = artemis.start().session + val inbox = RemoteInboxAddress(myIdentity).queueName // Create a queue, consumer and producer for handling P2P network messages. - p2pConsumer = session.createConsumer(P2P_QUEUE) + createQueueIfAbsent(inbox) + p2pConsumer = session.createConsumer(inbox) + if (serviceIdentity != null) { + val serviceAddress = RemoteInboxAddress(serviceIdentity).queueName + createQueueIfAbsent(serviceAddress) + val serviceHandler = session.createConsumer(serviceAddress) + serviceHandler.setMessageHandler { msg -> + val message: ReceivedMessage? = artemisToCordaMessage(msg) + if (message != null) + deliver(message) + state.locked { + msg.individualAcknowledge() + } + } + } } resumeMessageRedelivery() @@ -261,7 +283,7 @@ class P2PMessagingClient(config: NodeConfiguration, val platformVersion = message.required(platformVersionProperty) { getIntProperty(it) } // Use the magic deduplication property built into Artemis as our message identity too val uuid = message.required(HDR_DUPLICATE_DETECTION_ID) { UUID.fromString(message.getStringProperty(it)) } - log.trace { "Received message from: ${message.address} user: $user topic: $topic sessionID: $sessionID uuid: $uuid" } + log.info("Received message from: ${message.address} user: $user topic: $topic sessionID: $sessionID uuid: $uuid") return ArtemisReceivedMessage(TopicSession(topic, sessionID), CordaX500Name.parse(user), platformVersion, uuid, message) } catch (e: Exception) { @@ -346,6 +368,13 @@ class P2PMessagingClient(config: NodeConfiguration, // Ignore it: this can happen if the server has gone away before we do. } p2pConsumer = null + val s = serviceConsumer + try { + s?.close() + } catch (e: ActiveMQObjectClosedException) { + // Ignore it: this can happen if the server has gone away before we do. + } + serviceConsumer = null prevRunning } if (running && !nodeExecutor.isOnThread) { @@ -446,7 +475,7 @@ class P2PMessagingClient(config: NodeConfiguration, private fun getMQAddress(target: MessageRecipients): String { return if (target == myAddress) { // If we are sending to ourselves then route the message directly to our P2P queue. - P2P_QUEUE + RemoteInboxAddress(myIdentity).queueName } else { // Otherwise we send the message to an internal queue for the target residing on our broker. It's then the // broker's job to route the message to the target's P2P queue. @@ -463,7 +492,7 @@ class P2PMessagingClient(config: NodeConfiguration, val queueQuery = session.queueQuery(SimpleString(queueName)) if (!queueQuery.isExists) { log.info("Create fresh queue $queueName bound on same address") - session.createQueue(queueName, RoutingType.MULTICAST, queueName, true) + session.createQueue(queueName, RoutingType.ANYCAST, queueName, true) } } } diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/VerifierMessagingClient.kt b/node/src/main/kotlin/net/corda/node/services/messaging/VerifierMessagingClient.kt index 9a8c0cae66..9a8c82c4eb 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/VerifierMessagingClient.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/VerifierMessagingClient.kt @@ -7,15 +7,15 @@ import net.corda.core.transactions.LedgerTransaction import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.loggerFor import net.corda.node.services.transactions.OutOfProcessTransactionVerifierService -import net.corda.node.utilities.* +import net.corda.node.utilities.AffinityExecutor import net.corda.nodeapi.VerifierApi import net.corda.nodeapi.VerifierApi.VERIFICATION_REQUESTS_QUEUE_NAME import net.corda.nodeapi.VerifierApi.VERIFICATION_RESPONSES_QUEUE_NAME_PREFIX import net.corda.nodeapi.internal.config.SSLConfiguration import org.apache.activemq.artemis.api.core.RoutingType import org.apache.activemq.artemis.api.core.SimpleString -import org.apache.activemq.artemis.api.core.client.* -import java.util.concurrent.* +import org.apache.activemq.artemis.api.core.client.ClientConsumer +import java.util.concurrent.TimeUnit class VerifierMessagingClient(config: SSLConfiguration, serverAddress: NetworkHostAndPort, metrics: MetricRegistry, private val maxMessageSize: Int) : SingletonSerializeAsToken() { companion object { @@ -40,7 +40,7 @@ class VerifierMessagingClient(config: SSLConfiguration, serverAddress: NetworkHo val queueQuery = session.queueQuery(SimpleString(queueName)) if (!queueQuery.isExists) { log.info("Create fresh queue $queueName bound on same address") - session.createQueue(queueName, RoutingType.MULTICAST, queueName, true) + session.createQueue(queueName, RoutingType.ANYCAST, queueName, true) } } createQueueIfAbsent(VERIFICATION_REQUESTS_QUEUE_NAME) diff --git a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt index 4bc21ea475..ca54c89019 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt @@ -159,6 +159,12 @@ open class PersistentNetworkMapCache( private val nodesByKeyCache = NonInvalidatingCache>(1024, 8, { key -> database.transaction { queryByIdentityKey(session, key) } }) + override fun getNodesByOwningKeyIndex(identityKeyIndex: String): List { + return database.transaction { + queryByIdentityKeyIndex(session, identityKeyIndex) + } + } + override fun getNodeByAddress(address: NetworkHostAndPort): NodeInfo? = database.transaction { queryByAddress(session, address) } override fun getPeerCertificateByLegalName(name: CordaX500Name): PartyAndCertificate? = identityByLegalNameCache.get(name).orElse(null) @@ -244,15 +250,23 @@ open class PersistentNetworkMapCache( } private fun findByIdentityKey(session: Session, identityKey: PublicKey): List { + return findByIdentityKeyIndex(session, identityKey.toStringShort()) + } + + private fun findByIdentityKeyIndex(session: Session, identityKeyIndex: String): List { val query = session.createQuery( "SELECT n FROM ${NodeInfoSchemaV1.PersistentNodeInfo::class.java.name} n JOIN n.legalIdentitiesAndCerts l WHERE l.owningKeyHash = :owningKeyHash", NodeInfoSchemaV1.PersistentNodeInfo::class.java) - query.setParameter("owningKeyHash", identityKey.toStringShort()) + query.setParameter("owningKeyHash", identityKeyIndex) return query.resultList } private fun queryByIdentityKey(session: Session, identityKey: PublicKey): List { - val result = findByIdentityKey(session, identityKey) + return queryByIdentityKeyIndex(session, identityKey.toStringShort()) + } + + private fun queryByIdentityKeyIndex(session: Session, identityKeyIndex: String): List { + val result = findByIdentityKeyIndex(session, identityKeyIndex) return result.map { it.toNodeInfo() } } diff --git a/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt b/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt index 279df7ae4a..7a87b0e8a4 100644 --- a/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt @@ -173,6 +173,7 @@ class ArtemisMessagingTests { MOCK_VERSION_INFO.copy(platformVersion = platformVersion), server, identity.public, + null, ServiceAffinityExecutor("ArtemisMessagingTests", 1), database, maxMessageSize = maxMessageSize).apply { From d19eb4a9d7467f0119dfec0b91c0d187b74b0834 Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Wed, 17 Jan 2018 17:47:40 +0000 Subject: [PATCH 05/20] Removed useHTTPS from NodeConfiguration as it's not needed by the node. (#2375) It's still expected to be present in node.conf if the web server needs to use SSL. --- config/dev/generalnodea.conf | 1 - config/dev/generalnodeb.conf | 1 - config/dev/nameservernode.conf | 1 - docs/source/corda-configuration-file.rst | 5 ----- docs/source/deploying-a-node.rst | 2 -- .../src/main/resources/example-network-map-node.conf | 1 - .../example-code/src/main/resources/example-node.conf | 1 - .../corda/node/services/config/NodeConfiguration.kt | 5 ----- node/src/main/resources/reference.conf | 1 - .../node/services/config/NodeConfigurationImplTest.kt | 1 - .../kotlin/net/corda/test/spring/SpringDriver.kt | 2 +- .../src/main/kotlin/net/corda/testing/driver/Driver.kt | 5 ++++- .../net/corda/testing/node/internal/DriverDSLImpl.kt | 10 +++++++--- 13 files changed, 12 insertions(+), 24 deletions(-) diff --git a/config/dev/generalnodea.conf b/config/dev/generalnodea.conf index 12a3200105..7547300ca6 100644 --- a/config/dev/generalnodea.conf +++ b/config/dev/generalnodea.conf @@ -4,4 +4,3 @@ trustStorePassword : "trustpass" p2pAddress : "localhost:10002" rpcAddress : "localhost:10003" webAddress : "localhost:10004" -useHTTPS : false diff --git a/config/dev/generalnodeb.conf b/config/dev/generalnodeb.conf index 55ebb1e690..f922e91536 100644 --- a/config/dev/generalnodeb.conf +++ b/config/dev/generalnodeb.conf @@ -4,4 +4,3 @@ trustStorePassword : "trustpass" p2pAddress : "localhost:10005" rpcAddress : "localhost:10006" webAddress : "localhost:10007" -useHTTPS : false diff --git a/config/dev/nameservernode.conf b/config/dev/nameservernode.conf index 0519e56872..3f700628dc 100644 --- a/config/dev/nameservernode.conf +++ b/config/dev/nameservernode.conf @@ -6,4 +6,3 @@ webAddress : "localhost:10001" notary : { validating : true } -useHTTPS : false diff --git a/docs/source/corda-configuration-file.rst b/docs/source/corda-configuration-file.rst index 49572d1c8e..79ce625b0d 100644 --- a/docs/source/corda-configuration-file.rst +++ b/docs/source/corda-configuration-file.rst @@ -43,7 +43,6 @@ Simple Notary configuration file. notary : { validating : false } - useHTTPS : false devMode : true compatibilityZoneURL : "https://cz.corda.net" @@ -129,10 +128,6 @@ path to the node's base directory. Only one of ``raft``, ``bftSMaRt`` or ``custom`` configuration values may be specified. -:useHTTPS: If false the node's web server will be plain HTTP. If true the node will use the same certificate and private - key from the ``/certificates/sslkeystore.jks`` file as the ArtemisMQ port for HTTPS. If HTTPS is enabled - then unencrypted HTTP traffic to the node's **webAddress** port is not supported. - :rpcUsers: A list of users who are authorised to access the RPC system. Each user in the list is a config object with the following fields: diff --git a/docs/source/deploying-a-node.rst b/docs/source/deploying-a-node.rst index 4f94c02c95..6b17b9dd5d 100644 --- a/docs/source/deploying-a-node.rst +++ b/docs/source/deploying-a-node.rst @@ -46,7 +46,6 @@ handling, and ensures the Corda service is run at boot. myLegalName : "O=Bank of Breakfast Tea, L=London, C=GB" keyStorePassword : "cordacadevpass" trustStorePassword : "trustpass" - useHTTPS : false devMode : false rpcUsers=[ { @@ -217,7 +216,6 @@ at boot, and means the Corda service stays running with no users connected to th keyStorePassword : "cordacadevpass" trustStorePassword : "trustpass" extraAdvertisedServiceIds: [ "" ] - useHTTPS : false devMode : false rpcUsers=[ { diff --git a/docs/source/example-code/src/main/resources/example-network-map-node.conf b/docs/source/example-code/src/main/resources/example-network-map-node.conf index e75807bbc7..980f5f7031 100644 --- a/docs/source/example-code/src/main/resources/example-network-map-node.conf +++ b/docs/source/example-code/src/main/resources/example-network-map-node.conf @@ -4,4 +4,3 @@ trustStorePassword : "trustpass" p2pAddress : "my-network-map:10000" webAddress : "localhost:10001" sshdAddress : "localhost:10002" -useHTTPS : false diff --git a/docs/source/example-code/src/main/resources/example-node.conf b/docs/source/example-code/src/main/resources/example-node.conf index c01a10fd78..3715ff22e8 100644 --- a/docs/source/example-code/src/main/resources/example-node.conf +++ b/docs/source/example-code/src/main/resources/example-node.conf @@ -10,7 +10,6 @@ dataSourceProperties : { p2pAddress : "my-corda-node:10002" rpcAddress : "my-corda-node:10003" webAddress : "localhost:10004" -useHTTPS : false rpcUsers : [ { username=user1, password=letmein, permissions=[ StartFlow.net.corda.protocols.CashProtocol ] } ] diff --git a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt index 8347c28d21..61a7da64ab 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt @@ -17,8 +17,6 @@ import java.util.* val Int.MB: Long get() = this * 1024L * 1024L interface NodeConfiguration : NodeSSLConfiguration { - // myLegalName should be only used in the initial network registration, we should use the name from the certificate instead of this. - // TODO: Remove this so we don't accidentally use this identity in the code? val myLegalName: CordaX500Name val emailAddress: String val exportJMXto: String @@ -34,8 +32,6 @@ interface NodeConfiguration : NodeSSLConfiguration { val notary: NotaryConfig? val activeMQServer: ActiveMqServerConfiguration val additionalNodeInfoPollingFrequencyMsec: Long - // TODO Remove as this is only used by the driver - val useHTTPS: Boolean val p2pAddress: NetworkHostAndPort val rpcAddress: NetworkHostAndPort? val messagingServerAddress: NetworkHostAndPort? @@ -114,7 +110,6 @@ data class NodeConfigurationImpl( // 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 override val messageRedeliveryDelaySeconds: Int = 30, - override val useHTTPS: Boolean, override val p2pAddress: NetworkHostAndPort, override val rpcAddress: NetworkHostAndPort?, // TODO This field is slightly redundant as p2pAddress is sufficient to hold the address of the node's MQ broker. diff --git a/node/src/main/resources/reference.conf b/node/src/main/resources/reference.conf index fe29913bca..d73f0b8ae8 100644 --- a/node/src/main/resources/reference.conf +++ b/node/src/main/resources/reference.conf @@ -14,7 +14,6 @@ database = { exportHibernateJMXStatistics = "false" } devMode = true -useHTTPS = false h2port = 0 useTestClock = false verifierType = InMemory diff --git a/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt b/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt index f04ca1ec4d..1b6f20fa81 100644 --- a/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt @@ -40,7 +40,6 @@ class NodeConfigurationImplTest { dataSourceProperties = makeTestDataSourceProperties(ALICE_NAME.organisation), rpcUsers = emptyList(), verifierType = VerifierType.InMemory, - useHTTPS = false, p2pAddress = NetworkHostAndPort("localhost", 0), rpcAddress = NetworkHostAndPort("localhost", 1), messagingServerAddress = null, diff --git a/samples/irs-demo/src/integration-test/kotlin/net/corda/test/spring/SpringDriver.kt b/samples/irs-demo/src/integration-test/kotlin/net/corda/test/spring/SpringDriver.kt index c7d7a57ffd..9d83d481ba 100644 --- a/samples/irs-demo/src/integration-test/kotlin/net/corda/test/spring/SpringDriver.kt +++ b/samples/irs-demo/src/integration-test/kotlin/net/corda/test/spring/SpringDriver.kt @@ -71,7 +71,7 @@ data class SpringBootDriverDSL(private val driverDSL: DriverDSLImpl) : InternalD } private fun queryWebserver(handle: NodeHandle, process: Process, checkUrl: String): WebserverHandle { - val protocol = if (handle.configuration.useHTTPS) "https://" else "http://" + val protocol = if (handle.useHTTPS) "https://" else "http://" val url = URL(URL("$protocol${handle.webAddress}"), checkUrl) val client = OkHttpClient.Builder().connectTimeout(5, TimeUnit.SECONDS).readTimeout(10, TimeUnit.SECONDS).build() diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt index 7866a7cb91..ad258f6f70 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt @@ -15,8 +15,8 @@ import net.corda.node.internal.StartedNode import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.VerifierType import net.corda.testing.DUMMY_NOTARY_NAME -import net.corda.testing.node.User import net.corda.testing.node.NotarySpec +import net.corda.testing.node.User import net.corda.testing.node.internal.DriverDSLImpl import net.corda.testing.node.internal.genericDriver import net.corda.testing.node.internal.getTimestampAsDirectoryName @@ -41,6 +41,7 @@ sealed class NodeHandle { abstract val rpc: CordaRPCOps abstract val configuration: NodeConfiguration abstract val webAddress: NetworkHostAndPort + abstract val useHTTPS: Boolean /** * Stops the referenced node. @@ -52,6 +53,7 @@ sealed class NodeHandle { override val rpc: CordaRPCOps, override val configuration: NodeConfiguration, override val webAddress: NetworkHostAndPort, + override val useHTTPS: Boolean, val debugPort: Int?, val process: Process, private val onStopCallback: () -> Unit @@ -70,6 +72,7 @@ sealed class NodeHandle { override val rpc: CordaRPCOps, override val configuration: NodeConfiguration, override val webAddress: NetworkHostAndPort, + override val useHTTPS: Boolean, val node: StartedNode, val nodeThread: Thread, private val onStopCallback: () -> Unit diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt index 24d08bc896..21182a1da3 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt @@ -327,7 +327,7 @@ class DriverDSLImpl( } private fun queryWebserver(handle: NodeHandle, process: Process): WebserverHandle { - val protocol = if (handle.configuration.useHTTPS) "https://" else "http://" + val protocol = if (handle.useHTTPS) "https://" else "http://" val url = URL("$protocol${handle.webAddress}/api/status") val client = OkHttpClient.Builder().connectTimeout(5, TimeUnit.SECONDS).readTimeout(60, TimeUnit.SECONDS).build() @@ -605,10 +605,14 @@ class DriverDSLImpl( val baseDirectory = config.corda.baseDirectory.createDirectories() localNetworkMap?.networkParametersCopier?.install(baseDirectory) localNetworkMap?.nodeInfosCopier?.addConfig(baseDirectory) + val onNodeExit: () -> Unit = { localNetworkMap?.nodeInfosCopier?.removeConfig(baseDirectory) countObservables.remove(config.corda.myLegalName) } + + val useHTTPS = config.typesafe.run { hasPath("useHTTPS") && getBoolean("useHTTPS") } + if (startInProcess ?: startNodesInProcess) { val nodeAndThreadFuture = startInProcessNode(executorService, config, cordappPackages) shutdownManager.registerShutdown( @@ -622,7 +626,7 @@ class DriverDSLImpl( return nodeAndThreadFuture.flatMap { (node, thread) -> establishRpc(config, openFuture()).flatMap { rpc -> allNodesConnected(rpc).map { - NodeHandle.InProcess(rpc.nodeInfo(), rpc, config.corda, webAddress, node, thread, onNodeExit) + NodeHandle.InProcess(rpc.nodeInfo(), rpc, config.corda, webAddress, useHTTPS, node, thread, onNodeExit) } } } @@ -651,7 +655,7 @@ class DriverDSLImpl( } processDeathFuture.cancel(false) log.info("Node handle is ready. NodeInfo: ${rpc.nodeInfo()}, WebAddress: $webAddress") - NodeHandle.OutOfProcess(rpc.nodeInfo(), rpc, config.corda, webAddress, debugPort, process, onNodeExit) + NodeHandle.OutOfProcess(rpc.nodeInfo(), rpc, config.corda, webAddress, useHTTPS, debugPort, process, onNodeExit) } } } From 776289cb012f44f681a0d144b46c6eecb2b2466a Mon Sep 17 00:00:00 2001 From: joeldudleyr3 Date: Wed, 10 Jan 2018 15:48:29 +0000 Subject: [PATCH 06/20] Corrects glossary. --- docs/source/glossary.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/source/glossary.rst b/docs/source/glossary.rst index 4432ddb2a2..1fa01d7a59 100644 --- a/docs/source/glossary.rst +++ b/docs/source/glossary.rst @@ -1,6 +1,8 @@ Glossary ======== +AMQP + The serialisation mechanism used within Corda for everything except flow checkpoints and RPC. Artemis The message queuing middleware used within Corda Attachment @@ -30,7 +32,7 @@ Gradle Kotlin The language used to code Corda. Fully compatible with any JVM language, including (obviously) Java. Kryo - The serialisation mechanism used within Corda - which is subject to change in a future release. + The serialisation mechanism used within Corda for flow checkpoints and RPC. Input In Corda terms, an input state is one that is used and consumed within a transaction. Once consumed, it cannot be re-used. JVM From 4248660017075d73f237f55045cdf7d88aad5266 Mon Sep 17 00:00:00 2001 From: joeldudleyr3 Date: Wed, 10 Jan 2018 16:28:43 +0000 Subject: [PATCH 07/20] Various grammar fixes and clarifications. --- docs/source/serialization.rst | 177 +++++++++++++++++----------------- 1 file changed, 89 insertions(+), 88 deletions(-) diff --git a/docs/source/serialization.rst b/docs/source/serialization.rst index 036c283608..6bc84ee1ca 100644 --- a/docs/source/serialization.rst +++ b/docs/source/serialization.rst @@ -47,62 +47,62 @@ AMQP ==== Originally Corda used a ``Kryo``-based serialization scheme throughout for all serialization contexts. However, it was realised there -was a compelling use case for the definition and development of a custom format based upon AMQP 1.0. The primary drivers for this were +was a compelling use case for the definition and development of a custom format based upon AMQP 1.0. The primary drivers for this were: - #. A desire to have a schema describing what has been serialized along-side the actual data: + #. A desire to have a schema describing what has been serialized alongside the actual data: - #. To assist with versioning, both in terms of being able to interpret long ago archived data (e.g. trades from - a decade ago, long after the code has changed) and between differing code versions. - #. To make it easier to write user interfaces that can navigate the serialized form of data. - #. To support cross platform (non-JVM) interaction, where the format of a class file is not so easily interpreted. + #. To assist with versioning, both in terms of being able to interpret data archived long ago (e.g. trades from + a decade ago, long after the code has changed) and between differing code versions + #. To make it easier to write user interfaces that can navigate the serialized form of data + #. To support cross platform (non-JVM) interaction, where the format of a class file is not so easily interpreted #. A desire to use a documented and static wire format that is platform independent, and is not subject to change with - 3rd party library upgrades etc. + 3rd party library upgrades, etc. #. A desire to support open-ended polymorphism, where the number of subclasses of a superclass can expand over time - and do not need to be defined in the schema *upfront*, which is key to many Corda concepts, such as contract states. - #. Increased security from deserialized objects being constructed through supported constructors rather than having - data poked directly into their fields without an opportunity to validate consistency or intercept attempts to manipulate - supposed invariants. + and the subclasses do not need to be defined in the schema *upfront*. This is key to many Corda concepts, such as states. + #. Increased security by constructing deserialized objects through supported constructors, rather than having + data inserted directly into their fields without an opportunity to validate consistency or intercept attempts to manipulate + supposed invariants Delivering this is an ongoing effort by the Corda development team. At present, the ``Kryo``-based format is still used by the RPC framework on -both the client and server side. However, it is planned that this will move to the AMQP framework when ready. +both the client and server side. However, it is planned that the RPC framework will move to the AMQP framework when ready. The AMQP framework is currently used for: - #. The peer to peer context, representing inter-node communication. - #. The persistence layer, representing contract states persisted into the vault. + #. The peer-to-peer context, representing inter-node communication + #. The persistence layer, representing contract states persisted into the vault -Finally, for the checkpointing of flows Corda will continue to use the existing ``Kryo`` scheme. +Finally, for the checkpointing of flows, Corda will continue to use the existing ``Kryo`` scheme. This separation of serialization schemes into different contexts allows us to use the most suitable framework for that context rather than -attempting to force a one size fits all approach. Where ``Kryo`` is more suited to the serialization of a programs stack frames, being more flexible -than our AMQP framework in what it can construct and serialize, that flexibility makes it exceptionally difficult to make secure. Conversely -our AMQP framework allows us to concentrate on a robust a secure framework that can be reasoned about thus made safer with far fewer unforeseen +attempting to force a one-size-fits-all approach. ``Kryo`` is more suited to the serialization of a program's stack frames, as it is more flexible +than our AMQP framework in what it can construct and serialize. However, that flexibility makes it exceptionally difficult to make secure. Conversely, +our AMQP framework allows us to concentrate on a secure framework that can be reasoned about and thus made safer, with far fewer security holes. .. note:: Selection of serialization context should, for the most part, be opaque to CorDapp developers, the Corda framework selecting - the correct context as confugred. + the correct context as configured. -.. For information on our choice of AMQP 1.0, see :doc:`amqp-choice`. For detail on how we utilise AMQP 1.0 and represent +.. note:: For information on our choice of AMQP 1.0, see :doc:`amqp-choice`. For detail on how we utilise AMQP 1.0 and represent objects in AMQP types, see :doc:`amqp-format`. -We describe here what is and will be supported in the Corda AMQP format from the perspective -of CorDapp developers, to allow for CorDapps to take into consideration the future state. The AMQP serialization format will of -course continue to apply the whitelisting functionality that is already in place and described in :doc:`serialization`. +This document describes what is currently and what will be supported in the Corda AMQP format from the perspective +of CorDapp developers, to allow CorDapps to take into consideration the future state. The AMQP serialization format will +continue to apply the whitelisting functionality that is already in place and described in :doc:`serialization`. Core Types ---------- -Here we describe the classes and interfaces that the AMQP serialization format will support. +This section describes the classes and interfaces that the AMQP serialization format supports. Collection Types ```````````````` The following collection types are supported. Any implementation of the following will be mapped to *an* implementation of the interface or class on the other end. -e.g. If you, for example, use a Guava implementation of a collection it will deserialize as a different implementation, -but will continue to adhere to the most specific of any of the following interfaces. You should use only these types -as the declared types of fields and properties, and not the concrete implementation types. Collections must be used -in their generic form, the generic type parameters will be included in the schema, and the elements type checked against the -generic parameters when deserialized. +For example, if you use a Guava implementation of a collection, it will deserialize as the primitive collection type. + +The declared types of properties should only use these types, and not any concrete implementation types (e.g. +Guava implementations). Collections must be used in their generic form, the generic type parameters will be included in +the schema, and the element's type will be checked against the generic parameters when deserialized. :: @@ -116,8 +116,8 @@ generic parameters when deserialized. java.util.SortedMap java.util.NavigableMap -However, we will support the concrete implementation types below explicitly and also as the declared type of a field, as -a convenience. +However, as a convenience, we explicitly support the concrete implementation types below, and they can be used as the +declared types of properties. :: @@ -146,12 +146,12 @@ All the primitive types are supported. Arrays `````` -We also support arrays of any supported type, primitive or otherwise. +Arrays of any type are supported, primitive or otherwise. JDK Types ````````` -The following types are supported from the JDK libraries. +The following JDK library types are supported: :: @@ -195,10 +195,10 @@ The following types are supported from the JDK libraries. java.util.Currency java.util.UUID -Third Party Types +Third-Party Types ````````````````` -The following 3rd party types are supported. +The following 3rd-party types are supported: :: @@ -210,17 +210,17 @@ The following 3rd party types are supported. Corda Types ``````````` -Classes and interfaces in the Corda codebase annotated with ``@CordaSerializable`` are of course supported. +Any classes and interfaces in the Corda codebase annotated with ``@CordaSerializable`` are supported. -All Corda exceptions that are expected to be serialized inherit from ``CordaThrowable`` via either ``CordaException``, for -checked exceptions, or ``CordaRuntimeException``, for unchecked exceptions. Any ``Throwable`` that is serialized but does -not conform to ``CordaThrowable`` will be converted to a ``CordaRuntimeException`` with the original exception type +All Corda exceptions that are expected to be serialized inherit from ``CordaThrowable`` via either ``CordaException`` (for +checked exceptions) or ``CordaRuntimeException`` (for unchecked exceptions). Any ``Throwable`` that is serialized but does +not conform to ``CordaThrowable`` will be converted to a ``CordaRuntimeException``, with the original exception type and other properties retained within it. Custom Types ------------ -Here are the rules to adhere to for support of your own types: +You own types must adhere to the following rules to be supported: Classes ``````` @@ -229,39 +229,40 @@ General Rules ''''''''''''' #. The class must be compiled with parameter names included in the ``.class`` file. This is the default in Kotlin - but must be turned on in Java (``-parameters`` command line option to ``javac``). + but must be turned on in Java using the ``-parameters`` command line option to ``javac`` - .. note:: In circumstances where classes cannot be recompiled, such as when using a third party library, then - the creation of a proxy serializer can be used to avoid this problem. Details on creating such an object can be found on the + .. note:: In circumstances where classes cannot be recompiled, such as when using a third-party library, a + proxy serializer can be used to avoid this problem. Details on creating such an object can be found on the :doc:`cordapp-custom-serializers` page. - #. The class is annotated with ``@CordaSerializable``. - #. The declared types of constructor arguments, getters, and setters must be supported, and where generics are used the + #. The class must be annotated with ``@CordaSerializable`` + #. The declared types of constructor arguments, getters, and setters must be supported, and where generics are used, the generic parameter must be a supported type, an open wildcard (``*``), or a bounded wildcard which is currently - widened to an open wildcard. - #. Any superclass must adhere to the same rules, but can be abstract. - #. Object graph cycles are not supported, so an object cannot refer to itself, directly or indirectly. + widened to an open wildcard + #. Any superclass must adhere to the same rules, but can be abstract + #. Object graph cycles are not supported, so an object cannot refer to itself, directly or indirectly Constructor Instantiation ''''''''''''''''''''''''' -The primary way the AMQP serialization framework for Corda instantiates objects is via a defined constructor. This is -used to first determine which properties of an object are to be serialised then, on deserialization, it is used to +The primary way Corda's AMQP serialization framework instantiates objects is via a specified constructor. This is +used to first determine which properties of an object are to be serialised, then, on deserialization, it is used to instantiate the object with the serialized values. -This is the recommended design idiom for serializable objects in Corda as it allows for immutable state objects to -be created +It is recommended that serializable objects in Corda adhere to the following rules, as they allow immutable state +objects to be deserialised: - #. A Java Bean getter for each of the properties in the constructor, with the names matching up. For example, for a constructor - parameter ``foo``, there must be a getter called ``getFoo()``. If the type of ``foo`` is boolean, the getter may - optionally be called ``isFoo()``. This is why the class must be compiled with parameter names turned on. + #. A Java Bean getter for each of the properties in the constructor, with a name of the form ``getX``. For example, for a constructor + parameter ``foo``, there must be a getter called ``getFoo()``. If ``foo`` is a boolean, the getter may + optionally be called ``isFoo()`` (this is why the class must be compiled with parameter names turned on) #. A constructor which takes all of the properties that you wish to record in the serialized form. This is required in - order for the serialization framework to reconstruct an instance of your class. + order for the serialization framework to reconstruct an instance of your class #. If more than one constructor is provided, the serialization framework needs to know which one to use. The ``@ConstructorForDeserialization`` annotation can be used to indicate which one. For a Kotlin class, without the ``@ConstructorForDeserialization`` annotation, the - *primary constructor* will be selected. + *primary constructor* will be selected -In Kotlin, this maps cleanly to a data class where there getters are synthesized automatically. For example, +In Kotlin, this maps cleanly to a data class where there getters are synthesized automatically. For example, suppose we +have the following data class: .. container:: codeset @@ -269,9 +270,10 @@ In Kotlin, this maps cleanly to a data class where there getters are synthesized data class Example (val a: Int, val b: String) -Both properties a and b will be included in the serialised form. However, as stated above, properties not mentioned in -the constructor will not be serialised. For example, in the following code property c will not be considered part of the -serialised form +Properties ``a`` and ``b`` will be included in the serialised form. + +However, properties not mentioned in the constructor will not be serialised. For example, in the following code, +property ``c`` will not be considered part of the serialised form: .. container:: codeset @@ -289,14 +291,14 @@ serialised form Setter Instantiation '''''''''''''''''''' -As an alternative to constructor based initialisation Corda can also determine the important elements of an -object by inspecting the getter and setter methods present on a class. If a class has **only** a default +As an alternative to constructor-based initialisation, Corda can also determine the important elements of an +object by inspecting the getter and setter methods present on the class. If a class has **only** a default constructor **and** properties then the serializable properties will be determined by the presence of -both a getter and setter for that property that are both publicly visible. I.e. the class adheres to -the classic *idiom* of mutable JavaBeans. +both a getter and setter for that property that are both publicly visible (i.e. the class adheres to +the classic *idiom* of mutable JavaBeans). -On deserialization, a default instance will first be created and then, in turn, the setters invoked -on that object to populate the correct values. +On deserialization, a default instance will first be created, and then the setters will be invoked on that object to +populate it with the correct values. For example: @@ -322,7 +324,7 @@ Inaccessible Private Properties ``````````````````````````````` Whilst the Corda AMQP serialization framework supports private object properties without publicly -accessible getter methods this development idiom is strongly discouraged. +accessible getter methods, this development idiom is strongly discouraged. For example. @@ -348,15 +350,14 @@ For example. } } -When designing stateful objects is should be remembered that they are not, despite appearances, traditional +When designing stateful objects, is should be remembered that they are not, despite appearances, traditional programmatic constructs. They are signed over, transformed, serialised, and relationally mapped. As such, -all elements should be publicly accessible by design +all elements should be publicly accessible by design. -.. warning:: IDEs will indiciate erroneously that properties can be given something other than public - visibility. Ignore this as whilst it will work, as discussed above there are many reasons why this isn't - a good idea and those are beyond the scope of the IDEs inference rules +.. warning:: IDEs will indicate erroneously that properties can be given something other than public visibility. Ignore + this, as whilst it will work, as discussed above there are many reasons why this isn't a good idea. -Providing a public getter, as per the following example, is acceptable +Providing a public getter, as per the following example, is acceptable: .. container:: codeset @@ -390,7 +391,7 @@ Providing a public getter, as per the following example, is acceptable Enums ````` - #. All enums are supported, provided they are annotated with ``@CordaSerializable``. + #. All enums are supported, provided they are annotated with ``@CordaSerializable`` Exceptions @@ -399,24 +400,24 @@ Exceptions The following rules apply to supported ``Throwable`` implementations. #. If you wish for your exception to be serializable and transported type safely it should inherit from either - ``CordaException`` or ``CordaRuntimeException``. + ``CordaException`` or ``CordaRuntimeException`` #. If not, the ``Throwable`` will deserialize to a ``CordaRuntimeException`` with the details of the original - ``Throwable`` contained within it, including the class name of the original ``Throwable``. + ``Throwable`` contained within it, including the class name of the original ``Throwable`` Kotlin Objects `````````````` - #. Kotlin ``object`` s are singletons and treated differently. They are recorded into the stream with no properties + #. Kotlin ``object`` s are singletons and treated differently. They are recorded into the stream with no properties, and deserialize back to the singleton instance. Currently, the same is not true of Java singletons, - and they will deserialize to new instances of the class. - #. Kotlin's anonymous ``object`` s are not currently supported. I.e. constructs like: - ``object : Contract {...}`` will not serialize correctly and need to be re-written as an explicit class declaration. + which will deserialize to new instances of the class + #. Kotlin's anonymous ``object`` s (i.e. constructs like ``object : Contract {...}``) are not currently supported + and will not serialize correctly. They need to be re-written as an explicit class declaration The Carpenter ````````````` -We support a class carpenter that can dynamically manufacture classes from the supplied schema when deserializing -in the JVM without the supporting classes on the classpath. This can be useful where other components might expect to +We support a class carpenter that can dynamically manufacture classes from the supplied schema when deserializing, +without the supporting classes being present on the classpath. This can be useful where other components might expect to be able to use reflection over the deserialized data, and also for ensuring classes not on the classpath can be deserialized without loading potentially malicious code dynamically without security review outside of a fully sandboxed environment. A more detailed discussion of the carpenter will be provided in a future update to the documentation. @@ -425,25 +426,25 @@ Future Enhancements ``````````````````` #. Java singleton support. We will add support for identifying classes which are singletons and identifying the - static method responsible for returning the singleton instance. + static method responsible for returning the singleton instance #. Instance internalizing support. We will add support for identifying classes that should be resolved against an instances map to avoid - creating many duplicate instances that are equal. Similar to ``String.intern()``. + creating many duplicate instances that are equal (similar to ``String.intern()``) .. Type Evolution: Type Evolution -------------- -Type evolution is the mechanisms by which classes can be altered over time yet still remain serializable and deserializable across +Type evolution is the mechanism by which classes can be altered over time yet still remain serializable and deserializable across all versions of the class. This ensures an object serialized with an older idea of what the class "looked like" can be deserialized and a version of the current state of the class instantiated. -More detail can be found in :doc:`serialization-default-evolution` +More detail can be found in :doc:`serialization-default-evolution`. Enum Evolution `````````````` Corda supports interoperability of enumerated type versions. This allows such types to be changed over time without breaking -backward (or forward) compatibility. The rules and mechanisms for doing this are discussed in :doc:`serialization-enum-evolution`` +backward (or forward) compatibility. The rules and mechanisms for doing this are discussed in :doc:`serialization-enum-evolution``. From 7a458323a0f134b99e6e75a819679cffa129496f Mon Sep 17 00:00:00 2001 From: joeldudleyr3 Date: Wed, 10 Jan 2018 17:34:08 +0000 Subject: [PATCH 08/20] Clarified some further points. --- docs/source/serialization.rst | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/source/serialization.rst b/docs/source/serialization.rst index 6bc84ee1ca..96fab2adf0 100644 --- a/docs/source/serialization.rst +++ b/docs/source/serialization.rst @@ -37,7 +37,7 @@ It's reproduced here as an example of both ways you can do this for a couple of :end-before: END 7 .. note:: Several of the core interfaces at the heart of Corda are already annotated and so any classes that implement - them will automatically be whitelisted. This includes `Contract`, `ContractState` and `CommandData`. + them will automatically be whitelisted. This includes ``Contract``, ``ContractState`` and ``CommandData``. .. warning:: Java 8 Lambda expressions are not serializable except in flow checkpoints, and then not by default. The syntax to declare a serializable Lambda expression that will work with Corda is ``Runnable r = (Runnable & Serializable) () -> System.out.println("Hello World");``, or @@ -101,7 +101,7 @@ The following collection types are supported. Any implementation of the followi For example, if you use a Guava implementation of a collection, it will deserialize as the primitive collection type. The declared types of properties should only use these types, and not any concrete implementation types (e.g. -Guava implementations). Collections must be used in their generic form, the generic type parameters will be included in +Guava implementations). Collections must specify their generic type, the generic type parameters will be included in the schema, and the element's type will be checked against the generic parameters when deserialized. :: @@ -407,9 +407,10 @@ The following rules apply to supported ``Throwable`` implementations. Kotlin Objects `````````````` - #. Kotlin ``object`` s are singletons and treated differently. They are recorded into the stream with no properties, - and deserialize back to the singleton instance. Currently, the same is not true of Java singletons, - which will deserialize to new instances of the class + #. Kotlin's non-anonymous ``object`` s (i.e. constructs like ``object foo : Contract {...}``) are singletons and + treated differently. They are recorded into the stream with no properties, and deserialize back to the + singleton instance. Currently, the same is not true of Java singletons, which will deserialize to new instances + of the class #. Kotlin's anonymous ``object`` s (i.e. constructs like ``object : Contract {...}``) are not currently supported and will not serialize correctly. They need to be re-written as an explicit class declaration From a3fd653affb01a660b2be31314866d8d1f1715de Mon Sep 17 00:00:00 2001 From: Katelyn Baker Date: Wed, 10 Jan 2018 15:13:21 +0000 Subject: [PATCH 09/20] Serialization documentation update --- docs/source/serialization.rst | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/source/serialization.rst b/docs/source/serialization.rst index f217d749b9..036c283608 100644 --- a/docs/source/serialization.rst +++ b/docs/source/serialization.rst @@ -43,9 +43,6 @@ It's reproduced here as an example of both ways you can do this for a couple of expression that will work with Corda is ``Runnable r = (Runnable & Serializable) () -> System.out.println("Hello World");``, or ``Callable c = (Callable & Serializable) () -> "Hello World";``. -.. warning:: We will be replacing the use of Kryo in the serialization framework and so additional changes here are - likely. - AMQP ==== @@ -233,6 +230,11 @@ General Rules #. The class must be compiled with parameter names included in the ``.class`` file. This is the default in Kotlin but must be turned on in Java (``-parameters`` command line option to ``javac``). + + .. note:: In circumstances where classes cannot be recompiled, such as when using a third party library, then + the creation of a proxy serializer can be used to avoid this problem. Details on creating such an object can be found on the + :doc:`cordapp-custom-serializers` page. + #. The class is annotated with ``@CordaSerializable``. #. The declared types of constructor arguments, getters, and setters must be supported, and where generics are used the generic parameter must be a supported type, an open wildcard (``*``), or a bounded wildcard which is currently @@ -413,7 +415,7 @@ Kotlin Objects The Carpenter ````````````` -We will support a class carpenter that can dynamically manufacture classes from the supplied schema when deserializing +We support a class carpenter that can dynamically manufacture classes from the supplied schema when deserializing in the JVM without the supporting classes on the classpath. This can be useful where other components might expect to be able to use reflection over the deserialized data, and also for ensuring classes not on the classpath can be deserialized without loading potentially malicious code dynamically without security review outside of a fully sandboxed From e699dad076fc1d81b8bd9529747e2922728ce2e0 Mon Sep 17 00:00:00 2001 From: josecoll Date: Thu, 18 Jan 2018 10:10:06 +0000 Subject: [PATCH 10/20] CORDA-917 Bootstrap nodes without Notary schemas (by default) (#2376) * Default jolokia version with optional override in CorDapp project gradle file. * Bootstrap a node without Notary schemas by default. * Revert unrelated Jolokia code change. * Revert unrelated Jolokia code change. * Addressed PR review feedback. --- .../kotlin/net/corda/plugins/Cordformation.kt | 3 +- .../src/main/kotlin/net/corda/plugins/Node.kt | 3 +- .../net/corda/node/internal/AbstractNode.kt | 2 +- .../node/services/schema/NodeSchemaService.kt | 32 ++++++----- .../services/schema/NodeSchemaServiceTest.kt | 57 ++++++++++++++++++- .../DistributedImmutableMapTests.kt | 3 +- .../PersistentUniquenessProviderTests.kt | 3 +- 7 files changed, 82 insertions(+), 21 deletions(-) diff --git a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordformation.kt b/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordformation.kt index 43444ca270..26878fa357 100644 --- a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordformation.kt +++ b/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordformation.kt @@ -55,8 +55,7 @@ class Cordformation : Plugin { override fun apply(project: Project) { Utils.createCompileConfiguration("cordapp", project) Utils.createRuntimeConfiguration(CORDFORMATION_TYPE, project) - // TODO: improve how we re-use existing declared external variables from root gradle.build - val jolokiaVersion = try { project.rootProject.ext("jolokia_version") } catch (e: Exception) { "1.3.7" } + val jolokiaVersion = project.rootProject.ext("jolokia_version") project.dependencies.add(CORDFORMATION_TYPE, "org.jolokia:jolokia-jvm:$jolokiaVersion:agent") } } diff --git a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Node.kt b/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Node.kt index 1f0cdc9701..e5397ad5aa 100644 --- a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Node.kt +++ b/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Node.kt @@ -127,8 +127,7 @@ class Node(private val project: Project) : CordformNode() { * Installs the jolokia monitoring agent JAR to the node/drivers directory */ private fun installAgentJar() { - // TODO: improve how we re-use existing declared external variables from root gradle.build - val jolokiaVersion = try { project.rootProject.ext("jolokia_version") } catch (e: Exception) { "1.3.7" } + val jolokiaVersion = project.rootProject.ext("jolokia_version") val agentJar = project.configuration("runtime").files { (it.group == "org.jolokia") && (it.name == "jolokia-jvm") && diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index 94f864a55e..da9523a507 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -205,7 +205,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, check(started == null) { "Node has already been started" } log.info("Node starting up ...") initCertificate() - val schemaService = NodeSchemaService(cordappLoader.cordappSchemas) + val schemaService = NodeSchemaService(cordappLoader.cordappSchemas, configuration.notary != null) val (identity, identityKeyPair) = obtainIdentity(notaryConfig = null) val lh = lazyHub() configure(lh) diff --git a/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt b/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt index df4b85c3c5..4989843c9d 100644 --- a/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt +++ b/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt @@ -32,38 +32,44 @@ import net.corda.node.services.vault.VaultSchemaV1 * TODO: support plugins for schema version upgrading or custom mapping not supported by original [QueryableState]. * TODO: create whitelisted tables when a CorDapp is first installed */ -class NodeSchemaService(extraSchemas: Set = emptySet()) : SchemaService, SingletonSerializeAsToken() { - // Entities for compulsory services - object NodeServices +class NodeSchemaService(extraSchemas: Set = emptySet(), includeNotarySchemas: Boolean = false) : SchemaService, SingletonSerializeAsToken() { + // Core Entities used by a Node + object NodeCore - object NodeServicesV1 : MappedSchema(schemaFamily = NodeServices.javaClass, version = 1, + object NodeCoreV1 : MappedSchema(schemaFamily = NodeCore.javaClass, version = 1, mappedTypes = listOf(DBCheckpointStorage.DBCheckpoint::class.java, DBTransactionStorage.DBTransaction::class.java, DBTransactionMappingStorage.DBTransactionMapping::class.java, PersistentKeyManagementService.PersistentKey::class.java, - PersistentUniquenessProvider.PersistentUniqueness::class.java, - PersistentUniquenessProvider.PersistentNotaryCommit::class.java, NodeSchedulerService.PersistentScheduledState::class.java, NodeAttachmentService.DBAttachment::class.java, P2PMessagingClient.ProcessedMessage::class.java, P2PMessagingClient.RetryMessage::class.java, NodeAttachmentService.DBAttachment::class.java, - RaftUniquenessProvider.RaftState::class.java, - BFTNonValidatingNotaryService.PersistedCommittedState::class.java, PersistentIdentityService.PersistentIdentity::class.java, PersistentIdentityService.PersistentIdentityNames::class.java, ContractUpgradeServiceImpl.DBContractUpgrade::class.java )) + // Entities used by a Notary + object NodeNotary + + object NodeNotaryV1 : MappedSchema(schemaFamily = NodeNotary.javaClass, version = 1, + mappedTypes = listOf(PersistentUniquenessProvider.PersistentUniqueness::class.java, + PersistentUniquenessProvider.PersistentNotaryCommit::class.java, + RaftUniquenessProvider.RaftState::class.java, + BFTNonValidatingNotaryService.PersistedCommittedState::class.java + )) + // Required schemas are those used by internal Corda services - // For example, cash is used by the vault for coin selection (but will be extracted as a standalone CorDapp in future) private val requiredSchemas: Map = mapOf(Pair(CommonSchemaV1, SchemaOptions()), - Pair(VaultSchemaV1, SchemaOptions()), - Pair(NodeInfoSchemaV1, SchemaOptions()), - Pair(NodeServicesV1, SchemaOptions())) + Pair(VaultSchemaV1, SchemaOptions()), + Pair(NodeInfoSchemaV1, SchemaOptions()), + Pair(NodeCoreV1, SchemaOptions())) + private val notarySchemas = if (includeNotarySchemas) mapOf(Pair(NodeNotaryV1, SchemaOptions())) else emptyMap() - override val schemaOptions: Map = requiredSchemas + extraSchemas.associateBy({ it }, { SchemaOptions() }) + override val schemaOptions: Map = requiredSchemas + notarySchemas + extraSchemas.associateBy({ it }, { SchemaOptions() }) // Currently returns all schemas supported by the state, with no filtering or enrichment. override fun selectSchemas(state: ContractState): Iterable { diff --git a/node/src/test/kotlin/net/corda/node/services/schema/NodeSchemaServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/schema/NodeSchemaServiceTest.kt index 5a2a81913e..45bd2e5c7d 100644 --- a/node/src/test/kotlin/net/corda/node/services/schema/NodeSchemaServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/schema/NodeSchemaServiceTest.kt @@ -9,15 +9,19 @@ import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentState import net.corda.core.utilities.getOrThrow import net.corda.node.services.api.ServiceHubInternal +import net.corda.node.services.schema.NodeSchemaService.NodeCoreV1 +import net.corda.node.services.schema.NodeSchemaService.NodeNotaryV1 import net.corda.testing.driver.NodeHandle import net.corda.testing.driver.driver -import net.corda.testing.node.MockNetwork import net.corda.testing.internal.vault.DummyLinearStateSchemaV1 +import net.corda.testing.node.MockNetwork import org.hibernate.annotations.Cascade import org.hibernate.annotations.CascadeType +import org.junit.Ignore import org.junit.Test import javax.persistence.* import kotlin.test.assertEquals +import kotlin.test.assertFalse import kotlin.test.assertTrue class NodeSchemaServiceTest { @@ -30,7 +34,30 @@ class NodeSchemaServiceTest { val mockNode = mockNet.createNode() val schemaService = mockNode.services.schemaService assertTrue(schemaService.schemaOptions.containsKey(DummyLinearStateSchemaV1)) + mockNet.stopNodes() + } + @Test + fun `check node runs with minimal core schema set`() { + val mockNet = MockNetwork(cordappPackages = emptyList()) + val mockNode = mockNet.createNode() + val schemaService = mockNode.services.schemaService + + // check against NodeCore schemas + assertTrue(schemaService.schemaOptions.containsKey(NodeCoreV1)) + assertFalse(schemaService.schemaOptions.containsKey(NodeNotaryV1)) + mockNet.stopNodes() + } + + @Test + fun `check node runs inclusive of notary node schema set`() { + val mockNet = MockNetwork(cordappPackages = emptyList()) + val mockNotaryNode = mockNet.notaryNodes.first() + val schemaService = mockNotaryNode.services.schemaService + + // check against NodeCore + NodeNotary Schemas + assertTrue(schemaService.schemaOptions.containsKey(NodeCoreV1)) + assertTrue(schemaService.schemaOptions.containsKey(NodeNotaryV1)) mockNet.stopNodes() } @@ -59,6 +86,34 @@ class NodeSchemaServiceTest { assertEquals>(expected, tables.toMutableSet().apply { retainAll(expected) }) } + @Ignore + @Test + fun `check node runs with minimal core schema set using driverDSL`() { + // TODO: driver limitation: cannot restrict CorDapps that get automatically created by default, + // can ONLY specify additional ones using `extraCordappPackagesToScan` constructor argument. + driver(startNodesInProcess = true, notarySpecs = emptyList()) { + val node = startNode().getOrThrow() + val result = node.rpc.startFlow(::MappedSchemasFlow) + val mappedSchemas = result.returnValue.getOrThrow() + // check against NodeCore schemas + assertTrue(mappedSchemas.contains(NodeCoreV1.name)) + assertFalse(mappedSchemas.contains(NodeNotaryV1.name)) // still gets loaded due TODO restriction + } + + } + + @Test + fun `check node runs inclusive of notary node schema set using driverDSL`() { + driver(startNodesInProcess = true) { + val notaryNode = defaultNotaryNode.getOrThrow().rpc.startFlow(::MappedSchemasFlow) + val mappedSchemas = notaryNode.returnValue.getOrThrow() + // check against NodeCore + NodeNotary Schemas + assertTrue(mappedSchemas.contains(NodeCoreV1.name)) + assertTrue(mappedSchemas.contains(NodeNotaryV1.name)) + } + + } + @StartableByRPC class MappedSchemasFlow : FlowLogic>() { @Suspendable diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/DistributedImmutableMapTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/DistributedImmutableMapTests.kt index 7443507cb8..386aed020a 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/DistributedImmutableMapTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/DistributedImmutableMapTests.kt @@ -11,6 +11,7 @@ import net.corda.core.internal.concurrent.transpose import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.getOrThrow import net.corda.node.internal.configureDatabase +import net.corda.node.services.schema.NodeSchemaService import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.testing.internal.LogHelper @@ -88,7 +89,7 @@ class DistributedImmutableMapTests { private fun createReplica(myAddress: NetworkHostAndPort, clusterAddress: NetworkHostAndPort? = null): CompletableFuture { val storage = Storage.builder().withStorageLevel(StorageLevel.MEMORY).build() val address = Address(myAddress.host, myAddress.port) - val database = configureDatabase(makeTestDataSourceProperties(), DatabaseConfig(serverNameTablePrefix = "PORT_${myAddress.port}_"), rigorousMock()) + val database = configureDatabase(makeTestDataSourceProperties(), DatabaseConfig(serverNameTablePrefix = "PORT_${myAddress.port}_"), rigorousMock(), NodeSchemaService(includeNotarySchemas = true)) databases.add(database) val stateMachineFactory = { DistributedImmutableMap(database, RaftUniquenessProvider.Companion::createMap) } diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/PersistentUniquenessProviderTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/PersistentUniquenessProviderTests.kt index f4d03c9c39..55f1e089a6 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/PersistentUniquenessProviderTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/PersistentUniquenessProviderTests.kt @@ -4,6 +4,7 @@ import net.corda.core.crypto.SecureHash import net.corda.core.identity.CordaX500Name import net.corda.core.node.services.UniquenessException import net.corda.node.internal.configureDatabase +import net.corda.node.services.schema.NodeSchemaService import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.testing.* @@ -29,7 +30,7 @@ class PersistentUniquenessProviderTests { @Before fun setUp() { LogHelper.setLevel(PersistentUniquenessProvider::class) - database = configureDatabase(makeTestDataSourceProperties(), DatabaseConfig(), rigorousMock()) + database = configureDatabase(makeTestDataSourceProperties(), DatabaseConfig(), rigorousMock(), NodeSchemaService(includeNotarySchemas = true)) } @After From bbcafca959b2d468a6f4f9bb847a1bbb7bfc9fdc Mon Sep 17 00:00:00 2001 From: Andrzej Cichocki Date: Thu, 18 Jan 2018 10:11:34 +0000 Subject: [PATCH 11/20] Retire DatabaseTransactionManager. (#2385) --- .../internal/persistence/CordaPersistence.kt | 63 ++++++++++----- .../persistence/DatabaseTransaction.kt | 25 +++--- .../persistence/DatabaseTransactionManager.kt | 77 ------------------- .../persistence/HibernateConfiguration.kt | 9 ++- .../persistence/NodeAttachmentService.kt | 4 +- .../node/services/schema/HibernateObserver.kt | 4 +- .../statemachine/FlowStateMachineImpl.kt | 12 +-- .../node/services/vault/NodeVaultService.kt | 9 +-- .../services/schema/HibernateObserverTests.kt | 5 +- .../corda/node/utilities/ObservablesTests.kt | 6 +- 10 files changed, 77 insertions(+), 137 deletions(-) delete mode 100644 node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/DatabaseTransactionManager.kt diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/CordaPersistence.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/CordaPersistence.kt index 28674fd1e7..9a2b2c3b64 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/CordaPersistence.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/CordaPersistence.kt @@ -1,13 +1,16 @@ package net.corda.nodeapi.internal.persistence +import co.paralleluniverse.strands.Strand import net.corda.core.schemas.MappedSchema import net.corda.core.utilities.contextLogger import rx.Observable import rx.Subscriber +import rx.subjects.PublishSubject import rx.subjects.UnicastSubject import java.io.Closeable import java.sql.Connection import java.sql.SQLException +import java.util.* import java.util.concurrent.CopyOnWriteArrayList import javax.persistence.AttributeConverter import javax.sql.DataSource @@ -39,6 +42,9 @@ enum class TransactionIsolationLevel { val jdbcValue: Int = java.sql.Connection::class.java.getField("TRANSACTION_$name").get(null) as Int } +private val _contextDatabase = ThreadLocal() +val contextDatabase get() = _contextDatabase.get() ?: error("Was expecting to find CordaPersistence set on current thread: ${Strand.currentStrand()}") + class CordaPersistence( val dataSource: DataSource, databaseConfig: DatabaseConfig, @@ -49,7 +55,7 @@ class CordaPersistence( private val log = contextLogger() } - val defaultIsolationLevel = databaseConfig.transactionIsolationLevel + private val defaultIsolationLevel = databaseConfig.transactionIsolationLevel val hibernateConfig: HibernateConfiguration by lazy { transaction { HibernateConfiguration(schemas, databaseConfig, attributeConverters) @@ -57,33 +63,48 @@ class CordaPersistence( } val entityManagerFactory get() = hibernateConfig.sessionFactoryForRegisteredSchemas + data class Boundary(val txId: UUID) + + internal val transactionBoundaries = PublishSubject.create().toSerialized() + init { - DatabaseTransactionManager(this) + // Found a unit test that was forgetting to close the database transactions. When you close() on the top level + // database transaction it will reset the threadLocalTx back to null, so if it isn't then there is still a + // database transaction open. The [transaction] helper above handles this in a finally clause for you + // but any manual database transaction management is liable to have this problem. + contextTransactionOrNull?.let { + error("Was not expecting to find existing database transaction on current strand when setting database: ${Strand.currentStrand()}, $it") + } + _contextDatabase.set(this) // Check not in read-only mode. transaction { check(!connection.metaData.isReadOnly) { "Database should not be readonly." } } } - /** - * Creates an instance of [DatabaseTransaction], with the given transaction isolation level. - */ - fun createTransaction(isolationLevel: TransactionIsolationLevel): DatabaseTransaction { - // We need to set the database for the current [Thread] or [Fiber] here as some tests share threads across databases. - DatabaseTransactionManager.dataSource = this - return DatabaseTransactionManager.currentOrNew(isolationLevel) + fun currentOrNew(isolation: TransactionIsolationLevel = defaultIsolationLevel): DatabaseTransaction { + return contextTransactionOrNull ?: newTransaction(isolation) + } + + fun newTransaction(isolation: TransactionIsolationLevel = defaultIsolationLevel): DatabaseTransaction { + return DatabaseTransaction(isolation.jdbcValue, contextTransactionOrNull, this).also { + contextTransactionOrNull = it + } } /** - * Creates an instance of [DatabaseTransaction], with the default transaction isolation level. + * Creates an instance of [DatabaseTransaction], with the given transaction isolation level. */ - fun createTransaction(): DatabaseTransaction = createTransaction(defaultIsolationLevel) + fun createTransaction(isolationLevel: TransactionIsolationLevel = defaultIsolationLevel): DatabaseTransaction { + // We need to set the database for the current [Thread] or [Fiber] here as some tests share threads across databases. + _contextDatabase.set(this) + return currentOrNew(isolationLevel) + } fun createSession(): Connection { // We need to set the database for the current [Thread] or [Fiber] here as some tests share threads across databases. - DatabaseTransactionManager.dataSource = this - val ctx = DatabaseTransactionManager.currentOrNull() - return ctx?.connection ?: throw IllegalStateException("Was expecting to find database transaction: must wrap calling code within a transaction.") + _contextDatabase.set(this) + return contextTransaction.connection } /** @@ -92,7 +113,7 @@ class CordaPersistence( * @param statement to be executed in the scope of this transaction. */ fun transaction(isolationLevel: TransactionIsolationLevel, statement: DatabaseTransaction.() -> T): T { - DatabaseTransactionManager.dataSource = this + _contextDatabase.set(this) return transaction(isolationLevel, 2, statement) } @@ -103,7 +124,7 @@ class CordaPersistence( fun transaction(statement: DatabaseTransaction.() -> T): T = transaction(defaultIsolationLevel, statement) private fun transaction(isolationLevel: TransactionIsolationLevel, recoverableFailureTolerance: Int, statement: DatabaseTransaction.() -> T): T { - val outer = DatabaseTransactionManager.currentOrNull() + val outer = contextTransactionOrNull return if (outer != null) { outer.statement() } else { @@ -119,7 +140,7 @@ class CordaPersistence( log.warn("Cleanup task failed:", t) } while (true) { - val transaction = DatabaseTransactionManager.currentOrNew(isolationLevel) + val transaction = contextDatabase.currentOrNew(isolationLevel) // XXX: Does this code really support statement changing the contextDatabase? try { val answer = transaction.statement() transaction.commit() @@ -153,8 +174,8 @@ class CordaPersistence( * For examples, see the call hierarchy of this function. */ fun rx.Observer.bufferUntilDatabaseCommit(): rx.Observer { - val currentTxId = DatabaseTransactionManager.transactionId - val databaseTxBoundary: Observable = DatabaseTransactionManager.transactionBoundaries.first { it.txId == currentTxId } + val currentTxId = contextTransaction.id + val databaseTxBoundary: Observable = contextDatabase.transactionBoundaries.first { it.txId == currentTxId } val subject = UnicastSubject.create() subject.delaySubscription(databaseTxBoundary).subscribe(this) databaseTxBoundary.doOnCompleted { subject.onCompleted() } @@ -162,12 +183,12 @@ fun rx.Observer.bufferUntilDatabaseCommit(): rx.Observer { } // A subscriber that delegates to multiple others, wrapping a database transaction around the combination. -private class DatabaseTransactionWrappingSubscriber(val db: CordaPersistence?) : Subscriber() { +private class DatabaseTransactionWrappingSubscriber(private val db: CordaPersistence?) : Subscriber() { // Some unsubscribes happen inside onNext() so need something that supports concurrent modification. val delegates = CopyOnWriteArrayList>() fun forEachSubscriberWithDbTx(block: Subscriber.() -> Unit) { - (db ?: DatabaseTransactionManager.dataSource).transaction { + (db ?: contextDatabase).transaction { delegates.filter { !it.isUnsubscribed }.forEach { it.block() } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/DatabaseTransaction.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/DatabaseTransaction.kt index 5c78dc6a40..ecc48300e3 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/DatabaseTransaction.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/DatabaseTransaction.kt @@ -1,37 +1,40 @@ package net.corda.nodeapi.internal.persistence +import co.paralleluniverse.strands.Strand import org.hibernate.Session import org.hibernate.Transaction -import rx.subjects.Subject import java.sql.Connection import java.util.* +fun currentDBSession(): Session = contextTransaction.session +private val _contextTransaction = ThreadLocal() +var contextTransactionOrNull: DatabaseTransaction? + get() = _contextTransaction.get() + set(transaction) = _contextTransaction.set(transaction) +val contextTransaction get() = contextTransactionOrNull ?: error("Was expecting to find transaction set on current strand: ${Strand.currentStrand()}") + class DatabaseTransaction( isolation: Int, - private val threadLocal: ThreadLocal, - private val transactionBoundaries: Subject, - val cordaPersistence: CordaPersistence + private val outerTransaction: DatabaseTransaction?, + val database: CordaPersistence ) { val id: UUID = UUID.randomUUID() val connection: Connection by lazy(LazyThreadSafetyMode.NONE) { - cordaPersistence.dataSource.connection.apply { + database.dataSource.connection.apply { autoCommit = false transactionIsolation = isolation } } private val sessionDelegate = lazy { - val session = cordaPersistence.entityManagerFactory.withOptions().connection(connection).openSession() + val session = database.entityManagerFactory.withOptions().connection(connection).openSession() hibernateTransaction = session.beginTransaction() session } val session: Session by sessionDelegate private lateinit var hibernateTransaction: Transaction - - private val outerTransaction: DatabaseTransaction? = threadLocal.get() - fun commit() { if (sessionDelegate.isInitialized()) { hibernateTransaction.commit() @@ -53,9 +56,9 @@ class DatabaseTransaction( session.close() } connection.close() - threadLocal.set(outerTransaction) + contextTransactionOrNull = outerTransaction if (outerTransaction == null) { - transactionBoundaries.onNext(DatabaseTransactionManager.Boundary(id)) + database.transactionBoundaries.onNext(CordaPersistence.Boundary(id)) } } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/DatabaseTransactionManager.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/DatabaseTransactionManager.kt deleted file mode 100644 index ade1603002..0000000000 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/DatabaseTransactionManager.kt +++ /dev/null @@ -1,77 +0,0 @@ -package net.corda.nodeapi.internal.persistence - -import co.paralleluniverse.strands.Strand -import org.hibernate.Session -import rx.subjects.PublishSubject -import rx.subjects.Subject -import java.util.* -import java.util.concurrent.ConcurrentHashMap - -fun currentDBSession(): Session = DatabaseTransactionManager.current().session - -class DatabaseTransactionManager(initDataSource: CordaPersistence) { - companion object { - private val threadLocalDb = ThreadLocal() - private val threadLocalTx = ThreadLocal() - private val databaseToInstance = ConcurrentHashMap() - - fun setThreadLocalTx(tx: DatabaseTransaction?): DatabaseTransaction? { - val oldTx = threadLocalTx.get() - threadLocalTx.set(tx) - return oldTx - } - - fun restoreThreadLocalTx(context: DatabaseTransaction?) { - if (context != null) { - threadLocalDb.set(context.cordaPersistence) - } - threadLocalTx.set(context) - } - - var dataSource: CordaPersistence - get() = threadLocalDb.get() ?: throw IllegalStateException("Was expecting to find CordaPersistence set on current thread: ${Strand.currentStrand()}") - set(value) = threadLocalDb.set(value) - - val transactionId: UUID - get() = threadLocalTx.get()?.id ?: throw IllegalStateException("Was expecting to find transaction set on current strand: ${Strand.currentStrand()}") - - val manager: DatabaseTransactionManager get() = databaseToInstance[dataSource]!! - - val transactionBoundaries: Subject get() = manager._transactionBoundaries - - fun currentOrNull(): DatabaseTransaction? = manager.currentOrNull() - - fun currentOrNew(isolation: TransactionIsolationLevel = dataSource.defaultIsolationLevel): DatabaseTransaction { - return currentOrNull() ?: manager.newTransaction(isolation.jdbcValue) - } - - fun current(): DatabaseTransaction = currentOrNull() ?: error("No transaction in context.") - - fun newTransaction(isolation: TransactionIsolationLevel = dataSource.defaultIsolationLevel): DatabaseTransaction { - return manager.newTransaction(isolation.jdbcValue) - } - } - - data class Boundary(val txId: UUID) - - private val _transactionBoundaries = PublishSubject.create().toSerialized() - - init { - // Found a unit test that was forgetting to close the database transactions. When you close() on the top level - // database transaction it will reset the threadLocalTx back to null, so if it isn't then there is still a - // database transaction open. The [transaction] helper above handles this in a finally clause for you - // but any manual database transaction management is liable to have this problem. - if (threadLocalTx.get() != null) { - throw IllegalStateException("Was not expecting to find existing database transaction on current strand when setting database: ${Strand.currentStrand()}, ${threadLocalTx.get()}") - } - dataSource = initDataSource - databaseToInstance[dataSource] = this - } - - private fun newTransaction(isolation: Int) = - DatabaseTransaction(isolation, threadLocalTx, transactionBoundaries, dataSource).apply { - threadLocalTx.set(this) - } - - private fun currentOrNull(): DatabaseTransaction? = threadLocalTx.get() -} diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/HibernateConfiguration.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/HibernateConfiguration.kt index 4daf490c92..29f6783388 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/HibernateConfiguration.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/HibernateConfiguration.kt @@ -119,15 +119,16 @@ class HibernateConfiguration( class NodeDatabaseConnectionProvider : ConnectionProvider { override fun closeConnection(conn: Connection) { conn.autoCommit = false - val tx = DatabaseTransactionManager.current() - tx.commit() - tx.close() + contextTransaction.run { + commit() + close() + } } override fun supportsAggressiveRelease(): Boolean = true override fun getConnection(): Connection { - return DatabaseTransactionManager.newTransaction().connection + return contextDatabase.newTransaction().connection } override fun unwrap(unwrapType: Class): T { diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt b/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt index da6dd489f8..60c5eb373a 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt @@ -17,7 +17,6 @@ import net.corda.core.node.services.vault.AttachmentSort import net.corda.core.serialization.* import net.corda.core.utilities.contextLogger import net.corda.node.services.vault.HibernateAttachmentQueryCriteriaParser -import net.corda.nodeapi.internal.persistence.DatabaseTransactionManager import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX import net.corda.nodeapi.internal.persistence.currentDBSession import java.io.* @@ -242,8 +241,7 @@ class NodeAttachmentService(metrics: MetricRegistry) : AttachmentStorage, Single override fun queryAttachments(criteria: AttachmentQueryCriteria, sorting: AttachmentSort?): List { log.info("Attachment query criteria: $criteria, sorting: $sorting") - - val session = DatabaseTransactionManager.current().session + val session = currentDBSession() val criteriaBuilder = session.criteriaBuilder val criteriaQuery = criteriaBuilder.createQuery(DBAttachment::class.java) diff --git a/node/src/main/kotlin/net/corda/node/services/schema/HibernateObserver.kt b/node/src/main/kotlin/net/corda/node/services/schema/HibernateObserver.kt index eb58d0871e..62d5cee4d8 100644 --- a/node/src/main/kotlin/net/corda/node/services/schema/HibernateObserver.kt +++ b/node/src/main/kotlin/net/corda/node/services/schema/HibernateObserver.kt @@ -10,8 +10,8 @@ import net.corda.core.schemas.PersistentStateRef import net.corda.core.utilities.contextLogger import net.corda.core.utilities.debug import net.corda.node.services.api.SchemaService -import net.corda.nodeapi.internal.persistence.DatabaseTransactionManager import net.corda.nodeapi.internal.persistence.HibernateConfiguration +import net.corda.nodeapi.internal.persistence.contextTransaction import org.hibernate.FlushMode import rx.Observable @@ -54,7 +54,7 @@ class HibernateObserver private constructor(private val config: HibernateConfigu internal fun persistStatesWithSchema(statesAndRefs: List, schema: MappedSchema) { val sessionFactory = config.sessionFactoryForSchemas(setOf(schema)) val session = sessionFactory.withOptions(). - connection(DatabaseTransactionManager.current().connection). + connection(contextTransaction.connection). flushMode(FlushMode.MANUAL). openSession() session.use { thisSession -> diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt index 2fb26c4f23..fb3b06c737 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt @@ -27,7 +27,8 @@ import net.corda.node.services.logging.pushToLoggingContext import net.corda.node.services.statemachine.FlowSessionState.Initiating import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseTransaction -import net.corda.nodeapi.internal.persistence.DatabaseTransactionManager +import net.corda.nodeapi.internal.persistence.contextTransaction +import net.corda.nodeapi.internal.persistence.contextTransactionOrNull import org.slf4j.Logger import org.slf4j.LoggerFactory import java.nio.file.Paths @@ -122,7 +123,7 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, private fun createTransaction() { // Make sure we have a database transaction database.createTransaction() - logger.trace { "Starting database transaction ${DatabaseTransactionManager.currentOrNull()} on ${Strand.currentStrand()}" } + logger.trace { "Starting database transaction $contextTransactionOrNull on ${Strand.currentStrand()}" } } private fun processException(exception: Throwable, propagated: Boolean) { @@ -132,7 +133,7 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, } internal fun commitTransaction() { - val transaction = DatabaseTransactionManager.current() + val transaction = contextTransaction try { logger.trace { "Committing database transaction $transaction on ${Strand.currentStrand()}." } transaction.commit() @@ -468,7 +469,8 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, private fun suspend(ioRequest: FlowIORequest) { // We have to pass the thread local database transaction across via a transient field as the fiber park // swaps them out. - txTrampoline = DatabaseTransactionManager.setThreadLocalTx(null) + txTrampoline = contextTransactionOrNull + contextTransactionOrNull = null if (ioRequest is WaitingRequest) waitingForResponse = ioRequest @@ -477,7 +479,7 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, logger.trace { "Suspended on $ioRequest" } // restore the Tx onto the ThreadLocal so that we can commit the ensuing checkpoint to the DB try { - DatabaseTransactionManager.setThreadLocalTx(txTrampoline) + contextTransactionOrNull = txTrampoline txTrampoline = null actionOnSuspend(ioRequest) } catch (t: Throwable) { diff --git a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt index 478e139e06..c9df780765 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt @@ -17,12 +17,8 @@ import net.corda.core.transactions.NotaryChangeWireTransaction import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.* import net.corda.node.services.api.VaultServiceInternal -import net.corda.nodeapi.internal.persistence.HibernateConfiguration import net.corda.node.services.statemachine.FlowStateMachineImpl -import net.corda.nodeapi.internal.persistence.DatabaseTransactionManager -import net.corda.nodeapi.internal.persistence.bufferUntilDatabaseCommit -import net.corda.nodeapi.internal.persistence.currentDBSession -import net.corda.nodeapi.internal.persistence.wrapWithDatabaseTransaction +import net.corda.nodeapi.internal.persistence.* import org.hibernate.Session import rx.Observable import rx.subjects.PublishSubject @@ -479,8 +475,7 @@ class NodeVaultService( } } - private fun getSession() = DatabaseTransactionManager.currentOrNew().session - + private fun getSession() = contextDatabase.currentOrNew().session /** * Derive list from existing vault states and then incrementally update using vault observables */ diff --git a/node/src/test/kotlin/net/corda/node/services/schema/HibernateObserverTests.kt b/node/src/test/kotlin/net/corda/node/services/schema/HibernateObserverTests.kt index f40fa4a6ba..5c3349f236 100644 --- a/node/src/test/kotlin/net/corda/node/services/schema/HibernateObserverTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/schema/HibernateObserverTests.kt @@ -14,7 +14,6 @@ import net.corda.core.schemas.QueryableState import net.corda.node.services.api.SchemaService import net.corda.node.internal.configureDatabase import net.corda.nodeapi.internal.persistence.DatabaseConfig -import net.corda.nodeapi.internal.persistence.DatabaseTransactionManager import net.corda.testing.internal.LogHelper import net.corda.testing.TestIdentity import net.corda.testing.contracts.DummyContract @@ -71,11 +70,11 @@ class HibernateObserverTests { database.transaction { val MEGA_CORP = TestIdentity(CordaX500Name("MegaCorp", "London", "GB")).party rawUpdatesPublisher.onNext(Vault.Update(emptySet(), setOf(StateAndRef(TransactionState(TestState(), DummyContract.PROGRAM_ID, MEGA_CORP), StateRef(SecureHash.sha256("dummy"), 0))))) - val parentRowCountResult = DatabaseTransactionManager.current().connection.prepareStatement("select count(*) from Parents").executeQuery() + val parentRowCountResult = connection.prepareStatement("select count(*) from Parents").executeQuery() parentRowCountResult.next() val parentRows = parentRowCountResult.getInt(1) parentRowCountResult.close() - val childrenRowCountResult = DatabaseTransactionManager.current().connection.prepareStatement("select count(*) from Children").executeQuery() + val childrenRowCountResult = connection.prepareStatement("select count(*) from Children").executeQuery() childrenRowCountResult.next() val childrenRows = childrenRowCountResult.getInt(1) childrenRowCountResult.close() diff --git a/node/src/test/kotlin/net/corda/node/utilities/ObservablesTests.kt b/node/src/test/kotlin/net/corda/node/utilities/ObservablesTests.kt index 91210f052a..ab4227230b 100644 --- a/node/src/test/kotlin/net/corda/node/utilities/ObservablesTests.kt +++ b/node/src/test/kotlin/net/corda/node/utilities/ObservablesTests.kt @@ -16,9 +16,7 @@ import java.io.Closeable import java.util.* class ObservablesTests { - - private fun isInDatabaseTransaction(): Boolean = (DatabaseTransactionManager.currentOrNull() != null) - + private fun isInDatabaseTransaction() = contextTransactionOrNull != null private val toBeClosed = mutableListOf() private fun createDatabase(): CordaPersistence { @@ -168,7 +166,7 @@ class ObservablesTests { observableWithDbTx.first().subscribe { undelayedEvent.set(it to isInDatabaseTransaction()) } fun observeSecondEvent(event: Int, future: SettableFuture>) { - future.set(event to if (isInDatabaseTransaction()) DatabaseTransactionManager.transactionId else null) + future.set(event to if (isInDatabaseTransaction()) contextTransaction.id else null) } observableWithDbTx.skip(1).first().subscribe { observeSecondEvent(it, delayedEventFromSecondObserver) } From ccae3acf7ef3d02841b6042e2c80dc26d583e3e3 Mon Sep 17 00:00:00 2001 From: Clinton Alexander Date: Thu, 18 Jan 2018 12:31:56 +0000 Subject: [PATCH 12/20] Added -parameters as an additional command line options to revert it being removed during a merge. --- .idea/compiler.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 7234acdbc7..b5126dbc00 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -179,4 +179,7 @@ + + \ No newline at end of file From 133cf1367403c8e7fe9c3f48b2ad737ec86202a2 Mon Sep 17 00:00:00 2001 From: Katarzyna Streich Date: Thu, 18 Jan 2018 16:24:06 +0000 Subject: [PATCH 13/20] Remove primary key constraint on host and port (#336) * Remove primary key constraint on DBHostAndPort * Update migration file --- .../core/internal/schemas/NodeInfoSchema.kt | 18 ++++++++---------- .../network/PersistentNetworkMapCacheTest.kt | 15 +++++++++++++++ .../network/PersistentNetworkMapCache.kt | 6 +++--- .../migration/node-info.changelog-init.xml | 13 +++++++++++++ .../services/network/NetworkMapCacheTest.kt | 12 ++++++++++++ 5 files changed, 51 insertions(+), 13 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/internal/schemas/NodeInfoSchema.kt b/core/src/main/kotlin/net/corda/core/internal/schemas/NodeInfoSchema.kt index 1d08af2759..e77bdd95bf 100644 --- a/core/src/main/kotlin/net/corda/core/internal/schemas/NodeInfoSchema.kt +++ b/core/src/main/kotlin/net/corda/core/internal/schemas/NodeInfoSchema.kt @@ -66,26 +66,24 @@ object NodeInfoSchemaV1 : MappedSchema( } } - @Embeddable - data class PKHostAndPort( - val host: String? = null, - val port: Int? = null - ) : Serializable - @Entity @Table(name = "node_info_hosts") data class DBHostAndPort( - @EmbeddedId - private val pk: PKHostAndPort + @Id + @GeneratedValue + @Column(name = "hosts_id") + var id: Int, + val host: String? = null, + val port: Int? = null ) { companion object { fun fromHostAndPort(hostAndPort: NetworkHostAndPort) = DBHostAndPort( - PKHostAndPort(hostAndPort.host, hostAndPort.port) + 0, hostAndPort.host, hostAndPort.port ) } fun toHostAndPort(): NetworkHostAndPort { - return NetworkHostAndPort(this.pk.host!!, this.pk.port!!) + return NetworkHostAndPort(host!!, port!!) } } diff --git a/node/src/integration-test/kotlin/net/corda/node/services/network/PersistentNetworkMapCacheTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/network/PersistentNetworkMapCacheTest.kt index 7b82b44b22..77be9ab1a5 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/network/PersistentNetworkMapCacheTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/network/PersistentNetworkMapCacheTest.kt @@ -1,5 +1,6 @@ package net.corda.node.services.network +import net.corda.core.crypto.generateKeyPair import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.node.NodeInfo @@ -15,6 +16,7 @@ import org.junit.ClassRule import org.junit.Test import kotlin.test.assertEquals +// TODO Clean up these tests, they were written with old network map design in place. class PersistentNetworkMapCacheTest : NodeBasedTest() { companion object { val ALICE = TestIdentity(ALICE_NAME, 70).party @@ -60,6 +62,19 @@ class PersistentNetworkMapCacheTest : NodeBasedTest() { } } + // This test has to be done as normal node not mock, because MockNodes don't have addresses. + @Test + fun `insert two node infos with the same host and port`() { + val aliceNode = startNode(ALICE_NAME) + val charliePartyCert = getTestPartyAndCertificate(CHARLIE_NAME, generateKeyPair().public) + val aliceCache = aliceNode.services.networkMapCache + aliceCache.addNode(aliceNode.info.copy(legalIdentitiesAndCerts = listOf(charliePartyCert))) + val res = aliceNode.database.transaction { + aliceCache.allNodes.filter { aliceNode.info.addresses[0] in it.addresses } + } + assertEquals(2, res.size) + } + @Test fun `restart node with DB map cache`() { val alice = startNodesWithPort(listOf(ALICE))[0] diff --git a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt index e6a1a9f972..b086e1e4c6 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt @@ -279,13 +279,13 @@ open class PersistentNetworkMapCache( private fun queryByAddress(session: Session, hostAndPort: NetworkHostAndPort): NodeInfo? { val query = session.createQuery( - "SELECT n FROM ${NodeInfoSchemaV1.PersistentNodeInfo::class.java.name} n JOIN n.addresses a WHERE a.pk.host = :host AND a.pk.port = :port", + "SELECT n FROM ${NodeInfoSchemaV1.PersistentNodeInfo::class.java.name} n JOIN n.addresses a WHERE a.host = :host AND a.port = :port", NodeInfoSchemaV1.PersistentNodeInfo::class.java) query.setParameter("host", hostAndPort.host) query.setParameter("port", hostAndPort.port) + query.setMaxResults(1) val result = query.resultList - return if (result.isEmpty()) null - else result.map { it.toNodeInfo() }.singleOrNull() ?: throw IllegalStateException("More than one node with the same host and port") + return result.map { it.toNodeInfo() }.singleOrNull() } /** Object Relational Mapping support. */ diff --git a/node/src/main/resources/migration/node-info.changelog-init.xml b/node/src/main/resources/migration/node-info.changelog-init.xml index 0396ad675c..a4bea223d2 100644 --- a/node/src/main/resources/migration/node-info.changelog-init.xml +++ b/node/src/main/resources/migration/node-info.changelog-init.xml @@ -67,4 +67,17 @@ constraintName="FK__info_hosts__infos" referencedColumnNames="node_info_id" referencedTableName="node_infos"/> + + + + + + + + + + + + + diff --git a/node/src/test/kotlin/net/corda/node/services/network/NetworkMapCacheTest.kt b/node/src/test/kotlin/net/corda/node/services/network/NetworkMapCacheTest.kt index eb95a985dd..b56728fcf6 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/NetworkMapCacheTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/NetworkMapCacheTest.kt @@ -1,9 +1,11 @@ package net.corda.node.services.network +import net.corda.core.crypto.generateKeyPair import net.corda.core.node.services.NetworkMapCache import net.corda.node.services.api.NetworkMapCacheInternal import net.corda.testing.ALICE_NAME import net.corda.testing.BOB_NAME +import net.corda.testing.getTestPartyAndCertificate import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNodeParameters import net.corda.testing.singleIdentity @@ -106,4 +108,14 @@ class NetworkMapCacheTest { assertThat(bobCache.getNodeByLegalName(alice.name) == null) } } + + @Test + fun `add two nodes the same name different keys`() { + val aliceNode = mockNet.createPartyNode(ALICE_NAME) + val aliceCache = aliceNode.services.networkMapCache + val alicePartyAndCert2 = getTestPartyAndCertificate(ALICE_NAME, generateKeyPair().public) + aliceCache.addNode(aliceNode.info.copy(legalIdentitiesAndCerts = listOf(alicePartyAndCert2))) + // This is correct behaviour as we may have distributed service nodes. + assertEquals(2, aliceCache.getNodesByLegalName(ALICE_NAME).size) + } } From 2f8836c8ad758af46ab1025f1de3bd3cfbb21053 Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Thu, 18 Jan 2018 23:08:02 +0000 Subject: [PATCH 14/20] Some long overdue cleaning up of some of the doorman code. (#362) --- .../doorman/NodeRegistrationTest.kt | 5 +- .../com/r3/corda/networkmanage/hsm/HsmTest.kt | 20 +-- .../hsm/SigningServiceIntegrationTest.kt | 7 +- .../common/persistence/NetworkMapStorage.kt | 9 +- .../PersistentNetworkMapStorage.kt | 34 ++-- .../persistence/entity/NetworkMapEntity.kt | 19 +- .../entity/NetworkParametersEntity.kt | 18 +- .../common/signer/NetworkMapSigner.kt | 24 ++- .../corda/networkmanage/common/utils/Utils.kt | 10 +- .../r3/corda/networkmanage/doorman/Main.kt | 165 +++-------------- .../doorman/NetworkManagementServer.kt | 170 +++++++++++++----- .../doorman/NetworkManagementWebServer.kt | 59 ++++++ .../doorman/signer/CsrHandler.kt | 114 ++++-------- .../doorman/signer/JiraCsrHandler.kt | 69 +++++++ .../doorman/signer/LocalSigner.kt | 42 +---- ...oWebService.kt => NetworkMapWebService.kt} | 11 +- .../com/r3/corda/networkmanage/hsm/Main.kt | 41 ++++- .../hsm/signer/HsmNetworkMapSigner.kt | 63 ++----- .../PersistentNetworkMapStorageTest.kt | 10 +- .../common/signer/NetworkMapSignerTest.kt | 16 +- .../doorman/DefaultRequestProcessorTest.kt | 62 ------- .../doorman/signer/DefaultCsrHandlerTest.kt | 85 +++++++++ ...srHandlerTest.kt => JiraCsrHandlerTest.kt} | 11 +- .../NetworkMapWebServiceTest.kt} | 25 +-- .../RegistrationWebServiceTest.kt | 4 +- 25 files changed, 577 insertions(+), 516 deletions(-) create mode 100644 network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/NetworkManagementWebServer.kt create mode 100644 network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/JiraCsrHandler.kt rename network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/webservice/{NodeInfoWebService.kt => NetworkMapWebService.kt} (91%) delete mode 100644 network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/DefaultRequestProcessorTest.kt create mode 100644 network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/signer/DefaultCsrHandlerTest.kt rename network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/signer/{CsrHandlerTest.kt => JiraCsrHandlerTest.kt} (90%) rename network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/{NodeInfoWebServiceTest.kt => webservice/NetworkMapWebServiceTest.kt} (86%) rename network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/{ => webservice}/RegistrationWebServiceTest.kt (98%) diff --git a/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/doorman/NodeRegistrationTest.kt b/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/doorman/NodeRegistrationTest.kt index 4ece2a3a21..6fa65c1787 100644 --- a/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/doorman/NodeRegistrationTest.kt +++ b/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/doorman/NodeRegistrationTest.kt @@ -1,6 +1,7 @@ package com.r3.corda.networkmanage.doorman import com.r3.corda.networkmanage.common.persistence.configureDatabase +import com.r3.corda.networkmanage.common.utils.CertPathAndKey import com.r3.corda.networkmanage.doorman.signer.LocalSigner import net.corda.cordform.CordformNode import net.corda.core.crypto.random63BitValue @@ -141,11 +142,11 @@ class NodeRegistrationTest : IntegrationTest() { start( serverAddress, configureDatabase(makeTestDataSourceProperties(), DatabaseConfig(runMigration = true)), - LocalSigner(csrCa.keyPair, arrayOf(csrCa.certificate, rootCaCert)), + CertPathAndKey(listOf(csrCa.certificate, rootCaCert), csrCa.keyPair.private), DoormanConfig(approveAll = true, jiraConfig = null, approveInterval = timeoutMillis), networkParameters?.let { NetworkMapStartParams( - LocalSigner(networkMapCa.keyPair, arrayOf(networkMapCa.certificate, rootCaCert)), + LocalSigner(networkMapCa), networkParameters, NetworkMapConfig(cacheTimeout = timeoutMillis, signInterval = timeoutMillis) ) diff --git a/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/hsm/HsmTest.kt b/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/hsm/HsmTest.kt index 33c9cbef3a..c78f1ca852 100644 --- a/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/hsm/HsmTest.kt +++ b/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/hsm/HsmTest.kt @@ -12,6 +12,7 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.rules.TemporaryFolder +import java.util.concurrent.atomic.AtomicBoolean import kotlin.test.assertTrue class HsmTest { @@ -19,7 +20,12 @@ class HsmTest { @Rule @JvmField val hsmSimulator: HsmSimulator = HsmSimulator() - val testParameters = Parameters( + + @Rule + @JvmField + val tempFolder = TemporaryFolder() + + private val testParameters = Parameters( dataSourceProperties = mock(), device = "${hsmSimulator.port}@${hsmSimulator.host}", keySpecifier = 1, @@ -30,10 +36,6 @@ class HsmTest { validDays = 3650 ) - @Rule - @JvmField - val tempFolder = TemporaryFolder() - private lateinit var inputReader: InputReader @Before @@ -47,14 +49,12 @@ class HsmTest { fun `Authenticator executes the block once user is successfully authenticated`() { // given val authenticator = Authenticator(testParameters.createProvider(), inputReader = inputReader) - var executed = false + val executed = AtomicBoolean(false) // when - authenticator.connectAndAuthenticate({ provider, signers -> - executed = true - }) + authenticator.connectAndAuthenticate { _, _ -> executed.set(true) } // then - assertTrue(executed) + assertTrue(executed.get()) } } \ No newline at end of file diff --git a/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/hsm/SigningServiceIntegrationTest.kt b/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/hsm/SigningServiceIntegrationTest.kt index 765d47d5f0..8c4a3b8215 100644 --- a/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/hsm/SigningServiceIntegrationTest.kt +++ b/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/hsm/SigningServiceIntegrationTest.kt @@ -90,7 +90,12 @@ class SigningServiceIntegrationTest { val database = configureDatabase(makeTestDataSourceProperties(), DatabaseConfig(runMigration = true)) NetworkManagementServer().use { server -> - server.start(NetworkHostAndPort(HOST, 0), database, doormanServiceParameter = DoormanConfig(approveAll = true, approveInterval = 2.seconds.toMillis(), jiraConfig = null), startNetworkMap = null) + server.start( + hostAndPort = NetworkHostAndPort(HOST, 0), + database = database, + csrCertPathAndKey = null, + doormanServiceParameter = DoormanConfig(approveAll = true, approveInterval = 2.seconds.toMillis(), jiraConfig = null), + startNetworkMap = null) val doormanHostAndPort = server.hostAndPort // Start Corda network registration. val config = createConfig().also { diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/NetworkMapStorage.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/NetworkMapStorage.kt index cb86c9af6f..59c83b0abc 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/NetworkMapStorage.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/NetworkMapStorage.kt @@ -39,10 +39,9 @@ interface NetworkMapStorage { fun getSignedNetworkParameters(hash: SecureHash): SignedNetworkParameters? /** - * Retrieve network map parameters. - * @return signed current network map parameters or null if they don't exist + * Retrieve the network parameters of the current network map, or null if there's no network map. */ - fun getCurrentSignedNetworkParameters(): SignedNetworkParameters? + fun getNetworkParametersOfNetworkMap(): SignedNetworkParameters? /** * Persists given network parameters with signature if provided. @@ -55,5 +54,5 @@ interface NetworkMapStorage { * Note that they may not have been signed up yet. * @return latest network parameters */ - fun getLatestUnsignedNetworkParameters(): NetworkParameters -} \ No newline at end of file + fun getLatestNetworkParameters(): NetworkParameters? +} diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNetworkMapStorage.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNetworkMapStorage.kt index 0e328f853a..de7fed484b 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNetworkMapStorage.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNetworkMapStorage.kt @@ -6,10 +6,7 @@ import com.r3.corda.networkmanage.common.utils.SignedNetworkParameters import net.corda.core.crypto.SecureHash import net.corda.core.crypto.sha256 import net.corda.core.internal.DigitalSignatureWithCert -import net.corda.core.serialization.SerializedBytes -import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize -import net.corda.nodeapi.internal.network.NetworkMap import net.corda.nodeapi.internal.network.NetworkParameters import net.corda.nodeapi.internal.persistence.CordaPersistence @@ -19,18 +16,16 @@ import net.corda.nodeapi.internal.persistence.CordaPersistence class PersistentNetworkMapStorage(private val database: CordaPersistence) : NetworkMapStorage { override fun getCurrentNetworkMap(): SignedNetworkMap? { return database.transaction { - getCurrentNetworkMapEntity()?.let { - val signatureAndCertPath = it.signatureAndCertificate() - SignedNetworkMap(SerializedBytes(it.networkMap), signatureAndCertPath) - } + getCurrentNetworkMapEntity()?.toSignedNetworkMap() } } - override fun getCurrentSignedNetworkParameters(): SignedNetworkParameters? { + override fun getNetworkParametersOfNetworkMap(): SignedNetworkParameters? { return database.transaction { getCurrentNetworkMapEntity()?.let { - val netParamsHash = it.networkMap.deserialize().networkParameterHash - getSignedNetworkParameters(netParamsHash) + val netParamsHash = it.toNetworkMap().networkParameterHash + getSignedNetworkParameters(netParamsHash) ?: + throw IllegalStateException("Current network map is pointing to network parameters that do not exist: $netParamsHash") } } } @@ -47,7 +42,9 @@ class PersistentNetworkMapStorage(private val database: CordaPersistence) : Netw } override fun getSignedNetworkParameters(hash: SecureHash): SignedNetworkParameters? { - return getNetworkParametersEntity(hash.toString())?.signedParameters() + return getNetworkParametersEntity(hash.toString())?.let { + if (it.isSigned) it.toSignedNetworkParameters() else null + } } override fun getNodeInfoHashes(certificateStatus: CertificateStatus): List { @@ -79,18 +76,17 @@ class PersistentNetworkMapStorage(private val database: CordaPersistence) : Netw } } - override fun getLatestUnsignedNetworkParameters(): NetworkParameters = getLatestNetworkParametersEntity().networkParameters() - - private fun getLatestNetworkParametersEntity(): NetworkParametersEntity { + override fun getLatestNetworkParameters(): NetworkParameters? { return database.transaction { - val builder = session.criteriaBuilder - val query = builder.createQuery(NetworkParametersEntity::class.java).run { - from(NetworkParametersEntity::class.java).run { - orderBy(builder.desc(get(NetworkParametersEntity::created.name))) + val query = session.criteriaBuilder.run { + createQuery(NetworkParametersEntity::class.java).run { + from(NetworkParametersEntity::class.java).run { + orderBy(desc(get(NetworkParametersEntity::created.name))) + } } } // We just want the last entry - session.createQuery(query).setMaxResults(1).resultList.singleOrNull() ?: throw IllegalArgumentException("No network parameters found in network map storage") + session.createQuery(query).setMaxResults(1).uniqueResult()?.toNetworkParameters() } } diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/entity/NetworkMapEntity.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/entity/NetworkMapEntity.kt index f71a252213..65c14f560a 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/entity/NetworkMapEntity.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/entity/NetworkMapEntity.kt @@ -1,7 +1,11 @@ package com.r3.corda.networkmanage.common.persistence.entity +import com.r3.corda.networkmanage.common.utils.SignedNetworkMap import net.corda.core.internal.DigitalSignatureWithCert +import net.corda.core.serialization.SerializedBytes +import net.corda.core.serialization.deserialize import net.corda.nodeapi.internal.crypto.X509CertificateFactory +import net.corda.nodeapi.internal.network.NetworkMap import javax.persistence.* @Entity @@ -23,11 +27,12 @@ class NetworkMapEntity( @Column(name = "certificate") val certificate: ByteArray ) { - /** - * Deserializes NetworkMapEntity.signatureBytes into the [DigitalSignatureWithCert] instance - */ - fun signatureAndCertificate(): DigitalSignatureWithCert { - return DigitalSignatureWithCert(X509CertificateFactory().generateCertificate(certificate.inputStream()), signature) - } + fun toNetworkMap(): NetworkMap = networkMap.deserialize() -} \ No newline at end of file + fun toSignedNetworkMap(): SignedNetworkMap { + return SignedNetworkMap( + SerializedBytes(networkMap), + DigitalSignatureWithCert(X509CertificateFactory().generateCertificate(certificate.inputStream()), signature) + ) + } +} diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/entity/NetworkParametersEntity.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/entity/NetworkParametersEntity.kt index ca445a1f02..8cd860486c 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/entity/NetworkParametersEntity.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/entity/NetworkParametersEntity.kt @@ -35,13 +35,15 @@ class NetworkParametersEntity( @Column(name = "certificate") val certificate: ByteArray? ) { - fun networkParameters(): NetworkParameters = parametersBytes.deserialize() + val isSigned: Boolean get() = certificate != null && signature != null - // Return signed network parameters or null if they haven't been signed yet. - fun signedParameters(): SignedNetworkParameters? { - return if (certificate != null && signature != null) { - val sigWithCert = DigitalSignatureWithCert(X509CertificateFactory().generateCertificate(certificate.inputStream()), signature) - SignedDataWithCert(SerializedBytes(parametersBytes), sigWithCert) - } else null + fun toNetworkParameters(): NetworkParameters = parametersBytes.deserialize() + + fun toSignedNetworkParameters(): SignedNetworkParameters { + if (certificate == null || signature == null) throw IllegalStateException("Network parameters entity is not signed: $parametersHash") + return SignedDataWithCert( + SerializedBytes(parametersBytes), + DigitalSignatureWithCert(X509CertificateFactory().generateCertificate(certificate.inputStream()), signature) + ) } -} \ No newline at end of file +} diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/signer/NetworkMapSigner.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/signer/NetworkMapSigner.kt index 5d3d3b15e8..e75baa4c5b 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/signer/NetworkMapSigner.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/signer/NetworkMapSigner.kt @@ -4,20 +4,30 @@ import com.r3.corda.networkmanage.common.persistence.CertificateStatus import com.r3.corda.networkmanage.common.persistence.NetworkMapStorage import net.corda.core.internal.SignedDataWithCert import net.corda.core.serialization.serialize +import net.corda.core.utilities.contextLogger import net.corda.nodeapi.internal.network.NetworkMap import net.corda.nodeapi.internal.network.NetworkParameters class NetworkMapSigner(private val networkMapStorage: NetworkMapStorage, private val signer: Signer) { + private companion object { + val logger = contextLogger() + } + /** * Signs the network map and latest network parameters if they haven't been signed yet. */ fun signNetworkMap() { // TODO There is no network parameters update process in place yet. We assume that latest parameters are to be used // in current network map. - val latestNetworkParameters = networkMapStorage.getLatestUnsignedNetworkParameters() - val currentNetworkParameters = networkMapStorage.getCurrentSignedNetworkParameters() - if (currentNetworkParameters?.verified() != latestNetworkParameters) - signNetworkParameters(latestNetworkParameters) + val latestNetworkParameters = networkMapStorage.getLatestNetworkParameters() + if (latestNetworkParameters == null) { + logger.debug("No network parameters present") + return + } + val currentNetworkParameters = networkMapStorage.getNetworkParametersOfNetworkMap() + if (currentNetworkParameters?.verified() != latestNetworkParameters) { + persistSignedNetworkParameters(latestNetworkParameters) + } val currentSignedNetworkMap = networkMapStorage.getCurrentNetworkMap() val nodeInfoHashes = networkMapStorage.getNodeInfoHashes(CertificateStatus.VALID) val serialisedNetworkMap = NetworkMap(nodeInfoHashes, latestNetworkParameters.serialize().hash).serialize() @@ -27,10 +37,8 @@ class NetworkMapSigner(private val networkMapStorage: NetworkMapStorage, private } } - /** - * Signs latest inserted network parameters. - */ - fun signNetworkParameters(networkParameters: NetworkParameters) { + fun persistSignedNetworkParameters(networkParameters: NetworkParameters) { + logger.info("Signing and persisting network parameters: $networkParameters") val digitalSignature = signer.signObject(networkParameters).sig networkMapStorage.saveNetworkParameters(networkParameters, digitalSignature) } diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/utils/Utils.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/utils/Utils.kt index 8ac987e03a..3554580976 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/utils/Utils.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/utils/Utils.kt @@ -10,14 +10,20 @@ import net.corda.core.internal.SignedDataWithCert import net.corda.nodeapi.internal.crypto.X509CertificateFactory import net.corda.nodeapi.internal.network.NetworkMap import net.corda.nodeapi.internal.network.NetworkParameters +import java.security.KeyPair +import java.security.PrivateKey import java.security.PublicKey import java.security.cert.CertPath -import java.security.cert.Certificate +import java.security.cert.X509Certificate // TODO These should be defined in node-api typealias SignedNetworkParameters = SignedDataWithCert typealias SignedNetworkMap = SignedDataWithCert +data class CertPathAndKey(val certPath: List, val key: PrivateKey) { + fun toKeyPair(): KeyPair = KeyPair(certPath[0].publicKey, key) +} + // TODO: replace this with Crypto.hash when its available. /** * Returns SHA256 hash of this public key @@ -42,7 +48,7 @@ fun Array.toConfigWithOptions(registerOptions: OptionParser.() -> Un class ShowHelpException(val parser: OptionParser, val errorMessage: String? = null) : Exception() -fun buildCertPath(vararg certificates: Certificate): CertPath = X509CertificateFactory().delegate.generateCertPath(certificates.asList()) +fun buildCertPath(vararg certificates: X509Certificate): CertPath = X509CertificateFactory().generateCertPath(certificates.asList()) fun buildCertPath(certPathBytes: ByteArray): CertPath = X509CertificateFactory().delegate.generateCertPath(certPathBytes.inputStream()) diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/Main.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/Main.kt index 74fe5885b0..c7dd3f0271 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/Main.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/Main.kt @@ -1,16 +1,11 @@ package com.r3.corda.networkmanage.doorman -import com.atlassian.jira.rest.client.internal.async.AsynchronousJiraRestClientFactory -import com.r3.corda.networkmanage.common.persistence.* +import com.r3.corda.networkmanage.common.persistence.CertificationRequestStorage import com.r3.corda.networkmanage.common.persistence.CertificationRequestStorage.Companion.DOORMAN_SIGNATURE -import com.r3.corda.networkmanage.common.signer.NetworkMapSigner +import com.r3.corda.networkmanage.common.persistence.configureDatabase +import com.r3.corda.networkmanage.common.utils.CertPathAndKey import com.r3.corda.networkmanage.common.utils.ShowHelpException -import com.r3.corda.networkmanage.doorman.signer.DefaultCsrHandler -import com.r3.corda.networkmanage.doorman.signer.JiraCsrHandler import com.r3.corda.networkmanage.doorman.signer.LocalSigner -import com.r3.corda.networkmanage.doorman.webservice.MonitoringWebService -import com.r3.corda.networkmanage.doorman.webservice.NodeInfoWebService -import com.r3.corda.networkmanage.doorman.webservice.RegistrationWebService import com.r3.corda.networkmanage.hsm.configuration.Parameters.Companion.DEFAULT_CSR_CERTIFICATE_NAME import com.r3.corda.networkmanage.hsm.configuration.Parameters.Companion.DEFAULT_NETWORK_MAP_CERTIFICATE_NAME import net.corda.core.crypto.Crypto @@ -21,141 +16,19 @@ import net.corda.core.internal.div import net.corda.core.serialization.internal.SerializationEnvironmentImpl import net.corda.core.serialization.internal.nodeSerializationEnv import net.corda.core.utilities.NetworkHostAndPort -import net.corda.core.utilities.loggerFor import net.corda.nodeapi.internal.crypto.* import net.corda.nodeapi.internal.network.NetworkParameters -import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.serialization.AMQP_P2P_CONTEXT import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl import net.corda.nodeapi.internal.serialization.amqp.AMQPClientSerializationScheme import org.bouncycastle.pkcs.PKCS10CertificationRequest -import java.io.Closeable -import java.net.URI import java.nio.file.Path import java.security.cert.X509Certificate import java.time.Instant -import java.util.* -import java.util.concurrent.Executors -import java.util.concurrent.TimeUnit import javax.security.auth.x500.X500Principal import kotlin.concurrent.thread import kotlin.system.exitProcess -class NetworkManagementServer : Closeable { - private val doOnClose = mutableListOf<() -> Unit>() - lateinit var hostAndPort: NetworkHostAndPort - - override fun close() = doOnClose.forEach { it() } - - companion object { - private val logger = loggerFor() - } - - private fun getNetworkMapService(config: NetworkMapConfig, database: CordaPersistence, signer: LocalSigner?, updateNetworkParameters: NetworkParameters?): NodeInfoWebService { - val networkMapStorage = PersistentNetworkMapStorage(database) - val nodeInfoStorage = PersistentNodeInfoStorage(database) - val localNetworkMapSigner = if (signer != null) NetworkMapSigner(networkMapStorage, signer) else null - - updateNetworkParameters?.let { - // Persisting new network parameters - val currentNetworkParameters = networkMapStorage.getCurrentSignedNetworkParameters() - if (currentNetworkParameters == null) { - localNetworkMapSigner?.signNetworkParameters(it) ?: networkMapStorage.saveNetworkParameters(it, null) - } else { - throw UnsupportedOperationException("Network parameters already exist. Updating them via the file config is not supported yet.") - } - } - - // This call will fail if parameter is null in DB. - try { - val latestParameter = networkMapStorage.getLatestUnsignedNetworkParameters() - logger.info("Starting network map service with network parameters : $latestParameter") - } catch (e: NoSuchElementException) { - logger.error("No network parameter found, please upload new network parameter before starting network map service. The server will now exit.") - exitProcess(-1) - } - - // Thread sign network map in case of change (i.e. a new node info has been added or a node info has been removed). - if (localNetworkMapSigner != null) { - val scheduledExecutor = Executors.newScheduledThreadPool(1) - val signingThread = Runnable { - try { - localNetworkMapSigner.signNetworkMap() - } catch (e: Exception) { - // Log the error and carry on. - logger.error("Error encountered when processing node info changes.", e) - } - } - scheduledExecutor.scheduleAtFixedRate(signingThread, config.signInterval, config.signInterval, TimeUnit.MILLISECONDS) - doOnClose += { scheduledExecutor.shutdown() } - } - - return NodeInfoWebService(nodeInfoStorage, networkMapStorage, config) - } - - - private fun getDoormanService(config: DoormanConfig, database: CordaPersistence, signer: LocalSigner?, serverStatus: NetworkManagementServerStatus): RegistrationWebService { - logger.info("Starting Doorman server.") - val requestService = if (config.approveAll) { - logger.warn("Doorman server is in 'Approve All' mode, this will approve all incoming certificate signing requests.") - ApproveAllCertificateRequestStorage(PersistentCertificateRequestStorage(database)) - } else { - PersistentCertificateRequestStorage(database) - } - - val jiraConfig = config.jiraConfig - val requestProcessor = if (jiraConfig != null) { - val jiraWebAPI = AsynchronousJiraRestClientFactory().createWithBasicHttpAuthentication(URI(jiraConfig.address), jiraConfig.username, jiraConfig.password) - val jiraClient = JiraClient(jiraWebAPI, jiraConfig.projectCode, jiraConfig.doneTransitionCode) - JiraCsrHandler(jiraClient, requestService, DefaultCsrHandler(requestService, signer)) - } else { - DefaultCsrHandler(requestService, signer) - } - - val scheduledExecutor = Executors.newScheduledThreadPool(1) - val approvalThread = Runnable { - try { - serverStatus.lastRequestCheckTime = Instant.now() - // Create tickets for requests which don't have one yet. - requestProcessor.createTickets() - // Process Jira approved tickets. - requestProcessor.processApprovedRequests() - } catch (e: Exception) { - // Log the error and carry on. - logger.error("Error encountered when approving request.", e) - } - } - scheduledExecutor.scheduleAtFixedRate(approvalThread, config.approveInterval, config.approveInterval, TimeUnit.MILLISECONDS) - doOnClose += { scheduledExecutor.shutdown() } - - return RegistrationWebService(requestProcessor) - } - - fun start(hostAndPort: NetworkHostAndPort, - database: CordaPersistence, - doormanSigner: LocalSigner? = null, - doormanServiceParameter: DoormanConfig?, // TODO Doorman config shouldn't be optional as the doorman is always required to run - startNetworkMap: NetworkMapStartParams? - ) { - val services = mutableListOf() - val serverStatus = NetworkManagementServerStatus() - - startNetworkMap?.let { services += getNetworkMapService(it.config, database, it.signer, it.updateNetworkParameters) } - doormanServiceParameter?.let { services += getDoormanService(it, database, doormanSigner, serverStatus) } - - require(services.isNotEmpty()) { "No service created, please provide at least one service config." } - - // TODO: use mbean to expose audit data? - services += MonitoringWebService(serverStatus) - - val webServer = NetworkManagementWebServer(hostAndPort, *services.toTypedArray()) - webServer.start() - - doOnClose += webServer::close - this.hostAndPort = webServer.hostAndPort - } -} - data class NetworkMapStartParams(val signer: LocalSigner?, val updateNetworkParameters: NetworkParameters?, val config: NetworkMapConfig) data class NetworkManagementServerStatus(var serverStartTime: Instant = Instant.now(), var lastRequestCheckTime: Instant? = null) @@ -181,9 +54,8 @@ fun generateRootKeyPair(rootStoreFile: Path, rootKeystorePass: String?, rootPriv val rootPrivateKeyPassword = rootPrivateKeyPass ?: readPassword("Root Private Key Password: ") if (rootStore.containsAlias(X509Utilities.CORDA_ROOT_CA)) { - val oldKey = loadOrCreateKeyStore(rootStoreFile, rootKeystorePassword).getCertificate(X509Utilities.CORDA_ROOT_CA).publicKey - println("Key ${X509Utilities.CORDA_ROOT_CA} already exists in keystore, process will now terminate.") - println(oldKey) + println("${X509Utilities.CORDA_ROOT_CA} already exists in keystore, process will now terminate.") + println(rootStore.getCertificate(X509Utilities.CORDA_ROOT_CA)) exitProcess(1) } @@ -203,7 +75,7 @@ fun generateRootKeyPair(rootStoreFile: Path, rootKeystorePass: String?, rootPriv println("Trust store for distribution to nodes created in $nodeTrustStore") println("Root CA keypair and certificate stored in ${rootStoreFile.toAbsolutePath()}.") - println(loadKeyStore(rootStoreFile, rootKeystorePassword).getCertificate(X509Utilities.CORDA_ROOT_CA).publicKey) + println(selfSignCert) } fun generateSigningKeyPairs(keystoreFile: Path, rootStoreFile: Path, rootKeystorePass: String?, rootPrivateKeyPass: String?, keystorePass: String?, caPrivateKeyPass: String?) { @@ -262,7 +134,7 @@ fun generateSigningKeyPairs(keystoreFile: Path, rootStoreFile: Path, rootKeystor } -private fun buildLocalSigners(parameters: NetworkManagementServerParameters): Pair? { +private fun processKeyStore(parameters: NetworkManagementServerParameters): Pair? { if (parameters.keystorePath == null) return null // Get password from console if not in config. @@ -270,19 +142,22 @@ private fun buildLocalSigners(parameters: NetworkManagementServerParameters): Pa val privateKeyPassword = parameters.caPrivateKeyPassword ?: readPassword("Private key password: ") val keyStore = loadOrCreateKeyStore(parameters.keystorePath, keyStorePassword) - val (doormanSigner, networkMapSigner) = listOf(DEFAULT_CSR_CERTIFICATE_NAME, DEFAULT_NETWORK_MAP_CERTIFICATE_NAME).map { - val keyPair = keyStore.getKeyPair(it, privateKeyPassword) - val certPath = keyStore.getCertificateChain(it).map { it as X509Certificate } - LocalSigner(keyPair, certPath.toTypedArray()) + val csrCertPathAndKey = keyStore.run { + CertPathAndKey( + keyStore.getCertificateChain(DEFAULT_CSR_CERTIFICATE_NAME).map { it as X509Certificate }, + keyStore.getSupportedKey(DEFAULT_CSR_CERTIFICATE_NAME, privateKeyPassword) + ) } - return Pair(doormanSigner, networkMapSigner) + val networkMapSigner = LocalSigner(keyStore.getCertificateAndKeyPair(DEFAULT_NETWORK_MAP_CERTIFICATE_NAME, privateKeyPassword)) + + return Pair(csrCertPathAndKey, networkMapSigner) } /** * This storage automatically approves all created requests. */ -private class ApproveAllCertificateRequestStorage(private val delegate: CertificationRequestStorage) : CertificationRequestStorage by delegate { +class ApproveAllCertificateRequestStorage(private val delegate: CertificationRequestStorage) : CertificationRequestStorage by delegate { override fun saveRequest(request: PKCS10CertificationRequest): String { val requestId = delegate.saveRequest(request) delegate.markRequestTicketCreated(requestId) @@ -311,9 +186,9 @@ fun main(args: Array) { initialiseSerialization() val database = configureDatabase(dataSourceProperties) // TODO: move signing to signing server. - val localSigners = buildLocalSigners(this) + val csrAndNetworkMap = processKeyStore(this) - if (localSigners != null) { + if (csrAndNetworkMap != null) { println("Starting network management services with local signing") } @@ -325,10 +200,10 @@ fun main(args: Array) { parseNetworkParametersFrom(it) } val networkMapStartParams = networkMapConfig?.let { - NetworkMapStartParams(localSigners?.second, networkParameters, it) + NetworkMapStartParams(csrAndNetworkMap?.second, networkParameters, it) } - networkManagementServer.start(NetworkHostAndPort(host, port), database, localSigners?.first, doormanConfig, networkMapStartParams) + networkManagementServer.start(NetworkHostAndPort(host, port), database, csrAndNetworkMap?.first, doormanConfig, networkMapStartParams) Runtime.getRuntime().addShutdownHook(thread(start = false) { networkManagementServer.close() diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/NetworkManagementServer.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/NetworkManagementServer.kt index e849c3b086..030d92ce36 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/NetworkManagementServer.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/NetworkManagementServer.kt @@ -1,58 +1,142 @@ package com.r3.corda.networkmanage.doorman +import com.atlassian.jira.rest.client.internal.async.AsynchronousJiraRestClientFactory +import com.r3.corda.networkmanage.common.persistence.PersistentCertificateRequestStorage +import com.r3.corda.networkmanage.common.persistence.PersistentNetworkMapStorage +import com.r3.corda.networkmanage.common.persistence.PersistentNodeInfoStorage +import com.r3.corda.networkmanage.common.signer.NetworkMapSigner +import com.r3.corda.networkmanage.common.utils.CertPathAndKey +import com.r3.corda.networkmanage.doorman.signer.DefaultCsrHandler +import com.r3.corda.networkmanage.doorman.signer.JiraCsrHandler +import com.r3.corda.networkmanage.doorman.signer.LocalSigner +import com.r3.corda.networkmanage.doorman.webservice.MonitoringWebService +import com.r3.corda.networkmanage.doorman.webservice.NetworkMapWebService +import com.r3.corda.networkmanage.doorman.webservice.RegistrationWebService import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.loggerFor -import org.eclipse.jetty.server.Server -import org.eclipse.jetty.server.ServerConnector -import org.eclipse.jetty.server.handler.HandlerCollection -import org.eclipse.jetty.servlet.ServletContextHandler -import org.eclipse.jetty.servlet.ServletHolder -import org.glassfish.jersey.server.ResourceConfig -import org.glassfish.jersey.servlet.ServletContainer +import net.corda.nodeapi.internal.network.NetworkParameters +import net.corda.nodeapi.internal.persistence.CordaPersistence import java.io.Closeable -import java.net.InetSocketAddress +import java.net.URI +import java.time.Instant +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit -/** - * NetworkManagementWebServer runs on Jetty server and provides service via http. - */ -class NetworkManagementWebServer(hostAndPort: NetworkHostAndPort, private vararg val webServices: Any) : Closeable { +class NetworkManagementServer : Closeable { companion object { - val logger = loggerFor() + private val logger = loggerFor() } - private val server: Server = Server(InetSocketAddress(hostAndPort.host, hostAndPort.port)).apply { - handler = HandlerCollection().apply { - addHandler(buildServletContextHandler()) - } - } - - val hostAndPort: NetworkHostAndPort - get() = server.connectors.mapNotNull { it as? ServerConnector } - .map { NetworkHostAndPort(it.host, it.localPort) } - .first() + private val closeActions = mutableListOf<() -> Unit>() + lateinit var hostAndPort: NetworkHostAndPort override fun close() { - logger.info("Shutting down network management web services...") - server.stop() - server.join() - } - - fun start() { - logger.info("Starting network management web services...") - server.start() - logger.info("Network management web services started on $hostAndPort with ${webServices.map { it.javaClass.simpleName }}") - println("Network management web services started on $hostAndPort with ${webServices.map { it.javaClass.simpleName }}") - } - - private fun buildServletContextHandler(): ServletContextHandler { - return ServletContextHandler().apply { - contextPath = "/" - val resourceConfig = ResourceConfig().apply { - // Add your API provider classes (annotated for JAX-RS) here - webServices.forEach { register(it) } + for (closeAction in closeActions) { + try { + closeAction() + } catch (e: Exception) { + logger.warn("Discregarding exception thrown during close", e) } - val jerseyServlet = ServletHolder(ServletContainer(resourceConfig)).apply { initOrder = 0 }// Initialise at server start - addServlet(jerseyServlet, "/*") } } + + private fun getNetworkMapService(config: NetworkMapConfig, database: CordaPersistence, signer: LocalSigner?, newNetworkParameters: NetworkParameters?): NetworkMapWebService { + val networkMapStorage = PersistentNetworkMapStorage(database) + val nodeInfoStorage = PersistentNodeInfoStorage(database) + val localNetworkMapSigner = signer?.let { NetworkMapSigner(networkMapStorage, it) } + + newNetworkParameters?.let { + val netParamsOfNetworkMap = networkMapStorage.getNetworkParametersOfNetworkMap() + if (netParamsOfNetworkMap == null) { + localNetworkMapSigner?.persistSignedNetworkParameters(it) ?: networkMapStorage.saveNetworkParameters(it, null) + } else { + throw UnsupportedOperationException("Network parameters already exist. Updating them is not supported yet.") + } + } + + val latestParameters = networkMapStorage.getLatestNetworkParameters() ?: + throw IllegalStateException("No network parameters were found. Please upload new network parameters before starting network map service") + logger.info("Starting network map service with network parameters: $latestParameters") + + if (localNetworkMapSigner != null) { + logger.info("Starting background worker for signing the network map using the local key store") + val scheduledExecutor = Executors.newScheduledThreadPool(1) + scheduledExecutor.scheduleAtFixedRate({ + try { + localNetworkMapSigner.signNetworkMap() + } catch (e: Exception) { + // Log the error and carry on. + logger.error("Unable to sign network map", e) + } + }, config.signInterval, config.signInterval, TimeUnit.MILLISECONDS) + closeActions += scheduledExecutor::shutdown + } + + return NetworkMapWebService(nodeInfoStorage, networkMapStorage, config) + } + + + private fun getDoormanService(config: DoormanConfig, + database: CordaPersistence, + csrCertPathAndKey: CertPathAndKey?, + serverStatus: NetworkManagementServerStatus): RegistrationWebService { + logger.info("Starting Doorman server.") + val requestService = if (config.approveAll) { + logger.warn("Doorman server is in 'Approve All' mode, this will approve all incoming certificate signing requests.") + ApproveAllCertificateRequestStorage(PersistentCertificateRequestStorage(database)) + } else { + PersistentCertificateRequestStorage(database) + } + + val jiraConfig = config.jiraConfig + val requestProcessor = if (jiraConfig != null) { + val jiraWebAPI = AsynchronousJiraRestClientFactory().createWithBasicHttpAuthentication(URI(jiraConfig.address), jiraConfig.username, jiraConfig.password) + val jiraClient = JiraClient(jiraWebAPI, jiraConfig.projectCode, jiraConfig.doneTransitionCode) + JiraCsrHandler(jiraClient, requestService, DefaultCsrHandler(requestService, csrCertPathAndKey)) + } else { + DefaultCsrHandler(requestService, csrCertPathAndKey) + } + + val scheduledExecutor = Executors.newScheduledThreadPool(1) + val approvalThread = Runnable { + try { + serverStatus.lastRequestCheckTime = Instant.now() + // Create tickets for requests which don't have one yet. + requestProcessor.createTickets() + // Process Jira approved tickets. + requestProcessor.processApprovedRequests() + } catch (e: Exception) { + // Log the error and carry on. + logger.error("Error encountered when approving request.", e) + } + } + scheduledExecutor.scheduleAtFixedRate(approvalThread, config.approveInterval, config.approveInterval, TimeUnit.MILLISECONDS) + closeActions += scheduledExecutor::shutdown + + return RegistrationWebService(requestProcessor) + } + + fun start(hostAndPort: NetworkHostAndPort, + database: CordaPersistence, + csrCertPathAndKey: CertPathAndKey?, + doormanServiceParameter: DoormanConfig?, // TODO Doorman config shouldn't be optional as the doorman is always required to run + startNetworkMap: NetworkMapStartParams? + ) { + val services = mutableListOf() + val serverStatus = NetworkManagementServerStatus() + + startNetworkMap?.let { services += getNetworkMapService(it.config, database, it.signer, it.updateNetworkParameters) } + doormanServiceParameter?.let { services += getDoormanService(it, database, csrCertPathAndKey, serverStatus) } + + require(services.isNotEmpty()) { "No service created, please provide at least one service config." } + + // TODO: use mbean to expose audit data? + services += MonitoringWebService(serverStatus) + + val webServer = NetworkManagementWebServer(hostAndPort, *services.toTypedArray()) + webServer.start() + + closeActions += webServer::close + this.hostAndPort = webServer.hostAndPort + } } \ No newline at end of file diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/NetworkManagementWebServer.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/NetworkManagementWebServer.kt new file mode 100644 index 0000000000..fb99e68c44 --- /dev/null +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/NetworkManagementWebServer.kt @@ -0,0 +1,59 @@ +package com.r3.corda.networkmanage.doorman + +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.core.utilities.contextLogger +import net.corda.core.utilities.loggerFor +import org.eclipse.jetty.server.Server +import org.eclipse.jetty.server.ServerConnector +import org.eclipse.jetty.server.handler.HandlerCollection +import org.eclipse.jetty.servlet.ServletContextHandler +import org.eclipse.jetty.servlet.ServletHolder +import org.glassfish.jersey.server.ResourceConfig +import org.glassfish.jersey.servlet.ServletContainer +import java.io.Closeable +import java.net.InetSocketAddress + +/** + * NetworkManagementWebServer runs on Jetty server and provides service via http. + */ +class NetworkManagementWebServer(hostAndPort: NetworkHostAndPort, private vararg val webServices: Any) : Closeable { + companion object { + val logger = contextLogger() + } + + private val server: Server = Server(InetSocketAddress(hostAndPort.host, hostAndPort.port)).apply { + handler = HandlerCollection().apply { + addHandler(buildServletContextHandler()) + } + } + + val hostAndPort: NetworkHostAndPort + get() = server.connectors.mapNotNull { it as? ServerConnector } + .map { NetworkHostAndPort(it.host, it.localPort) } + .first() + + override fun close() { + logger.info("Shutting down network management web services...") + server.stop() + server.join() + } + + fun start() { + logger.info("Starting network management web services...") + server.start() + logger.info("Network management web services started on $hostAndPort with ${webServices.map { it.javaClass.simpleName }}") + println("Network management web services started on $hostAndPort with ${webServices.map { it.javaClass.simpleName }}") + } + + private fun buildServletContextHandler(): ServletContextHandler { + return ServletContextHandler().apply { + contextPath = "/" + val resourceConfig = ResourceConfig().apply { + // Add your API provider classes (annotated for JAX-RS) here + webServices.forEach { register(it) } + } + val jerseyServlet = ServletHolder(ServletContainer(resourceConfig)).apply { initOrder = 0 }// Initialise at server start + addServlet(jerseyServlet, "/*") + } + } +} \ No newline at end of file diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/CsrHandler.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/CsrHandler.kt index 2a6a9e5ee4..207a118c88 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/CsrHandler.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/CsrHandler.kt @@ -1,13 +1,20 @@ package com.r3.corda.networkmanage.doorman.signer import com.r3.corda.networkmanage.common.persistence.CertificateResponse -import com.r3.corda.networkmanage.common.persistence.CertificateSigningRequest import com.r3.corda.networkmanage.common.persistence.CertificationRequestStorage import com.r3.corda.networkmanage.common.persistence.CertificationRequestStorage.Companion.DOORMAN_SIGNATURE import com.r3.corda.networkmanage.common.persistence.RequestStatus -import com.r3.corda.networkmanage.doorman.JiraClient -import net.corda.core.utilities.loggerFor +import com.r3.corda.networkmanage.common.utils.CertPathAndKey +import net.corda.nodeapi.internal.crypto.CertificateType +import net.corda.nodeapi.internal.crypto.X509CertificateFactory +import net.corda.nodeapi.internal.crypto.X509Utilities +import org.bouncycastle.asn1.x509.GeneralName +import org.bouncycastle.asn1.x509.GeneralSubtree +import org.bouncycastle.asn1.x509.NameConstraints import org.bouncycastle.pkcs.PKCS10CertificationRequest +import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest +import java.security.cert.CertPath +import javax.security.auth.x500.X500Principal interface CsrHandler { fun saveRequest(rawRequest: PKCS10CertificationRequest): String @@ -16,26 +23,21 @@ interface CsrHandler { fun getResponse(requestId: String): CertificateResponse } -class DefaultCsrHandler(private val storage: CertificationRequestStorage, private val signer: LocalSigner?) : CsrHandler { +class DefaultCsrHandler(private val storage: CertificationRequestStorage, + private val csrCertPathAndKey: CertPathAndKey?) : CsrHandler { + override fun processApprovedRequests() { - storage.getRequests(RequestStatus.APPROVED) - .forEach { processRequest(it.requestId, it.request) } + if (csrCertPathAndKey == null) return + storage.getRequests(RequestStatus.APPROVED).forEach { + val nodeCertPath = createSignedNodeCertificate(it.request, csrCertPathAndKey) + // Since Doorman is deployed in the auto-signing mode, we use DOORMAN_SIGNATURE as the signer. + storage.putCertificatePath(it.requestId, nodeCertPath, listOf(DOORMAN_SIGNATURE)) + } } override fun createTickets() {} - private fun processRequest(requestId: String, request: PKCS10CertificationRequest) { - if (signer != null) { - val certs = signer.createSignedClientCertificate(request) - // Since Doorman is deployed in the auto-signing mode (i.e. signer != null), - // we use DOORMAN_SIGNATURE as the signer. - storage.putCertificatePath(requestId, certs, listOf(DOORMAN_SIGNATURE)) - } - } - - override fun saveRequest(rawRequest: PKCS10CertificationRequest): String { - return storage.saveRequest(rawRequest) - } + override fun saveRequest(rawRequest: PKCS10CertificationRequest): String = storage.saveRequest(rawRequest) override fun getResponse(requestId: String): CertificateResponse { val response = storage.getRequest(requestId) @@ -45,63 +47,25 @@ class DefaultCsrHandler(private val storage: CertificationRequestStorage, privat RequestStatus.SIGNED -> CertificateResponse.Ready(response.certData?.certPath ?: throw IllegalArgumentException("Certificate should not be null.")) } } -} -class JiraCsrHandler(private val jiraClient: JiraClient, private val storage: CertificationRequestStorage, private val delegate: CsrHandler) : CsrHandler by delegate { - private companion object { - val log = loggerFor() - } - - override fun saveRequest(rawRequest: PKCS10CertificationRequest): String { - val requestId = delegate.saveRequest(rawRequest) - // Make sure request has been accepted. - try { - if (delegate.getResponse(requestId) !is CertificateResponse.Unauthorised) { - jiraClient.createRequestTicket(requestId, rawRequest) - storage.markRequestTicketCreated(requestId) - } - } catch (e: Exception) { - log.warn("There was an error while creating Jira tickets", e) - } finally { - return requestId - } - } - - override fun processApprovedRequests() { - val approvedRequest = jiraClient.getApprovedRequests() - approvedRequest.forEach { (id, approvedBy) -> storage.approveRequest(id, approvedBy) } - delegate.processApprovedRequests() - - val signedRequests = approvedRequest.mapNotNull { (id, _) -> - val request = storage.getRequest(id) - - if (request != null && request.status == RequestStatus.SIGNED) { - request.certData?.certPath?.let { certs -> id to certs } - } else { - null - } - }.toMap() - jiraClient.updateSignedRequests(signedRequests) - } - - /** - * Creates Jira tickets for all request in [RequestStatus.NEW] state. - * - * Usually requests are expected to move to the [RequestStatus.TICKET_CREATED] state immediately, - * they might be left in the [RequestStatus.NEW] state if Jira is down. - */ - override fun createTickets() { - try { - for (signingRequest in storage.getRequests(RequestStatus.NEW)) { - createTicket(signingRequest) - } - } catch (e: Exception) { - log.warn("There were errors while creating Jira tickets", e) - } - } - - private fun createTicket(signingRequest: CertificateSigningRequest) { - jiraClient.createRequestTicket(signingRequest.requestId, signingRequest.request) - storage.markRequestTicketCreated(signingRequest.requestId) + private fun createSignedNodeCertificate(certificationRequest: PKCS10CertificationRequest, + csrCertPathAndKey: CertPathAndKey): CertPath { + // The sub certs issued by the client must satisfy this directory name (or legal name in Corda) constraints, + // sub certs' directory name must be within client CA's name's subtree, + // please see [sun.security.x509.X500Name.isWithinSubtree()] for more information. + // We assume all attributes in the subject name has been checked prior approval. + // TODO: add validation to subject name. + val request = JcaPKCS10CertificationRequest(certificationRequest) + val nameConstraints = NameConstraints( + arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, request.subject))), + arrayOf()) + val nodeCaCert = X509Utilities.createCertificate( + CertificateType.NODE_CA, + csrCertPathAndKey.certPath[0], + csrCertPathAndKey.toKeyPair(), + X500Principal(request.subject.encoded), + request.publicKey, + nameConstraints = nameConstraints) + return X509CertificateFactory().generateCertPath(listOf(nodeCaCert) + csrCertPathAndKey.certPath) } } diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/JiraCsrHandler.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/JiraCsrHandler.kt new file mode 100644 index 0000000000..e7f9d51046 --- /dev/null +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/JiraCsrHandler.kt @@ -0,0 +1,69 @@ +package com.r3.corda.networkmanage.doorman.signer + +import com.r3.corda.networkmanage.common.persistence.CertificateResponse +import com.r3.corda.networkmanage.common.persistence.CertificateSigningRequest +import com.r3.corda.networkmanage.common.persistence.CertificationRequestStorage +import com.r3.corda.networkmanage.common.persistence.RequestStatus +import com.r3.corda.networkmanage.doorman.JiraClient +import net.corda.core.utilities.contextLogger +import net.corda.core.utilities.loggerFor +import org.bouncycastle.pkcs.PKCS10CertificationRequest + +class JiraCsrHandler(private val jiraClient: JiraClient, private val storage: CertificationRequestStorage, private val delegate: CsrHandler) : CsrHandler by delegate { + private companion object { + val log = contextLogger() + } + + override fun saveRequest(rawRequest: PKCS10CertificationRequest): String { + val requestId = delegate.saveRequest(rawRequest) + // Make sure request has been accepted. + try { + if (delegate.getResponse(requestId) !is CertificateResponse.Unauthorised) { + jiraClient.createRequestTicket(requestId, rawRequest) + storage.markRequestTicketCreated(requestId) + } + } catch (e: Exception) { + log.warn("There was an error while creating Jira tickets", e) + } finally { + return requestId + } + } + + override fun processApprovedRequests() { + val approvedRequest = jiraClient.getApprovedRequests() + approvedRequest.forEach { (id, approvedBy) -> storage.approveRequest(id, approvedBy) } + delegate.processApprovedRequests() + + val signedRequests = approvedRequest.mapNotNull { (id, _) -> + val request = storage.getRequest(id) + + if (request != null && request.status == RequestStatus.SIGNED) { + request.certData?.certPath?.let { certs -> id to certs } + } else { + null + } + }.toMap() + jiraClient.updateSignedRequests(signedRequests) + } + + /** + * Creates Jira tickets for all request in [RequestStatus.NEW] state. + * + * Usually requests are expected to move to the [RequestStatus.TICKET_CREATED] state immediately, + * they might be left in the [RequestStatus.NEW] state if Jira is down. + */ + override fun createTickets() { + try { + for (signingRequest in storage.getRequests(RequestStatus.NEW)) { + createTicket(signingRequest) + } + } catch (e: Exception) { + log.warn("There were errors while creating Jira tickets", e) + } + } + + private fun createTicket(signingRequest: CertificateSigningRequest) { + jiraClient.createRequestTicket(signingRequest.requestId, signingRequest.request) + storage.markRequestTicketCreated(signingRequest.requestId) + } +} diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/LocalSigner.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/LocalSigner.kt index 18343d7e44..aed84388c6 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/LocalSigner.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/LocalSigner.kt @@ -1,48 +1,20 @@ package com.r3.corda.networkmanage.doorman.signer import com.r3.corda.networkmanage.common.signer.Signer -import com.r3.corda.networkmanage.common.utils.buildCertPath import net.corda.core.crypto.Crypto import net.corda.core.internal.DigitalSignatureWithCert -import net.corda.nodeapi.internal.crypto.CertificateType -import net.corda.nodeapi.internal.crypto.X509Utilities -import org.bouncycastle.asn1.x509.GeneralName -import org.bouncycastle.asn1.x509.GeneralSubtree -import org.bouncycastle.asn1.x509.NameConstraints -import org.bouncycastle.pkcs.PKCS10CertificationRequest -import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest -import java.security.KeyPair -import java.security.cert.CertPath +import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair +import java.security.KeyStore +import java.security.PrivateKey import java.security.cert.X509Certificate -import javax.security.auth.x500.X500Principal /** - * The [LocalSigner] class signs [PKCS10CertificationRequest] using provided CA key pair and certificate path. - * This is intended to be used in testing environment where hardware signing module is not available. + * This local signer is intended to be used in testing environment where hardware signing module is not available. */ -//TODO Use a list instead of array -class LocalSigner(private val signingKeyPair: KeyPair, private val signingCertPath: Array) : Signer { - // TODO This doesn't belong in this class - fun createSignedClientCertificate(certificationRequest: PKCS10CertificationRequest): CertPath { - // The sub certs issued by the client must satisfy this directory name (or legal name in Corda) constraints, sub certs' directory name must be within client CA's name's subtree, - // please see [sun.security.x509.X500Name.isWithinSubtree()] for more information. - // We assume all attributes in the subject name has been checked prior approval. - // TODO: add validation to subject name. - val request = JcaPKCS10CertificationRequest(certificationRequest) - val nameConstraints = NameConstraints( - arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, request.subject))), - arrayOf()) - val nodeCaCert = X509Utilities.createCertificate( - CertificateType.NODE_CA, - signingCertPath[0], - signingKeyPair, - X500Principal(request.subject.encoded), - request.publicKey, - nameConstraints = nameConstraints) - return buildCertPath(nodeCaCert, *signingCertPath) - } +class LocalSigner(private val signingKey: PrivateKey, private val signingCert: X509Certificate) : Signer { + constructor(certAndKeyPair: CertificateAndKeyPair) : this(certAndKeyPair.keyPair.private, certAndKeyPair.certificate) override fun signBytes(data: ByteArray): DigitalSignatureWithCert { - return DigitalSignatureWithCert(signingCertPath[0], Crypto.doSign(signingKeyPair.private, data)) + return DigitalSignatureWithCert(signingCert, Crypto.doSign(signingKey, data)) } } diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/webservice/NodeInfoWebService.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/webservice/NetworkMapWebService.kt similarity index 91% rename from network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/webservice/NodeInfoWebService.kt rename to network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/webservice/NetworkMapWebService.kt index 57418b3d9c..93706599c5 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/webservice/NodeInfoWebService.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/webservice/NetworkMapWebService.kt @@ -7,9 +7,8 @@ import com.r3.corda.networkmanage.common.persistence.NetworkMapStorage import com.r3.corda.networkmanage.common.persistence.NodeInfoStorage import com.r3.corda.networkmanage.common.persistence.NodeInfoWithSigned import com.r3.corda.networkmanage.common.utils.SignedNetworkMap -import com.r3.corda.networkmanage.common.utils.SignedNetworkParameters import com.r3.corda.networkmanage.doorman.NetworkMapConfig -import com.r3.corda.networkmanage.doorman.webservice.NodeInfoWebService.Companion.NETWORK_MAP_PATH +import com.r3.corda.networkmanage.doorman.webservice.NetworkMapWebService.Companion.NETWORK_MAP_PATH import net.corda.core.crypto.SecureHash import net.corda.core.node.NodeInfo import net.corda.core.serialization.deserialize @@ -31,9 +30,9 @@ import javax.ws.rs.core.Response.ok import javax.ws.rs.core.Response.status @Path(NETWORK_MAP_PATH) -class NodeInfoWebService(private val nodeInfoStorage: NodeInfoStorage, - private val networkMapStorage: NetworkMapStorage, - private val config: NetworkMapConfig) { +class NetworkMapWebService(private val nodeInfoStorage: NodeInfoStorage, + private val networkMapStorage: NetworkMapStorage, + private val config: NetworkMapConfig) { companion object { val log = contextLogger() @@ -42,7 +41,7 @@ class NodeInfoWebService(private val nodeInfoStorage: NodeInfoStorage, private val networkMapCache: LoadingCache> = CacheBuilder.newBuilder() .expireAfterWrite(config.cacheTimeout, TimeUnit.MILLISECONDS) - .build(CacheLoader.from { _ -> Pair(networkMapStorage.getCurrentNetworkMap(), networkMapStorage.getCurrentSignedNetworkParameters()?.verified()) }) + .build(CacheLoader.from { _ -> Pair(networkMapStorage.getCurrentNetworkMap(), networkMapStorage.getNetworkParametersOfNetworkMap()?.verified()) }) @POST @Path("publish") diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/Main.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/Main.kt index 4e25900f68..4ab7e0a7f7 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/Main.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/Main.kt @@ -1,7 +1,10 @@ package com.r3.corda.networkmanage.hsm +import com.google.common.util.concurrent.MoreExecutors +import com.r3.corda.networkmanage.common.persistence.NetworkMapStorage import com.r3.corda.networkmanage.common.persistence.PersistentNetworkMapStorage import com.r3.corda.networkmanage.common.persistence.configureDatabase +import com.r3.corda.networkmanage.common.signer.NetworkMapSigner import com.r3.corda.networkmanage.common.utils.ShowHelpException import com.r3.corda.networkmanage.hsm.authentication.AuthMode import com.r3.corda.networkmanage.hsm.authentication.Authenticator @@ -16,10 +19,19 @@ import com.r3.corda.networkmanage.hsm.persistence.DBSignedCertificateRequestStor import com.r3.corda.networkmanage.hsm.signer.HsmCsrSigner import com.r3.corda.networkmanage.hsm.signer.HsmNetworkMapSigner import com.r3.corda.networkmanage.hsm.utils.mapCryptoServerException +import net.corda.core.utilities.minutes +import org.apache.logging.log4j.LogManager import org.bouncycastle.jce.provider.BouncyCastleProvider import java.security.Security +import java.time.Duration +import java.util.concurrent.Executors +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.TimeUnit.MILLISECONDS +import java.util.concurrent.TimeUnit.SECONDS import javax.crypto.Cipher +private val log = LogManager.getLogger("com.r3.corda.networkmanage.hsm.Main") + fun main(args: Array) { // Grabbed from https://stackoverflow.com/questions/7953567/checking-if-unlimited-cryptography-is-available if (Cipher.getMaxAllowedKeyLength("AES") < 256) { @@ -46,12 +58,15 @@ fun run(parameters: Parameters) { checkNotNull(dataSourceProperties) val database = configureDatabase(dataSourceProperties, databaseConfig) val csrStorage = DBSignedCertificateRequestStorage(database) - val networkMapStorage = PersistentNetworkMapStorage(database) - val hsmNetworkMapSigningThread = HsmNetworkMapSigner( - networkMapStorage, + val hsmSigner = HsmNetworkMapSigner( networkMapCertificateName, networkMapPrivateKeyPassword, - Authenticator(createProvider(), AuthMode.KEY_FILE, autoUsername, authKeyFilePath, authKeyFilePassword, signAuthThreshold)).start() + Authenticator(createProvider(), AuthMode.KEY_FILE, autoUsername, authKeyFilePath, authKeyFilePassword, signAuthThreshold)) + + val networkMapStorage = PersistentNetworkMapStorage(database) + val scheduler = Executors.newSingleThreadScheduledExecutor() + startNetworkingMapSigningPolling(networkMapStorage, hsmSigner, scheduler, 10.minutes) + val sign: (List) -> Unit = { val signer = HsmCsrSigner( csrStorage, @@ -62,6 +77,7 @@ fun run(parameters: Parameters) { Authenticator(createProvider(), authMode, autoUsername, authKeyFilePath, authKeyFilePassword, signAuthThreshold)) signer.sign(it) } + Menu().withExceptionHandler(::processError).addItem("1", "Generate root and intermediate certificates", { if (confirmedKeyGen()) { val generator = KeyCertificateGenerator( @@ -104,10 +120,25 @@ fun run(parameters: Parameters) { println("There is no approved and unsigned CSR") } }).showMenu() - hsmNetworkMapSigningThread.stop() + + MoreExecutors.shutdownAndAwaitTermination(scheduler, 30, SECONDS) } } +private fun startNetworkingMapSigningPolling(networkMapStorage: NetworkMapStorage, + signer: HsmNetworkMapSigner, + executor: ScheduledExecutorService, + signingPeriod: Duration) { + val networkMapSigner = NetworkMapSigner(networkMapStorage, signer) + executor.scheduleAtFixedRate({ + try { + networkMapSigner.signNetworkMap() + } catch (e: Exception) { + log.warn("Exception thrown while signing network map", e) + } + }, signingPeriod.toMillis(), signingPeriod.toMillis(), MILLISECONDS) +} + private fun processError(exception: Exception) { val processed = mapCryptoServerException(exception) System.err.println("An error occurred: ${processed.message}") diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/signer/HsmNetworkMapSigner.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/signer/HsmNetworkMapSigner.kt index 322233220f..aa0c4e6e17 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/signer/HsmNetworkMapSigner.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/signer/HsmNetworkMapSigner.kt @@ -1,78 +1,39 @@ package com.r3.corda.networkmanage.hsm.signer -import com.google.common.util.concurrent.MoreExecutors -import com.r3.corda.networkmanage.common.persistence.NetworkMapStorage -import com.r3.corda.networkmanage.common.signer.NetworkMapSigner import com.r3.corda.networkmanage.common.signer.Signer import com.r3.corda.networkmanage.hsm.authentication.Authenticator import com.r3.corda.networkmanage.hsm.utils.X509Utilities import com.r3.corda.networkmanage.hsm.utils.X509Utilities.getAndInitializeKeyStore import com.r3.corda.networkmanage.hsm.utils.X509Utilities.verify import net.corda.core.internal.DigitalSignatureWithCert -import net.corda.core.utilities.loggerFor -import net.corda.core.utilities.minutes +import net.corda.nodeapi.internal.crypto.getX509Certificate import java.security.PrivateKey import java.security.Signature -import java.security.cert.X509Certificate -import java.time.Duration -import java.util.concurrent.Executors -import java.util.concurrent.ScheduledExecutorService -import java.util.concurrent.TimeUnit /** - * Encapsulates logic for periodic network map signing execution. - * It uses HSM as the signing entity with keys and certificates specified at the construction time. + * Signer which connects to a HSM using the given [authenticator] to sign bytes. */ // TODO Rename this to HsmSigner -class HsmNetworkMapSigner(networkMapStorage: NetworkMapStorage, - private val caCertificateKeyName: String, - private val caPrivateKeyPass: String, - private val authenticator: Authenticator, - private val signingPeriod: Duration = DEFAULT_SIGNING_PERIOD_MS) : Signer { - - companion object { - val log = loggerFor() - val DEFAULT_SIGNING_PERIOD_MS = 10.minutes - - private val TERMINATION_TIMEOUT_SEC = 2L - } - - private val networkMapSigner = NetworkMapSigner(networkMapStorage, this) - private lateinit var scheduledExecutor: ScheduledExecutorService - - // TODO This doesn't belong in this class - fun start(): HsmNetworkMapSigner { - val signingPeriodMillis = signingPeriod.toMillis() - scheduledExecutor = Executors.newSingleThreadScheduledExecutor() - scheduledExecutor.scheduleAtFixedRate({ - try { - networkMapSigner.signNetworkMap() - } catch (exception: Exception) { - log.warn("Exception thrown while signing network map", exception) - } - }, signingPeriodMillis, signingPeriodMillis, TimeUnit.MILLISECONDS) - return this - } - - fun stop() { - MoreExecutors.shutdownAndAwaitTermination(scheduledExecutor, TERMINATION_TIMEOUT_SEC, TimeUnit.SECONDS) - } - +class HsmNetworkMapSigner(private val certificateKeyName: String, + private val privateKeyPassword: String, + private val authenticator: Authenticator) : Signer { /** * Signs given data using [CryptoServerJCE.CryptoServerProvider], which connects to the underlying HSM. */ override fun signBytes(data: ByteArray): DigitalSignatureWithCert { return authenticator.connectAndAuthenticate { provider, _ -> val keyStore = getAndInitializeKeyStore(provider) - val caCertificateChain = keyStore.getCertificateChain(caCertificateKeyName) - val caKey = keyStore.getKey(caCertificateKeyName, caPrivateKeyPass.toCharArray()) as PrivateKey + val certificate = keyStore.getX509Certificate(certificateKeyName) + // Don't worry this is not a real private key but a pointer to one that resides in the HSM. It only works + // when used with the given provider. + val key = keyStore.getKey(certificateKeyName, privateKeyPassword.toCharArray()) as PrivateKey val signature = Signature.getInstance(X509Utilities.SIGNATURE_ALGORITHM, provider).run { - initSign(caKey) + initSign(key) update(data) sign() } - verify(data, signature, caCertificateChain[0].publicKey) - DigitalSignatureWithCert(caCertificateChain[0] as X509Certificate, signature) + verify(data, signature, certificate.publicKey) + DigitalSignatureWithCert(certificate, signature) } } } diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNetworkMapStorageTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNetworkMapStorageTest.kt index af75cf07b7..77d041da43 100644 --- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNetworkMapStorageTest.kt +++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNetworkMapStorageTest.kt @@ -65,7 +65,7 @@ class PersistentNetworkMapStorageTest : TestBase() { // then val persistedSignedNetworkMap = networkMapStorage.getCurrentNetworkMap() - val persistedSignedParameters = networkMapStorage.getCurrentSignedNetworkParameters() + val persistedSignedParameters = networkMapStorage.getNetworkParametersOfNetworkMap() assertEquals(networkParameters, persistedSignedParameters?.verifiedNetworkMapCert(rootCaCert)) assertEquals(parametersSignature, persistedSignedParameters?.sig) @@ -84,13 +84,13 @@ class PersistentNetworkMapStorageTest : TestBase() { networkMapStorage.saveNetworkParameters(params2, null) // when - val latest = networkMapStorage.getLatestUnsignedNetworkParameters() + val latest = networkMapStorage.getLatestNetworkParameters()?.minimumPlatformVersion // then - assertEquals(2, latest.minimumPlatformVersion) + assertEquals(2, latest) } @Test - fun `getCurrentNetworkParameters returns current network map parameters`() { + fun `getNetworkParametersOfNetworkMap returns current network map parameters`() { // given // Create network parameters val testParameters1 = testNetworkParameters(emptyList()) @@ -107,7 +107,7 @@ class PersistentNetworkMapStorageTest : TestBase() { networkMapStorage.saveNetworkParameters(testParameters2, testParameters2.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate).sig) // when - val result = networkMapStorage.getCurrentSignedNetworkParameters()?.verifiedNetworkMapCert(rootCaCert) + val result = networkMapStorage.getNetworkParametersOfNetworkMap()?.verifiedNetworkMapCert(rootCaCert) // then assertEquals(1, result?.minimumPlatformVersion) diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/signer/NetworkMapSignerTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/signer/NetworkMapSignerTest.kt index dc29826d1b..25d9f7e7f5 100644 --- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/signer/NetworkMapSignerTest.kt +++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/signer/NetworkMapSignerTest.kt @@ -51,8 +51,8 @@ class NetworkMapSignerTest : TestBase() { val signedNetworkMap = networkMap.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate) whenever(networkMapStorage.getCurrentNetworkMap()).thenReturn(signedNetworkMap) whenever(networkMapStorage.getNodeInfoHashes(any())).thenReturn(signedNodeInfoHashes) - whenever(networkMapStorage.getLatestUnsignedNetworkParameters()).thenReturn(latestNetworkParameters) - whenever(networkMapStorage.getCurrentSignedNetworkParameters()).thenReturn(currentParameters.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate)) + whenever(networkMapStorage.getLatestNetworkParameters()).thenReturn(latestNetworkParameters) + whenever(networkMapStorage.getNetworkParametersOfNetworkMap()).thenReturn(currentParameters.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate)) whenever(signer.signBytes(any())).then { DigitalSignatureWithCert(networkMapCa.certificate, Crypto.doSign(networkMapCa.keyPair.private, it.arguments[0] as ByteArray)) } @@ -67,8 +67,8 @@ class NetworkMapSignerTest : TestBase() { // then // Verify networkMapStorage calls verify(networkMapStorage).getNodeInfoHashes(any()) - verify(networkMapStorage).getLatestUnsignedNetworkParameters() - verify(networkMapStorage).getCurrentSignedNetworkParameters() + verify(networkMapStorage).getLatestNetworkParameters() + verify(networkMapStorage).getNetworkParametersOfNetworkMap() argumentCaptor().apply { verify(networkMapStorage).saveNetworkMap(capture()) val capturedNetworkMap = firstValue.verifiedNetworkMapCert(rootCaCert) @@ -87,8 +87,8 @@ class NetworkMapSignerTest : TestBase() { val signedNetworkMap = networkMap.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate) whenever(networkMapStorage.getCurrentNetworkMap()).thenReturn(signedNetworkMap) whenever(networkMapStorage.getNodeInfoHashes(any())).thenReturn(emptyList()) - whenever(networkMapStorage.getLatestUnsignedNetworkParameters()).thenReturn(networkParameters) - whenever(networkMapStorage.getCurrentSignedNetworkParameters()).thenReturn(networkParameters.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate)) + whenever(networkMapStorage.getLatestNetworkParameters()).thenReturn(networkParameters) + whenever(networkMapStorage.getNetworkParametersOfNetworkMap()).thenReturn(networkParameters.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate)) // when networkMapSigner.signNetworkMap() @@ -104,7 +104,7 @@ class NetworkMapSignerTest : TestBase() { val networkParameters = testNetworkParameters(emptyList()) whenever(networkMapStorage.getCurrentNetworkMap()).thenReturn(null) whenever(networkMapStorage.getNodeInfoHashes(any())).thenReturn(emptyList()) - whenever(networkMapStorage.getLatestUnsignedNetworkParameters()).thenReturn(networkParameters) + whenever(networkMapStorage.getLatestNetworkParameters()).thenReturn(networkParameters) whenever(signer.signBytes(any())).then { DigitalSignatureWithCert(networkMapCa.certificate, Crypto.doSign(networkMapCa.keyPair.private, it.arguments[0] as ByteArray)) } @@ -118,7 +118,7 @@ class NetworkMapSignerTest : TestBase() { // then // Verify networkMapStorage calls verify(networkMapStorage).getNodeInfoHashes(any()) - verify(networkMapStorage).getLatestUnsignedNetworkParameters() + verify(networkMapStorage).getLatestNetworkParameters() argumentCaptor().apply { verify(networkMapStorage).saveNetworkMap(capture()) val networkMap = firstValue.verifiedNetworkMapCert(rootCaCert) diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/DefaultRequestProcessorTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/DefaultRequestProcessorTest.kt deleted file mode 100644 index b0091a4cf6..0000000000 --- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/DefaultRequestProcessorTest.kt +++ /dev/null @@ -1,62 +0,0 @@ -package com.r3.corda.networkmanage.doorman - -import com.nhaarman.mockito_kotlin.any -import com.nhaarman.mockito_kotlin.mock -import com.nhaarman.mockito_kotlin.times -import com.nhaarman.mockito_kotlin.verify -import com.r3.corda.networkmanage.TestBase -import com.r3.corda.networkmanage.common.persistence.CertificateResponse -import com.r3.corda.networkmanage.common.persistence.CertificateStatus -import com.r3.corda.networkmanage.common.persistence.CertificationRequestStorage -import com.r3.corda.networkmanage.common.persistence.RequestStatus -import com.r3.corda.networkmanage.common.utils.buildCertPath -import com.r3.corda.networkmanage.doorman.signer.DefaultCsrHandler -import com.r3.corda.networkmanage.doorman.signer.LocalSigner -import net.corda.core.crypto.Crypto -import net.corda.nodeapi.internal.crypto.X509Utilities -import org.junit.Test -import javax.security.auth.x500.X500Principal -import kotlin.test.assertEquals - -class DefaultRequestProcessorTest : TestBase() { - @Test - fun `get response`() { - val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val cert = X509Utilities.createSelfSignedCACertificate(X500Principal("O=Test,L=London,C=GB"), keyPair) - - val requestStorage: CertificationRequestStorage = mock { - on { getRequest("New") }.thenReturn(certificateSigningRequest()) - on { getRequest("Signed") }.thenReturn(certificateSigningRequest(status = RequestStatus.SIGNED, certData = certificateData("", CertificateStatus.VALID, buildCertPath(cert)))) - on { getRequest("Rejected") }.thenReturn(certificateSigningRequest(status = RequestStatus.REJECTED, remark = "Random reason")) - } - val signer: LocalSigner = mock() - val requestProcessor = DefaultCsrHandler(requestStorage, signer) - - assertEquals(CertificateResponse.NotReady, requestProcessor.getResponse("random")) - assertEquals(CertificateResponse.NotReady, requestProcessor.getResponse("New")) - assertEquals(CertificateResponse.Ready(buildCertPath(cert)), requestProcessor.getResponse("Signed")) - assertEquals(CertificateResponse.Unauthorised("Random reason"), requestProcessor.getResponse("Rejected")) - } - - @Test - fun `process request`() { - val (request1, request2, request3) = (1..3).map { - X509Utilities.createCertificateSigningRequest(X500Principal("O=Test1,L=London,C=GB"), "my@email.com", Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)) - } - - val requestStorage: CertificationRequestStorage = mock { - on { getRequests(RequestStatus.APPROVED) }.thenReturn(listOf( - certificateSigningRequest(requestId = "1", request = request1, status = RequestStatus.APPROVED), - certificateSigningRequest(requestId = "2", request = request2, status = RequestStatus.APPROVED), - certificateSigningRequest(requestId = "3", request = request3, status = RequestStatus.APPROVED) - )) - } - val signer: LocalSigner = mock() - val requestProcessor = DefaultCsrHandler(requestStorage, signer) - - requestProcessor.processApprovedRequests() - - verify(signer, times(3)).createSignedClientCertificate(any()) - verify(requestStorage, times(1)).getRequests(any()) - } -} diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/signer/DefaultCsrHandlerTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/signer/DefaultCsrHandlerTest.kt new file mode 100644 index 0000000000..e518801bec --- /dev/null +++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/signer/DefaultCsrHandlerTest.kt @@ -0,0 +1,85 @@ +package com.r3.corda.networkmanage.doorman.signer + +import com.nhaarman.mockito_kotlin.* +import com.r3.corda.networkmanage.TestBase +import com.r3.corda.networkmanage.common.persistence.CertificateResponse +import com.r3.corda.networkmanage.common.persistence.CertificateStatus +import com.r3.corda.networkmanage.common.persistence.CertificationRequestStorage +import com.r3.corda.networkmanage.common.persistence.CertificationRequestStorage.Companion.DOORMAN_SIGNATURE +import com.r3.corda.networkmanage.common.persistence.RequestStatus +import com.r3.corda.networkmanage.common.utils.CertPathAndKey +import com.r3.corda.networkmanage.common.utils.buildCertPath +import net.corda.core.crypto.Crypto +import net.corda.core.internal.CertRole +import net.corda.nodeapi.internal.crypto.X509Utilities +import net.corda.testing.internal.createDevIntermediateCaCertPath +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import java.security.cert.CertPath +import java.security.cert.X509Certificate +import javax.security.auth.x500.X500Principal +import kotlin.test.assertEquals + +class DefaultCsrHandlerTest : TestBase() { + @Test + fun getResponse() { + val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val cert = X509Utilities.createSelfSignedCACertificate(X500Principal("O=Test,L=London,C=GB"), keyPair) + + val requestStorage: CertificationRequestStorage = mock { + on { getRequest("New") }.thenReturn(certificateSigningRequest()) + on { getRequest("Signed") }.thenReturn(certificateSigningRequest(status = RequestStatus.SIGNED, certData = certificateData("", CertificateStatus.VALID, buildCertPath(cert)))) + on { getRequest("Rejected") }.thenReturn(certificateSigningRequest(status = RequestStatus.REJECTED, remark = "Random reason")) + } + val requestProcessor = DefaultCsrHandler(requestStorage, null) + + assertEquals(CertificateResponse.NotReady, requestProcessor.getResponse("random")) + assertEquals(CertificateResponse.NotReady, requestProcessor.getResponse("New")) + assertEquals(CertificateResponse.Ready(buildCertPath(cert)), requestProcessor.getResponse("Signed")) + assertEquals(CertificateResponse.Unauthorised("Random reason"), requestProcessor.getResponse("Rejected")) + } + + @Test + fun processApprovedRequests() { + val requests = (1..3).map { + X509Utilities.createCertificateSigningRequest( + X500Principal("O=Test$it,L=London,C=GB"), + "my@email.com", + Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)) + } + + val requestStorage: CertificationRequestStorage = mock { + on { getRequests(RequestStatus.APPROVED) }.thenReturn(listOf( + certificateSigningRequest(requestId = "1", request = requests[0], status = RequestStatus.APPROVED), + certificateSigningRequest(requestId = "2", request = requests[1], status = RequestStatus.APPROVED) + )) + on { getRequests(RequestStatus.REJECTED) }.thenReturn(listOf( + certificateSigningRequest(requestId = "3", request = requests[2], status = RequestStatus.REJECTED) + )) + } + + val (rootCa, csrCa) = createDevIntermediateCaCertPath() + val csrCertPathAndKey = CertPathAndKey(listOf(csrCa.certificate, rootCa.certificate), csrCa.keyPair.private) + val requestProcessor = DefaultCsrHandler(requestStorage, csrCertPathAndKey) + + requestProcessor.processApprovedRequests() + + val certPathCapture = argumentCaptor() + + // Verify only the approved requests are taken + verify(requestStorage, times(1)).getRequests(RequestStatus.APPROVED) + verify(requestStorage, times(1)).putCertificatePath(eq("1"), certPathCapture.capture(), eq(listOf(DOORMAN_SIGNATURE))) + verify(requestStorage, times(1)).putCertificatePath(eq("2"), certPathCapture.capture(), eq(listOf(DOORMAN_SIGNATURE))) + + // Then make sure the generated node cert paths are correct + certPathCapture.allValues.forEachIndexed { index, certPath -> + X509Utilities.validateCertificateChain(rootCa.certificate, *certPath.certificates.toTypedArray()) + assertThat(certPath.certificates).hasSize(3).element(1).isEqualTo(csrCa.certificate) + (certPath.certificates[0] as X509Certificate).apply { + assertThat(CertRole.extract(this)).isEqualTo(CertRole.NODE_CA) + assertThat(publicKey).isEqualTo(Crypto.toSupportedPublicKey(requests[index].subjectPublicKeyInfo)) + assertThat(subjectX500Principal).isEqualTo(X500Principal("O=Test${index + 1},L=London,C=GB")) + } + } + } +} diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/signer/CsrHandlerTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/signer/JiraCsrHandlerTest.kt similarity index 90% rename from network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/signer/CsrHandlerTest.kt rename to network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/signer/JiraCsrHandlerTest.kt index 42314f4b38..19498bb744 100644 --- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/signer/CsrHandlerTest.kt +++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/signer/JiraCsrHandlerTest.kt @@ -14,25 +14,26 @@ import org.junit.Rule import org.junit.Test import org.mockito.Mock import org.mockito.junit.MockitoJUnit +import org.mockito.junit.MockitoRule import java.security.cert.CertPath class JiraCsrHandlerTest { @Rule @JvmField - val mockitoRule = MockitoJUnit.rule() + val mockitoRule: MockitoRule = MockitoJUnit.rule() @Mock - lateinit var jiraClient: JiraClient + private lateinit var jiraClient: JiraClient @Mock - lateinit var certificationRequestStorage: CertificationRequestStorage + private lateinit var certificationRequestStorage: CertificationRequestStorage @Mock - lateinit var defaultCsrHandler: DefaultCsrHandler + private lateinit var defaultCsrHandler: DefaultCsrHandler @Mock - var certPath: CertPath = mock() + private val certPath: CertPath = mock() private lateinit var jiraCsrHandler: JiraCsrHandler private val requestId = "id" diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/NodeInfoWebServiceTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/webservice/NetworkMapWebServiceTest.kt similarity index 86% rename from network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/NodeInfoWebServiceTest.kt rename to network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/webservice/NetworkMapWebServiceTest.kt index 037daf4892..430d298a47 100644 --- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/NodeInfoWebServiceTest.kt +++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/webservice/NetworkMapWebServiceTest.kt @@ -1,4 +1,4 @@ -package com.r3.corda.networkmanage.doorman +package com.r3.corda.networkmanage.doorman.webservice import com.nhaarman.mockito_kotlin.mock import com.nhaarman.mockito_kotlin.times @@ -7,7 +7,8 @@ import com.r3.corda.networkmanage.common.persistence.NetworkMapStorage import com.r3.corda.networkmanage.common.persistence.NodeInfoStorage import com.r3.corda.networkmanage.common.utils.SignedNetworkMap import com.r3.corda.networkmanage.common.utils.SignedNetworkParameters -import com.r3.corda.networkmanage.doorman.webservice.NodeInfoWebService +import com.r3.corda.networkmanage.doorman.NetworkManagementWebServer +import com.r3.corda.networkmanage.doorman.NetworkMapConfig import net.corda.core.crypto.SecureHash.Companion.randomSHA256 import net.corda.core.identity.CordaX500Name import net.corda.core.internal.checkOkResponse @@ -36,7 +37,7 @@ import java.security.cert.X509Certificate import javax.ws.rs.core.MediaType import kotlin.test.assertEquals -class NodeInfoWebServiceTest { +class NetworkMapWebServiceTest { @Rule @JvmField val testSerialization = SerializationEnvironmentRule(true) @@ -56,12 +57,12 @@ class NodeInfoWebServiceTest { @Test fun `submit nodeInfo`() { val networkMapStorage: NetworkMapStorage = mock { - on { getCurrentSignedNetworkParameters() }.thenReturn(testNetworkParameters(emptyList()).signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate)) + on { getNetworkParametersOfNetworkMap() }.thenReturn(testNetworkParameters(emptyList()).signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate)) } // Create node info. val (_, signedNodeInfo) = createNodeInfoAndSigned(CordaX500Name("Test", "London", "GB")) - NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(mock(), networkMapStorage, testNetworkMapConfig)).use { + NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NetworkMapWebService(mock(), networkMapStorage, testNetworkMapConfig)).use { it.start() val nodeInfoAndSignature = signedNodeInfo.serialize().bytes // Post node info and signature to doorman, this should pass without any exception. @@ -72,12 +73,12 @@ class NodeInfoWebServiceTest { @Test fun `submit old nodeInfo`() { val networkMapStorage: NetworkMapStorage = mock { - on { getCurrentSignedNetworkParameters() }.thenReturn(testNetworkParameters(emptyList(), minimumPlatformVersion = 2).signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate)) + on { getNetworkParametersOfNetworkMap() }.thenReturn(testNetworkParameters(emptyList(), minimumPlatformVersion = 2).signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate)) } // Create node info. val (_, signedNodeInfo) = createNodeInfoAndSigned(CordaX500Name("Test", "London", "GB"), platformVersion = 1) - NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(mock(), networkMapStorage, testNetworkMapConfig)).use { + NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NetworkMapWebService(mock(), networkMapStorage, testNetworkMapConfig)).use { it.start() val nodeInfoAndSignature = signedNodeInfo.serialize().bytes assertThatThrownBy { it.doPost("publish", nodeInfoAndSignature) } @@ -88,12 +89,12 @@ class NodeInfoWebServiceTest { @Test fun `submit nodeInfo when no network parameters`() { val networkMapStorage: NetworkMapStorage = mock { - on { getCurrentSignedNetworkParameters() }.thenReturn(null) + on { getNetworkParametersOfNetworkMap() }.thenReturn(null) } // Create node info. val (_, signedNodeInfo) = createNodeInfoAndSigned(CordaX500Name("Test", "London", "GB"), platformVersion = 1) - NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(mock(), networkMapStorage, testNetworkMapConfig)).use { + NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NetworkMapWebService(mock(), networkMapStorage, testNetworkMapConfig)).use { it.start() val nodeInfoAndSignature = signedNodeInfo.serialize().bytes assertThatThrownBy { it.doPost("publish", nodeInfoAndSignature) } @@ -110,7 +111,7 @@ class NodeInfoWebServiceTest { on { getCurrentNetworkMap() }.thenReturn(signedNetworkMap) } - NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(mock(), networkMapStorage, testNetworkMapConfig)).use { + NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NetworkMapWebService(mock(), networkMapStorage, testNetworkMapConfig)).use { it.start() val signedNetworkMapResponse = it.doGet("") verify(networkMapStorage, times(1)).getCurrentNetworkMap() @@ -127,7 +128,7 @@ class NodeInfoWebServiceTest { on { getNodeInfo(nodeInfoHash) }.thenReturn(signedNodeInfo) } - NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(nodeInfoStorage, mock(), testNetworkMapConfig)).use { + NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NetworkMapWebService(nodeInfoStorage, mock(), testNetworkMapConfig)).use { it.start() val nodeInfoResponse = it.doGet("node-info/$nodeInfoHash") verify(nodeInfoStorage, times(1)).getNodeInfo(nodeInfoHash) @@ -149,7 +150,7 @@ class NodeInfoWebServiceTest { on { getSignedNetworkParameters(networkParametersHash) }.thenReturn(signedNetworkParameters) } - NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(mock(), networkMapStorage, testNetworkMapConfig)).use { + NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NetworkMapWebService(mock(), networkMapStorage, testNetworkMapConfig)).use { it.start() val netParamsResponse = it.doGet("network-parameters/$networkParametersHash") verify(networkMapStorage, times(1)).getSignedNetworkParameters(networkParametersHash) diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/RegistrationWebServiceTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/webservice/RegistrationWebServiceTest.kt similarity index 98% rename from network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/RegistrationWebServiceTest.kt rename to network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/webservice/RegistrationWebServiceTest.kt index dc1b0d1949..d9821f38fc 100644 --- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/RegistrationWebServiceTest.kt +++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/webservice/RegistrationWebServiceTest.kt @@ -1,11 +1,11 @@ -package com.r3.corda.networkmanage.doorman +package com.r3.corda.networkmanage.doorman.webservice import com.nhaarman.mockito_kotlin.* import com.r3.corda.networkmanage.TestBase import com.r3.corda.networkmanage.common.persistence.CertificateResponse import com.r3.corda.networkmanage.common.utils.buildCertPath +import com.r3.corda.networkmanage.doorman.NetworkManagementWebServer import com.r3.corda.networkmanage.doorman.signer.CsrHandler -import com.r3.corda.networkmanage.doorman.webservice.RegistrationWebService import net.corda.core.crypto.Crypto import net.corda.core.crypto.SecureHash import net.corda.core.identity.CordaX500Name From 6ddf68484612a3e8173a4682dfaee09b4d367e45 Mon Sep 17 00:00:00 2001 From: Andrzej Cichocki Date: Fri, 19 Jan 2018 15:12:33 +0000 Subject: [PATCH 15/20] Fix compile errors. --- .../net/corda/flowhook/FlowHookContainer.kt | 4 ++-- .../internal/persistence/CordaPersistence.kt | 4 +++- .../internal/persistence/DatabaseTransaction.kt | 2 +- .../services/persistence/DBCheckpointStorage.kt | 3 +-- .../services/statemachine/ActionExecutorImpl.kt | 16 +++++++++------- .../statemachine/TransitionExecutorImpl.kt | 7 ++++--- .../node/services/vault/VaultSoftLockManager.kt | 6 +++--- 7 files changed, 23 insertions(+), 19 deletions(-) diff --git a/experimental/flow-hook/src/main/kotlin/net/corda/flowhook/FlowHookContainer.kt b/experimental/flow-hook/src/main/kotlin/net/corda/flowhook/FlowHookContainer.kt index 686e3d46a4..abb55cca2c 100644 --- a/experimental/flow-hook/src/main/kotlin/net/corda/flowhook/FlowHookContainer.kt +++ b/experimental/flow-hook/src/main/kotlin/net/corda/flowhook/FlowHookContainer.kt @@ -2,7 +2,7 @@ package net.corda.flowhook import co.paralleluniverse.fibers.Fiber import net.corda.node.services.statemachine.Event -import net.corda.nodeapi.internal.persistence.DatabaseTransactionManager +import net.corda.nodeapi.internal.persistence.contextTransactionOrNull import java.sql.Connection @Suppress("UNUSED") @@ -156,7 +156,7 @@ object FlowHookContainer { private fun currentTransactionOrThread(): Any { return try { - DatabaseTransactionManager.currentOrNull() + contextTransactionOrNull } catch (exception: IllegalStateException) { null } ?: Thread.currentThread() diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/CordaPersistence.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/CordaPersistence.kt index 3e818b4f76..a1c6210a5e 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/CordaPersistence.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/CordaPersistence.kt @@ -44,7 +44,9 @@ enum class TransactionIsolationLevel { } private val _contextDatabase = ThreadLocal() -val contextDatabase get() = _contextDatabase.get() ?: error("Was expecting to find CordaPersistence set on current thread: ${Strand.currentStrand()}") +var contextDatabase: CordaPersistence + get() = _contextDatabase.get() ?: error("Was expecting to find CordaPersistence set on current thread: ${Strand.currentStrand()}") + set(database) = _contextDatabase.set(database) class CordaPersistence( val dataSource: DataSource, diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/DatabaseTransaction.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/DatabaseTransaction.kt index a1c16fa9eb..ffcdd55c24 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/DatabaseTransaction.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/DatabaseTransaction.kt @@ -15,7 +15,7 @@ val contextTransaction get() = contextTransactionOrNull ?: error("Was expecting class DatabaseTransaction( isolation: Int, - private val outerTransaction: DatabaseTransaction?, + val outerTransaction: DatabaseTransaction?, val database: CordaPersistence ) { val id: UUID = UUID.randomUUID() diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DBCheckpointStorage.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DBCheckpointStorage.kt index 91b64b81e6..6fed52b7d8 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/DBCheckpointStorage.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/DBCheckpointStorage.kt @@ -5,7 +5,6 @@ import net.corda.core.serialization.SerializedBytes import net.corda.core.utilities.debug import net.corda.node.services.api.CheckpointStorage import net.corda.node.services.statemachine.Checkpoint -import net.corda.nodeapi.internal.persistence.DatabaseTransactionManager import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX import net.corda.nodeapi.internal.persistence.currentDBSession import org.slf4j.LoggerFactory @@ -43,7 +42,7 @@ class DBCheckpointStorage : CheckpointStorage { } override fun removeCheckpoint(id: StateMachineRunId): Boolean { - val session = DatabaseTransactionManager.current().session + val session = currentDBSession() val criteriaBuilder = session.criteriaBuilder val delete = criteriaBuilder.createCriteriaDelete(DBCheckpoint::class.java) val root = delete.from(DBCheckpoint::class.java) diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/ActionExecutorImpl.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/ActionExecutorImpl.kt index 6449a8b090..6efdc09f7b 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/ActionExecutorImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/ActionExecutorImpl.kt @@ -10,7 +10,9 @@ import net.corda.core.utilities.contextLogger import net.corda.core.utilities.trace import net.corda.node.services.api.CheckpointStorage import net.corda.node.services.api.ServiceHubInternal -import net.corda.nodeapi.internal.persistence.DatabaseTransactionManager +import net.corda.nodeapi.internal.persistence.contextDatabase +import net.corda.nodeapi.internal.persistence.contextTransaction +import net.corda.nodeapi.internal.persistence.contextTransactionOrNull import java.time.Duration import java.time.Instant import java.util.concurrent.TimeUnit @@ -163,24 +165,24 @@ class ActionExecutorImpl( @Suspendable private fun executeCreateTransaction() { - if (DatabaseTransactionManager.currentOrNull() != null) { + if (contextTransactionOrNull != null) { throw IllegalStateException("Refusing to create a second transaction") } - DatabaseTransactionManager.newTransaction() + contextDatabase.newTransaction() } @Suspendable private fun executeRollbackTransaction() { - DatabaseTransactionManager.currentOrNull()?.close() + contextTransactionOrNull?.close() } @Suspendable private fun executeCommitTransaction() { try { - DatabaseTransactionManager.current().commit() + contextTransaction.commit() } finally { - DatabaseTransactionManager.current().close() - DatabaseTransactionManager.setThreadLocalTx(null) + contextTransaction.close() + contextTransactionOrNull = null } } diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/TransitionExecutorImpl.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/TransitionExecutorImpl.kt index b895a40863..2cf328a450 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/TransitionExecutorImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/TransitionExecutorImpl.kt @@ -5,7 +5,8 @@ import net.corda.core.utilities.contextLogger import net.corda.node.services.statemachine.transitions.FlowContinuation import net.corda.node.services.statemachine.transitions.TransitionResult import net.corda.nodeapi.internal.persistence.CordaPersistence -import net.corda.nodeapi.internal.persistence.DatabaseTransactionManager +import net.corda.nodeapi.internal.persistence.contextDatabase +import net.corda.nodeapi.internal.persistence.contextTransactionOrNull import java.security.SecureRandom /** @@ -31,12 +32,12 @@ class TransitionExecutorImpl( transition: TransitionResult, actionExecutor: ActionExecutor ): Pair { - DatabaseTransactionManager.dataSource = database + contextDatabase = database for (action in transition.actions) { try { actionExecutor.executeAction(fiber, action) } catch (exception: Throwable) { - DatabaseTransactionManager.currentOrNull()?.close() + contextTransactionOrNull?.close() if (transition.newState.checkpoint.errorState is ErrorState.Errored) { // If we errored while transitioning to an error state then we cannot record the additional // error as that may result in an infinite loop, e.g. error propagation fails -> record error -> propagate fails again. diff --git a/node/src/main/kotlin/net/corda/node/services/vault/VaultSoftLockManager.kt b/node/src/main/kotlin/net/corda/node/services/vault/VaultSoftLockManager.kt index 7e13cfab85..33cd73e74b 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/VaultSoftLockManager.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/VaultSoftLockManager.kt @@ -10,7 +10,7 @@ import net.corda.core.utilities.toNonEmptySet import net.corda.core.utilities.trace import net.corda.node.services.statemachine.FlowStateMachineImpl import net.corda.node.services.statemachine.StateMachineManager -import net.corda.nodeapi.internal.persistence.DatabaseTransactionManager +import net.corda.nodeapi.internal.persistence.contextDatabase import java.util.* class VaultSoftLockManager private constructor(private val vault: VaultService) { @@ -52,14 +52,14 @@ class VaultSoftLockManager private constructor(private val vault: VaultService) private fun registerSoftLocks(flowId: UUID, stateRefs: NonEmptySet) { log.trace { "Reserving soft locks for flow id $flowId and states $stateRefs" } - DatabaseTransactionManager.dataSource.transaction { + contextDatabase.transaction { vault.softLockReserve(flowId, stateRefs) } } private fun unregisterSoftLocks(flowId: UUID, logic: FlowLogic<*>) { log.trace { "Releasing soft locks for flow ${logic.javaClass.simpleName} with flow id $flowId" } - DatabaseTransactionManager.dataSource.transaction { + contextDatabase.transaction { vault.softLockRelease(flowId) } } From a889a5bef130d358b62dcb4df591b14c609481d7 Mon Sep 17 00:00:00 2001 From: Andrzej Cichocki Date: Fri, 19 Jan 2018 16:50:19 +0000 Subject: [PATCH 16/20] Revert jolokiaVersion code. --- .../src/main/kotlin/net/corda/plugins/Cordformation.kt | 3 ++- .../cordformation/src/main/kotlin/net/corda/plugins/Node.kt | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordformation.kt b/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordformation.kt index a40e5f52ec..82a818992d 100644 --- a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordformation.kt +++ b/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordformation.kt @@ -55,7 +55,8 @@ class Cordformation : Plugin { override fun apply(project: Project) { Utils.createCompileConfiguration("cordapp", project) Utils.createRuntimeConfiguration(CORDFORMATION_TYPE, project) - val jolokiaVersion = project.rootProject.ext("jolokia_version") + // TODO: improve how we re-use existing declared external variables from root gradle.build + val jolokiaVersion = try { project.rootProject.ext("jolokia_version") } catch (e: Exception) { "1.3.7" } project.dependencies.add(CORDFORMATION_TYPE, "org.jolokia:jolokia-jvm:$jolokiaVersion:agent") } } diff --git a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Node.kt b/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Node.kt index f79249f82e..e7e5b3fc1c 100644 --- a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Node.kt +++ b/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Node.kt @@ -128,7 +128,8 @@ class Node(private val project: Project) : CordformNode() { * Installs the jolokia monitoring agent JAR to the node/drivers directory */ private fun installAgentJar() { - val jolokiaVersion = project.rootProject.ext("jolokia_version") + // TODO: improve how we re-use existing declared external variables from root gradle.build + val jolokiaVersion = try { project.rootProject.ext("jolokia_version") } catch (e: Exception) { "1.3.7" } val agentJar = project.configuration("runtime").files { (it.group == "org.jolokia") && (it.name == "jolokia-jvm") && From 3094e441153d793136d300140936b4171355da17 Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Fri, 19 Jan 2018 22:21:43 +0000 Subject: [PATCH 17/20] Dump exception to System.err (#213) --- .../src/main/kotlin/com/r3/corda/networkmanage/hsm/Main.kt | 3 ++- .../com/r3/corda/networkmanage/hsm/utils/HsmErrors.kt | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/Main.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/Main.kt index 4ab7e0a7f7..a586ef5329 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/Main.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/Main.kt @@ -141,7 +141,8 @@ private fun startNetworkingMapSigningPolling(networkMapStorage: NetworkMapStorag private fun processError(exception: Exception) { val processed = mapCryptoServerException(exception) - System.err.println("An error occurred: ${processed.message}") + System.err.println("An error occurred:") + processed.printStackTrace() } private fun confirmedSign(selectedItems: List): Boolean { diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/utils/HsmErrors.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/utils/HsmErrors.kt index 10ecec3ee1..9a017b5675 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/utils/HsmErrors.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/hsm/utils/HsmErrors.kt @@ -33,9 +33,9 @@ object HsmErrors { fun mapCryptoServerException(exception: Exception): Exception { // Try to decode the error code val crypto = exception as? CryptoServerException ?: exception.cause as? CryptoServerException - if (crypto != null) { - return Exception("(CryptoServer) ${HsmErrors.errors[crypto.ErrorCode]}", exception) + return if (crypto != null) { + Exception("(CryptoServer) ${HsmErrors.errors[crypto.ErrorCode]}", exception) } else { - return exception + exception } } \ No newline at end of file From 43604ed21235f2e6a98bc111610dfd26dc274ef3 Mon Sep 17 00:00:00 2001 From: Patrick Kuo Date: Mon, 22 Jan 2018 10:45:25 +0000 Subject: [PATCH 18/20] Handle rejected jira issues in doorman (#371) * handle reject status from jira - WIP * fix up after rebase * address PR issue and fix build error after rebase --- network-management/README.md | 5 +- network-management/build.gradle | 2 +- network-management/doorman.conf | 1 - .../CertificationRequestStorage.kt | 2 +- .../PersistentCertificateRequestStorage.kt | 2 +- .../doorman/DoormanParameters.kt | 3 +- .../corda/networkmanage/doorman/JiraCient.kt | 78 +++++++++++++++---- .../doorman/NetworkManagementServer.kt | 6 +- .../doorman/signer/CsrHandler.kt | 10 +-- .../doorman/signer/JiraCsrHandler.kt | 49 ++++++++---- .../webservice/NetworkMapWebService.kt | 1 - .../doorman/DoormanParametersTest.kt | 1 - .../networkmanage/doorman/JiraClientTest.kt | 55 +++++++++++++ .../doorman/signer/DefaultCsrHandlerTest.kt | 2 +- .../doorman/signer/JiraCsrHandlerTest.kt | 72 +++++++++++++++-- .../webservice/RegistrationWebServiceTest.kt | 8 +- 16 files changed, 231 insertions(+), 66 deletions(-) create mode 100644 network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/JiraClientTest.kt diff --git a/network-management/README.md b/network-management/README.md index 356c6c5348..11a60535d3 100644 --- a/network-management/README.md +++ b/network-management/README.md @@ -65,13 +65,15 @@ The doorman service can use JIRA to manage the certificate signing request appro projectCode = "TD" username = "username" password = "password" - doneTransitionCode = 41 } . . . } ``` +#### JIRA project configuration +* The JIRA project should setup as "Business Project" with "Task" workflow. +* Custom text field input "Request ID", and "Reject Reason" should be created in JIRA, doorman will exit with error without these custom fields. ### Auto approval When `approveAll` is set to `true`, the doorman will approve all requests on receive. (*This should only be enabled in a test environment) @@ -118,7 +120,6 @@ doormanConfig { projectCode = "TD" username = "username" password = "password" - doneTransitionCode = 41 } } diff --git a/network-management/build.gradle b/network-management/build.gradle index 40abcf0792..c022c203cc 100644 --- a/network-management/build.gradle +++ b/network-management/build.gradle @@ -88,7 +88,7 @@ dependencies { testCompile "com.nhaarman:mockito-kotlin:0.6.1" testCompile "com.spotify:docker-client:8.9.1" - compile('com.atlassian.jira:jira-rest-java-client-core:4.0.0') { + compile('com.atlassian.jira:jira-rest-java-client-core:5.0.4') { // The jira client includes jersey-core 1.5 which breaks everything. exclude module: 'jersey-core' } diff --git a/network-management/doorman.conf b/network-management/doorman.conf index e3a10650f8..ed58ea2d6f 100644 --- a/network-management/doorman.conf +++ b/network-management/doorman.conf @@ -23,7 +23,6 @@ doormanConfig{ projectCode = "TD" username = "username" password = "password" - doneTransitionCode = 41 } } diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/CertificationRequestStorage.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/CertificationRequestStorage.kt index b1115ff1df..9bc8d2e9f7 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/CertificationRequestStorage.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/CertificationRequestStorage.kt @@ -59,7 +59,7 @@ interface CertificationRequestStorage { * @param rejectedBy authority (its identifier) rejecting this request. * @param rejectReason brief description of the rejection reason */ - fun rejectRequest(requestId: String, rejectedBy: String, rejectReason: String) + fun rejectRequest(requestId: String, rejectedBy: String, rejectReason: String?) /** * Store certificate path with [requestId], this will store the encoded [CertPath] and transit request status to [RequestStatus.SIGNED]. diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRequestStorage.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRequestStorage.kt index 339fcb7978..f1511ff33f 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRequestStorage.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRequestStorage.kt @@ -93,7 +93,7 @@ class PersistentCertificateRequestStorage(private val database: CordaPersistence } } - override fun rejectRequest(requestId: String, rejectedBy: String, rejectReason: String) { + override fun rejectRequest(requestId: String, rejectedBy: String, rejectReason: String?) { database.transaction(TransactionIsolationLevel.SERIALIZABLE) { val request = findRequest(requestId) request ?: throw IllegalArgumentException("Error when rejecting request with id: $requestId. Request does not exist.") diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/DoormanParameters.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/DoormanParameters.kt index 4ffaf397ce..5c2aabae0e 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/DoormanParameters.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/DoormanParameters.kt @@ -65,8 +65,7 @@ data class JiraConfig( val address: String, val projectCode: String, val username: String, - val password: String, - val doneTransitionCode: Int + val password: String ) /** diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/JiraCient.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/JiraCient.kt index d7ebf4abc0..9cc722a325 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/JiraCient.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/JiraCient.kt @@ -1,13 +1,15 @@ package com.r3.corda.networkmanage.doorman +import com.atlassian.jira.rest.client.api.IssueRestClient import com.atlassian.jira.rest.client.api.JiraRestClient +import com.atlassian.jira.rest.client.api.domain.Comment import com.atlassian.jira.rest.client.api.domain.Field import com.atlassian.jira.rest.client.api.domain.Issue import com.atlassian.jira.rest.client.api.domain.IssueType import com.atlassian.jira.rest.client.api.domain.input.IssueInputBuilder import com.atlassian.jira.rest.client.api.domain.input.TransitionInput import net.corda.core.identity.CordaX500Name -import net.corda.core.utilities.loggerFor +import net.corda.core.utilities.contextLogger import net.corda.nodeapi.internal.crypto.X509Utilities import org.bouncycastle.asn1.x500.style.BCStyle import org.bouncycastle.openssl.jcajce.JcaPEMWriter @@ -17,15 +19,20 @@ import java.io.StringWriter import java.security.cert.CertPath import javax.security.auth.x500.X500Principal -class JiraClient(private val restClient: JiraRestClient, private val projectCode: String, private val doneTransitionCode: Int) { +class JiraClient(private val restClient: JiraRestClient, private val projectCode: String) { companion object { - val logger = loggerFor() + val logger = contextLogger() } - // The JIRA project must have a Request ID field and the Task issue type. + // The JIRA project must have a Request ID and reject reason field, and the Task issue type. private val requestIdField: Field = restClient.metadataClient.fields.claim().find { it.name == "Request ID" } ?: throw IllegalArgumentException("Request ID field not found in JIRA '$projectCode'") + private val rejectReasonField: Field = restClient.metadataClient.fields.claim().find { it.name == "Reject Reason" } ?: throw IllegalArgumentException("Reject Reason field not found in JIRA '$projectCode'") private val taskIssueType: IssueType = restClient.metadataClient.issueTypes.claim().find { it.name == "Task" } ?: throw IllegalArgumentException("Task issue type field not found in JIRA '$projectCode'") + private var doneTransitionId: Int = -1 + private var canceledTransitionId: Int = -1 + private var startProgressTransitionId: Int = -1 + fun createRequestTicket(requestId: String, signingRequest: PKCS10CertificationRequest) { // Check there isn't already a ticket for this request. if (getIssueById(requestId) != null) { @@ -54,14 +61,26 @@ class JiraClient(private val restClient: JiraRestClient, private val projectCode restClient.issueClient.createIssue(issue.build()).fail { logger.error("Exception when creating JIRA issue.", it) }.claim() } - fun getApprovedRequests(): List> { + fun getApprovedRequests(): List { val issues = restClient.searchClient.searchJql("project = $projectCode AND status = Approved").claim().issues - return issues.map { issue -> - issue.getField(requestIdField.id)?.value?.toString().let { - val requestId = it ?: throw IllegalArgumentException("RequestId cannot be null.") - val approvedBy = issue.assignee?.displayName ?: "Unknown" - Pair(requestId, approvedBy) - } + return issues.mapNotNull { issue -> + val requestId = issue.getField(requestIdField.id)?.value?.toString() ?: throw IllegalArgumentException("Error processing request '${issue.key}' : RequestId cannot be null.") + // Issue retrieved via search doesn't contain change logs. + val fullIssue = restClient.issueClient.getIssue(issue.key, listOf(IssueRestClient.Expandos.CHANGELOG)).claim() + val approvedBy = fullIssue.changelog?.last { it.items.any { it.field == "status" && it.toString == "Approved" } } + ApprovedRequest(requestId, approvedBy?.author?.displayName ?: "Unknown") + } + } + + fun getRejectedRequests(): List { + val issues = restClient.searchClient.searchJql("project = $projectCode AND status = Rejected").claim().issues + return issues.mapNotNull { issue -> + val requestId = issue.getField(requestIdField.id)?.value?.toString() ?: throw IllegalArgumentException("Error processing request '${issue.key}' : RequestId cannot be null.") + val rejectedReason = issue.getField(rejectReasonField.id)?.value?.toString() + // Issue retrieved via search doesn't contain comments. + val fullIssue = restClient.issueClient.getIssue(issue.key, listOf(IssueRestClient.Expandos.CHANGELOG)).claim() + val rejectedBy = fullIssue.changelog?.last { it.items.any { it.field == "status" && it.toString == "Rejected" } } + RejectedRequest(requestId, rejectedBy?.author?.displayName ?: "Unknown", rejectedReason) } } @@ -69,16 +88,41 @@ class JiraClient(private val restClient: JiraRestClient, private val projectCode // Retrieving certificates for signed CSRs to attach to the jira tasks. signedRequests.forEach { (id, certPath) -> val certificate = certPath.certificates.first() - // Jira only support ~ (contains) search for custom textfield. val issue = getIssueById(id) if (issue != null) { - restClient.issueClient.transition(issue, TransitionInput(doneTransitionCode)).fail { logger.error("Exception when transiting JIRA status.", it) }.claim() - restClient.issueClient.addAttachment(issue.attachmentsUri, certificate?.encoded?.inputStream(), "${X509Utilities.CORDA_CLIENT_CA}.cer") - .fail { logger.error("Exception when uploading attachment to JIRA.", it) }.claim() + if (doneTransitionId == -1) { + doneTransitionId = restClient.issueClient.getTransitions(issue.transitionsUri).claim().single { it.name == "Done" }.id + } + restClient.issueClient.transition(issue, TransitionInput(doneTransitionId)).fail { logger.error("Exception when transiting JIRA status.", it) }.claim() + restClient.issueClient.addAttachment(issue.attachmentsUri, certificate.encoded.inputStream(), "${X509Utilities.CORDA_CLIENT_CA}.cer") + .fail { logger.error("Error processing request '${issue.key}' : Exception when uploading attachment to JIRA.", it) }.claim() } } } - private fun getIssueById(requestId: String): Issue? = - restClient.searchClient.searchJql("'Request ID' ~ $requestId").claim().issues.firstOrNull() + fun updateRejectedRequests(rejectedRequests: List) { + rejectedRequests.mapNotNull { getIssueById(it) } + .forEach { issue -> + // Move status to in progress. + if (startProgressTransitionId == -1) { + startProgressTransitionId = restClient.issueClient.getTransitions(issue.transitionsUri).claim().single { it.name == "Start Progress" }.id + } + restClient.issueClient.transition(issue, TransitionInput(startProgressTransitionId)).fail { logger.error("Error processing request '${issue.key}' : Exception when transiting JIRA status.", it) }.claim() + // Move status to cancelled. + if (canceledTransitionId == -1) { + canceledTransitionId = restClient.issueClient.getTransitions(issue.transitionsUri).claim().single { it.name == "Stop Progress" }.id + } + restClient.issueClient.transition(issue, TransitionInput(canceledTransitionId)).fail { logger.error("Error processing request '${issue.key}' : Exception when transiting JIRA status.", it) }.claim() + restClient.issueClient.addComment(issue.commentsUri, Comment.valueOf("Request cancelled by doorman.")).claim() + } + } + + private fun getIssueById(requestId: String): Issue? { + // Jira only support ~ (contains) search for custom textfield. + return restClient.searchClient.searchJql("'Request ID' ~ $requestId").claim().issues.firstOrNull() + } } + +data class ApprovedRequest(val requestId: String, val approvedBy: String) + +data class RejectedRequest(val requestId: String, val rejectedBy: String, val reason: String?) diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/NetworkManagementServer.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/NetworkManagementServer.kt index 030d92ce36..919ca722be 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/NetworkManagementServer.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/NetworkManagementServer.kt @@ -91,7 +91,7 @@ class NetworkManagementServer : Closeable { val jiraConfig = config.jiraConfig val requestProcessor = if (jiraConfig != null) { val jiraWebAPI = AsynchronousJiraRestClientFactory().createWithBasicHttpAuthentication(URI(jiraConfig.address), jiraConfig.username, jiraConfig.password) - val jiraClient = JiraClient(jiraWebAPI, jiraConfig.projectCode, jiraConfig.doneTransitionCode) + val jiraClient = JiraClient(jiraWebAPI, jiraConfig.projectCode) JiraCsrHandler(jiraClient, requestService, DefaultCsrHandler(requestService, csrCertPathAndKey)) } else { DefaultCsrHandler(requestService, csrCertPathAndKey) @@ -101,10 +101,8 @@ class NetworkManagementServer : Closeable { val approvalThread = Runnable { try { serverStatus.lastRequestCheckTime = Instant.now() - // Create tickets for requests which don't have one yet. - requestProcessor.createTickets() // Process Jira approved tickets. - requestProcessor.processApprovedRequests() + requestProcessor.processRequests() } catch (e: Exception) { // Log the error and carry on. logger.error("Error encountered when approving request.", e) diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/CsrHandler.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/CsrHandler.kt index 207a118c88..2048459c75 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/CsrHandler.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/CsrHandler.kt @@ -18,25 +18,23 @@ import javax.security.auth.x500.X500Principal interface CsrHandler { fun saveRequest(rawRequest: PKCS10CertificationRequest): String - fun createTickets() - fun processApprovedRequests() + fun processRequests() fun getResponse(requestId: String): CertificateResponse } class DefaultCsrHandler(private val storage: CertificationRequestStorage, private val csrCertPathAndKey: CertPathAndKey?) : CsrHandler { - override fun processApprovedRequests() { + override fun processRequests() { if (csrCertPathAndKey == null) return storage.getRequests(RequestStatus.APPROVED).forEach { val nodeCertPath = createSignedNodeCertificate(it.request, csrCertPathAndKey) - // Since Doorman is deployed in the auto-signing mode, we use DOORMAN_SIGNATURE as the signer. + // Since Doorman is deployed in the auto-signing mode (i.e. signer != null), + // we use DOORMAN_SIGNATURE as the signer. storage.putCertificatePath(it.requestId, nodeCertPath, listOf(DOORMAN_SIGNATURE)) } } - override fun createTickets() {} - override fun saveRequest(rawRequest: PKCS10CertificationRequest): String = storage.saveRequest(rawRequest) override fun getResponse(requestId: String): CertificateResponse { diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/JiraCsrHandler.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/JiraCsrHandler.kt index e7f9d51046..b7bea5490e 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/JiraCsrHandler.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/JiraCsrHandler.kt @@ -4,7 +4,9 @@ import com.r3.corda.networkmanage.common.persistence.CertificateResponse import com.r3.corda.networkmanage.common.persistence.CertificateSigningRequest import com.r3.corda.networkmanage.common.persistence.CertificationRequestStorage import com.r3.corda.networkmanage.common.persistence.RequestStatus +import com.r3.corda.networkmanage.doorman.ApprovedRequest import com.r3.corda.networkmanage.doorman.JiraClient +import com.r3.corda.networkmanage.doorman.RejectedRequest import net.corda.core.utilities.contextLogger import net.corda.core.utilities.loggerFor import org.bouncycastle.pkcs.PKCS10CertificationRequest @@ -29,21 +31,34 @@ class JiraCsrHandler(private val jiraClient: JiraClient, private val storage: Ce } } - override fun processApprovedRequests() { + override fun processRequests() { + createTickets() + val (approvedRequests, rejectedRequests) = updateRequestStatus() + delegate.processRequests() + updateJiraTickets(approvedRequests, rejectedRequests) + } + + private fun updateRequestStatus(): Pair, List> { + // Update local request statuses. val approvedRequest = jiraClient.getApprovedRequests() approvedRequest.forEach { (id, approvedBy) -> storage.approveRequest(id, approvedBy) } - delegate.processApprovedRequests() + val rejectedRequest = jiraClient.getRejectedRequests() + rejectedRequest.forEach { (id, rejectedBy, reason) -> storage.rejectRequest(id, rejectedBy, reason) } + return Pair(approvedRequest, rejectedRequest) + } - val signedRequests = approvedRequest.mapNotNull { (id, _) -> - val request = storage.getRequest(id) - - if (request != null && request.status == RequestStatus.SIGNED) { - request.certData?.certPath?.let { certs -> id to certs } - } else { - null - } - }.toMap() + private fun updateJiraTickets(approvedRequest: List, rejectedRequest: List) { + // Reconfirm request status and update jira status + val signedRequests = approvedRequest.mapNotNull { storage.getRequest(it.requestId) } + .filter { it.status == RequestStatus.SIGNED && it.certData != null } + .associateBy { it.requestId } + .mapValues { it.value.certData!!.certPath } jiraClient.updateSignedRequests(signedRequests) + + val rejectedRequestIDs = rejectedRequest.mapNotNull { storage.getRequest(it.requestId) } + .filter { it.status == RequestStatus.REJECTED } + .map { it.requestId } + jiraClient.updateRejectedRequests(rejectedRequestIDs) } /** @@ -52,13 +67,13 @@ class JiraCsrHandler(private val jiraClient: JiraClient, private val storage: Ce * Usually requests are expected to move to the [RequestStatus.TICKET_CREATED] state immediately, * they might be left in the [RequestStatus.NEW] state if Jira is down. */ - override fun createTickets() { - try { - for (signingRequest in storage.getRequests(RequestStatus.NEW)) { - createTicket(signingRequest) + private fun createTickets() { + storage.getRequests(RequestStatus.NEW).forEach { + try { + createTicket(it) + } catch (e: Exception) { + log.warn("There were errors while creating Jira tickets for request '${it.requestId}'", e) } - } catch (e: Exception) { - log.warn("There were errors while creating Jira tickets", e) } } diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/webservice/NetworkMapWebService.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/webservice/NetworkMapWebService.kt index 93706599c5..cf881f2712 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/webservice/NetworkMapWebService.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/webservice/NetworkMapWebService.kt @@ -86,7 +86,6 @@ class NetworkMapWebService(private val nodeInfoStorage: NodeInfoStorage, @GET @Path("my-ip") fun myIp(@Context request: HttpServletRequest): Response { - // TODO: Verify this returns IP correctly. return ok(request.getHeader("X-Forwarded-For")?.split(",")?.first() ?: "${request.remoteHost}:${request.remotePort}").build() } diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/DoormanParametersTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/DoormanParametersTest.kt index d2b92b752a..d44ff5304a 100644 --- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/DoormanParametersTest.kt +++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/DoormanParametersTest.kt @@ -52,6 +52,5 @@ class DoormanParametersTest { assertEquals("TD", parameter.jiraConfig?.projectCode) assertEquals("username", parameter.jiraConfig?.username) assertEquals("password", parameter.jiraConfig?.password) - assertEquals(41, parameter.jiraConfig?.doneTransitionCode) } } diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/JiraClientTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/JiraClientTest.kt new file mode 100644 index 0000000000..d3558d0c6e --- /dev/null +++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/JiraClientTest.kt @@ -0,0 +1,55 @@ +package com.r3.corda.networkmanage.doorman + +import com.atlassian.jira.rest.client.internal.async.AsynchronousJiraRestClientFactory +import com.r3.corda.networkmanage.common.utils.buildCertPath +import net.corda.core.crypto.Crypto +import net.corda.core.crypto.SecureHash +import net.corda.core.identity.CordaX500Name +import net.corda.nodeapi.internal.crypto.X509Utilities +import org.junit.Before +import org.junit.Ignore +import org.junit.Test +import java.net.URI + +@Ignore +// This is manual test for testing Jira API. +class JiraClientTest { + private lateinit var jiraClient: JiraClient + @Before + fun init() { + val jiraWebAPI = AsynchronousJiraRestClientFactory().createWithBasicHttpAuthentication(URI("http://jira.url.com"), "username", "password") + jiraClient = JiraClient(jiraWebAPI, "DOOR") + } + + @Test + fun createRequestTicket() { + val request = X509Utilities.createCertificateSigningRequest(CordaX500Name("JiraAPITest", "R3 Ltd 3", "London", "GB").x500Principal, "test@test.com", Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)) + jiraClient.createRequestTicket(SecureHash.randomSHA256().toString(), request) + } + + @Test + fun getApprovedRequests() { + jiraClient.getApprovedRequests().forEach { println(it) } + } + + @Test + fun getRejectedRequests() { + val requests = jiraClient.getRejectedRequests() + requests.forEach { println(it) } + } + + @Test + fun updateSignedRequests() { + val requests = jiraClient.getApprovedRequests() + val selfSignedCA = X509Utilities.createSelfSignedCACertificate(CordaX500Name("test", "london", "GB").x500Principal, Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)) + jiraClient.updateSignedRequests(requests.map { it.requestId to buildCertPath(selfSignedCA) }.toMap()) + } + + @Test + fun updateRejectedRequests() { + val requests = jiraClient.getRejectedRequests() + jiraClient.updateRejectedRequests(requests.map { it.requestId }) + + assert(jiraClient.getRejectedRequests().isEmpty()) + } +} diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/signer/DefaultCsrHandlerTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/signer/DefaultCsrHandlerTest.kt index e518801bec..67af01bd64 100644 --- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/signer/DefaultCsrHandlerTest.kt +++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/signer/DefaultCsrHandlerTest.kt @@ -62,7 +62,7 @@ class DefaultCsrHandlerTest : TestBase() { val csrCertPathAndKey = CertPathAndKey(listOf(csrCa.certificate, rootCa.certificate), csrCa.keyPair.private) val requestProcessor = DefaultCsrHandler(requestStorage, csrCertPathAndKey) - requestProcessor.processApprovedRequests() + requestProcessor.processRequests() val certPathCapture = argumentCaptor() diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/signer/JiraCsrHandlerTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/signer/JiraCsrHandlerTest.kt index 19498bb744..02b6566774 100644 --- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/signer/JiraCsrHandlerTest.kt +++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/signer/JiraCsrHandlerTest.kt @@ -1,12 +1,12 @@ package com.r3.corda.networkmanage.doorman.signer import com.nhaarman.mockito_kotlin.* -import com.r3.corda.networkmanage.common.persistence.CertificateResponse -import com.r3.corda.networkmanage.common.persistence.CertificateSigningRequest -import com.r3.corda.networkmanage.common.persistence.CertificationRequestStorage -import com.r3.corda.networkmanage.common.persistence.RequestStatus +import com.r3.corda.networkmanage.common.persistence.* +import com.r3.corda.networkmanage.doorman.ApprovedRequest import com.r3.corda.networkmanage.doorman.JiraClient +import com.r3.corda.networkmanage.doorman.RejectedRequest import net.corda.core.crypto.Crypto +import net.corda.core.crypto.SecureHash import net.corda.core.identity.CordaX500Name import net.corda.nodeapi.internal.crypto.X509Utilities import org.junit.Before @@ -16,9 +16,9 @@ import org.mockito.Mock import org.mockito.junit.MockitoJUnit import org.mockito.junit.MockitoRule import java.security.cert.CertPath +import kotlin.test.assertEquals class JiraCsrHandlerTest { - @Rule @JvmField val mockitoRule: MockitoRule = MockitoJUnit.rule() @@ -80,9 +80,67 @@ class JiraCsrHandlerTest { whenever(certificationRequestStorage.getRequests(RequestStatus.NEW)).thenReturn(listOf(csr)) // Test - jiraCsrHandler.createTickets() + jiraCsrHandler.processRequests() verify(jiraClient).createRequestTicket(requestId, csr.request) verify(certificationRequestStorage).markRequestTicketCreated(requestId) } -} \ No newline at end of file + + @Test + fun `sync tickets status`() { + val id1 = SecureHash.randomSHA256().toString() + val id2 = SecureHash.randomSHA256().toString() + val csr1 = CertificateSigningRequest(id1, "name1", RequestStatus.NEW, pkcS10CertificationRequest, null, emptyList(), null) + val csr2 = CertificateSigningRequest(id2, "name2", RequestStatus.NEW, pkcS10CertificationRequest, null, emptyList(), null) + + val requests = mutableMapOf(id1 to csr1, id2 to csr2) + + // Mocking storage behaviour. + whenever(certificationRequestStorage.getRequests(RequestStatus.NEW)).thenReturn(requests.values.filter { it.status == RequestStatus.NEW }) + whenever(certificationRequestStorage.getRequest(any())).thenAnswer { requests[it.getArgument(0)] } + whenever(certificationRequestStorage.approveRequest(any(), any())).then { + val id = it.getArgument(0) + if (requests[id]?.status == RequestStatus.NEW) { + requests[id] = requests[id]!!.copy(status = RequestStatus.APPROVED, modifiedBy = listOf(it.getArgument(1))) + } + null + } + whenever(certificationRequestStorage.rejectRequest(any(), any(), any())).then { + val id = it.getArgument(0) + requests[id] = requests[id]!!.copy(status = RequestStatus.REJECTED, modifiedBy = listOf(it.getArgument(1)), remark = it.getArgument(2)) + null + } + + // Status change from jira. + whenever(jiraClient.getApprovedRequests()).thenReturn(listOf(ApprovedRequest(id1, "Me"))) + whenever(jiraClient.getRejectedRequests()).thenReturn(listOf(RejectedRequest(id2, "Me", "Test reject"))) + + // Test. + jiraCsrHandler.processRequests() + + verify(jiraClient).createRequestTicket(id1, csr1.request) + verify(jiraClient).createRequestTicket(id2, csr2.request) + + verify(certificationRequestStorage).markRequestTicketCreated(id1) + verify(certificationRequestStorage).markRequestTicketCreated(id2) + + // Verify request has the correct status in DB. + assertEquals(RequestStatus.APPROVED, requests[id1]!!.status) + assertEquals(RequestStatus.REJECTED, requests[id2]!!.status) + + // Verify jira client get the correct call. + verify(jiraClient).updateRejectedRequests(listOf(id2)) + verify(jiraClient).updateSignedRequests(emptyMap()) + + // Sign request 1 + val certPath = mock() + val certData = CertificateData("", CertificateStatus.VALID, certPath) + requests[id1] = requests[id1]!!.copy(status = RequestStatus.SIGNED, certData = certData) + + // Process request again. + jiraCsrHandler.processRequests() + + // Update signed request should be called. + verify(jiraClient).updateSignedRequests(mapOf(id1 to certPath)) + } +} diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/webservice/RegistrationWebServiceTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/webservice/RegistrationWebServiceTest.kt index d9821f38fc..ead1e2f91a 100644 --- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/webservice/RegistrationWebServiceTest.kt +++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/webservice/RegistrationWebServiceTest.kt @@ -100,7 +100,7 @@ class RegistrationWebServiceTest : TestBase() { CertificateResponse.Ready(it) } ?: CertificateResponse.NotReady } - on { processApprovedRequests() }.then { + on { processRequests() }.then { val request = X509Utilities.createCertificateSigningRequest(subject, "my@mail.com", keyPair) certificateStore[id] = JcaPKCS10CertificationRequest(request).run { val tlsCert = X509Utilities.createCertificate( @@ -118,7 +118,7 @@ class RegistrationWebServiceTest : TestBase() { startSigningServer(requestProcessor) assertThat(pollForResponse(id)).isEqualTo(PollResponse.NotReady) - requestProcessor.processApprovedRequests() + requestProcessor.processRequests() val certificates = (pollForResponse(id) as PollResponse.Ready).certChain verify(requestProcessor, times(2)).getResponse(any()) @@ -141,7 +141,7 @@ class RegistrationWebServiceTest : TestBase() { CertificateResponse.Ready(it) } ?: CertificateResponse.NotReady } - on { processApprovedRequests() }.then { + on { processRequests() }.then { val request = X509Utilities.createCertificateSigningRequest( CordaX500Name(locality = "London", organisation = "Legal Name", country = "GB").x500Principal, "my@mail.com", @@ -165,7 +165,7 @@ class RegistrationWebServiceTest : TestBase() { startSigningServer(storage) assertThat(pollForResponse(id)).isEqualTo(PollResponse.NotReady) - storage.processApprovedRequests() + storage.processRequests() val certificates = (pollForResponse(id) as PollResponse.Ready).certChain verify(storage, times(2)).getResponse(any()) From cee975c1c15614785f3a0faac73957ee7a499587 Mon Sep 17 00:00:00 2001 From: Michal Kit Date: Mon, 22 Jan 2018 12:11:31 +0000 Subject: [PATCH 19/20] =?UTF-8?q?Reverting=20schema=20changes=20and=20foll?= =?UTF-8?q?owing=20the=20liquibase=20change=20tracking=20=E2=80=A6=20(#385?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Reverting schema changes and following the liquibase change tracking flow * Addressing review comments * Adding other tables cleanup * Renaming the changeset file --- .../network-manager.changelog-init.xml | 15 +++-------- .../network-manager.changelog-master.xml | 1 + ...nager.changelog-signing-network-params.xml | 27 +++++++++++++++++++ 3 files changed, 32 insertions(+), 11 deletions(-) create mode 100644 network-management/src/main/resources/migration/network-manager.changelog-signing-network-params.xml diff --git a/network-management/src/main/resources/migration/network-manager.changelog-init.xml b/network-management/src/main/resources/migration/network-manager.changelog-init.xml index 0b9be92b91..59a5f4985e 100644 --- a/network-management/src/main/resources/migration/network-manager.changelog-init.xml +++ b/network-management/src/main/resources/migration/network-manager.changelog-init.xml @@ -93,13 +93,11 @@ - + - - - - + + @@ -122,7 +120,7 @@ - + @@ -169,11 +167,6 @@ - - - - - + diff --git a/network-management/src/main/resources/migration/network-manager.changelog-signing-network-params.xml b/network-management/src/main/resources/migration/network-manager.changelog-signing-network-params.xml new file mode 100644 index 0000000000..cdb43a5362 --- /dev/null +++ b/network-management/src/main/resources/migration/network-manager.changelog-signing-network-params.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + From e1098dee4b804ff196f4544b575bb5185b8358c8 Mon Sep 17 00:00:00 2001 From: Michal Kit Date: Mon, 22 Jan 2018 14:10:33 +0000 Subject: [PATCH 20/20] Adding public key constraint (#319) * ENT-1356 Adding public key constraint * Addressing review comments * Removing SERIALIZABLE from transaction * Adding stashed changes * Removing SERIALIZABLE from node info storage * Addressing review comments * Addressing possible certificate inconsitency (design gap) + clearing whole database for new liquibase changeset * Addressing review comments --- .../CertificationRequestStorage.kt | 4 +- .../PersistentCertificateRequestStorage.kt | 27 ++++++-- .../persistence/PersistentNodeInfoStorage.kt | 36 +++++----- .../common/persistence/SchemaService.kt | 0 .../entity/CertificateSigningRequestEntity.kt | 15 ++-- .../network-manager.changelog-master.xml | 1 + ...network-manager.changelog-pub-key-move.xml | 22 ++++++ .../com/r3/corda/networkmanage/TestBase.kt | 6 +- ...PersistentCertificateRequestStorageTest.kt | 68 ++++++++++++------- .../PersistentNetworkMapStorageTest.kt | 19 +----- .../PersistentNodeInfoStorageTest.kt | 33 ++++----- .../doorman/signer/DefaultCsrHandlerTest.kt | 2 +- .../doorman/signer/JiraCsrHandlerTest.kt | 15 ++-- .../nodeapi/internal/KeyStoreConfigHelpers.kt | 13 ++-- .../testing/internal/InternalTestUtils.kt | 5 +- .../testing/internal/TestNodeInfoBuilder.kt | 29 ++++++-- 16 files changed, 186 insertions(+), 109 deletions(-) delete mode 100644 network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/SchemaService.kt create mode 100644 network-management/src/main/resources/migration/network-manager.changelog-pub-key-move.xml diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/CertificationRequestStorage.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/CertificationRequestStorage.kt index 9bc8d2e9f7..f0f8280406 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/CertificationRequestStorage.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/CertificationRequestStorage.kt @@ -1,12 +1,14 @@ package com.r3.corda.networkmanage.common.persistence +import net.corda.core.crypto.SecureHash import org.bouncycastle.pkcs.PKCS10CertificationRequest import java.security.cert.CertPath -data class CertificateData(val publicKeyHash: String, val certStatus: CertificateStatus, val certPath: CertPath) +data class CertificateData(val certStatus: CertificateStatus, val certPath: CertPath) data class CertificateSigningRequest(val requestId: String, val legalName: String, + val publicKeyHash: SecureHash, val status: RequestStatus, val request: PKCS10CertificationRequest, val remark: String?, diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRequestStorage.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRequestStorage.kt index f1511ff33f..5a4b1011bf 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRequestStorage.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRequestStorage.kt @@ -3,6 +3,7 @@ package com.r3.corda.networkmanage.common.persistence import com.r3.corda.networkmanage.common.persistence.entity.CertificateDataEntity import com.r3.corda.networkmanage.common.persistence.entity.CertificateSigningRequestEntity import com.r3.corda.networkmanage.common.utils.hashString +import net.corda.core.crypto.Crypto.toSupportedPublicKey import net.corda.core.crypto.SecureHash import net.corda.core.identity.CordaX500Name import net.corda.nodeapi.internal.persistence.CordaPersistence @@ -26,14 +27,12 @@ class PersistentCertificateRequestStorage(private val database: CordaPersistence builder.and(requestIdEq, statusEq) } request ?: throw IllegalArgumentException("Cannot retrieve 'APPROVED' certificate signing request for request id: $requestId") - val publicKeyHash = certificates.certificates.first().publicKey.hashString() val certificateSigningRequest = request.copy( modifiedBy = signedBy, modifiedAt = Instant.now(), status = RequestStatus.SIGNED) session.merge(certificateSigningRequest) val certificateDataEntity = CertificateDataEntity( - publicKeyHash = publicKeyHash, certificateStatus = CertificateStatus.VALID, certificatePathBytes = certificates.encoded, certificateSigningRequest = certificateSigningRequest) @@ -48,6 +47,7 @@ class PersistentCertificateRequestStorage(private val database: CordaPersistence session.save(CertificateSigningRequestEntity( requestId = requestId, legalName = legalName, + publicKeyHash = toSupportedPublicKey(request.subjectPublicKeyInfo).hashString(), requestBytes = request.encoded, remark = rejectReason, modifiedBy = emptyList(), @@ -134,7 +134,7 @@ class PersistentCertificateRequestStorage(private val database: CordaPersistence return Pair(request.subject.toString(), "Name validation failed: ${e.message}") } - val query = session.criteriaBuilder.run { + val duplicateNameQuery = session.criteriaBuilder.run { val criteriaQuery = createQuery(CertificateSigningRequestEntity::class.java) criteriaQuery.from(CertificateSigningRequestEntity::class.java).run { criteriaQuery.where(equal(get(CertificateSigningRequestEntity::legalName.name), legalName)) @@ -144,10 +144,27 @@ class PersistentCertificateRequestStorage(private val database: CordaPersistence // TODO consider scenario: There is a CSR that is signed but the certificate itself has expired or was revoked // Also, at the moment we assume that once the CSR is approved it cannot be rejected. // What if we approved something by mistake. - val duplicates = session.createQuery(query).resultList.filter { + val nameDuplicates = session.createQuery(duplicateNameQuery).resultList.filter { it.status != RequestStatus.REJECTED } - return Pair(legalName, if (duplicates.isEmpty()) null else "Duplicate legal name") + if (nameDuplicates.isNotEmpty()) { + return Pair(legalName, "Duplicate legal name") + } + + val publicKey = toSupportedPublicKey(request.subjectPublicKeyInfo).hashString() + val duplicatePkQuery = session.criteriaBuilder.run { + val criteriaQuery = createQuery(CertificateSigningRequestEntity::class.java) + criteriaQuery.from(CertificateSigningRequestEntity::class.java).run { + criteriaQuery.where(equal(get(CertificateSigningRequestEntity::publicKeyHash.name), publicKey)) + } + } + + //TODO Consider following scenario: There is a CSR that is signed but the certificate itself has expired or was revoked + val pkDuplicates = session.createQuery(duplicatePkQuery).resultList.filter { + it.status != RequestStatus.REJECTED + } + + return Pair(legalName, if (pkDuplicates.isEmpty()) null else "Duplicate public key") } } \ No newline at end of file diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNodeInfoStorage.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNodeInfoStorage.kt index 5c8d249eac..4d97e79629 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNodeInfoStorage.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNodeInfoStorage.kt @@ -1,6 +1,5 @@ package com.r3.corda.networkmanage.common.persistence -import com.r3.corda.networkmanage.common.persistence.entity.CertificateDataEntity import com.r3.corda.networkmanage.common.persistence.entity.CertificateSigningRequestEntity import com.r3.corda.networkmanage.common.persistence.entity.NodeInfoEntity import com.r3.corda.networkmanage.common.utils.buildCertPath @@ -10,7 +9,7 @@ import net.corda.core.internal.CertRole import net.corda.core.serialization.serialize import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.persistence.CordaPersistence -import net.corda.nodeapi.internal.persistence.TransactionIsolationLevel +import net.corda.nodeapi.internal.persistence.DatabaseTransaction import java.security.cert.CertPath /** @@ -21,16 +20,13 @@ class PersistentNodeInfoStorage(private val database: CordaPersistence) : NodeIn val nodeInfo = nodeInfoWithSigned.nodeInfo val signedNodeInfo = nodeInfoWithSigned.signedNodeInfo val nodeCaCert = nodeInfo.legalIdentitiesAndCerts[0].certPath.certificates.find { CertRole.extract(it) == CertRole.NODE_CA } - return database.transaction(TransactionIsolationLevel.SERIALIZABLE) { + return database.transaction { // TODO Move these checks out of data access layer val request = nodeCaCert?.let { - singleRequestWhere(CertificateDataEntity::class.java) { builder, path -> - val certPublicKeyHashEq = builder.equal(path.get(CertificateDataEntity::publicKeyHash.name), it.publicKey.encoded.sha256().toString()) - val certStatusValid = builder.equal(path.get(CertificateDataEntity::certificateStatus.name), CertificateStatus.VALID) - builder.and(certPublicKeyHashEq, certStatusValid) - } + getSignedRequestByPublicHash(it.publicKey.encoded.sha256(), this) } request ?: throw IllegalArgumentException("Unknown node info, this public key is not registered with the network management service.") + require(request.certificateData!!.certificateStatus == CertificateStatus.VALID) { "Certificate is no longer valid" } /* * Delete any previous [NodeInfoEntity] instance for this CSR @@ -40,13 +36,13 @@ class PersistentNodeInfoStorage(private val database: CordaPersistence) : NodeIn * but it has been confirmed that this fact has been acknowledged at the design time and we are fine with it. */ deleteRequest(NodeInfoEntity::class.java) { builder, path -> - builder.equal(path.get(NodeInfoEntity::certificateSigningRequest.name), request.certificateSigningRequest) + builder.equal(path.get(NodeInfoEntity::certificateSigningRequest.name), request) } val hash = signedNodeInfo.raw.hash val hashedNodeInfo = NodeInfoEntity( nodeInfoHash = hash.toString(), - certificateSigningRequest = request.certificateSigningRequest, + certificateSigningRequest = request, signedNodeInfoBytes = signedNodeInfo.serialize().bytes) session.save(hashedNodeInfo) hash @@ -61,16 +57,16 @@ class PersistentNodeInfoStorage(private val database: CordaPersistence) : NodeIn override fun getCertificatePath(publicKeyHash: SecureHash): CertPath? { return database.transaction { - val builder = session.criteriaBuilder - val query = builder.createQuery(ByteArray::class.java).run { - from(CertificateSigningRequestEntity::class.java).run { - select(get(CertificateSigningRequestEntity::certificateData.name) - .get(CertificateDataEntity::certificatePathBytes.name)) - where(builder.equal(get(CertificateSigningRequestEntity::certificateData.name) - .get(CertificateDataEntity::publicKeyHash.name), publicKeyHash.toString())) - } - } - session.createQuery(query).uniqueResultOptional().orElseGet { null }?.let { buildCertPath(it) } + val request = getSignedRequestByPublicHash(publicKeyHash, this) + request?.let { buildCertPath(it.certificateData!!.certificatePathBytes) } + } + } + + private fun getSignedRequestByPublicHash(publicKeyHash: SecureHash, transaction: DatabaseTransaction): CertificateSigningRequestEntity? { + return transaction.singleRequestWhere(CertificateSigningRequestEntity::class.java) { builder, path -> + val publicKeyEq = builder.equal(path.get(CertificateSigningRequestEntity::publicKeyHash.name), publicKeyHash.toString()) + val statusEq = builder.equal(path.get(CertificateSigningRequestEntity::status.name), RequestStatus.SIGNED) + builder.and(publicKeyEq, statusEq) } } } \ No newline at end of file diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/SchemaService.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/SchemaService.kt deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/entity/CertificateSigningRequestEntity.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/entity/CertificateSigningRequestEntity.kt index 40f44c6599..aa803cdef2 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/entity/CertificateSigningRequestEntity.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/entity/CertificateSigningRequestEntity.kt @@ -4,6 +4,7 @@ import com.r3.corda.networkmanage.common.persistence.CertificateData import com.r3.corda.networkmanage.common.persistence.CertificateSigningRequest import com.r3.corda.networkmanage.common.persistence.CertificateStatus import com.r3.corda.networkmanage.common.persistence.RequestStatus +import net.corda.core.crypto.SecureHash import org.bouncycastle.pkcs.PKCS10CertificationRequest import org.hibernate.envers.Audited import java.security.cert.CertPath @@ -12,7 +13,7 @@ import java.time.Instant import javax.persistence.* @Entity -@Table(name = "certificate_signing_request") +@Table(name = "certificate_signing_request", indexes = arrayOf(Index(name = "IDX_PUB_KEY_HASH", columnList = "public_key_hash"))) class CertificateSigningRequestEntity( @Id @Column(name = "request_id", length = 64) @@ -22,6 +23,9 @@ class CertificateSigningRequestEntity( @Column(name = "legal_name", length = 256, nullable = false) val legalName: String, + @Column(name = "public_key_hash", length = 64) + val publicKeyHash: String, + @Audited @Column(name = "status", nullable = false) @Enumerated(EnumType.STRING) @@ -50,6 +54,7 @@ class CertificateSigningRequestEntity( fun toCertificateSigningRequest() = CertificateSigningRequest( requestId = requestId, legalName = legalName, + publicKeyHash = SecureHash.parse(publicKeyHash), status = status, request = request(), remark = remark, @@ -59,6 +64,7 @@ class CertificateSigningRequestEntity( fun copy(requestId: String = this.requestId, legalName: String = this.legalName, + publicKeyHash: String = this.publicKeyHash, status: RequestStatus = this.status, modifiedBy: List = this.modifiedBy, modifiedAt: Instant = this.modifiedAt, @@ -69,6 +75,7 @@ class CertificateSigningRequestEntity( return CertificateSigningRequestEntity( requestId = requestId, legalName = legalName, + publicKeyHash = publicKeyHash, status = status, modifiedAt = modifiedAt, modifiedBy = modifiedBy, @@ -82,16 +89,13 @@ class CertificateSigningRequestEntity( } @Entity -@Table(name = "certificate_data", indexes = arrayOf(Index(name = "IDX_PUB_KEY_HASH", columnList = "public_key_hash"))) +@Table(name = "certificate_data") class CertificateDataEntity( @Id @GeneratedValue(strategy = GenerationType.SEQUENCE) val id: Long? = null, - @Column(name = "public_key_hash", length = 64) - val publicKeyHash: String, - @Column(name = "certificate_status") val certificateStatus: CertificateStatus, @@ -105,7 +109,6 @@ class CertificateDataEntity( ) { fun toCertificateData(): CertificateData { return CertificateData( - publicKeyHash = publicKeyHash, certStatus = certificateStatus, certPath = toCertificatePath() ) diff --git a/network-management/src/main/resources/migration/network-manager.changelog-master.xml b/network-management/src/main/resources/migration/network-manager.changelog-master.xml index c6eff52f8a..f782ed4640 100644 --- a/network-management/src/main/resources/migration/network-manager.changelog-master.xml +++ b/network-management/src/main/resources/migration/network-manager.changelog-master.xml @@ -3,5 +3,6 @@ + diff --git a/network-management/src/main/resources/migration/network-manager.changelog-pub-key-move.xml b/network-management/src/main/resources/migration/network-manager.changelog-pub-key-move.xml new file mode 100644 index 0000000000..3e5625b441 --- /dev/null +++ b/network-management/src/main/resources/migration/network-manager.changelog-pub-key-move.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/TestBase.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/TestBase.kt index a7e37bc829..abc8b28401 100644 --- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/TestBase.kt +++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/TestBase.kt @@ -20,6 +20,7 @@ abstract class TestBase { requestId: String = SecureHash.randomSHA256().toString(), status: RequestStatus = RequestStatus.NEW, legalName: String = "TestLegalName", + publicKeyHash: SecureHash = SecureHash.randomSHA256(), remark: String = "Test remark", request: PKCS10CertificationRequest = mock(), certData: CertificateData = mock(), @@ -29,6 +30,7 @@ abstract class TestBase { requestId = requestId, status = status, legalName = legalName, + publicKeyHash = publicKeyHash, remark = remark, certData = certData, request = request, @@ -36,11 +38,9 @@ abstract class TestBase { ) } - protected fun certificateData(publicKeyHash: String = SecureHash.randomSHA256().toString(), - certStatus: CertificateStatus = CertificateStatus.VALID, + protected fun certificateData(certStatus: CertificateStatus = CertificateStatus.VALID, certPath: CertPath = mock()): CertificateData { return CertificateData( - publicKeyHash = publicKeyHash, certStatus = certStatus, certPath = certPath ) diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRequestStorageTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRequestStorageTest.kt index ec16998e8f..cfe2541c58 100644 --- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRequestStorageTest.kt +++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRequestStorageTest.kt @@ -19,6 +19,7 @@ import org.junit.After import org.junit.Before import org.junit.Test import java.security.KeyPair +import java.security.cert.CertPath import java.util.* import javax.security.auth.x500.X500Principal import kotlin.test.* @@ -69,7 +70,7 @@ class PersistentCertificateRequestStorageTest : TestBase() { @Test fun `sign request`() { - val (csr, _) = createRequest("LegalName") + val (csr, nodeKeyPair) = createRequest("LegalName") // Add request to DB. val requestId = storage.saveRequest(csr) // New request should equals to 1. @@ -86,11 +87,7 @@ class PersistentCertificateRequestStorageTest : TestBase() { // Sign certificate storage.putCertificatePath( requestId, - JcaPKCS10CertificationRequest(csr).run { - // TODO We need a utility in InternalUtils for converting X500Name -> CordaX500Name - val (rootCa, intermediateCa, nodeCa) = createDevNodeCaCertPath(CordaX500Name.build(X500Principal(subject.encoded))) - buildCertPath(nodeCa.certificate, intermediateCa.certificate, rootCa.certificate) - }, + generateSignedCertPath(csr, nodeKeyPair), listOf(DOORMAN_SIGNATURE) ) // Check request is ready @@ -99,33 +96,51 @@ class PersistentCertificateRequestStorageTest : TestBase() { @Test fun `sign request ignores subsequent sign requests`() { - val (csr, _) = createRequest("LegalName") + val (csr, nodeKeyPair) = createRequest("LegalName") // Add request to DB. val requestId = storage.saveRequest(csr) // Store certificate to DB. storage.markRequestTicketCreated(requestId) storage.approveRequest(requestId, DOORMAN_SIGNATURE) + // Sign certificate storage.putCertificatePath( requestId, - JcaPKCS10CertificationRequest(csr).run { - val (rootCa, intermediateCa, nodeCa) = createDevNodeCaCertPath(CordaX500Name.build(X500Principal(subject.encoded))) - buildCertPath(nodeCa.certificate, intermediateCa.certificate, rootCa.certificate) - }, + generateSignedCertPath(csr, nodeKeyPair), listOf(DOORMAN_SIGNATURE) ) - // Sign certificate // When subsequent signature requested assertFailsWith(IllegalArgumentException::class) { storage.putCertificatePath( requestId, - JcaPKCS10CertificationRequest(csr).run { - val (rootCa, intermediateCa, nodeCa) = createDevNodeCaCertPath(CordaX500Name.build(X500Principal(subject.encoded))) - buildCertPath(nodeCa.certificate, intermediateCa.certificate, rootCa.certificate) - }, + generateSignedCertPath(csr, nodeKeyPair), listOf(DOORMAN_SIGNATURE)) } } + @Test + fun `sign request rejects requests with the same public key`() { + val (csr, nodeKeyPair) = createRequest("LegalName") + // Add request to DB. + val requestId = storage.saveRequest(csr) + // Store certificate to DB. + storage.markRequestTicketCreated(requestId) + storage.approveRequest(requestId, DOORMAN_SIGNATURE) + // Sign certificate + storage.putCertificatePath( + requestId, + generateSignedCertPath(csr, nodeKeyPair), + listOf(DOORMAN_SIGNATURE) + ) + // Sign certificate + // When request with the same public key is requested + val (newCsr, _) = createRequest("NewLegalName", nodeKeyPair) + val duplicateRequestId = storage.saveRequest(newCsr) + assertThat(storage.getRequests(RequestStatus.NEW)).isEmpty() + val duplicateRequest = storage.getRequest(duplicateRequestId) + assertThat(duplicateRequest!!.status).isEqualTo(RequestStatus.REJECTED) + assertThat(duplicateRequest.remark).isEqualTo("Duplicate public key") + } + @Test fun `reject request`() { val requestId = storage.saveRequest(createRequest("BankA").first) @@ -159,18 +174,14 @@ class PersistentCertificateRequestStorageTest : TestBase() { @Test fun `request with the same legal name as a previously signed request`() { - val csr = createRequest("BankA").first + val (csr, nodeKeyPair) = createRequest("BankA") val requestId = storage.saveRequest(csr) storage.markRequestTicketCreated(requestId) storage.approveRequest(requestId, DOORMAN_SIGNATURE) // Sign certificate storage.putCertificatePath( requestId, - JcaPKCS10CertificationRequest(csr).run { - // TODO We need a utility in InternalUtils for converting X500Name -> CordaX500Name - val (rootCa, intermediateCa, nodeCa) = createDevNodeCaCertPath(CordaX500Name.build(X500Principal(subject.encoded))) - buildCertPath(nodeCa.certificate, intermediateCa.certificate, rootCa.certificate) - }, + generateSignedCertPath(csr, nodeKeyPair), listOf(DOORMAN_SIGNATURE) ) val rejectedRequestId = storage.saveRequest(createRequest("BankA").first) @@ -215,6 +226,14 @@ class PersistentCertificateRequestStorageTest : TestBase() { } } + private fun generateSignedCertPath(csr: PKCS10CertificationRequest, keyPair: KeyPair): CertPath { + return JcaPKCS10CertificationRequest(csr).run { + // TODO We need a utility in InternalUtils for converting X500Name -> CordaX500Name + val (rootCa, intermediateCa, nodeCa) = createDevNodeCaCertPath(CordaX500Name.build(X500Principal(subject.encoded)), keyPair) + buildCertPath(nodeCa.certificate, intermediateCa.certificate, rootCa.certificate) + } + } + private fun makeTestDataSourceProperties(nodeName: String = SecureHash.randomSHA256().toString()): Properties { val props = Properties() props.setProperty("dataSourceClassName", "org.h2.jdbcx.JdbcDataSource") @@ -225,8 +244,7 @@ class PersistentCertificateRequestStorageTest : TestBase() { } } -internal fun createRequest(organisation: String): Pair { - val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) +internal fun createRequest(organisation: String, keyPair: KeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)): Pair { val request = X509Utilities.createCertificateSigningRequest(X500Principal("O=$organisation,L=London,C=GB"), "my@mail.com", keyPair) return Pair(request, keyPair) -} +} \ No newline at end of file diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNetworkMapStorageTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNetworkMapStorageTest.kt index 77d041da43..cbc79e1902 100644 --- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNetworkMapStorageTest.kt +++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNetworkMapStorageTest.kt @@ -5,13 +5,11 @@ import net.corda.core.identity.CordaX500Name import net.corda.core.internal.signWithCert import net.corda.nodeapi.internal.createDevNetworkMapCa import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair -import net.corda.nodeapi.internal.crypto.X509CertificateFactory import net.corda.nodeapi.internal.network.NetworkMap import net.corda.nodeapi.internal.network.verifiedNetworkMapCert import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.testing.common.internal.testNetworkParameters -import net.corda.testing.internal.TestNodeInfoBuilder import net.corda.testing.internal.createDevIntermediateCaCertPath import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties import org.assertj.core.api.Assertions.assertThat @@ -50,7 +48,7 @@ class PersistentNetworkMapStorageTest : TestBase() { fun `saveNetworkMap and saveNetworkParameters create current network map and parameters`() { // given // Create node info. - val signedNodeInfo = createValidSignedNodeInfo("Test") + val (signedNodeInfo) = createValidSignedNodeInfo("Test", requestStorage) val nodeInfoHash = nodeInfoStorage.putNodeInfo(signedNodeInfo) val networkParameters = testNetworkParameters(emptyList()) @@ -117,8 +115,8 @@ class PersistentNetworkMapStorageTest : TestBase() { fun `getValidNodeInfoHashes returns only valid and signed node info hashes`() { // given // Create node infos. - val signedNodeInfoA = createValidSignedNodeInfo("TestA") - val signedNodeInfoB = createValidSignedNodeInfo("TestB") + val (signedNodeInfoA) = createValidSignedNodeInfo("TestA", requestStorage) + val (signedNodeInfoB) = createValidSignedNodeInfo("TestB", requestStorage) // Put signed node info data val nodeInfoHashA = nodeInfoStorage.putNodeInfo(signedNodeInfoA) @@ -139,15 +137,4 @@ class PersistentNetworkMapStorageTest : TestBase() { // then assertThat(validNodeInfoHash).containsOnly(nodeInfoHashA, nodeInfoHashB) } - - private fun createValidSignedNodeInfo(organisation: String): NodeInfoWithSigned { - val nodeInfoBuilder = TestNodeInfoBuilder() - val requestId = requestStorage.saveRequest(createRequest(organisation).first) - requestStorage.markRequestTicketCreated(requestId) - requestStorage.approveRequest(requestId, "TestUser") - val (identity) = nodeInfoBuilder.addIdentity(CordaX500Name(organisation, "London", "GB")) - val nodeCaCertPath = X509CertificateFactory().generateCertPath(identity.certPath.certificates.drop(1)) - requestStorage.putCertificatePath(requestId, nodeCaCertPath, emptyList()) - return NodeInfoWithSigned(nodeInfoBuilder.buildWithSigned().second) - } } \ No newline at end of file diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNodeInfoStorageTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNodeInfoStorageTest.kt index 01eba0be7a..47260c73d1 100644 --- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNodeInfoStorageTest.kt +++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentNodeInfoStorageTest.kt @@ -9,7 +9,6 @@ import net.corda.core.identity.CordaX500Name import net.corda.core.serialization.serialize import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair import net.corda.nodeapi.internal.crypto.CertificateType -import net.corda.nodeapi.internal.crypto.X509CertificateFactory import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig @@ -23,6 +22,7 @@ import org.junit.Before import org.junit.Test import java.security.PrivateKey import java.security.cert.X509Certificate +import javax.security.auth.x500.X500Principal import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertNull @@ -83,8 +83,8 @@ class PersistentNodeInfoStorageTest : TestBase() { @Test fun `getNodeInfo returns persisted SignedNodeInfo using the hash of just the NodeInfo`() { // given - val (nodeA) = createValidSignedNodeInfo("TestA") - val (nodeB) = createValidSignedNodeInfo("TestB") + val (nodeA) = createValidSignedNodeInfo("TestA", requestStorage) + val (nodeB) = createValidSignedNodeInfo("TestB", requestStorage) // Put signed node info data nodeInfoStorage.putNodeInfo(nodeA) @@ -102,7 +102,7 @@ class PersistentNodeInfoStorageTest : TestBase() { @Test fun `same public key with different node info`() { // Create node info. - val (node1, key) = createValidSignedNodeInfo("Test", serial = 1) + val (node1, key) = createValidSignedNodeInfo("Test", requestStorage) val nodeInfo2 = node1.nodeInfo.copy(serial = 2) val node2 = NodeInfoWithSigned(nodeInfo2.signWith(listOf(key))) @@ -120,7 +120,7 @@ class PersistentNodeInfoStorageTest : TestBase() { @Test fun `putNodeInfo persists SignedNodeInfo with its signature`() { // given - val (nodeInfoWithSigned) = createValidSignedNodeInfo("Test") + val (nodeInfoWithSigned) = createValidSignedNodeInfo("Test", requestStorage) // when val nodeInfoHash = nodeInfoStorage.putNodeInfo(nodeInfoWithSigned) @@ -129,16 +129,17 @@ class PersistentNodeInfoStorageTest : TestBase() { val persistedSignedNodeInfo = nodeInfoStorage.getNodeInfo(nodeInfoHash) assertThat(persistedSignedNodeInfo?.signatures).isEqualTo(nodeInfoWithSigned.signedNodeInfo.signatures) } +} - private fun createValidSignedNodeInfo(organisation: String, serial: Long = 1): Pair { - val nodeInfoBuilder = TestNodeInfoBuilder() - val requestId = requestStorage.saveRequest(createRequest(organisation).first) - requestStorage.markRequestTicketCreated(requestId) - requestStorage.approveRequest(requestId, "TestUser") - val (identity, key) = nodeInfoBuilder.addIdentity(CordaX500Name(organisation, "London", "GB")) - val nodeCaCertPath = X509CertificateFactory().generateCertPath(identity.certPath.certificates.drop(1)) - requestStorage.putCertificatePath(requestId, nodeCaCertPath, emptyList()) - val (_, signedNodeInfo) = nodeInfoBuilder.buildWithSigned(serial) - return Pair(NodeInfoWithSigned(signedNodeInfo), key) - } +internal fun createValidSignedNodeInfo(organisation: String, + storage: CertificationRequestStorage): Pair { + val (csr, nodeKeyPair) = createRequest(organisation) + val requestId = storage.saveRequest(csr) + storage.markRequestTicketCreated(requestId) + storage.approveRequest(requestId, "TestUser") + val nodeInfoBuilder = TestNodeInfoBuilder() + val (identity, key) = nodeInfoBuilder.addIdentity(CordaX500Name.build(X500Principal(csr.subject.encoded)), nodeKeyPair) + storage.putCertificatePath(requestId, identity.certPath, listOf("Test")) + val (_, signedNodeInfo) = nodeInfoBuilder.buildWithSigned(1) + return Pair(NodeInfoWithSigned(signedNodeInfo), key) } \ No newline at end of file diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/signer/DefaultCsrHandlerTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/signer/DefaultCsrHandlerTest.kt index 67af01bd64..221d7aec11 100644 --- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/signer/DefaultCsrHandlerTest.kt +++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/signer/DefaultCsrHandlerTest.kt @@ -28,7 +28,7 @@ class DefaultCsrHandlerTest : TestBase() { val requestStorage: CertificationRequestStorage = mock { on { getRequest("New") }.thenReturn(certificateSigningRequest()) - on { getRequest("Signed") }.thenReturn(certificateSigningRequest(status = RequestStatus.SIGNED, certData = certificateData("", CertificateStatus.VALID, buildCertPath(cert)))) + on { getRequest("Signed") }.thenReturn(certificateSigningRequest(status = RequestStatus.SIGNED, certData = certificateData(CertificateStatus.VALID, buildCertPath(cert)))) on { getRequest("Rejected") }.thenReturn(certificateSigningRequest(status = RequestStatus.REJECTED, remark = "Random reason")) } val requestProcessor = DefaultCsrHandler(requestStorage, null) diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/signer/JiraCsrHandlerTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/signer/JiraCsrHandlerTest.kt index 02b6566774..7a9d18d92f 100644 --- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/signer/JiraCsrHandlerTest.kt +++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/signer/JiraCsrHandlerTest.kt @@ -1,6 +1,7 @@ package com.r3.corda.networkmanage.doorman.signer import com.nhaarman.mockito_kotlin.* +import com.r3.corda.networkmanage.TestBase import com.r3.corda.networkmanage.common.persistence.* import com.r3.corda.networkmanage.doorman.ApprovedRequest import com.r3.corda.networkmanage.doorman.JiraClient @@ -18,7 +19,7 @@ import org.mockito.junit.MockitoRule import java.security.cert.CertPath import kotlin.test.assertEquals -class JiraCsrHandlerTest { +class JiraCsrHandlerTest : TestBase() { @Rule @JvmField val mockitoRule: MockitoRule = MockitoJUnit.rule() @@ -76,7 +77,11 @@ class JiraCsrHandlerTest { @Test fun `create tickets`() { - val csr = CertificateSigningRequest(requestId, "name", RequestStatus.NEW, pkcS10CertificationRequest, null, emptyList(), null) + val csr = certificateSigningRequest( + requestId = requestId, + legalName = "name", + status = RequestStatus.NEW, + request = pkcS10CertificationRequest) whenever(certificationRequestStorage.getRequests(RequestStatus.NEW)).thenReturn(listOf(csr)) // Test @@ -90,8 +95,8 @@ class JiraCsrHandlerTest { fun `sync tickets status`() { val id1 = SecureHash.randomSHA256().toString() val id2 = SecureHash.randomSHA256().toString() - val csr1 = CertificateSigningRequest(id1, "name1", RequestStatus.NEW, pkcS10CertificationRequest, null, emptyList(), null) - val csr2 = CertificateSigningRequest(id2, "name2", RequestStatus.NEW, pkcS10CertificationRequest, null, emptyList(), null) + val csr1 = CertificateSigningRequest(id1, "name1", SecureHash.randomSHA256(), RequestStatus.NEW, pkcS10CertificationRequest, null, emptyList(), null) + val csr2 = CertificateSigningRequest(id2, "name2", SecureHash.randomSHA256(), RequestStatus.NEW, pkcS10CertificationRequest, null, emptyList(), null) val requests = mutableMapOf(id1 to csr1, id2 to csr2) @@ -134,7 +139,7 @@ class JiraCsrHandlerTest { // Sign request 1 val certPath = mock() - val certData = CertificateData("", CertificateStatus.VALID, certPath) + val certData = CertificateData(CertificateStatus.VALID, certPath) requests[id1] = requests[id1]!!.copy(status = RequestStatus.SIGNED, certData = certData) // Process request again. diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/KeyStoreConfigHelpers.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/KeyStoreConfigHelpers.kt index 2ba6192454..dd93ce2269 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/KeyStoreConfigHelpers.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/KeyStoreConfigHelpers.kt @@ -1,6 +1,7 @@ package net.corda.nodeapi.internal import net.corda.core.crypto.Crypto +import net.corda.core.crypto.Crypto.generateKeyPair import net.corda.core.identity.CordaX500Name import net.corda.core.internal.x500Name import net.corda.nodeapi.internal.config.SSLConfiguration @@ -8,6 +9,7 @@ import net.corda.nodeapi.internal.crypto.* import org.bouncycastle.asn1.x509.GeneralName import org.bouncycastle.asn1.x509.GeneralSubtree import org.bouncycastle.asn1.x509.NameConstraints +import java.security.KeyPair import java.security.cert.X509Certificate import javax.security.auth.x500.X500Principal @@ -31,7 +33,7 @@ fun SSLConfiguration.createDevKeyStores(legalName: CordaX500Name, save(nodeKeystore, keyStorePassword) } - val tlsKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) + val tlsKeyPair = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) val tlsCert = X509Utilities.createCertificate(CertificateType.TLS, nodeCaCert, nodeCaKeyPair, legalName.x500Principal, tlsKeyPair.public) loadOrCreateKeyStore(sslKeystore, keyStorePassword).apply { @@ -59,17 +61,18 @@ fun createDevNetworkMapCa(rootCa: CertificateAndKeyPair = DEV_ROOT_CA): Certific * Create a dev node CA cert, as a sub-cert of the given [intermediateCa], and matching key pair using the given * [CordaX500Name] as the cert subject. */ -fun createDevNodeCa(intermediateCa: CertificateAndKeyPair, legalName: CordaX500Name): CertificateAndKeyPair { - val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) +fun createDevNodeCa(intermediateCa: CertificateAndKeyPair, + legalName: CordaX500Name, + nodeKeyPair: KeyPair = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)): CertificateAndKeyPair { val nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, legalName.x500Name))), arrayOf()) val cert = X509Utilities.createCertificate( CertificateType.NODE_CA, intermediateCa.certificate, intermediateCa.keyPair, legalName.x500Principal, - keyPair.public, + nodeKeyPair.public, nameConstraints = nameConstraints) - return CertificateAndKeyPair(cert, keyPair) + return CertificateAndKeyPair(cert, nodeKeyPair) } val DEV_INTERMEDIATE_CA: CertificateAndKeyPair get() = DevCaHelper.loadDevCa(X509Utilities.CORDA_INTERMEDIATE_CA) diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt index 60a7c40b00..1d73ec5363 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt @@ -2,6 +2,7 @@ package net.corda.testing.internal import com.nhaarman.mockito_kotlin.doAnswer import net.corda.core.crypto.Crypto +import net.corda.core.crypto.Crypto.generateKeyPair import net.corda.core.identity.CordaX500Name import net.corda.core.utilities.loggerFor import net.corda.node.services.config.configureDevKeyAndTrustStores @@ -15,6 +16,7 @@ import org.mockito.Mockito import org.mockito.internal.stubbing.answers.ThrowsException import java.lang.reflect.Modifier import java.nio.file.Files +import java.security.KeyPair import java.util.* import javax.security.auth.x500.X500Principal @@ -102,11 +104,12 @@ fun createDevIntermediateCaCertPath( */ fun createDevNodeCaCertPath( legalName: CordaX500Name, + nodeKeyPair: KeyPair = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME), rootCaName: X500Principal = defaultRootCaName, intermediateCaName: X500Principal = defaultIntermediateCaName ): Triple { val (rootCa, intermediateCa) = createDevIntermediateCaCertPath(rootCaName, intermediateCaName) - val nodeCa = createDevNodeCa(intermediateCa, legalName) + val nodeCa = createDevNodeCa(intermediateCa, legalName, nodeKeyPair) return Triple(rootCa, intermediateCa, nodeCa) } diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/TestNodeInfoBuilder.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/TestNodeInfoBuilder.kt index bdeba56fc6..3eb84a4471 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/TestNodeInfoBuilder.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/TestNodeInfoBuilder.kt @@ -8,16 +8,35 @@ import net.corda.core.node.NodeInfo import net.corda.core.serialization.serialize import net.corda.core.utilities.NetworkHostAndPort import net.corda.nodeapi.internal.SignedNodeInfo -import net.corda.testing.getTestPartyAndCertificate +import net.corda.nodeapi.internal.createDevNodeCa +import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair +import net.corda.nodeapi.internal.crypto.CertificateType +import net.corda.nodeapi.internal.crypto.X509CertificateFactory +import net.corda.nodeapi.internal.crypto.X509Utilities +import net.corda.testing.DEV_INTERMEDIATE_CA +import net.corda.testing.DEV_ROOT_CA +import java.security.KeyPair import java.security.PrivateKey +import java.security.cert.X509Certificate -class TestNodeInfoBuilder { +class TestNodeInfoBuilder(private val intermediateAndRoot: Pair = DEV_INTERMEDIATE_CA to DEV_ROOT_CA.certificate) { private val identitiesAndPrivateKeys = ArrayList>() - fun addIdentity(name: CordaX500Name): Pair { + fun addIdentity(name: CordaX500Name, nodeKeyPair: KeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)): Pair { + val nodeCertificateAndKeyPair = createDevNodeCa(intermediateAndRoot.first, name, nodeKeyPair) val identityKeyPair = Crypto.generateKeyPair() - val identity = getTestPartyAndCertificate(name, identityKeyPair.public) - return Pair(identity, identityKeyPair.private).also { + val identityCert = X509Utilities.createCertificate( + CertificateType.LEGAL_IDENTITY, + nodeCertificateAndKeyPair.certificate, + nodeCertificateAndKeyPair.keyPair, + nodeCertificateAndKeyPair.certificate.subjectX500Principal, + identityKeyPair.public) + val certPath = X509CertificateFactory() + .generateCertPath(identityCert, + nodeCertificateAndKeyPair.certificate, + intermediateAndRoot.first.certificate, + intermediateAndRoot.second) + return Pair(PartyAndCertificate(certPath), identityKeyPair.private).also { identitiesAndPrivateKeys += it } }