Configurable authorization/authentication data sources [CORDA-827] (#2145)

* Add support for external data source of access control data (RPC/Shell users credential and permissions), with optional in-memory caching.
* Support password encoded with Apache Shiro fully reversible Modular Crypt Format.
* Introduce 'security' field in Node configuration and related docsite page.
This commit is contained in:
igor nitto 2017-12-11 08:39:09 +00:00 committed by GitHub
parent 991c59e753
commit da38e6f673
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 1235 additions and 212 deletions

View File

@ -49,6 +49,7 @@ buildscript {
ext.beanutils_version = '1.9.3'
ext.crash_version = 'cce5a00f114343c1145c1d7756e1dd6df3ea984e'
ext.jsr305_version = constants.getProperty("jsr305Version")
ext.shiro_version = '1.4.0'
ext.artifactory_plugin_version = constants.getProperty('artifactoryPluginVersion')
// Update 121 is required for ObjectInputFilter and at time of writing 131 was latest:

View File

@ -63,8 +63,8 @@ class RPCStabilityTests {
val executor = Executors.newScheduledThreadPool(1)
fun startAndStop() {
rpcDriver {
val server = startRpcServer<RPCOps>(ops = DummyOps)
startRpcClient<RPCOps>(server.get().broker.hostAndPort!!).get()
val server = startRpcServer<RPCOps>(ops = DummyOps).get()
startRpcClient<RPCOps>(server.broker.hostAndPort!!).get()
}
}
repeat(5) {

View File

@ -22,7 +22,10 @@ import net.corda.core.internal.ThreadBox
import net.corda.core.messaging.RPCOps
import net.corda.core.serialization.SerializationContext
import net.corda.core.serialization.serialize
import net.corda.core.utilities.*
import net.corda.core.utilities.Try
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.debug
import net.corda.core.utilities.getOrThrow
import net.corda.nodeapi.ArtemisConsumer
import net.corda.nodeapi.ArtemisProducer
import net.corda.nodeapi.RPCApi

View File

@ -1,8 +1,6 @@
package net.corda.client.rpc
import net.corda.core.messaging.CordaRPCOps
import net.corda.core.messaging.RPCOps
import net.corda.node.services.Permissions.Companion.invokeRpc
import net.corda.node.services.messaging.rpcContext
import net.corda.nodeapi.internal.config.User
import net.corda.testing.internal.RPCDriverDSL
@ -10,15 +8,12 @@ import net.corda.testing.internal.rpcDriver
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import kotlin.reflect.KVisibility
import kotlin.reflect.full.declaredMemberFunctions
import kotlin.test.assertFailsWith
@RunWith(Parameterized::class)
class RPCPermissionsTests : AbstractRPCTest() {
companion object {
const val DUMMY_FLOW = "StartFlow.net.corda.flows.DummyFlow"
const val OTHER_FLOW = "StartFlow.net.corda.flows.OtherFlow"
const val ALL_ALLOWED = "ALL"
}
@ -26,12 +21,21 @@ class RPCPermissionsTests : AbstractRPCTest() {
* RPC operation.
*/
interface TestOps : RPCOps {
fun validatePermission(str: String)
fun validatePermission(method: String, target: String? = null)
}
class TestOpsImpl : TestOps {
override val protocolVersion = 1
override fun validatePermission(str: String) { rpcContext().requirePermission(str) }
override fun validatePermission(method: String, target: String?) {
val authorized = if (target == null) {
rpcContext().isPermitted(method)
} else {
rpcContext().isPermitted(method, target)
}
if (!authorized) {
throw PermissionException("RPC user not authorized")
}
}
}
/**
@ -46,9 +50,9 @@ class RPCPermissionsTests : AbstractRPCTest() {
rpcDriver {
val emptyUser = userOf("empty", emptySet())
val proxy = testProxyFor(emptyUser)
assertFailsWith(PermissionException::class,
"User ${emptyUser.username} should not be allowed to use $DUMMY_FLOW.",
{ proxy.validatePermission(DUMMY_FLOW) })
assertNotAllowed {
proxy.validatePermission("startFlowDynamic", "net.corda.flows.DummyFlow")
}
}
}
@ -57,7 +61,8 @@ class RPCPermissionsTests : AbstractRPCTest() {
rpcDriver {
val adminUser = userOf("admin", setOf(ALL_ALLOWED))
val proxy = testProxyFor(adminUser)
proxy.validatePermission(DUMMY_FLOW)
proxy.validatePermission("startFlowDynamic", "net.corda.flows.DummyFlow")
proxy.validatePermission("startTrackedFlowDynamic", "net.corda.flows.DummyFlow")
}
}
@ -66,7 +71,8 @@ class RPCPermissionsTests : AbstractRPCTest() {
rpcDriver {
val joeUser = userOf("joe", setOf(DUMMY_FLOW))
val proxy = testProxyFor(joeUser)
proxy.validatePermission(DUMMY_FLOW)
proxy.validatePermission("startFlowDynamic", "net.corda.flows.DummyFlow")
proxy.validatePermission("startTrackedFlowDynamic", "net.corda.flows.DummyFlow")
}
}
@ -75,36 +81,46 @@ class RPCPermissionsTests : AbstractRPCTest() {
rpcDriver {
val joeUser = userOf("joe", setOf(DUMMY_FLOW))
val proxy = testProxyFor(joeUser)
assertFailsWith(PermissionException::class,
"User ${joeUser.username} should not be allowed to use $OTHER_FLOW",
{ proxy.validatePermission(OTHER_FLOW) })
}
}
@Test
fun `check ALL is implemented the correct way round`() {
rpcDriver {
val joeUser = userOf("joe", setOf(DUMMY_FLOW))
val proxy = testProxyFor(joeUser)
assertFailsWith(PermissionException::class,
"Permission $ALL_ALLOWED should not do anything for User ${joeUser.username}",
{ proxy.validatePermission(ALL_ALLOWED) })
}
}
@Test
fun `fine grained permissions are enforced`() {
val allPermissions = CordaRPCOps::class.declaredMemberFunctions.filter { it.visibility == KVisibility.PUBLIC }.map { invokeRpc(it) }
allPermissions.forEach { permission ->
rpcDriver {
val user = userOf("Mark", setOf(permission))
val proxy = testProxyFor(user)
proxy.validatePermission(permission)
(allPermissions - permission).forEach { notOwnedPermission ->
assertFailsWith(PermissionException::class, { proxy.validatePermission(notOwnedPermission) })
}
assertNotAllowed {
proxy.validatePermission("startFlowDynamic", "net.corda.flows.OtherFlow")
}
assertNotAllowed {
proxy.validatePermission("startTrackedFlowDynamic", "net.corda.flows.OtherFlow")
}
}
}
@Test
fun `joe user is not allowed to call other RPC methods`() {
rpcDriver {
val joeUser = userOf("joe", setOf(DUMMY_FLOW))
val proxy = testProxyFor(joeUser)
assertNotAllowed {
proxy.validatePermission("nodeInfo")
}
assertNotAllowed {
proxy.validatePermission("networkMapFeed")
}
}
}
@Test
fun `checking invokeRpc permissions entitlements`() {
rpcDriver {
val joeUser = userOf("joe", setOf("InvokeRpc.networkMapFeed"))
val proxy = testProxyFor(joeUser)
assertNotAllowed {
proxy.validatePermission("nodeInfo")
}
assertNotAllowed {
proxy.validatePermission("startTrackedFlowDynamic", "net.corda.flows.OtherFlow")
}
proxy.validatePermission("networkMapFeed")
}
}
private fun assertNotAllowed(action: () -> Unit) {
assertFailsWith(PermissionException::class, "User should not be allowed to perform this action.", action)
}
}

View File

@ -10,6 +10,7 @@ Corda nodes
corda-configuration-file
clientrpc
shell
node-auth-config
node-database
node-administration
out-of-process-verification

View File

@ -0,0 +1,136 @@
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.

View File

@ -80,6 +80,7 @@ private fun Config.getSingleValue(path: String, type: KType): Any? {
URL::class -> URL(getString(path))
CordaX500Name::class -> CordaX500Name.parse(getString(path))
Properties::class -> getConfig(path).toProperties()
Config::class -> getConfig(path)
else -> if (typeClass.java.isEnum) {
parseEnum(typeClass.java, getString(path))
} else {

View File

@ -162,6 +162,9 @@ dependencies {
// FastClasspathScanner: classpath scanning
compile 'io.github.lukehutch:fast-classpath-scanner:2.0.21'
// Apache Shiro: authentication, authorization and session management.
compile "org.apache.shiro:shiro-core:${shiro_version}"
// Integration test helpers
integrationTestCompile "junit:junit:$junit_version"
integrationTestCompile "org.assertj:assertj-core:${assertj_version}"

View File

@ -20,6 +20,7 @@ import kotlin.test.assertTrue
import kotlin.test.fail
import org.assertj.core.api.Assertions.assertThat
import java.util.regex.Pattern
import kotlin.reflect.jvm.jvmName
class SSHServerTest {
@ -113,7 +114,7 @@ class SSHServerTest {
channel.disconnect()
session.disconnect()
assertThat(response).matches("(?s)User not permissioned with any of \\[[^]]*${flowNameEscaped}.*")
assertThat(response).matches("(?s)User not authorized to perform RPC call .*")
}
}

View File

@ -0,0 +1,303 @@
package net.corda.node.services
import co.paralleluniverse.fibers.Suspendable
import net.corda.client.rpc.CordaRPCClient
import net.corda.client.rpc.PermissionException
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.InitiatingFlow
import net.corda.core.flows.StartableByRPC
import net.corda.core.messaging.CordaRPCOps
import net.corda.core.messaging.startFlow
import net.corda.finance.flows.CashIssueFlow
import net.corda.node.internal.Node
import net.corda.node.internal.StartedNode
import net.corda.node.services.config.PasswordEncryption
import net.corda.node.services.config.SecurityConfiguration
import net.corda.node.services.config.AuthDataSourceType
import net.corda.nodeapi.internal.config.User
import net.corda.nodeapi.internal.config.toConfig
import net.corda.testing.internal.NodeBasedTest
import net.corda.testing.*
import org.apache.activemq.artemis.api.core.ActiveMQSecurityException
import org.junit.After
import org.junit.Before
import org.junit.Test
import java.sql.DriverManager
import java.sql.Statement
import java.util.*
import kotlin.test.assertFailsWith
abstract class UserAuthServiceTest : NodeBasedTest() {
protected lateinit var node: StartedNode<Node>
protected lateinit var client: CordaRPCClient
@Test
fun `login with correct credentials`() {
client.start("user", "foo")
}
@Test
fun `login with wrong credentials`() {
client.start("user", "foo")
assertFailsWith(
ActiveMQSecurityException::class,
"Login with incorrect password should fail") {
client.start("user", "bar")
}
assertFailsWith(
ActiveMQSecurityException::class,
"Login with unknown username should fail") {
client.start("X", "foo")
}
}
@Test
fun `check flow permissions are respected`() {
client.start("user", "foo").use {
val proxy = it.proxy
proxy.startFlowDynamic(DummyFlow::class.java)
proxy.startTrackedFlowDynamic(DummyFlow::class.java)
proxy.startFlow(::DummyFlow)
assertFailsWith(
PermissionException::class,
"This user should not be authorized to start flow `CashIssueFlow`") {
proxy.startFlowDynamic(CashIssueFlow::class.java)
}
assertFailsWith(
PermissionException::class,
"This user should not be authorized to start flow `CashIssueFlow`") {
proxy.startTrackedFlowDynamic(CashIssueFlow::class.java)
}
}
}
@Test
fun `check permissions on RPC calls are respected`() {
client.start("user", "foo").use {
val proxy = it.proxy
proxy.stateMachinesFeed()
assertFailsWith(
PermissionException::class,
"This user should not be authorized to call 'nodeInfo'") {
proxy.nodeInfo()
}
}
}
@StartableByRPC
@InitiatingFlow
class DummyFlow : FlowLogic<Unit>() {
@Suspendable
override fun call() = Unit
}
}
class UserAuthServiceEmbedded : UserAuthServiceTest() {
private val rpcUser = User("user", "foo", permissions = setOf(
Permissions.startFlow<DummyFlow>(),
Permissions.invokeRpc("vaultQueryBy"),
Permissions.invokeRpc(CordaRPCOps::stateMachinesFeed),
Permissions.invokeRpc("vaultQueryByCriteria")))
@Before
fun setup() {
val securityConfig = SecurityConfiguration(
authService = SecurityConfiguration.AuthService.fromUsers(listOf(rpcUser)))
val configOverrides = mapOf("security" to securityConfig.toConfig().root().unwrapped())
node = startNode(ALICE_NAME, rpcUsers = emptyList(), configOverrides = configOverrides)
client = CordaRPCClient(node.internals.configuration.rpcAddress!!)
}
}
class UserAuthServiceTestsJDBC : UserAuthServiceTest() {
private val db = UsersDB(
name = "SecurityDataSourceTestDB",
users = listOf(UserAndRoles(username = "user",
password = "foo",
roles = listOf("default"))),
roleAndPermissions = listOf(
RoleAndPermissions(
role = "default",
permissions = listOf(
Permissions.startFlow<DummyFlow>(),
Permissions.invokeRpc("vaultQueryBy"),
Permissions.invokeRpc(CordaRPCOps::stateMachinesFeed),
Permissions.invokeRpc("vaultQueryByCriteria"))),
RoleAndPermissions(
role = "admin",
permissions = listOf("ALL")
)))
@Before
fun setup() {
val securityConfig = SecurityConfiguration(
authService = SecurityConfiguration.AuthService(
dataSource = SecurityConfiguration.AuthService.DataSource(
type = AuthDataSourceType.DB,
passwordEncryption = PasswordEncryption.NONE,
connection = Properties().apply {
setProperty("jdbcUrl", db.jdbcUrl)
setProperty("username", "")
setProperty("password", "")
setProperty("driverClassName", "org.h2.Driver")
}
)
)
)
val configOverrides = mapOf("security" to securityConfig.toConfig().root().unwrapped())
node = startNode(ALICE_NAME, rpcUsers = emptyList(), configOverrides = configOverrides)
client = CordaRPCClient(node.internals.configuration.rpcAddress!!)
}
@Test
fun `Add new users on-the-fly`() {
assertFailsWith(
ActiveMQSecurityException::class,
"Login with incorrect password should fail") {
client.start("user2", "bar")
}
db.insert(UserAndRoles(
username = "user2",
password = "bar",
roles = listOf("default")))
client.start("user2", "bar")
}
@Test
fun `Modify user permissions during RPC session`() {
db.insert(UserAndRoles(
username = "user3",
password = "bar",
roles = emptyList()))
client.start("user3", "bar").use {
val proxy = it.proxy
assertFailsWith(
PermissionException::class,
"This user should not be authorized to call 'nodeInfo'") {
proxy.stateMachinesFeed()
}
db.addRoleToUser("user3", "default")
proxy.stateMachinesFeed()
}
}
@Test
fun `Revoke user permissions during RPC session`() {
db.insert(UserAndRoles(
username = "user4",
password = "test",
roles = listOf("default")))
client.start("user4", "test").use {
val proxy = it.proxy
proxy.stateMachinesFeed()
db.deleteUser("user4")
assertFailsWith(
PermissionException::class,
"This user should not be authorized to call 'nodeInfo'") {
proxy.stateMachinesFeed()
}
}
}
@After
fun tearDown() {
db.close()
}
}
private data class UserAndRoles(val username: String, val password: String, val roles: List<String>)
private data class RoleAndPermissions(val role: String, val permissions: List<String>)
private class UsersDB : AutoCloseable {
val jdbcUrl: String
companion object {
val DB_CREATE_SCHEMA = """
CREATE TABLE users (username VARCHAR(256), password TEXT);
CREATE TABLE user_roles (username VARCHAR(256), role_name VARCHAR(256));
CREATE TABLE roles_permissions (role_name VARCHAR(256), permission TEXT);
"""
}
fun insert(user: UserAndRoles) {
session {
it.execute("INSERT INTO users VALUES ('${user.username}', '${user.password}')")
for (role in user.roles) {
it.execute("INSERT INTO user_roles VALUES ('${user.username}', '${role}')")
}
}
}
fun insert(roleAndPermissions: RoleAndPermissions) {
val (role, permissions) = roleAndPermissions
session {
for (permission in permissions) {
it.execute("INSERT INTO roles_permissions VALUES ('$role', '$permission')")
}
}
}
fun addRoleToUser(username: String, role: String) {
session {
it.execute("INSERT INTO user_roles VALUES ('$username', '$role')")
}
}
fun deleteRole(role: String) {
session {
it.execute("DELETE FROM role_permissions WHERE role_name = '$role'")
}
}
fun deleteUser(username: String) {
session {
it.execute("DELETE FROM users WHERE username = '$username'")
it.execute("DELETE FROM user_roles WHERE username = '$username'")
}
}
inline private fun session(statement: (Statement) -> Unit) {
DriverManager.getConnection(jdbcUrl).use {
it.autoCommit = false
it.createStatement().use(statement)
it.commit()
}
}
constructor(name: String,
users: List<UserAndRoles> = emptyList(),
roleAndPermissions: List<RoleAndPermissions> = emptyList()) {
jdbcUrl = "jdbc:h2:mem:${name};DB_CLOSE_DELAY=-1"
session {
it.execute(DB_CREATE_SCHEMA)
}
require(users.map { it.username }.toSet().size == users.size) {
"Duplicate username in input"
}
users.forEach { insert(it) }
roleAndPermissions.forEach { insert(it) }
}
override fun close() {
DriverManager.getConnection(jdbcUrl).use {
it.createStatement().use {
it.execute("DROP ALL OBJECTS")
}
}
}
}

View File

@ -38,7 +38,7 @@ import net.corda.node.internal.cordapp.CordappProviderInternal
import net.corda.node.services.ContractUpgradeHandler
import net.corda.node.services.FinalityHandler
import net.corda.node.services.NotaryChangeHandler
import net.corda.node.services.RPCUserService
import net.corda.node.internal.security.RPCSecurityManager
import net.corda.node.services.api.*
import net.corda.node.services.config.BFTSMaRtConfiguration
import net.corda.node.services.config.NodeConfiguration
@ -137,7 +137,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
protected val _nodeReadyFuture = openFuture<Unit>()
protected val networkMapClient: NetworkMapClient? by lazy { configuration.compatibilityZoneURL?.let(::NetworkMapClient) }
lateinit var userService: RPCUserService get
lateinit var securityManager: RPCSecurityManager get
/** Completes once the node has successfully registered with the network map service
* or has loaded network map data from local database */
@ -265,7 +265,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
protected abstract fun getRxIoScheduler(): Scheduler
open fun startShell(rpcOps: CordaRPCOps) {
InteractiveShell.startShell(configuration, rpcOps, userService, _services.identityService, _services.database)
InteractiveShell.startShell(configuration, rpcOps, securityManager, _services.identityService, _services.database)
}
private fun initNodeInfo(networkMapCache: NetworkMapCacheBaseInternal,

View File

@ -2,6 +2,7 @@ package net.corda.node.internal
import com.codahale.metrics.JmxReporter
import net.corda.core.concurrent.CordaFuture
import net.corda.core.context.AuthServiceId
import net.corda.core.internal.concurrent.openFuture
import net.corda.core.internal.concurrent.thenMatch
import net.corda.core.internal.uncheckedCast
@ -16,11 +17,10 @@ import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.contextLogger
import net.corda.node.VersionInfo
import net.corda.node.internal.cordapp.CordappLoader
import net.corda.node.internal.security.RPCSecurityManagerImpl
import net.corda.node.serialization.KryoServerSerializationScheme
import net.corda.node.services.RPCUserServiceImpl
import net.corda.node.services.api.SchemaService
import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.config.VerifierType
import net.corda.node.services.config.*
import net.corda.node.services.messaging.*
import net.corda.node.services.transactions.InMemoryTransactionVerifierService
import net.corda.node.utilities.AddressUtils
@ -133,7 +133,12 @@ open class Node(configuration: NodeConfiguration,
private var shutdownHook: ShutdownHook? = null
override fun makeMessagingService(database: CordaPersistence, info: NodeInfo): MessagingService {
userService = RPCUserServiceImpl(configuration.rpcUsers)
// Construct security manager reading users data either from the 'security' config section
// if present or from rpcUsers list if the former is missing from config.
val securityManagerConfig = configuration.security?.authService ?:
SecurityConfiguration.AuthService.fromUsers(configuration.rpcUsers)
securityManager = RPCSecurityManagerImpl(securityManagerConfig)
val serverAddress = configuration.messagingServerAddress ?: makeLocalMessageBroker()
val advertisedAddress = info.addresses.single()
@ -156,7 +161,7 @@ open class Node(configuration: NodeConfiguration,
private fun makeLocalMessageBroker(): NetworkHostAndPort {
with(configuration) {
messageBroker = ArtemisMessagingServer(this, p2pAddress.port, rpcAddress?.port, services.networkMapCache, userService)
messageBroker = ArtemisMessagingServer(this, p2pAddress.port, rpcAddress?.port, services.networkMapCache, securityManager)
return NetworkHostAndPort("localhost", p2pAddress.port)
}
}
@ -208,7 +213,7 @@ open class Node(configuration: NodeConfiguration,
// Start up the MQ clients.
rpcMessagingClient.run {
runOnStop += this::stop
start(rpcOps, userService)
start(rpcOps, securityManager)
}
verifierMessagingClient?.run {
runOnStop += this::stop
@ -221,10 +226,10 @@ open class Node(configuration: NodeConfiguration,
}
/**
* If the node is persisting to an embedded H2 database, then expose this via TCP with a JDBC URL of the form:
* If the node is persisting to an embedded H2 database, then expose this via TCP with a DB URL of the form:
* jdbc:h2:tcp://<host>:<port>/node
* with username and password as per the DataSource connection details. The key element to enabling this support is to
* ensure that you specify a JDBC connection URL of the form jdbc:h2:file: in the node config and that you include
* ensure that you specify a DB connection URL of the form jdbc:h2:file: in the node config and that you include
* the H2 option AUTO_SERVER_PORT set to the port you desire to use (0 will give a dynamically allocated port number)
* but exclude the H2 option AUTO_SERVER=TRUE.
* This is not using the H2 "automatic mixed mode" directly but leans on many of the underpinnings. For more details

View File

@ -1,5 +1,6 @@
package net.corda.node.internal
import net.corda.client.rpc.PermissionException
import net.corda.core.contracts.ContractState
import net.corda.core.crypto.SecureHash
import net.corda.core.flows.FlowLogic
@ -156,9 +157,12 @@ class RpcAuthorisationProxy(private val implementation: CordaRPCOps, private val
private inline fun <RESULT> guard(methodName: String, action: () -> RESULT) = guard(methodName, emptyList(), action)
// TODO change to KFunction reference after Kotlin fixes https://youtrack.jetbrains.com/issue/KT-12140
private inline fun <RESULT> guard(methodName: String, args: List<Any?>, action: () -> RESULT): RESULT {
context().requireEitherPermission(permissionsAllowing.invoke(methodName, args))
return action()
private inline fun <RESULT> guard(methodName: String, args: List<Class<*>>, action: () -> RESULT) : RESULT {
if (!context().isPermitted(methodName, *(args.map { it.name }.toTypedArray()))) {
throw PermissionException("User not authorized to perform RPC call $methodName with target $args")
}
else {
return action()
}
}
}

View File

@ -0,0 +1,28 @@
package net.corda.node.internal.security
/**
* Provides permission checking for the subject identified by the given [principal].
*/
interface AuthorizingSubject {
/**
* Identity of underlying subject
*/
val principal: String
/**
* Determines if the underlying subject is entitled to perform a certain action,
* (e.g. an RPC invocation) represented by an [action] string followed by an
* optional list of arguments.
*/
fun isPermitted(action : String, vararg arguments : String) : Boolean
}
/**
* An implementation of [AuthorizingSubject] permitting all actions
*/
class AdminSubject(override val principal : String) : AuthorizingSubject {
override fun isPermitted(action: String, vararg arguments: String) = true
}

View File

@ -0,0 +1,43 @@
package net.corda.node.internal.security
import java.util.*
class Password(valueRaw: CharArray) : AutoCloseable {
constructor(value: String) : this(value.toCharArray())
private val internalValue = valueRaw.copyOf()
val value: CharArray
get() = internalValue.copyOf()
val valueAsString: String
get() = internalValue.joinToString("")
override fun close() {
internalValue.indices.forEach { index ->
internalValue[index] = MASK
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Password
if (!Arrays.equals(internalValue, other.internalValue)) return false
return true
}
override fun hashCode(): Int {
return Arrays.hashCode(internalValue)
}
override fun toString(): String = (0..5).map { MASK }.joinToString("")
private companion object {
private const val MASK = '*'
}
}

View File

@ -0,0 +1,41 @@
package net.corda.node.internal.security
import net.corda.core.context.AuthServiceId
import org.apache.shiro.authc.AuthenticationException
import javax.security.auth.login.FailedLoginException
/**
* Manage security of RPC users, providing logic for user authentication and authorization.
*/
interface RPCSecurityManager : AutoCloseable {
/**
* An identifier associated to this security service
*/
val id: AuthServiceId
/**
* Perform user authentication from principal and password. Return an [AuthorizingSubject] containing
* the permissions of the user identified by the given [principal] if authentication via password succeeds,
* otherwise a [FailedLoginException] is thrown.
*/
fun authenticate(principal: String, password: Password): AuthorizingSubject
/**
* Construct an [AuthorizingSubject] instance con permissions of the user associated to
* the given principal. Throws an exception if the principal cannot be resolved to a known user.
*/
fun buildSubject(principal: String): AuthorizingSubject
}
/**
* Non-throwing version of authenticate, returning null instead of throwing in case of authentication failure
*/
fun RPCSecurityManager.tryAuthenticate(principal: String, password: Password): AuthorizingSubject? {
password.use {
return try {
authenticate(principal, password)
} catch (e: AuthenticationException) {
null
}
}
}

View File

@ -0,0 +1,308 @@
package net.corda.node.internal.security
import com.google.common.cache.CacheBuilder
import com.google.common.cache.Cache
import com.google.common.primitives.Ints
import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource
import net.corda.core.context.AuthServiceId
import net.corda.core.utilities.loggerFor
import net.corda.node.services.config.PasswordEncryption
import net.corda.node.services.config.SecurityConfiguration
import net.corda.node.services.config.AuthDataSourceType
import net.corda.nodeapi.internal.config.User
import org.apache.shiro.authc.*
import org.apache.shiro.authc.credential.PasswordMatcher
import org.apache.shiro.authc.credential.SimpleCredentialsMatcher
import org.apache.shiro.authz.AuthorizationInfo
import org.apache.shiro.authz.Permission
import org.apache.shiro.authz.SimpleAuthorizationInfo
import org.apache.shiro.authz.permission.DomainPermission
import org.apache.shiro.authz.permission.PermissionResolver
import org.apache.shiro.cache.CacheManager
import org.apache.shiro.mgt.DefaultSecurityManager
import org.apache.shiro.realm.AuthorizingRealm
import org.apache.shiro.realm.jdbc.JdbcRealm
import org.apache.shiro.subject.PrincipalCollection
import org.apache.shiro.subject.SimplePrincipalCollection
import javax.security.auth.login.FailedLoginException
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.TimeUnit
private typealias AuthServiceConfig = SecurityConfiguration.AuthService
/**
* Default implementation of [RPCSecurityManager] adapting
* [org.apache.shiro.mgt.SecurityManager]
*/
class RPCSecurityManagerImpl(config: AuthServiceConfig) : RPCSecurityManager {
override val id = config.id
private val manager: DefaultSecurityManager
init {
manager = buildImpl(config)
}
override fun close() {
manager.destroy()
}
@Throws(FailedLoginException::class)
override fun authenticate(principal: String, password: Password): AuthorizingSubject {
password.use {
val authToken = UsernamePasswordToken(principal, it.value)
try {
manager.authenticate(authToken)
} catch (authcException: AuthenticationException) {
throw FailedLoginException(authcException.toString())
}
return ShiroAuthorizingSubject(
subjectId = SimplePrincipalCollection(principal, id.value),
manager = manager)
}
}
override fun buildSubject(principal: String): AuthorizingSubject =
ShiroAuthorizingSubject(
subjectId = SimplePrincipalCollection(principal, id.value),
manager = manager)
companion object {
private val logger = loggerFor<RPCSecurityManagerImpl>()
/**
* Instantiate RPCSecurityManager initialised with users data from a list of [User]
*/
fun fromUserList(id: AuthServiceId, users: List<User>) =
RPCSecurityManagerImpl(
AuthServiceConfig.fromUsers(users).copy(id = id))
// Build internal Shiro securityManager instance
private fun buildImpl(config: AuthServiceConfig): DefaultSecurityManager {
val realm = when (config.dataSource.type) {
AuthDataSourceType.DB -> {
logger.info("Constructing DB-backed security data source: ${config.dataSource.connection}")
NodeJdbcRealm(config.dataSource)
}
AuthDataSourceType.INMEMORY -> {
logger.info("Constructing realm from list of users in config ${config.dataSource.users!!}")
InMemoryRealm(config.dataSource.users, config.id.value, config.dataSource.passwordEncryption)
}
}
return DefaultSecurityManager(realm).also {
// Setup optional cache layer if configured
it.cacheManager = config.options?.cache?.let {
GuavaCacheManager(
timeToLiveSeconds = it.expiryTimeInSecs,
maxSize = it.capacity)
}
}
}
}
}
/**
* Provide a representation of RPC permissions based on Apache Shiro permissions framework.
* A permission represents a set of actions: for example, the set of all RPC invocations, or the set
* of RPC invocations acting on a given class of Flows in input. A permission `implies` another one if
* its set of actions contains the set of actions in the other one. In Apache Shiro, permissions are
* represented by instances of the [Permission] interface which offers a single method: [implies], to
* test if the 'x implies y' binary predicate is satisfied.
*/
private class RPCPermission : DomainPermission {
/**
* Helper constructor directly setting actions and target field
*
* @param methods Set of allowed RPC methods
* @param target An optional "target" type on which methods act
*/
constructor(methods: Set<String>, target: String? = null) : super(methods, target?.let { setOf(it) })
/**
* Default constructor instantiate an "ALL" permission
*/
constructor() : super()
}
/**
* A [org.apache.shiro.authz.permission.PermissionResolver] implementation for RPC permissions.
* Provides a method to construct an [RPCPermission] instance from its string representation
* in the form used by a Node admin.
*
* Currently valid permission strings have the forms:
*
* - `ALL`: allowing all type of RPC calls
*
* - `InvokeRpc.$RPCMethodName`: allowing to call a given RPC method without restrictions on its arguments.
*
* - `StartFlow.$FlowClassName`: allowing to call a `startFlow*` RPC method targeting a Flow instance
* of a given class
*
*/
private object RPCPermissionResolver : PermissionResolver {
private val SEPARATOR = '.'
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")
override fun resolvePermission(representation: String): Permission {
val action = representation.substringBefore(SEPARATOR).toLowerCase()
when (action) {
ACTION_INVOKE_RPC -> {
val rpcCall = representation.substringAfter(SEPARATOR)
require(representation.count { it == SEPARATOR } == 1) {
"Malformed permission string"
}
return RPCPermission(setOf(rpcCall))
}
ACTION_START_FLOW -> {
val targetFlow = representation.substringAfter(SEPARATOR)
require(targetFlow.isNotEmpty()) {
"Missing target flow after StartFlow"
}
return RPCPermission(FLOW_RPC_CALLS, targetFlow)
}
ACTION_ALL -> {
// Leaving empty set of targets and actions to match everything
return RPCPermission()
}
else -> throw IllegalArgumentException("Unkwnow permission action specifier: $action")
}
}
}
private class ShiroAuthorizingSubject(
private val subjectId: PrincipalCollection,
private val manager: DefaultSecurityManager) : AuthorizingSubject {
override val principal get() = subjectId.primaryPrincipal.toString()
override fun isPermitted(action: String, vararg arguments: String) =
manager.isPermitted(subjectId, RPCPermission(setOf(action), arguments.firstOrNull()))
}
private fun buildCredentialMatcher(type: PasswordEncryption) = when (type) {
PasswordEncryption.NONE -> SimpleCredentialsMatcher()
PasswordEncryption.SHIRO_1_CRYPT -> PasswordMatcher()
}
private class InMemoryRealm(users: List<User>,
realmId: String,
passwordEncryption: PasswordEncryption = PasswordEncryption.NONE) : AuthorizingRealm() {
private val authorizationInfoByUser: Map<String, AuthorizationInfo>
private val authenticationInfoByUser: Map<String, AuthenticationInfo>
init {
permissionResolver = RPCPermissionResolver
users.forEach {
require(it.username.matches("\\w+".toRegex())) {
"Username ${it.username} contains invalid characters"
}
}
val resolvePermission = { s: String -> permissionResolver.resolvePermission(s) }
authorizationInfoByUser = users.associate {
it.username to SimpleAuthorizationInfo().apply {
objectPermissions = it.permissions.map { resolvePermission(it) }.toSet()
roles = emptySet<String>()
stringPermissions = emptySet<String>()
}
}
authenticationInfoByUser = users.associate {
it.username to SimpleAuthenticationInfo().apply {
credentials = it.password
principals = SimplePrincipalCollection(it.username, realmId)
}
}
credentialsMatcher = buildCredentialMatcher(passwordEncryption)
}
// Methods from AuthorizingRealm interface used by Shiro to query
// for authentication/authorization data for a given user
override fun doGetAuthenticationInfo(token: AuthenticationToken) =
authenticationInfoByUser[token.principal as String]
override fun doGetAuthorizationInfo(principals: PrincipalCollection) =
authorizationInfoByUser[principals.primaryPrincipal as String]
}
private class NodeJdbcRealm(config: SecurityConfiguration.AuthService.DataSource) : JdbcRealm() {
init {
credentialsMatcher = buildCredentialMatcher(config.passwordEncryption)
setPermissionsLookupEnabled(true)
dataSource = HikariDataSource(HikariConfig(config.connection!!))
permissionResolver = RPCPermissionResolver
}
}
private typealias ShiroCache<K, V> = org.apache.shiro.cache.Cache<K, V>
/**
* Adapts a [com.google.common.cache.Cache] to a [org.apache.shiro.cache.Cache] implementation.
*/
private fun <K, V> Cache<K, V>.toShiroCache(name: String) = object : ShiroCache<K, V> {
val name = name
private val impl = this@toShiroCache
override operator fun get(key: K) = impl.getIfPresent(key)
override fun put(key: K, value: V): V? {
val lastValue = get(key)
impl.put(key, value)
return lastValue
}
override fun remove(key: K): V? {
val lastValue = get(key)
impl.invalidate(key)
return lastValue
}
override fun clear() {
impl.invalidateAll()
}
override fun size() = Ints.checkedCast(impl.size())
override fun keys() = impl.asMap().keys
override fun values() = impl.asMap().values
override fun toString() = "Guava cache adapter [$impl]"
}
/**
* Implementation of [org.apache.shiro.cache.CacheManager] based on
* cache implementation in [com.google.common.cache]
*/
private class GuavaCacheManager(val maxSize: Long,
val timeToLiveSeconds: Long) : CacheManager {
private val instances = ConcurrentHashMap<String, ShiroCache<*, *>>()
override fun <K, V> getCache(name: String): ShiroCache<K, V> {
val result = instances[name] ?: buildCache<K, V>(name)
instances.putIfAbsent(name, result)
return result as ShiroCache<K, V>
}
private fun <K, V> buildCache(name: String) : ShiroCache<K, V> {
logger.info("Constructing cache '$name' with maximumSize=$maxSize, TTL=${timeToLiveSeconds}s")
return CacheBuilder.newBuilder()
.expireAfterWrite(timeToLiveSeconds, TimeUnit.SECONDS)
.maximumSize(maxSize)
.build<K, V>()
.toShiroCache(name)
}
companion object {
private val logger = loggerFor<GuavaCacheManager>()
}
}

View File

@ -1,33 +0,0 @@
package net.corda.node.services
import net.corda.core.context.AuthServiceId
import net.corda.nodeapi.internal.config.User
/**
* Service for retrieving [User] objects representing RPC users who are authorised to use the RPC system. A [User]
* contains their login username and password along with a set of permissions for RPC services they are allowed access
* to. These permissions are represented as [String]s to allow RPC implementations to add their own permissioning.
*/
interface RPCUserService {
fun getUser(username: String): User?
val users: List<User>
val id: AuthServiceId
}
// TODO Store passwords as salted hashes
// TODO Or ditch this and consider something like Apache Shiro
// TODO Need access to permission checks from inside flows and at other point during audit checking.
class RPCUserServiceImpl(override val users: List<User>) : RPCUserService {
override val id: AuthServiceId = AuthServiceId("NODE_FILE_CONFIGURATION")
init {
users.forEach {
require(it.username.matches("\\w+".toRegex())) { "Username ${it.username} contains invalid characters" }
}
}
override fun getUser(username: String): User? = users.find { it.username == username }
}

View File

@ -1,6 +1,7 @@
package net.corda.node.services.config
import com.typesafe.config.Config
import net.corda.core.context.AuthServiceId
import net.corda.core.identity.CordaX500Name
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.seconds
@ -21,6 +22,7 @@ interface NodeConfiguration : NodeSSLConfiguration {
val exportJMXto: String
val dataSourceProperties: Properties
val rpcUsers: List<User>
val security: SecurityConfiguration?
val devMode: Boolean
val devModeOptions: DevModeOptions?
val compatibilityZoneURL: URL?
@ -93,6 +95,7 @@ data class NodeConfigurationImpl(
override val dataSourceProperties: Properties,
override val compatibilityZoneURL: URL? = null,
override val rpcUsers: List<User>,
override val security : SecurityConfiguration? = null,
override val verifierType: VerifierType,
// TODO typesafe config supports the notion of durations. Make use of that by mapping it to java.time.Duration.
// Then rename this to messageRedeliveryDelay and make it of type Duration
@ -115,6 +118,7 @@ data class NodeConfigurationImpl(
override val sshd: SSHDConfiguration? = null,
override val database: DatabaseConfig = DatabaseConfig(initialiseSchema = devMode, exportHibernateJMXStatistics = devMode)
) : NodeConfiguration {
override val exportJMXto: String get() = "http"
init {
@ -122,6 +126,9 @@ data class NodeConfigurationImpl(
require(!useTestClock || devMode) { "Cannot use test clock outside of dev mode" }
require(devModeOptions == null || devMode) { "Cannot use devModeOptions outside of dev mode" }
require(myLegalName.commonName == null) { "Common name must be null: $myLegalName" }
require(security == null || rpcUsers.isEmpty()) {
"Cannot specify both 'rpcUsers' and 'security' in configuration"
}
}
}
@ -149,4 +156,78 @@ data class CertChainPolicyConfig(val role: String, private val policy: CertChain
}
}
data class SSHDConfiguration(val port: Int)
data class SSHDConfiguration(val port: Int)
// Supported types of authentication/authorization data providers
enum class AuthDataSourceType {
// External RDBMS
DB,
// Static dataset hard-coded in config
INMEMORY
}
// Password encryption scheme
enum class PasswordEncryption {
// Password stored in clear
NONE,
// Password salt-hashed using Apache Shiro flexible encryption format
// [org.apache.shiro.crypto.hash.format.Shiro1CryptFormat]
SHIRO_1_CRYPT
}
// Subset of Node configuration related to security aspects
data class SecurityConfiguration(val authService: SecurityConfiguration.AuthService) {
// Configure RPC/Shell users authentication/authorization service
data class AuthService(val dataSource: AuthService.DataSource,
val id: AuthServiceId = defaultAuthServiceId(dataSource.type),
val options: AuthService.Options? = null) {
init {
require(!(dataSource.type == AuthDataSourceType.INMEMORY &&
options?.cache != null)) {
"No cache supported for INMEMORY data provider"
}
}
// Optional components: cache
data class Options(val cache: Options.Cache?) {
// Cache parameters
data class Cache(val expiryTimeInSecs: Long, val capacity: Long)
}
// Provider of users credentials and permissions data
data class DataSource(val type: AuthDataSourceType,
val passwordEncryption: PasswordEncryption = PasswordEncryption.NONE,
val connection: Properties? = null,
val users: List<User>? = null) {
init {
when (type) {
AuthDataSourceType.INMEMORY -> require(users != null && connection == null)
AuthDataSourceType.DB -> require(users == null && connection != null)
}
}
}
companion object {
// If unspecified, we assign an AuthServiceId by default based on the
// underlying data provider
fun defaultAuthServiceId(type: AuthDataSourceType) = when (type) {
AuthDataSourceType.INMEMORY -> AuthServiceId("NODE_CONFIG")
AuthDataSourceType.DB -> AuthServiceId("REMOTE_DATABASE")
}
fun fromUsers(users: List<User>) = AuthService(
dataSource = DataSource(
type = AuthDataSourceType.INMEMORY,
users = users,
passwordEncryption = PasswordEncryption.NONE),
id = AuthServiceId("NODE_CONFIG"))
}
}
}

View File

@ -12,9 +12,13 @@ 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.serialization.SingletonSerializeAsToken
import net.corda.core.utilities.*
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.debug
import net.corda.core.utilities.parsePublicKeyBase58
import net.corda.node.internal.Node
import net.corda.node.services.RPCUserService
import net.corda.node.internal.security.Password
import net.corda.node.internal.security.RPCSecurityManager
import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.messaging.NodeLoginModule.Companion.NODE_ROLE
import net.corda.node.services.messaging.NodeLoginModule.Companion.PEER_ROLE
@ -25,13 +29,13 @@ import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_CLIENT_TLS
import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_ROOT_CA
import net.corda.nodeapi.internal.crypto.loadKeyStore
import net.corda.nodeapi.*
import net.corda.nodeapi.internal.ArtemisMessagingComponent.ArtemisPeerAddress
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.INTERNAL_PREFIX
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_USER
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NOTIFICATIONS_ADDRESS
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_QUEUE
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEERS_PREFIX
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEER_USER
import net.corda.nodeapi.internal.ArtemisMessagingComponent.ArtemisPeerAddress
import net.corda.nodeapi.internal.ArtemisMessagingComponent.NodeAddress
import net.corda.nodeapi.internal.requireOnDefaultFileSystem
import org.apache.activemq.artemis.api.core.SimpleString
@ -97,7 +101,7 @@ class ArtemisMessagingServer(private val config: NodeConfiguration,
private val p2pPort: Int,
val rpcPort: Int?,
val networkMapCache: NetworkMapCache,
val userService: RPCUserService) : SingletonSerializeAsToken() {
val securityManager: RPCSecurityManager) : SingletonSerializeAsToken() {
companion object {
private val log = contextLogger()
/** 10 MiB maximum allowed file size for attachments, including message headers. TODO: acquire this value from Network Map when supported. */
@ -234,7 +238,7 @@ class ArtemisMessagingServer(private val config: NodeConfiguration,
* 3. RPC users. These are only given sufficient access to perform RPC with us.
* 4. Verifiers. These are given read access to the verification request queue and write access to the response queue.
*/
private fun ConfigurationImpl.configureAddressSecurity() : Pair<Configuration, LoginListener> {
private fun ConfigurationImpl.configureAddressSecurity(): Pair<Configuration, LoginListener> {
val nodeInternalRole = Role(NODE_ROLE, true, true, true, true, true, true, true, true)
securityRoles["$INTERNAL_PREFIX#"] = setOf(nodeInternalRole) // Do not add any other roles here as it's only for the node
securityRoles[P2P_QUEUE] = setOf(nodeInternalRole, restrictedRole(PEER_ROLE, send = true))
@ -285,7 +289,7 @@ class ArtemisMessagingServer(private val config: NodeConfiguration,
override fun getAppConfigurationEntry(name: String): Array<AppConfigurationEntry> {
val options = mapOf(
LoginListener::javaClass.name to loginListener,
RPCUserService::class.java.name to userService,
RPCSecurityManager::class.java.name to securityManager,
NodeLoginModule.CERT_CHAIN_CHECKS_OPTION_NAME to certChecks)
return arrayOf(AppConfigurationEntry(name, REQUIRED, options))
}
@ -560,7 +564,7 @@ class NodeLoginModule : LoginModule {
private var loginSucceeded: Boolean = false
private lateinit var subject: Subject
private lateinit var callbackHandler: CallbackHandler
private lateinit var userService: RPCUserService
private lateinit var securityManager: RPCSecurityManager
private lateinit var loginListener: LoginListener
private lateinit var peerCertCheck: CertificateChainCheckPolicy.Check
private lateinit var nodeCertCheck: CertificateChainCheckPolicy.Check
@ -570,7 +574,7 @@ class NodeLoginModule : LoginModule {
override fun initialize(subject: Subject, callbackHandler: CallbackHandler, sharedState: Map<String, *>, options: Map<String, *>) {
this.subject = subject
this.callbackHandler = callbackHandler
userService = options[RPCUserService::class.java.name] as RPCUserService
securityManager = options[RPCSecurityManager::class.java.name] as RPCSecurityManager
loginListener = options[LoginListener::javaClass.name] as LoginListener
val certChainChecks: Map<String, CertificateChainCheckPolicy.Check> = uncheckedCast(options[CERT_CHAIN_CHECKS_OPTION_NAME])
peerCertCheck = certChainChecks[PEER_ROLE]!!
@ -601,7 +605,7 @@ class NodeLoginModule : LoginModule {
PEER_ROLE -> authenticatePeer(certificates)
NODE_ROLE -> authenticateNode(certificates)
VERIFIER_ROLE -> authenticateVerifier(certificates)
RPC_ROLE -> authenticateRpcUser(password, username)
RPC_ROLE -> authenticateRpcUser(username, Password(password))
else -> throw FailedLoginException("Peer does not belong on our network")
}
principals += UserPrincipal(validatedUser)
@ -632,13 +636,8 @@ class NodeLoginModule : LoginModule {
return certificates.first().subjectDN.name
}
private fun authenticateRpcUser(password: String, username: String): String {
val rpcUser = userService.getUser(username) ?: throw FailedLoginException("User does not exist")
if (password != rpcUser.password) {
// TODO Switch to hashed passwords
// TODO Retrieve client IP address to include in exception message
throw FailedLoginException("Password for user $username does not match")
}
private fun authenticateRpcUser(username: String, password: Password): String {
securityManager.authenticate(username, password)
loginListener(username)
principals += RolePrincipal(RPC_ROLE) // This enables the RPC client to send requests
principals += RolePrincipal("${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.$username") // This enables the RPC client to receive responses

View File

@ -4,7 +4,7 @@ import net.corda.core.identity.CordaX500Name
import net.corda.core.messaging.RPCOps
import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.node.services.RPCUserService
import net.corda.node.internal.security.RPCSecurityManager
import net.corda.nodeapi.internal.config.SSLConfiguration
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_USER
import net.corda.nodeapi.internal.crypto.X509Utilities
@ -16,10 +16,10 @@ class RPCMessagingClient(private val config: SSLConfiguration, serverAddress: Ne
private val artemis = ArtemisMessagingClient(config, serverAddress)
private var rpcServer: RPCServer? = null
fun start(rpcOps: RPCOps, userService: RPCUserService) = synchronized(this) {
fun start(rpcOps: RPCOps, securityManager: RPCSecurityManager) = synchronized(this) {
val locator = artemis.start().sessionFactory.serverLocator
val myCert = loadKeyStore(config.sslKeystore, config.keyStorePassword).getX509Certificate(X509Utilities.CORDA_CLIENT_TLS)
rpcServer = RPCServer(rpcOps, NODE_USER, NODE_USER, locator, userService, CordaX500Name.build(myCert.subjectX500Principal))
rpcServer = RPCServer(rpcOps, NODE_USER, NODE_USER, locator, securityManager, CordaX500Name.build(myCert.subjectX500Principal))
}
fun start2(serverControl: ActiveMQServerControl) = synchronized(this) {

View File

@ -26,11 +26,10 @@ import net.corda.core.serialization.SerializationContext
import net.corda.core.serialization.SerializationDefaults.RPC_SERVER_CONTEXT
import net.corda.core.serialization.deserialize
import net.corda.core.utilities.*
import net.corda.node.services.RPCUserService
import net.corda.node.internal.security.AuthorizingSubject
import net.corda.node.internal.security.RPCSecurityManager
import net.corda.node.services.logging.pushToLoggingContext
import net.corda.nodeapi.*
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_USER
import net.corda.nodeapi.internal.config.User
import org.apache.activemq.artemis.api.core.Message
import org.apache.activemq.artemis.api.core.SimpleString
import org.apache.activemq.artemis.api.core.client.ActiveMQClient.DEFAULT_ACK_BATCH_SIZE
@ -85,7 +84,7 @@ class RPCServer(
private val rpcServerUsername: String,
private val rpcServerPassword: String,
private val serverLocator: ServerLocator,
private val userService: RPCUserService,
private val securityManager: RPCSecurityManager,
private val nodeLegalName: CordaX500Name,
private val rpcConfiguration: RPCServerConfiguration = RPCServerConfiguration.default
) {
@ -213,6 +212,7 @@ class RPCServer(
reaperScheduledFuture?.cancel(false)
rpcExecutor?.shutdownNow()
reaperExecutor?.shutdownNow()
securityManager.close()
sessionAndConsumers.forEach {
it.sessionFactory.close()
}
@ -365,15 +365,10 @@ class RPCServer(
return RpcAuthContext(InvocationContext.rpc(rpcActor.first, trace, externalTrace, impersonatedActor), rpcActor.second)
}
private fun actorFrom(message: ClientMessage): Pair<Actor, RpcPermissions> {
private fun actorFrom(message: ClientMessage): Pair<Actor, AuthorizingSubject> {
val validatedUser = message.getStringProperty(Message.HDR_VALIDATED_USER) ?: throw IllegalArgumentException("Missing validated user from the Artemis message")
val targetLegalIdentity = message.getStringProperty(RPCApi.RPC_TARGET_LEGAL_IDENTITY)?.let(CordaX500Name.Companion::parse) ?: nodeLegalName
// TODO switch userService based on targetLegalIdentity
val rpcUser = userService.getUser(validatedUser) ?:
throw IllegalArgumentException("Validated user '$validatedUser' is not an RPC user")
return Pair(
Actor(Id(rpcUser.username), userService.id, targetLegalIdentity),
RpcPermissions(rpcUser.permissions))
return Pair(Actor(Id(validatedUser), securityManager.id, targetLegalIdentity), securityManager.buildSubject(validatedUser))
}
}

View File

@ -1,30 +1,9 @@
package net.corda.node.services.messaging
import net.corda.client.rpc.PermissionException
import net.corda.core.context.InvocationContext
import net.corda.node.services.Permissions
import net.corda.nodeapi.internal.ArtemisMessagingComponent
import net.corda.node.internal.security.AuthorizingSubject
data class RpcAuthContext(val invocation: InvocationContext, val grantedPermissions: RpcPermissions) {
data class RpcAuthContext(val invocation: InvocationContext,
private val authorizer: AuthorizingSubject)
: AuthorizingSubject by authorizer
fun requirePermission(permission: String) = requireEitherPermission(setOf(permission))
fun requireEitherPermission(permissions: Set<String>): RpcAuthContext {
// TODO remove the NODE_USER condition once webserver and shell won't need it anymore
if (invocation.principal().name != ArtemisMessagingComponent.NODE_USER && !grantedPermissions.coverAny(permissions)) {
throw PermissionException("User not permissioned with any of $permissions, permissions are ${this.grantedPermissions}.")
}
return this
}
}
data class RpcPermissions(private val values: Set<String> = emptySet()) {
companion object {
val NONE = RpcPermissions()
val ALL = RpcPermissions(setOf("ALL"))
}
fun coverAny(permissions: Set<String>) = !values.intersect(permissions + Permissions.all()).isEmpty()
}

View File

@ -4,31 +4,30 @@ import net.corda.core.context.Actor
import net.corda.core.context.InvocationContext
import net.corda.core.identity.CordaX500Name
import net.corda.core.messaging.CordaRPCOps
import net.corda.node.services.RPCUserService
import net.corda.node.services.messaging.RpcPermissions
import net.corda.node.internal.security.Password
import net.corda.node.internal.security.RPCSecurityManager
import net.corda.node.internal.security.tryAuthenticate
import org.crsh.auth.AuthInfo
import org.crsh.auth.AuthenticationPlugin
import org.crsh.plugin.CRaSHPlugin
class CordaAuthenticationPlugin(val rpcOps:CordaRPCOps, val userService:RPCUserService, val nodeLegalName:CordaX500Name) : CRaSHPlugin<AuthenticationPlugin<String>>(), AuthenticationPlugin<String> {
class CordaAuthenticationPlugin(private val rpcOps: CordaRPCOps, private val securityManager: RPCSecurityManager, private val nodeLegalName: CordaX500Name) : CRaSHPlugin<AuthenticationPlugin<String>>(), AuthenticationPlugin<String> {
override fun getImplementation(): AuthenticationPlugin<String> = this
override fun getName(): String = "corda"
override fun authenticate(username: String?, credential: String?): AuthInfo {
if (username == null || credential == null) {
return AuthInfo.UNSUCCESSFUL
}
val user = userService.getUser(username)
if (user != null && user.password == credential) {
val actor = Actor(Actor.Id(username), userService.id, nodeLegalName)
return CordaSSHAuthInfo(true, makeRPCOpsWithContext(rpcOps, InvocationContext.rpc(actor), RpcPermissions(user.permissions)))
val authorizingSubject = securityManager.tryAuthenticate(username, Password(credential))
if (authorizingSubject != null) {
val actor = Actor(Actor.Id(username), securityManager.id, nodeLegalName)
return CordaSSHAuthInfo(true, makeRPCOpsWithContext(rpcOps, InvocationContext.rpc(actor), authorizingSubject))
}
return AuthInfo.UNSUCCESSFUL;
return AuthInfo.UNSUCCESSFUL
}
override fun getCredentialType(): Class<String> = String::class.java

View File

@ -25,11 +25,11 @@ import net.corda.core.messaging.StateMachineUpdate
import net.corda.core.node.services.IdentityService
import net.corda.node.internal.Node
import net.corda.node.internal.StartedNode
import net.corda.node.services.RPCUserService
import net.corda.node.internal.security.AdminSubject
import net.corda.node.internal.security.RPCSecurityManager
import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.messaging.CURRENT_RPC_CONTEXT
import net.corda.node.services.messaging.RpcAuthContext
import net.corda.node.services.messaging.RpcPermissions
import net.corda.node.utilities.ANSIProgressRenderer
import net.corda.node.utilities.StdoutANSIProgressRenderer
import net.corda.nodeapi.internal.persistence.CordaPersistence
@ -82,19 +82,19 @@ object InteractiveShell {
private lateinit var node: StartedNode<Node>
@VisibleForTesting
internal lateinit var database: CordaPersistence
private lateinit var rpcOps:CordaRPCOps
private lateinit var userService:RPCUserService
private lateinit var identityService:IdentityService
private var shell:Shell? = null
private lateinit var rpcOps: CordaRPCOps
private lateinit var securityManager: RPCSecurityManager
private lateinit var identityService: IdentityService
private var shell: Shell? = null
private lateinit var nodeLegalName: CordaX500Name
/**
* Starts an interactive shell connected to the local terminal. This shell gives administrator access to the node
* internals.
*/
fun startShell(configuration:NodeConfiguration, cordaRPCOps: CordaRPCOps, userService: RPCUserService, identityService: IdentityService, database: CordaPersistence) {
fun startShell(configuration: NodeConfiguration, cordaRPCOps: CordaRPCOps, securityManager: RPCSecurityManager, identityService: IdentityService, database: CordaPersistence) {
this.rpcOps = cordaRPCOps
this.userService = userService
this.securityManager = securityManager
this.identityService = identityService
this.nodeLegalName = configuration.myLegalName
this.database = database
@ -123,14 +123,14 @@ object InteractiveShell {
}
}
fun runLocalShell(node:StartedNode<Node>) {
fun runLocalShell(node: StartedNode<Node>) {
val terminal = TerminalFactory.create()
val consoleReader = ConsoleReader("Corda", FileInputStream(FileDescriptor.`in`), System.out, terminal)
val jlineProcessor = JLineProcessor(terminal.isAnsiSupported, shell, consoleReader, System.out)
InterruptHandler { jlineProcessor.interrupt() }.install()
thread(name = "Command line shell processor", isDaemon = true) {
// Give whoever has local shell access administrator access to the node.
val context = RpcAuthContext(net.corda.core.context.InvocationContext.shell(), RpcPermissions.ALL)
val context = RpcAuthContext(net.corda.core.context.InvocationContext.shell(), AdminSubject("SHELL_USER"))
CURRENT_RPC_CONTEXT.set(context)
Emoji.renderIfSupported {
jlineProcessor.run()
@ -169,7 +169,7 @@ object InteractiveShell {
// Don't use the Java language plugin (we may not have tools.jar available at runtime), this
// will cause any commands using JIT Java compilation to be suppressed. In CRaSH upstream that
// is only the 'jmx' command.
return super.getPlugins().filterNot { it is JavaLanguage } + CordaAuthenticationPlugin(rpcOps, userService, nodeLegalName)
return super.getPlugins().filterNot { it is JavaLanguage } + CordaAuthenticationPlugin(rpcOps, securityManager, nodeLegalName)
}
}
val attributes = mapOf(
@ -180,7 +180,7 @@ object InteractiveShell {
context.refresh()
this.config = config
start(context)
return context.getPlugin(ShellFactory::class.java).create(null, CordaSSHAuthInfo(false, makeRPCOpsWithContext(rpcOps, net.corda.core.context.InvocationContext.shell(), RpcPermissions.ALL), StdoutANSIProgressRenderer))
return context.getPlugin(ShellFactory::class.java).create(null, CordaSSHAuthInfo(false, makeRPCOpsWithContext(rpcOps, net.corda.core.context.InvocationContext.shell(), AdminSubject("SHELL_USER")), StdoutANSIProgressRenderer))
}
}
@ -248,7 +248,7 @@ object InteractiveShell {
} catch (e: NoApplicableConstructor) {
output.println("No matching constructor found:", Color.red)
e.errors.forEach { output.println("- $it", Color.red) }
} catch (e:PermissionException) {
} catch (e: PermissionException) {
output.println(e.message ?: "Access denied", Color.red)
} finally {
InputStreamDeserializer.closeAll()
@ -271,9 +271,9 @@ object InteractiveShell {
*/
@Throws(NoApplicableConstructor::class)
fun <T> runFlowFromString(invoke: (Class<out FlowLogic<T>>, Array<out Any?>) -> FlowProgressHandle<T>,
inputData: String,
clazz: Class<out FlowLogic<T>>,
om: ObjectMapper = yamlInputMapper): FlowProgressHandle<T> {
inputData: String,
clazz: Class<out FlowLogic<T>>,
om: ObjectMapper = yamlInputMapper): FlowProgressHandle<T> {
// For each constructor, attempt to parse the input data as a method call. Use the first that succeeds,
// and keep track of the reasons we failed so we can print them out if no constructors are usable.
val parser = StringToMethodCallParser(clazz, om)

View File

@ -1,36 +1,39 @@
package net.corda.node.shell
import net.corda.core.context.InvocationContext
import net.corda.core.messaging.*
import net.corda.core.messaging.CordaRPCOps
import net.corda.core.utilities.getOrThrow
import net.corda.node.internal.security.AuthorizingSubject
import net.corda.node.services.messaging.CURRENT_RPC_CONTEXT
import net.corda.node.services.messaging.RpcAuthContext
import net.corda.node.services.messaging.RpcPermissions
import java.lang.reflect.InvocationTargetException
import java.lang.reflect.Proxy
import java.util.concurrent.CompletableFuture
import java.util.concurrent.Future
fun makeRPCOpsWithContext(cordaRPCOps: CordaRPCOps, invocationContext:InvocationContext, rpcPermissions: RpcPermissions) : CordaRPCOps {
return Proxy.newProxyInstance(CordaRPCOps::class.java.classLoader, arrayOf(CordaRPCOps::class.java), { proxy, method, args ->
RPCContextRunner(invocationContext, rpcPermissions) {
try {
method.invoke(cordaRPCOps, *(args ?: arrayOf()))
} catch (e: InvocationTargetException) {
// Unpack exception.
throw e.targetException
}
}.get().getOrThrow()
}) as CordaRPCOps
fun makeRPCOpsWithContext(cordaRPCOps: CordaRPCOps, invocationContext:InvocationContext, authorizingSubject: AuthorizingSubject) : CordaRPCOps {
return Proxy.newProxyInstance(CordaRPCOps::class.java.classLoader, arrayOf(CordaRPCOps::class.java), { _, method, args ->
RPCContextRunner(invocationContext, authorizingSubject) {
try {
method.invoke(cordaRPCOps, *(args ?: arrayOf()))
} catch (e: InvocationTargetException) {
// Unpack exception.
throw e.targetException
}
}.get().getOrThrow()
}) as CordaRPCOps
}
private class RPCContextRunner<T>(val invocationContext:InvocationContext, val rpcPermissions: RpcPermissions, val block:() -> T) : Thread() {
private class RPCContextRunner<T>(val invocationContext: InvocationContext, val authorizingSubject: AuthorizingSubject, val block:() -> T): Thread() {
private var result: CompletableFuture<T> = CompletableFuture()
override fun run() {
CURRENT_RPC_CONTEXT.set(RpcAuthContext(invocationContext, rpcPermissions))
CURRENT_RPC_CONTEXT.set(RpcAuthContext(invocationContext, authorizingSubject))
try {
result.complete(block())
} catch (e:Throwable) {
} catch (e: Throwable) {
result.completeExceptionally(e)
} finally {
CURRENT_RPC_CONTEXT.remove()

View File

@ -2,6 +2,7 @@ package net.corda.node
import co.paralleluniverse.fibers.Suspendable
import net.corda.client.rpc.PermissionException
import net.corda.core.context.AuthServiceId
import net.corda.core.context.InvocationContext
import net.corda.core.contracts.Amount
import net.corda.core.contracts.ContractState
@ -26,11 +27,12 @@ import net.corda.finance.flows.CashIssueFlow
import net.corda.finance.flows.CashPaymentFlow
import net.corda.node.internal.SecureCordaRPCOps
import net.corda.node.internal.StartedNode
import net.corda.node.internal.security.RPCSecurityManagerImpl
import net.corda.node.services.Permissions.Companion.invokeRpc
import net.corda.node.services.Permissions.Companion.startFlow
import net.corda.node.services.messaging.CURRENT_RPC_CONTEXT
import net.corda.node.services.messaging.RpcAuthContext
import net.corda.node.services.messaging.RpcPermissions
import net.corda.nodeapi.internal.config.User
import net.corda.testing.*
import net.corda.testing.node.MockNetwork
import net.corda.testing.node.MockNetwork.MockNode
@ -48,6 +50,15 @@ import kotlin.test.assertFalse
import kotlin.test.assertNull
import kotlin.test.assertTrue
// Mock an AuthorizingSubject instance sticking to a fixed set of permissions
private fun buildSubject(principal: String, permissionStrings: Set<String>) =
RPCSecurityManagerImpl.fromUserList(
id = AuthServiceId("TEST"),
users = listOf(User(username = principal,
password = "",
permissions = permissionStrings)))
.buildSubject(principal)
class CordaRPCOpsImplTest {
private companion object {
val testJar = "net/corda/node/testing/test.jar"
@ -67,7 +78,7 @@ class CordaRPCOpsImplTest {
mockNet = MockNetwork(cordappPackages = listOf("net.corda.finance.contracts.asset"))
aliceNode = mockNet.createNode(MockNodeParameters(legalName = ALICE_NAME))
rpc = SecureCordaRPCOps(aliceNode.services, aliceNode.smm, aliceNode.database, aliceNode.services)
CURRENT_RPC_CONTEXT.set(RpcAuthContext(InvocationContext.rpc(testActor()), RpcPermissions.NONE))
CURRENT_RPC_CONTEXT.set(RpcAuthContext(InvocationContext.rpc(testActor()), buildSubject("TEST_USER", emptySet())))
mockNet.runNetwork()
withPermissions(invokeRpc(CordaRPCOps::notaryIdentities)) {
@ -301,7 +312,8 @@ class CordaRPCOpsImplTest {
val previous = CURRENT_RPC_CONTEXT.get()
try {
CURRENT_RPC_CONTEXT.set(previous.copy(grantedPermissions = RpcPermissions(permissions.toSet())))
CURRENT_RPC_CONTEXT.set(previous.copy(authorizer =
buildSubject(previous.principal, permissions.toSet())))
action.invoke()
} finally {
CURRENT_RPC_CONTEXT.set(previous)

View File

@ -1,11 +1,12 @@
package net.corda.node.services
import net.corda.core.context.AuthServiceId
import net.corda.node.internal.security.RPCSecurityManagerImpl
import net.corda.nodeapi.internal.config.User
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Test
class RPCUserServiceTest {
class RPCSecurityManagerTest {
@Test
fun `Artemis special characters not permitted in RPC usernames`() {
@ -15,6 +16,6 @@ class RPCUserServiceTest {
}
private fun configWithRPCUsername(username: String) {
RPCUserServiceImpl(listOf(User(username, "password", setOf())))
RPCSecurityManagerImpl.fromUserList(users = listOf(User(username, "password", setOf())), id = AuthServiceId("TEST"))
}
}

View File

@ -1,9 +1,10 @@
package net.corda.node.services.messaging
import net.corda.core.context.AuthServiceId
import net.corda.core.crypto.generateKeyPair
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.node.services.RPCUserService
import net.corda.node.services.RPCUserServiceImpl
import net.corda.node.internal.security.RPCSecurityManager
import net.corda.node.internal.security.RPCSecurityManagerImpl
import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.config.configureWithDevSSLCertificate
import net.corda.node.services.network.NetworkMapCacheImpl
@ -50,7 +51,7 @@ class ArtemisMessagingTests {
private lateinit var config: NodeConfiguration
private lateinit var database: CordaPersistence
private lateinit var userService: RPCUserService
private lateinit var securityManager: RPCSecurityManager
private var messagingClient: P2PMessagingClient? = null
private var messagingServer: ArtemisMessagingServer? = null
@ -58,7 +59,7 @@ class ArtemisMessagingTests {
@Before
fun setUp() {
userService = RPCUserServiceImpl(emptyList())
securityManager = RPCSecurityManagerImpl.fromUserList(users = emptyList(), id = AuthServiceId("TEST"))
config = testNodeConfiguration(
baseDirectory = temporaryFolder.root.toPath(),
myLegalName = ALICE.name)
@ -169,7 +170,7 @@ class ArtemisMessagingTests {
}
private fun createMessagingServer(local: Int = serverPort, rpc: Int = rpcPort): ArtemisMessagingServer {
return ArtemisMessagingServer(config, local, rpc, networkMapCache, userService).apply {
return ArtemisMessagingServer(config, local, rpc, networkMapCache, securityManager).apply {
config.configureWithDevSSLCertificate()
messagingServer = this
}

View File

@ -868,7 +868,7 @@ class HibernateConfigurationTest {
}
/**
* Test invoking SQL query using JDBC connection (session)
* Test invoking SQL query using DB connection (session)
*/
@Test
fun `test calling an arbitrary JDBC native query`() {

View File

@ -2037,7 +2037,7 @@ class VaultQueryTests {
* USE CASE demonstrations (outside of mainline Corda)
*
* 1) Template / Tutorial CorDapp service using Vault API Custom Query to access attributes of IOU State
* 2) Template / Tutorial Flow using a JDBC session to execute a custom query
* 2) Template / Tutorial Flow using a DB session to execute a custom query
* 3) Template / Tutorial CorDapp service query extension executing Named Queries via JPA
* 4) Advanced pagination queries using Spring Data (and/or Hibernate/JPQL)
*/

View File

@ -16,7 +16,7 @@ import net.corda.core.internal.div
import net.corda.core.internal.uncheckedCast
import net.corda.core.messaging.RPCOps
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.node.services.RPCUserService
import net.corda.node.internal.security.RPCSecurityManagerImpl
import net.corda.node.services.messaging.ArtemisMessagingServer
import net.corda.node.services.messaging.RPCServer
import net.corda.node.services.messaging.RPCServerConfiguration
@ -428,17 +428,13 @@ data class RPCDriverDSL(
minLargeMessageSize = ArtemisMessagingServer.MAX_FILE_SIZE
isUseGlobalPools = false
}
val userService = object : RPCUserService {
override fun getUser(username: String): User? = if (username == rpcUser.username) rpcUser else null
override val users: List<User> get() = listOf(rpcUser)
override val id: AuthServiceId = AuthServiceId("RPC_DRIVER")
}
val rpcSecurityManager = RPCSecurityManagerImpl.fromUserList(users = listOf(rpcUser), id = AuthServiceId("TEST_SECURITY_MANAGER"))
val rpcServer = RPCServer(
ops,
rpcUser.username,
rpcUser.password,
locator,
userService,
rpcSecurityManager,
nodeLegalName,
configuration
)

View File

@ -0,0 +1,96 @@
package net.corda.node.internal.security
import org.hamcrest.CoreMatchers.containsString
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.core.IsEqual.equalTo
import org.hamcrest.core.IsNot.not
import org.junit.Test
internal class PasswordTest {
@Test
fun immutability() {
val charArray = "dadada".toCharArray()
val password = Password(charArray)
assertThat(password.value, equalTo(charArray))
charArray[0] = 'm'
assertThat(password.value, not(equalTo(charArray)))
val value = password.value
value[1] = 'e'
assertThat(password.value, not(equalTo(value)))
}
@Test
fun constructor_and_getters() {
val value = "dadada"
assertThat(Password(value.toCharArray()).value, equalTo(value.toCharArray()))
assertThat(Password(value.toCharArray()).valueAsString, equalTo(value))
assertThat(Password(value).value, equalTo(value.toCharArray()))
assertThat(Password(value).valueAsString, equalTo(value))
}
@Test
fun equals() {
val passwordValue1 = Password("value1")
val passwordValue2 = Password("value2")
val passwordValue12 = Password("value1")
assertThat(passwordValue1, equalTo(passwordValue1))
assertThat(passwordValue1, not(equalTo(passwordValue2)))
assertThat(passwordValue2, not(equalTo(passwordValue1)))
assertThat(passwordValue1, equalTo(passwordValue12))
assertThat(passwordValue12, equalTo(passwordValue1))
}
@Test
fun hashcode() {
val passwordValue1 = Password("value1")
val passwordValue2 = Password("value2")
val passwordValue12 = Password("value1")
assertThat(passwordValue1.hashCode(), equalTo(passwordValue1.hashCode()))
// not strictly required by hashCode() contract, but desirable
assertThat(passwordValue1.hashCode(), not(equalTo(passwordValue2.hashCode())))
assertThat(passwordValue2.hashCode(), not(equalTo(passwordValue1.hashCode())))
assertThat(passwordValue1.hashCode(), equalTo(passwordValue12.hashCode()))
assertThat(passwordValue12.hashCode(), equalTo(passwordValue1.hashCode()))
}
@Test
fun close() {
val value = "ipjd1@pijmps112112"
val password = Password(value)
password.use {
val readValue = it.valueAsString
assertThat(readValue, equalTo(value))
}
val readValue = password.valueAsString
assertThat(readValue, not(equalTo(value)))
}
@Test
fun toString_is_masked() {
val value = "ipjd1@pijmps112112"
val password = Password(value)
val toString = password.toString()
assertThat(toString, not(containsString(value)))
}
}