From c17fe29a62759abe21b79004e1b3eb985d152d6b Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Fri, 7 Apr 2017 11:23:25 +0100 Subject: [PATCH] Converted FullNodeConfiguration into a data class and added ability to parse Configs into data classes --- docs/source/corda-configuration-file.rst | 4 +- docs/source/example-code/build.gradle | 2 +- .../src/main/resources/example-node.conf | 2 +- .../net/corda/docs/ExampleConfigTest.kt | 12 +- docs/source/tutorial-clientrpc-api.rst | 4 +- .../main/groovy/net/corda/plugins/Node.groovy | 2 +- node-api/build.gradle | 5 + .../kotlin/net/corda/nodeapi/RPCStructures.kt | 7 +- .../corda/nodeapi/config/ConfigUtilities.kt | 148 +++++++---- .../corda/nodeapi/config/ConfigParsingTest.kt | 238 ++++++++++++++++++ .../main/kotlin/net/corda/node/ArgsParser.kt | 9 +- node/src/main/kotlin/net/corda/node/Corda.kt | 15 +- .../kotlin/net/corda/node/driver/Driver.kt | 53 ++-- .../kotlin/net/corda/node/internal/Node.kt | 3 +- .../net/corda/node/services/RPCUserService.kt | 10 +- .../node/services/config/NodeConfiguration.kt | 137 +++++----- .../messaging/ArtemisMessagingServer.kt | 10 +- .../node/services/messaging/RPCDispatcher.kt | 2 +- .../node/services/RPCUserServiceImplTest.kt | 76 ------ .../config/FullNodeConfigurationTest.kt | 21 ++ .../messaging/ArtemisMessagingTests.kt | 3 +- samples/attachment-demo/build.gradle | 2 +- samples/bank-of-corda-demo/build.gradle | 4 +- samples/raft-notary-demo/build.gradle | 2 +- samples/trader-demo/build.gradle | 2 +- .../kotlin/net/corda/testing/CoreTestUtils.kt | 38 ++- .../net/corda/testing/node/NodeBasedTest.kt | 5 +- .../net/corda/testing/node/SimpleNode.kt | 2 +- .../kotlin/net/corda/demobench/model/User.kt | 4 +- .../corda/demobench/model/NodeConfigTest.kt | 15 +- .../net/corda/demobench/model/UserTest.kt | 6 +- .../net/corda/webserver/WebServerConfig.kt | 2 - 32 files changed, 544 insertions(+), 301 deletions(-) create mode 100644 node-api/src/test/kotlin/net/corda/nodeapi/config/ConfigParsingTest.kt delete mode 100644 node/src/test/kotlin/net/corda/node/services/RPCUserServiceImplTest.kt create mode 100644 node/src/test/kotlin/net/corda/node/services/config/FullNodeConfigurationTest.kt diff --git a/docs/source/corda-configuration-file.rst b/docs/source/corda-configuration-file.rst index eace15faad..a4e7f7a93a 100644 --- a/docs/source/corda-configuration-file.rst +++ b/docs/source/corda-configuration-file.rst @@ -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 diff --git a/docs/source/example-code/build.gradle b/docs/source/example-code/build.gradle index f9ed909ea9..9ae3fcf5a2 100644 --- a/docs/source/example-code/build.gradle +++ b/docs/source/example-code/build.gradle @@ -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"]] ] diff --git a/docs/source/example-code/src/main/resources/example-node.conf b/docs/source/example-code/src/main/resources/example-node.conf index 2cc35d9bda..eccf34c03c 100644 --- a/docs/source/example-code/src/main/resources/example-node.conf +++ b/docs/source/example-code/src/main/resources/example-node.conf @@ -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. diff --git a/docs/source/example-code/src/test/kotlin/net/corda/docs/ExampleConfigTest.kt b/docs/source/example-code/src/test/kotlin/net/corda/docs/ExampleConfigTest.kt index a0c3c3d785..a797479c86 100644 --- a/docs/source/example-code/src/test/kotlin/net/corda/docs/ExampleConfigTest.kt +++ b/docs/source/example-code/src/test/kotlin/net/corda/docs/ExampleConfigTest.kt @@ -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() } } diff --git a/docs/source/tutorial-clientrpc-api.rst b/docs/source/tutorial-clientrpc-api.rst index 10bdef34ef..0e6afef34e 100644 --- a/docs/source/tutorial-clientrpc-api.rst +++ b/docs/source/tutorial-clientrpc-api.rst @@ -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"]] ] diff --git a/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Node.groovy b/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Node.groovy index 3c78f17b12..6f23f88ee0 100644 --- a/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Node.groovy +++ b/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Node.groovy @@ -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. diff --git a/node-api/build.gradle b/node-api/build.gradle index 00803ed047..7396d687b4 100644 --- a/node-api/build.gradle +++ b/node-api/build.gradle @@ -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') } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/RPCStructures.kt b/node-api/src/main/kotlin/net/corda/nodeapi/RPCStructures.kt index 21dc8c2917..396027eb74 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/RPCStructures.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/RPCStructures.kt @@ -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) { +data class User( + @OldConfig("user") + val username: String, + val password: String, + val permissions: Set) { override fun toString(): String = "${javaClass.simpleName}($username, permissions=$permissions)" } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/config/ConfigUtilities.kt b/node-api/src/main/kotlin/net/corda/nodeapi/config/ConfigUtilities.kt index c1392c71af..5db04682a4 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/config/ConfigUtilities.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/config/ConfigUtilities.kt @@ -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 > enumBridge(clazz: Class, enumValueString: String): T { - return java.lang.Enum.valueOf(clazz, enumValueString) -} -private class DummyEnum : Enum("", 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 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, getString(metadata.name)) as T + return getValueInternal(metadata.name, metadata.returnType) +} + +fun Config.parseAs(clazz: KClass): 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(path, param.type) } - throw IllegalArgumentException("Unsupported type ${metadata.returnType}") + return constructor.callBy(args) +} + +inline fun 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 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(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 { + 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 = 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 Config.getOrElse(lambda: () -> T): OptionalConfig = 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().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 Config.getListOrElse(path: String, default: Config.() -> List): List { - return if (hasPath(path)) { - (if (T::class == String::class) getStringList(path) else getConfigList(path)) as List - } else { - this.default() - } -} +private fun parseEnum(enumType: Class<*>, name: String): Enum<*> = enumBridge(enumType as Class, name) // Any enum will do + +private fun > enumBridge(clazz: Class, name: String): T = java.lang.Enum.valueOf(clazz, name) + +private val logger = LoggerFactory.getLogger("net.corda.nodeapi.config") diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/config/ConfigParsingTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/config/ConfigParsingTest.kt new file mode 100644 index 0000000000..15deba5eb4 --- /dev/null +++ b/node-api/src/test/kotlin/net/corda/nodeapi/config/ConfigParsingTest.kt @@ -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("hello world!", "bye") + } + + @Test + fun `Int`() { + testPropertyType(1, 2) + } + + @Test + fun `Long`() { + testPropertyType(Long.MAX_VALUE, Long.MIN_VALUE) + } + + @Test + fun `Double`() { + testPropertyType(1.2, 3.4) + } + + @Test + fun `Boolean`() { + testPropertyType(true, false) + } + + @Test + fun `Enum`() { + testPropertyType(TestEnum.Value2, TestEnum.Value1, valuesToString = true) + } + + @Test + fun `LocalDate`() { + testPropertyType(LocalDate.now(), LocalDate.now().plusDays(1), valuesToString = true) + } + + @Test + fun `Instant`() { + testPropertyType(Instant.now(), Instant.now().plusMillis(100), valuesToString = true) + } + + @Test + fun `HostAndPort`() { + testPropertyType( + HostAndPort.fromParts("localhost", 2223), + HostAndPort.fromParts("localhost", 2225), + valuesToString = true) + } + + @Test + fun `Path`() { + val path = Paths.get("tmp") / "test" + testPropertyType(path, path / "file", valuesToString = true) + } + + @Test + fun `URL`() { + testPropertyType(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().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().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().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().values).containsExactly( + Properties(), + Properties().apply { this["key"] = "prop" }) + } + + @Test + fun `Set`() { + val config = config("values" to listOf("a", "a", "b")) + assertThat(config.parseAs().values).containsOnly("a", "b") + assertThat(empty().parseAs().values).isEmpty() + } + + @Test + fun `multi property data class`() { + val data = config( + "b" to true, + "i" to 123, + "l" to listOf("a", "b")) + .parseAs() + 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()).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()).isEqualTo(data) + } + + @Test + fun `default value property`() { + assertThat(config("a" to 3).parseAs()).isEqualTo(DefaultData(3, 2)) + assertThat(config("a" to 3, "defaultOfTwo" to 3).parseAs()).isEqualTo(DefaultData(3, 3)) + } + + @Test + fun `nullable property`() { + assertThat(empty().parseAs().nullable).isNull() + assertThat(config("nullable" to null).parseAs().nullable).isNull() + assertThat(config("nullable" to "not null").parseAs().nullable).isEqualTo("not null") + } + + @Test + fun `old config property`() { + assertThat(config("oldValue" to "old").parseAs().newValue).isEqualTo("old") + assertThat(config("newValue" to "new").parseAs().newValue).isEqualTo("new") + } + + private inline fun , reified L : ListData, V : Any> testPropertyType( + value1: V, + value2: V, + valuesToString: Boolean = false) { + testSingleProperty(value1, valuesToString) + testListProperty(value1, value2, valuesToString) + } + + private inline fun , 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().value).isEqualTo(data.value) + } + + private inline fun , 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().values).isEqualTo(data.values) + } + assertThat(empty().parseAs().values).isEmpty() + } + + private fun config(vararg values: Pair): Config { + val config = ConfigValueFactory.fromMap(mapOf(*values)) + println(config.render(defaults().setOriginComments(false))) + return config.toConfig() + } + + private interface SingleData { + val value: T + } + + private interface ListData { + val values: List + } + + data class StringData(override val value: String) : SingleData + data class StringListData(override val values: List) : ListData + data class StringSetData(val values: Set) + data class IntData(override val value: Int) : SingleData + data class IntListData(override val values: List) : ListData + data class LongData(override val value: Long) : SingleData + data class LongListData(override val values: List) : ListData + data class DoubleData(override val value: Double) : SingleData + data class DoubleListData(override val values: List) : ListData + data class BooleanData(override val value: Boolean) : SingleData + data class BooleanListData(override val values: List) : ListData + data class EnumData(override val value: TestEnum) : SingleData + data class EnumListData(override val values: List) : ListData + data class LocalDateData(override val value: LocalDate) : SingleData + data class LocalDateListData(override val values: List) : ListData + data class InstantData(override val value: Instant) : SingleData + data class InstantListData(override val values: List) : ListData + data class HostAndPortData(override val value: HostAndPort) : SingleData + data class HostAndPortListData(override val values: List) : ListData + data class PathData(override val value: Path) : SingleData + data class PathListData(override val values: List) : ListData + data class URLData(override val value: URL) : SingleData + data class URLListData(override val values: List) : ListData + data class PropertiesData(override val value: Properties) : SingleData + data class PropertiesListData(override val values: List) : ListData + data class MultiPropertyData(val i: Int, val b: Boolean, val l: List) + data class NestedData(val first: StringData) + data class DataListData(val list: List) + 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 } + +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/ArgsParser.kt b/node/src/main/kotlin/net/corda/node/ArgsParser.kt index 62bddc50aa..6292608493 100644 --- a/node/src/main/kotlin/net/corda/node/ArgsParser.kt +++ b/node/src/main/kotlin/net/corda/node/ArgsParser.kt @@ -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 = emptyMap()): Config { - return ConfigHelper.loadConfig(baseDirectory, configFile, allowMissingConfig, configOverrides) + fun loadConfig(allowMissingConfig: Boolean = false, configOverrides: Map = emptyMap()): FullNodeConfiguration { + return ConfigHelper + .loadConfig(baseDirectory, configFile, allowMissingConfig, configOverrides) + .parseAs() } } diff --git a/node/src/main/kotlin/net/corda/node/Corda.kt b/node/src/main/kotlin/net/corda/node/Corda.kt index 49eb9fa647..3952ecaa35 100644 --- a/node/src/main/kotlin/net/corda/node/Corda.kt +++ b/node/src/main/kotlin/net/corda/node/Corda.kt @@ -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) { 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) { 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 { diff --git a/node/src/main/kotlin/net/corda/node/driver/Driver.kt b/node/src/main/kotlin/net/corda/node/driver/Driver.kt index 1ff1fc686e..f1cd67b32a 100644 --- a/node/src/main/kotlin/net/corda/node/driver/Driver.kt +++ b/node/src/main/kotlin/net/corda/node/driver/Driver.kt @@ -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() - 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 { 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(), 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 ): ListenableFuture { return executorService.submit { // 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 { return executorService.submit { 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 } } } } } diff --git a/node/src/main/kotlin/net/corda/node/internal/Node.kt b/node/src/main/kotlin/net/corda/node/internal/Node.kt index 6e0d5433cc..1b28b8b46b 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -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( diff --git a/node/src/main/kotlin/net/corda/node/services/RPCUserService.kt b/node/src/main/kotlin/net/corda/node/services/RPCUserService.kt index 64e479dbe0..52469576f7 100644 --- a/node/src/main/kotlin/net/corda/node/services/RPCUserService.kt +++ b/node/src/main/kotlin/net/corda/node/services/RPCUserService.kt @@ -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 get() = _users.values.toList() +class RPCUserServiceImpl(override val users: List) : RPCUserService { + override fun getUser(username: String): User? = users.find { it.username == username } } fun startFlowPermission(className: String) = "StartFlow.$className" diff --git a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt index b850407f42..31ade8ea7a 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt @@ -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 get() = emptyList() + val dataSourceProperties: Properties + val rpcUsers: List val devMode: Boolean val certificateSigningService: URL - val certificateChainCheckPolicies: Map + val certificateChainCheckPolicies: List 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, + 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, + val notaryNodeId: Int?, + val notaryNodeAddress: HostAndPort?, + val notaryClusterAddresses: List, + override val certificateChainCheckPolicies: List, + 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 = config - .getListOrElse("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("permissions") { emptyList() }.toSet() - User(username, password, permissions) - } - override val certificateChainCheckPolicies = config.getOptionalConfig("certificateChainCheckPolicies")?.run { - entrySet().associateByTo(HashMap(), { it.key }, { parseCertificateChainCheckPolicy(getConfig(it.key)) }) - } ?: emptyMap() - 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 = config.getListOrElse("extraAdvertisedServiceIds") { emptyList() } - val useTestClock: Boolean by config.getOrElse { false } - val notaryNodeId: Int? by config - val notaryNodeAddress: HostAndPort? by config - val notaryClusterAddresses: List = config - .getListOrElse("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) { + val certificateChainCheckPolicy: CertificateChainCheckPolicy get() { + return when (policy) { + CertChainPolicyType.Any -> CertificateChainCheckPolicy.Any + CertChainPolicyType.RootMustMatch -> CertificateChainCheckPolicy.RootMustMatch + CertChainPolicyType.LeafMustMatch -> CertificateChainCheckPolicy.LeafMustMatch + CertChainPolicyType.MustContainOneOf -> CertificateChainCheckPolicy.MustContainOneOf(trustedAliases) + } + } +} diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt b/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt index 3e20c8ac82..029f9a7bfd 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt @@ -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 diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/RPCDispatcher.kt b/node/src/main/kotlin/net/corda/node/services/messaging/RPCDispatcher.kt index 335edc8728..b2199873d1 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/RPCDispatcher.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/RPCDispatcher.kt @@ -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 diff --git a/node/src/test/kotlin/net/corda/node/services/RPCUserServiceImplTest.kt b/node/src/test/kotlin/net/corda/node/services/RPCUserServiceImplTest.kt deleted file mode 100644 index 7a4d86af56..0000000000 --- a/node/src/test/kotlin/net/corda/node/services/RPCUserServiceImplTest.kt +++ /dev/null @@ -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))) - } -} \ No newline at end of file diff --git a/node/src/test/kotlin/net/corda/node/services/config/FullNodeConfigurationTest.kt b/node/src/test/kotlin/net/corda/node/services/config/FullNodeConfigurationTest.kt new file mode 100644 index 0000000000..edf727af61 --- /dev/null +++ b/node/src/test/kotlin/net/corda/node/services/config/FullNodeConfigurationTest.kt @@ -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("#") + } +} diff --git a/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt b/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt index 63f4aad323..99994f4ed2 100644 --- a/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt @@ -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", diff --git a/samples/attachment-demo/build.gradle b/samples/attachment-demo/build.gradle index d9cd1b22a8..6cc01c2d9b 100644 --- a/samples/attachment-demo/build.gradle +++ b/samples/attachment-demo/build.gradle @@ -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" diff --git a/samples/bank-of-corda-demo/build.gradle b/samples/bank-of-corda-demo/build.gradle index 9b0a50eb63..9c69c89cca 100644 --- a/samples/bank-of-corda-demo/build.gradle +++ b/samples/bank-of-corda-demo/build.gradle @@ -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"]] ] diff --git a/samples/raft-notary-demo/build.gradle b/samples/raft-notary-demo/build.gradle index 9b763e9bdf..b022cd307f 100644 --- a/samples/raft-notary-demo/build.gradle +++ b/samples/raft-notary-demo/build.gradle @@ -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' ]]] diff --git a/samples/trader-demo/build.gradle b/samples/trader-demo/build.gradle index bbbff4491d..cc2c4c0584 100644 --- a/samples/trader-demo/build.gradle +++ b/samples/trader-demo/build.gradle @@ -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" ]]] diff --git a/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt b/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt index a42634bf6c..e315941d66 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt @@ -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 > 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 = 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 = emptyMap(), + override val certificateChainCheckPolicies: List = 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 { diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt b/test-utils/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt index 57584b6e19..997638525c 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt @@ -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().createNode(MOCK_NODE_VERSION_INFO) node.start() nodes += node thread(name = legalName) { diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/SimpleNode.kt b/test-utils/src/main/kotlin/net/corda/testing/node/SimpleNode.kt index 18b6af8374..a628b4351d 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/SimpleNode.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/node/SimpleNode.kt @@ -30,7 +30,7 @@ class SimpleNode(val config: NodeConfiguration, val address: HostAndPort = freeL private val databaseWithCloseable: Pair = 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) diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/User.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/User.kt index b6d6c325ab..231d5d5b7c 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/User.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/User.kt @@ -5,14 +5,14 @@ import net.corda.nodeapi.User import java.util.* fun User.toMap(): Map = mapOf( - "user" to username, + "username" to username, "password" to password, "permissions" to permissions ) @Suppress("UNCHECKED_CAST") fun toUser(map: Map) = User( - map.getOrElse("user", { "none" }) as String, + map.getOrElse("username", { "none" }) as String, map.getOrElse("password", { "none" }) as String, LinkedHashSet(map.getOrElse("permissions", { emptyList() }) as Collection) ) diff --git a/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt b/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt index e3b5853b1c..9c8cafb068 100644 --- a/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt +++ b/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt @@ -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() 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) diff --git a/tools/demobench/src/test/kotlin/net/corda/demobench/model/UserTest.kt b/tools/demobench/src/test/kotlin/net/corda/demobench/model/UserTest.kt index 75d16928b4..fa89c15592 100644 --- a/tools/demobench/src/test/kotlin/net/corda/demobench/model/UserTest.kt +++ b/tools/demobench/src/test/kotlin/net/corda/demobench/model/UserTest.kt @@ -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"]) } diff --git a/webserver/src/main/kotlin/net/corda/webserver/WebServerConfig.kt b/webserver/src/main/kotlin/net/corda/webserver/WebServerConfig.kt index 693e5f5598..1afef7a671 100644 --- a/webserver/src/main/kotlin/net/corda/webserver/WebServerConfig.kt +++ b/webserver/src/main/kotlin/net/corda/webserver/WebServerConfig.kt @@ -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