mirror of
https://github.com/corda/corda.git
synced 2025-04-12 05:40:48 +00:00
[CORDA-827] Improved unit tests coverage and documentation (#2229)
* Extend unit test on RPCSecurityManager * Fix corner cases in permission parsing and bug in tryAuthenticate * Rework docsite page * Add missing ChangeLog entry
This commit is contained in:
parent
929341e7ee
commit
5720697b0d
@ -1,6 +1,9 @@
|
||||
package net.corda.client.rpc
|
||||
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.messaging.RPCOps
|
||||
import net.corda.node.services.Permissions
|
||||
import net.corda.node.services.messaging.rpcContext
|
||||
import net.corda.nodeapi.internal.config.User
|
||||
import net.corda.testing.node.internal.RPCDriverDSL
|
||||
|
@ -6,6 +6,8 @@ from the previous milestone release.
|
||||
|
||||
UNRELEASED
|
||||
----------
|
||||
* Support for external user credentials data source and password encryption [CORDA-827].
|
||||
|
||||
* Exporting additional JMX metrics (artemis, hibernate statistics) and loading Jolokia agent at JVM startup when using
|
||||
DriverDSL and/or cordformation node runner.
|
||||
|
||||
|
@ -26,7 +26,7 @@ permissions that RPC can use for fine-grain access control.
|
||||
|
||||
These users are added to the node's ``node.conf`` file.
|
||||
|
||||
The syntax for adding an RPC user is:
|
||||
The simplest way of adding an RPC user is to include it in the ``rpcUsers`` list:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
@ -59,9 +59,6 @@ Users need permissions to invoke any RPC call. By default, nothing is allowed. T
|
||||
...
|
||||
]
|
||||
|
||||
.. note:: Currently, the node's web server has super-user access, meaning that it can run any RPC operation without
|
||||
logging in. This will be changed in a future release.
|
||||
|
||||
Permissions Syntax
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
|
||||
@ -71,6 +68,136 @@ Fine grained permissions allow a user to invoke a specific RPC operation, or to
|
||||
- to invoke a RPC operation: ``InvokeRpc.<rpc method name>`` e.g., ``InvokeRpc.nodeInfo``.
|
||||
.. note:: Permission ``InvokeRpc.startFlow`` allows a user to initiate all flows.
|
||||
|
||||
RPC security management
|
||||
-----------------------
|
||||
|
||||
Hard coding user accounts in the ``rpcUsers`` field provides a quick way of allowing node's RPC to be accessed by a fixed
|
||||
set of authenticated users but has some obvious shortcomings. To support use cases aiming for higher security and flexibility,
|
||||
Corda RPC security system offers additional features such as:
|
||||
|
||||
* Fetching users credentials and permissions from external data source (e.g.: a remote RDBMS), with optional caching
|
||||
in node memory. In particular, this allows user credentials and permissions externally to be updated externally without
|
||||
requiring node's restart.
|
||||
* Password stored in hash-encrypted form. This is regarded as must-have when security is a concern. Corda currently supports
|
||||
a flexible password hash format conforming to the Modular Crypt Format and defined by the `Apache Shiro framework <https://shiro.apache.org/static/1.2.5/apidocs/org/apache/shiro/crypto/hash/format/Shiro1CryptFormat.html>`_
|
||||
|
||||
These features are controlled by a set of options nested in the ``security`` field of a node configuration.
|
||||
|
||||
.. warning:: The ``rpcUsers`` field is now deprecated in favour of the set the ``security`` config structure. A node
|
||||
configuration specifying both ``rpcUsers`` and ``security`` fields will trigger an exception during node startup.
|
||||
|
||||
The following example configuration points the node to a remote RDBMS storing hash-encrypted passwords and enable caching
|
||||
of user data in node's memory:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. sourcecode:: groovy
|
||||
|
||||
security = {
|
||||
authService = {
|
||||
dataSource = {
|
||||
type = "DB",
|
||||
passwordEncryption = "SHIRO_1_CRYPT",
|
||||
connection = {
|
||||
jdbcUrl = "<jdbc connection string>"
|
||||
username = "<db username>"
|
||||
password = "<db user password>"
|
||||
driverClassName = "<JDBC driver>"
|
||||
}
|
||||
}
|
||||
options = {
|
||||
cache = {
|
||||
expireAfterSecs = 120
|
||||
maxEntries = 10000
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Moreover, for practical reasons, we can still have an hard-coded static list of users embedded in the ``security``
|
||||
structure like in the old ``rpcUsers`` format, by specifying a ``dataSource`` of ``INMEMORY`` type:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. sourcecode:: groovy
|
||||
|
||||
security = {
|
||||
authService = {
|
||||
dataSource = {
|
||||
type = "INMEMORY",
|
||||
users = [
|
||||
{
|
||||
username = "<username>",
|
||||
password = "<password>",
|
||||
permissions = ["<permission 1>", "<permission 2>", ...]
|
||||
},
|
||||
...
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Authentication/authorisation data
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
The ``dataSource`` field defines the data provider supplying credentials and permissions for users. It currently exists
|
||||
in two forms, identified by the subfield ``type``:
|
||||
|
||||
:INMEMORY: A list of user credentials and permissions hard-coded in configuration in the ``users`` field (see example above)
|
||||
|
||||
:DB: An external RDBMS accessed via the JDBC connection described by ``connection``. The current implementation
|
||||
expect the database to store data according to the following schema:
|
||||
|
||||
- Table ``users`` containing columns ``username`` and ``password``. The ``username`` column *must have unique values*.
|
||||
- Table ``user_roles`` containing columns ``username`` and ``role_name`` associating a user to a set of *roles*
|
||||
- Table ``roles_permissions`` containing columns ``role_name`` and ``permission`` associating a role to a set of
|
||||
permission strings
|
||||
|
||||
Unlike the ``INMEMORY`` case, in the user database permissions are assigned to *roles* rather than individual users.
|
||||
|
||||
.. note:: There is no prescription on the SQL type of the columns (although our tests were conducted on ``username`` and
|
||||
``role_name`` declared of SQL type ``VARCHAR`` and ``password`` of ``TEXT`` type). It is also possible to have extra columns
|
||||
in each table alongside the expected ones.
|
||||
|
||||
Password encryption
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Storing passwords in plain text is discouraged in production environment where security is critical. Passwords are assumed
|
||||
to be in plain format by default, unless a different format is specified ny the ``passwordEncryption`` field, like:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. sourcecode:: groovy
|
||||
|
||||
passwordEncryption = SHIRO_1_CRYPT
|
||||
|
||||
``SHIRO_1_CRYPT`` identifies the `Apache Shiro fully reversible
|
||||
Modular Crypt Format <https://shiro.apache.org/static/1.2.5/apidocs/org/apache/shiro/crypto/hash/format/Shiro1CryptFormat.html>`_,
|
||||
currently the only non-plain password hash-encryption format supported by Corda. Passwords can be hash-encrypted in this
|
||||
format using the `Apache Shiro Hasher command line tool <https://shiro.apache.org/command-line-hasher.html>`_.
|
||||
|
||||
Caching users data
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
|
||||
A cache layer on top of the external data source of users credentials and permissions can significantly benefit
|
||||
performances in some cases, with the disadvantage of causing a (controllable) delay in picking up updates to the underlying data.
|
||||
Caching is disabled by default, it can be enabled by defining the ``options.cache`` field in ``security.authService``,
|
||||
for example:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. sourcecode:: groovy
|
||||
|
||||
options = {
|
||||
cache = {
|
||||
expireAfterSecs = 120
|
||||
maxEntries = 10000
|
||||
}
|
||||
}
|
||||
|
||||
This will enable a non-persistent cache contained in the node's memory with maximum number of entries set to ``maxEntries``
|
||||
with entries expiring and refreshed after ``expireAfterSecs`` number of seconds.
|
||||
|
||||
Observables
|
||||
-----------
|
||||
The RPC system handles observables in a special way. When a method returns an observable, whether directly or
|
||||
|
@ -90,6 +90,9 @@ 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.
|
||||
|
||||
:security: Contains various nested fields controlling user authentication/authorization, in particular for RPC accesses. See
|
||||
:doc:`clientrpc` for details.
|
||||
|
||||
: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
|
||||
|
@ -10,7 +10,6 @@ Corda nodes
|
||||
corda-configuration-file
|
||||
clientrpc
|
||||
shell
|
||||
node-auth-config
|
||||
node-database
|
||||
node-administration
|
||||
out-of-process-verification
|
@ -1,136 +0,0 @@
|
||||
Access security settings
|
||||
========================
|
||||
|
||||
Access to node functionalities via SSH or RPC is protected by an authentication and authorisation policy.
|
||||
|
||||
The field ``security`` in ``node.conf`` exposes various sub-fields related to authentication/authorisation specifying:
|
||||
|
||||
* The data source providing credentials and permissions for users (e.g.: a remote RDBMS)
|
||||
* An optional password encryption method.
|
||||
* An optional caching of users data from Node side.
|
||||
|
||||
.. warning:: Specifying both ``rpcUsers`` and ``security`` fields in ``node.conf`` is considered an illegal setting and
|
||||
rejected by the node at startup since ``rpcUsers`` is effectively deprecated in favour of ``security.authService``.
|
||||
|
||||
**Example 1:** connect to remote RDBMS for credentials/permissions, with encrypted user passwords and
|
||||
caching on node-side:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. sourcecode:: groovy
|
||||
|
||||
security = {
|
||||
authService = {
|
||||
dataSource = {
|
||||
type = "DB",
|
||||
passwordEncryption = "SHIRO_1_CRYPT",
|
||||
connection = {
|
||||
jdbcUrl = "<jdbc connection string>"
|
||||
username = "<db username>"
|
||||
password = "<db user password>"
|
||||
driverClassName = "<JDBC driver>"
|
||||
}
|
||||
}
|
||||
options = {
|
||||
cache = {
|
||||
expiryTimeSecs = 120
|
||||
capacity = 10000
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
**Example 2:** list of user credentials and permissions hard-coded in ``node.conf``
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. sourcecode:: groovy
|
||||
|
||||
security = {
|
||||
authService = {
|
||||
dataSource = {
|
||||
type = "INMEMORY",
|
||||
users =[
|
||||
{
|
||||
username = "user1"
|
||||
password = "password"
|
||||
permissions = [
|
||||
"StartFlow.net.corda.flows.ExampleFlow1",
|
||||
"StartFlow.net.corda.flows.ExampleFlow2",
|
||||
...
|
||||
]
|
||||
},
|
||||
...
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Let us look in more details at the structure of ``security.authService``:
|
||||
|
||||
Authentication/authorisation data
|
||||
---------------------------------
|
||||
|
||||
The ``dataSource`` field defines the data provider supplying credentials and permissions for users. The ``type``
|
||||
subfield identify the type of data provider, currently supported one are:
|
||||
|
||||
* **INMEMORY:** a list of user credentials and permissions hard-coded in configuration in the ``users`` field
|
||||
(see example 2 above)
|
||||
|
||||
* **DB:** An external RDBMS accessed via the JDBC connection described by ``connection``. The current implementation
|
||||
expect the database to store data according to the following schema:
|
||||
|
||||
- Table ``users`` containing columns ``username`` and ``password``.
|
||||
The ``username`` column *must have unique values*.
|
||||
- Table ``user_roles`` containing columns ``username`` and ``role_name`` associating a user to a set of *roles*
|
||||
- Table ``roles_permissions`` containing columns ``role_name`` and ``permission`` associating a role to a set of
|
||||
permission strings
|
||||
|
||||
Note in particular how in the DB case permissions are assigned to _roles_ rather than individual users.
|
||||
Also, there is no prescription on the SQL type of the columns (although in our tests we defined ``username`` and
|
||||
``role_name`` of SQL type ``VARCHAR`` and ``password`` of ``TEXT`` type) and it is allowed to put additional columns
|
||||
besides the one expected by the implementation.
|
||||
|
||||
Password encryption
|
||||
-------------------
|
||||
|
||||
Storing passwords in plain text is discouraged in production systems aiming for high security requirements. We support
|
||||
reading passwords stored using the Apache Shiro fully reversible Modular Crypt Format, specified in the documentation
|
||||
of ``org.apache.shiro.crypto.hash.format.Shiro1CryptFormat``.
|
||||
|
||||
Password are assumed in plain format by default. To specify an encryption it is necessary to use the field:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. sourcecode:: groovy
|
||||
|
||||
passwordEncryption = SHIRO_1_CRYPT
|
||||
|
||||
Hash encrypted password based on the Shiro1CryptFormat can be produced with the `Apache Shiro Hasher tool <https://shiro.apache.org/command-line-hasher.html>`_
|
||||
|
||||
Cache
|
||||
-----
|
||||
|
||||
Adding a cache layer on top of an external provider of users credentials and permissions can significantly benefit
|
||||
performances in some cases, with the disadvantage of introducing a latency in the propagation of changes to the data.
|
||||
|
||||
Caching of users data is disabled by default, it can be enabled by defining the ``options.cache`` field, like seen in
|
||||
the examples above:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. sourcecode:: groovy
|
||||
|
||||
options = {
|
||||
cache = {
|
||||
expiryTimeSecs = 120
|
||||
capacity = 10000
|
||||
}
|
||||
}
|
||||
|
||||
This will enable an in-memory cache with maximum capacity (number of entries) and maximum life time of entries given by
|
||||
respectively the values set by the ``capacity`` and ``expiryTimeSecs`` fields.
|
||||
|
||||
|
||||
|
||||
|
@ -34,7 +34,7 @@ fun RPCSecurityManager.tryAuthenticate(principal: String, password: Password): A
|
||||
password.use {
|
||||
return try {
|
||||
authenticate(principal, password)
|
||||
} catch (e: AuthenticationException) {
|
||||
} catch (e: FailedLoginException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
@ -95,8 +95,8 @@ class RPCSecurityManagerImpl(config: AuthServiceConfig) : RPCSecurityManager {
|
||||
// Setup optional cache layer if configured
|
||||
it.cacheManager = config.options?.cache?.let {
|
||||
GuavaCacheManager(
|
||||
timeToLiveSeconds = it.expiryTimeInSecs,
|
||||
maxSize = it.capacity)
|
||||
timeToLiveSeconds = it.expireAfterSecs,
|
||||
maxSize = it.maxEntries)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -149,22 +149,29 @@ private object RPCPermissionResolver : PermissionResolver {
|
||||
private val ACTION_START_FLOW = "startflow"
|
||||
private val ACTION_INVOKE_RPC = "invokerpc"
|
||||
private val ACTION_ALL = "all"
|
||||
|
||||
private val FLOW_RPC_CALLS = setOf("startFlowDynamic", "startTrackedFlowDynamic")
|
||||
private val FLOW_RPC_CALLS = setOf(
|
||||
"startFlowDynamic",
|
||||
"startTrackedFlowDynamic",
|
||||
"startFlow",
|
||||
"startTrackedFlow")
|
||||
|
||||
override fun resolvePermission(representation: String): Permission {
|
||||
|
||||
val action = representation.substringBefore(SEPARATOR).toLowerCase()
|
||||
val action = representation.substringBefore(SEPARATOR).toLowerCase()
|
||||
when (action) {
|
||||
ACTION_INVOKE_RPC -> {
|
||||
val rpcCall = representation.substringAfter(SEPARATOR)
|
||||
require(representation.count { it == SEPARATOR } == 1) {
|
||||
val rpcCall = representation.substringAfter(SEPARATOR, "")
|
||||
require(representation.count { it == SEPARATOR } == 1 && !rpcCall.isEmpty()) {
|
||||
"Malformed permission string"
|
||||
}
|
||||
return RPCPermission(setOf(rpcCall))
|
||||
val permitted = when(rpcCall) {
|
||||
"startFlow" -> setOf("startFlowDynamic", rpcCall)
|
||||
"startTrackedFlow" -> setOf("startTrackedFlowDynamic", rpcCall)
|
||||
else -> setOf(rpcCall)
|
||||
}
|
||||
return RPCPermission(permitted)
|
||||
}
|
||||
ACTION_START_FLOW -> {
|
||||
val targetFlow = representation.substringAfter(SEPARATOR)
|
||||
val targetFlow = representation.substringAfter(SEPARATOR, "")
|
||||
require(targetFlow.isNotEmpty()) {
|
||||
"Missing target flow after StartFlow"
|
||||
}
|
||||
|
@ -196,7 +196,7 @@ data class SecurityConfiguration(val authService: SecurityConfiguration.AuthServ
|
||||
data class Options(val cache: Options.Cache?) {
|
||||
|
||||
// Cache parameters
|
||||
data class Cache(val expiryTimeInSecs: Long, val capacity: Long)
|
||||
data class Cache(val expireAfterSecs: Long, val maxEntries: Long)
|
||||
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,19 @@
|
||||
package net.corda.node.services
|
||||
|
||||
import net.corda.core.context.AuthServiceId
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.node.internal.security.Password
|
||||
import net.corda.node.internal.security.RPCSecurityManagerImpl
|
||||
import net.corda.node.internal.security.tryAuthenticate
|
||||
import net.corda.nodeapi.internal.config.User
|
||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||
import org.junit.Test
|
||||
import javax.security.auth.login.FailedLoginException
|
||||
import kotlin.reflect.KFunction
|
||||
import kotlin.test.assertFails
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertNull
|
||||
|
||||
class RPCSecurityManagerTest {
|
||||
|
||||
@ -15,7 +24,147 @@ class RPCSecurityManagerTest {
|
||||
assertThatThrownBy { configWithRPCUsername("user#1") }.hasMessageContaining("#")
|
||||
}
|
||||
|
||||
private fun configWithRPCUsername(username: String) {
|
||||
RPCSecurityManagerImpl.fromUserList(users = listOf(User(username, "password", setOf())), id = AuthServiceId("TEST"))
|
||||
@Test
|
||||
fun `Generic RPC call authorization`() {
|
||||
checkUserPermissions(
|
||||
permitted = setOf(arrayListOf("nodeInfo"), arrayListOf("notaryIdentities")),
|
||||
permissions = setOf(
|
||||
Permissions.invokeRpc(CordaRPCOps::nodeInfo),
|
||||
Permissions.invokeRpc(CordaRPCOps::notaryIdentities)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Flow invocation authorization`() {
|
||||
checkUserPermissions(
|
||||
permissions = setOf(Permissions.startFlow<DummyFlow>()),
|
||||
permitted = setOf(
|
||||
arrayListOf("startTrackedFlowDynamic", "net.corda.node.services.RPCSecurityManagerTest\$DummyFlow"),
|
||||
arrayListOf("startFlowDynamic", "net.corda.node.services.RPCSecurityManagerTest\$DummyFlow")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Check startFlow RPC permission implies startFlowDynamic`() {
|
||||
checkUserPermissions(
|
||||
permissions = setOf(Permissions.invokeRpc("startFlow")),
|
||||
permitted = setOf(arrayListOf("startFlow"), arrayListOf("startFlowDynamic")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Check startTrackedFlow RPC permission implies startTrackedFlowDynamic`() {
|
||||
checkUserPermissions(
|
||||
permitted = setOf(arrayListOf("startTrackedFlow"), arrayListOf("startTrackedFlowDynamic")),
|
||||
permissions = setOf(Permissions.invokeRpc("startTrackedFlow")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Admin authorization`() {
|
||||
checkUserPermissions(
|
||||
permissions = setOf("all"),
|
||||
permitted = allActions.map { arrayListOf(it) }.toSet())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Malformed permission strings`() {
|
||||
assertMalformedPermission("bar")
|
||||
assertMalformedPermission("InvokeRpc.nodeInfo.XXX")
|
||||
assertMalformedPermission("")
|
||||
assertMalformedPermission(".")
|
||||
assertMalformedPermission("..")
|
||||
assertMalformedPermission("startFlow")
|
||||
assertMalformedPermission("startFlow.")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Login with unknown user`() {
|
||||
val userRealm = RPCSecurityManagerImpl.fromUserList(
|
||||
users = listOf(User("user", "xxxx", emptySet())),
|
||||
id = AuthServiceId("TEST"))
|
||||
userRealm.authenticate("user", Password("xxxx"))
|
||||
assertFailsWith(FailedLoginException::class, "Login with wrong password should fail") {
|
||||
userRealm.authenticate("foo", Password("xxxx"))
|
||||
}
|
||||
assertNull(userRealm.tryAuthenticate("foo", Password("wrong")),
|
||||
"Login with wrong password should fail")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Login with wrong credentials`() {
|
||||
val userRealm = RPCSecurityManagerImpl.fromUserList(
|
||||
users = listOf(User("user", "password", emptySet())),
|
||||
id = AuthServiceId("TEST"))
|
||||
userRealm.authenticate("user", Password("password"))
|
||||
assertFailsWith(FailedLoginException::class, "Login with wrong password should fail") {
|
||||
userRealm.authenticate("user", Password("wrong"))
|
||||
}
|
||||
assertNull(userRealm.tryAuthenticate("user", Password("wrong")),
|
||||
"Login with wrong password should fail")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Build invalid subject`() {
|
||||
val userRealm = RPCSecurityManagerImpl.fromUserList(
|
||||
users = listOf(User("user", "password", emptySet())),
|
||||
id = AuthServiceId("TEST"))
|
||||
val subject = userRealm.buildSubject("foo")
|
||||
for (action in allActions) {
|
||||
assert(!subject.isPermitted(action)) {
|
||||
"Invalid subject should not be allowed to call $action"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun configWithRPCUsername(username: String) {
|
||||
RPCSecurityManagerImpl.fromUserList(
|
||||
users = listOf(User(username, "password", setOf())), id = AuthServiceId("TEST"))
|
||||
}
|
||||
|
||||
private fun checkUserPermissions(permissions: Set<String>, permitted: Set<ArrayList<String>>) {
|
||||
val user = User(username = "user", password = "password", permissions = permissions)
|
||||
val userRealms = RPCSecurityManagerImpl.fromUserList(users = listOf(user), id = AuthServiceId("TEST"))
|
||||
val disabled = allActions.filter { !permitted.contains(listOf(it)) }
|
||||
for (subject in listOf(
|
||||
userRealms.authenticate("user", Password("password")),
|
||||
userRealms.tryAuthenticate("user", Password("password"))!!,
|
||||
userRealms.buildSubject("user"))) {
|
||||
for (request in permitted) {
|
||||
val call = request.first()
|
||||
val args = request.drop(1).toTypedArray()
|
||||
assert(subject.isPermitted(request.first(), *args)) {
|
||||
"User ${subject.principal} should be permitted ${call} with target '${request.toList()}'"
|
||||
}
|
||||
if (args.isEmpty()) {
|
||||
assert(subject.isPermitted(request.first(), "XXX")) {
|
||||
"User ${subject.principal} should be permitted ${call} with any target"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
disabled.forEach {
|
||||
assert(!subject.isPermitted(it)) {
|
||||
"Permissions $permissions should not allow to call $it"
|
||||
}
|
||||
}
|
||||
|
||||
disabled.filter { !permitted.contains(listOf(it, "foo")) }.forEach {
|
||||
assert(!subject.isPermitted(it, "foo")) {
|
||||
"Permissions $permissions should not allow to call $it with argument 'foo'"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun assertMalformedPermission(permission: String) {
|
||||
assertFails {
|
||||
RPCSecurityManagerImpl.fromUserList(
|
||||
users = listOf(User("x", "x", setOf(permission))),
|
||||
id = AuthServiceId("TEST"))
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val allActions = CordaRPCOps::class.members.filterIsInstance<KFunction<*>>().map { it.name }.toSet() +
|
||||
setOf("startFlow", "startTrackedFlow")
|
||||
}
|
||||
|
||||
private abstract class DummyFlow : FlowLogic<Unit>()
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user