mirror of
https://github.com/corda/corda.git
synced 2024-12-29 09:18:58 +00:00
Merge branch 'master' into colljos-schema-split-for-notaries
This commit is contained in:
commit
4bff002b41
3
.idea/compiler.xml
generated
3
.idea/compiler.xml
generated
@ -179,4 +179,7 @@
|
|||||||
<module name="webserver_test" target="1.8" />
|
<module name="webserver_test" target="1.8" />
|
||||||
</bytecodeTargetLevel>
|
</bytecodeTargetLevel>
|
||||||
</component>
|
</component>
|
||||||
|
<component name="JavacSettings">
|
||||||
|
<option name="ADDITIONAL_OPTIONS_STRING" value="-parameters" />
|
||||||
|
</component>
|
||||||
</project>
|
</project>
|
@ -29,7 +29,7 @@ import net.corda.core.utilities.getOrThrow
|
|||||||
import net.corda.nodeapi.ArtemisConsumer
|
import net.corda.nodeapi.ArtemisConsumer
|
||||||
import net.corda.nodeapi.ArtemisProducer
|
import net.corda.nodeapi.ArtemisProducer
|
||||||
import net.corda.nodeapi.RPCApi
|
import net.corda.nodeapi.RPCApi
|
||||||
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.SimpleString
|
||||||
import org.apache.activemq.artemis.api.core.client.ActiveMQClient.DEFAULT_ACK_BATCH_SIZE
|
import org.apache.activemq.artemis.api.core.client.ActiveMQClient.DEFAULT_ACK_BATCH_SIZE
|
||||||
import org.apache.activemq.artemis.api.core.client.ClientMessage
|
import org.apache.activemq.artemis.api.core.client.ClientMessage
|
||||||
@ -194,7 +194,7 @@ class RPCClientProxyHandler(
|
|||||||
TimeUnit.MILLISECONDS
|
TimeUnit.MILLISECONDS
|
||||||
)
|
)
|
||||||
sessionAndProducerPool.run {
|
sessionAndProducerPool.run {
|
||||||
it.session.createTemporaryQueue(clientAddress, ActiveMQDefaultConfiguration.getDefaultRoutingType(), clientAddress)
|
it.session.createTemporaryQueue(clientAddress, RoutingType.ANYCAST, clientAddress)
|
||||||
}
|
}
|
||||||
val sessionFactory = serverLocator.createSessionFactory()
|
val sessionFactory = serverLocator.createSessionFactory()
|
||||||
val session = sessionFactory.createSession(rpcUsername, rpcPassword, false, true, true, false, DEFAULT_ACK_BATCH_SIZE)
|
val session = sessionFactory.createSession(rpcUsername, rpcPassword, false, true, true, false, DEFAULT_ACK_BATCH_SIZE)
|
||||||
|
@ -4,4 +4,3 @@ trustStorePassword : "trustpass"
|
|||||||
p2pAddress : "localhost:10002"
|
p2pAddress : "localhost:10002"
|
||||||
rpcAddress : "localhost:10003"
|
rpcAddress : "localhost:10003"
|
||||||
webAddress : "localhost:10004"
|
webAddress : "localhost:10004"
|
||||||
useHTTPS : false
|
|
||||||
|
@ -4,4 +4,3 @@ trustStorePassword : "trustpass"
|
|||||||
p2pAddress : "localhost:10005"
|
p2pAddress : "localhost:10005"
|
||||||
rpcAddress : "localhost:10006"
|
rpcAddress : "localhost:10006"
|
||||||
webAddress : "localhost:10007"
|
webAddress : "localhost:10007"
|
||||||
useHTTPS : false
|
|
||||||
|
@ -6,4 +6,3 @@ webAddress : "localhost:10001"
|
|||||||
notary : {
|
notary : {
|
||||||
validating : true
|
validating : true
|
||||||
}
|
}
|
||||||
useHTTPS : false
|
|
||||||
|
@ -66,26 +66,24 @@ object NodeInfoSchemaV1 : MappedSchema(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Embeddable
|
|
||||||
data class PKHostAndPort(
|
|
||||||
val host: String? = null,
|
|
||||||
val port: Int? = null
|
|
||||||
) : Serializable
|
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "node_info_hosts")
|
@Table(name = "node_info_hosts")
|
||||||
data class DBHostAndPort(
|
data class DBHostAndPort(
|
||||||
@EmbeddedId
|
@Id
|
||||||
private val pk: PKHostAndPort
|
@GeneratedValue
|
||||||
|
@Column(name = "hosts_id")
|
||||||
|
var id: Int,
|
||||||
|
val host: String? = null,
|
||||||
|
val port: Int? = null
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
fun fromHostAndPort(hostAndPort: NetworkHostAndPort) = DBHostAndPort(
|
fun fromHostAndPort(hostAndPort: NetworkHostAndPort) = DBHostAndPort(
|
||||||
PKHostAndPort(hostAndPort.host, hostAndPort.port)
|
0, hostAndPort.host, hostAndPort.port
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toHostAndPort(): NetworkHostAndPort {
|
fun toHostAndPort(): NetworkHostAndPort {
|
||||||
return NetworkHostAndPort(this.pk.host!!, this.pk.port!!)
|
return NetworkHostAndPort(host!!, port!!)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -158,6 +158,13 @@ R3 Corda 3.0 Developer Preview
|
|||||||
* Peer-to-peer communications is now via AMQP 1.0 as default.
|
* 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.
|
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.
|
||||||
|
|
||||||
* Enterprise Corda only: Compatibility with SQL Server 2017 and SQL Azure databases.
|
* Enterprise Corda only: Compatibility with SQL Server 2017 and SQL Azure databases.
|
||||||
|
|
||||||
* Enterprise Corda only: node configuration property ``database.schema`` and documented existing database properties.
|
* Enterprise Corda only: node configuration property ``database.schema`` and documented existing database properties.
|
||||||
|
@ -43,7 +43,6 @@ Simple Notary configuration file.
|
|||||||
notary : {
|
notary : {
|
||||||
validating : false
|
validating : false
|
||||||
}
|
}
|
||||||
useHTTPS : false
|
|
||||||
devMode : true
|
devMode : true
|
||||||
compatibilityZoneURL : "https://cz.corda.net"
|
compatibilityZoneURL : "https://cz.corda.net"
|
||||||
|
|
||||||
@ -136,10 +135,6 @@ path to the node's base directory.
|
|||||||
|
|
||||||
Only one of ``raft``, ``bftSMaRt`` or ``custom`` configuration values may be specified.
|
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 ``<workspace>/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
|
: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:
|
following fields:
|
||||||
|
|
||||||
|
@ -46,7 +46,6 @@ handling, and ensures the Corda service is run at boot.
|
|||||||
myLegalName : "O=Bank of Breakfast Tea, L=London, C=GB"
|
myLegalName : "O=Bank of Breakfast Tea, L=London, C=GB"
|
||||||
keyStorePassword : "cordacadevpass"
|
keyStorePassword : "cordacadevpass"
|
||||||
trustStorePassword : "trustpass"
|
trustStorePassword : "trustpass"
|
||||||
useHTTPS : false
|
|
||||||
devMode : false
|
devMode : false
|
||||||
rpcUsers=[
|
rpcUsers=[
|
||||||
{
|
{
|
||||||
@ -217,7 +216,6 @@ at boot, and means the Corda service stays running with no users connected to th
|
|||||||
keyStorePassword : "cordacadevpass"
|
keyStorePassword : "cordacadevpass"
|
||||||
trustStorePassword : "trustpass"
|
trustStorePassword : "trustpass"
|
||||||
extraAdvertisedServiceIds: [ "" ]
|
extraAdvertisedServiceIds: [ "" ]
|
||||||
useHTTPS : false
|
|
||||||
devMode : false
|
devMode : false
|
||||||
rpcUsers=[
|
rpcUsers=[
|
||||||
{
|
{
|
||||||
|
@ -4,4 +4,3 @@ trustStorePassword : "trustpass"
|
|||||||
p2pAddress : "my-network-map:10000"
|
p2pAddress : "my-network-map:10000"
|
||||||
webAddress : "localhost:10001"
|
webAddress : "localhost:10001"
|
||||||
sshdAddress : "localhost:10002"
|
sshdAddress : "localhost:10002"
|
||||||
useHTTPS : false
|
|
||||||
|
@ -10,7 +10,6 @@ dataSourceProperties : {
|
|||||||
p2pAddress : "my-corda-node:10002"
|
p2pAddress : "my-corda-node:10002"
|
||||||
rpcAddress : "my-corda-node:10003"
|
rpcAddress : "my-corda-node:10003"
|
||||||
webAddress : "localhost:10004"
|
webAddress : "localhost:10004"
|
||||||
useHTTPS : false
|
|
||||||
rpcUsers : [
|
rpcUsers : [
|
||||||
{ username=user1, password=letmein, permissions=[ StartFlow.net.corda.protocols.CashProtocol ] }
|
{ username=user1, password=letmein, permissions=[ StartFlow.net.corda.protocols.CashProtocol ] }
|
||||||
]
|
]
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
Glossary
|
Glossary
|
||||||
========
|
========
|
||||||
|
|
||||||
|
AMQP
|
||||||
|
The serialisation mechanism used within Corda for everything except flow checkpoints and RPC.
|
||||||
Artemis
|
Artemis
|
||||||
The message queuing middleware used within Corda
|
The message queuing middleware used within Corda
|
||||||
Attachment
|
Attachment
|
||||||
@ -30,7 +32,7 @@ Gradle
|
|||||||
Kotlin
|
Kotlin
|
||||||
The language used to code Corda. Fully compatible with any JVM language, including (obviously) Java.
|
The language used to code Corda. Fully compatible with any JVM language, including (obviously) Java.
|
||||||
Kryo
|
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
|
Input
|
||||||
In Corda terms, an input state is one that is used and consumed within a transaction. Once consumed, it cannot be re-used.
|
In Corda terms, an input state is one that is used and consumed within a transaction. Once consumed, it cannot be re-used.
|
||||||
JVM
|
JVM
|
||||||
|
@ -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
|
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.
|
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
|
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
|
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).
|
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``:
|
:``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.
|
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
|
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.
|
peer's network address.
|
||||||
|
|
||||||
:``internal.services.$identity``:
|
:``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
|
#. 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
|
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
|
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
|
#. 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.
|
is successful then they are only given sufficient permission to perform RPC, otherwise they are rejected.
|
||||||
|
@ -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
|
:end-before: END 7
|
||||||
|
|
||||||
.. note:: Several of the core interfaces at the heart of Corda are already annotated and so any classes that implement
|
.. 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
|
.. 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
|
expression that will work with Corda is ``Runnable r = (Runnable & Serializable) () -> System.out.println("Hello World");``, or
|
||||||
@ -52,62 +52,62 @@ AMQP
|
|||||||
====
|
====
|
||||||
|
|
||||||
Originally Corda used a ``Kryo``-based serialization scheme throughout for all serialization contexts. However, it was realised there
|
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
|
#. 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.
|
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 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 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
|
#. 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
|
#. 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.
|
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 from deserialized objects being constructed through supported constructors rather than having
|
#. Increased security by constructing deserialized objects through supported constructors, rather than having
|
||||||
data poked directly into their fields without an opportunity to validate consistency or intercept attempts to manipulate
|
data inserted directly into their fields without an opportunity to validate consistency or intercept attempts to manipulate
|
||||||
supposed invariants.
|
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
|
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 AMQP framework is currently used for:
|
||||||
|
|
||||||
#. The peer to peer context, representing inter-node communication.
|
#. The peer-to-peer context, representing inter-node communication
|
||||||
#. The persistence layer, representing contract states persisted into the vault.
|
#. 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
|
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
|
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, that flexibility makes it exceptionally difficult to make secure. Conversely
|
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 robust a secure framework that can be reasoned about thus made safer with far fewer unforeseen
|
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.
|
security holes.
|
||||||
|
|
||||||
.. note:: Selection of serialization context should, for the most part, be opaque to CorDapp developers, the Corda framework selecting
|
.. 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`.
|
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
|
This document describes what is currently and what 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
|
of CorDapp developers, to allow CorDapps to take into consideration the future state. The AMQP serialization format will
|
||||||
course continue to apply the whitelisting functionality that is already in place and described in :doc:`serialization`.
|
continue to apply the whitelisting functionality that is already in place and described in :doc:`serialization`.
|
||||||
|
|
||||||
Core Types
|
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
|
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.
|
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,
|
For example, if you use a Guava implementation of a collection, it will deserialize as the primitive collection type.
|
||||||
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
|
The declared types of properties should only use these types, and not any concrete implementation types (e.g.
|
||||||
in their generic form, the generic type parameters will be included in the schema, and the elements type checked against the
|
Guava implementations). Collections must specify their generic type, the generic type parameters will be included in
|
||||||
generic parameters when deserialized.
|
the schema, and the element's type will be checked against the generic parameters when deserialized.
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
@ -121,8 +121,8 @@ generic parameters when deserialized.
|
|||||||
java.util.SortedMap
|
java.util.SortedMap
|
||||||
java.util.NavigableMap
|
java.util.NavigableMap
|
||||||
|
|
||||||
However, we will support the concrete implementation types below explicitly and also as the declared type of a field, as
|
However, as a convenience, we explicitly support the concrete implementation types below, and they can be used as the
|
||||||
a convenience.
|
declared types of properties.
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
@ -151,12 +151,12 @@ All the primitive types are supported.
|
|||||||
Arrays
|
Arrays
|
||||||
``````
|
``````
|
||||||
|
|
||||||
We also support arrays of any supported type, primitive or otherwise.
|
Arrays of any type are supported, primitive or otherwise.
|
||||||
|
|
||||||
JDK Types
|
JDK Types
|
||||||
`````````
|
`````````
|
||||||
|
|
||||||
The following types are supported from the JDK libraries.
|
The following JDK library types are supported:
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
@ -200,10 +200,10 @@ The following types are supported from the JDK libraries.
|
|||||||
java.util.Currency
|
java.util.Currency
|
||||||
java.util.UUID
|
java.util.UUID
|
||||||
|
|
||||||
Third Party Types
|
Third-Party Types
|
||||||
`````````````````
|
`````````````````
|
||||||
|
|
||||||
The following 3rd party types are supported.
|
The following 3rd-party types are supported:
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
@ -215,11 +215,11 @@ The following 3rd party types are supported.
|
|||||||
Corda Types
|
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
|
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
|
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
|
not conform to ``CordaThrowable`` will be converted to a ``CordaRuntimeException``, with the original exception type
|
||||||
and other properties retained within it.
|
and other properties retained within it.
|
||||||
|
|
||||||
.. _amqp_custom_types_ref:
|
.. _amqp_custom_types_ref:
|
||||||
@ -227,7 +227,7 @@ and other properties retained within it.
|
|||||||
Custom Types
|
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
|
Classes
|
||||||
```````
|
```````
|
||||||
@ -236,34 +236,40 @@ General Rules
|
|||||||
'''''''''''''
|
'''''''''''''
|
||||||
|
|
||||||
#. The class must be compiled with parameter names included in the ``.class`` file. This is the default in Kotlin
|
#. 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``
|
||||||
#. The class is annotated with ``@CordaSerializable``.
|
|
||||||
#. The declared types of constructor arguments, getters, and setters must be supported, and where generics are used 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 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
|
generic parameter must be a supported type, an open wildcard (``*``), or a bounded wildcard which is currently
|
||||||
widened to an open wildcard.
|
widened to an open wildcard
|
||||||
#. Any superclass must adhere to the same rules, but can be abstract.
|
#. 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.
|
#. Object graph cycles are not supported, so an object cannot refer to itself, directly or indirectly
|
||||||
|
|
||||||
Constructor Instantiation
|
Constructor Instantiation
|
||||||
'''''''''''''''''''''''''
|
'''''''''''''''''''''''''
|
||||||
|
|
||||||
The primary way the AMQP serialization framework for Corda instantiates objects is via a defined constructor. This is
|
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
|
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.
|
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
|
It is recommended that serializable objects in Corda adhere to the following rules, as they allow immutable state
|
||||||
be created
|
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
|
#. 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 the type of ``foo`` is boolean, the getter may
|
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.
|
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
|
#. 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``
|
#. 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
|
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
|
.. container:: codeset
|
||||||
|
|
||||||
@ -271,9 +277,10 @@ In Kotlin, this maps cleanly to a data class where there getters are synthesized
|
|||||||
|
|
||||||
data class Example (val a: Int, val b: String)
|
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
|
Properties ``a`` and ``b`` will be included in the serialised form.
|
||||||
the constructor will not be serialised. For example, in the following code property c will not be considered part of 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
|
.. container:: codeset
|
||||||
|
|
||||||
@ -291,14 +298,14 @@ serialised form
|
|||||||
Setter Instantiation
|
Setter Instantiation
|
||||||
''''''''''''''''''''
|
''''''''''''''''''''
|
||||||
|
|
||||||
As an alternative to constructor based initialisation Corda can also determine the important elements of an
|
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
|
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
|
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
|
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.
|
the classic *idiom* of mutable JavaBeans).
|
||||||
|
|
||||||
On deserialization, a default instance will first be created and then, in turn, the setters invoked
|
On deserialization, a default instance will first be created, and then the setters will be invoked on that object to
|
||||||
on that object to populate the correct values.
|
populate it with the correct values.
|
||||||
|
|
||||||
For example:
|
For example:
|
||||||
|
|
||||||
@ -324,7 +331,7 @@ Inaccessible Private Properties
|
|||||||
```````````````````````````````
|
```````````````````````````````
|
||||||
|
|
||||||
Whilst the Corda AMQP serialization framework supports private object properties without publicly
|
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.
|
For example.
|
||||||
|
|
||||||
@ -350,15 +357,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,
|
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
|
.. warning:: IDEs will indicate erroneously that properties can be given something other than public visibility. Ignore
|
||||||
visibility. Ignore this as whilst it will work, as discussed above there are many reasons why this isn't
|
this, as whilst it will work, as discussed above there are many reasons why this isn't a good idea.
|
||||||
a good idea and those are beyond the scope of the IDEs inference rules
|
|
||||||
|
|
||||||
Providing a public getter, as per the following example, is acceptable
|
Providing a public getter, as per the following example, is acceptable:
|
||||||
|
|
||||||
.. container:: codeset
|
.. container:: codeset
|
||||||
|
|
||||||
@ -392,7 +398,7 @@ Providing a public getter, as per the following example, is acceptable
|
|||||||
Enums
|
Enums
|
||||||
`````
|
`````
|
||||||
|
|
||||||
#. All enums are supported, provided they are annotated with ``@CordaSerializable``.
|
#. All enums are supported, provided they are annotated with ``@CordaSerializable``
|
||||||
|
|
||||||
|
|
||||||
Exceptions
|
Exceptions
|
||||||
@ -401,24 +407,25 @@ Exceptions
|
|||||||
The following rules apply to supported ``Throwable`` implementations.
|
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
|
#. 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
|
#. 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 Objects
|
||||||
``````````````
|
``````````````
|
||||||
|
|
||||||
#. Kotlin ``object`` s are singletons and treated differently. They are recorded into the stream with no properties
|
#. Kotlin's non-anonymous ``object`` s (i.e. constructs like ``object foo : Contract {...}``) are singletons and
|
||||||
and deserialize back to the singleton instance. Currently, the same is not true of Java singletons,
|
treated differently. They are recorded into the stream with no properties, and deserialize back to the
|
||||||
and they will deserialize to new instances of the class.
|
singleton instance. Currently, the same is not true of Java singletons, which will deserialize to new instances
|
||||||
#. Kotlin's anonymous ``object`` s are not currently supported. I.e. constructs like:
|
of the class
|
||||||
``object : Contract {...}`` will not serialize correctly and need to be re-written as an explicit class declaration.
|
#. 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
|
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
|
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
|
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
|
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.
|
environment. A more detailed discussion of the carpenter will be provided in a future update to the documentation.
|
||||||
@ -427,25 +434,25 @@ Future Enhancements
|
|||||||
```````````````````
|
```````````````````
|
||||||
|
|
||||||
#. Java singleton support. We will add support for identifying classes which are singletons and identifying the
|
#. 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
|
#. 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
|
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
|
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.
|
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
|
Enum Evolution
|
||||||
``````````````
|
``````````````
|
||||||
Corda supports interoperability of enumerated type versions. This allows such types to be changed over time without breaking
|
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``.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,3 +1,9 @@
|
|||||||
|
.. highlight:: kotlin
|
||||||
|
.. raw:: html
|
||||||
|
|
||||||
|
<script type="text/javascript" src="_static/jquery.js"></script>
|
||||||
|
<script type="text/javascript" src="_static/codesets.js"></script>
|
||||||
|
|
||||||
Upgrading a CorDapp (outside of platform version upgrades)
|
Upgrading a CorDapp (outside of platform version upgrades)
|
||||||
==========================================================
|
==========================================================
|
||||||
|
|
||||||
@ -127,18 +133,40 @@ 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
|
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:
|
programmatically evolve flows across versions. For example:
|
||||||
|
|
||||||
|
.. container:: codeset
|
||||||
|
|
||||||
.. sourcecode:: kotlin
|
.. sourcecode:: kotlin
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
override fun call() {
|
override fun call() {
|
||||||
val otherFlowVersion = otherSession.getCounterpartyFlowInfo().flowVersion
|
val otherFlowVersion = otherSession.getCounterpartyFlowInfo().flowVersion
|
||||||
val receivedString = if (otherFlowVersion == 1) {
|
val receivedString = if (otherFlowVersion == 1) {
|
||||||
receive<Int>(otherParty).unwrap { it.toString() }
|
otherSession.receive<Int>().unwrap { it.toString() }
|
||||||
} else {
|
} else {
|
||||||
receive<String>(otherParty).unwrap { it }
|
otherSession.receive<String>().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
|
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
|
expect a String. This flow is still able to communicate with parties that are running the older CorDapp containing
|
||||||
the older flow.
|
the older flow.
|
||||||
@ -147,6 +175,8 @@ How do I deal with interface changes to inlined subflows?
|
|||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
Here is an example of an in-lined subflow:
|
Here is an example of an in-lined subflow:
|
||||||
|
|
||||||
|
.. container:: codeset
|
||||||
|
|
||||||
.. sourcecode:: kotlin
|
.. sourcecode:: kotlin
|
||||||
|
|
||||||
@StartableByRPC
|
@StartableByRPC
|
||||||
@ -172,6 +202,47 @@ Here is an example of an in-lined subflow:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.. sourcecode:: java
|
||||||
|
|
||||||
|
@StartableByRPC
|
||||||
|
@InitiatingFlow
|
||||||
|
class FlowA extends FlowLogic<Void> {
|
||||||
|
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<Void> {
|
||||||
|
// Omitted.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: No annotations. This is used as an inlined subflow.
|
||||||
|
class FlowB extends FlowLogic<Void> {
|
||||||
|
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.
|
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``
|
Suppose flow ``A`` calls inlined subflow B, which, in turn, initiates a session with a counterparty. The ``FlowLogic``
|
||||||
type used by the counterparty to determine which counter-flow to invoke is determined by ``A``, and not by ``B``. This
|
type used by the counterparty to determine which counter-flow to invoke is determined by ``A``, and not by ``B``. This
|
||||||
@ -361,6 +432,8 @@ for details.
|
|||||||
For backwards compatible changes such as adding columns, the procedure for upgrading a state schema is to extend the
|
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:
|
existing object relational mapper. For example, we can update:
|
||||||
|
|
||||||
|
.. container:: codeset
|
||||||
|
|
||||||
.. sourcecode:: kotlin
|
.. sourcecode:: kotlin
|
||||||
|
|
||||||
object ObligationSchemaV1 : MappedSchema(Obligation::class.java, 1, listOf(ObligationEntity::class.java)) {
|
object ObligationSchemaV1 : MappedSchema(Obligation::class.java, 1, listOf(ObligationEntity::class.java)) {
|
||||||
@ -374,8 +447,57 @@ existing object relational mapper. For example, we can update:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.. 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:
|
To:
|
||||||
|
|
||||||
|
.. container:: codeset
|
||||||
|
|
||||||
.. sourcecode:: kotlin
|
.. sourcecode:: kotlin
|
||||||
|
|
||||||
object ObligationSchemaV1 : MappedSchema(Obligation::class.java, 1, listOf(ObligationEntity::class.java)) {
|
object ObligationSchemaV1 : MappedSchema(Obligation::class.java, 1, listOf(ObligationEntity::class.java)) {
|
||||||
@ -386,7 +508,60 @@ To:
|
|||||||
@Column @Lob var lender: ByteArray = obligation.lender.owningKey.encoded
|
@Column @Lob var lender: ByteArray = obligation.lender.owningKey.encoded
|
||||||
@Column @Lob var borrower: ByteArray = obligation.borrower.owningKey.encoded
|
@Column @Lob var borrower: ByteArray = obligation.borrower.owningKey.encoded
|
||||||
@Column var linear_id: String = obligation.linearId.id.toString()
|
@Column var linear_id: String = obligation.linearId.id.toString()
|
||||||
@Column var defaulted: Bool = obligation.amount.inDefault // NEW COLUNM!
|
@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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -397,12 +572,22 @@ 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
|
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:
|
add it to the ``supportedSchemas`` property of your ``QueryableState``, like so:
|
||||||
|
|
||||||
|
.. container:: codeset
|
||||||
|
|
||||||
.. sourcecode:: kotlin
|
.. sourcecode:: kotlin
|
||||||
|
|
||||||
override fun supportedSchemas(): Iterable<MappedSchema> = listOf(ExampleSchemaV1, ExampleSchemaV2)
|
override fun supportedSchemas(): Iterable<MappedSchema> = listOf(ExampleSchemaV1, ExampleSchemaV2)
|
||||||
|
|
||||||
|
.. sourcecode:: java
|
||||||
|
|
||||||
|
@Override public Iterable<MappedSchema> supportedSchemas() {
|
||||||
|
return ImmutableList.of(new ExampleSchemaV1(), new ExampleSchemaV2());
|
||||||
|
}
|
||||||
|
|
||||||
Then, in ``generateMappedObject``, add support for the new schema:
|
Then, in ``generateMappedObject``, add support for the new schema:
|
||||||
|
|
||||||
|
.. container:: codeset
|
||||||
|
|
||||||
.. sourcecode:: kotlin
|
.. sourcecode:: kotlin
|
||||||
|
|
||||||
override fun generateMappedObject(schema: MappedSchema): PersistentState {
|
override fun generateMappedObject(schema: MappedSchema): PersistentState {
|
||||||
@ -413,5 +598,17 @@ Then, in ``generateMappedObject``, add support for the new 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
|
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.
|
separate database tables where possible - one for each supported schema.
|
@ -2,7 +2,7 @@ package net.corda.flowhook
|
|||||||
|
|
||||||
import co.paralleluniverse.fibers.Fiber
|
import co.paralleluniverse.fibers.Fiber
|
||||||
import net.corda.node.services.statemachine.Event
|
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
|
import java.sql.Connection
|
||||||
|
|
||||||
@Suppress("UNUSED")
|
@Suppress("UNUSED")
|
||||||
@ -156,7 +156,7 @@ object FlowHookContainer {
|
|||||||
|
|
||||||
private fun currentTransactionOrThread(): Any {
|
private fun currentTransactionOrThread(): Any {
|
||||||
return try {
|
return try {
|
||||||
DatabaseTransactionManager.currentOrNull()
|
contextTransactionOrNull
|
||||||
} catch (exception: IllegalStateException) {
|
} catch (exception: IllegalStateException) {
|
||||||
null
|
null
|
||||||
} ?: Thread.currentThread()
|
} ?: Thread.currentThread()
|
||||||
|
@ -65,13 +65,15 @@ The doorman service can use JIRA to manage the certificate signing request appro
|
|||||||
projectCode = "TD"
|
projectCode = "TD"
|
||||||
username = "username"
|
username = "username"
|
||||||
password = "password"
|
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
|
### 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)
|
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"
|
projectCode = "TD"
|
||||||
username = "username"
|
username = "username"
|
||||||
password = "password"
|
password = "password"
|
||||||
doneTransitionCode = 41
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,7 +88,7 @@ dependencies {
|
|||||||
testCompile "com.nhaarman:mockito-kotlin:0.6.1"
|
testCompile "com.nhaarman:mockito-kotlin:0.6.1"
|
||||||
testCompile "com.spotify:docker-client:8.9.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.
|
// The jira client includes jersey-core 1.5 which breaks everything.
|
||||||
exclude module: 'jersey-core'
|
exclude module: 'jersey-core'
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,6 @@ doormanConfig{
|
|||||||
projectCode = "TD"
|
projectCode = "TD"
|
||||||
username = "username"
|
username = "username"
|
||||||
password = "password"
|
password = "password"
|
||||||
doneTransitionCode = 41
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package com.r3.corda.networkmanage.doorman
|
package com.r3.corda.networkmanage.doorman
|
||||||
|
|
||||||
import com.r3.corda.networkmanage.common.persistence.configureDatabase
|
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 com.r3.corda.networkmanage.doorman.signer.LocalSigner
|
||||||
import net.corda.cordform.CordformNode
|
import net.corda.cordform.CordformNode
|
||||||
import net.corda.core.crypto.random63BitValue
|
import net.corda.core.crypto.random63BitValue
|
||||||
@ -141,11 +142,11 @@ class NodeRegistrationTest : IntegrationTest() {
|
|||||||
start(
|
start(
|
||||||
serverAddress,
|
serverAddress,
|
||||||
configureDatabase(makeTestDataSourceProperties(), DatabaseConfig(runMigration = true)),
|
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),
|
DoormanConfig(approveAll = true, jiraConfig = null, approveInterval = timeoutMillis),
|
||||||
networkParameters?.let {
|
networkParameters?.let {
|
||||||
NetworkMapStartParams(
|
NetworkMapStartParams(
|
||||||
LocalSigner(networkMapCa.keyPair, arrayOf(networkMapCa.certificate, rootCaCert)),
|
LocalSigner(networkMapCa),
|
||||||
networkParameters,
|
networkParameters,
|
||||||
NetworkMapConfig(cacheTimeout = timeoutMillis, signInterval = timeoutMillis)
|
NetworkMapConfig(cacheTimeout = timeoutMillis, signInterval = timeoutMillis)
|
||||||
)
|
)
|
||||||
|
@ -12,6 +12,7 @@ import org.junit.Before
|
|||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.rules.TemporaryFolder
|
import org.junit.rules.TemporaryFolder
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import kotlin.test.assertTrue
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
class HsmTest {
|
class HsmTest {
|
||||||
@ -19,7 +20,12 @@ class HsmTest {
|
|||||||
@Rule
|
@Rule
|
||||||
@JvmField
|
@JvmField
|
||||||
val hsmSimulator: HsmSimulator = HsmSimulator()
|
val hsmSimulator: HsmSimulator = HsmSimulator()
|
||||||
val testParameters = Parameters(
|
|
||||||
|
@Rule
|
||||||
|
@JvmField
|
||||||
|
val tempFolder = TemporaryFolder()
|
||||||
|
|
||||||
|
private val testParameters = Parameters(
|
||||||
dataSourceProperties = mock(),
|
dataSourceProperties = mock(),
|
||||||
device = "${hsmSimulator.port}@${hsmSimulator.host}",
|
device = "${hsmSimulator.port}@${hsmSimulator.host}",
|
||||||
keySpecifier = 1,
|
keySpecifier = 1,
|
||||||
@ -30,10 +36,6 @@ class HsmTest {
|
|||||||
validDays = 3650
|
validDays = 3650
|
||||||
)
|
)
|
||||||
|
|
||||||
@Rule
|
|
||||||
@JvmField
|
|
||||||
val tempFolder = TemporaryFolder()
|
|
||||||
|
|
||||||
private lateinit var inputReader: InputReader
|
private lateinit var inputReader: InputReader
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
@ -47,14 +49,12 @@ class HsmTest {
|
|||||||
fun `Authenticator executes the block once user is successfully authenticated`() {
|
fun `Authenticator executes the block once user is successfully authenticated`() {
|
||||||
// given
|
// given
|
||||||
val authenticator = Authenticator(testParameters.createProvider(), inputReader = inputReader)
|
val authenticator = Authenticator(testParameters.createProvider(), inputReader = inputReader)
|
||||||
var executed = false
|
val executed = AtomicBoolean(false)
|
||||||
|
|
||||||
// when
|
// when
|
||||||
authenticator.connectAndAuthenticate({ provider, signers ->
|
authenticator.connectAndAuthenticate { _, _ -> executed.set(true) }
|
||||||
executed = true
|
|
||||||
})
|
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assertTrue(executed)
|
assertTrue(executed.get())
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -90,7 +90,12 @@ class SigningServiceIntegrationTest {
|
|||||||
val database = configureDatabase(makeTestDataSourceProperties(), DatabaseConfig(runMigration = true))
|
val database = configureDatabase(makeTestDataSourceProperties(), DatabaseConfig(runMigration = true))
|
||||||
|
|
||||||
NetworkManagementServer().use { server ->
|
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
|
val doormanHostAndPort = server.hostAndPort
|
||||||
// Start Corda network registration.
|
// Start Corda network registration.
|
||||||
val config = createConfig().also {
|
val config = createConfig().also {
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
package com.r3.corda.networkmanage.common.persistence
|
package com.r3.corda.networkmanage.common.persistence
|
||||||
|
|
||||||
|
import net.corda.core.crypto.SecureHash
|
||||||
import org.bouncycastle.pkcs.PKCS10CertificationRequest
|
import org.bouncycastle.pkcs.PKCS10CertificationRequest
|
||||||
import java.security.cert.CertPath
|
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,
|
data class CertificateSigningRequest(val requestId: String,
|
||||||
val legalName: String,
|
val legalName: String,
|
||||||
|
val publicKeyHash: SecureHash,
|
||||||
val status: RequestStatus,
|
val status: RequestStatus,
|
||||||
val request: PKCS10CertificationRequest,
|
val request: PKCS10CertificationRequest,
|
||||||
val remark: String?,
|
val remark: String?,
|
||||||
@ -59,7 +61,7 @@ interface CertificationRequestStorage {
|
|||||||
* @param rejectedBy authority (its identifier) rejecting this request.
|
* @param rejectedBy authority (its identifier) rejecting this request.
|
||||||
* @param rejectReason brief description of the rejection reason
|
* @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].
|
* Store certificate path with [requestId], this will store the encoded [CertPath] and transit request status to [RequestStatus.SIGNED].
|
||||||
|
@ -39,10 +39,9 @@ interface NetworkMapStorage {
|
|||||||
fun getSignedNetworkParameters(hash: SecureHash): SignedNetworkParameters?
|
fun getSignedNetworkParameters(hash: SecureHash): SignedNetworkParameters?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve network map parameters.
|
* Retrieve the network parameters of the current network map, or null if there's no network map.
|
||||||
* @return signed current network map parameters or null if they don't exist
|
|
||||||
*/
|
*/
|
||||||
fun getCurrentSignedNetworkParameters(): SignedNetworkParameters?
|
fun getNetworkParametersOfNetworkMap(): SignedNetworkParameters?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Persists given network parameters with signature if provided.
|
* Persists given network parameters with signature if provided.
|
||||||
@ -55,5 +54,5 @@ interface NetworkMapStorage {
|
|||||||
* Note that they may not have been signed up yet.
|
* Note that they may not have been signed up yet.
|
||||||
* @return latest network parameters
|
* @return latest network parameters
|
||||||
*/
|
*/
|
||||||
fun getLatestUnsignedNetworkParameters(): NetworkParameters
|
fun getLatestNetworkParameters(): NetworkParameters?
|
||||||
}
|
}
|
@ -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.CertificateDataEntity
|
||||||
import com.r3.corda.networkmanage.common.persistence.entity.CertificateSigningRequestEntity
|
import com.r3.corda.networkmanage.common.persistence.entity.CertificateSigningRequestEntity
|
||||||
import com.r3.corda.networkmanage.common.utils.hashString
|
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.crypto.SecureHash
|
||||||
import net.corda.core.identity.CordaX500Name
|
import net.corda.core.identity.CordaX500Name
|
||||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||||
@ -26,14 +27,12 @@ class PersistentCertificateRequestStorage(private val database: CordaPersistence
|
|||||||
builder.and(requestIdEq, statusEq)
|
builder.and(requestIdEq, statusEq)
|
||||||
}
|
}
|
||||||
request ?: throw IllegalArgumentException("Cannot retrieve 'APPROVED' certificate signing request for request id: $requestId")
|
request ?: throw IllegalArgumentException("Cannot retrieve 'APPROVED' certificate signing request for request id: $requestId")
|
||||||
val publicKeyHash = certificates.certificates.first().publicKey.hashString()
|
|
||||||
val certificateSigningRequest = request.copy(
|
val certificateSigningRequest = request.copy(
|
||||||
modifiedBy = signedBy,
|
modifiedBy = signedBy,
|
||||||
modifiedAt = Instant.now(),
|
modifiedAt = Instant.now(),
|
||||||
status = RequestStatus.SIGNED)
|
status = RequestStatus.SIGNED)
|
||||||
session.merge(certificateSigningRequest)
|
session.merge(certificateSigningRequest)
|
||||||
val certificateDataEntity = CertificateDataEntity(
|
val certificateDataEntity = CertificateDataEntity(
|
||||||
publicKeyHash = publicKeyHash,
|
|
||||||
certificateStatus = CertificateStatus.VALID,
|
certificateStatus = CertificateStatus.VALID,
|
||||||
certificatePathBytes = certificates.encoded,
|
certificatePathBytes = certificates.encoded,
|
||||||
certificateSigningRequest = certificateSigningRequest)
|
certificateSigningRequest = certificateSigningRequest)
|
||||||
@ -48,6 +47,7 @@ class PersistentCertificateRequestStorage(private val database: CordaPersistence
|
|||||||
session.save(CertificateSigningRequestEntity(
|
session.save(CertificateSigningRequestEntity(
|
||||||
requestId = requestId,
|
requestId = requestId,
|
||||||
legalName = legalName,
|
legalName = legalName,
|
||||||
|
publicKeyHash = toSupportedPublicKey(request.subjectPublicKeyInfo).hashString(),
|
||||||
requestBytes = request.encoded,
|
requestBytes = request.encoded,
|
||||||
remark = rejectReason,
|
remark = rejectReason,
|
||||||
modifiedBy = emptyList(),
|
modifiedBy = emptyList(),
|
||||||
@ -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) {
|
database.transaction(TransactionIsolationLevel.SERIALIZABLE) {
|
||||||
val request = findRequest(requestId)
|
val request = findRequest(requestId)
|
||||||
request ?: throw IllegalArgumentException("Error when rejecting request with id: $requestId. Request does not exist.")
|
request ?: throw IllegalArgumentException("Error when rejecting request with id: $requestId. Request does not exist.")
|
||||||
@ -134,7 +134,7 @@ class PersistentCertificateRequestStorage(private val database: CordaPersistence
|
|||||||
return Pair(request.subject.toString(), "Name validation failed: ${e.message}")
|
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)
|
val criteriaQuery = createQuery(CertificateSigningRequestEntity::class.java)
|
||||||
criteriaQuery.from(CertificateSigningRequestEntity::class.java).run {
|
criteriaQuery.from(CertificateSigningRequestEntity::class.java).run {
|
||||||
criteriaQuery.where(equal(get<String>(CertificateSigningRequestEntity::legalName.name), legalName))
|
criteriaQuery.where(equal(get<String>(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
|
// 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.
|
// Also, at the moment we assume that once the CSR is approved it cannot be rejected.
|
||||||
// What if we approved something by mistake.
|
// 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
|
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<String>(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")
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -6,10 +6,7 @@ import com.r3.corda.networkmanage.common.utils.SignedNetworkParameters
|
|||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.crypto.sha256
|
import net.corda.core.crypto.sha256
|
||||||
import net.corda.core.internal.DigitalSignatureWithCert
|
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.core.serialization.serialize
|
||||||
import net.corda.nodeapi.internal.network.NetworkMap
|
|
||||||
import net.corda.nodeapi.internal.network.NetworkParameters
|
import net.corda.nodeapi.internal.network.NetworkParameters
|
||||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
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 {
|
class PersistentNetworkMapStorage(private val database: CordaPersistence) : NetworkMapStorage {
|
||||||
override fun getCurrentNetworkMap(): SignedNetworkMap? {
|
override fun getCurrentNetworkMap(): SignedNetworkMap? {
|
||||||
return database.transaction {
|
return database.transaction {
|
||||||
getCurrentNetworkMapEntity()?.let {
|
getCurrentNetworkMapEntity()?.toSignedNetworkMap()
|
||||||
val signatureAndCertPath = it.signatureAndCertificate()
|
|
||||||
SignedNetworkMap(SerializedBytes(it.networkMap), signatureAndCertPath)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getCurrentSignedNetworkParameters(): SignedNetworkParameters? {
|
override fun getNetworkParametersOfNetworkMap(): SignedNetworkParameters? {
|
||||||
return database.transaction {
|
return database.transaction {
|
||||||
getCurrentNetworkMapEntity()?.let {
|
getCurrentNetworkMapEntity()?.let {
|
||||||
val netParamsHash = it.networkMap.deserialize<NetworkMap>().networkParameterHash
|
val netParamsHash = it.toNetworkMap().networkParameterHash
|
||||||
getSignedNetworkParameters(netParamsHash)
|
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? {
|
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<SecureHash> {
|
override fun getNodeInfoHashes(certificateStatus: CertificateStatus): List<SecureHash> {
|
||||||
@ -79,18 +76,17 @@ class PersistentNetworkMapStorage(private val database: CordaPersistence) : Netw
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getLatestUnsignedNetworkParameters(): NetworkParameters = getLatestNetworkParametersEntity().networkParameters()
|
override fun getLatestNetworkParameters(): NetworkParameters? {
|
||||||
|
|
||||||
private fun getLatestNetworkParametersEntity(): NetworkParametersEntity {
|
|
||||||
return database.transaction {
|
return database.transaction {
|
||||||
val builder = session.criteriaBuilder
|
val query = session.criteriaBuilder.run {
|
||||||
val query = builder.createQuery(NetworkParametersEntity::class.java).run {
|
createQuery(NetworkParametersEntity::class.java).run {
|
||||||
from(NetworkParametersEntity::class.java).run {
|
from(NetworkParametersEntity::class.java).run {
|
||||||
orderBy(builder.desc(get<String>(NetworkParametersEntity::created.name)))
|
orderBy(desc(get<String>(NetworkParametersEntity::created.name)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// We just want the last entry
|
// 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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package com.r3.corda.networkmanage.common.persistence
|
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.CertificateSigningRequestEntity
|
||||||
import com.r3.corda.networkmanage.common.persistence.entity.NodeInfoEntity
|
import com.r3.corda.networkmanage.common.persistence.entity.NodeInfoEntity
|
||||||
import com.r3.corda.networkmanage.common.utils.buildCertPath
|
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.core.serialization.serialize
|
||||||
import net.corda.nodeapi.internal.SignedNodeInfo
|
import net.corda.nodeapi.internal.SignedNodeInfo
|
||||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
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
|
import java.security.cert.CertPath
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -21,16 +20,13 @@ class PersistentNodeInfoStorage(private val database: CordaPersistence) : NodeIn
|
|||||||
val nodeInfo = nodeInfoWithSigned.nodeInfo
|
val nodeInfo = nodeInfoWithSigned.nodeInfo
|
||||||
val signedNodeInfo = nodeInfoWithSigned.signedNodeInfo
|
val signedNodeInfo = nodeInfoWithSigned.signedNodeInfo
|
||||||
val nodeCaCert = nodeInfo.legalIdentitiesAndCerts[0].certPath.certificates.find { CertRole.extract(it) == CertRole.NODE_CA }
|
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
|
// TODO Move these checks out of data access layer
|
||||||
val request = nodeCaCert?.let {
|
val request = nodeCaCert?.let {
|
||||||
singleRequestWhere(CertificateDataEntity::class.java) { builder, path ->
|
getSignedRequestByPublicHash(it.publicKey.encoded.sha256(), this)
|
||||||
val certPublicKeyHashEq = builder.equal(path.get<String>(CertificateDataEntity::publicKeyHash.name), it.publicKey.encoded.sha256().toString())
|
|
||||||
val certStatusValid = builder.equal(path.get<CertificateStatus>(CertificateDataEntity::certificateStatus.name), CertificateStatus.VALID)
|
|
||||||
builder.and(certPublicKeyHashEq, certStatusValid)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
request ?: throw IllegalArgumentException("Unknown node info, this public key is not registered with the network management service.")
|
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
|
* 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.
|
* 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 ->
|
deleteRequest(NodeInfoEntity::class.java) { builder, path ->
|
||||||
builder.equal(path.get<CertificateSigningRequestEntity>(NodeInfoEntity::certificateSigningRequest.name), request.certificateSigningRequest)
|
builder.equal(path.get<CertificateSigningRequestEntity>(NodeInfoEntity::certificateSigningRequest.name), request)
|
||||||
}
|
}
|
||||||
val hash = signedNodeInfo.raw.hash
|
val hash = signedNodeInfo.raw.hash
|
||||||
|
|
||||||
val hashedNodeInfo = NodeInfoEntity(
|
val hashedNodeInfo = NodeInfoEntity(
|
||||||
nodeInfoHash = hash.toString(),
|
nodeInfoHash = hash.toString(),
|
||||||
certificateSigningRequest = request.certificateSigningRequest,
|
certificateSigningRequest = request,
|
||||||
signedNodeInfoBytes = signedNodeInfo.serialize().bytes)
|
signedNodeInfoBytes = signedNodeInfo.serialize().bytes)
|
||||||
session.save(hashedNodeInfo)
|
session.save(hashedNodeInfo)
|
||||||
hash
|
hash
|
||||||
@ -61,16 +57,16 @@ class PersistentNodeInfoStorage(private val database: CordaPersistence) : NodeIn
|
|||||||
|
|
||||||
override fun getCertificatePath(publicKeyHash: SecureHash): CertPath? {
|
override fun getCertificatePath(publicKeyHash: SecureHash): CertPath? {
|
||||||
return database.transaction {
|
return database.transaction {
|
||||||
val builder = session.criteriaBuilder
|
val request = getSignedRequestByPublicHash(publicKeyHash, this)
|
||||||
val query = builder.createQuery(ByteArray::class.java).run {
|
request?.let { buildCertPath(it.certificateData!!.certificatePathBytes) }
|
||||||
from(CertificateSigningRequestEntity::class.java).run {
|
|
||||||
select(get<CertificateDataEntity>(CertificateSigningRequestEntity::certificateData.name)
|
|
||||||
.get<ByteArray>(CertificateDataEntity::certificatePathBytes.name))
|
|
||||||
where(builder.equal(get<CertificateDataEntity>(CertificateSigningRequestEntity::certificateData.name)
|
|
||||||
.get<String>(CertificateDataEntity::publicKeyHash.name), publicKeyHash.toString()))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
session.createQuery(query).uniqueResultOptional().orElseGet { null }?.let { buildCertPath(it) }
|
|
||||||
|
private fun getSignedRequestByPublicHash(publicKeyHash: SecureHash, transaction: DatabaseTransaction): CertificateSigningRequestEntity? {
|
||||||
|
return transaction.singleRequestWhere(CertificateSigningRequestEntity::class.java) { builder, path ->
|
||||||
|
val publicKeyEq = builder.equal(path.get<String>(CertificateSigningRequestEntity::publicKeyHash.name), publicKeyHash.toString())
|
||||||
|
val statusEq = builder.equal(path.get<RequestStatus>(CertificateSigningRequestEntity::status.name), RequestStatus.SIGNED)
|
||||||
|
builder.and(publicKeyEq, statusEq)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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.CertificateSigningRequest
|
||||||
import com.r3.corda.networkmanage.common.persistence.CertificateStatus
|
import com.r3.corda.networkmanage.common.persistence.CertificateStatus
|
||||||
import com.r3.corda.networkmanage.common.persistence.RequestStatus
|
import com.r3.corda.networkmanage.common.persistence.RequestStatus
|
||||||
|
import net.corda.core.crypto.SecureHash
|
||||||
import org.bouncycastle.pkcs.PKCS10CertificationRequest
|
import org.bouncycastle.pkcs.PKCS10CertificationRequest
|
||||||
import org.hibernate.envers.Audited
|
import org.hibernate.envers.Audited
|
||||||
import java.security.cert.CertPath
|
import java.security.cert.CertPath
|
||||||
@ -12,7 +13,7 @@ import java.time.Instant
|
|||||||
import javax.persistence.*
|
import javax.persistence.*
|
||||||
|
|
||||||
@Entity
|
@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(
|
class CertificateSigningRequestEntity(
|
||||||
@Id
|
@Id
|
||||||
@Column(name = "request_id", length = 64)
|
@Column(name = "request_id", length = 64)
|
||||||
@ -22,6 +23,9 @@ class CertificateSigningRequestEntity(
|
|||||||
@Column(name = "legal_name", length = 256, nullable = false)
|
@Column(name = "legal_name", length = 256, nullable = false)
|
||||||
val legalName: String,
|
val legalName: String,
|
||||||
|
|
||||||
|
@Column(name = "public_key_hash", length = 64)
|
||||||
|
val publicKeyHash: String,
|
||||||
|
|
||||||
@Audited
|
@Audited
|
||||||
@Column(name = "status", nullable = false)
|
@Column(name = "status", nullable = false)
|
||||||
@Enumerated(EnumType.STRING)
|
@Enumerated(EnumType.STRING)
|
||||||
@ -50,6 +54,7 @@ class CertificateSigningRequestEntity(
|
|||||||
fun toCertificateSigningRequest() = CertificateSigningRequest(
|
fun toCertificateSigningRequest() = CertificateSigningRequest(
|
||||||
requestId = requestId,
|
requestId = requestId,
|
||||||
legalName = legalName,
|
legalName = legalName,
|
||||||
|
publicKeyHash = SecureHash.parse(publicKeyHash),
|
||||||
status = status,
|
status = status,
|
||||||
request = request(),
|
request = request(),
|
||||||
remark = remark,
|
remark = remark,
|
||||||
@ -59,6 +64,7 @@ class CertificateSigningRequestEntity(
|
|||||||
|
|
||||||
fun copy(requestId: String = this.requestId,
|
fun copy(requestId: String = this.requestId,
|
||||||
legalName: String = this.legalName,
|
legalName: String = this.legalName,
|
||||||
|
publicKeyHash: String = this.publicKeyHash,
|
||||||
status: RequestStatus = this.status,
|
status: RequestStatus = this.status,
|
||||||
modifiedBy: List<String> = this.modifiedBy,
|
modifiedBy: List<String> = this.modifiedBy,
|
||||||
modifiedAt: Instant = this.modifiedAt,
|
modifiedAt: Instant = this.modifiedAt,
|
||||||
@ -69,6 +75,7 @@ class CertificateSigningRequestEntity(
|
|||||||
return CertificateSigningRequestEntity(
|
return CertificateSigningRequestEntity(
|
||||||
requestId = requestId,
|
requestId = requestId,
|
||||||
legalName = legalName,
|
legalName = legalName,
|
||||||
|
publicKeyHash = publicKeyHash,
|
||||||
status = status,
|
status = status,
|
||||||
modifiedAt = modifiedAt,
|
modifiedAt = modifiedAt,
|
||||||
modifiedBy = modifiedBy,
|
modifiedBy = modifiedBy,
|
||||||
@ -82,16 +89,13 @@ class CertificateSigningRequestEntity(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "certificate_data", indexes = arrayOf(Index(name = "IDX_PUB_KEY_HASH", columnList = "public_key_hash")))
|
@Table(name = "certificate_data")
|
||||||
class CertificateDataEntity(
|
class CertificateDataEntity(
|
||||||
|
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue(strategy = GenerationType.SEQUENCE)
|
@GeneratedValue(strategy = GenerationType.SEQUENCE)
|
||||||
val id: Long? = null,
|
val id: Long? = null,
|
||||||
|
|
||||||
@Column(name = "public_key_hash", length = 64)
|
|
||||||
val publicKeyHash: String,
|
|
||||||
|
|
||||||
@Column(name = "certificate_status")
|
@Column(name = "certificate_status")
|
||||||
val certificateStatus: CertificateStatus,
|
val certificateStatus: CertificateStatus,
|
||||||
|
|
||||||
@ -105,7 +109,6 @@ class CertificateDataEntity(
|
|||||||
) {
|
) {
|
||||||
fun toCertificateData(): CertificateData {
|
fun toCertificateData(): CertificateData {
|
||||||
return CertificateData(
|
return CertificateData(
|
||||||
publicKeyHash = publicKeyHash,
|
|
||||||
certStatus = certificateStatus,
|
certStatus = certificateStatus,
|
||||||
certPath = toCertificatePath()
|
certPath = toCertificatePath()
|
||||||
)
|
)
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
package com.r3.corda.networkmanage.common.persistence.entity
|
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.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.crypto.X509CertificateFactory
|
||||||
|
import net.corda.nodeapi.internal.network.NetworkMap
|
||||||
import javax.persistence.*
|
import javax.persistence.*
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@ -23,11 +27,12 @@ class NetworkMapEntity(
|
|||||||
@Column(name = "certificate")
|
@Column(name = "certificate")
|
||||||
val certificate: ByteArray
|
val certificate: ByteArray
|
||||||
) {
|
) {
|
||||||
/**
|
fun toNetworkMap(): NetworkMap = networkMap.deserialize()
|
||||||
* Deserializes NetworkMapEntity.signatureBytes into the [DigitalSignatureWithCert] instance
|
|
||||||
*/
|
|
||||||
fun signatureAndCertificate(): DigitalSignatureWithCert {
|
|
||||||
return DigitalSignatureWithCert(X509CertificateFactory().generateCertificate(certificate.inputStream()), signature)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
fun toSignedNetworkMap(): SignedNetworkMap {
|
||||||
|
return SignedNetworkMap(
|
||||||
|
SerializedBytes(networkMap),
|
||||||
|
DigitalSignatureWithCert(X509CertificateFactory().generateCertificate(certificate.inputStream()), signature)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
@ -35,13 +35,15 @@ class NetworkParametersEntity(
|
|||||||
@Column(name = "certificate")
|
@Column(name = "certificate")
|
||||||
val certificate: ByteArray?
|
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 toNetworkParameters(): NetworkParameters = parametersBytes.deserialize()
|
||||||
fun signedParameters(): SignedNetworkParameters? {
|
|
||||||
return if (certificate != null && signature != null) {
|
fun toSignedNetworkParameters(): SignedNetworkParameters {
|
||||||
val sigWithCert = DigitalSignatureWithCert(X509CertificateFactory().generateCertificate(certificate.inputStream()), signature)
|
if (certificate == null || signature == null) throw IllegalStateException("Network parameters entity is not signed: $parametersHash")
|
||||||
SignedDataWithCert(SerializedBytes(parametersBytes), sigWithCert)
|
return SignedDataWithCert(
|
||||||
} else null
|
SerializedBytes(parametersBytes),
|
||||||
|
DigitalSignatureWithCert(X509CertificateFactory().generateCertificate(certificate.inputStream()), signature)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -4,20 +4,30 @@ import com.r3.corda.networkmanage.common.persistence.CertificateStatus
|
|||||||
import com.r3.corda.networkmanage.common.persistence.NetworkMapStorage
|
import com.r3.corda.networkmanage.common.persistence.NetworkMapStorage
|
||||||
import net.corda.core.internal.SignedDataWithCert
|
import net.corda.core.internal.SignedDataWithCert
|
||||||
import net.corda.core.serialization.serialize
|
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.NetworkMap
|
||||||
import net.corda.nodeapi.internal.network.NetworkParameters
|
import net.corda.nodeapi.internal.network.NetworkParameters
|
||||||
|
|
||||||
class NetworkMapSigner(private val networkMapStorage: NetworkMapStorage, private val signer: Signer) {
|
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.
|
* Signs the network map and latest network parameters if they haven't been signed yet.
|
||||||
*/
|
*/
|
||||||
fun signNetworkMap() {
|
fun signNetworkMap() {
|
||||||
// TODO There is no network parameters update process in place yet. We assume that latest parameters are to be used
|
// TODO There is no network parameters update process in place yet. We assume that latest parameters are to be used
|
||||||
// in current network map.
|
// in current network map.
|
||||||
val latestNetworkParameters = networkMapStorage.getLatestUnsignedNetworkParameters()
|
val latestNetworkParameters = networkMapStorage.getLatestNetworkParameters()
|
||||||
val currentNetworkParameters = networkMapStorage.getCurrentSignedNetworkParameters()
|
if (latestNetworkParameters == null) {
|
||||||
if (currentNetworkParameters?.verified() != latestNetworkParameters)
|
logger.debug("No network parameters present")
|
||||||
signNetworkParameters(latestNetworkParameters)
|
return
|
||||||
|
}
|
||||||
|
val currentNetworkParameters = networkMapStorage.getNetworkParametersOfNetworkMap()
|
||||||
|
if (currentNetworkParameters?.verified() != latestNetworkParameters) {
|
||||||
|
persistSignedNetworkParameters(latestNetworkParameters)
|
||||||
|
}
|
||||||
val currentSignedNetworkMap = networkMapStorage.getCurrentNetworkMap()
|
val currentSignedNetworkMap = networkMapStorage.getCurrentNetworkMap()
|
||||||
val nodeInfoHashes = networkMapStorage.getNodeInfoHashes(CertificateStatus.VALID)
|
val nodeInfoHashes = networkMapStorage.getNodeInfoHashes(CertificateStatus.VALID)
|
||||||
val serialisedNetworkMap = NetworkMap(nodeInfoHashes, latestNetworkParameters.serialize().hash).serialize()
|
val serialisedNetworkMap = NetworkMap(nodeInfoHashes, latestNetworkParameters.serialize().hash).serialize()
|
||||||
@ -27,10 +37,8 @@ class NetworkMapSigner(private val networkMapStorage: NetworkMapStorage, private
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
fun persistSignedNetworkParameters(networkParameters: NetworkParameters) {
|
||||||
* Signs latest inserted network parameters.
|
logger.info("Signing and persisting network parameters: $networkParameters")
|
||||||
*/
|
|
||||||
fun signNetworkParameters(networkParameters: NetworkParameters) {
|
|
||||||
val digitalSignature = signer.signObject(networkParameters).sig
|
val digitalSignature = signer.signObject(networkParameters).sig
|
||||||
networkMapStorage.saveNetworkParameters(networkParameters, digitalSignature)
|
networkMapStorage.saveNetworkParameters(networkParameters, digitalSignature)
|
||||||
}
|
}
|
||||||
|
@ -10,14 +10,20 @@ import net.corda.core.internal.SignedDataWithCert
|
|||||||
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
|
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
|
||||||
import net.corda.nodeapi.internal.network.NetworkMap
|
import net.corda.nodeapi.internal.network.NetworkMap
|
||||||
import net.corda.nodeapi.internal.network.NetworkParameters
|
import net.corda.nodeapi.internal.network.NetworkParameters
|
||||||
|
import java.security.KeyPair
|
||||||
|
import java.security.PrivateKey
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
import java.security.cert.CertPath
|
import java.security.cert.CertPath
|
||||||
import java.security.cert.Certificate
|
import java.security.cert.X509Certificate
|
||||||
|
|
||||||
// TODO These should be defined in node-api
|
// TODO These should be defined in node-api
|
||||||
typealias SignedNetworkParameters = SignedDataWithCert<NetworkParameters>
|
typealias SignedNetworkParameters = SignedDataWithCert<NetworkParameters>
|
||||||
typealias SignedNetworkMap = SignedDataWithCert<NetworkMap>
|
typealias SignedNetworkMap = SignedDataWithCert<NetworkMap>
|
||||||
|
|
||||||
|
data class CertPathAndKey(val certPath: List<X509Certificate>, val key: PrivateKey) {
|
||||||
|
fun toKeyPair(): KeyPair = KeyPair(certPath[0].publicKey, key)
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: replace this with Crypto.hash when its available.
|
// TODO: replace this with Crypto.hash when its available.
|
||||||
/**
|
/**
|
||||||
* Returns SHA256 hash of this public key
|
* Returns SHA256 hash of this public key
|
||||||
@ -42,7 +48,7 @@ fun Array<out String>.toConfigWithOptions(registerOptions: OptionParser.() -> Un
|
|||||||
|
|
||||||
class ShowHelpException(val parser: OptionParser, val errorMessage: String? = null) : Exception()
|
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())
|
fun buildCertPath(certPathBytes: ByteArray): CertPath = X509CertificateFactory().delegate.generateCertPath(certPathBytes.inputStream())
|
||||||
|
|
||||||
|
@ -65,8 +65,7 @@ data class JiraConfig(
|
|||||||
val address: String,
|
val address: String,
|
||||||
val projectCode: String,
|
val projectCode: String,
|
||||||
val username: String,
|
val username: String,
|
||||||
val password: String,
|
val password: String
|
||||||
val doneTransitionCode: Int
|
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
package com.r3.corda.networkmanage.doorman
|
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.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.Field
|
||||||
import com.atlassian.jira.rest.client.api.domain.Issue
|
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.IssueType
|
||||||
import com.atlassian.jira.rest.client.api.domain.input.IssueInputBuilder
|
import com.atlassian.jira.rest.client.api.domain.input.IssueInputBuilder
|
||||||
import com.atlassian.jira.rest.client.api.domain.input.TransitionInput
|
import com.atlassian.jira.rest.client.api.domain.input.TransitionInput
|
||||||
import net.corda.core.identity.CordaX500Name
|
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 net.corda.nodeapi.internal.crypto.X509Utilities
|
||||||
import org.bouncycastle.asn1.x500.style.BCStyle
|
import org.bouncycastle.asn1.x500.style.BCStyle
|
||||||
import org.bouncycastle.openssl.jcajce.JcaPEMWriter
|
import org.bouncycastle.openssl.jcajce.JcaPEMWriter
|
||||||
@ -17,15 +19,20 @@ import java.io.StringWriter
|
|||||||
import java.security.cert.CertPath
|
import java.security.cert.CertPath
|
||||||
import javax.security.auth.x500.X500Principal
|
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 {
|
companion object {
|
||||||
val logger = loggerFor<JiraClient>()
|
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 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 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) {
|
fun createRequestTicket(requestId: String, signingRequest: PKCS10CertificationRequest) {
|
||||||
// Check there isn't already a ticket for this request.
|
// Check there isn't already a ticket for this request.
|
||||||
if (getIssueById(requestId) != null) {
|
if (getIssueById(requestId) != null) {
|
||||||
@ -54,31 +61,68 @@ class JiraClient(private val restClient: JiraRestClient, private val projectCode
|
|||||||
restClient.issueClient.createIssue(issue.build()).fail { logger.error("Exception when creating JIRA issue.", it) }.claim()
|
restClient.issueClient.createIssue(issue.build()).fail { logger.error("Exception when creating JIRA issue.", it) }.claim()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getApprovedRequests(): List<Pair<String, String>> {
|
fun getApprovedRequests(): List<ApprovedRequest> {
|
||||||
val issues = restClient.searchClient.searchJql("project = $projectCode AND status = Approved").claim().issues
|
val issues = restClient.searchClient.searchJql("project = $projectCode AND status = Approved").claim().issues
|
||||||
return issues.map { issue ->
|
return issues.mapNotNull { issue ->
|
||||||
issue.getField(requestIdField.id)?.value?.toString().let {
|
val requestId = issue.getField(requestIdField.id)?.value?.toString() ?: throw IllegalArgumentException("Error processing request '${issue.key}' : RequestId cannot be null.")
|
||||||
val requestId = it ?: throw IllegalArgumentException("RequestId cannot be null.")
|
// Issue retrieved via search doesn't contain change logs.
|
||||||
val approvedBy = issue.assignee?.displayName ?: "Unknown"
|
val fullIssue = restClient.issueClient.getIssue(issue.key, listOf(IssueRestClient.Expandos.CHANGELOG)).claim()
|
||||||
Pair(requestId, approvedBy)
|
val approvedBy = fullIssue.changelog?.last { it.items.any { it.field == "status" && it.toString == "Approved" } }
|
||||||
|
ApprovedRequest(requestId, approvedBy?.author?.displayName ?: "Unknown")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getRejectedRequests(): List<RejectedRequest> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateSignedRequests(signedRequests: Map<String, CertPath>) {
|
fun updateSignedRequests(signedRequests: Map<String, CertPath>) {
|
||||||
// Retrieving certificates for signed CSRs to attach to the jira tasks.
|
// Retrieving certificates for signed CSRs to attach to the jira tasks.
|
||||||
signedRequests.forEach { (id, certPath) ->
|
signedRequests.forEach { (id, certPath) ->
|
||||||
val certificate = certPath.certificates.first()
|
val certificate = certPath.certificates.first()
|
||||||
// Jira only support ~ (contains) search for custom textfield.
|
|
||||||
val issue = getIssueById(id)
|
val issue = getIssueById(id)
|
||||||
if (issue != null) {
|
if (issue != null) {
|
||||||
restClient.issueClient.transition(issue, TransitionInput(doneTransitionCode)).fail { logger.error("Exception when transiting JIRA status.", it) }.claim()
|
if (doneTransitionId == -1) {
|
||||||
restClient.issueClient.addAttachment(issue.attachmentsUri, certificate?.encoded?.inputStream(), "${X509Utilities.CORDA_CLIENT_CA}.cer")
|
doneTransitionId = restClient.issueClient.getTransitions(issue.transitionsUri).claim().single { it.name == "Done" }.id
|
||||||
.fail { logger.error("Exception when uploading attachment to JIRA.", it) }.claim()
|
}
|
||||||
|
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? =
|
fun updateRejectedRequests(rejectedRequests: List<String>) {
|
||||||
restClient.searchClient.searchJql("'Request ID' ~ $requestId").claim().issues.firstOrNull()
|
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?)
|
||||||
|
@ -1,16 +1,11 @@
|
|||||||
package com.r3.corda.networkmanage.doorman
|
package com.r3.corda.networkmanage.doorman
|
||||||
|
|
||||||
import com.atlassian.jira.rest.client.internal.async.AsynchronousJiraRestClientFactory
|
import com.r3.corda.networkmanage.common.persistence.CertificationRequestStorage
|
||||||
import com.r3.corda.networkmanage.common.persistence.*
|
|
||||||
import com.r3.corda.networkmanage.common.persistence.CertificationRequestStorage.Companion.DOORMAN_SIGNATURE
|
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.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.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_CSR_CERTIFICATE_NAME
|
||||||
import com.r3.corda.networkmanage.hsm.configuration.Parameters.Companion.DEFAULT_NETWORK_MAP_CERTIFICATE_NAME
|
import com.r3.corda.networkmanage.hsm.configuration.Parameters.Companion.DEFAULT_NETWORK_MAP_CERTIFICATE_NAME
|
||||||
import net.corda.core.crypto.Crypto
|
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.SerializationEnvironmentImpl
|
||||||
import net.corda.core.serialization.internal.nodeSerializationEnv
|
import net.corda.core.serialization.internal.nodeSerializationEnv
|
||||||
import net.corda.core.utilities.NetworkHostAndPort
|
import net.corda.core.utilities.NetworkHostAndPort
|
||||||
import net.corda.core.utilities.loggerFor
|
|
||||||
import net.corda.nodeapi.internal.crypto.*
|
import net.corda.nodeapi.internal.crypto.*
|
||||||
import net.corda.nodeapi.internal.network.NetworkParameters
|
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.AMQP_P2P_CONTEXT
|
||||||
import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl
|
import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl
|
||||||
import net.corda.nodeapi.internal.serialization.amqp.AMQPClientSerializationScheme
|
import net.corda.nodeapi.internal.serialization.amqp.AMQPClientSerializationScheme
|
||||||
import org.bouncycastle.pkcs.PKCS10CertificationRequest
|
import org.bouncycastle.pkcs.PKCS10CertificationRequest
|
||||||
import java.io.Closeable
|
|
||||||
import java.net.URI
|
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.security.cert.X509Certificate
|
import java.security.cert.X509Certificate
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.util.*
|
|
||||||
import java.util.concurrent.Executors
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import javax.security.auth.x500.X500Principal
|
import javax.security.auth.x500.X500Principal
|
||||||
import kotlin.concurrent.thread
|
import kotlin.concurrent.thread
|
||||||
import kotlin.system.exitProcess
|
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<NetworkManagementServer>()
|
|
||||||
}
|
|
||||||
|
|
||||||
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<Any>()
|
|
||||||
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 NetworkMapStartParams(val signer: LocalSigner?, val updateNetworkParameters: NetworkParameters?, val config: NetworkMapConfig)
|
||||||
|
|
||||||
data class NetworkManagementServerStatus(var serverStartTime: Instant = Instant.now(), var lastRequestCheckTime: Instant? = null)
|
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: ")
|
val rootPrivateKeyPassword = rootPrivateKeyPass ?: readPassword("Root Private Key Password: ")
|
||||||
|
|
||||||
if (rootStore.containsAlias(X509Utilities.CORDA_ROOT_CA)) {
|
if (rootStore.containsAlias(X509Utilities.CORDA_ROOT_CA)) {
|
||||||
val oldKey = loadOrCreateKeyStore(rootStoreFile, rootKeystorePassword).getCertificate(X509Utilities.CORDA_ROOT_CA).publicKey
|
println("${X509Utilities.CORDA_ROOT_CA} already exists in keystore, process will now terminate.")
|
||||||
println("Key ${X509Utilities.CORDA_ROOT_CA} already exists in keystore, process will now terminate.")
|
println(rootStore.getCertificate(X509Utilities.CORDA_ROOT_CA))
|
||||||
println(oldKey)
|
|
||||||
exitProcess(1)
|
exitProcess(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -203,7 +75,7 @@ fun generateRootKeyPair(rootStoreFile: Path, rootKeystorePass: String?, rootPriv
|
|||||||
println("Trust store for distribution to nodes created in $nodeTrustStore")
|
println("Trust store for distribution to nodes created in $nodeTrustStore")
|
||||||
|
|
||||||
println("Root CA keypair and certificate stored in ${rootStoreFile.toAbsolutePath()}.")
|
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?) {
|
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<LocalSigner, LocalSigner>? {
|
private fun processKeyStore(parameters: NetworkManagementServerParameters): Pair<CertPathAndKey, LocalSigner>? {
|
||||||
if (parameters.keystorePath == null) return null
|
if (parameters.keystorePath == null) return null
|
||||||
|
|
||||||
// Get password from console if not in config.
|
// 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 privateKeyPassword = parameters.caPrivateKeyPassword ?: readPassword("Private key password: ")
|
||||||
val keyStore = loadOrCreateKeyStore(parameters.keystorePath, keyStorePassword)
|
val keyStore = loadOrCreateKeyStore(parameters.keystorePath, keyStorePassword)
|
||||||
|
|
||||||
val (doormanSigner, networkMapSigner) = listOf(DEFAULT_CSR_CERTIFICATE_NAME, DEFAULT_NETWORK_MAP_CERTIFICATE_NAME).map {
|
val csrCertPathAndKey = keyStore.run {
|
||||||
val keyPair = keyStore.getKeyPair(it, privateKeyPassword)
|
CertPathAndKey(
|
||||||
val certPath = keyStore.getCertificateChain(it).map { it as X509Certificate }
|
keyStore.getCertificateChain(DEFAULT_CSR_CERTIFICATE_NAME).map { it as X509Certificate },
|
||||||
LocalSigner(keyPair, certPath.toTypedArray())
|
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.
|
* 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 {
|
override fun saveRequest(request: PKCS10CertificationRequest): String {
|
||||||
val requestId = delegate.saveRequest(request)
|
val requestId = delegate.saveRequest(request)
|
||||||
delegate.markRequestTicketCreated(requestId)
|
delegate.markRequestTicketCreated(requestId)
|
||||||
@ -311,9 +186,9 @@ fun main(args: Array<String>) {
|
|||||||
initialiseSerialization()
|
initialiseSerialization()
|
||||||
val database = configureDatabase(dataSourceProperties)
|
val database = configureDatabase(dataSourceProperties)
|
||||||
// TODO: move signing to signing server.
|
// 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")
|
println("Starting network management services with local signing")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -325,10 +200,10 @@ fun main(args: Array<String>) {
|
|||||||
parseNetworkParametersFrom(it)
|
parseNetworkParametersFrom(it)
|
||||||
}
|
}
|
||||||
val networkMapStartParams = networkMapConfig?.let {
|
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) {
|
Runtime.getRuntime().addShutdownHook(thread(start = false) {
|
||||||
networkManagementServer.close()
|
networkManagementServer.close()
|
||||||
|
@ -1,58 +1,140 @@
|
|||||||
package com.r3.corda.networkmanage.doorman
|
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.NetworkHostAndPort
|
||||||
import net.corda.core.utilities.loggerFor
|
import net.corda.core.utilities.loggerFor
|
||||||
import org.eclipse.jetty.server.Server
|
import net.corda.nodeapi.internal.network.NetworkParameters
|
||||||
import org.eclipse.jetty.server.ServerConnector
|
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||||
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.io.Closeable
|
||||||
import java.net.InetSocketAddress
|
import java.net.URI
|
||||||
|
import java.time.Instant
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
/**
|
class NetworkManagementServer : Closeable {
|
||||||
* NetworkManagementWebServer runs on Jetty server and provides service via http.
|
|
||||||
*/
|
|
||||||
class NetworkManagementWebServer(hostAndPort: NetworkHostAndPort, private vararg val webServices: Any) : Closeable {
|
|
||||||
companion object {
|
companion object {
|
||||||
val logger = loggerFor<NetworkManagementServer>()
|
private val logger = loggerFor<NetworkManagementServer>()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val server: Server = Server(InetSocketAddress(hostAndPort.host, hostAndPort.port)).apply {
|
private val closeActions = mutableListOf<() -> Unit>()
|
||||||
handler = HandlerCollection().apply {
|
lateinit var hostAndPort: NetworkHostAndPort
|
||||||
addHandler(buildServletContextHandler())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val hostAndPort: NetworkHostAndPort
|
|
||||||
get() = server.connectors.mapNotNull { it as? ServerConnector }
|
|
||||||
.map { NetworkHostAndPort(it.host, it.localPort) }
|
|
||||||
.first()
|
|
||||||
|
|
||||||
override fun close() {
|
override fun close() {
|
||||||
logger.info("Shutting down network management web services...")
|
for (closeAction in closeActions) {
|
||||||
server.stop()
|
try {
|
||||||
server.join()
|
closeAction()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger.warn("Discregarding exception thrown during close", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun start() {
|
private fun getNetworkMapService(config: NetworkMapConfig, database: CordaPersistence, signer: LocalSigner?, newNetworkParameters: NetworkParameters?): NetworkMapWebService {
|
||||||
logger.info("Starting network management web services...")
|
val networkMapStorage = PersistentNetworkMapStorage(database)
|
||||||
server.start()
|
val nodeInfoStorage = PersistentNodeInfoStorage(database)
|
||||||
logger.info("Network management web services started on $hostAndPort with ${webServices.map { it.javaClass.simpleName }}")
|
val localNetworkMapSigner = signer?.let { NetworkMapSigner(networkMapStorage, it) }
|
||||||
println("Network management web services started on $hostAndPort with ${webServices.map { it.javaClass.simpleName }}")
|
|
||||||
|
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.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildServletContextHandler(): ServletContextHandler {
|
val latestParameters = networkMapStorage.getLatestNetworkParameters() ?:
|
||||||
return ServletContextHandler().apply {
|
throw IllegalStateException("No network parameters were found. Please upload new network parameters before starting network map service")
|
||||||
contextPath = "/"
|
logger.info("Starting network map service with network parameters: $latestParameters")
|
||||||
val resourceConfig = ResourceConfig().apply {
|
|
||||||
// Add your API provider classes (annotated for JAX-RS) here
|
if (localNetworkMapSigner != null) {
|
||||||
webServices.forEach { register(it) }
|
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)
|
||||||
}
|
}
|
||||||
val jerseyServlet = ServletHolder(ServletContainer(resourceConfig)).apply { initOrder = 0 }// Initialise at server start
|
}, config.signInterval, config.signInterval, TimeUnit.MILLISECONDS)
|
||||||
addServlet(jerseyServlet, "/*")
|
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)
|
||||||
|
JiraCsrHandler(jiraClient, requestService, DefaultCsrHandler(requestService, csrCertPathAndKey))
|
||||||
|
} else {
|
||||||
|
DefaultCsrHandler(requestService, csrCertPathAndKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
val scheduledExecutor = Executors.newScheduledThreadPool(1)
|
||||||
|
val approvalThread = Runnable {
|
||||||
|
try {
|
||||||
|
serverStatus.lastRequestCheckTime = Instant.now()
|
||||||
|
// Process Jira approved tickets.
|
||||||
|
requestProcessor.processRequests()
|
||||||
|
} 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<Any>()
|
||||||
|
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
|
||||||
|
}
|
||||||
}
|
}
|
@ -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, "/*")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,41 +1,41 @@
|
|||||||
package com.r3.corda.networkmanage.doorman.signer
|
package com.r3.corda.networkmanage.doorman.signer
|
||||||
|
|
||||||
import com.r3.corda.networkmanage.common.persistence.CertificateResponse
|
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
|
||||||
import com.r3.corda.networkmanage.common.persistence.CertificationRequestStorage.Companion.DOORMAN_SIGNATURE
|
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.persistence.RequestStatus
|
||||||
import com.r3.corda.networkmanage.doorman.JiraClient
|
import com.r3.corda.networkmanage.common.utils.CertPathAndKey
|
||||||
import net.corda.core.utilities.loggerFor
|
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.PKCS10CertificationRequest
|
||||||
|
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest
|
||||||
|
import java.security.cert.CertPath
|
||||||
|
import javax.security.auth.x500.X500Principal
|
||||||
|
|
||||||
interface CsrHandler {
|
interface CsrHandler {
|
||||||
fun saveRequest(rawRequest: PKCS10CertificationRequest): String
|
fun saveRequest(rawRequest: PKCS10CertificationRequest): String
|
||||||
fun createTickets()
|
fun processRequests()
|
||||||
fun processApprovedRequests()
|
|
||||||
fun getResponse(requestId: String): CertificateResponse
|
fun getResponse(requestId: String): CertificateResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
class DefaultCsrHandler(private val storage: CertificationRequestStorage, private val signer: LocalSigner?) : CsrHandler {
|
class DefaultCsrHandler(private val storage: CertificationRequestStorage,
|
||||||
override fun processApprovedRequests() {
|
private val csrCertPathAndKey: CertPathAndKey?) : CsrHandler {
|
||||||
storage.getRequests(RequestStatus.APPROVED)
|
|
||||||
.forEach { processRequest(it.requestId, it.request) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createTickets() {}
|
override fun processRequests() {
|
||||||
|
if (csrCertPathAndKey == null) return
|
||||||
private fun processRequest(requestId: String, request: PKCS10CertificationRequest) {
|
storage.getRequests(RequestStatus.APPROVED).forEach {
|
||||||
if (signer != null) {
|
val nodeCertPath = createSignedNodeCertificate(it.request, csrCertPathAndKey)
|
||||||
val certs = signer.createSignedClientCertificate(request)
|
|
||||||
// Since Doorman is deployed in the auto-signing mode (i.e. signer != null),
|
// Since Doorman is deployed in the auto-signing mode (i.e. signer != null),
|
||||||
// we use DOORMAN_SIGNATURE as the signer.
|
// we use DOORMAN_SIGNATURE as the signer.
|
||||||
storage.putCertificatePath(requestId, certs, listOf(DOORMAN_SIGNATURE))
|
storage.putCertificatePath(it.requestId, nodeCertPath, listOf(DOORMAN_SIGNATURE))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun saveRequest(rawRequest: PKCS10CertificationRequest): String {
|
override fun saveRequest(rawRequest: PKCS10CertificationRequest): String = storage.saveRequest(rawRequest)
|
||||||
return storage.saveRequest(rawRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getResponse(requestId: String): CertificateResponse {
|
override fun getResponse(requestId: String): CertificateResponse {
|
||||||
val response = storage.getRequest(requestId)
|
val response = storage.getRequest(requestId)
|
||||||
@ -45,63 +45,25 @@ class DefaultCsrHandler(private val storage: CertificationRequestStorage, privat
|
|||||||
RequestStatus.SIGNED -> CertificateResponse.Ready(response.certData?.certPath ?: throw IllegalArgumentException("Certificate should not be null."))
|
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 fun createSignedNodeCertificate(certificationRequest: PKCS10CertificationRequest,
|
||||||
private companion object {
|
csrCertPathAndKey: CertPathAndKey): CertPath {
|
||||||
val log = loggerFor<JiraCsrHandler>()
|
// 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.
|
||||||
override fun saveRequest(rawRequest: PKCS10CertificationRequest): String {
|
// We assume all attributes in the subject name has been checked prior approval.
|
||||||
val requestId = delegate.saveRequest(rawRequest)
|
// TODO: add validation to subject name.
|
||||||
// Make sure request has been accepted.
|
val request = JcaPKCS10CertificationRequest(certificationRequest)
|
||||||
try {
|
val nameConstraints = NameConstraints(
|
||||||
if (delegate.getResponse(requestId) !is CertificateResponse.Unauthorised) {
|
arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, request.subject))),
|
||||||
jiraClient.createRequestTicket(requestId, rawRequest)
|
arrayOf())
|
||||||
storage.markRequestTicketCreated(requestId)
|
val nodeCaCert = X509Utilities.createCertificate(
|
||||||
}
|
CertificateType.NODE_CA,
|
||||||
} catch (e: Exception) {
|
csrCertPathAndKey.certPath[0],
|
||||||
log.warn("There was an error while creating Jira tickets", e)
|
csrCertPathAndKey.toKeyPair(),
|
||||||
} finally {
|
X500Principal(request.subject.encoded),
|
||||||
return requestId
|
request.publicKey,
|
||||||
}
|
nameConstraints = nameConstraints)
|
||||||
}
|
return X509CertificateFactory().generateCertPath(listOf(nodeCaCert) + csrCertPathAndKey.certPath)
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,84 @@
|
|||||||
|
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.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
|
||||||
|
|
||||||
|
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 processRequests() {
|
||||||
|
createTickets()
|
||||||
|
val (approvedRequests, rejectedRequests) = updateRequestStatus()
|
||||||
|
delegate.processRequests()
|
||||||
|
updateJiraTickets(approvedRequests, rejectedRequests)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateRequestStatus(): Pair<List<ApprovedRequest>, List<RejectedRequest>> {
|
||||||
|
// Update local request statuses.
|
||||||
|
val approvedRequest = jiraClient.getApprovedRequests()
|
||||||
|
approvedRequest.forEach { (id, approvedBy) -> storage.approveRequest(id, approvedBy) }
|
||||||
|
val rejectedRequest = jiraClient.getRejectedRequests()
|
||||||
|
rejectedRequest.forEach { (id, rejectedBy, reason) -> storage.rejectRequest(id, rejectedBy, reason) }
|
||||||
|
return Pair(approvedRequest, rejectedRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateJiraTickets(approvedRequest: List<ApprovedRequest>, rejectedRequest: List<RejectedRequest>) {
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createTicket(signingRequest: CertificateSigningRequest) {
|
||||||
|
jiraClient.createRequestTicket(signingRequest.requestId, signingRequest.request)
|
||||||
|
storage.markRequestTicketCreated(signingRequest.requestId)
|
||||||
|
}
|
||||||
|
}
|
@ -1,48 +1,20 @@
|
|||||||
package com.r3.corda.networkmanage.doorman.signer
|
package com.r3.corda.networkmanage.doorman.signer
|
||||||
|
|
||||||
import com.r3.corda.networkmanage.common.signer.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.crypto.Crypto
|
||||||
import net.corda.core.internal.DigitalSignatureWithCert
|
import net.corda.core.internal.DigitalSignatureWithCert
|
||||||
import net.corda.nodeapi.internal.crypto.CertificateType
|
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
|
||||||
import net.corda.nodeapi.internal.crypto.X509Utilities
|
import java.security.KeyStore
|
||||||
import org.bouncycastle.asn1.x509.GeneralName
|
import java.security.PrivateKey
|
||||||
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 java.security.cert.X509Certificate
|
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 local signer is intended to be used in testing environment where hardware signing module is not available.
|
||||||
* This 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 signingKey: PrivateKey, private val signingCert: X509Certificate) : Signer {
|
||||||
class LocalSigner(private val signingKeyPair: KeyPair, private val signingCertPath: Array<X509Certificate>) : Signer {
|
constructor(certAndKeyPair: CertificateAndKeyPair) : this(certAndKeyPair.keyPair.private, certAndKeyPair.certificate)
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun signBytes(data: ByteArray): DigitalSignatureWithCert {
|
override fun signBytes(data: ByteArray): DigitalSignatureWithCert {
|
||||||
return DigitalSignatureWithCert(signingCertPath[0], Crypto.doSign(signingKeyPair.private, data))
|
return DigitalSignatureWithCert(signingCert, Crypto.doSign(signingKey, data))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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.NodeInfoStorage
|
||||||
import com.r3.corda.networkmanage.common.persistence.NodeInfoWithSigned
|
import com.r3.corda.networkmanage.common.persistence.NodeInfoWithSigned
|
||||||
import com.r3.corda.networkmanage.common.utils.SignedNetworkMap
|
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.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.crypto.SecureHash
|
||||||
import net.corda.core.node.NodeInfo
|
import net.corda.core.node.NodeInfo
|
||||||
import net.corda.core.serialization.deserialize
|
import net.corda.core.serialization.deserialize
|
||||||
@ -31,7 +30,7 @@ import javax.ws.rs.core.Response.ok
|
|||||||
import javax.ws.rs.core.Response.status
|
import javax.ws.rs.core.Response.status
|
||||||
|
|
||||||
@Path(NETWORK_MAP_PATH)
|
@Path(NETWORK_MAP_PATH)
|
||||||
class NodeInfoWebService(private val nodeInfoStorage: NodeInfoStorage,
|
class NetworkMapWebService(private val nodeInfoStorage: NodeInfoStorage,
|
||||||
private val networkMapStorage: NetworkMapStorage,
|
private val networkMapStorage: NetworkMapStorage,
|
||||||
private val config: NetworkMapConfig) {
|
private val config: NetworkMapConfig) {
|
||||||
|
|
||||||
@ -42,7 +41,7 @@ class NodeInfoWebService(private val nodeInfoStorage: NodeInfoStorage,
|
|||||||
|
|
||||||
private val networkMapCache: LoadingCache<Boolean, Pair<SignedNetworkMap?, NetworkParameters?>> = CacheBuilder.newBuilder()
|
private val networkMapCache: LoadingCache<Boolean, Pair<SignedNetworkMap?, NetworkParameters?>> = CacheBuilder.newBuilder()
|
||||||
.expireAfterWrite(config.cacheTimeout, TimeUnit.MILLISECONDS)
|
.expireAfterWrite(config.cacheTimeout, TimeUnit.MILLISECONDS)
|
||||||
.build(CacheLoader.from { _ -> Pair(networkMapStorage.getCurrentNetworkMap(), networkMapStorage.getCurrentSignedNetworkParameters()?.verified()) })
|
.build(CacheLoader.from { _ -> Pair(networkMapStorage.getCurrentNetworkMap(), networkMapStorage.getNetworkParametersOfNetworkMap()?.verified()) })
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
@Path("publish")
|
@Path("publish")
|
||||||
@ -87,7 +86,6 @@ class NodeInfoWebService(private val nodeInfoStorage: NodeInfoStorage,
|
|||||||
@GET
|
@GET
|
||||||
@Path("my-ip")
|
@Path("my-ip")
|
||||||
fun myIp(@Context request: HttpServletRequest): Response {
|
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()
|
return ok(request.getHeader("X-Forwarded-For")?.split(",")?.first() ?: "${request.remoteHost}:${request.remotePort}").build()
|
||||||
}
|
}
|
||||||
|
|
@ -1,7 +1,10 @@
|
|||||||
package com.r3.corda.networkmanage.hsm
|
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.PersistentNetworkMapStorage
|
||||||
import com.r3.corda.networkmanage.common.persistence.configureDatabase
|
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.common.utils.ShowHelpException
|
||||||
import com.r3.corda.networkmanage.hsm.authentication.AuthMode
|
import com.r3.corda.networkmanage.hsm.authentication.AuthMode
|
||||||
import com.r3.corda.networkmanage.hsm.authentication.Authenticator
|
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.HsmCsrSigner
|
||||||
import com.r3.corda.networkmanage.hsm.signer.HsmNetworkMapSigner
|
import com.r3.corda.networkmanage.hsm.signer.HsmNetworkMapSigner
|
||||||
import com.r3.corda.networkmanage.hsm.utils.mapCryptoServerException
|
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 org.bouncycastle.jce.provider.BouncyCastleProvider
|
||||||
import java.security.Security
|
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
|
import javax.crypto.Cipher
|
||||||
|
|
||||||
|
private val log = LogManager.getLogger("com.r3.corda.networkmanage.hsm.Main")
|
||||||
|
|
||||||
fun main(args: Array<String>) {
|
fun main(args: Array<String>) {
|
||||||
// Grabbed from https://stackoverflow.com/questions/7953567/checking-if-unlimited-cryptography-is-available
|
// Grabbed from https://stackoverflow.com/questions/7953567/checking-if-unlimited-cryptography-is-available
|
||||||
if (Cipher.getMaxAllowedKeyLength("AES") < 256) {
|
if (Cipher.getMaxAllowedKeyLength("AES") < 256) {
|
||||||
@ -46,12 +58,15 @@ fun run(parameters: Parameters) {
|
|||||||
checkNotNull(dataSourceProperties)
|
checkNotNull(dataSourceProperties)
|
||||||
val database = configureDatabase(dataSourceProperties, databaseConfig)
|
val database = configureDatabase(dataSourceProperties, databaseConfig)
|
||||||
val csrStorage = DBSignedCertificateRequestStorage(database)
|
val csrStorage = DBSignedCertificateRequestStorage(database)
|
||||||
val networkMapStorage = PersistentNetworkMapStorage(database)
|
val hsmSigner = HsmNetworkMapSigner(
|
||||||
val hsmNetworkMapSigningThread = HsmNetworkMapSigner(
|
|
||||||
networkMapStorage,
|
|
||||||
networkMapCertificateName,
|
networkMapCertificateName,
|
||||||
networkMapPrivateKeyPassword,
|
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<ApprovedCertificateRequestData>) -> Unit = {
|
val sign: (List<ApprovedCertificateRequestData>) -> Unit = {
|
||||||
val signer = HsmCsrSigner(
|
val signer = HsmCsrSigner(
|
||||||
csrStorage,
|
csrStorage,
|
||||||
@ -62,6 +77,7 @@ fun run(parameters: Parameters) {
|
|||||||
Authenticator(createProvider(), authMode, autoUsername, authKeyFilePath, authKeyFilePassword, signAuthThreshold))
|
Authenticator(createProvider(), authMode, autoUsername, authKeyFilePath, authKeyFilePassword, signAuthThreshold))
|
||||||
signer.sign(it)
|
signer.sign(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
Menu().withExceptionHandler(::processError).addItem("1", "Generate root and intermediate certificates", {
|
Menu().withExceptionHandler(::processError).addItem("1", "Generate root and intermediate certificates", {
|
||||||
if (confirmedKeyGen()) {
|
if (confirmedKeyGen()) {
|
||||||
val generator = KeyCertificateGenerator(
|
val generator = KeyCertificateGenerator(
|
||||||
@ -104,13 +120,29 @@ fun run(parameters: Parameters) {
|
|||||||
println("There is no approved and unsigned CSR")
|
println("There is no approved and unsigned CSR")
|
||||||
}
|
}
|
||||||
}).showMenu()
|
}).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) {
|
private fun processError(exception: Exception) {
|
||||||
val processed = mapCryptoServerException(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<ApprovedCertificateRequestData>): Boolean {
|
private fun confirmedSign(selectedItems: List<ApprovedCertificateRequestData>): Boolean {
|
||||||
|
@ -1,78 +1,39 @@
|
|||||||
package com.r3.corda.networkmanage.hsm.signer
|
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.common.signer.Signer
|
||||||
import com.r3.corda.networkmanage.hsm.authentication.Authenticator
|
import com.r3.corda.networkmanage.hsm.authentication.Authenticator
|
||||||
import com.r3.corda.networkmanage.hsm.utils.X509Utilities
|
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.getAndInitializeKeyStore
|
||||||
import com.r3.corda.networkmanage.hsm.utils.X509Utilities.verify
|
import com.r3.corda.networkmanage.hsm.utils.X509Utilities.verify
|
||||||
import net.corda.core.internal.DigitalSignatureWithCert
|
import net.corda.core.internal.DigitalSignatureWithCert
|
||||||
import net.corda.core.utilities.loggerFor
|
import net.corda.nodeapi.internal.crypto.getX509Certificate
|
||||||
import net.corda.core.utilities.minutes
|
|
||||||
import java.security.PrivateKey
|
import java.security.PrivateKey
|
||||||
import java.security.Signature
|
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.
|
* Signer which connects to a HSM using the given [authenticator] to sign bytes.
|
||||||
* It uses HSM as the signing entity with keys and certificates specified at the construction time.
|
|
||||||
*/
|
*/
|
||||||
// TODO Rename this to HsmSigner
|
// TODO Rename this to HsmSigner
|
||||||
class HsmNetworkMapSigner(networkMapStorage: NetworkMapStorage,
|
class HsmNetworkMapSigner(private val certificateKeyName: String,
|
||||||
private val caCertificateKeyName: String,
|
private val privateKeyPassword: String,
|
||||||
private val caPrivateKeyPass: String,
|
private val authenticator: Authenticator) : Signer {
|
||||||
private val authenticator: Authenticator,
|
|
||||||
private val signingPeriod: Duration = DEFAULT_SIGNING_PERIOD_MS) : Signer {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val log = loggerFor<HsmNetworkMapSigner>()
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Signs given data using [CryptoServerJCE.CryptoServerProvider], which connects to the underlying HSM.
|
* Signs given data using [CryptoServerJCE.CryptoServerProvider], which connects to the underlying HSM.
|
||||||
*/
|
*/
|
||||||
override fun signBytes(data: ByteArray): DigitalSignatureWithCert {
|
override fun signBytes(data: ByteArray): DigitalSignatureWithCert {
|
||||||
return authenticator.connectAndAuthenticate { provider, _ ->
|
return authenticator.connectAndAuthenticate { provider, _ ->
|
||||||
val keyStore = getAndInitializeKeyStore(provider)
|
val keyStore = getAndInitializeKeyStore(provider)
|
||||||
val caCertificateChain = keyStore.getCertificateChain(caCertificateKeyName)
|
val certificate = keyStore.getX509Certificate(certificateKeyName)
|
||||||
val caKey = keyStore.getKey(caCertificateKeyName, caPrivateKeyPass.toCharArray()) as PrivateKey
|
// 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 {
|
val signature = Signature.getInstance(X509Utilities.SIGNATURE_ALGORITHM, provider).run {
|
||||||
initSign(caKey)
|
initSign(key)
|
||||||
update(data)
|
update(data)
|
||||||
sign()
|
sign()
|
||||||
}
|
}
|
||||||
verify(data, signature, caCertificateChain[0].publicKey)
|
verify(data, signature, certificate.publicKey)
|
||||||
DigitalSignatureWithCert(caCertificateChain[0] as X509Certificate, signature)
|
DigitalSignatureWithCert(certificate, signature)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,9 +33,9 @@ object HsmErrors {
|
|||||||
fun mapCryptoServerException(exception: Exception): Exception {
|
fun mapCryptoServerException(exception: Exception): Exception {
|
||||||
// Try to decode the error code
|
// Try to decode the error code
|
||||||
val crypto = exception as? CryptoServerException ?: exception.cause as? CryptoServerException
|
val crypto = exception as? CryptoServerException ?: exception.cause as? CryptoServerException
|
||||||
if (crypto != null) {
|
return if (crypto != null) {
|
||||||
return Exception("(CryptoServer) ${HsmErrors.errors[crypto.ErrorCode]}", exception)
|
Exception("(CryptoServer) ${HsmErrors.errors[crypto.ErrorCode]}", exception)
|
||||||
} else {
|
} else {
|
||||||
return exception
|
exception
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -93,13 +93,11 @@
|
|||||||
</changeSet>
|
</changeSet>
|
||||||
<changeSet author="R3.Corda" id="1513267683777-8">
|
<changeSet author="R3.Corda" id="1513267683777-8">
|
||||||
<createTable tableName="network_parameters">
|
<createTable tableName="network_parameters">
|
||||||
<column name="hash" type="NVARCHAR(64)">
|
<column name="version" type="BIGINT">
|
||||||
<constraints nullable="false"/>
|
<constraints nullable="false"/>
|
||||||
</column>
|
</column>
|
||||||
<column name="certificate" type="BLOB"/>
|
<column name="bytes" type="BLOB"/>
|
||||||
<column name="created" type="TIMESTAMP"/>
|
<column name="hash" type="NVARCHAR(64)"/>
|
||||||
<column name="parameters_bytes" type="BLOB"/>
|
|
||||||
<column name="signature" type="BLOB"/>
|
|
||||||
</createTable>
|
</createTable>
|
||||||
</changeSet>
|
</changeSet>
|
||||||
<changeSet author="R3.Corda" id="1513267683777-9">
|
<changeSet author="R3.Corda" id="1513267683777-9">
|
||||||
@ -122,7 +120,7 @@
|
|||||||
</createTable>
|
</createTable>
|
||||||
</changeSet>
|
</changeSet>
|
||||||
<changeSet author="R3.Corda" id="1513267683777-11">
|
<changeSet author="R3.Corda" id="1513267683777-11">
|
||||||
<addPrimaryKey columnNames="hash" constraintName="CONSTRAINT_3" tableName="network_parameters"/>
|
<addPrimaryKey columnNames="version" constraintName="CONSTRAINT_3" tableName="network_parameters"/>
|
||||||
</changeSet>
|
</changeSet>
|
||||||
<changeSet author="R3.Corda" id="1513267683777-12">
|
<changeSet author="R3.Corda" id="1513267683777-12">
|
||||||
<addPrimaryKey columnNames="id" constraintName="CONSTRAINT_7" tableName="certificate_data"/>
|
<addPrimaryKey columnNames="id" constraintName="CONSTRAINT_7" tableName="certificate_data"/>
|
||||||
@ -169,11 +167,6 @@
|
|||||||
<column name="public_key_hash"/>
|
<column name="public_key_hash"/>
|
||||||
</createIndex>
|
</createIndex>
|
||||||
</changeSet>
|
</changeSet>
|
||||||
<changeSet author="R3.Corda" id="network-parameters-idx">
|
|
||||||
<createIndex indexName="IDX_NET_PARAMS_HASH" tableName="network_parameters">
|
|
||||||
<column name="hash"/>
|
|
||||||
</createIndex>
|
|
||||||
</changeSet>
|
|
||||||
<changeSet author="R3.Corda" id="1513267683777-24">
|
<changeSet author="R3.Corda" id="1513267683777-24">
|
||||||
<addForeignKeyConstraint baseColumnNames="rev" baseTableName="certificate_signing_request_AUD"
|
<addForeignKeyConstraint baseColumnNames="rev" baseTableName="certificate_signing_request_AUD"
|
||||||
constraintName="FK5g5cagcrx7siu8lwtavirunxd"
|
constraintName="FK5g5cagcrx7siu8lwtavirunxd"
|
||||||
|
@ -2,5 +2,7 @@
|
|||||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd" >
|
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd" >
|
||||||
|
|
||||||
<include file="migration/network-manager.changelog-init.xml"/>
|
<include file="migration/network-manager.changelog-init.xml"/>
|
||||||
|
<include file="migration/network-manager.changelog-signing-network-params.xml"/>
|
||||||
|
<include file="migration/network-manager.changelog-pub-key-move.xml"/>
|
||||||
|
|
||||||
</databaseChangeLog>
|
</databaseChangeLog>
|
||||||
|
@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
|
||||||
|
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd">
|
||||||
|
<changeSet author="R3.Corda" id="Move public key hash">
|
||||||
|
<delete tableName="certificate_signing_request"/>
|
||||||
|
<delete tableName="certificate_data"/>
|
||||||
|
<addColumn tableName="certificate_signing_request">
|
||||||
|
<column name="public_key_hash" type="NVARCHAR(64)"/>
|
||||||
|
</addColumn>
|
||||||
|
<dropIndex indexName="IDX_PUB_KEY_HASH" tableName="certificate_data"/>
|
||||||
|
<createIndex indexName="IDX_PUB_KEY_HASH" tableName="certificate_signing_request">
|
||||||
|
<column name="public_key_hash"/>
|
||||||
|
</createIndex>
|
||||||
|
<dropColumn columnName="public_key_hash" tableName="certificate_data"/>
|
||||||
|
<delete tableName="CertificateSigningRequestEntity_modifiedBy"/>
|
||||||
|
<delete tableName="CertificateSigningRequestEntity_modifiedBy_AUD"/>
|
||||||
|
<delete tableName="certificate_signing_request_AUD"/>
|
||||||
|
<delete tableName="network_map"/>
|
||||||
|
<delete tableName="network_parameters"/>
|
||||||
|
<delete tableName="node_info"/>
|
||||||
|
<delete tableName="REVINFO"/>
|
||||||
|
</changeSet>
|
||||||
|
</databaseChangeLog>
|
@ -0,0 +1,27 @@
|
|||||||
|
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
|
||||||
|
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd">
|
||||||
|
<changeSet author="R3.Corda" id="signing_network_map_parameters">
|
||||||
|
<dropTable tableName="network_parameters"/>
|
||||||
|
<createTable tableName="network_parameters">
|
||||||
|
<column name="hash" type="NVARCHAR(64)">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="certificate" type="BLOB"/>
|
||||||
|
<column name="created" type="TIMESTAMP"/>
|
||||||
|
<column name="parameters_bytes" type="BLOB"/>
|
||||||
|
<column name="signature" type="BLOB"/>
|
||||||
|
</createTable>
|
||||||
|
<addPrimaryKey columnNames="hash" constraintName="CONSTRAINT_3" tableName="network_parameters"/>
|
||||||
|
<createIndex indexName="IDX_NET_PARAMS_HASH" tableName="network_parameters">
|
||||||
|
<column name="hash"/>
|
||||||
|
</createIndex>
|
||||||
|
<delete tableName="certificate_data"/>
|
||||||
|
<delete tableName="CertificateSigningRequestEntity_modifiedBy"/>
|
||||||
|
<delete tableName="CertificateSigningRequestEntity_modifiedBy_AUD"/>
|
||||||
|
<delete tableName="certificate_signing_request"/>
|
||||||
|
<delete tableName="certificate_signing_request_AUD"/>
|
||||||
|
<delete tableName="network_map"/>
|
||||||
|
<delete tableName="node_info"/>
|
||||||
|
<delete tableName="REVINFO"/>
|
||||||
|
</changeSet>
|
||||||
|
</databaseChangeLog>
|
@ -20,6 +20,7 @@ abstract class TestBase {
|
|||||||
requestId: String = SecureHash.randomSHA256().toString(),
|
requestId: String = SecureHash.randomSHA256().toString(),
|
||||||
status: RequestStatus = RequestStatus.NEW,
|
status: RequestStatus = RequestStatus.NEW,
|
||||||
legalName: String = "TestLegalName",
|
legalName: String = "TestLegalName",
|
||||||
|
publicKeyHash: SecureHash = SecureHash.randomSHA256(),
|
||||||
remark: String = "Test remark",
|
remark: String = "Test remark",
|
||||||
request: PKCS10CertificationRequest = mock(),
|
request: PKCS10CertificationRequest = mock(),
|
||||||
certData: CertificateData = mock(),
|
certData: CertificateData = mock(),
|
||||||
@ -29,6 +30,7 @@ abstract class TestBase {
|
|||||||
requestId = requestId,
|
requestId = requestId,
|
||||||
status = status,
|
status = status,
|
||||||
legalName = legalName,
|
legalName = legalName,
|
||||||
|
publicKeyHash = publicKeyHash,
|
||||||
remark = remark,
|
remark = remark,
|
||||||
certData = certData,
|
certData = certData,
|
||||||
request = request,
|
request = request,
|
||||||
@ -36,11 +38,9 @@ abstract class TestBase {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun certificateData(publicKeyHash: String = SecureHash.randomSHA256().toString(),
|
protected fun certificateData(certStatus: CertificateStatus = CertificateStatus.VALID,
|
||||||
certStatus: CertificateStatus = CertificateStatus.VALID,
|
|
||||||
certPath: CertPath = mock()): CertificateData {
|
certPath: CertPath = mock()): CertificateData {
|
||||||
return CertificateData(
|
return CertificateData(
|
||||||
publicKeyHash = publicKeyHash,
|
|
||||||
certStatus = certStatus,
|
certStatus = certStatus,
|
||||||
certPath = certPath
|
certPath = certPath
|
||||||
)
|
)
|
||||||
|
@ -19,6 +19,7 @@ import org.junit.After
|
|||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.security.KeyPair
|
import java.security.KeyPair
|
||||||
|
import java.security.cert.CertPath
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.security.auth.x500.X500Principal
|
import javax.security.auth.x500.X500Principal
|
||||||
import kotlin.test.*
|
import kotlin.test.*
|
||||||
@ -69,7 +70,7 @@ class PersistentCertificateRequestStorageTest : TestBase() {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `sign request`() {
|
fun `sign request`() {
|
||||||
val (csr, _) = createRequest("LegalName")
|
val (csr, nodeKeyPair) = createRequest("LegalName")
|
||||||
// Add request to DB.
|
// Add request to DB.
|
||||||
val requestId = storage.saveRequest(csr)
|
val requestId = storage.saveRequest(csr)
|
||||||
// New request should equals to 1.
|
// New request should equals to 1.
|
||||||
@ -86,11 +87,7 @@ class PersistentCertificateRequestStorageTest : TestBase() {
|
|||||||
// Sign certificate
|
// Sign certificate
|
||||||
storage.putCertificatePath(
|
storage.putCertificatePath(
|
||||||
requestId,
|
requestId,
|
||||||
JcaPKCS10CertificationRequest(csr).run {
|
generateSignedCertPath(csr, nodeKeyPair),
|
||||||
// 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)
|
|
||||||
},
|
|
||||||
listOf(DOORMAN_SIGNATURE)
|
listOf(DOORMAN_SIGNATURE)
|
||||||
)
|
)
|
||||||
// Check request is ready
|
// Check request is ready
|
||||||
@ -99,33 +96,51 @@ class PersistentCertificateRequestStorageTest : TestBase() {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `sign request ignores subsequent sign requests`() {
|
fun `sign request ignores subsequent sign requests`() {
|
||||||
val (csr, _) = createRequest("LegalName")
|
val (csr, nodeKeyPair) = createRequest("LegalName")
|
||||||
// Add request to DB.
|
// Add request to DB.
|
||||||
val requestId = storage.saveRequest(csr)
|
val requestId = storage.saveRequest(csr)
|
||||||
// Store certificate to DB.
|
// Store certificate to DB.
|
||||||
storage.markRequestTicketCreated(requestId)
|
storage.markRequestTicketCreated(requestId)
|
||||||
storage.approveRequest(requestId, DOORMAN_SIGNATURE)
|
storage.approveRequest(requestId, DOORMAN_SIGNATURE)
|
||||||
|
// Sign certificate
|
||||||
storage.putCertificatePath(
|
storage.putCertificatePath(
|
||||||
requestId,
|
requestId,
|
||||||
JcaPKCS10CertificationRequest(csr).run {
|
generateSignedCertPath(csr, nodeKeyPair),
|
||||||
val (rootCa, intermediateCa, nodeCa) = createDevNodeCaCertPath(CordaX500Name.build(X500Principal(subject.encoded)))
|
|
||||||
buildCertPath(nodeCa.certificate, intermediateCa.certificate, rootCa.certificate)
|
|
||||||
},
|
|
||||||
listOf(DOORMAN_SIGNATURE)
|
listOf(DOORMAN_SIGNATURE)
|
||||||
)
|
)
|
||||||
// Sign certificate
|
|
||||||
// When subsequent signature requested
|
// When subsequent signature requested
|
||||||
assertFailsWith(IllegalArgumentException::class) {
|
assertFailsWith(IllegalArgumentException::class) {
|
||||||
storage.putCertificatePath(
|
storage.putCertificatePath(
|
||||||
requestId,
|
requestId,
|
||||||
JcaPKCS10CertificationRequest(csr).run {
|
generateSignedCertPath(csr, nodeKeyPair),
|
||||||
val (rootCa, intermediateCa, nodeCa) = createDevNodeCaCertPath(CordaX500Name.build(X500Principal(subject.encoded)))
|
|
||||||
buildCertPath(nodeCa.certificate, intermediateCa.certificate, rootCa.certificate)
|
|
||||||
},
|
|
||||||
listOf(DOORMAN_SIGNATURE))
|
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
|
@Test
|
||||||
fun `reject request`() {
|
fun `reject request`() {
|
||||||
val requestId = storage.saveRequest(createRequest("BankA").first)
|
val requestId = storage.saveRequest(createRequest("BankA").first)
|
||||||
@ -159,18 +174,14 @@ class PersistentCertificateRequestStorageTest : TestBase() {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `request with the same legal name as a previously signed request`() {
|
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)
|
val requestId = storage.saveRequest(csr)
|
||||||
storage.markRequestTicketCreated(requestId)
|
storage.markRequestTicketCreated(requestId)
|
||||||
storage.approveRequest(requestId, DOORMAN_SIGNATURE)
|
storage.approveRequest(requestId, DOORMAN_SIGNATURE)
|
||||||
// Sign certificate
|
// Sign certificate
|
||||||
storage.putCertificatePath(
|
storage.putCertificatePath(
|
||||||
requestId,
|
requestId,
|
||||||
JcaPKCS10CertificationRequest(csr).run {
|
generateSignedCertPath(csr, nodeKeyPair),
|
||||||
// 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)
|
|
||||||
},
|
|
||||||
listOf(DOORMAN_SIGNATURE)
|
listOf(DOORMAN_SIGNATURE)
|
||||||
)
|
)
|
||||||
val rejectedRequestId = storage.saveRequest(createRequest("BankA").first)
|
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 {
|
private fun makeTestDataSourceProperties(nodeName: String = SecureHash.randomSHA256().toString()): Properties {
|
||||||
val props = Properties()
|
val props = Properties()
|
||||||
props.setProperty("dataSourceClassName", "org.h2.jdbcx.JdbcDataSource")
|
props.setProperty("dataSourceClassName", "org.h2.jdbcx.JdbcDataSource")
|
||||||
@ -225,8 +244,7 @@ class PersistentCertificateRequestStorageTest : TestBase() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun createRequest(organisation: String): Pair<PKCS10CertificationRequest, KeyPair> {
|
internal fun createRequest(organisation: String, keyPair: KeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)): Pair<PKCS10CertificationRequest, KeyPair> {
|
||||||
val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
|
||||||
val request = X509Utilities.createCertificateSigningRequest(X500Principal("O=$organisation,L=London,C=GB"), "my@mail.com", keyPair)
|
val request = X509Utilities.createCertificateSigningRequest(X500Principal("O=$organisation,L=London,C=GB"), "my@mail.com", keyPair)
|
||||||
return Pair(request, keyPair)
|
return Pair(request, keyPair)
|
||||||
}
|
}
|
@ -5,13 +5,11 @@ import net.corda.core.identity.CordaX500Name
|
|||||||
import net.corda.core.internal.signWithCert
|
import net.corda.core.internal.signWithCert
|
||||||
import net.corda.nodeapi.internal.createDevNetworkMapCa
|
import net.corda.nodeapi.internal.createDevNetworkMapCa
|
||||||
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
|
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.NetworkMap
|
||||||
import net.corda.nodeapi.internal.network.verifiedNetworkMapCert
|
import net.corda.nodeapi.internal.network.verifiedNetworkMapCert
|
||||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||||
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
||||||
import net.corda.testing.common.internal.testNetworkParameters
|
import net.corda.testing.common.internal.testNetworkParameters
|
||||||
import net.corda.testing.internal.TestNodeInfoBuilder
|
|
||||||
import net.corda.testing.internal.createDevIntermediateCaCertPath
|
import net.corda.testing.internal.createDevIntermediateCaCertPath
|
||||||
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
|
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
@ -50,7 +48,7 @@ class PersistentNetworkMapStorageTest : TestBase() {
|
|||||||
fun `saveNetworkMap and saveNetworkParameters create current network map and parameters`() {
|
fun `saveNetworkMap and saveNetworkParameters create current network map and parameters`() {
|
||||||
// given
|
// given
|
||||||
// Create node info.
|
// Create node info.
|
||||||
val signedNodeInfo = createValidSignedNodeInfo("Test")
|
val (signedNodeInfo) = createValidSignedNodeInfo("Test", requestStorage)
|
||||||
val nodeInfoHash = nodeInfoStorage.putNodeInfo(signedNodeInfo)
|
val nodeInfoHash = nodeInfoStorage.putNodeInfo(signedNodeInfo)
|
||||||
|
|
||||||
val networkParameters = testNetworkParameters(emptyList())
|
val networkParameters = testNetworkParameters(emptyList())
|
||||||
@ -65,7 +63,7 @@ class PersistentNetworkMapStorageTest : TestBase() {
|
|||||||
|
|
||||||
// then
|
// then
|
||||||
val persistedSignedNetworkMap = networkMapStorage.getCurrentNetworkMap()
|
val persistedSignedNetworkMap = networkMapStorage.getCurrentNetworkMap()
|
||||||
val persistedSignedParameters = networkMapStorage.getCurrentSignedNetworkParameters()
|
val persistedSignedParameters = networkMapStorage.getNetworkParametersOfNetworkMap()
|
||||||
|
|
||||||
assertEquals(networkParameters, persistedSignedParameters?.verifiedNetworkMapCert(rootCaCert))
|
assertEquals(networkParameters, persistedSignedParameters?.verifiedNetworkMapCert(rootCaCert))
|
||||||
assertEquals(parametersSignature, persistedSignedParameters?.sig)
|
assertEquals(parametersSignature, persistedSignedParameters?.sig)
|
||||||
@ -84,13 +82,13 @@ class PersistentNetworkMapStorageTest : TestBase() {
|
|||||||
networkMapStorage.saveNetworkParameters(params2, null)
|
networkMapStorage.saveNetworkParameters(params2, null)
|
||||||
|
|
||||||
// when
|
// when
|
||||||
val latest = networkMapStorage.getLatestUnsignedNetworkParameters()
|
val latest = networkMapStorage.getLatestNetworkParameters()?.minimumPlatformVersion
|
||||||
// then
|
// then
|
||||||
assertEquals(2, latest.minimumPlatformVersion)
|
assertEquals(2, latest)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `getCurrentNetworkParameters returns current network map parameters`() {
|
fun `getNetworkParametersOfNetworkMap returns current network map parameters`() {
|
||||||
// given
|
// given
|
||||||
// Create network parameters
|
// Create network parameters
|
||||||
val testParameters1 = testNetworkParameters(emptyList())
|
val testParameters1 = testNetworkParameters(emptyList())
|
||||||
@ -107,7 +105,7 @@ class PersistentNetworkMapStorageTest : TestBase() {
|
|||||||
networkMapStorage.saveNetworkParameters(testParameters2, testParameters2.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate).sig)
|
networkMapStorage.saveNetworkParameters(testParameters2, testParameters2.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate).sig)
|
||||||
|
|
||||||
// when
|
// when
|
||||||
val result = networkMapStorage.getCurrentSignedNetworkParameters()?.verifiedNetworkMapCert(rootCaCert)
|
val result = networkMapStorage.getNetworkParametersOfNetworkMap()?.verifiedNetworkMapCert(rootCaCert)
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assertEquals(1, result?.minimumPlatformVersion)
|
assertEquals(1, result?.minimumPlatformVersion)
|
||||||
@ -117,8 +115,8 @@ class PersistentNetworkMapStorageTest : TestBase() {
|
|||||||
fun `getValidNodeInfoHashes returns only valid and signed node info hashes`() {
|
fun `getValidNodeInfoHashes returns only valid and signed node info hashes`() {
|
||||||
// given
|
// given
|
||||||
// Create node infos.
|
// Create node infos.
|
||||||
val signedNodeInfoA = createValidSignedNodeInfo("TestA")
|
val (signedNodeInfoA) = createValidSignedNodeInfo("TestA", requestStorage)
|
||||||
val signedNodeInfoB = createValidSignedNodeInfo("TestB")
|
val (signedNodeInfoB) = createValidSignedNodeInfo("TestB", requestStorage)
|
||||||
|
|
||||||
// Put signed node info data
|
// Put signed node info data
|
||||||
val nodeInfoHashA = nodeInfoStorage.putNodeInfo(signedNodeInfoA)
|
val nodeInfoHashA = nodeInfoStorage.putNodeInfo(signedNodeInfoA)
|
||||||
@ -139,15 +137,4 @@ class PersistentNetworkMapStorageTest : TestBase() {
|
|||||||
// then
|
// then
|
||||||
assertThat(validNodeInfoHash).containsOnly(nodeInfoHashA, nodeInfoHashB)
|
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -9,7 +9,6 @@ import net.corda.core.identity.CordaX500Name
|
|||||||
import net.corda.core.serialization.serialize
|
import net.corda.core.serialization.serialize
|
||||||
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
|
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
|
||||||
import net.corda.nodeapi.internal.crypto.CertificateType
|
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.crypto.X509Utilities
|
||||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||||
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
||||||
@ -23,6 +22,7 @@ import org.junit.Before
|
|||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.security.PrivateKey
|
import java.security.PrivateKey
|
||||||
import java.security.cert.X509Certificate
|
import java.security.cert.X509Certificate
|
||||||
|
import javax.security.auth.x500.X500Principal
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
import kotlin.test.assertNotNull
|
import kotlin.test.assertNotNull
|
||||||
import kotlin.test.assertNull
|
import kotlin.test.assertNull
|
||||||
@ -83,8 +83,8 @@ class PersistentNodeInfoStorageTest : TestBase() {
|
|||||||
@Test
|
@Test
|
||||||
fun `getNodeInfo returns persisted SignedNodeInfo using the hash of just the NodeInfo`() {
|
fun `getNodeInfo returns persisted SignedNodeInfo using the hash of just the NodeInfo`() {
|
||||||
// given
|
// given
|
||||||
val (nodeA) = createValidSignedNodeInfo("TestA")
|
val (nodeA) = createValidSignedNodeInfo("TestA", requestStorage)
|
||||||
val (nodeB) = createValidSignedNodeInfo("TestB")
|
val (nodeB) = createValidSignedNodeInfo("TestB", requestStorage)
|
||||||
|
|
||||||
// Put signed node info data
|
// Put signed node info data
|
||||||
nodeInfoStorage.putNodeInfo(nodeA)
|
nodeInfoStorage.putNodeInfo(nodeA)
|
||||||
@ -102,7 +102,7 @@ class PersistentNodeInfoStorageTest : TestBase() {
|
|||||||
@Test
|
@Test
|
||||||
fun `same public key with different node info`() {
|
fun `same public key with different node info`() {
|
||||||
// Create 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 nodeInfo2 = node1.nodeInfo.copy(serial = 2)
|
||||||
val node2 = NodeInfoWithSigned(nodeInfo2.signWith(listOf(key)))
|
val node2 = NodeInfoWithSigned(nodeInfo2.signWith(listOf(key)))
|
||||||
|
|
||||||
@ -120,7 +120,7 @@ class PersistentNodeInfoStorageTest : TestBase() {
|
|||||||
@Test
|
@Test
|
||||||
fun `putNodeInfo persists SignedNodeInfo with its signature`() {
|
fun `putNodeInfo persists SignedNodeInfo with its signature`() {
|
||||||
// given
|
// given
|
||||||
val (nodeInfoWithSigned) = createValidSignedNodeInfo("Test")
|
val (nodeInfoWithSigned) = createValidSignedNodeInfo("Test", requestStorage)
|
||||||
|
|
||||||
// when
|
// when
|
||||||
val nodeInfoHash = nodeInfoStorage.putNodeInfo(nodeInfoWithSigned)
|
val nodeInfoHash = nodeInfoStorage.putNodeInfo(nodeInfoWithSigned)
|
||||||
@ -129,16 +129,17 @@ class PersistentNodeInfoStorageTest : TestBase() {
|
|||||||
val persistedSignedNodeInfo = nodeInfoStorage.getNodeInfo(nodeInfoHash)
|
val persistedSignedNodeInfo = nodeInfoStorage.getNodeInfo(nodeInfoHash)
|
||||||
assertThat(persistedSignedNodeInfo?.signatures).isEqualTo(nodeInfoWithSigned.signedNodeInfo.signatures)
|
assertThat(persistedSignedNodeInfo?.signatures).isEqualTo(nodeInfoWithSigned.signedNodeInfo.signatures)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun createValidSignedNodeInfo(organisation: String, serial: Long = 1): Pair<NodeInfoWithSigned, PrivateKey> {
|
internal fun createValidSignedNodeInfo(organisation: String,
|
||||||
|
storage: CertificationRequestStorage): Pair<NodeInfoWithSigned, PrivateKey> {
|
||||||
|
val (csr, nodeKeyPair) = createRequest(organisation)
|
||||||
|
val requestId = storage.saveRequest(csr)
|
||||||
|
storage.markRequestTicketCreated(requestId)
|
||||||
|
storage.approveRequest(requestId, "TestUser")
|
||||||
val nodeInfoBuilder = TestNodeInfoBuilder()
|
val nodeInfoBuilder = TestNodeInfoBuilder()
|
||||||
val requestId = requestStorage.saveRequest(createRequest(organisation).first)
|
val (identity, key) = nodeInfoBuilder.addIdentity(CordaX500Name.build(X500Principal(csr.subject.encoded)), nodeKeyPair)
|
||||||
requestStorage.markRequestTicketCreated(requestId)
|
storage.putCertificatePath(requestId, identity.certPath, listOf("Test"))
|
||||||
requestStorage.approveRequest(requestId, "TestUser")
|
val (_, signedNodeInfo) = nodeInfoBuilder.buildWithSigned(1)
|
||||||
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)
|
return Pair(NodeInfoWithSigned(signedNodeInfo), key)
|
||||||
}
|
}
|
||||||
}
|
|
@ -51,8 +51,8 @@ class NetworkMapSignerTest : TestBase() {
|
|||||||
val signedNetworkMap = networkMap.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate)
|
val signedNetworkMap = networkMap.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate)
|
||||||
whenever(networkMapStorage.getCurrentNetworkMap()).thenReturn(signedNetworkMap)
|
whenever(networkMapStorage.getCurrentNetworkMap()).thenReturn(signedNetworkMap)
|
||||||
whenever(networkMapStorage.getNodeInfoHashes(any())).thenReturn(signedNodeInfoHashes)
|
whenever(networkMapStorage.getNodeInfoHashes(any())).thenReturn(signedNodeInfoHashes)
|
||||||
whenever(networkMapStorage.getLatestUnsignedNetworkParameters()).thenReturn(latestNetworkParameters)
|
whenever(networkMapStorage.getLatestNetworkParameters()).thenReturn(latestNetworkParameters)
|
||||||
whenever(networkMapStorage.getCurrentSignedNetworkParameters()).thenReturn(currentParameters.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate))
|
whenever(networkMapStorage.getNetworkParametersOfNetworkMap()).thenReturn(currentParameters.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate))
|
||||||
whenever(signer.signBytes(any())).then {
|
whenever(signer.signBytes(any())).then {
|
||||||
DigitalSignatureWithCert(networkMapCa.certificate, Crypto.doSign(networkMapCa.keyPair.private, it.arguments[0] as ByteArray))
|
DigitalSignatureWithCert(networkMapCa.certificate, Crypto.doSign(networkMapCa.keyPair.private, it.arguments[0] as ByteArray))
|
||||||
}
|
}
|
||||||
@ -67,8 +67,8 @@ class NetworkMapSignerTest : TestBase() {
|
|||||||
// then
|
// then
|
||||||
// Verify networkMapStorage calls
|
// Verify networkMapStorage calls
|
||||||
verify(networkMapStorage).getNodeInfoHashes(any())
|
verify(networkMapStorage).getNodeInfoHashes(any())
|
||||||
verify(networkMapStorage).getLatestUnsignedNetworkParameters()
|
verify(networkMapStorage).getLatestNetworkParameters()
|
||||||
verify(networkMapStorage).getCurrentSignedNetworkParameters()
|
verify(networkMapStorage).getNetworkParametersOfNetworkMap()
|
||||||
argumentCaptor<SignedNetworkMap>().apply {
|
argumentCaptor<SignedNetworkMap>().apply {
|
||||||
verify(networkMapStorage).saveNetworkMap(capture())
|
verify(networkMapStorage).saveNetworkMap(capture())
|
||||||
val capturedNetworkMap = firstValue.verifiedNetworkMapCert(rootCaCert)
|
val capturedNetworkMap = firstValue.verifiedNetworkMapCert(rootCaCert)
|
||||||
@ -87,8 +87,8 @@ class NetworkMapSignerTest : TestBase() {
|
|||||||
val signedNetworkMap = networkMap.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate)
|
val signedNetworkMap = networkMap.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate)
|
||||||
whenever(networkMapStorage.getCurrentNetworkMap()).thenReturn(signedNetworkMap)
|
whenever(networkMapStorage.getCurrentNetworkMap()).thenReturn(signedNetworkMap)
|
||||||
whenever(networkMapStorage.getNodeInfoHashes(any())).thenReturn(emptyList())
|
whenever(networkMapStorage.getNodeInfoHashes(any())).thenReturn(emptyList())
|
||||||
whenever(networkMapStorage.getLatestUnsignedNetworkParameters()).thenReturn(networkParameters)
|
whenever(networkMapStorage.getLatestNetworkParameters()).thenReturn(networkParameters)
|
||||||
whenever(networkMapStorage.getCurrentSignedNetworkParameters()).thenReturn(networkParameters.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate))
|
whenever(networkMapStorage.getNetworkParametersOfNetworkMap()).thenReturn(networkParameters.signWithCert(networkMapCa.keyPair.private, networkMapCa.certificate))
|
||||||
|
|
||||||
// when
|
// when
|
||||||
networkMapSigner.signNetworkMap()
|
networkMapSigner.signNetworkMap()
|
||||||
@ -104,7 +104,7 @@ class NetworkMapSignerTest : TestBase() {
|
|||||||
val networkParameters = testNetworkParameters(emptyList())
|
val networkParameters = testNetworkParameters(emptyList())
|
||||||
whenever(networkMapStorage.getCurrentNetworkMap()).thenReturn(null)
|
whenever(networkMapStorage.getCurrentNetworkMap()).thenReturn(null)
|
||||||
whenever(networkMapStorage.getNodeInfoHashes(any())).thenReturn(emptyList())
|
whenever(networkMapStorage.getNodeInfoHashes(any())).thenReturn(emptyList())
|
||||||
whenever(networkMapStorage.getLatestUnsignedNetworkParameters()).thenReturn(networkParameters)
|
whenever(networkMapStorage.getLatestNetworkParameters()).thenReturn(networkParameters)
|
||||||
whenever(signer.signBytes(any())).then {
|
whenever(signer.signBytes(any())).then {
|
||||||
DigitalSignatureWithCert(networkMapCa.certificate, Crypto.doSign(networkMapCa.keyPair.private, it.arguments[0] as ByteArray))
|
DigitalSignatureWithCert(networkMapCa.certificate, Crypto.doSign(networkMapCa.keyPair.private, it.arguments[0] as ByteArray))
|
||||||
}
|
}
|
||||||
@ -118,7 +118,7 @@ class NetworkMapSignerTest : TestBase() {
|
|||||||
// then
|
// then
|
||||||
// Verify networkMapStorage calls
|
// Verify networkMapStorage calls
|
||||||
verify(networkMapStorage).getNodeInfoHashes(any())
|
verify(networkMapStorage).getNodeInfoHashes(any())
|
||||||
verify(networkMapStorage).getLatestUnsignedNetworkParameters()
|
verify(networkMapStorage).getLatestNetworkParameters()
|
||||||
argumentCaptor<SignedNetworkMap>().apply {
|
argumentCaptor<SignedNetworkMap>().apply {
|
||||||
verify(networkMapStorage).saveNetworkMap(capture())
|
verify(networkMapStorage).saveNetworkMap(capture())
|
||||||
val networkMap = firstValue.verifiedNetworkMapCert(rootCaCert)
|
val networkMap = firstValue.verifiedNetworkMapCert(rootCaCert)
|
||||||
|
@ -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())
|
|
||||||
}
|
|
||||||
}
|
|
@ -52,6 +52,5 @@ class DoormanParametersTest {
|
|||||||
assertEquals("TD", parameter.jiraConfig?.projectCode)
|
assertEquals("TD", parameter.jiraConfig?.projectCode)
|
||||||
assertEquals("username", parameter.jiraConfig?.username)
|
assertEquals("username", parameter.jiraConfig?.username)
|
||||||
assertEquals("password", parameter.jiraConfig?.password)
|
assertEquals("password", parameter.jiraConfig?.password)
|
||||||
assertEquals(41, parameter.jiraConfig?.doneTransitionCode)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
@ -1,87 +0,0 @@
|
|||||||
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.doorman.JiraClient
|
|
||||||
import net.corda.core.crypto.Crypto
|
|
||||||
import net.corda.core.identity.CordaX500Name
|
|
||||||
import net.corda.nodeapi.internal.crypto.X509Utilities
|
|
||||||
import org.junit.Before
|
|
||||||
import org.junit.Rule
|
|
||||||
import org.junit.Test
|
|
||||||
import org.mockito.Mock
|
|
||||||
import org.mockito.junit.MockitoJUnit
|
|
||||||
import java.security.cert.CertPath
|
|
||||||
|
|
||||||
class JiraCsrHandlerTest {
|
|
||||||
|
|
||||||
@Rule
|
|
||||||
@JvmField
|
|
||||||
val mockitoRule = MockitoJUnit.rule()
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
lateinit var jiraClient: JiraClient
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
lateinit var certificationRequestStorage: CertificationRequestStorage
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
lateinit var defaultCsrHandler: DefaultCsrHandler
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
var certPath: CertPath = mock()
|
|
||||||
|
|
||||||
private lateinit var jiraCsrHandler: JiraCsrHandler
|
|
||||||
private val requestId = "id"
|
|
||||||
private lateinit var certificateResponse: CertificateResponse.Ready
|
|
||||||
|
|
||||||
private val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
|
||||||
private val pkcS10CertificationRequest = X509Utilities.createCertificateSigningRequest(
|
|
||||||
CordaX500Name(locality = "London", organisation = "LegalName", country = "GB").x500Principal,
|
|
||||||
"my@mail.com",
|
|
||||||
keyPair)
|
|
||||||
|
|
||||||
@Before
|
|
||||||
fun setup() {
|
|
||||||
jiraCsrHandler = JiraCsrHandler(jiraClient, certificationRequestStorage, defaultCsrHandler)
|
|
||||||
certificateResponse = CertificateResponse.Ready(certPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `If jira connection fails we don't mark the ticket as created`() {
|
|
||||||
whenever(defaultCsrHandler.saveRequest(any())).thenReturn(requestId)
|
|
||||||
whenever(defaultCsrHandler.getResponse(requestId)).thenReturn(certificateResponse)
|
|
||||||
whenever(jiraClient.createRequestTicket(eq(requestId), any())).thenThrow(IllegalStateException("something broke"))
|
|
||||||
|
|
||||||
// Test
|
|
||||||
jiraCsrHandler.saveRequest(pkcS10CertificationRequest)
|
|
||||||
|
|
||||||
verify(certificationRequestStorage, never()).markRequestTicketCreated(requestId)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `If jira connection works we mark the ticket as created`() {
|
|
||||||
whenever(defaultCsrHandler.saveRequest(any())).thenReturn(requestId)
|
|
||||||
whenever(defaultCsrHandler.getResponse(requestId)).thenReturn(certificateResponse)
|
|
||||||
|
|
||||||
// Test
|
|
||||||
jiraCsrHandler.saveRequest(pkcS10CertificationRequest)
|
|
||||||
|
|
||||||
verify(certificationRequestStorage, times(1)).markRequestTicketCreated(requestId)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `create tickets`() {
|
|
||||||
val csr = CertificateSigningRequest(requestId, "name", RequestStatus.NEW, pkcS10CertificationRequest, null, emptyList(), null)
|
|
||||||
whenever(certificationRequestStorage.getRequests(RequestStatus.NEW)).thenReturn(listOf(csr))
|
|
||||||
|
|
||||||
// Test
|
|
||||||
jiraCsrHandler.createTickets()
|
|
||||||
|
|
||||||
verify(jiraClient).createRequestTicket(requestId, csr.request)
|
|
||||||
verify(certificationRequestStorage).markRequestTicketCreated(requestId)
|
|
||||||
}
|
|
||||||
}
|
|
@ -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.processRequests()
|
||||||
|
|
||||||
|
val certPathCapture = argumentCaptor<CertPath>()
|
||||||
|
|
||||||
|
// 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"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,151 @@
|
|||||||
|
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
|
||||||
|
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
|
||||||
|
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
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
class JiraCsrHandlerTest : TestBase() {
|
||||||
|
@Rule
|
||||||
|
@JvmField
|
||||||
|
val mockitoRule: MockitoRule = MockitoJUnit.rule()
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private lateinit var jiraClient: JiraClient
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private lateinit var certificationRequestStorage: CertificationRequestStorage
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private lateinit var defaultCsrHandler: DefaultCsrHandler
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private val certPath: CertPath = mock()
|
||||||
|
|
||||||
|
private lateinit var jiraCsrHandler: JiraCsrHandler
|
||||||
|
private val requestId = "id"
|
||||||
|
private lateinit var certificateResponse: CertificateResponse.Ready
|
||||||
|
|
||||||
|
private val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||||
|
private val pkcS10CertificationRequest = X509Utilities.createCertificateSigningRequest(
|
||||||
|
CordaX500Name(locality = "London", organisation = "LegalName", country = "GB").x500Principal,
|
||||||
|
"my@mail.com",
|
||||||
|
keyPair)
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
jiraCsrHandler = JiraCsrHandler(jiraClient, certificationRequestStorage, defaultCsrHandler)
|
||||||
|
certificateResponse = CertificateResponse.Ready(certPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `If jira connection fails we don't mark the ticket as created`() {
|
||||||
|
whenever(defaultCsrHandler.saveRequest(any())).thenReturn(requestId)
|
||||||
|
whenever(defaultCsrHandler.getResponse(requestId)).thenReturn(certificateResponse)
|
||||||
|
whenever(jiraClient.createRequestTicket(eq(requestId), any())).thenThrow(IllegalStateException("something broke"))
|
||||||
|
|
||||||
|
// Test
|
||||||
|
jiraCsrHandler.saveRequest(pkcS10CertificationRequest)
|
||||||
|
|
||||||
|
verify(certificationRequestStorage, never()).markRequestTicketCreated(requestId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `If jira connection works we mark the ticket as created`() {
|
||||||
|
whenever(defaultCsrHandler.saveRequest(any())).thenReturn(requestId)
|
||||||
|
whenever(defaultCsrHandler.getResponse(requestId)).thenReturn(certificateResponse)
|
||||||
|
|
||||||
|
// Test
|
||||||
|
jiraCsrHandler.saveRequest(pkcS10CertificationRequest)
|
||||||
|
|
||||||
|
verify(certificationRequestStorage, times(1)).markRequestTicketCreated(requestId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `create tickets`() {
|
||||||
|
val csr = certificateSigningRequest(
|
||||||
|
requestId = requestId,
|
||||||
|
legalName = "name",
|
||||||
|
status = RequestStatus.NEW,
|
||||||
|
request = pkcS10CertificationRequest)
|
||||||
|
whenever(certificationRequestStorage.getRequests(RequestStatus.NEW)).thenReturn(listOf(csr))
|
||||||
|
|
||||||
|
// Test
|
||||||
|
jiraCsrHandler.processRequests()
|
||||||
|
|
||||||
|
verify(jiraClient).createRequestTicket(requestId, csr.request)
|
||||||
|
verify(certificationRequestStorage).markRequestTicketCreated(requestId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `sync tickets status`() {
|
||||||
|
val id1 = SecureHash.randomSHA256().toString()
|
||||||
|
val id2 = SecureHash.randomSHA256().toString()
|
||||||
|
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)
|
||||||
|
|
||||||
|
// 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<String>(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<String>(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<CertPath>()
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
@ -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.mock
|
||||||
import com.nhaarman.mockito_kotlin.times
|
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.persistence.NodeInfoStorage
|
||||||
import com.r3.corda.networkmanage.common.utils.SignedNetworkMap
|
import com.r3.corda.networkmanage.common.utils.SignedNetworkMap
|
||||||
import com.r3.corda.networkmanage.common.utils.SignedNetworkParameters
|
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.crypto.SecureHash.Companion.randomSHA256
|
||||||
import net.corda.core.identity.CordaX500Name
|
import net.corda.core.identity.CordaX500Name
|
||||||
import net.corda.core.internal.checkOkResponse
|
import net.corda.core.internal.checkOkResponse
|
||||||
@ -36,7 +37,7 @@ import java.security.cert.X509Certificate
|
|||||||
import javax.ws.rs.core.MediaType
|
import javax.ws.rs.core.MediaType
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
class NodeInfoWebServiceTest {
|
class NetworkMapWebServiceTest {
|
||||||
@Rule
|
@Rule
|
||||||
@JvmField
|
@JvmField
|
||||||
val testSerialization = SerializationEnvironmentRule(true)
|
val testSerialization = SerializationEnvironmentRule(true)
|
||||||
@ -56,12 +57,12 @@ class NodeInfoWebServiceTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `submit nodeInfo`() {
|
fun `submit nodeInfo`() {
|
||||||
val networkMapStorage: NetworkMapStorage = mock {
|
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.
|
// Create node info.
|
||||||
val (_, signedNodeInfo) = createNodeInfoAndSigned(CordaX500Name("Test", "London", "GB"))
|
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()
|
it.start()
|
||||||
val nodeInfoAndSignature = signedNodeInfo.serialize().bytes
|
val nodeInfoAndSignature = signedNodeInfo.serialize().bytes
|
||||||
// Post node info and signature to doorman, this should pass without any exception.
|
// Post node info and signature to doorman, this should pass without any exception.
|
||||||
@ -72,12 +73,12 @@ class NodeInfoWebServiceTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `submit old nodeInfo`() {
|
fun `submit old nodeInfo`() {
|
||||||
val networkMapStorage: NetworkMapStorage = mock {
|
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.
|
// Create node info.
|
||||||
val (_, signedNodeInfo) = createNodeInfoAndSigned(CordaX500Name("Test", "London", "GB"), platformVersion = 1)
|
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()
|
it.start()
|
||||||
val nodeInfoAndSignature = signedNodeInfo.serialize().bytes
|
val nodeInfoAndSignature = signedNodeInfo.serialize().bytes
|
||||||
assertThatThrownBy { it.doPost("publish", nodeInfoAndSignature) }
|
assertThatThrownBy { it.doPost("publish", nodeInfoAndSignature) }
|
||||||
@ -88,12 +89,12 @@ class NodeInfoWebServiceTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `submit nodeInfo when no network parameters`() {
|
fun `submit nodeInfo when no network parameters`() {
|
||||||
val networkMapStorage: NetworkMapStorage = mock {
|
val networkMapStorage: NetworkMapStorage = mock {
|
||||||
on { getCurrentSignedNetworkParameters() }.thenReturn(null)
|
on { getNetworkParametersOfNetworkMap() }.thenReturn(null)
|
||||||
}
|
}
|
||||||
// Create node info.
|
// Create node info.
|
||||||
val (_, signedNodeInfo) = createNodeInfoAndSigned(CordaX500Name("Test", "London", "GB"), platformVersion = 1)
|
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()
|
it.start()
|
||||||
val nodeInfoAndSignature = signedNodeInfo.serialize().bytes
|
val nodeInfoAndSignature = signedNodeInfo.serialize().bytes
|
||||||
assertThatThrownBy { it.doPost("publish", nodeInfoAndSignature) }
|
assertThatThrownBy { it.doPost("publish", nodeInfoAndSignature) }
|
||||||
@ -110,7 +111,7 @@ class NodeInfoWebServiceTest {
|
|||||||
on { getCurrentNetworkMap() }.thenReturn(signedNetworkMap)
|
on { getCurrentNetworkMap() }.thenReturn(signedNetworkMap)
|
||||||
}
|
}
|
||||||
|
|
||||||
NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NodeInfoWebService(mock(), networkMapStorage, testNetworkMapConfig)).use {
|
NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), NetworkMapWebService(mock(), networkMapStorage, testNetworkMapConfig)).use {
|
||||||
it.start()
|
it.start()
|
||||||
val signedNetworkMapResponse = it.doGet<SignedNetworkMap>("")
|
val signedNetworkMapResponse = it.doGet<SignedNetworkMap>("")
|
||||||
verify(networkMapStorage, times(1)).getCurrentNetworkMap()
|
verify(networkMapStorage, times(1)).getCurrentNetworkMap()
|
||||||
@ -127,7 +128,7 @@ class NodeInfoWebServiceTest {
|
|||||||
on { getNodeInfo(nodeInfoHash) }.thenReturn(signedNodeInfo)
|
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()
|
it.start()
|
||||||
val nodeInfoResponse = it.doGet<SignedNodeInfo>("node-info/$nodeInfoHash")
|
val nodeInfoResponse = it.doGet<SignedNodeInfo>("node-info/$nodeInfoHash")
|
||||||
verify(nodeInfoStorage, times(1)).getNodeInfo(nodeInfoHash)
|
verify(nodeInfoStorage, times(1)).getNodeInfo(nodeInfoHash)
|
||||||
@ -149,7 +150,7 @@ class NodeInfoWebServiceTest {
|
|||||||
on { getSignedNetworkParameters(networkParametersHash) }.thenReturn(signedNetworkParameters)
|
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()
|
it.start()
|
||||||
val netParamsResponse = it.doGet<SignedNetworkParameters>("network-parameters/$networkParametersHash")
|
val netParamsResponse = it.doGet<SignedNetworkParameters>("network-parameters/$networkParametersHash")
|
||||||
verify(networkMapStorage, times(1)).getSignedNetworkParameters(networkParametersHash)
|
verify(networkMapStorage, times(1)).getSignedNetworkParameters(networkParametersHash)
|
@ -1,11 +1,11 @@
|
|||||||
package com.r3.corda.networkmanage.doorman
|
package com.r3.corda.networkmanage.doorman.webservice
|
||||||
|
|
||||||
import com.nhaarman.mockito_kotlin.*
|
import com.nhaarman.mockito_kotlin.*
|
||||||
import com.r3.corda.networkmanage.TestBase
|
import com.r3.corda.networkmanage.TestBase
|
||||||
import com.r3.corda.networkmanage.common.persistence.CertificateResponse
|
import com.r3.corda.networkmanage.common.persistence.CertificateResponse
|
||||||
import com.r3.corda.networkmanage.common.utils.buildCertPath
|
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.signer.CsrHandler
|
||||||
import com.r3.corda.networkmanage.doorman.webservice.RegistrationWebService
|
|
||||||
import net.corda.core.crypto.Crypto
|
import net.corda.core.crypto.Crypto
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.identity.CordaX500Name
|
import net.corda.core.identity.CordaX500Name
|
||||||
@ -100,7 +100,7 @@ class RegistrationWebServiceTest : TestBase() {
|
|||||||
CertificateResponse.Ready(it)
|
CertificateResponse.Ready(it)
|
||||||
} ?: CertificateResponse.NotReady
|
} ?: CertificateResponse.NotReady
|
||||||
}
|
}
|
||||||
on { processApprovedRequests() }.then {
|
on { processRequests() }.then {
|
||||||
val request = X509Utilities.createCertificateSigningRequest(subject, "my@mail.com", keyPair)
|
val request = X509Utilities.createCertificateSigningRequest(subject, "my@mail.com", keyPair)
|
||||||
certificateStore[id] = JcaPKCS10CertificationRequest(request).run {
|
certificateStore[id] = JcaPKCS10CertificationRequest(request).run {
|
||||||
val tlsCert = X509Utilities.createCertificate(
|
val tlsCert = X509Utilities.createCertificate(
|
||||||
@ -118,7 +118,7 @@ class RegistrationWebServiceTest : TestBase() {
|
|||||||
startSigningServer(requestProcessor)
|
startSigningServer(requestProcessor)
|
||||||
assertThat(pollForResponse(id)).isEqualTo(PollResponse.NotReady)
|
assertThat(pollForResponse(id)).isEqualTo(PollResponse.NotReady)
|
||||||
|
|
||||||
requestProcessor.processApprovedRequests()
|
requestProcessor.processRequests()
|
||||||
|
|
||||||
val certificates = (pollForResponse(id) as PollResponse.Ready).certChain
|
val certificates = (pollForResponse(id) as PollResponse.Ready).certChain
|
||||||
verify(requestProcessor, times(2)).getResponse(any())
|
verify(requestProcessor, times(2)).getResponse(any())
|
||||||
@ -141,7 +141,7 @@ class RegistrationWebServiceTest : TestBase() {
|
|||||||
CertificateResponse.Ready(it)
|
CertificateResponse.Ready(it)
|
||||||
} ?: CertificateResponse.NotReady
|
} ?: CertificateResponse.NotReady
|
||||||
}
|
}
|
||||||
on { processApprovedRequests() }.then {
|
on { processRequests() }.then {
|
||||||
val request = X509Utilities.createCertificateSigningRequest(
|
val request = X509Utilities.createCertificateSigningRequest(
|
||||||
CordaX500Name(locality = "London", organisation = "Legal Name", country = "GB").x500Principal,
|
CordaX500Name(locality = "London", organisation = "Legal Name", country = "GB").x500Principal,
|
||||||
"my@mail.com",
|
"my@mail.com",
|
||||||
@ -165,7 +165,7 @@ class RegistrationWebServiceTest : TestBase() {
|
|||||||
|
|
||||||
startSigningServer(storage)
|
startSigningServer(storage)
|
||||||
assertThat(pollForResponse(id)).isEqualTo(PollResponse.NotReady)
|
assertThat(pollForResponse(id)).isEqualTo(PollResponse.NotReady)
|
||||||
storage.processApprovedRequests()
|
storage.processRequests()
|
||||||
|
|
||||||
val certificates = (pollForResponse(id) as PollResponse.Ready).certChain
|
val certificates = (pollForResponse(id) as PollResponse.Ready).certChain
|
||||||
verify(storage, times(2)).getResponse(any())
|
verify(storage, times(2)).getResponse(any())
|
@ -1,11 +1,12 @@
|
|||||||
package net.corda.nodeapi.internal
|
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.MessageRecipientGroup
|
||||||
import net.corda.core.messaging.MessageRecipients
|
import net.corda.core.messaging.MessageRecipients
|
||||||
import net.corda.core.messaging.SingleMessageRecipient
|
import net.corda.core.messaging.SingleMessageRecipient
|
||||||
import net.corda.core.serialization.CordaSerializable
|
import net.corda.core.serialization.CordaSerializable
|
||||||
import net.corda.core.utilities.NetworkHostAndPort
|
import net.corda.core.utilities.NetworkHostAndPort
|
||||||
import net.corda.core.utilities.toBase58String
|
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -23,7 +24,7 @@ class ArtemisMessagingComponent {
|
|||||||
const val PEER_USER = "SystemUsers/Peer"
|
const val PEER_USER = "SystemUsers/Peer"
|
||||||
const val INTERNAL_PREFIX = "internal."
|
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 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"
|
const val NOTIFICATIONS_ADDRESS = "${INTERNAL_PREFIX}activemq.notifications"
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,7 +50,7 @@ class ArtemisMessagingComponent {
|
|||||||
@CordaSerializable
|
@CordaSerializable
|
||||||
data class NodeAddress(override val queueName: String, override val hostAndPort: NetworkHostAndPort) : ArtemisPeerAddress {
|
data class NodeAddress(override val queueName: String, override val hostAndPort: NetworkHostAndPort) : ArtemisPeerAddress {
|
||||||
constructor(peerIdentity: PublicKey, hostAndPort: NetworkHostAndPort) :
|
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.
|
* @param identity The service identity's owning key.
|
||||||
*/
|
*/
|
||||||
data class ServiceAddress(val identity: PublicKey) : ArtemisAddress, MessageRecipientGroup {
|
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()}"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package net.corda.nodeapi.internal
|
package net.corda.nodeapi.internal
|
||||||
|
|
||||||
import net.corda.core.crypto.Crypto
|
import net.corda.core.crypto.Crypto
|
||||||
|
import net.corda.core.crypto.Crypto.generateKeyPair
|
||||||
import net.corda.core.identity.CordaX500Name
|
import net.corda.core.identity.CordaX500Name
|
||||||
import net.corda.core.internal.x500Name
|
import net.corda.core.internal.x500Name
|
||||||
import net.corda.nodeapi.internal.config.SSLConfiguration
|
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.GeneralName
|
||||||
import org.bouncycastle.asn1.x509.GeneralSubtree
|
import org.bouncycastle.asn1.x509.GeneralSubtree
|
||||||
import org.bouncycastle.asn1.x509.NameConstraints
|
import org.bouncycastle.asn1.x509.NameConstraints
|
||||||
|
import java.security.KeyPair
|
||||||
import java.security.cert.X509Certificate
|
import java.security.cert.X509Certificate
|
||||||
import javax.security.auth.x500.X500Principal
|
import javax.security.auth.x500.X500Principal
|
||||||
|
|
||||||
@ -31,7 +33,7 @@ fun SSLConfiguration.createDevKeyStores(legalName: CordaX500Name,
|
|||||||
save(nodeKeystore, keyStorePassword)
|
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)
|
val tlsCert = X509Utilities.createCertificate(CertificateType.TLS, nodeCaCert, nodeCaKeyPair, legalName.x500Principal, tlsKeyPair.public)
|
||||||
|
|
||||||
loadOrCreateKeyStore(sslKeystore, keyStorePassword).apply {
|
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
|
* 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.
|
* [CordaX500Name] as the cert subject.
|
||||||
*/
|
*/
|
||||||
fun createDevNodeCa(intermediateCa: CertificateAndKeyPair, legalName: CordaX500Name): CertificateAndKeyPair {
|
fun createDevNodeCa(intermediateCa: CertificateAndKeyPair,
|
||||||
val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
legalName: CordaX500Name,
|
||||||
|
nodeKeyPair: KeyPair = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)): CertificateAndKeyPair {
|
||||||
val nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, legalName.x500Name))), arrayOf())
|
val nameConstraints = NameConstraints(arrayOf(GeneralSubtree(GeneralName(GeneralName.directoryName, legalName.x500Name))), arrayOf())
|
||||||
val cert = X509Utilities.createCertificate(
|
val cert = X509Utilities.createCertificate(
|
||||||
CertificateType.NODE_CA,
|
CertificateType.NODE_CA,
|
||||||
intermediateCa.certificate,
|
intermediateCa.certificate,
|
||||||
intermediateCa.keyPair,
|
intermediateCa.keyPair,
|
||||||
legalName.x500Principal,
|
legalName.x500Principal,
|
||||||
keyPair.public,
|
nodeKeyPair.public,
|
||||||
nameConstraints = nameConstraints)
|
nameConstraints = nameConstraints)
|
||||||
return CertificateAndKeyPair(cert, keyPair)
|
return CertificateAndKeyPair(cert, nodeKeyPair)
|
||||||
}
|
}
|
||||||
|
|
||||||
val DEV_INTERMEDIATE_CA: CertificateAndKeyPair get() = DevCaHelper.loadDevCa(X509Utilities.CORDA_INTERMEDIATE_CA)
|
val DEV_INTERMEDIATE_CA: CertificateAndKeyPair get() = DevCaHelper.loadDevCa(X509Utilities.CORDA_INTERMEDIATE_CA)
|
||||||
|
@ -1,13 +1,16 @@
|
|||||||
package net.corda.nodeapi.internal.persistence
|
package net.corda.nodeapi.internal.persistence
|
||||||
|
|
||||||
|
import co.paralleluniverse.strands.Strand
|
||||||
import net.corda.core.schemas.MappedSchema
|
import net.corda.core.schemas.MappedSchema
|
||||||
import net.corda.core.utilities.contextLogger
|
import net.corda.core.utilities.contextLogger
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.Subscriber
|
import rx.Subscriber
|
||||||
|
import rx.subjects.PublishSubject
|
||||||
import rx.subjects.UnicastSubject
|
import rx.subjects.UnicastSubject
|
||||||
import java.io.Closeable
|
import java.io.Closeable
|
||||||
import java.sql.Connection
|
import java.sql.Connection
|
||||||
import java.sql.SQLException
|
import java.sql.SQLException
|
||||||
|
import java.util.*
|
||||||
import java.util.concurrent.CopyOnWriteArrayList
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
import javax.persistence.AttributeConverter
|
import javax.persistence.AttributeConverter
|
||||||
import javax.sql.DataSource
|
import javax.sql.DataSource
|
||||||
@ -40,6 +43,11 @@ enum class TransactionIsolationLevel {
|
|||||||
val jdbcValue: Int = java.sql.Connection::class.java.getField(jdbcString).get(null) as Int
|
val jdbcValue: Int = java.sql.Connection::class.java.getField(jdbcString).get(null) as Int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val _contextDatabase = ThreadLocal<CordaPersistence>()
|
||||||
|
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(
|
class CordaPersistence(
|
||||||
val dataSource: DataSource,
|
val dataSource: DataSource,
|
||||||
databaseConfig: DatabaseConfig,
|
databaseConfig: DatabaseConfig,
|
||||||
@ -51,7 +59,7 @@ class CordaPersistence(
|
|||||||
private val log = contextLogger()
|
private val log = contextLogger()
|
||||||
}
|
}
|
||||||
|
|
||||||
val defaultIsolationLevel = databaseConfig.transactionIsolationLevel
|
private val defaultIsolationLevel = databaseConfig.transactionIsolationLevel
|
||||||
val hibernateConfig: HibernateConfiguration by lazy {
|
val hibernateConfig: HibernateConfiguration by lazy {
|
||||||
|
|
||||||
transaction {
|
transaction {
|
||||||
@ -60,8 +68,19 @@ class CordaPersistence(
|
|||||||
}
|
}
|
||||||
val entityManagerFactory get() = hibernateConfig.sessionFactoryForRegisteredSchemas
|
val entityManagerFactory get() = hibernateConfig.sessionFactoryForRegisteredSchemas
|
||||||
|
|
||||||
|
data class Boundary(val txId: UUID)
|
||||||
|
|
||||||
|
internal val transactionBoundaries = PublishSubject.create<Boundary>().toSerialized()
|
||||||
|
|
||||||
init {
|
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.
|
// Check not in read-only mode.
|
||||||
transaction {
|
transaction {
|
||||||
check(!connection.metaData.isReadOnly) { "Database should not be readonly." }
|
check(!connection.metaData.isReadOnly) { "Database should not be readonly." }
|
||||||
@ -72,25 +91,29 @@ class CordaPersistence(
|
|||||||
const val DATA_SOURCE_URL = "dataSource.url"
|
const val DATA_SOURCE_URL = "dataSource.url"
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
fun currentOrNew(isolation: TransactionIsolationLevel = defaultIsolationLevel): DatabaseTransaction {
|
||||||
* Creates an instance of [DatabaseTransaction], with the given transaction isolation level.
|
return contextTransactionOrNull ?: newTransaction(isolation)
|
||||||
*/
|
}
|
||||||
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.
|
fun newTransaction(isolation: TransactionIsolationLevel = defaultIsolationLevel): DatabaseTransaction {
|
||||||
DatabaseTransactionManager.dataSource = this
|
return DatabaseTransaction(isolation.jdbcValue, contextTransactionOrNull, this).also {
|
||||||
return DatabaseTransactionManager.currentOrNew(isolationLevel)
|
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 {
|
fun createSession(): Connection {
|
||||||
// We need to set the database for the current [Thread] or [Fiber] here as some tests share threads across databases.
|
// We need to set the database for the current [Thread] or [Fiber] here as some tests share threads across databases.
|
||||||
DatabaseTransactionManager.dataSource = this
|
_contextDatabase.set(this)
|
||||||
val ctx = DatabaseTransactionManager.currentOrNull()
|
return contextTransaction.connection
|
||||||
return ctx?.connection ?: throw IllegalStateException("Was expecting to find database transaction: must wrap calling code within a transaction.")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -99,7 +122,7 @@ class CordaPersistence(
|
|||||||
* @param statement to be executed in the scope of this transaction.
|
* @param statement to be executed in the scope of this transaction.
|
||||||
*/
|
*/
|
||||||
fun <T> transaction(isolationLevel: TransactionIsolationLevel, statement: DatabaseTransaction.() -> T): T {
|
fun <T> transaction(isolationLevel: TransactionIsolationLevel, statement: DatabaseTransaction.() -> T): T {
|
||||||
DatabaseTransactionManager.dataSource = this
|
_contextDatabase.set(this)
|
||||||
return transaction(isolationLevel, 2, statement)
|
return transaction(isolationLevel, 2, statement)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,7 +133,7 @@ class CordaPersistence(
|
|||||||
fun <T> transaction(statement: DatabaseTransaction.() -> T): T = transaction(defaultIsolationLevel, statement)
|
fun <T> transaction(statement: DatabaseTransaction.() -> T): T = transaction(defaultIsolationLevel, statement)
|
||||||
|
|
||||||
private fun <T> transaction(isolationLevel: TransactionIsolationLevel, recoverableFailureTolerance: Int, statement: DatabaseTransaction.() -> T): T {
|
private fun <T> transaction(isolationLevel: TransactionIsolationLevel, recoverableFailureTolerance: Int, statement: DatabaseTransaction.() -> T): T {
|
||||||
val outer = DatabaseTransactionManager.currentOrNull()
|
val outer = contextTransactionOrNull
|
||||||
return if (outer != null) {
|
return if (outer != null) {
|
||||||
outer.statement()
|
outer.statement()
|
||||||
} else {
|
} else {
|
||||||
@ -126,7 +149,7 @@ class CordaPersistence(
|
|||||||
log.warn("Cleanup task failed:", t)
|
log.warn("Cleanup task failed:", t)
|
||||||
}
|
}
|
||||||
while (true) {
|
while (true) {
|
||||||
val transaction = DatabaseTransactionManager.currentOrNew(isolationLevel)
|
val transaction = contextDatabase.currentOrNew(isolationLevel) // XXX: Does this code really support statement changing the contextDatabase?
|
||||||
try {
|
try {
|
||||||
val answer = transaction.statement()
|
val answer = transaction.statement()
|
||||||
transaction.commit()
|
transaction.commit()
|
||||||
@ -160,8 +183,8 @@ class CordaPersistence(
|
|||||||
* For examples, see the call hierarchy of this function.
|
* For examples, see the call hierarchy of this function.
|
||||||
*/
|
*/
|
||||||
fun <T : Any> rx.Observer<T>.bufferUntilDatabaseCommit(): rx.Observer<T> {
|
fun <T : Any> rx.Observer<T>.bufferUntilDatabaseCommit(): rx.Observer<T> {
|
||||||
val currentTxId = DatabaseTransactionManager.transactionId
|
val currentTxId = contextTransaction.id
|
||||||
val databaseTxBoundary: Observable<DatabaseTransactionManager.Boundary> = DatabaseTransactionManager.transactionBoundaries.first { it.txId == currentTxId }
|
val databaseTxBoundary: Observable<CordaPersistence.Boundary> = contextDatabase.transactionBoundaries.first { it.txId == currentTxId }
|
||||||
val subject = UnicastSubject.create<T>()
|
val subject = UnicastSubject.create<T>()
|
||||||
subject.delaySubscription(databaseTxBoundary).subscribe(this)
|
subject.delaySubscription(databaseTxBoundary).subscribe(this)
|
||||||
databaseTxBoundary.doOnCompleted { subject.onCompleted() }
|
databaseTxBoundary.doOnCompleted { subject.onCompleted() }
|
||||||
@ -169,12 +192,12 @@ fun <T : Any> rx.Observer<T>.bufferUntilDatabaseCommit(): rx.Observer<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// A subscriber that delegates to multiple others, wrapping a database transaction around the combination.
|
// A subscriber that delegates to multiple others, wrapping a database transaction around the combination.
|
||||||
private class DatabaseTransactionWrappingSubscriber<U>(val db: CordaPersistence?) : Subscriber<U>() {
|
private class DatabaseTransactionWrappingSubscriber<U>(private val db: CordaPersistence?) : Subscriber<U>() {
|
||||||
// Some unsubscribes happen inside onNext() so need something that supports concurrent modification.
|
// Some unsubscribes happen inside onNext() so need something that supports concurrent modification.
|
||||||
val delegates = CopyOnWriteArrayList<Subscriber<in U>>()
|
val delegates = CopyOnWriteArrayList<Subscriber<in U>>()
|
||||||
|
|
||||||
fun forEachSubscriberWithDbTx(block: Subscriber<in U>.() -> Unit) {
|
fun forEachSubscriberWithDbTx(block: Subscriber<in U>.() -> Unit) {
|
||||||
(db ?: DatabaseTransactionManager.dataSource).transaction {
|
(db ?: contextDatabase).transaction {
|
||||||
delegates.filter { !it.isUnsubscribed }.forEach {
|
delegates.filter { !it.isUnsubscribed }.forEach {
|
||||||
it.block()
|
it.block()
|
||||||
}
|
}
|
||||||
|
@ -1,23 +1,29 @@
|
|||||||
package net.corda.nodeapi.internal.persistence
|
package net.corda.nodeapi.internal.persistence
|
||||||
|
|
||||||
|
import co.paralleluniverse.strands.Strand
|
||||||
import org.hibernate.Session
|
import org.hibernate.Session
|
||||||
import org.hibernate.Transaction
|
import org.hibernate.Transaction
|
||||||
import rx.subjects.Subject
|
|
||||||
import java.sql.Connection
|
import java.sql.Connection
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
|
fun currentDBSession(): Session = contextTransaction.session
|
||||||
|
private val _contextTransaction = ThreadLocal<DatabaseTransaction>()
|
||||||
|
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(
|
class DatabaseTransaction(
|
||||||
isolation: Int,
|
isolation: Int,
|
||||||
private val threadLocal: ThreadLocal<DatabaseTransaction>,
|
val outerTransaction: DatabaseTransaction?,
|
||||||
private val transactionBoundaries: Subject<DatabaseTransactionManager.Boundary, DatabaseTransactionManager.Boundary>,
|
val database: CordaPersistence
|
||||||
val cordaPersistence: CordaPersistence
|
|
||||||
) {
|
) {
|
||||||
val id: UUID = UUID.randomUUID()
|
val id: UUID = UUID.randomUUID()
|
||||||
|
|
||||||
private var _connectionCreated = false
|
private var _connectionCreated = false
|
||||||
val connectionCreated get() = _connectionCreated
|
val connectionCreated get() = _connectionCreated
|
||||||
val connection: Connection by lazy(LazyThreadSafetyMode.NONE) {
|
val connection: Connection by lazy(LazyThreadSafetyMode.NONE) {
|
||||||
cordaPersistence.dataSource.connection
|
database.dataSource.connection
|
||||||
.apply {
|
.apply {
|
||||||
_connectionCreated = true
|
_connectionCreated = true
|
||||||
// only set the transaction isolation level if it's actually changed - setting isn't free.
|
// only set the transaction isolation level if it's actually changed - setting isn't free.
|
||||||
@ -28,16 +34,13 @@ class DatabaseTransaction(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val sessionDelegate = lazy {
|
private val sessionDelegate = lazy {
|
||||||
val session = cordaPersistence.entityManagerFactory.withOptions().connection(connection).openSession()
|
val session = database.entityManagerFactory.withOptions().connection(connection).openSession()
|
||||||
hibernateTransaction = session.beginTransaction()
|
hibernateTransaction = session.beginTransaction()
|
||||||
session
|
session
|
||||||
}
|
}
|
||||||
|
|
||||||
val session: Session by sessionDelegate
|
val session: Session by sessionDelegate
|
||||||
private lateinit var hibernateTransaction: Transaction
|
private lateinit var hibernateTransaction: Transaction
|
||||||
|
|
||||||
val outerTransaction: DatabaseTransaction? = threadLocal.get()
|
|
||||||
|
|
||||||
fun commit() {
|
fun commit() {
|
||||||
if (sessionDelegate.isInitialized()) {
|
if (sessionDelegate.isInitialized()) {
|
||||||
hibernateTransaction.commit()
|
hibernateTransaction.commit()
|
||||||
@ -63,9 +66,9 @@ class DatabaseTransaction(
|
|||||||
if (_connectionCreated) {
|
if (_connectionCreated) {
|
||||||
connection.close()
|
connection.close()
|
||||||
}
|
}
|
||||||
threadLocal.set(outerTransaction)
|
contextTransactionOrNull = outerTransaction
|
||||||
if (outerTransaction == null) {
|
if (outerTransaction == null) {
|
||||||
transactionBoundaries.onNext(DatabaseTransactionManager.Boundary(id))
|
database.transactionBoundaries.onNext(CordaPersistence.Boundary(id))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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<CordaPersistence>()
|
|
||||||
private val threadLocalTx = ThreadLocal<DatabaseTransaction>()
|
|
||||||
private val databaseToInstance = ConcurrentHashMap<CordaPersistence, DatabaseTransactionManager>()
|
|
||||||
|
|
||||||
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<Boundary, Boundary> 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<Boundary>().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()
|
|
||||||
}
|
|
@ -128,15 +128,16 @@ class HibernateConfiguration(
|
|||||||
class NodeDatabaseConnectionProvider : ConnectionProvider {
|
class NodeDatabaseConnectionProvider : ConnectionProvider {
|
||||||
override fun closeConnection(conn: Connection) {
|
override fun closeConnection(conn: Connection) {
|
||||||
conn.autoCommit = false
|
conn.autoCommit = false
|
||||||
val tx = DatabaseTransactionManager.current()
|
contextTransaction.run {
|
||||||
tx.commit()
|
commit()
|
||||||
tx.close()
|
close()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun supportsAggressiveRelease(): Boolean = true
|
override fun supportsAggressiveRelease(): Boolean = true
|
||||||
|
|
||||||
override fun getConnection(): Connection {
|
override fun getConnection(): Connection {
|
||||||
return DatabaseTransactionManager.newTransaction().connection
|
return contextDatabase.newTransaction().connection
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun <T : Any?> unwrap(unwrapType: Class<T>): T {
|
override fun <T : Any?> unwrap(unwrapType: Class<T>): T {
|
||||||
|
@ -3,18 +3,18 @@ package net.corda.node.amqp
|
|||||||
import com.nhaarman.mockito_kotlin.any
|
import com.nhaarman.mockito_kotlin.any
|
||||||
import com.nhaarman.mockito_kotlin.doReturn
|
import com.nhaarman.mockito_kotlin.doReturn
|
||||||
import com.nhaarman.mockito_kotlin.whenever
|
import com.nhaarman.mockito_kotlin.whenever
|
||||||
|
import net.corda.core.crypto.toStringShort
|
||||||
import net.corda.core.internal.div
|
import net.corda.core.internal.div
|
||||||
import net.corda.core.node.NodeInfo
|
import net.corda.core.node.NodeInfo
|
||||||
import net.corda.core.node.services.NetworkMapCache
|
import net.corda.core.node.services.NetworkMapCache
|
||||||
import net.corda.core.utilities.NetworkHostAndPort
|
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.protonwrapper.netty.AMQPServer
|
||||||
import net.corda.node.internal.security.RPCSecurityManager
|
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.config.*
|
||||||
import net.corda.node.services.messaging.ArtemisMessagingClient
|
import net.corda.node.services.messaging.ArtemisMessagingClient
|
||||||
import net.corda.node.services.messaging.ArtemisMessagingServer
|
import net.corda.node.services.messaging.ArtemisMessagingServer
|
||||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent
|
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.nodeapi.internal.crypto.loadKeyStore
|
||||||
import net.corda.testing.*
|
import net.corda.testing.*
|
||||||
import net.corda.testing.internal.rigorousMock
|
import net.corda.testing.internal.rigorousMock
|
||||||
@ -53,7 +53,7 @@ class AMQPBridgeTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `test acked and nacked messages`() {
|
fun `test acked and nacked messages`() {
|
||||||
// Create local queue
|
// Create local queue
|
||||||
val sourceQueueName = "internal.peers." + BOB.publicKey.toBase58String()
|
val sourceQueueName = "internal.peers." + BOB.publicKey.toStringShort()
|
||||||
val (artemisServer, artemisClient) = createArtemis(sourceQueueName)
|
val (artemisServer, artemisClient) = createArtemis(sourceQueueName)
|
||||||
|
|
||||||
// Pre-populate local queue with 3 messages
|
// Pre-populate local queue with 3 messages
|
||||||
@ -133,11 +133,13 @@ class AMQPBridgeTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `Test legacy bridge still works`() {
|
fun `Test legacy bridge still works`() {
|
||||||
// Create local queue
|
// Create local queue
|
||||||
val sourceQueueName = "internal.peers." + ALICE.publicKey.toBase58String()
|
val sourceQueueName = "internal.peers." + BOB.publicKey.toStringShort()
|
||||||
val (artemisLegacyServer, artemisLegacyClient) = createLegacyArtemis(sourceQueueName)
|
val (artemisLegacyServer, artemisLegacyClient) = createLegacyArtemis(sourceQueueName)
|
||||||
|
|
||||||
|
|
||||||
val (artemisServer, artemisClient) = createArtemis(null)
|
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!!
|
val artemis = artemisLegacyClient.started!!
|
||||||
for (i in 0 until 3) {
|
for (i in 0 until 3) {
|
||||||
@ -150,8 +152,7 @@ class AMQPBridgeTest {
|
|||||||
artemis.producer.send(sourceQueueName, artemisMessage)
|
artemis.producer.send(sourceQueueName, artemisMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val subs = artemisClient.started!!.session.createConsumer(inbox)
|
||||||
val subs = artemisClient.started!!.session.createConsumer(P2P_QUEUE)
|
|
||||||
for (i in 0 until 3) {
|
for (i in 0 until 3) {
|
||||||
val msg = subs.receive()
|
val msg = subs.receive()
|
||||||
val messageBody = ByteArray(msg.bodySize).apply { msg.bodyBuffer.readBytes(this) }
|
val messageBody = ByteArray(msg.bodySize).apply { msg.bodyBuffer.readBytes(this) }
|
||||||
@ -177,9 +178,9 @@ class AMQPBridgeTest {
|
|||||||
doReturn(true).whenever(it).useAMQPBridges
|
doReturn(true).whenever(it).useAMQPBridges
|
||||||
}
|
}
|
||||||
artemisConfig.configureWithDevSSLCertificate()
|
artemisConfig.configureWithDevSSLCertificate()
|
||||||
val networkMap = rigorousMock<NetworkMapCache>().also {
|
val networkMap = rigorousMock<NetworkMapCacheInternal>().also {
|
||||||
doReturn(Observable.never<NetworkMapCache.MapChange>()).whenever(it).changed
|
doReturn(Observable.never<NetworkMapCache.MapChange>()).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<RPCSecurityManager>()
|
val userService = rigorousMock<RPCSecurityManager>()
|
||||||
val artemisServer = ArtemisMessagingServer(artemisConfig, artemisPort, null, networkMap, userService, MAX_MESSAGE_SIZE)
|
val artemisServer = ArtemisMessagingServer(artemisConfig, artemisPort, null, networkMap, userService, MAX_MESSAGE_SIZE)
|
||||||
@ -189,7 +190,7 @@ class AMQPBridgeTest {
|
|||||||
val artemis = artemisClient.started!!
|
val artemis = artemisClient.started!!
|
||||||
if (sourceQueueName != null) {
|
if (sourceQueueName != null) {
|
||||||
// Local queue for outgoing messages
|
// 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)
|
return Pair(artemisServer, artemisClient)
|
||||||
}
|
}
|
||||||
@ -206,9 +207,9 @@ class AMQPBridgeTest {
|
|||||||
doReturn(ActiveMqServerConfiguration(BridgeConfiguration(0, 0, 0.0))).whenever(it).activeMQServer
|
doReturn(ActiveMqServerConfiguration(BridgeConfiguration(0, 0, 0.0))).whenever(it).activeMQServer
|
||||||
}
|
}
|
||||||
artemisConfig.configureWithDevSSLCertificate()
|
artemisConfig.configureWithDevSSLCertificate()
|
||||||
val networkMap = rigorousMock<NetworkMapCache>().also {
|
val networkMap = rigorousMock<NetworkMapCacheInternal>().also {
|
||||||
doReturn(Observable.never<NetworkMapCache.MapChange>()).whenever(it).changed
|
doReturn(Observable.never<NetworkMapCache.MapChange>()).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<RPCSecurityManager>()
|
val userService = rigorousMock<RPCSecurityManager>()
|
||||||
val artemisServer = ArtemisMessagingServer(artemisConfig, artemisPort2, null, networkMap, userService, MAX_MESSAGE_SIZE)
|
val artemisServer = ArtemisMessagingServer(artemisConfig, artemisPort2, null, networkMap, userService, MAX_MESSAGE_SIZE)
|
||||||
@ -217,7 +218,7 @@ class AMQPBridgeTest {
|
|||||||
artemisClient.start()
|
artemisClient.start()
|
||||||
val artemis = artemisClient.started!!
|
val artemis = artemisClient.started!!
|
||||||
// Local queue for outgoing messages
|
// 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)
|
return Pair(artemisServer, artemisClient)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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.AMQPClient
|
||||||
import net.corda.node.internal.protonwrapper.netty.AMQPServer
|
import net.corda.node.internal.protonwrapper.netty.AMQPServer
|
||||||
import net.corda.node.internal.security.RPCSecurityManager
|
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.CertChainPolicyConfig
|
||||||
import net.corda.node.services.config.NodeConfiguration
|
import net.corda.node.services.config.NodeConfiguration
|
||||||
import net.corda.node.services.config.configureWithDevSSLCertificate
|
import net.corda.node.services.config.configureWithDevSSLCertificate
|
||||||
import net.corda.node.services.messaging.ArtemisMessagingClient
|
import net.corda.node.services.messaging.ArtemisMessagingClient
|
||||||
import net.corda.node.services.messaging.ArtemisMessagingServer
|
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.ArtemisMessagingComponent.Companion.PEER_USER
|
||||||
import net.corda.nodeapi.internal.crypto.loadKeyStore
|
import net.corda.nodeapi.internal.crypto.loadKeyStore
|
||||||
import net.corda.testing.*
|
import net.corda.testing.*
|
||||||
@ -48,7 +50,7 @@ class ProtonWrapperTests {
|
|||||||
amqpServer.start()
|
amqpServer.start()
|
||||||
val receiveSubs = amqpServer.onReceive.subscribe {
|
val receiveSubs = amqpServer.onReceive.subscribe {
|
||||||
assertEquals(BOB_NAME.toString(), it.sourceLegalName)
|
assertEquals(BOB_NAME.toString(), it.sourceLegalName)
|
||||||
assertEquals("p2p.inbound", it.topic)
|
assertEquals(P2P_PREFIX + "Test", it.topic)
|
||||||
assertEquals("Test", String(it.payload))
|
assertEquals("Test", String(it.payload))
|
||||||
it.complete(true)
|
it.complete(true)
|
||||||
}
|
}
|
||||||
@ -64,7 +66,7 @@ class ProtonWrapperTests {
|
|||||||
assertEquals(true, clientConnect.connected)
|
assertEquals(true, clientConnect.connected)
|
||||||
assertEquals(ALICE_NAME, CordaX500Name.build(clientConnect.remoteCert!!.subjectX500Principal))
|
assertEquals(ALICE_NAME, CordaX500Name.build(clientConnect.remoteCert!!.subjectX500Principal))
|
||||||
val msg = amqpClient.createMessage("Test".toByteArray(),
|
val msg = amqpClient.createMessage("Test".toByteArray(),
|
||||||
"p2p.inbound",
|
P2P_PREFIX + "Test",
|
||||||
ALICE_NAME.toString(),
|
ALICE_NAME.toString(),
|
||||||
emptyMap())
|
emptyMap())
|
||||||
amqpClient.write(msg)
|
amqpClient.write(msg)
|
||||||
@ -151,8 +153,8 @@ class ProtonWrapperTests {
|
|||||||
assertEquals(true, clientConnected.get().connected)
|
assertEquals(true, clientConnected.get().connected)
|
||||||
assertEquals(CHARLIE_NAME, CordaX500Name.build(clientConnected.get().remoteCert!!.subjectX500Principal))
|
assertEquals(CHARLIE_NAME, CordaX500Name.build(clientConnected.get().remoteCert!!.subjectX500Principal))
|
||||||
val artemis = artemisClient.started!!
|
val artemis = artemisClient.started!!
|
||||||
val sendAddress = "p2p.inbound"
|
val sendAddress = P2P_PREFIX + "Test"
|
||||||
artemis.session.createQueue(sendAddress, RoutingType.MULTICAST, "queue", true)
|
artemis.session.createQueue(sendAddress, RoutingType.ANYCAST, "queue", true)
|
||||||
val consumer = artemis.session.createConsumer("queue")
|
val consumer = artemis.session.createConsumer("queue")
|
||||||
val testData = "Test".toByteArray()
|
val testData = "Test".toByteArray()
|
||||||
val testProperty = mutableMapOf<Any?, Any?>()
|
val testProperty = mutableMapOf<Any?, Any?>()
|
||||||
@ -230,7 +232,7 @@ class ProtonWrapperTests {
|
|||||||
}
|
}
|
||||||
artemisConfig.configureWithDevSSLCertificate()
|
artemisConfig.configureWithDevSSLCertificate()
|
||||||
|
|
||||||
val networkMap = rigorousMock<NetworkMapCache>().also {
|
val networkMap = rigorousMock<NetworkMapCacheInternal>().also {
|
||||||
doReturn(never<NetworkMapCache.MapChange>()).whenever(it).changed
|
doReturn(never<NetworkMapCache.MapChange>()).whenever(it).changed
|
||||||
}
|
}
|
||||||
val userService = rigorousMock<RPCSecurityManager>()
|
val userService = rigorousMock<RPCSecurityManager>()
|
||||||
|
@ -13,6 +13,7 @@ import net.corda.core.identity.Party
|
|||||||
import net.corda.core.transactions.TransactionBuilder
|
import net.corda.core.transactions.TransactionBuilder
|
||||||
import net.corda.core.utilities.getOrThrow
|
import net.corda.core.utilities.getOrThrow
|
||||||
import net.corda.node.internal.StartedNode
|
import net.corda.node.internal.StartedNode
|
||||||
|
import net.corda.node.services.config.MySQLConfiguration
|
||||||
import net.corda.node.services.config.NotaryConfig
|
import net.corda.node.services.config.NotaryConfig
|
||||||
import net.corda.nodeapi.internal.DevIdentityGenerator
|
import net.corda.nodeapi.internal.DevIdentityGenerator
|
||||||
import net.corda.nodeapi.internal.network.NetworkParametersCopier
|
import net.corda.nodeapi.internal.network.NetworkParametersCopier
|
||||||
@ -135,7 +136,7 @@ class MySQLNotaryServiceTests : IntegrationTest() {
|
|||||||
legalName = notaryName,
|
legalName = notaryName,
|
||||||
entropyRoot = BigInteger.valueOf(60L),
|
entropyRoot = BigInteger.valueOf(60L),
|
||||||
configOverrides = {
|
configOverrides = {
|
||||||
val notaryConfig = NotaryConfig(validating = false, mysql = dataStoreProperties)
|
val notaryConfig = NotaryConfig(validating = false, mysql = MySQLConfiguration(dataStoreProperties))
|
||||||
doReturn(notaryConfig).whenever(it).notary
|
doReturn(notaryConfig).whenever(it).notary
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package net.corda.node.services.network
|
package net.corda.node.services.network
|
||||||
|
|
||||||
|
import net.corda.core.crypto.generateKeyPair
|
||||||
import net.corda.core.identity.CordaX500Name
|
import net.corda.core.identity.CordaX500Name
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.node.NodeInfo
|
import net.corda.core.node.NodeInfo
|
||||||
@ -15,6 +16,7 @@ import org.junit.ClassRule
|
|||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
// TODO Clean up these tests, they were written with old network map design in place.
|
||||||
class PersistentNetworkMapCacheTest : NodeBasedTest() {
|
class PersistentNetworkMapCacheTest : NodeBasedTest() {
|
||||||
companion object {
|
companion object {
|
||||||
val ALICE = TestIdentity(ALICE_NAME, 70).party
|
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
|
@Test
|
||||||
fun `restart node with DB map cache`() {
|
fun `restart node with DB map cache`() {
|
||||||
val alice = startNodesWithPort(listOf(ALICE))[0]
|
val alice = startNodesWithPort(listOf(ALICE))[0]
|
||||||
|
@ -5,6 +5,7 @@ import net.corda.client.rpc.CordaRPCClient
|
|||||||
import net.corda.client.rpc.CordaRPCConnection
|
import net.corda.client.rpc.CordaRPCConnection
|
||||||
import net.corda.core.crypto.generateKeyPair
|
import net.corda.core.crypto.generateKeyPair
|
||||||
import net.corda.core.crypto.random63BitValue
|
import net.corda.core.crypto.random63BitValue
|
||||||
|
import net.corda.core.crypto.toStringShort
|
||||||
import net.corda.core.flows.FlowLogic
|
import net.corda.core.flows.FlowLogic
|
||||||
import net.corda.core.flows.FlowSession
|
import net.corda.core.flows.FlowSession
|
||||||
import net.corda.core.flows.InitiatedBy
|
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.messaging.CordaRPCOps
|
||||||
import net.corda.core.utilities.NetworkHostAndPort
|
import net.corda.core.utilities.NetworkHostAndPort
|
||||||
import net.corda.core.utilities.getOrThrow
|
import net.corda.core.utilities.getOrThrow
|
||||||
import net.corda.core.utilities.toBase58String
|
|
||||||
import net.corda.core.utilities.unwrap
|
import net.corda.core.utilities.unwrap
|
||||||
import net.corda.node.internal.Node
|
import net.corda.node.internal.Node
|
||||||
import net.corda.node.internal.StartedNode
|
import net.corda.node.internal.StartedNode
|
||||||
import net.corda.nodeapi.RPCApi
|
import net.corda.nodeapi.RPCApi
|
||||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.INTERNAL_PREFIX
|
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.INTERNAL_PREFIX
|
||||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NOTIFICATIONS_ADDRESS
|
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NOTIFICATIONS_ADDRESS
|
||||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_QUEUE
|
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_PREFIX
|
||||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEERS_PREFIX
|
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEERS_PREFIX
|
||||||
import net.corda.nodeapi.internal.config.SSLConfiguration
|
import net.corda.nodeapi.internal.config.SSLConfiguration
|
||||||
import net.corda.testing.ALICE_NAME
|
import net.corda.testing.ALICE_NAME
|
||||||
@ -79,30 +79,30 @@ abstract class MQSecurityTest : NodeBasedTest() {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `consume message from P2P queue`() {
|
fun `consume message from P2P queue`() {
|
||||||
assertConsumeAttackFails(P2P_QUEUE)
|
assertConsumeAttackFails("$P2P_PREFIX${alice.info.chooseIdentity().owningKey.toStringShort()}")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `consume message from peer queue`() {
|
fun `consume message from peer queue`() {
|
||||||
val bobParty = startBobAndCommunicateWithAlice()
|
val bobParty = startBobAndCommunicateWithAlice()
|
||||||
assertConsumeAttackFails("$PEERS_PREFIX${bobParty.owningKey.toBase58String()}")
|
assertConsumeAttackFails("$PEERS_PREFIX${bobParty.owningKey.toStringShort()}")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `send message to address of peer which has been communicated with`() {
|
fun `send message to address of peer which has been communicated with`() {
|
||||||
val bobParty = startBobAndCommunicateWithAlice()
|
val bobParty = startBobAndCommunicateWithAlice()
|
||||||
assertSendAttackFails("$PEERS_PREFIX${bobParty.owningKey.toBase58String()}")
|
assertSendAttackFails("$PEERS_PREFIX${bobParty.owningKey.toStringShort()}")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `create queue for peer which has not been communicated with`() {
|
fun `create queue for peer which has not been communicated with`() {
|
||||||
val bob = startNode(BOB_NAME)
|
val bob = startNode(BOB_NAME)
|
||||||
assertAllQueueCreationAttacksFail("$PEERS_PREFIX${bob.info.chooseIdentity().owningKey.toBase58String()}")
|
assertAllQueueCreationAttacksFail("$PEERS_PREFIX${bob.info.chooseIdentity().owningKey.toStringShort()}")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `create queue for unknown peer`() {
|
fun `create queue for unknown peer`() {
|
||||||
val invalidPeerQueue = "$PEERS_PREFIX${generateKeyPair().public.toBase58String()}"
|
val invalidPeerQueue = "$PEERS_PREFIX${generateKeyPair().public.toStringShort()}"
|
||||||
assertAllQueueCreationAttacksFail(invalidPeerQueue)
|
assertAllQueueCreationAttacksFail(invalidPeerQueue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -222,7 +222,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
|
|||||||
check(started == null) { "Node has already been started" }
|
check(started == null) { "Node has already been started" }
|
||||||
log.info("Node starting up ...")
|
log.info("Node starting up ...")
|
||||||
initCertificate()
|
initCertificate()
|
||||||
val schemaService = NodeSchemaService(cordappLoader.cordappSchemas)
|
val schemaService = NodeSchemaService(cordappLoader.cordappSchemas, configuration.notary != null)
|
||||||
val (identity, identityKeyPair) = obtainIdentity(notaryConfig = null)
|
val (identity, identityKeyPair) = obtainIdentity(notaryConfig = null)
|
||||||
val lh = lazyHub()
|
val lh = lazyHub()
|
||||||
configure(lh)
|
configure(lh)
|
||||||
|
@ -40,6 +40,7 @@ import org.slf4j.Logger
|
|||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import rx.Scheduler
|
import rx.Scheduler
|
||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
|
import java.security.PublicKey
|
||||||
import java.time.Clock
|
import java.time.Clock
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
import javax.management.ObjectName
|
import javax.management.ObjectName
|
||||||
@ -166,11 +167,14 @@ open class Node(configuration: NodeConfiguration,
|
|||||||
VerifierType.OutOfProcess -> VerifierMessagingClient(configuration, serverAddress, services.monitoringService.metrics, networkParameters.maxMessageSize)
|
VerifierType.OutOfProcess -> VerifierMessagingClient(configuration, serverAddress, services.monitoringService.metrics, networkParameters.maxMessageSize)
|
||||||
VerifierType.InMemory -> null
|
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(
|
return P2PMessagingClient(
|
||||||
configuration,
|
configuration,
|
||||||
versionInfo,
|
versionInfo,
|
||||||
serverAddress,
|
serverAddress,
|
||||||
info.legalIdentities[0].owningKey,
|
info.legalIdentities[0].owningKey,
|
||||||
|
serviceIdentity,
|
||||||
serverThread,
|
serverThread,
|
||||||
database,
|
database,
|
||||||
advertisedAddress,
|
advertisedAddress,
|
||||||
|
@ -29,6 +29,10 @@ interface NetworkMapCacheBaseInternal : NetworkMapCacheBase {
|
|||||||
|
|
||||||
fun getNodeByHash(nodeHash: SecureHash): NodeInfo?
|
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<NodeInfo>
|
||||||
|
|
||||||
/** Adds a node to the local cache (generally only used for adding ourselves). */
|
/** Adds a node to the local cache (generally only used for adding ourselves). */
|
||||||
fun addNode(node: NodeInfo)
|
fun addNode(node: NodeInfo)
|
||||||
|
|
||||||
|
@ -18,8 +18,6 @@ import java.util.*
|
|||||||
val Int.MB: Long get() = this * 1024L * 1024L
|
val Int.MB: Long get() = this * 1024L * 1024L
|
||||||
|
|
||||||
interface NodeConfiguration : NodeSSLConfiguration {
|
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 myLegalName: CordaX500Name
|
||||||
val emailAddress: String
|
val emailAddress: String
|
||||||
val exportJMXto: String
|
val exportJMXto: String
|
||||||
@ -35,8 +33,6 @@ interface NodeConfiguration : NodeSSLConfiguration {
|
|||||||
val notary: NotaryConfig?
|
val notary: NotaryConfig?
|
||||||
val activeMQServer: ActiveMqServerConfiguration
|
val activeMQServer: ActiveMqServerConfiguration
|
||||||
val additionalNodeInfoPollingFrequencyMsec: Long
|
val additionalNodeInfoPollingFrequencyMsec: Long
|
||||||
// TODO Remove as this is only used by the driver
|
|
||||||
val useHTTPS: Boolean
|
|
||||||
val p2pAddress: NetworkHostAndPort
|
val p2pAddress: NetworkHostAndPort
|
||||||
val rpcAddress: NetworkHostAndPort?
|
val rpcAddress: NetworkHostAndPort?
|
||||||
val messagingServerAddress: NetworkHostAndPort?
|
val messagingServerAddress: NetworkHostAndPort?
|
||||||
@ -71,7 +67,7 @@ data class NotaryConfig(val validating: Boolean,
|
|||||||
val raft: RaftConfig? = null,
|
val raft: RaftConfig? = null,
|
||||||
val bftSMaRt: BFTSMaRtConfiguration? = null,
|
val bftSMaRt: BFTSMaRtConfiguration? = null,
|
||||||
val custom: Boolean = false,
|
val custom: Boolean = false,
|
||||||
val mysql: Properties? = null
|
val mysql: MySQLConfiguration? = null
|
||||||
) {
|
) {
|
||||||
init {
|
init {
|
||||||
require(raft == null || bftSMaRt == null || !custom || mysql == null) {
|
require(raft == null || bftSMaRt == null || !custom || mysql == null) {
|
||||||
@ -81,6 +77,15 @@ data class NotaryConfig(val validating: Boolean,
|
|||||||
val isClusterConfig: Boolean get() = raft != null || bftSMaRt != null
|
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<NetworkHostAndPort>)
|
data class RaftConfig(val nodeAddress: NetworkHostAndPort, val clusterAddresses: List<NetworkHostAndPort>)
|
||||||
|
|
||||||
/** @param exposeRaces for testing only, so its default is not in reference.conf but here. */
|
/** @param exposeRaces for testing only, so its default is not in reference.conf but here. */
|
||||||
@ -118,7 +123,6 @@ data class NodeConfigurationImpl(
|
|||||||
// TODO typesafe config supports the notion of durations. Make use of that by mapping it to java.time.Duration.
|
// TODO typesafe config supports the notion of durations. Make use of that by mapping it to java.time.Duration.
|
||||||
// Then rename this to messageRedeliveryDelay and make it of type Duration
|
// Then rename this to messageRedeliveryDelay and make it of type Duration
|
||||||
override val messageRedeliveryDelaySeconds: Int = 30,
|
override val messageRedeliveryDelaySeconds: Int = 30,
|
||||||
override val useHTTPS: Boolean,
|
|
||||||
override val p2pAddress: NetworkHostAndPort,
|
override val p2pAddress: NetworkHostAndPort,
|
||||||
override val rpcAddress: NetworkHostAndPort?,
|
override val rpcAddress: NetworkHostAndPort?,
|
||||||
override val relay: RelayConfiguration?,
|
override val relay: RelayConfiguration?,
|
||||||
|
@ -12,8 +12,8 @@ import net.corda.node.services.config.NodeConfiguration
|
|||||||
import net.corda.node.services.messaging.AMQPBridgeManager.AMQPBridge.Companion.getBridgeName
|
import net.corda.node.services.messaging.AMQPBridgeManager.AMQPBridge.Companion.getBridgeName
|
||||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent
|
import net.corda.nodeapi.internal.ArtemisMessagingComponent
|
||||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_USER
|
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_USER
|
||||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_QUEUE
|
|
||||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEER_USER
|
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 net.corda.nodeapi.internal.crypto.loadKeyStore
|
||||||
import org.apache.activemq.artemis.api.core.SimpleString
|
import org.apache.activemq.artemis.api.core.SimpleString
|
||||||
import org.apache.activemq.artemis.api.core.client.ActiveMQClient.DEFAULT_ACK_BATCH_SIZE
|
import org.apache.activemq.artemis.api.core.client.ActiveMQClient.DEFAULT_ACK_BATCH_SIZE
|
||||||
@ -132,7 +132,8 @@ internal class AMQPBridgeManager(val config: NodeConfiguration, val p2pAddress:
|
|||||||
properties[key.toString()] = value
|
properties[key.toString()] = value
|
||||||
}
|
}
|
||||||
log.debug { "Bridged Send to ${legalNames.first()} uuid: ${artemisMessage.getObjectProperty("_AMQ_DUPL_ID")}" }
|
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(),
|
legalNames.first().toString(),
|
||||||
properties)
|
properties)
|
||||||
sendableMessage.onComplete.then {
|
sendableMessage.onComplete.then {
|
||||||
|
@ -8,16 +8,15 @@ import net.corda.core.internal.div
|
|||||||
import net.corda.core.internal.noneOrSingle
|
import net.corda.core.internal.noneOrSingle
|
||||||
import net.corda.core.internal.uncheckedCast
|
import net.corda.core.internal.uncheckedCast
|
||||||
import net.corda.core.node.NodeInfo
|
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.node.services.NetworkMapCache.MapChange
|
||||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||||
import net.corda.core.utilities.NetworkHostAndPort
|
import net.corda.core.utilities.NetworkHostAndPort
|
||||||
import net.corda.core.utilities.contextLogger
|
import net.corda.core.utilities.contextLogger
|
||||||
import net.corda.core.utilities.debug
|
import net.corda.core.utilities.debug
|
||||||
import net.corda.core.utilities.parsePublicKeyBase58
|
|
||||||
import net.corda.node.internal.Node
|
import net.corda.node.internal.Node
|
||||||
import net.corda.node.internal.security.Password
|
import net.corda.node.internal.security.Password
|
||||||
import net.corda.node.internal.security.RPCSecurityManager
|
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.config.NodeConfiguration
|
||||||
import net.corda.node.services.messaging.NodeLoginModule.Companion.NODE_ROLE
|
import net.corda.node.services.messaging.NodeLoginModule.Companion.NODE_ROLE
|
||||||
import net.corda.node.services.messaging.NodeLoginModule.Companion.PEER_ROLE
|
import net.corda.node.services.messaging.NodeLoginModule.Companion.PEER_ROLE
|
||||||
@ -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.INTERNAL_PREFIX
|
||||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_USER
|
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_USER
|
||||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NOTIFICATIONS_ADDRESS
|
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NOTIFICATIONS_ADDRESS
|
||||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_QUEUE
|
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_PREFIX
|
||||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEERS_PREFIX
|
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEERS_PREFIX
|
||||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEER_USER
|
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEER_USER
|
||||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.NodeAddress
|
import net.corda.nodeapi.internal.ArtemisMessagingComponent.NodeAddress
|
||||||
@ -94,7 +93,7 @@ import javax.security.cert.CertificateException
|
|||||||
class ArtemisMessagingServer(private val config: NodeConfiguration,
|
class ArtemisMessagingServer(private val config: NodeConfiguration,
|
||||||
private val p2pPort: Int,
|
private val p2pPort: Int,
|
||||||
val rpcPort: Int?,
|
val rpcPort: Int?,
|
||||||
val networkMapCache: NetworkMapCache,
|
val networkMapCache: NetworkMapCacheInternal,
|
||||||
val securityManager: RPCSecurityManager,
|
val securityManager: RPCSecurityManager,
|
||||||
val maxMessageSize: Int) : SingletonSerializeAsToken() {
|
val maxMessageSize: Int) : SingletonSerializeAsToken() {
|
||||||
companion object {
|
companion object {
|
||||||
@ -191,7 +190,6 @@ class ArtemisMessagingServer(private val config: NodeConfiguration,
|
|||||||
// by having its password be an unknown securely random 128-bit value.
|
// by having its password be an unknown securely random 128-bit value.
|
||||||
clusterPassword = BigInteger(128, newSecureRandom()).toString(16)
|
clusterPassword = BigInteger(128, newSecureRandom()).toString(16)
|
||||||
queueConfigurations = listOf(
|
queueConfigurations = listOf(
|
||||||
queueConfig(P2P_QUEUE, durable = true),
|
|
||||||
// Create an RPC queue: this will service locally connected clients only (not via a bridge) and those
|
// 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,
|
// clients must have authenticated. We could use a single consumer for everything and perhaps we should,
|
||||||
// but these queues are not worth persisting.
|
// but these queues are not worth persisting.
|
||||||
@ -243,7 +241,7 @@ class ArtemisMessagingServer(private val config: NodeConfiguration,
|
|||||||
private fun ConfigurationImpl.configureAddressSecurity(): Pair<Configuration, LoginListener> {
|
private fun ConfigurationImpl.configureAddressSecurity(): Pair<Configuration, LoginListener> {
|
||||||
val nodeInternalRole = Role(NODE_ROLE, true, true, true, true, true, true, true, true)
|
val nodeInternalRole = Role(NODE_ROLE, true, true, true, true, true, true, true, true)
|
||||||
securityRoles["$INTERNAL_PREFIX#"] = setOf(nodeInternalRole) // Do not add any other roles here as it's only for the node
|
securityRoles["$INTERNAL_PREFIX#"] = setOf(nodeInternalRole) // Do not add any other roles here as it's only for the node
|
||||||
securityRoles[P2P_QUEUE] = setOf(nodeInternalRole, restrictedRole(PEER_ROLE, send = true))
|
securityRoles["$P2P_PREFIX#"] = setOf(nodeInternalRole, restrictedRole(PEER_ROLE, send = true))
|
||||||
securityRoles[RPCApi.RPC_SERVER_QUEUE_NAME] = setOf(nodeInternalRole, restrictedRole(RPC_ROLE, send = true))
|
securityRoles[RPCApi.RPC_SERVER_QUEUE_NAME] = setOf(nodeInternalRole, restrictedRole(RPC_ROLE, send = true))
|
||||||
// Each RPC user must have its own role and its own queue. This prevents users accessing each other's queues
|
// Each RPC user must have its own role and its own queue. This prevents users accessing each other's queues
|
||||||
// and stealing RPC responses.
|
// and stealing RPC responses.
|
||||||
@ -309,8 +307,7 @@ class ArtemisMessagingServer(private val config: NodeConfiguration,
|
|||||||
|
|
||||||
if (queueName.startsWith(PEERS_PREFIX)) {
|
if (queueName.startsWith(PEERS_PREFIX)) {
|
||||||
try {
|
try {
|
||||||
val identity = parsePublicKeyBase58(queueName.substring(PEERS_PREFIX.length))
|
val nodeInfos = networkMapCache.getNodesByOwningKeyIndex(queueName.substring(PEERS_PREFIX.length))
|
||||||
val nodeInfos = networkMapCache.getNodesByLegalIdentityKey(identity)
|
|
||||||
if (nodeInfos.isNotEmpty()) {
|
if (nodeInfos.isNotEmpty()) {
|
||||||
nodeInfos.forEach { deployBridgeToPeer(it) }
|
nodeInfos.forEach { deployBridgeToPeer(it) }
|
||||||
} else {
|
} else {
|
||||||
|
@ -10,15 +10,17 @@ import net.corda.node.services.config.NodeConfiguration
|
|||||||
import net.corda.nodeapi.ArtemisTcpTransport
|
import net.corda.nodeapi.ArtemisTcpTransport
|
||||||
import net.corda.nodeapi.ConnectionDirection
|
import net.corda.nodeapi.ConnectionDirection
|
||||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent
|
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.Companion.PEER_USER
|
||||||
|
import net.corda.nodeapi.internal.ArtemisMessagingComponent.RemoteInboxAddress.Companion.translateLocalQueueToInboxAddress
|
||||||
import net.corda.nodeapi.internal.crypto.X509Utilities
|
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.config.BridgeConfiguration
|
||||||
import org.apache.activemq.artemis.core.remoting.impl.netty.NettyConnection
|
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.NettyConnector
|
||||||
import org.apache.activemq.artemis.core.remoting.impl.netty.NettyConnectorFactory
|
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.remoting.impl.netty.TransportConstants
|
||||||
import org.apache.activemq.artemis.core.server.ActiveMQServer
|
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.spi.core.remoting.*
|
||||||
import org.apache.activemq.artemis.utils.ConfigurationHelper
|
import org.apache.activemq.artemis.utils.ConfigurationHelper
|
||||||
import java.time.Duration
|
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,
|
* 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
|
* as defined by ArtemisAddress.queueName. A bridge is then created to forward messages from this queue to the node's
|
||||||
* P2P address.
|
* P2P address.
|
||||||
@ -64,7 +66,6 @@ internal class CoreBridgeManager(val config: NodeConfiguration, val activeMQServ
|
|||||||
activeMQServer.deployBridge(BridgeConfiguration().apply {
|
activeMQServer.deployBridge(BridgeConfiguration().apply {
|
||||||
name = getBridgeName(queueName, target)
|
name = getBridgeName(queueName, target)
|
||||||
this.queueName = queueName
|
this.queueName = queueName
|
||||||
forwardingAddress = P2P_QUEUE
|
|
||||||
staticConnectors = listOf(target.toString())
|
staticConnectors = listOf(target.toString())
|
||||||
confirmationWindowSize = 100000 // a guess
|
confirmationWindowSize = 100000 // a guess
|
||||||
isUseDuplicateDetection = true // Enable the bridge's automatic deduplication logic
|
isUseDuplicateDetection = true // Enable the bridge's automatic deduplication logic
|
||||||
@ -77,6 +78,7 @@ internal class CoreBridgeManager(val config: NodeConfiguration, val activeMQServ
|
|||||||
// our TLS certificate.
|
// our TLS certificate.
|
||||||
user = PEER_USER
|
user = PEER_USER
|
||||||
password = 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() {
|
class VerifyingNettyConnectorFactory : NettyConnectorFactory() {
|
||||||
override fun createConnector(configuration: MutableMap<String, Any>,
|
override fun createConnector(configuration: MutableMap<String, Any>,
|
||||||
handler: BufferHandler?,
|
handler: BufferHandler?,
|
||||||
|
@ -19,7 +19,6 @@ import net.corda.node.utilities.AffinityExecutor
|
|||||||
import net.corda.node.utilities.AppendOnlyPersistentMap
|
import net.corda.node.utilities.AppendOnlyPersistentMap
|
||||||
import net.corda.node.utilities.PersistentMap
|
import net.corda.node.utilities.PersistentMap
|
||||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.*
|
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.CordaPersistence
|
||||||
import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX
|
import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX
|
||||||
import org.apache.activemq.artemis.api.core.ActiveMQObjectClosedException
|
import org.apache.activemq.artemis.api.core.ActiveMQObjectClosedException
|
||||||
@ -54,21 +53,28 @@ import javax.persistence.Lob
|
|||||||
* invoke methods on the provided implementation. There is more documentation on this in the docsite and the
|
* invoke methods on the provided implementation. There is more documentation on this in the docsite and the
|
||||||
* CordaRPCClient class.
|
* CordaRPCClient class.
|
||||||
*
|
*
|
||||||
* @param serverAddress The address of the broker instance to connect to (might be running in the same process).
|
* @param config The configuration of the node, which is used for controlling the message redelivery options.
|
||||||
* @param myIdentity The public key to be used as the ArtemisMQ address and queue name for the node.
|
* @param versionInfo All messages from the node carry the version info and received messages are checked against this for compatibility.
|
||||||
* @param nodeExecutor An executor to run received message tasks upon.
|
* @param serverAddress The host and port of the Artemis broker.
|
||||||
* @param advertisedAddress The node address for inbound connections, advertised to the network map service and peers.
|
* @param myIdentity The primary identity of the node, which defines the messaging address for externally received messages.
|
||||||
* If not provided, will default to [serverAddress].
|
* 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
|
@ThreadSafe
|
||||||
class P2PMessagingClient(config: NodeConfiguration,
|
class P2PMessagingClient(config: NodeConfiguration,
|
||||||
private val versionInfo: VersionInfo,
|
private val versionInfo: VersionInfo,
|
||||||
serverAddress: NetworkHostAndPort,
|
serverAddress: NetworkHostAndPort,
|
||||||
private val myIdentity: PublicKey,
|
private val myIdentity: PublicKey,
|
||||||
|
private val serviceIdentity: PublicKey?,
|
||||||
private val nodeExecutor: AffinityExecutor.ServiceAffinityExecutor,
|
private val nodeExecutor: AffinityExecutor.ServiceAffinityExecutor,
|
||||||
private val database: CordaPersistence,
|
private val database: CordaPersistence,
|
||||||
advertisedAddress: NetworkHostAndPort = serverAddress,
|
advertisedAddress: NetworkHostAndPort = serverAddress,
|
||||||
private val maxMessageSize: Int
|
maxMessageSize: Int
|
||||||
) : SingletonSerializeAsToken(), MessagingService {
|
) : SingletonSerializeAsToken(), MessagingService {
|
||||||
companion object {
|
companion object {
|
||||||
private val log = contextLogger()
|
private val log = contextLogger()
|
||||||
@ -126,6 +132,7 @@ class P2PMessagingClient(config: NodeConfiguration,
|
|||||||
private class InnerState {
|
private class InnerState {
|
||||||
var running = false
|
var running = false
|
||||||
var p2pConsumer: ClientConsumer? = null
|
var p2pConsumer: ClientConsumer? = null
|
||||||
|
var serviceConsumer: ClientConsumer? = null
|
||||||
}
|
}
|
||||||
|
|
||||||
private val messagesToRedeliver = database.transaction {
|
private val messagesToRedeliver = database.transaction {
|
||||||
@ -181,8 +188,20 @@ class P2PMessagingClient(config: NodeConfiguration,
|
|||||||
fun start() {
|
fun start() {
|
||||||
state.locked {
|
state.locked {
|
||||||
val session = artemis.start().session
|
val session = artemis.start().session
|
||||||
|
val inbox = RemoteInboxAddress(myIdentity).queueName
|
||||||
// Create a queue, consumer and producer for handling P2P network messages.
|
// 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(msg, message)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resumeMessageRedelivery()
|
resumeMessageRedelivery()
|
||||||
@ -331,6 +350,13 @@ class P2PMessagingClient(config: NodeConfiguration,
|
|||||||
// Ignore it: this can happen if the server has gone away before we do.
|
// Ignore it: this can happen if the server has gone away before we do.
|
||||||
}
|
}
|
||||||
p2pConsumer = null
|
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
|
prevRunning
|
||||||
}
|
}
|
||||||
if (running && !nodeExecutor.isOnThread) {
|
if (running && !nodeExecutor.isOnThread) {
|
||||||
@ -430,7 +456,7 @@ class P2PMessagingClient(config: NodeConfiguration,
|
|||||||
private fun getMQAddress(target: MessageRecipients): String {
|
private fun getMQAddress(target: MessageRecipients): String {
|
||||||
return if (target == myAddress) {
|
return if (target == myAddress) {
|
||||||
// If we are sending to ourselves then route the message directly to our P2P queue.
|
// If we are sending to ourselves then route the message directly to our P2P queue.
|
||||||
P2P_QUEUE
|
RemoteInboxAddress(myIdentity).queueName
|
||||||
} else {
|
} else {
|
||||||
// Otherwise we send the message to an internal queue for the target residing on our broker. It's then the
|
// 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.
|
// broker's job to route the message to the target's P2P queue.
|
||||||
@ -447,7 +473,7 @@ class P2PMessagingClient(config: NodeConfiguration,
|
|||||||
val queueQuery = session.queueQuery(SimpleString(queueName))
|
val queueQuery = session.queueQuery(SimpleString(queueName))
|
||||||
if (!queueQuery.isExists) {
|
if (!queueQuery.isExists) {
|
||||||
log.info("Create fresh queue $queueName bound on same address")
|
log.info("Create fresh queue $queueName bound on same address")
|
||||||
session.createQueue(queueName, RoutingType.MULTICAST, queueName, true)
|
session.createQueue(queueName, RoutingType.ANYCAST, queueName, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,15 +7,15 @@ import net.corda.core.transactions.LedgerTransaction
|
|||||||
import net.corda.core.utilities.NetworkHostAndPort
|
import net.corda.core.utilities.NetworkHostAndPort
|
||||||
import net.corda.core.utilities.loggerFor
|
import net.corda.core.utilities.loggerFor
|
||||||
import net.corda.node.services.transactions.OutOfProcessTransactionVerifierService
|
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
|
||||||
import net.corda.nodeapi.VerifierApi.VERIFICATION_REQUESTS_QUEUE_NAME
|
import net.corda.nodeapi.VerifierApi.VERIFICATION_REQUESTS_QUEUE_NAME
|
||||||
import net.corda.nodeapi.VerifierApi.VERIFICATION_RESPONSES_QUEUE_NAME_PREFIX
|
import net.corda.nodeapi.VerifierApi.VERIFICATION_RESPONSES_QUEUE_NAME_PREFIX
|
||||||
import net.corda.nodeapi.internal.config.SSLConfiguration
|
import net.corda.nodeapi.internal.config.SSLConfiguration
|
||||||
import org.apache.activemq.artemis.api.core.RoutingType
|
import org.apache.activemq.artemis.api.core.RoutingType
|
||||||
import org.apache.activemq.artemis.api.core.SimpleString
|
import org.apache.activemq.artemis.api.core.SimpleString
|
||||||
import org.apache.activemq.artemis.api.core.client.*
|
import org.apache.activemq.artemis.api.core.client.ClientConsumer
|
||||||
import java.util.concurrent.*
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class VerifierMessagingClient(config: SSLConfiguration, serverAddress: NetworkHostAndPort, metrics: MetricRegistry, private val maxMessageSize: Int) : SingletonSerializeAsToken() {
|
class VerifierMessagingClient(config: SSLConfiguration, serverAddress: NetworkHostAndPort, metrics: MetricRegistry, private val maxMessageSize: Int) : SingletonSerializeAsToken() {
|
||||||
companion object {
|
companion object {
|
||||||
@ -40,7 +40,7 @@ class VerifierMessagingClient(config: SSLConfiguration, serverAddress: NetworkHo
|
|||||||
val queueQuery = session.queueQuery(SimpleString(queueName))
|
val queueQuery = session.queueQuery(SimpleString(queueName))
|
||||||
if (!queueQuery.isExists) {
|
if (!queueQuery.isExists) {
|
||||||
log.info("Create fresh queue $queueName bound on same address")
|
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)
|
createQueueIfAbsent(VERIFICATION_REQUESTS_QUEUE_NAME)
|
||||||
|
@ -160,6 +160,12 @@ open class PersistentNetworkMapCache(
|
|||||||
|
|
||||||
private val nodesByKeyCache = NonInvalidatingCache<PublicKey, List<NodeInfo>>(1024, 8, { key -> database.transaction { queryByIdentityKey(session, key) } })
|
private val nodesByKeyCache = NonInvalidatingCache<PublicKey, List<NodeInfo>>(1024, 8, { key -> database.transaction { queryByIdentityKey(session, key) } })
|
||||||
|
|
||||||
|
override fun getNodesByOwningKeyIndex(identityKeyIndex: String): List<NodeInfo> {
|
||||||
|
return database.transaction {
|
||||||
|
queryByIdentityKeyIndex(session, identityKeyIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun getNodeByAddress(address: NetworkHostAndPort): NodeInfo? = database.transaction { queryByAddress(session, address) }
|
override fun getNodeByAddress(address: NetworkHostAndPort): NodeInfo? = database.transaction { queryByAddress(session, address) }
|
||||||
|
|
||||||
override fun getPeerCertificateByLegalName(name: CordaX500Name): PartyAndCertificate? = identityByLegalNameCache.get(name).orElse(null)
|
override fun getPeerCertificateByLegalName(name: CordaX500Name): PartyAndCertificate? = identityByLegalNameCache.get(name).orElse(null)
|
||||||
@ -245,15 +251,23 @@ open class PersistentNetworkMapCache(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun findByIdentityKey(session: Session, identityKey: PublicKey): List<NodeInfoSchemaV1.PersistentNodeInfo> {
|
private fun findByIdentityKey(session: Session, identityKey: PublicKey): List<NodeInfoSchemaV1.PersistentNodeInfo> {
|
||||||
|
return findByIdentityKeyIndex(session, identityKey.toStringShort())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findByIdentityKeyIndex(session: Session, identityKeyIndex: String): List<NodeInfoSchemaV1.PersistentNodeInfo> {
|
||||||
val query = session.createQuery(
|
val query = session.createQuery(
|
||||||
"SELECT n FROM ${NodeInfoSchemaV1.PersistentNodeInfo::class.java.name} n JOIN n.legalIdentitiesAndCerts l WHERE l.owningKeyHash = :owningKeyHash",
|
"SELECT n FROM ${NodeInfoSchemaV1.PersistentNodeInfo::class.java.name} n JOIN n.legalIdentitiesAndCerts l WHERE l.owningKeyHash = :owningKeyHash",
|
||||||
NodeInfoSchemaV1.PersistentNodeInfo::class.java)
|
NodeInfoSchemaV1.PersistentNodeInfo::class.java)
|
||||||
query.setParameter("owningKeyHash", identityKey.toStringShort())
|
query.setParameter("owningKeyHash", identityKeyIndex)
|
||||||
return query.resultList
|
return query.resultList
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun queryByIdentityKey(session: Session, identityKey: PublicKey): List<NodeInfo> {
|
private fun queryByIdentityKey(session: Session, identityKey: PublicKey): List<NodeInfo> {
|
||||||
val result = findByIdentityKey(session, identityKey)
|
return queryByIdentityKeyIndex(session, identityKey.toStringShort())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun queryByIdentityKeyIndex(session: Session, identityKeyIndex: String): List<NodeInfo> {
|
||||||
|
val result = findByIdentityKeyIndex(session, identityKeyIndex)
|
||||||
return result.map { it.toNodeInfo() }
|
return result.map { it.toNodeInfo() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -279,13 +293,13 @@ open class PersistentNetworkMapCache(
|
|||||||
|
|
||||||
private fun queryByAddress(session: Session, hostAndPort: NetworkHostAndPort): NodeInfo? {
|
private fun queryByAddress(session: Session, hostAndPort: NetworkHostAndPort): NodeInfo? {
|
||||||
val query = session.createQuery(
|
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)
|
NodeInfoSchemaV1.PersistentNodeInfo::class.java)
|
||||||
query.setParameter("host", hostAndPort.host)
|
query.setParameter("host", hostAndPort.host)
|
||||||
query.setParameter("port", hostAndPort.port)
|
query.setParameter("port", hostAndPort.port)
|
||||||
|
query.setMaxResults(1)
|
||||||
val result = query.resultList
|
val result = query.resultList
|
||||||
return if (result.isEmpty()) null
|
return result.map { it.toNodeInfo() }.singleOrNull()
|
||||||
else result.map { it.toNodeInfo() }.singleOrNull() ?: throw IllegalStateException("More than one node with the same host and port")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Object Relational Mapping support. */
|
/** Object Relational Mapping support. */
|
||||||
|
@ -5,7 +5,6 @@ import net.corda.core.serialization.SerializedBytes
|
|||||||
import net.corda.core.utilities.debug
|
import net.corda.core.utilities.debug
|
||||||
import net.corda.node.services.api.CheckpointStorage
|
import net.corda.node.services.api.CheckpointStorage
|
||||||
import net.corda.node.services.statemachine.Checkpoint
|
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.NODE_DATABASE_PREFIX
|
||||||
import net.corda.nodeapi.internal.persistence.currentDBSession
|
import net.corda.nodeapi.internal.persistence.currentDBSession
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
@ -43,7 +42,7 @@ class DBCheckpointStorage : CheckpointStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun removeCheckpoint(id: StateMachineRunId): Boolean {
|
override fun removeCheckpoint(id: StateMachineRunId): Boolean {
|
||||||
val session = DatabaseTransactionManager.current().session
|
val session = currentDBSession()
|
||||||
val criteriaBuilder = session.criteriaBuilder
|
val criteriaBuilder = session.criteriaBuilder
|
||||||
val delete = criteriaBuilder.createCriteriaDelete(DBCheckpoint::class.java)
|
val delete = criteriaBuilder.createCriteriaDelete(DBCheckpoint::class.java)
|
||||||
val root = delete.from(DBCheckpoint::class.java)
|
val root = delete.from(DBCheckpoint::class.java)
|
||||||
|
@ -17,7 +17,6 @@ import net.corda.core.node.services.vault.AttachmentSort
|
|||||||
import net.corda.core.serialization.*
|
import net.corda.core.serialization.*
|
||||||
import net.corda.core.utilities.contextLogger
|
import net.corda.core.utilities.contextLogger
|
||||||
import net.corda.node.services.vault.HibernateAttachmentQueryCriteriaParser
|
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.NODE_DATABASE_PREFIX
|
||||||
import net.corda.nodeapi.internal.persistence.currentDBSession
|
import net.corda.nodeapi.internal.persistence.currentDBSession
|
||||||
import java.io.*
|
import java.io.*
|
||||||
@ -242,8 +241,7 @@ class NodeAttachmentService(metrics: MetricRegistry) : AttachmentStorage, Single
|
|||||||
|
|
||||||
override fun queryAttachments(criteria: AttachmentQueryCriteria, sorting: AttachmentSort?): List<AttachmentId> {
|
override fun queryAttachments(criteria: AttachmentQueryCriteria, sorting: AttachmentSort?): List<AttachmentId> {
|
||||||
log.info("Attachment query criteria: $criteria, sorting: $sorting")
|
log.info("Attachment query criteria: $criteria, sorting: $sorting")
|
||||||
|
val session = currentDBSession()
|
||||||
val session = DatabaseTransactionManager.current().session
|
|
||||||
val criteriaBuilder = session.criteriaBuilder
|
val criteriaBuilder = session.criteriaBuilder
|
||||||
|
|
||||||
val criteriaQuery = criteriaBuilder.createQuery(DBAttachment::class.java)
|
val criteriaQuery = criteriaBuilder.createQuery(DBAttachment::class.java)
|
||||||
|
@ -10,8 +10,8 @@ import net.corda.core.schemas.PersistentStateRef
|
|||||||
import net.corda.core.utilities.contextLogger
|
import net.corda.core.utilities.contextLogger
|
||||||
import net.corda.core.utilities.debug
|
import net.corda.core.utilities.debug
|
||||||
import net.corda.node.services.api.SchemaService
|
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.HibernateConfiguration
|
||||||
|
import net.corda.nodeapi.internal.persistence.contextTransaction
|
||||||
import org.hibernate.FlushMode
|
import org.hibernate.FlushMode
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
|
||||||
@ -54,7 +54,7 @@ class HibernateObserver private constructor(private val config: HibernateConfigu
|
|||||||
internal fun persistStatesWithSchema(statesAndRefs: List<ContractStateAndRef>, schema: MappedSchema) {
|
internal fun persistStatesWithSchema(statesAndRefs: List<ContractStateAndRef>, schema: MappedSchema) {
|
||||||
val sessionFactory = config.sessionFactoryForSchemas(setOf(schema))
|
val sessionFactory = config.sessionFactoryForSchemas(setOf(schema))
|
||||||
val session = sessionFactory.withOptions().
|
val session = sessionFactory.withOptions().
|
||||||
connection(DatabaseTransactionManager.current().connection).
|
connection(contextTransaction.connection).
|
||||||
flushMode(FlushMode.MANUAL).
|
flushMode(FlushMode.MANUAL).
|
||||||
openSession()
|
openSession()
|
||||||
session.use { thisSession ->
|
session.use { thisSession ->
|
||||||
|
@ -29,24 +29,20 @@ import net.corda.node.services.vault.VaultSchemaV1
|
|||||||
* TODO: support plugins for schema version upgrading or custom mapping not supported by original [QueryableState].
|
* 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
|
* TODO: create whitelisted tables when a CorDapp is first installed
|
||||||
*/
|
*/
|
||||||
class NodeSchemaService(extraSchemas: Set<MappedSchema> = emptySet()) : SchemaService, SingletonSerializeAsToken() {
|
class NodeSchemaService(extraSchemas: Set<MappedSchema> = emptySet(), includeNotarySchemas: Boolean = false) : SchemaService, SingletonSerializeAsToken() {
|
||||||
// Entities for compulsory services
|
// Core Entities used by a Node
|
||||||
object NodeServices
|
object NodeCore
|
||||||
|
|
||||||
object NodeServicesV1 : MappedSchema(schemaFamily = NodeServices.javaClass, version = 1,
|
object NodeCoreV1 : MappedSchema(schemaFamily = NodeCore.javaClass, version = 1,
|
||||||
mappedTypes = listOf(DBCheckpointStorage.DBCheckpoint::class.java,
|
mappedTypes = listOf(DBCheckpointStorage.DBCheckpoint::class.java,
|
||||||
DBTransactionStorage.DBTransaction::class.java,
|
DBTransactionStorage.DBTransaction::class.java,
|
||||||
DBTransactionMappingStorage.DBTransactionMapping::class.java,
|
DBTransactionMappingStorage.DBTransactionMapping::class.java,
|
||||||
PersistentKeyManagementService.PersistentKey::class.java,
|
PersistentKeyManagementService.PersistentKey::class.java,
|
||||||
PersistentUniquenessProvider.PersistentUniqueness::class.java,
|
|
||||||
PersistentUniquenessProvider.PersistentNotaryCommit::class.java,
|
|
||||||
NodeSchedulerService.PersistentScheduledState::class.java,
|
NodeSchedulerService.PersistentScheduledState::class.java,
|
||||||
NodeAttachmentService.DBAttachment::class.java,
|
NodeAttachmentService.DBAttachment::class.java,
|
||||||
P2PMessagingClient.ProcessedMessage::class.java,
|
P2PMessagingClient.ProcessedMessage::class.java,
|
||||||
P2PMessagingClient.RetryMessage::class.java,
|
P2PMessagingClient.RetryMessage::class.java,
|
||||||
NodeAttachmentService.DBAttachment::class.java,
|
NodeAttachmentService.DBAttachment::class.java,
|
||||||
RaftUniquenessProvider.RaftState::class.java,
|
|
||||||
BFTNonValidatingNotaryService.PersistedCommittedState::class.java,
|
|
||||||
PersistentIdentityService.PersistentIdentity::class.java,
|
PersistentIdentityService.PersistentIdentity::class.java,
|
||||||
PersistentIdentityService.PersistentIdentityNames::class.java,
|
PersistentIdentityService.PersistentIdentityNames::class.java,
|
||||||
ContractUpgradeServiceImpl.DBContractUpgrade::class.java,
|
ContractUpgradeServiceImpl.DBContractUpgrade::class.java,
|
||||||
@ -67,15 +63,25 @@ class NodeSchemaService(extraSchemas: Set<MappedSchema> = emptySet()) : SchemaSe
|
|||||||
override val migrationResource = "node-notary.changelog-master"
|
override val migrationResource = "node-notary.changelog-master"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
// 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<MappedSchema, SchemaService.SchemaOptions> =
|
private val requiredSchemas: Map<MappedSchema, SchemaService.SchemaOptions> =
|
||||||
mapOf(Pair(CommonSchemaV1, SchemaOptions()),
|
mapOf(Pair(CommonSchemaV1, SchemaOptions()),
|
||||||
Pair(VaultSchemaV1, SchemaOptions()),
|
Pair(VaultSchemaV1, SchemaOptions()),
|
||||||
Pair(NodeInfoSchemaV1, SchemaOptions()),
|
Pair(NodeInfoSchemaV1, SchemaOptions()),
|
||||||
Pair(NodeServicesV1, SchemaOptions()))
|
Pair(NodeCoreV1, SchemaOptions()))
|
||||||
|
private val notarySchemas = if (includeNotarySchemas) mapOf(Pair(NodeNotaryV1, SchemaOptions())) else emptyMap<MappedSchema, SchemaService.SchemaOptions>()
|
||||||
|
|
||||||
override val schemaOptions: Map<MappedSchema, SchemaService.SchemaOptions> = requiredSchemas + extraSchemas.associateBy({ it }, { SchemaOptions() })
|
override val schemaOptions: Map<MappedSchema, SchemaService.SchemaOptions> = requiredSchemas + notarySchemas + extraSchemas.associateBy({ it }, { SchemaOptions() })
|
||||||
|
|
||||||
// Currently returns all schemas supported by the state, with no filtering or enrichment.
|
// Currently returns all schemas supported by the state, with no filtering or enrichment.
|
||||||
override fun selectSchemas(state: ContractState): Iterable<MappedSchema> {
|
override fun selectSchemas(state: ContractState): Iterable<MappedSchema> {
|
||||||
|
@ -10,7 +10,9 @@ import net.corda.core.utilities.contextLogger
|
|||||||
import net.corda.core.utilities.trace
|
import net.corda.core.utilities.trace
|
||||||
import net.corda.node.services.api.CheckpointStorage
|
import net.corda.node.services.api.CheckpointStorage
|
||||||
import net.corda.node.services.api.ServiceHubInternal
|
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.Duration
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
@ -163,24 +165,24 @@ class ActionExecutorImpl(
|
|||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
private fun executeCreateTransaction() {
|
private fun executeCreateTransaction() {
|
||||||
if (DatabaseTransactionManager.currentOrNull() != null) {
|
if (contextTransactionOrNull != null) {
|
||||||
throw IllegalStateException("Refusing to create a second transaction")
|
throw IllegalStateException("Refusing to create a second transaction")
|
||||||
}
|
}
|
||||||
DatabaseTransactionManager.newTransaction()
|
contextDatabase.newTransaction()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
private fun executeRollbackTransaction() {
|
private fun executeRollbackTransaction() {
|
||||||
DatabaseTransactionManager.currentOrNull()?.close()
|
contextTransactionOrNull?.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
private fun executeCommitTransaction() {
|
private fun executeCommitTransaction() {
|
||||||
try {
|
try {
|
||||||
DatabaseTransactionManager.current().commit()
|
contextTransaction.commit()
|
||||||
} finally {
|
} finally {
|
||||||
DatabaseTransactionManager.current().close()
|
contextTransaction.close()
|
||||||
DatabaseTransactionManager.setThreadLocalTx(null)
|
contextTransactionOrNull = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,7 +27,8 @@ import net.corda.node.services.statemachine.transitions.FlowContinuation
|
|||||||
import net.corda.node.services.statemachine.transitions.StateMachine
|
import net.corda.node.services.statemachine.transitions.StateMachine
|
||||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||||
import net.corda.nodeapi.internal.persistence.DatabaseTransaction
|
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.Logger
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.nio.file.Paths
|
import java.nio.file.Paths
|
||||||
@ -58,8 +59,8 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun extractThreadLocalTransaction(): TransientReference<DatabaseTransaction> {
|
private fun extractThreadLocalTransaction(): TransientReference<DatabaseTransaction> {
|
||||||
val transaction = DatabaseTransactionManager.current()
|
val transaction = contextTransaction
|
||||||
DatabaseTransactionManager.setThreadLocalTx(null)
|
contextTransactionOrNull = null
|
||||||
return TransientReference(transaction)
|
return TransientReference(transaction)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -234,7 +235,7 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
|
|||||||
parkAndSerialize { _, _ ->
|
parkAndSerialize { _, _ ->
|
||||||
logger.trace { "Suspended on $ioRequest" }
|
logger.trace { "Suspended on $ioRequest" }
|
||||||
|
|
||||||
DatabaseTransactionManager.setThreadLocalTx(transaction.value)
|
contextTransactionOrNull = transaction.value
|
||||||
val event = try {
|
val event = try {
|
||||||
Event.Suspend(
|
Event.Suspend(
|
||||||
ioRequest = ioRequest,
|
ioRequest = ioRequest,
|
||||||
|
@ -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.FlowContinuation
|
||||||
import net.corda.node.services.statemachine.transitions.TransitionResult
|
import net.corda.node.services.statemachine.transitions.TransitionResult
|
||||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
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
|
import java.security.SecureRandom
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -31,12 +32,12 @@ class TransitionExecutorImpl(
|
|||||||
transition: TransitionResult,
|
transition: TransitionResult,
|
||||||
actionExecutor: ActionExecutor
|
actionExecutor: ActionExecutor
|
||||||
): Pair<FlowContinuation, StateMachineState> {
|
): Pair<FlowContinuation, StateMachineState> {
|
||||||
DatabaseTransactionManager.dataSource = database
|
contextDatabase = database
|
||||||
for (action in transition.actions) {
|
for (action in transition.actions) {
|
||||||
try {
|
try {
|
||||||
actionExecutor.executeAction(fiber, action)
|
actionExecutor.executeAction(fiber, action)
|
||||||
} catch (exception: Throwable) {
|
} catch (exception: Throwable) {
|
||||||
DatabaseTransactionManager.currentOrNull()?.close()
|
contextTransactionOrNull?.close()
|
||||||
if (transition.newState.checkpoint.errorState is ErrorState.Errored) {
|
if (transition.newState.checkpoint.errorState is ErrorState.Errored) {
|
||||||
// If we errored while transitioning to an error state then we cannot record the additional
|
// 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.
|
// error as that may result in an infinite loop, e.g. error propagation fails -> record error -> propagate fails again.
|
||||||
|
@ -5,6 +5,7 @@ import net.corda.core.flows.FlowSession
|
|||||||
import net.corda.core.node.services.TimeWindowChecker
|
import net.corda.core.node.services.TimeWindowChecker
|
||||||
import net.corda.core.node.services.TrustedAuthorityNotaryService
|
import net.corda.core.node.services.TrustedAuthorityNotaryService
|
||||||
import net.corda.node.services.api.ServiceHubInternal
|
import net.corda.node.services.api.ServiceHubInternal
|
||||||
|
import net.corda.node.services.config.MySQLConfiguration
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@ -12,14 +13,14 @@ import java.util.*
|
|||||||
abstract class MySQLNotaryService(
|
abstract class MySQLNotaryService(
|
||||||
final override val services: ServiceHubInternal,
|
final override val services: ServiceHubInternal,
|
||||||
override val notaryIdentityKey: PublicKey,
|
override val notaryIdentityKey: PublicKey,
|
||||||
dataSourceProperties: Properties,
|
configuration: MySQLConfiguration,
|
||||||
/** Database table will be automatically created in dev mode */
|
/** Database table will be automatically created in dev mode */
|
||||||
val devMode: Boolean) : TrustedAuthorityNotaryService() {
|
val devMode: Boolean) : TrustedAuthorityNotaryService() {
|
||||||
|
|
||||||
override val timeWindowChecker = TimeWindowChecker(services.clock)
|
override val timeWindowChecker = TimeWindowChecker(services.clock)
|
||||||
override val uniquenessProvider = MySQLUniquenessProvider(
|
override val uniquenessProvider = MySQLUniquenessProvider(
|
||||||
services.monitoringService.metrics,
|
services.monitoringService.metrics,
|
||||||
dataSourceProperties
|
configuration
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun start() {
|
override fun start() {
|
||||||
@ -33,14 +34,14 @@ abstract class MySQLNotaryService(
|
|||||||
|
|
||||||
class MySQLNonValidatingNotaryService(services: ServiceHubInternal,
|
class MySQLNonValidatingNotaryService(services: ServiceHubInternal,
|
||||||
notaryIdentityKey: PublicKey,
|
notaryIdentityKey: PublicKey,
|
||||||
dataSourceProperties: Properties,
|
configuration: MySQLConfiguration,
|
||||||
devMode: Boolean = false) : MySQLNotaryService(services, notaryIdentityKey, dataSourceProperties, devMode) {
|
devMode: Boolean = false) : MySQLNotaryService(services, notaryIdentityKey, configuration, devMode) {
|
||||||
override fun createServiceFlow(otherPartySession: FlowSession): FlowLogic<Void?> = NonValidatingNotaryFlow(otherPartySession, this)
|
override fun createServiceFlow(otherPartySession: FlowSession): FlowLogic<Void?> = NonValidatingNotaryFlow(otherPartySession, this)
|
||||||
}
|
}
|
||||||
|
|
||||||
class MySQLValidatingNotaryService(services: ServiceHubInternal,
|
class MySQLValidatingNotaryService(services: ServiceHubInternal,
|
||||||
notaryIdentityKey: PublicKey,
|
notaryIdentityKey: PublicKey,
|
||||||
dataSourceProperties: Properties,
|
configuration: MySQLConfiguration,
|
||||||
devMode: Boolean = false) : MySQLNotaryService(services, notaryIdentityKey, dataSourceProperties, devMode) {
|
devMode: Boolean = false) : MySQLNotaryService(services, notaryIdentityKey, configuration, devMode) {
|
||||||
override fun createServiceFlow(otherPartySession: FlowSession): FlowLogic<Void?> = ValidatingNotaryFlow(otherPartySession, this)
|
override fun createServiceFlow(otherPartySession: FlowSession): FlowLogic<Void?> = ValidatingNotaryFlow(otherPartySession, this)
|
||||||
}
|
}
|
@ -14,9 +14,11 @@ import net.corda.core.serialization.SingletonSerializeAsToken
|
|||||||
import net.corda.core.serialization.deserialize
|
import net.corda.core.serialization.deserialize
|
||||||
import net.corda.core.serialization.serialize
|
import net.corda.core.serialization.serialize
|
||||||
import net.corda.core.utilities.loggerFor
|
import net.corda.core.utilities.loggerFor
|
||||||
|
import net.corda.node.services.config.MySQLConfiguration
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
import java.sql.BatchUpdateException
|
import java.sql.BatchUpdateException
|
||||||
import java.sql.Connection
|
import java.sql.Connection
|
||||||
|
import java.sql.SQLTransientConnectionException
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -27,7 +29,7 @@ import java.util.*
|
|||||||
*/
|
*/
|
||||||
class MySQLUniquenessProvider(
|
class MySQLUniquenessProvider(
|
||||||
metrics: MetricRegistry,
|
metrics: MetricRegistry,
|
||||||
dataSourceProperties: Properties
|
configuration: MySQLConfiguration
|
||||||
) : UniquenessProvider, SingletonSerializeAsToken() {
|
) : UniquenessProvider, SingletonSerializeAsToken() {
|
||||||
companion object {
|
companion object {
|
||||||
private val log = loggerFor<MySQLUniquenessProvider>()
|
private val log = loggerFor<MySQLUniquenessProvider>()
|
||||||
@ -57,13 +59,29 @@ class MySQLUniquenessProvider(
|
|||||||
* This is a useful heath metric.
|
* This is a useful heath metric.
|
||||||
*/
|
*/
|
||||||
private val rollbackCounter = metrics.counter("$metricPrefix.Rollback")
|
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. */
|
/** Track double spend attempts. Note that this will also include notarisation retries. */
|
||||||
private val conflictCounter = metrics.counter("$metricPrefix.Conflicts")
|
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
|
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() {
|
fun createTable() {
|
||||||
log.debug("Attempting to create DB table if it does not yet exist: $createTableStatement")
|
log.debug("Attempting to create DB table if it does not yet exist: $createTableStatement")
|
||||||
|
@ -17,12 +17,8 @@ import net.corda.core.transactions.NotaryChangeWireTransaction
|
|||||||
import net.corda.core.transactions.WireTransaction
|
import net.corda.core.transactions.WireTransaction
|
||||||
import net.corda.core.utilities.*
|
import net.corda.core.utilities.*
|
||||||
import net.corda.node.services.api.VaultServiceInternal
|
import net.corda.node.services.api.VaultServiceInternal
|
||||||
import net.corda.nodeapi.internal.persistence.HibernateConfiguration
|
|
||||||
import net.corda.node.services.statemachine.FlowStateMachineImpl
|
import net.corda.node.services.statemachine.FlowStateMachineImpl
|
||||||
import net.corda.nodeapi.internal.persistence.DatabaseTransactionManager
|
import net.corda.nodeapi.internal.persistence.*
|
||||||
import net.corda.nodeapi.internal.persistence.bufferUntilDatabaseCommit
|
|
||||||
import net.corda.nodeapi.internal.persistence.currentDBSession
|
|
||||||
import net.corda.nodeapi.internal.persistence.wrapWithDatabaseTransaction
|
|
||||||
import org.hibernate.Session
|
import org.hibernate.Session
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.subjects.PublishSubject
|
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
|
* Derive list from existing vault states and then incrementally update using vault observables
|
||||||
*/
|
*/
|
||||||
|
@ -10,7 +10,7 @@ import net.corda.core.utilities.toNonEmptySet
|
|||||||
import net.corda.core.utilities.trace
|
import net.corda.core.utilities.trace
|
||||||
import net.corda.node.services.statemachine.FlowStateMachineImpl
|
import net.corda.node.services.statemachine.FlowStateMachineImpl
|
||||||
import net.corda.node.services.statemachine.StateMachineManager
|
import net.corda.node.services.statemachine.StateMachineManager
|
||||||
import net.corda.nodeapi.internal.persistence.DatabaseTransactionManager
|
import net.corda.nodeapi.internal.persistence.contextDatabase
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class VaultSoftLockManager private constructor(private val vault: VaultService) {
|
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<StateRef>) {
|
private fun registerSoftLocks(flowId: UUID, stateRefs: NonEmptySet<StateRef>) {
|
||||||
log.trace { "Reserving soft locks for flow id $flowId and states $stateRefs" }
|
log.trace { "Reserving soft locks for flow id $flowId and states $stateRefs" }
|
||||||
DatabaseTransactionManager.dataSource.transaction {
|
contextDatabase.transaction {
|
||||||
vault.softLockReserve(flowId, stateRefs)
|
vault.softLockReserve(flowId, stateRefs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun unregisterSoftLocks(flowId: UUID, logic: FlowLogic<*>) {
|
private fun unregisterSoftLocks(flowId: UUID, logic: FlowLogic<*>) {
|
||||||
log.trace { "Releasing soft locks for flow ${logic.javaClass.simpleName} with flow id $flowId" }
|
log.trace { "Releasing soft locks for flow ${logic.javaClass.simpleName} with flow id $flowId" }
|
||||||
DatabaseTransactionManager.dataSource.transaction {
|
contextDatabase.transaction {
|
||||||
vault.softLockRelease(flowId)
|
vault.softLockRelease(flowId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -67,4 +67,17 @@
|
|||||||
constraintName="FK__info_hosts__infos"
|
constraintName="FK__info_hosts__infos"
|
||||||
referencedColumnNames="node_info_id" referencedTableName="node_infos"/>
|
referencedColumnNames="node_info_id" referencedTableName="node_infos"/>
|
||||||
</changeSet>
|
</changeSet>
|
||||||
|
<changeSet author="R3.Corda" id="remove_host_port_pk">
|
||||||
|
<delete tableName="node_infos"/>
|
||||||
|
<delete tableName="node_link_nodeinfo_party"/>
|
||||||
|
<delete tableName="node_info_hosts"/>
|
||||||
|
<delete tableName="node_info_party_cert"/>
|
||||||
|
<dropPrimaryKey tableName="node_info_hosts" constraintName="node_info_hosts_pkey"/>
|
||||||
|
<addColumn tableName="node_info_hosts">
|
||||||
|
<column name="hosts_id" type="INT">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
</addColumn>
|
||||||
|
<addPrimaryKey columnNames="hosts_id" constraintName="node_info_hosts_pkey_id" tableName="node_info_hosts"/>
|
||||||
|
</changeSet>
|
||||||
</databaseChangeLog>
|
</databaseChangeLog>
|
||||||
|
@ -14,7 +14,6 @@ database = {
|
|||||||
exportHibernateJMXStatistics = "false"
|
exportHibernateJMXStatistics = "false"
|
||||||
}
|
}
|
||||||
devMode = true
|
devMode = true
|
||||||
useHTTPS = false
|
|
||||||
h2port = 0
|
h2port = 0
|
||||||
useTestClock = false
|
useTestClock = false
|
||||||
verifierType = InMemory
|
verifierType = InMemory
|
||||||
|
@ -76,7 +76,6 @@ class NodeConfigurationImplTest {
|
|||||||
dataSourceProperties = makeTestDataSourceProperties(ALICE_NAME.organisation),
|
dataSourceProperties = makeTestDataSourceProperties(ALICE_NAME.organisation),
|
||||||
rpcUsers = emptyList(),
|
rpcUsers = emptyList(),
|
||||||
verifierType = VerifierType.InMemory,
|
verifierType = VerifierType.InMemory,
|
||||||
useHTTPS = false,
|
|
||||||
p2pAddress = NetworkHostAndPort("localhost", 0),
|
p2pAddress = NetworkHostAndPort("localhost", 0),
|
||||||
rpcAddress = NetworkHostAndPort("localhost", 1),
|
rpcAddress = NetworkHostAndPort("localhost", 1),
|
||||||
messagingServerAddress = null,
|
messagingServerAddress = null,
|
||||||
|
@ -178,6 +178,7 @@ class ArtemisMessagingTests {
|
|||||||
MOCK_VERSION_INFO.copy(platformVersion = platformVersion),
|
MOCK_VERSION_INFO.copy(platformVersion = platformVersion),
|
||||||
server,
|
server,
|
||||||
identity.public,
|
identity.public,
|
||||||
|
null,
|
||||||
ServiceAffinityExecutor("ArtemisMessagingTests", 1),
|
ServiceAffinityExecutor("ArtemisMessagingTests", 1),
|
||||||
database,
|
database,
|
||||||
maxMessageSize = maxMessageSize).apply {
|
maxMessageSize = maxMessageSize).apply {
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
package net.corda.node.services.network
|
package net.corda.node.services.network
|
||||||
|
|
||||||
|
import net.corda.core.crypto.generateKeyPair
|
||||||
import net.corda.core.node.services.NetworkMapCache
|
import net.corda.core.node.services.NetworkMapCache
|
||||||
import net.corda.node.services.api.NetworkMapCacheInternal
|
import net.corda.node.services.api.NetworkMapCacheInternal
|
||||||
import net.corda.testing.ALICE_NAME
|
import net.corda.testing.ALICE_NAME
|
||||||
import net.corda.testing.BOB_NAME
|
import net.corda.testing.BOB_NAME
|
||||||
|
import net.corda.testing.getTestPartyAndCertificate
|
||||||
import net.corda.testing.node.MockNetwork
|
import net.corda.testing.node.MockNetwork
|
||||||
import net.corda.testing.node.MockNodeParameters
|
import net.corda.testing.node.MockNodeParameters
|
||||||
import net.corda.testing.singleIdentity
|
import net.corda.testing.singleIdentity
|
||||||
@ -106,4 +108,14 @@ class NetworkMapCacheTest {
|
|||||||
assertThat(bobCache.getNodeByLegalName(alice.name) == null)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,6 @@ import net.corda.core.schemas.QueryableState
|
|||||||
import net.corda.node.services.api.SchemaService
|
import net.corda.node.services.api.SchemaService
|
||||||
import net.corda.node.internal.configureDatabase
|
import net.corda.node.internal.configureDatabase
|
||||||
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
||||||
import net.corda.nodeapi.internal.persistence.DatabaseTransactionManager
|
|
||||||
import net.corda.testing.internal.LogHelper
|
import net.corda.testing.internal.LogHelper
|
||||||
import net.corda.testing.TestIdentity
|
import net.corda.testing.TestIdentity
|
||||||
import net.corda.testing.contracts.DummyContract
|
import net.corda.testing.contracts.DummyContract
|
||||||
@ -74,11 +73,11 @@ class HibernateObserverTests {
|
|||||||
database.transaction {
|
database.transaction {
|
||||||
val MEGA_CORP = TestIdentity(CordaX500Name("MegaCorp", "London", "GB")).party
|
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)))))
|
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()
|
parentRowCountResult.next()
|
||||||
val parentRows = parentRowCountResult.getInt(1)
|
val parentRows = parentRowCountResult.getInt(1)
|
||||||
parentRowCountResult.close()
|
parentRowCountResult.close()
|
||||||
val childrenRowCountResult = DatabaseTransactionManager.current().connection.prepareStatement("select count(*) from Children").executeQuery()
|
val childrenRowCountResult = connection.prepareStatement("select count(*) from Children").executeQuery()
|
||||||
childrenRowCountResult.next()
|
childrenRowCountResult.next()
|
||||||
val childrenRows = childrenRowCountResult.getInt(1)
|
val childrenRows = childrenRowCountResult.getInt(1)
|
||||||
childrenRowCountResult.close()
|
childrenRowCountResult.close()
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user