Converted FullNodeConfiguration into a data class and added ability to parse Configs into data classes

This commit is contained in:
Shams Asari
2017-04-07 11:23:25 +01:00
parent 868a490bba
commit c17fe29a62
32 changed files with 544 additions and 301 deletions

View File

@ -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>()
}
}

View File

@ -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 {

View File

@ -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 } }
}
}
}

View File

@ -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(

View File

@ -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"

View File

@ -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)
}
}
}

View File

@ -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

View File

@ -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

View File

@ -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)))
}
}

View File

@ -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("#")
}
}

View File

@ -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",