Moved the RPC user config out of the properties file and into the main config file

This commit is contained in:
Shams Asari 2016-11-11 15:59:37 +00:00
parent f4925c0fa9
commit c326a9ae46
12 changed files with 137 additions and 152 deletions

12
.idea/modules.xml generated
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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