mirror of
https://github.com/corda/corda.git
synced 2024-12-24 07:06:44 +00:00
Moved the RPC user config out of the properties file and into the main config file
This commit is contained in:
parent
f4925c0fa9
commit
c326a9ae46
12
.idea/modules.xml
generated
12
.idea/modules.xml
generated
@ -15,12 +15,9 @@
|
|||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/core/core.iml" filepath="$PROJECT_DIR$/.idea/modules/core/core.iml" group="core" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/core/core.iml" filepath="$PROJECT_DIR$/.idea/modules/core/core.iml" group="core" />
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/core/core_main.iml" filepath="$PROJECT_DIR$/.idea/modules/core/core_main.iml" group="core" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/core/core_main.iml" filepath="$PROJECT_DIR$/.idea/modules/core/core_main.iml" group="core" />
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/core/core_test.iml" filepath="$PROJECT_DIR$/.idea/modules/core/core_test.iml" group="core" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/core/core_test.iml" filepath="$PROJECT_DIR$/.idea/modules/core/core_test.iml" group="core" />
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/docs/docs.iml" filepath="$PROJECT_DIR$/.idea/modules/docs/docs.iml" group="docs" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/docs/source/example-code/docs_source_example-code.iml" filepath="$PROJECT_DIR$/.idea/modules/docs/source/example-code/docs_source_example-code.iml" group="docs/source/example-code" />
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/docs/docs_main.iml" filepath="$PROJECT_DIR$/.idea/modules/docs/docs_main.iml" group="docs" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/docs/source/example-code/docs_source_example-code_main.iml" filepath="$PROJECT_DIR$/.idea/modules/docs/source/example-code/docs_source_example-code_main.iml" group="docs/source/example-code" />
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/docs/docs_test.iml" filepath="$PROJECT_DIR$/.idea/modules/docs/docs_test.iml" group="docs" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/docs/source/example-code/docs_source_example-code_test.iml" filepath="$PROJECT_DIR$/.idea/modules/docs/source/example-code/docs_source_example-code_test.iml" group="docs/source/example-code" />
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/docs/source/example-code/example-code.iml" filepath="$PROJECT_DIR$/.idea/modules/docs/source/example-code/example-code.iml" group="docs/source/example-code" />
|
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/docs/source/example-code/example-code_main.iml" filepath="$PROJECT_DIR$/.idea/modules/docs/source/example-code/example-code_main.iml" group="docs/source/example-code" />
|
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/docs/source/example-code/example-code_test.iml" filepath="$PROJECT_DIR$/.idea/modules/docs/source/example-code/example-code_test.iml" group="docs/source/example-code" />
|
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/experimental/experimental.iml" filepath="$PROJECT_DIR$/.idea/modules/experimental/experimental.iml" group="experimental" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/experimental/experimental.iml" filepath="$PROJECT_DIR$/.idea/modules/experimental/experimental.iml" group="experimental" />
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/experimental/experimental_main.iml" filepath="$PROJECT_DIR$/.idea/modules/experimental/experimental_main.iml" group="experimental" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/experimental/experimental_main.iml" filepath="$PROJECT_DIR$/.idea/modules/experimental/experimental_main.iml" group="experimental" />
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/experimental/experimental_test.iml" filepath="$PROJECT_DIR$/.idea/modules/experimental/experimental_test.iml" group="experimental" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/experimental/experimental_test.iml" filepath="$PROJECT_DIR$/.idea/modules/experimental/experimental_test.iml" group="experimental" />
|
||||||
@ -49,9 +46,6 @@
|
|||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/r3prototyping.iml" filepath="$PROJECT_DIR$/.idea/modules/r3prototyping.iml" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/r3prototyping.iml" filepath="$PROJECT_DIR$/.idea/modules/r3prototyping.iml" />
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/r3prototyping_main.iml" filepath="$PROJECT_DIR$/.idea/modules/r3prototyping_main.iml" group="r3prototyping" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/r3prototyping_main.iml" filepath="$PROJECT_DIR$/.idea/modules/r3prototyping_main.iml" group="r3prototyping" />
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/r3prototyping_test.iml" filepath="$PROJECT_DIR$/.idea/modules/r3prototyping_test.iml" group="r3prototyping" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/r3prototyping_test.iml" filepath="$PROJECT_DIR$/.idea/modules/r3prototyping_test.iml" group="r3prototyping" />
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/docs/source/source.iml" filepath="$PROJECT_DIR$/.idea/modules/docs/source/source.iml" group="docs/source" />
|
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/docs/source/source_main.iml" filepath="$PROJECT_DIR$/.idea/modules/docs/source/source_main.iml" group="docs/source" />
|
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/docs/source/source_test.iml" filepath="$PROJECT_DIR$/.idea/modules/docs/source/source_test.iml" group="docs/source" />
|
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/test-utils/test-utils.iml" filepath="$PROJECT_DIR$/.idea/modules/test-utils/test-utils.iml" group="test-utils" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/test-utils/test-utils.iml" filepath="$PROJECT_DIR$/.idea/modules/test-utils/test-utils.iml" group="test-utils" />
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/test-utils/test-utils_main.iml" filepath="$PROJECT_DIR$/.idea/modules/test-utils/test-utils_main.iml" group="test-utils" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/test-utils/test-utils_main.iml" filepath="$PROJECT_DIR$/.idea/modules/test-utils/test-utils_main.iml" group="test-utils" />
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/test-utils/test-utils_test.iml" filepath="$PROJECT_DIR$/.idea/modules/test-utils/test-utils_test.iml" group="test-utils" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/test-utils/test-utils_test.iml" filepath="$PROJECT_DIR$/.idea/modules/test-utils/test-utils_test.iml" group="test-utils" />
|
||||||
|
@ -22,8 +22,10 @@ import java.nio.file.*
|
|||||||
import java.nio.file.attribute.FileAttribute
|
import java.nio.file.attribute.FileAttribute
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.time.temporal.Temporal
|
import java.time.temporal.Temporal
|
||||||
|
import java.util.concurrent.CompletableFuture
|
||||||
import java.util.concurrent.ExecutionException
|
import java.util.concurrent.ExecutionException
|
||||||
import java.util.concurrent.Executor
|
import java.util.concurrent.Executor
|
||||||
|
import java.util.concurrent.Future
|
||||||
import java.util.concurrent.locks.ReentrantLock
|
import java.util.concurrent.locks.ReentrantLock
|
||||||
import java.util.stream.Stream
|
import java.util.stream.Stream
|
||||||
import java.util.zip.ZipInputStream
|
import java.util.zip.ZipInputStream
|
||||||
@ -58,6 +60,9 @@ infix fun Long.checkedAdd(b: Long) = Math.addExact(this, b)
|
|||||||
*/
|
*/
|
||||||
fun random63BitValue(): Long = Math.abs(newSecureRandom().nextLong())
|
fun random63BitValue(): Long = Math.abs(newSecureRandom().nextLong())
|
||||||
|
|
||||||
|
// TODO Convert the CompletableFuture into a ListenableFuture
|
||||||
|
fun <T> future(block: () -> T): Future<T> = CompletableFuture.supplyAsync(block)
|
||||||
|
|
||||||
// Some utilities for working with Guava listenable futures.
|
// Some utilities for working with Guava listenable futures.
|
||||||
fun <T> ListenableFuture<T>.then(executor: Executor, body: () -> Unit) = addListener(Runnable(body), executor)
|
fun <T> ListenableFuture<T>.then(executor: Executor, body: () -> Unit) = addListener(Runnable(body), executor)
|
||||||
|
|
||||||
|
@ -21,9 +21,8 @@ Security
|
|||||||
--------
|
--------
|
||||||
|
|
||||||
Users wanting to use the RPC library are first required to authenticate themselves with the node using a valid username
|
Users wanting to use the RPC library are first required to authenticate themselves with the node using a valid username
|
||||||
and password. These are kept in ``rpc-users.properties`` in the node base directory. This file also specifies
|
and password. These are specified in the configuration file. Each user can be configured with a set of permissions which
|
||||||
permissions for each user, which the RPC implementation can use to control access. The file format is described in
|
the RPC can use for fine-grain access control.
|
||||||
:doc:`corda-configuration-files`.
|
|
||||||
|
|
||||||
Observables
|
Observables
|
||||||
-----------
|
-----------
|
||||||
|
@ -41,6 +41,9 @@ General node configuration file for hosting the IRSDemo services.
|
|||||||
extraAdvertisedServiceIds: "corda.interest_rates"
|
extraAdvertisedServiceIds: "corda.interest_rates"
|
||||||
networkMapAddress : "localhost:12345"
|
networkMapAddress : "localhost:12345"
|
||||||
useHTTPS : false
|
useHTTPS : false
|
||||||
|
rpcUsers : [
|
||||||
|
{ user=user1, password=letmein, permissions=[ cash ] }
|
||||||
|
]
|
||||||
|
|
||||||
NetworkMapService plus Simple Notary configuration file.
|
NetworkMapService plus Simple Notary configuration file.
|
||||||
|
|
||||||
@ -97,20 +100,12 @@ Configuration File Fields
|
|||||||
|
|
||||||
:useHTTPS: If false the node's web server will be plain HTTP. If true the node will use the same certificate and private key from the ``<workspace>/certificates/sslkeystore.jks`` file as the ArtemisMQ port for HTTPS. If HTTPS is enabled then unencrypted HTTP traffic to the node's **webAddress** port is not supported.
|
:useHTTPS: If false the node's web server will be plain HTTP. If true the node will use the same certificate and private key from the ``<workspace>/certificates/sslkeystore.jks`` file as the ArtemisMQ port for HTTPS. If HTTPS is enabled then unencrypted HTTP traffic to the node's **webAddress** port is not supported.
|
||||||
|
|
||||||
RPC Users File
|
: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:
|
||||||
|
|
||||||
Corda also uses the ``rpc-users.properties`` file, found in the base directory, to control access to the RPC subsystem.
|
:user: Username consisting only of word characters (a-z, A-Z, 0-9 and _)
|
||||||
This is a Java properties file (details can be found in the `Javadocs <https://docs.oracle.com/javase/8/docs/api/java/util/Properties.html#load-java.io.Reader->`_)
|
:password: The password
|
||||||
which specifies a list of users with their password and list of permissions they're enabled for.
|
:permissions: A list of permission strings which RPC methods can use to control access
|
||||||
|
|
||||||
.. code-block:: text
|
If this field is absent or an empty list then RPC is effectively locked down.
|
||||||
:caption: Sample
|
|
||||||
|
|
||||||
admin=notsecure,ADMIN
|
|
||||||
user1=letmein,CASH,PAPER
|
|
||||||
|
|
||||||
In this example ``user1`` has password ``letmein`` and has permissions for ``CASH`` and ``PAPER``. The permissions are
|
|
||||||
free-form strings which can be used by the RPC methods to control access.
|
|
||||||
|
|
||||||
If ``rpc-users.properties`` is empty or doesn't exist then the RPC subsystem is effectively locked down.
|
|
||||||
|
@ -8,10 +8,10 @@ import com.typesafe.config.ConfigRenderOptions
|
|||||||
import net.corda.core.ThreadBox
|
import net.corda.core.ThreadBox
|
||||||
import net.corda.core.crypto.Party
|
import net.corda.core.crypto.Party
|
||||||
import net.corda.core.div
|
import net.corda.core.div
|
||||||
|
import net.corda.core.future
|
||||||
import net.corda.core.node.NodeInfo
|
import net.corda.core.node.NodeInfo
|
||||||
import net.corda.core.node.services.ServiceInfo
|
import net.corda.core.node.services.ServiceInfo
|
||||||
import net.corda.core.utilities.loggerFor
|
import net.corda.core.utilities.loggerFor
|
||||||
import net.corda.core.write
|
|
||||||
import net.corda.node.services.User
|
import net.corda.node.services.User
|
||||||
import net.corda.node.services.config.ConfigHelper
|
import net.corda.node.services.config.ConfigHelper
|
||||||
import net.corda.node.services.config.FullNodeConfiguration
|
import net.corda.node.services.config.FullNodeConfiguration
|
||||||
@ -28,7 +28,9 @@ import java.time.Instant
|
|||||||
import java.time.ZoneOffset.UTC
|
import java.time.ZoneOffset.UTC
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.*
|
import java.util.concurrent.Future
|
||||||
|
import java.util.concurrent.TimeUnit.SECONDS
|
||||||
|
import java.util.concurrent.TimeoutException
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This file defines a small "Driver" DSL for starting up nodes that is only intended for development, demos and tests.
|
* This file defines a small "Driver" DSL for starting up nodes that is only intended for development, demos and tests.
|
||||||
@ -246,11 +248,11 @@ open class DriverDSL(
|
|||||||
registeredProcesses.forEach(Process::destroy)
|
registeredProcesses.forEach(Process::destroy)
|
||||||
}
|
}
|
||||||
/** Wait 5 seconds, then [Process.destroyForcibly] */
|
/** Wait 5 seconds, then [Process.destroyForcibly] */
|
||||||
val finishedFuture = Executors.newSingleThreadExecutor().submit {
|
val finishedFuture = future {
|
||||||
waitForAllNodesToFinish()
|
waitForAllNodesToFinish()
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
finishedFuture.get(5, TimeUnit.SECONDS)
|
finishedFuture.get(5, SECONDS)
|
||||||
} catch (exception: TimeoutException) {
|
} catch (exception: TimeoutException) {
|
||||||
finishedFuture.cancel(true)
|
finishedFuture.cancel(true)
|
||||||
state.locked {
|
state.locked {
|
||||||
@ -306,22 +308,19 @@ open class DriverDSL(
|
|||||||
"webAddress" to apiAddress.toString(),
|
"webAddress" to apiAddress.toString(),
|
||||||
"extraAdvertisedServiceIds" to advertisedServices.joinToString(","),
|
"extraAdvertisedServiceIds" to advertisedServices.joinToString(","),
|
||||||
"networkMapAddress" to networkMapAddress.toString(),
|
"networkMapAddress" to networkMapAddress.toString(),
|
||||||
"useTestClock" to useTestClock
|
"useTestClock" to useTestClock,
|
||||||
|
"rpcUsers" to rpcUsers.map { mapOf(
|
||||||
|
"user" to it.username,
|
||||||
|
"password" to it.password,
|
||||||
|
"permissions" to it.permissions)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
val nodeConfig = FullNodeConfiguration(config)
|
return future {
|
||||||
|
registerProcess(DriverDSL.startNode(FullNodeConfiguration(config), quasarJarPath, debugPort))
|
||||||
nodeConfig.rpcUsersFile.write(createDirs = true) {
|
|
||||||
rpcUsers.map { it.username to "${it.password},${it.permissions.joinToString(",")}" }
|
|
||||||
.toMap(Properties())
|
|
||||||
.store(it, null)
|
|
||||||
}
|
|
||||||
|
|
||||||
return Executors.newSingleThreadExecutor().submit(Callable<NodeInfoAndConfig> {
|
|
||||||
registerProcess(DriverDSL.startNode(nodeConfig, quasarJarPath, debugPort))
|
|
||||||
NodeInfoAndConfig(queryNodeInfo(apiAddress)!!, config)
|
NodeInfoAndConfig(queryNodeInfo(apiAddress)!!, config)
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun start() {
|
override fun start() {
|
||||||
|
@ -8,8 +8,8 @@ import net.corda.core.node.services.ServiceInfo
|
|||||||
import net.corda.core.then
|
import net.corda.core.then
|
||||||
import net.corda.core.utilities.loggerFor
|
import net.corda.core.utilities.loggerFor
|
||||||
import net.corda.node.serialization.NodeClock
|
import net.corda.node.serialization.NodeClock
|
||||||
import net.corda.node.services.PropertiesFileRPCUserService
|
|
||||||
import net.corda.node.services.RPCUserService
|
import net.corda.node.services.RPCUserService
|
||||||
|
import net.corda.node.services.RPCUserServiceImpl
|
||||||
import net.corda.node.services.api.MessagingServiceInternal
|
import net.corda.node.services.api.MessagingServiceInternal
|
||||||
import net.corda.node.services.config.FullNodeConfiguration
|
import net.corda.node.services.config.FullNodeConfiguration
|
||||||
import net.corda.node.services.messaging.ArtemisMessagingServer
|
import net.corda.node.services.messaging.ArtemisMessagingServer
|
||||||
@ -118,7 +118,7 @@ class Node(override val configuration: FullNodeConfiguration, networkMapAddress:
|
|||||||
private lateinit var userService: RPCUserService
|
private lateinit var userService: RPCUserService
|
||||||
|
|
||||||
override fun makeMessagingService(): MessagingServiceInternal {
|
override fun makeMessagingService(): MessagingServiceInternal {
|
||||||
userService = PropertiesFileRPCUserService(configuration.rpcUsersFile)
|
userService = RPCUserServiceImpl(configuration.config)
|
||||||
val serverAddr = with(configuration) {
|
val serverAddr = with(configuration) {
|
||||||
messagingServerAddress ?: {
|
messagingServerAddress ?: {
|
||||||
messageBroker = ArtemisMessagingServer(this, artemisAddress, services.networkMapCache, userService)
|
messageBroker = ArtemisMessagingServer(this, artemisAddress, services.networkMapCache, userService)
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
package net.corda.node.services
|
package net.corda.node.services
|
||||||
|
|
||||||
import net.corda.core.exists
|
import com.typesafe.config.Config
|
||||||
import net.corda.core.read
|
import net.corda.node.services.config.getListOrElse
|
||||||
import java.nio.file.Path
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service for retrieving [User] objects representing RPC users who are authorised to use the RPC system. A [User]
|
* Service for retrieving [User] objects representing RPC users who are authorised to use the RPC system. A [User]
|
||||||
@ -15,30 +13,22 @@ interface RPCUserService {
|
|||||||
val users: List<User>
|
val users: List<User>
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO If this sticks around then change it to use HOCON ...
|
// TODO Store passwords as salted hashes
|
||||||
// TODO ... and also store passwords as salted hashes.
|
// TODO Or ditch this and consider something like Apache Shiro
|
||||||
// TODO Otherwise consider something like Apache Shiro
|
class RPCUserServiceImpl(config: Config) : RPCUserService {
|
||||||
class PropertiesFileRPCUserService(file: Path) : RPCUserService {
|
|
||||||
|
|
||||||
private val _users: Map<String, User>
|
private val _users: Map<String, User>
|
||||||
|
|
||||||
init {
|
init {
|
||||||
_users = if (file.exists()) {
|
_users = config.getListOrElse<Config>("rpcUsers") { emptyList() }
|
||||||
val properties = Properties()
|
.map {
|
||||||
file.read {
|
val username = it.getString("user")
|
||||||
properties.load(it)
|
require(username.matches("\\w+".toRegex())) { "Username $username contains invalid characters" }
|
||||||
}
|
val password = it.getString("password")
|
||||||
properties.map {
|
val permissions = it.getListOrElse<String>("permissions") { emptyList() }.map(String::toUpperCase).toSet()
|
||||||
val parts = it.value.toString().split(delimiters = ",")
|
User(username, password, permissions)
|
||||||
val username = it.key.toString()
|
}
|
||||||
require(!username.contains("""\.|\*|#""".toRegex())) { """Usernames cannot have the following characters: * . # """ }
|
.associateBy(User::username)
|
||||||
val password = parts[0]
|
|
||||||
val permissions = parts.drop(1).map(String::toUpperCase).toSet()
|
|
||||||
User(username, password, permissions)
|
|
||||||
}.associateBy(User::username)
|
|
||||||
} else {
|
|
||||||
emptyMap()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getUser(usename: String): User? = _users[usename]
|
override fun getUser(usename: String): User? = _users[usename]
|
||||||
|
@ -87,6 +87,15 @@ fun Config.getProperties(path: String): Properties {
|
|||||||
return props
|
return props
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Strictly for dev only automatically construct a server certificate/private key signed from
|
* Strictly for dev only automatically construct a server certificate/private key signed from
|
||||||
* the CA certs in Node resources. Then provision KeyStores into certificates folder under node path.
|
* the CA certs in Node resources. Then provision KeyStores into certificates folder under node path.
|
||||||
|
@ -24,7 +24,6 @@ interface NodeSSLConfiguration {
|
|||||||
interface NodeConfiguration : NodeSSLConfiguration {
|
interface NodeConfiguration : NodeSSLConfiguration {
|
||||||
val basedir: Path
|
val basedir: Path
|
||||||
override val certificatesPath: Path get() = basedir / "certificates"
|
override val certificatesPath: Path get() = basedir / "certificates"
|
||||||
val rpcUsersFile: Path get() = basedir / "rpc-users.properties"
|
|
||||||
val myLegalName: String
|
val myLegalName: String
|
||||||
val nearestCity: String
|
val nearestCity: String
|
||||||
val emailAddress: String
|
val emailAddress: String
|
||||||
|
@ -0,0 +1,75 @@
|
|||||||
|
package com.r3corda.node.services
|
||||||
|
|
||||||
|
import com.typesafe.config.ConfigFactory
|
||||||
|
import net.corda.node.services.RPCUserServiceImpl
|
||||||
|
import net.corda.node.services.User
|
||||||
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
|
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
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(ConfigFactory.parseString(configString))
|
||||||
|
}
|
||||||
|
}
|
@ -18,6 +18,7 @@ import net.corda.node.utilities.configureDatabase
|
|||||||
import net.corda.node.utilities.databaseTransaction
|
import net.corda.node.utilities.databaseTransaction
|
||||||
import net.corda.testing.freeLocalHostAndPort
|
import net.corda.testing.freeLocalHostAndPort
|
||||||
import net.corda.testing.node.makeTestDataSourceProperties
|
import net.corda.testing.node.makeTestDataSourceProperties
|
||||||
|
import com.typesafe.config.ConfigFactory
|
||||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||||
import org.jetbrains.exposed.sql.Database
|
import org.jetbrains.exposed.sql.Database
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
@ -59,7 +60,7 @@ class ArtemisMessagingTests {
|
|||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
userService = PropertiesFileRPCUserService(temporaryFolder.newFile().toPath())
|
userService = RPCUserServiceImpl(ConfigFactory.empty())
|
||||||
// TODO: create a base class that provides a default implementation
|
// TODO: create a base class that provides a default implementation
|
||||||
config = object : NodeConfiguration {
|
config = object : NodeConfiguration {
|
||||||
override val basedir: Path = temporaryFolder.newFolder().toPath()
|
override val basedir: Path = temporaryFolder.newFolder().toPath()
|
||||||
|
@ -1,81 +0,0 @@
|
|||||||
package net.corda.node.services
|
|
||||||
|
|
||||||
import com.google.common.jimfs.Configuration.unix
|
|
||||||
import com.google.common.jimfs.Jimfs
|
|
||||||
import net.corda.core.writeLines
|
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
|
||||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
|
||||||
import org.junit.After
|
|
||||||
import org.junit.Test
|
|
||||||
|
|
||||||
class PropertiesFileRPCUserServiceTest {
|
|
||||||
|
|
||||||
private val fileSystem = Jimfs.newFileSystem(unix())
|
|
||||||
private val file = fileSystem.getPath("users.properties")
|
|
||||||
|
|
||||||
@After
|
|
||||||
fun cleanUp() {
|
|
||||||
fileSystem.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `file doesn't exist`() {
|
|
||||||
val service = PropertiesFileRPCUserService(file)
|
|
||||||
assertThat(service.getUser("user")).isNull()
|
|
||||||
assertThat(service.users).isEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `empty file`() {
|
|
||||||
val service = loadWithContents()
|
|
||||||
assertThat(service.getUser("user")).isNull()
|
|
||||||
assertThat(service.users).isEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `no permissions`() {
|
|
||||||
val service = loadWithContents("user=password")
|
|
||||||
assertThat(service.getUser("user")).isEqualTo(User("user", "password", permissions = emptySet()))
|
|
||||||
assertThat(service.users).containsOnly(User("user", "password", permissions = emptySet()))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `single permission, which is in lower case`() {
|
|
||||||
val service = loadWithContents("user=password,cash")
|
|
||||||
assertThat(service.getUser("user")?.permissions).containsOnly("CASH")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `two permissions, which are upper case`() {
|
|
||||||
val service = loadWithContents("user=password,CASH,ADMIN")
|
|
||||||
assertThat(service.getUser("user")?.permissions).containsOnly("CASH", "ADMIN")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `two users`() {
|
|
||||||
val service = loadWithContents("user=password,ADMIN", "user2=password2")
|
|
||||||
assertThat(service.getUser("user")).isNotNull()
|
|
||||||
assertThat(service.getUser("user2")).isNotNull()
|
|
||||||
assertThat(service.users).containsOnly(
|
|
||||||
User("user", "password", permissions = setOf("ADMIN")),
|
|
||||||
User("user2", "password2", permissions = emptySet()))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `unknown user`() {
|
|
||||||
val service = loadWithContents("user=password")
|
|
||||||
assertThat(service.getUser("test")).isNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `Artemis special characters not permitted in usernames`() {
|
|
||||||
assertThatThrownBy { loadWithContents("user.name=password") }
|
|
||||||
assertThatThrownBy { loadWithContents("user*name=password") }
|
|
||||||
assertThatThrownBy { loadWithContents("user#name=password") }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun loadWithContents(vararg lines: String): PropertiesFileRPCUserService {
|
|
||||||
file.writeLines(lines.asList())
|
|
||||||
return PropertiesFileRPCUserService(file)
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user