mirror of
https://github.com/corda/corda.git
synced 2025-05-02 16:53:22 +00:00
Converted FullNodeConfiguration into a data class and added ability to parse Configs into data classes
This commit is contained in:
parent
868a490bba
commit
c17fe29a62
@ -85,7 +85,7 @@ path to the node's base directory.
|
||||
|
||||
:rpcAddress: The address of the RPC system on which RPC requests can be made to the node. If not provided then the node will run without RPC.
|
||||
|
||||
:webAddress: The host and port on which the bundled webserver will listen if it is started.
|
||||
:webAddress: The host and port on which the webserver will listen if it is started. This is not used by the node itself.
|
||||
|
||||
.. note:: If HTTPS is enabled then the browser security checks will require that the accessing url host name is one
|
||||
of either the machine name, fully qualified machine name, or server IP address to line up with the Subject Alternative
|
||||
@ -123,7 +123,7 @@ path to the node's base directory.
|
||||
: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:
|
||||
|
||||
:user: Username consisting only of word characters (a-z, A-Z, 0-9 and _)
|
||||
:username: Username consisting only of word characters (a-z, A-Z, 0-9 and _)
|
||||
:password: The password
|
||||
:permissions: A list of permission strings which RPC methods can use to control access
|
||||
|
||||
|
@ -95,7 +95,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
|
||||
webPort 10007
|
||||
cordapps = []
|
||||
rpcUsers = [
|
||||
['user' : "user",
|
||||
['username' : "user",
|
||||
'password' : "password",
|
||||
'permissions' : ["StartFlow.net.corda.flows.CashFlow"]]
|
||||
]
|
||||
|
@ -18,7 +18,7 @@ networkMapService : {
|
||||
}
|
||||
useHTTPS : false
|
||||
rpcUsers : [
|
||||
{ user=user1, password=letmein, permissions=[ StartProtocol.net.corda.protocols.CashProtocol ] }
|
||||
{ username=user1, password=letmein, permissions=[ StartProtocol.net.corda.protocols.CashProtocol ] }
|
||||
]
|
||||
devMode : true
|
||||
// Certificate signing service will be hosted by R3 in the near future.
|
||||
|
@ -2,6 +2,7 @@ package net.corda.docs
|
||||
|
||||
import net.corda.node.services.config.ConfigHelper
|
||||
import net.corda.node.services.config.FullNodeConfiguration
|
||||
import net.corda.nodeapi.config.parseAs
|
||||
import net.corda.verifier.Verifier
|
||||
import org.junit.Test
|
||||
import java.nio.file.Path
|
||||
@ -30,13 +31,10 @@ class ExampleConfigTest {
|
||||
"example-network-map-node.conf"
|
||||
) {
|
||||
val baseDirectory = Paths.get("some-example-base-dir")
|
||||
FullNodeConfiguration(
|
||||
baseDirectory,
|
||||
ConfigHelper.loadConfig(
|
||||
baseDirectory = baseDirectory,
|
||||
configFile = it
|
||||
)
|
||||
)
|
||||
ConfigHelper.loadConfig(
|
||||
baseDirectory = baseDirectory,
|
||||
configFile = it
|
||||
).parseAs<FullNodeConfiguration>()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -123,7 +123,7 @@ When starting a standalone node using a configuration file we must supply the RP
|
||||
.. code-block:: text
|
||||
|
||||
rpcUsers : [
|
||||
{ user=user, password=password, permissions=[ StartFlow.net.corda.flows.CashFlow ] }
|
||||
{ username=user, password=password, permissions=[ StartFlow.net.corda.flows.CashFlow ] }
|
||||
]
|
||||
|
||||
When using the gradle Cordformation plugin to configure and deploy a node you must supply the RPC credentials in a similar manner:
|
||||
@ -131,7 +131,7 @@ When using the gradle Cordformation plugin to configure and deploy a node you mu
|
||||
.. code-block:: text
|
||||
|
||||
rpcUsers = [
|
||||
['user' : "user",
|
||||
['username' : "user",
|
||||
'password' : "password",
|
||||
'permissions' : ["StartFlow.net.corda.flows.CashFlow"]]
|
||||
]
|
||||
|
@ -36,7 +36,7 @@ class Node {
|
||||
/**
|
||||
* Set the RPC users for this node. This configuration block allows arbitrary configuration.
|
||||
* The recommended current structure is:
|
||||
* [[['user': "username_here", 'password': "password_here", 'permissions': ["permissions_here"]]]
|
||||
* [[['username': "username_here", 'password': "password_here", 'permissions': ["permissions_here"]]]
|
||||
* The above is a list to a map of keys to values using Groovy map and list shorthands.
|
||||
*
|
||||
* @note Incorrect configurations will not cause a DSL error.
|
||||
|
@ -38,4 +38,9 @@ dependencies {
|
||||
|
||||
// TypeSafe Config: for simple and human friendly config files.
|
||||
compile "com.typesafe:config:$typesafe_config_version"
|
||||
|
||||
// Unit testing helpers.
|
||||
testCompile "junit:junit:$junit_version"
|
||||
testCompile "org.assertj:assertj-core:${assertj_version}"
|
||||
testCompile project(':test-utils')
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import net.corda.core.flows.FlowException
|
||||
import net.corda.core.serialization.*
|
||||
import net.corda.core.toFuture
|
||||
import net.corda.core.toObservable
|
||||
import net.corda.nodeapi.config.OldConfig
|
||||
import org.apache.commons.fileupload.MultipartStream
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
@ -24,7 +25,11 @@ val rpcLog: Logger by lazy { LoggerFactory.getLogger("net.corda.rpc") }
|
||||
/** Used in the RPC wire protocol to wrap an observation with the handle of the observable it's intended for. */
|
||||
data class MarshalledObservation(val forHandle: Int, val what: Notification<*>)
|
||||
|
||||
data class User(val username: String, val password: String, val permissions: Set<String>) {
|
||||
data class User(
|
||||
@OldConfig("user")
|
||||
val username: String,
|
||||
val password: String,
|
||||
val permissions: Set<String>) {
|
||||
override fun toString(): String = "${javaClass.simpleName}($username, permissions=$permissions)"
|
||||
}
|
||||
|
||||
|
@ -2,73 +2,125 @@ package net.corda.nodeapi.config
|
||||
|
||||
import com.google.common.net.HostAndPort
|
||||
import com.typesafe.config.Config
|
||||
import com.typesafe.config.ConfigUtil
|
||||
import net.corda.core.noneOrSingle
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.net.Proxy
|
||||
import java.net.URL
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.util.*
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.KProperty
|
||||
import kotlin.reflect.jvm.javaType
|
||||
import kotlin.reflect.KType
|
||||
import kotlin.reflect.full.memberProperties
|
||||
import kotlin.reflect.full.primaryConstructor
|
||||
import kotlin.reflect.jvm.jvmErasure
|
||||
|
||||
private fun <T : Enum<T>> enumBridge(clazz: Class<T>, enumValueString: String): T {
|
||||
return java.lang.Enum.valueOf(clazz, enumValueString)
|
||||
}
|
||||
private class DummyEnum : Enum<DummyEnum>("", 0)
|
||||
@Target(AnnotationTarget.PROPERTY)
|
||||
annotation class OldConfig(val value: String)
|
||||
|
||||
@Suppress("UNCHECKED_CAST", "PLATFORM_CLASS_MAPPED_TO_KOTLIN")
|
||||
// TODO Move other config parsing to use parseAs and remove this
|
||||
operator fun <T> Config.getValue(receiver: Any, metadata: KProperty<*>): T {
|
||||
if (metadata.returnType.isMarkedNullable && !hasPath(metadata.name)) {
|
||||
return null as T
|
||||
}
|
||||
val returnType = metadata.returnType.javaType
|
||||
return when (metadata.returnType.javaType) {
|
||||
String::class.java -> getString(metadata.name) as T
|
||||
Int::class.java -> getInt(metadata.name) as T
|
||||
Integer::class.java -> getInt(metadata.name) as T
|
||||
Long::class.java -> getLong(metadata.name) as T
|
||||
Double::class.java -> getDouble(metadata.name) as T
|
||||
Boolean::class.java -> getBoolean(metadata.name) as T
|
||||
LocalDate::class.java -> LocalDate.parse(getString(metadata.name)) as T
|
||||
Instant::class.java -> Instant.parse(getString(metadata.name)) as T
|
||||
HostAndPort::class.java -> HostAndPort.fromString(getString(metadata.name)) as T
|
||||
Path::class.java -> Paths.get(getString(metadata.name)) as T
|
||||
URL::class.java -> URL(getString(metadata.name)) as T
|
||||
Properties::class.java -> getProperties(metadata.name) as T
|
||||
else -> {
|
||||
if (returnType is Class<*> && Enum::class.java.isAssignableFrom(returnType)) {
|
||||
return enumBridge(returnType as Class<DummyEnum>, getString(metadata.name)) as T
|
||||
return getValueInternal(metadata.name, metadata.returnType)
|
||||
}
|
||||
|
||||
fun <T : Any> Config.parseAs(clazz: KClass<T>): T {
|
||||
require(clazz.isData) { "Only Kotlin data classes can be parsed" }
|
||||
val constructor = clazz.primaryConstructor!!
|
||||
val args = constructor.parameters
|
||||
.filterNot { it.isOptional && !hasPath(it.name!!) }
|
||||
.associateBy({ it }) { param ->
|
||||
// Get the matching property for this parameter
|
||||
val property = clazz.memberProperties.first { it.name == param.name }
|
||||
val path = defaultToOldPath(property)
|
||||
getValueInternal<Any>(path, param.type)
|
||||
}
|
||||
throw IllegalArgumentException("Unsupported type ${metadata.returnType}")
|
||||
return constructor.callBy(args)
|
||||
}
|
||||
|
||||
inline fun <reified T : Any> Config.parseAs(): T = parseAs(T::class)
|
||||
|
||||
fun Config.toProperties(): Properties {
|
||||
return entrySet().associateByTo(
|
||||
Properties(),
|
||||
{ ConfigUtil.splitPath(it.key).joinToString(".") },
|
||||
{ it.value.unwrapped().toString() })
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun <T> Config.getValueInternal(path: String, type: KType): T {
|
||||
return (if (type.arguments.isEmpty()) getSingleValue(path, type) else getCollectionValue(path, type)) as T
|
||||
}
|
||||
|
||||
private fun Config.getSingleValue(path: String, type: KType): Any? {
|
||||
if (type.isMarkedNullable && !hasPath(path)) return null
|
||||
val typeClass = type.jvmErasure
|
||||
return when (typeClass) {
|
||||
String::class -> getString(path)
|
||||
Int::class -> getInt(path)
|
||||
Long::class -> getLong(path)
|
||||
Double::class -> getDouble(path)
|
||||
Boolean::class -> getBoolean(path)
|
||||
LocalDate::class -> LocalDate.parse(getString(path))
|
||||
Instant::class -> Instant.parse(getString(path))
|
||||
HostAndPort::class -> HostAndPort.fromString(getString(path))
|
||||
Path::class -> Paths.get(getString(path))
|
||||
URL::class -> URL(getString(path))
|
||||
Properties::class -> getConfig(path).toProperties()
|
||||
else -> if (typeClass.java.isEnum) {
|
||||
parseEnum(typeClass.java, getString(path))
|
||||
} else {
|
||||
getConfig(path).parseAs(typeClass)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class for optional configurations
|
||||
*/
|
||||
class OptionalConfig<out T>(val conf: Config, val lambda: () -> T) {
|
||||
operator fun getValue(receiver: Any, metadata: KProperty<*>): T {
|
||||
return if (conf.hasPath(metadata.name)) conf.getValue(receiver, metadata) else lambda()
|
||||
private fun Config.getCollectionValue(path: String, type: KType): Collection<Any> {
|
||||
val typeClass = type.jvmErasure
|
||||
require(typeClass == List::class || typeClass == Set::class) { "$typeClass is not supported" }
|
||||
val elementClass = type.arguments[0].type?.jvmErasure ?: throw IllegalArgumentException("Cannot work with star projection: $type")
|
||||
if (!hasPath(path)) {
|
||||
return if (typeClass == List::class) emptyList() else emptySet()
|
||||
}
|
||||
val values: List<Any> = when (elementClass) {
|
||||
String::class -> getStringList(path)
|
||||
Int::class -> getIntList(path)
|
||||
Long::class -> getLongList(path)
|
||||
Double::class -> getDoubleList(path)
|
||||
Boolean::class -> getBooleanList(path)
|
||||
LocalDate::class -> getStringList(path).map(LocalDate::parse)
|
||||
Instant::class -> getStringList(path).map(Instant::parse)
|
||||
HostAndPort::class -> getStringList(path).map(HostAndPort::fromString)
|
||||
Path::class -> getStringList(path).map { Paths.get(it) }
|
||||
URL::class -> getStringList(path).map(::URL)
|
||||
Properties::class -> getConfigList(path).map(Config::toProperties)
|
||||
else -> if (elementClass.java.isEnum) {
|
||||
getStringList(path).map { parseEnum(elementClass.java, it) }
|
||||
} else {
|
||||
getConfigList(path).map { it.parseAs(elementClass) }
|
||||
}
|
||||
}
|
||||
return if (typeClass == Set::class) values.toSet() else values
|
||||
}
|
||||
|
||||
fun <T> Config.getOrElse(lambda: () -> T): OptionalConfig<T> = OptionalConfig(this, lambda)
|
||||
|
||||
fun Config.getProperties(path: String): Properties {
|
||||
val obj = this.getObject(path)
|
||||
val props = Properties()
|
||||
for ((property, objectValue) in obj.entries) {
|
||||
props.setProperty(property, objectValue.unwrapped().toString())
|
||||
private fun Config.defaultToOldPath(property: KProperty<*>): String {
|
||||
if (!hasPath(property.name)) {
|
||||
val oldConfig = property.annotations.filterIsInstance<OldConfig>().noneOrSingle()
|
||||
if (oldConfig != null && hasPath(oldConfig.value)) {
|
||||
logger.warn("Config key ${oldConfig.value} has been deprecated and will be removed in a future release. " +
|
||||
"Use ${property.name} instead")
|
||||
return oldConfig.value
|
||||
}
|
||||
}
|
||||
return props
|
||||
return property.name
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
inline fun <reified T : Any> Config.getListOrElse(path: String, default: Config.() -> List<T>): List<T> {
|
||||
return if (hasPath(path)) {
|
||||
(if (T::class == String::class) getStringList(path) else getConfigList(path)) as List<T>
|
||||
} else {
|
||||
this.default()
|
||||
}
|
||||
}
|
||||
private fun parseEnum(enumType: Class<*>, name: String): Enum<*> = enumBridge(enumType as Class<Proxy.Type>, name) // Any enum will do
|
||||
|
||||
private fun <T : Enum<T>> enumBridge(clazz: Class<T>, name: String): T = java.lang.Enum.valueOf(clazz, name)
|
||||
|
||||
private val logger = LoggerFactory.getLogger("net.corda.nodeapi.config")
|
||||
|
@ -0,0 +1,238 @@
|
||||
package net.corda.nodeapi.config
|
||||
|
||||
import com.google.common.net.HostAndPort
|
||||
import com.typesafe.config.Config
|
||||
import com.typesafe.config.ConfigFactory.empty
|
||||
import com.typesafe.config.ConfigRenderOptions.defaults
|
||||
import com.typesafe.config.ConfigValueFactory
|
||||
import net.corda.core.div
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.Test
|
||||
import java.net.URL
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.util.*
|
||||
import kotlin.reflect.full.primaryConstructor
|
||||
|
||||
class ConfigParsingTest {
|
||||
@Test
|
||||
fun `String`() {
|
||||
testPropertyType<StringData, StringListData, String>("hello world!", "bye")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Int`() {
|
||||
testPropertyType<IntData, IntListData, Int>(1, 2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Long`() {
|
||||
testPropertyType<LongData, LongListData, Long>(Long.MAX_VALUE, Long.MIN_VALUE)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Double`() {
|
||||
testPropertyType<DoubleData, DoubleListData, Double>(1.2, 3.4)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Boolean`() {
|
||||
testPropertyType<BooleanData, BooleanListData, Boolean>(true, false)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Enum`() {
|
||||
testPropertyType<EnumData, EnumListData, TestEnum>(TestEnum.Value2, TestEnum.Value1, valuesToString = true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `LocalDate`() {
|
||||
testPropertyType<LocalDateData, LocalDateListData, LocalDate>(LocalDate.now(), LocalDate.now().plusDays(1), valuesToString = true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Instant`() {
|
||||
testPropertyType<InstantData, InstantListData, Instant>(Instant.now(), Instant.now().plusMillis(100), valuesToString = true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `HostAndPort`() {
|
||||
testPropertyType<HostAndPortData, HostAndPortListData, HostAndPort>(
|
||||
HostAndPort.fromParts("localhost", 2223),
|
||||
HostAndPort.fromParts("localhost", 2225),
|
||||
valuesToString = true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Path`() {
|
||||
val path = Paths.get("tmp") / "test"
|
||||
testPropertyType<PathData, PathListData, Path>(path, path / "file", valuesToString = true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `URL`() {
|
||||
testPropertyType<URLData, URLListData, URL>(URL("http://localhost:1234"), URL("http://localhost:1235"), valuesToString = true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `flat Properties`() {
|
||||
val config = config("value" to mapOf("key" to "prop"))
|
||||
assertThat(config.parseAs<PropertiesData>().value).isEqualTo(Properties().apply { this["key"] = "prop" })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Properties key with dot`() {
|
||||
val config = config("value" to mapOf("key.key2" to "prop"))
|
||||
assertThat(config.parseAs<PropertiesData>().value).isEqualTo(Properties().apply { this["key.key2"] = "prop" })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `nested Properties`() {
|
||||
val config = config("value" to mapOf("first" to mapOf("second" to "prop")))
|
||||
assertThat(config.parseAs<PropertiesData>().value).isEqualTo(Properties().apply { this["first.second"] = "prop" })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `List of Properties`() {
|
||||
val config = config("values" to listOf(emptyMap(), mapOf("key" to "prop")))
|
||||
assertThat(config.parseAs<PropertiesListData>().values).containsExactly(
|
||||
Properties(),
|
||||
Properties().apply { this["key"] = "prop" })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Set`() {
|
||||
val config = config("values" to listOf("a", "a", "b"))
|
||||
assertThat(config.parseAs<StringSetData>().values).containsOnly("a", "b")
|
||||
assertThat(empty().parseAs<StringSetData>().values).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `multi property data class`() {
|
||||
val data = config(
|
||||
"b" to true,
|
||||
"i" to 123,
|
||||
"l" to listOf("a", "b"))
|
||||
.parseAs<MultiPropertyData>()
|
||||
assertThat(data.i).isEqualTo(123)
|
||||
assertThat(data.b).isTrue()
|
||||
assertThat(data.l).containsExactly("a", "b")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `nested data classes`() {
|
||||
val config = config(
|
||||
"first" to mapOf(
|
||||
"value" to "nested"))
|
||||
val data = NestedData(StringData("nested"))
|
||||
assertThat(config.parseAs<NestedData>()).isEqualTo(data)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `List of data classes`() {
|
||||
val config = config(
|
||||
"list" to listOf(
|
||||
mapOf("value" to "1"),
|
||||
mapOf("value" to "2")))
|
||||
val data = DataListData(listOf(StringData("1"), StringData("2")))
|
||||
assertThat(config.parseAs<DataListData>()).isEqualTo(data)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `default value property`() {
|
||||
assertThat(config("a" to 3).parseAs<DefaultData>()).isEqualTo(DefaultData(3, 2))
|
||||
assertThat(config("a" to 3, "defaultOfTwo" to 3).parseAs<DefaultData>()).isEqualTo(DefaultData(3, 3))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `nullable property`() {
|
||||
assertThat(empty().parseAs<NullableData>().nullable).isNull()
|
||||
assertThat(config("nullable" to null).parseAs<NullableData>().nullable).isNull()
|
||||
assertThat(config("nullable" to "not null").parseAs<NullableData>().nullable).isEqualTo("not null")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `old config property`() {
|
||||
assertThat(config("oldValue" to "old").parseAs<OldData>().newValue).isEqualTo("old")
|
||||
assertThat(config("newValue" to "new").parseAs<OldData>().newValue).isEqualTo("new")
|
||||
}
|
||||
|
||||
private inline fun <reified S : SingleData<V>, reified L : ListData<V>, V : Any> testPropertyType(
|
||||
value1: V,
|
||||
value2: V,
|
||||
valuesToString: Boolean = false) {
|
||||
testSingleProperty<S, V>(value1, valuesToString)
|
||||
testListProperty<L, V>(value1, value2, valuesToString)
|
||||
}
|
||||
|
||||
private inline fun <reified T : SingleData<V>, V : Any> testSingleProperty(value: V, valueToString: Boolean) {
|
||||
val constructor = T::class.primaryConstructor!!
|
||||
val config = config("value" to if (valueToString) value.toString() else value)
|
||||
val data = constructor.call(value)
|
||||
assertThat(config.parseAs<T>().value).isEqualTo(data.value)
|
||||
}
|
||||
|
||||
private inline fun <reified T : ListData<V>, V : Any> testListProperty(value1: V, value2: V, valuesToString: Boolean) {
|
||||
val rawValues = listOf(value1, value2)
|
||||
val configValues = if (valuesToString) rawValues.map(Any::toString) else rawValues
|
||||
val constructor = T::class.primaryConstructor!!
|
||||
for (n in 0..2) {
|
||||
val config = config("values" to configValues.take(n))
|
||||
val data = constructor.call(rawValues.take(n))
|
||||
assertThat(config.parseAs<T>().values).isEqualTo(data.values)
|
||||
}
|
||||
assertThat(empty().parseAs<T>().values).isEmpty()
|
||||
}
|
||||
|
||||
private fun config(vararg values: Pair<String, *>): Config {
|
||||
val config = ConfigValueFactory.fromMap(mapOf(*values))
|
||||
println(config.render(defaults().setOriginComments(false)))
|
||||
return config.toConfig()
|
||||
}
|
||||
|
||||
private interface SingleData<out T> {
|
||||
val value: T
|
||||
}
|
||||
|
||||
private interface ListData<out T> {
|
||||
val values: List<T>
|
||||
}
|
||||
|
||||
data class StringData(override val value: String) : SingleData<String>
|
||||
data class StringListData(override val values: List<String>) : ListData<String>
|
||||
data class StringSetData(val values: Set<String>)
|
||||
data class IntData(override val value: Int) : SingleData<Int>
|
||||
data class IntListData(override val values: List<Int>) : ListData<Int>
|
||||
data class LongData(override val value: Long) : SingleData<Long>
|
||||
data class LongListData(override val values: List<Long>) : ListData<Long>
|
||||
data class DoubleData(override val value: Double) : SingleData<Double>
|
||||
data class DoubleListData(override val values: List<Double>) : ListData<Double>
|
||||
data class BooleanData(override val value: Boolean) : SingleData<Boolean>
|
||||
data class BooleanListData(override val values: List<Boolean>) : ListData<Boolean>
|
||||
data class EnumData(override val value: TestEnum) : SingleData<TestEnum>
|
||||
data class EnumListData(override val values: List<TestEnum>) : ListData<TestEnum>
|
||||
data class LocalDateData(override val value: LocalDate) : SingleData<LocalDate>
|
||||
data class LocalDateListData(override val values: List<LocalDate>) : ListData<LocalDate>
|
||||
data class InstantData(override val value: Instant) : SingleData<Instant>
|
||||
data class InstantListData(override val values: List<Instant>) : ListData<Instant>
|
||||
data class HostAndPortData(override val value: HostAndPort) : SingleData<HostAndPort>
|
||||
data class HostAndPortListData(override val values: List<HostAndPort>) : ListData<HostAndPort>
|
||||
data class PathData(override val value: Path) : SingleData<Path>
|
||||
data class PathListData(override val values: List<Path>) : ListData<Path>
|
||||
data class URLData(override val value: URL) : SingleData<URL>
|
||||
data class URLListData(override val values: List<URL>) : ListData<URL>
|
||||
data class PropertiesData(override val value: Properties) : SingleData<Properties>
|
||||
data class PropertiesListData(override val values: List<Properties>) : ListData<Properties>
|
||||
data class MultiPropertyData(val i: Int, val b: Boolean, val l: List<String>)
|
||||
data class NestedData(val first: StringData)
|
||||
data class DataListData(val list: List<StringData>)
|
||||
data class DefaultData(val a: Int, val defaultOfTwo: Int = 2)
|
||||
data class NullableData(val nullable: String?)
|
||||
data class OldData(
|
||||
@OldConfig("oldValue")
|
||||
val newValue: String)
|
||||
enum class TestEnum { Value1, Value2 }
|
||||
|
||||
}
|
@ -1,10 +1,11 @@
|
||||
package net.corda.node
|
||||
|
||||
import com.typesafe.config.Config
|
||||
import joptsimple.OptionParser
|
||||
import joptsimple.util.EnumConverter
|
||||
import net.corda.core.div
|
||||
import net.corda.node.services.config.ConfigHelper
|
||||
import net.corda.node.services.config.FullNodeConfiguration
|
||||
import net.corda.nodeapi.config.parseAs
|
||||
import org.slf4j.event.Level
|
||||
import java.io.PrintStream
|
||||
import java.nio.file.Path
|
||||
@ -64,7 +65,9 @@ data class CmdLineOptions(val baseDirectory: Path,
|
||||
val isVersion: Boolean,
|
||||
val noLocalShell: Boolean,
|
||||
val sshdServer: Boolean) {
|
||||
fun loadConfig(allowMissingConfig: Boolean = false, configOverrides: Map<String, Any?> = emptyMap()): Config {
|
||||
return ConfigHelper.loadConfig(baseDirectory, configFile, allowMissingConfig, configOverrides)
|
||||
fun loadConfig(allowMissingConfig: Boolean = false, configOverrides: Map<String, Any?> = emptyMap()): FullNodeConfiguration {
|
||||
return ConfigHelper
|
||||
.loadConfig(baseDirectory, configFile, allowMissingConfig, configOverrides)
|
||||
.parseAs<FullNodeConfiguration>()
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,6 @@
|
||||
package net.corda.node
|
||||
|
||||
import com.jcabi.manifests.Manifests
|
||||
import com.typesafe.config.Config
|
||||
import com.typesafe.config.ConfigException
|
||||
import joptsimple.OptionException
|
||||
import net.corda.core.*
|
||||
@ -86,9 +85,7 @@ fun main(args: Array<String>) {
|
||||
printBasicNodeInfo("Logs can be found in", System.getProperty("log-path"))
|
||||
|
||||
val conf = try {
|
||||
val conf = cmdlineOptions.loadConfig()
|
||||
checkConfigVersion(conf)
|
||||
FullNodeConfiguration(cmdlineOptions.baseDirectory, conf)
|
||||
cmdlineOptions.loadConfig()
|
||||
} catch (e: ConfigException) {
|
||||
println("Unable to load the configuration file: ${e.rootCause.message}")
|
||||
exitProcess(2)
|
||||
@ -157,16 +154,6 @@ fun main(args: Array<String>) {
|
||||
exitProcess(0)
|
||||
}
|
||||
|
||||
private fun checkConfigVersion(conf: Config) {
|
||||
// TODO: Remove this check in future milestone.
|
||||
if (conf.hasPath("artemisAddress")) {
|
||||
// artemisAddress has been renamed to p2pAddress in M10.
|
||||
println("artemisAddress has been renamed to p2pAddress in M10, please upgrade your configuration file and start Corda node again.")
|
||||
println("Corda will now exit...")
|
||||
exitProcess(1)
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkJavaVersion() {
|
||||
// Check we're not running a version of Java with a known bug: https://github.com/corda/corda/issues/83
|
||||
try {
|
||||
|
@ -27,6 +27,7 @@ import net.corda.node.utilities.ServiceIdentityGenerator
|
||||
import net.corda.nodeapi.ArtemisMessagingComponent
|
||||
import net.corda.nodeapi.User
|
||||
import net.corda.nodeapi.config.SSLConfiguration
|
||||
import net.corda.nodeapi.config.parseAs
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.slf4j.Logger
|
||||
@ -115,6 +116,7 @@ data class NodeHandle(
|
||||
val nodeInfo: NodeInfo,
|
||||
val rpc: CordaRPCOps,
|
||||
val configuration: FullNodeConfiguration,
|
||||
val webAddress: HostAndPort,
|
||||
val process: Process
|
||||
) {
|
||||
fun rpcClientToNode(): CordaRPCClient = CordaRPCClient(configuration.rpcAddress!!)
|
||||
@ -421,7 +423,7 @@ class DriverDSL(
|
||||
"useTestClock" to useTestClock,
|
||||
"rpcUsers" to rpcUsers.map {
|
||||
mapOf(
|
||||
"user" to it.username,
|
||||
"username" to it.username,
|
||||
"password" to it.password,
|
||||
"permissions" to it.permissions
|
||||
)
|
||||
@ -429,22 +431,19 @@ class DriverDSL(
|
||||
"verifierType" to verifierType.name
|
||||
) + customOverrides
|
||||
|
||||
val configuration = FullNodeConfiguration(
|
||||
baseDirectory,
|
||||
ConfigHelper.loadConfig(
|
||||
val config = ConfigHelper.loadConfig(
|
||||
baseDirectory = baseDirectory,
|
||||
allowMissingConfig = true,
|
||||
configOverrides = configOverrides
|
||||
)
|
||||
)
|
||||
configOverrides = configOverrides)
|
||||
val configuration = config.parseAs<FullNodeConfiguration>()
|
||||
|
||||
val processFuture = startNode(executorService, configuration, quasarJarPath, debugPort, systemProperties)
|
||||
val processFuture = startNode(executorService, configuration, config, quasarJarPath, debugPort, systemProperties)
|
||||
registerProcess(processFuture)
|
||||
return processFuture.flatMap { process ->
|
||||
// We continue to use SSL enabled port for RPC when its for node user.
|
||||
establishRpc(p2pAddress, configuration).flatMap { rpc ->
|
||||
rpc.waitUntilRegisteredWithNetworkMap().map {
|
||||
NodeHandle(rpc.nodeIdentity(), rpc, configuration, process)
|
||||
NodeHandle(rpc.nodeIdentity(), rpc, configuration, webAddress, process)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -482,34 +481,29 @@ class DriverDSL(
|
||||
}
|
||||
}
|
||||
|
||||
private fun queryWebserver(configuration: FullNodeConfiguration, process: Process): HostAndPort? {
|
||||
val protocol = if (configuration.useHTTPS) {
|
||||
"https://"
|
||||
} else {
|
||||
"http://"
|
||||
}
|
||||
val url = URL(protocol + configuration.webAddress.toString() + "/api/status")
|
||||
val client = OkHttpClient.Builder().connectTimeout(5, TimeUnit.SECONDS).readTimeout(60, TimeUnit.SECONDS).build()
|
||||
private fun queryWebserver(handle: NodeHandle, process: Process): HostAndPort {
|
||||
val protocol = if (handle.configuration.useHTTPS) "https://" else "http://"
|
||||
val url = URL("$protocol${handle.webAddress}/api/status")
|
||||
val client = OkHttpClient.Builder().connectTimeout(5, SECONDS).readTimeout(60, SECONDS).build()
|
||||
|
||||
while (process.isAlive) try {
|
||||
val response = client.newCall(Request.Builder().url(url).build()).execute()
|
||||
if (response.isSuccessful && (response.body().string() == "started")) {
|
||||
return configuration.webAddress
|
||||
return handle.webAddress
|
||||
}
|
||||
} catch(e: ConnectException) {
|
||||
log.debug("Retrying webserver info at ${configuration.webAddress}")
|
||||
log.debug("Retrying webserver info at ${handle.webAddress}")
|
||||
}
|
||||
|
||||
log.error("Webserver at ${configuration.webAddress} has died")
|
||||
return null
|
||||
throw IllegalStateException("Webserver at ${handle.webAddress} has died")
|
||||
}
|
||||
|
||||
override fun startWebserver(handle: NodeHandle): ListenableFuture<HostAndPort> {
|
||||
val debugPort = if (isDebug) debugPortAllocation.nextPort() else null
|
||||
val process = DriverDSL.startWebserver(executorService, handle.configuration, debugPort)
|
||||
val process = DriverDSL.startWebserver(executorService, handle, debugPort)
|
||||
registerProcess(process)
|
||||
return process.map {
|
||||
queryWebserver(handle.configuration, it)!!
|
||||
queryWebserver(handle, it)
|
||||
}
|
||||
}
|
||||
|
||||
@ -537,7 +531,7 @@ class DriverDSL(
|
||||
)
|
||||
|
||||
log.info("Starting network-map-service")
|
||||
val startNode = startNode(executorService, FullNodeConfiguration(baseDirectory, config), quasarJarPath, debugPort, systemProperties)
|
||||
val startNode = startNode(executorService, config.parseAs<FullNodeConfiguration>(), config, quasarJarPath, debugPort, systemProperties)
|
||||
registerProcess(startNode)
|
||||
}
|
||||
|
||||
@ -553,13 +547,14 @@ class DriverDSL(
|
||||
private fun startNode(
|
||||
executorService: ListeningScheduledExecutorService,
|
||||
nodeConf: FullNodeConfiguration,
|
||||
config: Config,
|
||||
quasarJarPath: String,
|
||||
debugPort: Int?,
|
||||
overriddenSystemProperties: Map<String, String>
|
||||
): ListenableFuture<Process> {
|
||||
return executorService.submit<Process> {
|
||||
// Write node.conf
|
||||
writeConfig(nodeConf.baseDirectory, "node.conf", nodeConf.config)
|
||||
writeConfig(nodeConf.baseDirectory, "node.conf", config)
|
||||
|
||||
val systemProperties = mapOf(
|
||||
"name" to nodeConf.myLegalName,
|
||||
@ -586,19 +581,19 @@ class DriverDSL(
|
||||
|
||||
private fun startWebserver(
|
||||
executorService: ListeningScheduledExecutorService,
|
||||
nodeConf: FullNodeConfiguration,
|
||||
handle: NodeHandle,
|
||||
debugPort: Int?
|
||||
): ListenableFuture<Process> {
|
||||
return executorService.submit<Process> {
|
||||
val className = "net.corda.webserver.WebServer"
|
||||
ProcessUtilities.startJavaProcess(
|
||||
className = className, // cannot directly get class for this, so just use string
|
||||
arguments = listOf("--base-directory", nodeConf.baseDirectory.toString()),
|
||||
arguments = listOf("--base-directory", handle.configuration.baseDirectory.toString()),
|
||||
jdwpPort = debugPort,
|
||||
extraJvmArguments = listOf("-Dname=node-${nodeConf.p2pAddress}-webserver"),
|
||||
extraJvmArguments = listOf("-Dname=node-${handle.configuration.p2pAddress}-webserver"),
|
||||
errorLogPath = Paths.get("error.$className.log")
|
||||
)
|
||||
}.flatMap { process -> addressMustBeBound(executorService, nodeConf.webAddress).map { process } }
|
||||
}.flatMap { process -> addressMustBeBound(executorService, handle.webAddress).map { process } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -26,6 +26,7 @@ import net.corda.node.services.transactions.PersistentUniquenessProvider
|
||||
import net.corda.node.services.transactions.RaftUniquenessProvider
|
||||
import net.corda.node.services.transactions.RaftValidatingNotaryService
|
||||
import net.corda.node.services.transactions.RaftNonValidatingNotaryService
|
||||
import net.corda.node.services.transactions.*
|
||||
import net.corda.node.utilities.AddressUtils
|
||||
import net.corda.node.utilities.AffinityExecutor
|
||||
import net.corda.nodeapi.ArtemisMessagingComponent.NetworkMapAddress
|
||||
@ -128,7 +129,7 @@ class Node(override val configuration: FullNodeConfiguration,
|
||||
}
|
||||
|
||||
override fun makeMessagingService(): MessagingServiceInternal {
|
||||
userService = RPCUserServiceImpl(configuration)
|
||||
userService = RPCUserServiceImpl(configuration.rpcUsers)
|
||||
val serverAddress = configuration.messagingServerAddress ?: makeLocalMessageBroker()
|
||||
val myIdentityOrNullIfNetworkMapService = if (networkMapAddress != null) obtainLegalIdentity().owningKey else null
|
||||
return NodeMessagingClient(
|
||||
|
@ -1,7 +1,6 @@
|
||||
package net.corda.node.services
|
||||
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
import net.corda.nodeapi.User
|
||||
|
||||
/**
|
||||
@ -16,13 +15,8 @@ interface RPCUserService {
|
||||
|
||||
// TODO Store passwords as salted hashes
|
||||
// TODO Or ditch this and consider something like Apache Shiro
|
||||
class RPCUserServiceImpl(config: NodeConfiguration) : RPCUserService {
|
||||
|
||||
private val _users = config.rpcUsers.associateBy(User::username)
|
||||
|
||||
override fun getUser(username: String): User? = _users[username]
|
||||
|
||||
override val users: List<User> get() = _users.values.toList()
|
||||
class RPCUserServiceImpl(override val users: List<User>) : RPCUserService {
|
||||
override fun getUser(username: String): User? = users.find { it.username == username }
|
||||
}
|
||||
|
||||
fun startFlowPermission(className: String) = "StartFlow.$className"
|
||||
|
@ -1,7 +1,6 @@
|
||||
package net.corda.node.services.config
|
||||
|
||||
import com.google.common.net.HostAndPort
|
||||
import com.typesafe.config.Config
|
||||
import net.corda.core.div
|
||||
import net.corda.core.node.NodeVersionInfo
|
||||
import net.corda.core.node.services.ServiceInfo
|
||||
@ -12,19 +11,13 @@ import net.corda.node.services.messaging.CertificateChainCheckPolicy
|
||||
import net.corda.node.services.network.NetworkMapService
|
||||
import net.corda.node.utilities.TestClock
|
||||
import net.corda.nodeapi.User
|
||||
import net.corda.nodeapi.config.getListOrElse
|
||||
import net.corda.nodeapi.config.getOrElse
|
||||
import net.corda.nodeapi.config.getValue
|
||||
import net.corda.nodeapi.config.OldConfig
|
||||
import net.corda.nodeapi.config.SSLConfiguration
|
||||
import java.net.URL
|
||||
import java.nio.file.Path
|
||||
import java.util.*
|
||||
|
||||
enum class VerifierType {
|
||||
InMemory,
|
||||
OutOfProcess
|
||||
}
|
||||
|
||||
interface NodeConfiguration : net.corda.nodeapi.config.SSLConfiguration {
|
||||
interface NodeConfiguration : SSLConfiguration {
|
||||
val baseDirectory: Path
|
||||
override val certificatesDirectory: Path get() = baseDirectory / "certificates"
|
||||
val myLegalName: String
|
||||
@ -32,83 +25,89 @@ interface NodeConfiguration : net.corda.nodeapi.config.SSLConfiguration {
|
||||
val nearestCity: String
|
||||
val emailAddress: String
|
||||
val exportJMXto: String
|
||||
val dataSourceProperties: Properties get() = Properties()
|
||||
val rpcUsers: List<User> get() = emptyList()
|
||||
val dataSourceProperties: Properties
|
||||
val rpcUsers: List<User>
|
||||
val devMode: Boolean
|
||||
val certificateSigningService: URL
|
||||
val certificateChainCheckPolicies: Map<String, CertificateChainCheckPolicy>
|
||||
val certificateChainCheckPolicies: List<CertChainPolicyConfig>
|
||||
val verifierType: VerifierType
|
||||
}
|
||||
|
||||
/**
|
||||
* [baseDirectory] is not retrieved from the config file but rather from a command line argument.
|
||||
*/
|
||||
class FullNodeConfiguration(override val baseDirectory: Path, val config: Config) : NodeConfiguration {
|
||||
override val myLegalName: String by config
|
||||
override val nearestCity: String by config
|
||||
override val emailAddress: String by config
|
||||
data class FullNodeConfiguration(
|
||||
// TODO Remove this subsitution value and use baseDirectory as the subsitution instead
|
||||
@Deprecated(
|
||||
"This is a subsitution value which points to the baseDirectory and is manually added into the config before parsing",
|
||||
ReplaceWith("baseDirectory"))
|
||||
val basedir: Path,
|
||||
override val myLegalName: String,
|
||||
override val nearestCity: String,
|
||||
override val emailAddress: String,
|
||||
override val keyStorePassword: String,
|
||||
override val trustStorePassword: String,
|
||||
override val dataSourceProperties: Properties,
|
||||
override val certificateSigningService: URL,
|
||||
override val networkMapService: NetworkMapInfo?,
|
||||
override val rpcUsers: List<User>,
|
||||
override val verifierType: VerifierType,
|
||||
val useHTTPS: Boolean,
|
||||
@OldConfig("artemisAddress")
|
||||
val p2pAddress: HostAndPort,
|
||||
val rpcAddress: HostAndPort?,
|
||||
// TODO This field is slightly redundant as p2pAddress is sufficient to hold the address of the node's MQ broker.
|
||||
// Instead this should be a Boolean indicating whether that broker is an internal one started by the node or an external one
|
||||
val messagingServerAddress: HostAndPort?,
|
||||
val extraAdvertisedServiceIds: List<String>,
|
||||
val notaryNodeId: Int?,
|
||||
val notaryNodeAddress: HostAndPort?,
|
||||
val notaryClusterAddresses: List<HostAndPort>,
|
||||
override val certificateChainCheckPolicies: List<CertChainPolicyConfig>,
|
||||
override val devMode: Boolean = false,
|
||||
val useTestClock: Boolean = false
|
||||
) : NodeConfiguration {
|
||||
/** This is not retrieved from the config file but rather from a command line argument. */
|
||||
@Suppress("DEPRECATION")
|
||||
override val baseDirectory: Path get() = basedir
|
||||
override val exportJMXto: String get() = "http"
|
||||
override val keyStorePassword: String by config
|
||||
override val trustStorePassword: String by config
|
||||
override val dataSourceProperties: Properties by config
|
||||
override val devMode: Boolean by config.getOrElse { false }
|
||||
override val certificateSigningService: URL by config
|
||||
override val networkMapService: NetworkMapInfo? = config.getOptionalConfig("networkMapService")?.run {
|
||||
NetworkMapInfo(
|
||||
HostAndPort.fromString(getString("address")),
|
||||
getString("legalName"))
|
||||
}
|
||||
override val rpcUsers: List<User> = config
|
||||
.getListOrElse<Config>("rpcUsers") { emptyList() }
|
||||
.map {
|
||||
val username = it.getString("user")
|
||||
require(username.matches("\\w+".toRegex())) { "Username $username contains invalid characters" }
|
||||
val password = it.getString("password")
|
||||
val permissions = it.getListOrElse<String>("permissions") { emptyList() }.toSet()
|
||||
User(username, password, permissions)
|
||||
}
|
||||
override val certificateChainCheckPolicies = config.getOptionalConfig("certificateChainCheckPolicies")?.run {
|
||||
entrySet().associateByTo(HashMap(), { it.key }, { parseCertificateChainCheckPolicy(getConfig(it.key)) })
|
||||
} ?: emptyMap<String, CertificateChainCheckPolicy>()
|
||||
override val verifierType: VerifierType by config
|
||||
val useHTTPS: Boolean by config
|
||||
val p2pAddress: HostAndPort by config
|
||||
val rpcAddress: HostAndPort? by config
|
||||
val webAddress: HostAndPort by config
|
||||
// TODO This field is slightly redundant as p2pAddress is sufficient to hold the address of the node's MQ broker.
|
||||
// Instead this should be a Boolean indicating whether that broker is an internal one started by the node or an external one
|
||||
val messagingServerAddress: HostAndPort? by config
|
||||
val extraAdvertisedServiceIds: List<String> = config.getListOrElse<String>("extraAdvertisedServiceIds") { emptyList() }
|
||||
val useTestClock: Boolean by config.getOrElse { false }
|
||||
val notaryNodeId: Int? by config
|
||||
val notaryNodeAddress: HostAndPort? by config
|
||||
val notaryClusterAddresses: List<HostAndPort> = config
|
||||
.getListOrElse<String>("notaryClusterAddresses") { emptyList() }
|
||||
.map { HostAndPort.fromString(it) }
|
||||
|
||||
fun createNode(nodeVersionInfo: NodeVersionInfo): Node {
|
||||
init {
|
||||
// This is a sanity feature do not remove.
|
||||
require(!useTestClock || devMode) { "Cannot use test clock outside of dev mode" }
|
||||
// TODO Move this to ArtemisMessagingServer
|
||||
rpcUsers.forEach {
|
||||
require(it.username.matches("\\w+".toRegex())) { "Username ${it.username} contains invalid characters" }
|
||||
}
|
||||
}
|
||||
|
||||
fun createNode(nodeVersionInfo: NodeVersionInfo): Node {
|
||||
val advertisedServices = extraAdvertisedServiceIds
|
||||
.filter(String::isNotBlank)
|
||||
.map { ServiceInfo.parse(it) }
|
||||
.toMutableSet()
|
||||
if (networkMapService == null) advertisedServices.add(ServiceInfo(NetworkMapService.type))
|
||||
if (networkMapService == null) advertisedServices += ServiceInfo(NetworkMapService.type)
|
||||
|
||||
return Node(this, advertisedServices, nodeVersionInfo, if (useTestClock) TestClock() else NodeClock())
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseCertificateChainCheckPolicy(config: Config): CertificateChainCheckPolicy {
|
||||
val policy = config.getString("policy")
|
||||
return when (policy) {
|
||||
"Any" -> CertificateChainCheckPolicy.Any
|
||||
"RootMustMatch" -> CertificateChainCheckPolicy.RootMustMatch
|
||||
"LeafMustMatch" -> CertificateChainCheckPolicy.LeafMustMatch
|
||||
"MustContainOneOf" -> CertificateChainCheckPolicy.MustContainOneOf(config.getStringList("trustedAliases").toSet())
|
||||
else -> throw IllegalArgumentException("Invalid certificate chain check policy $policy")
|
||||
}
|
||||
enum class VerifierType {
|
||||
InMemory,
|
||||
OutOfProcess
|
||||
}
|
||||
|
||||
private fun Config.getOptionalConfig(path: String): Config? = if (hasPath(path)) getConfig(path) else null
|
||||
enum class CertChainPolicyType {
|
||||
Any,
|
||||
RootMustMatch,
|
||||
LeafMustMatch,
|
||||
MustContainOneOf
|
||||
}
|
||||
|
||||
data class CertChainPolicyConfig(val role: String, val policy: CertChainPolicyType, val trustedAliases: Set<String>) {
|
||||
val certificateChainCheckPolicy: CertificateChainCheckPolicy get() {
|
||||
return when (policy) {
|
||||
CertChainPolicyType.Any -> CertificateChainCheckPolicy.Any
|
||||
CertChainPolicyType.RootMustMatch -> CertificateChainCheckPolicy.RootMustMatch
|
||||
CertChainPolicyType.LeafMustMatch -> CertificateChainCheckPolicy.LeafMustMatch
|
||||
CertChainPolicyType.MustContainOneOf -> CertificateChainCheckPolicy.MustContainOneOf(trustedAliases)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,16 +4,13 @@ import com.google.common.net.HostAndPort
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.google.common.util.concurrent.SettableFuture
|
||||
import io.netty.handler.ssl.SslHandler
|
||||
import net.corda.core.ThreadBox
|
||||
import net.corda.core.*
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.crypto.X509Utilities.CORDA_CLIENT_CA
|
||||
import net.corda.core.crypto.X509Utilities.CORDA_ROOT_CA
|
||||
import net.corda.core.div
|
||||
import net.corda.core.minutes
|
||||
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.seconds
|
||||
import net.corda.core.utilities.debug
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.node.printBasicNodeInfo
|
||||
@ -251,8 +248,9 @@ class ArtemisMessagingServer(override val config: NodeConfiguration,
|
||||
)
|
||||
val keyStore = X509Utilities.loadKeyStore(config.keyStoreFile, config.keyStorePassword)
|
||||
val trustStore = X509Utilities.loadKeyStore(config.trustStoreFile, config.trustStorePassword)
|
||||
val certChecks = defaultCertPolicies.mapValues {
|
||||
(config.certificateChainCheckPolicies[it.key] ?: it.value).createCheck(keyStore, trustStore)
|
||||
val certChecks = defaultCertPolicies.mapValues { (role, defaultPolicy) ->
|
||||
val configPolicy = config.certificateChainCheckPolicies.noneOrSingle { it.role == role }?.certificateChainCheckPolicy
|
||||
(configPolicy ?: defaultPolicy).createCheck(keyStore, trustStore)
|
||||
}
|
||||
val securityConfig = object : SecurityConfiguration() {
|
||||
// Override to make it work with our login module
|
||||
|
@ -15,9 +15,9 @@ import net.corda.core.messaging.RPCReturnsObservables
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.utilities.debug
|
||||
import net.corda.node.services.RPCUserService
|
||||
import net.corda.node.utilities.AffinityExecutor
|
||||
import net.corda.core.utilities.debug
|
||||
import net.corda.nodeapi.*
|
||||
import net.corda.nodeapi.ArtemisMessagingComponent.Companion.NODE_USER
|
||||
import org.apache.activemq.artemis.api.core.Message
|
||||
|
@ -1,76 +0,0 @@
|
||||
package net.corda.node.services
|
||||
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import net.corda.node.services.config.FullNodeConfiguration
|
||||
import net.corda.nodeapi.User
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||
import org.junit.Test
|
||||
import java.nio.file.Paths
|
||||
|
||||
class RPCUserServiceImplTest {
|
||||
|
||||
@Test
|
||||
fun `missing config`() {
|
||||
val service = loadWithContents("{}")
|
||||
assertThat(service.getUser("user")).isNull()
|
||||
assertThat(service.users).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `no users`() {
|
||||
val service = loadWithContents("rpcUsers : []")
|
||||
assertThat(service.getUser("user")).isNull()
|
||||
assertThat(service.users).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `no permissions`() {
|
||||
val service = loadWithContents("rpcUsers : [{ user=user1, password=letmein }]")
|
||||
val expectedUser = User("user1", "letmein", permissions = emptySet())
|
||||
assertThat(service.getUser("user1")).isEqualTo(expectedUser)
|
||||
assertThat(service.users).containsOnly(expectedUser)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `single permission, which is in lower case`() {
|
||||
val service = loadWithContents("rpcUsers : [{ user=user1, password=letmein, permissions=[cash] }]")
|
||||
assertThat(service.getUser("user1")?.permissions).containsOnly("cash")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `two permissions, which are upper case`() {
|
||||
val service = loadWithContents("rpcUsers : [{ user=user1, password=letmein, permissions=[CASH, ADMIN] }]")
|
||||
assertThat(service.getUser("user1")?.permissions).containsOnly("CASH", "ADMIN")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `two users`() {
|
||||
val service = loadWithContents("""rpcUsers : [
|
||||
{ user=user, password=password, permissions=[ADMIN] }
|
||||
{ user=user2, password=password2, permissions=[] }
|
||||
]""")
|
||||
val user1 = User("user", "password", permissions = setOf("ADMIN"))
|
||||
val user2 = User("user2", "password2", permissions = emptySet())
|
||||
assertThat(service.getUser("user")).isEqualTo(user1)
|
||||
assertThat(service.getUser("user2")).isEqualTo(user2)
|
||||
assertThat(service.users).containsOnly(user1, user2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unknown user`() {
|
||||
val service = loadWithContents("rpcUsers : [{ user=user1, password=letmein }]")
|
||||
assertThat(service.getUser("test")).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Artemis special characters not permitted in usernames`() {
|
||||
assertThatThrownBy { loadWithContents("rpcUsers : [{ user=user.1, password=letmein }]") }.hasMessageContaining(".")
|
||||
assertThatThrownBy { loadWithContents("rpcUsers : [{ user=user*1, password=letmein }]") }.hasMessageContaining("*")
|
||||
assertThatThrownBy { loadWithContents("""rpcUsers : [{ user="user#1", password=letmein }]""") }.hasMessageContaining("#")
|
||||
}
|
||||
|
||||
private fun loadWithContents(configString: String): RPCUserServiceImpl {
|
||||
return RPCUserServiceImpl(FullNodeConfiguration(Paths.get("."), ConfigFactory.parseString(configString)))
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
package net.corda.node.services.config
|
||||
|
||||
import net.corda.nodeapi.User
|
||||
import net.corda.testing.testConfiguration
|
||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||
import org.junit.Test
|
||||
import java.nio.file.Paths
|
||||
|
||||
class FullNodeConfigurationTest {
|
||||
@Test
|
||||
fun `Artemis special characters not permitted in RPC usernames`() {
|
||||
fun configWithRPCUsername(username: String): FullNodeConfiguration {
|
||||
return testConfiguration(Paths.get("."), "NodeA", 0).copy(
|
||||
rpcUsers = listOf(User(username, "pass", emptySet())))
|
||||
}
|
||||
|
||||
assertThatThrownBy { configWithRPCUsername("user.1") }.hasMessageContaining(".")
|
||||
assertThatThrownBy { configWithRPCUsername("user*1") }.hasMessageContaining("*")
|
||||
assertThatThrownBy { configWithRPCUsername("user#1") }.hasMessageContaining("#")
|
||||
}
|
||||
}
|
@ -5,7 +5,6 @@ import com.google.common.net.HostAndPort
|
||||
import com.google.common.util.concurrent.Futures
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.google.common.util.concurrent.SettableFuture
|
||||
import com.typesafe.config.ConfigFactory.empty
|
||||
import net.corda.core.crypto.composite
|
||||
import net.corda.core.crypto.generateKeyPair
|
||||
import net.corda.core.messaging.Message
|
||||
@ -72,7 +71,7 @@ class ArtemisMessagingTests {
|
||||
@Before
|
||||
fun setUp() {
|
||||
val baseDirectory = temporaryFolder.root.toPath()
|
||||
userService = RPCUserServiceImpl(FullNodeConfiguration(baseDirectory, empty()))
|
||||
userService = RPCUserServiceImpl(emptyList())
|
||||
config = TestNodeConfiguration(
|
||||
baseDirectory = baseDirectory,
|
||||
myLegalName = "me",
|
||||
|
@ -52,7 +52,7 @@ dependencies {
|
||||
}
|
||||
|
||||
task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
|
||||
ext.rpcUsers = [ ['user' : "demo", 'password' : "demo", 'permissions' : ["StartFlow.net.corda.flows.FinalityFlow"]] ]
|
||||
ext.rpcUsers = [ ['username' : "demo", 'password' : "demo", 'permissions' : ["StartFlow.net.corda.flows.FinalityFlow"]] ]
|
||||
|
||||
directory "./build/nodes"
|
||||
networkMap "Controller"
|
||||
|
@ -70,7 +70,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
|
||||
webPort 10007
|
||||
cordapps = []
|
||||
rpcUsers = [
|
||||
['user' : "bankUser",
|
||||
['username' : "bankUser",
|
||||
'password' : "test",
|
||||
'permissions' : ["StartFlow.net.corda.flows.CashPaymentFlow",
|
||||
"StartFlow.net.corda.flows.IssuerFlow\$IssuanceRequester"]]
|
||||
@ -85,7 +85,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
|
||||
webPort 10010
|
||||
cordapps = []
|
||||
rpcUsers = [
|
||||
['user' : "bigCorpUser",
|
||||
['username' : "bigCorpUser",
|
||||
'password' : "test",
|
||||
'permissions' : ["StartFlow.net.corda.flows.CashPaymentFlow"]]
|
||||
]
|
||||
|
@ -83,7 +83,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar', 'generateN
|
||||
p2pPort 10002
|
||||
rpcPort 10003
|
||||
cordapps = []
|
||||
rpcUsers = [['user': "demo", 'password': "demo", 'permissions': [
|
||||
rpcUsers = [['username': "demo", 'password': "demo", 'permissions': [
|
||||
'StartFlow.net.corda.notarydemo.flows.DummyIssueAndMove',
|
||||
'StartFlow.net.corda.flows.NotaryFlow$Client'
|
||||
]]]
|
||||
|
@ -59,7 +59,7 @@ dependencies {
|
||||
}
|
||||
|
||||
task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
|
||||
ext.rpcUsers = [['user': "demo", 'password': "demo", 'permissions': [
|
||||
ext.rpcUsers = [['username': "demo", 'password': "demo", 'permissions': [
|
||||
'StartFlow.net.corda.flows.IssuerFlow$IssuanceRequester',
|
||||
"StartFlow.net.corda.traderdemo.flow.SellerFlow"
|
||||
]]]
|
||||
|
@ -5,8 +5,6 @@ package net.corda.testing
|
||||
|
||||
import com.google.common.net.HostAndPort
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.typesafe.config.Config
|
||||
import net.corda.nodeapi.config.SSLConfiguration
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.flows.FlowLogic
|
||||
@ -20,12 +18,11 @@ import net.corda.core.utilities.DUMMY_NOTARY
|
||||
import net.corda.core.utilities.DUMMY_NOTARY_KEY
|
||||
import net.corda.node.internal.AbstractNode
|
||||
import net.corda.node.internal.NetworkMapInfo
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
import net.corda.node.services.config.configureDevKeyAndTrustStores
|
||||
import net.corda.node.services.config.VerifierType
|
||||
import net.corda.node.services.messaging.CertificateChainCheckPolicy
|
||||
import net.corda.node.services.config.*
|
||||
import net.corda.node.services.statemachine.FlowStateMachineImpl
|
||||
import net.corda.node.utilities.AddOrRemove.ADD
|
||||
import net.corda.nodeapi.User
|
||||
import net.corda.nodeapi.config.SSLConfiguration
|
||||
import net.corda.testing.node.MockIdentityService
|
||||
import net.corda.testing.node.MockServices
|
||||
import net.corda.testing.node.makeTestDataSourceProperties
|
||||
@ -162,22 +159,47 @@ inline fun <reified P : FlowLogic<*>> AbstractNode.initiateSingleShotFlow(
|
||||
return future
|
||||
}
|
||||
|
||||
// TODO Replace this with testConfiguration
|
||||
data class TestNodeConfiguration(
|
||||
override val baseDirectory: Path,
|
||||
override val myLegalName: String,
|
||||
override val networkMapService: NetworkMapInfo?,
|
||||
override val keyStorePassword: String = "cordacadevpass",
|
||||
override val trustStorePassword: String = "trustpass",
|
||||
override val rpcUsers: List<User> = emptyList(),
|
||||
override val dataSourceProperties: Properties = makeTestDataSourceProperties(myLegalName),
|
||||
override val nearestCity: String = "Null Island",
|
||||
override val emailAddress: String = "",
|
||||
override val exportJMXto: String = "",
|
||||
override val devMode: Boolean = true,
|
||||
override val certificateSigningService: URL = URL("http://localhost"),
|
||||
override val certificateChainCheckPolicies: Map<String, CertificateChainCheckPolicy> = emptyMap(),
|
||||
override val certificateChainCheckPolicies: List<CertChainPolicyConfig> = emptyList(),
|
||||
override val verifierType: VerifierType = VerifierType.InMemory) : NodeConfiguration
|
||||
|
||||
fun Config.getHostAndPort(name: String) = HostAndPort.fromString(getString(name))
|
||||
fun testConfiguration(baseDirectory: Path, legalName: String, basePort: Int): FullNodeConfiguration {
|
||||
return FullNodeConfiguration(
|
||||
basedir = baseDirectory,
|
||||
myLegalName = legalName,
|
||||
networkMapService = null,
|
||||
nearestCity = "Null Island",
|
||||
emailAddress = "",
|
||||
keyStorePassword = "cordacadevpass",
|
||||
trustStorePassword = "trustpass",
|
||||
dataSourceProperties = makeTestDataSourceProperties(legalName),
|
||||
certificateSigningService = URL("http://localhost"),
|
||||
rpcUsers = emptyList(),
|
||||
verifierType = VerifierType.InMemory,
|
||||
useHTTPS = false,
|
||||
p2pAddress = HostAndPort.fromParts("localhost", basePort),
|
||||
rpcAddress = HostAndPort.fromParts("localhost", basePort + 1),
|
||||
messagingServerAddress = null,
|
||||
extraAdvertisedServiceIds = emptyList(),
|
||||
notaryNodeId = null,
|
||||
notaryNodeAddress = null,
|
||||
notaryClusterAddresses = emptyList(),
|
||||
certificateChainCheckPolicies = emptyList(),
|
||||
devMode = true)
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun configureTestSSL(legalName: String = "Mega Corp."): SSLConfiguration = object : SSLConfiguration {
|
||||
|
@ -14,6 +14,7 @@ import net.corda.node.services.config.FullNodeConfiguration
|
||||
import net.corda.node.services.transactions.RaftValidatingNotaryService
|
||||
import net.corda.node.utilities.ServiceIdentityGenerator
|
||||
import net.corda.nodeapi.User
|
||||
import net.corda.nodeapi.config.parseAs
|
||||
import net.corda.testing.MOCK_NODE_VERSION_INFO
|
||||
import net.corda.testing.getFreeLocalPorts
|
||||
import org.junit.After
|
||||
@ -126,7 +127,7 @@ abstract class NodeBasedTest {
|
||||
"extraAdvertisedServiceIds" to advertisedServices.map { it.toString() },
|
||||
"rpcUsers" to rpcUsers.map {
|
||||
mapOf(
|
||||
"user" to it.username,
|
||||
"username" to it.username,
|
||||
"password" to it.password,
|
||||
"permissions" to it.permissions
|
||||
)
|
||||
@ -134,7 +135,7 @@ abstract class NodeBasedTest {
|
||||
) + configOverrides
|
||||
)
|
||||
|
||||
val node = FullNodeConfiguration(baseDirectory, config).createNode(MOCK_NODE_VERSION_INFO)
|
||||
val node = config.parseAs<FullNodeConfiguration>().createNode(MOCK_NODE_VERSION_INFO)
|
||||
node.start()
|
||||
nodes += node
|
||||
thread(name = legalName) {
|
||||
|
@ -30,7 +30,7 @@ class SimpleNode(val config: NodeConfiguration, val address: HostAndPort = freeL
|
||||
|
||||
private val databaseWithCloseable: Pair<Closeable, Database> = configureDatabase(config.dataSourceProperties)
|
||||
val database: Database get() = databaseWithCloseable.second
|
||||
val userService = RPCUserServiceImpl(config)
|
||||
val userService = RPCUserServiceImpl(config.rpcUsers)
|
||||
val monitoringService = MonitoringService(MetricRegistry())
|
||||
val identity: KeyPair = generateKeyPair()
|
||||
val executor = ServiceAffinityExecutor(config.myLegalName, 1)
|
||||
|
@ -5,14 +5,14 @@ import net.corda.nodeapi.User
|
||||
import java.util.*
|
||||
|
||||
fun User.toMap(): Map<String, Any> = mapOf(
|
||||
"user" to username,
|
||||
"username" to username,
|
||||
"password" to password,
|
||||
"permissions" to permissions
|
||||
)
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun toUser(map: Map<String, Any>) = User(
|
||||
map.getOrElse("user", { "none" }) as String,
|
||||
map.getOrElse("username", { "none" }) as String,
|
||||
map.getOrElse("password", { "none" }) as String,
|
||||
LinkedHashSet<String>(map.getOrElse("permissions", { emptyList<String>() }) as Collection<String>)
|
||||
)
|
||||
|
@ -7,11 +7,15 @@ import net.corda.core.div
|
||||
import net.corda.node.internal.NetworkMapInfo
|
||||
import net.corda.node.services.config.FullNodeConfiguration
|
||||
import net.corda.nodeapi.User
|
||||
import net.corda.nodeapi.config.parseAs
|
||||
import net.corda.webserver.WebServerConfig
|
||||
import org.junit.Test
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import kotlin.test.*
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class NodeConfigTest {
|
||||
|
||||
@ -129,7 +133,7 @@ class NodeConfigTest {
|
||||
+ "\"p2pAddress\":\"localhost:10001\","
|
||||
+ "\"rpcAddress\":\"localhost:40002\","
|
||||
+ "\"rpcUsers\":["
|
||||
+ "{\"password\":\"letmein\",\"permissions\":[\"ALL\"],\"user\":\"jenny\"}"
|
||||
+ "{\"password\":\"letmein\",\"permissions\":[\"ALL\"],\"username\":\"jenny\"}"
|
||||
+ "],"
|
||||
+ "\"useTestClock\":true,"
|
||||
+ "\"webAddress\":\"localhost:20001\""
|
||||
@ -159,7 +163,7 @@ class NodeConfigTest {
|
||||
+ "\"p2pAddress\":\"localhost:10001\","
|
||||
+ "\"rpcAddress\":\"localhost:40002\","
|
||||
+ "\"rpcUsers\":["
|
||||
+ "{\"password\":\"letmein\",\"permissions\":[\"ALL\"],\"user\":\"jenny\"}"
|
||||
+ "{\"password\":\"letmein\",\"permissions\":[\"ALL\"],\"username\":\"jenny\"}"
|
||||
+ "],"
|
||||
+ "\"useTestClock\":true,"
|
||||
+ "\"webAddress\":\"localhost:20001\""
|
||||
@ -184,11 +188,10 @@ class NodeConfigTest {
|
||||
.withValue("basedir", ConfigValueFactory.fromAnyRef(baseDir.toString()))
|
||||
.withFallback(ConfigFactory.parseResources("reference.conf"))
|
||||
.resolve()
|
||||
val fullConfig = FullNodeConfiguration(baseDir, nodeConfig)
|
||||
val fullConfig = nodeConfig.parseAs<FullNodeConfiguration>()
|
||||
|
||||
assertEquals("My Name", fullConfig.myLegalName)
|
||||
assertEquals("Stockholm", fullConfig.nearestCity)
|
||||
assertEquals(localPort(20001), fullConfig.webAddress)
|
||||
assertEquals(localPort(40002), fullConfig.rpcAddress)
|
||||
assertEquals(localPort(10001), fullConfig.p2pAddress)
|
||||
assertEquals(listOf("my.service"), fullConfig.extraAdvertisedServiceIds)
|
||||
|
@ -2,7 +2,7 @@ package net.corda.demobench.model
|
||||
|
||||
import net.corda.nodeapi.User
|
||||
import org.junit.Test
|
||||
import kotlin.test.*
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class UserTest {
|
||||
|
||||
@ -17,7 +17,7 @@ class UserTest {
|
||||
@Test
|
||||
fun createFromMap() {
|
||||
val map = mapOf(
|
||||
"user" to "MyName",
|
||||
"username" to "MyName",
|
||||
"password" to "MyPassword",
|
||||
"permissions" to listOf("Flow.MyFlow")
|
||||
)
|
||||
@ -31,7 +31,7 @@ class UserTest {
|
||||
fun userToMap() {
|
||||
val user = User("MyName", "MyPassword", setOf("Flow.MyFlow"))
|
||||
val map = user.toMap()
|
||||
assertEquals("MyName", map["user"])
|
||||
assertEquals("MyName", map["username"])
|
||||
assertEquals("MyPassword", map["password"])
|
||||
assertEquals(setOf("Flow.MyFlow"), map["permissions"])
|
||||
}
|
||||
|
@ -3,9 +3,7 @@ package net.corda.webserver
|
||||
import com.google.common.net.HostAndPort
|
||||
import com.typesafe.config.Config
|
||||
import net.corda.core.div
|
||||
import net.corda.nodeapi.User
|
||||
import net.corda.nodeapi.config.SSLConfiguration
|
||||
import net.corda.nodeapi.config.getListOrElse
|
||||
import net.corda.nodeapi.config.getValue
|
||||
import java.nio.file.Path
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user