Merge branch 'master' into shams-master-merge-081217

# Conflicts:
#	node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt
#	testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt
#	testing/node-driver/src/main/kotlin/net/corda/testing/internal/DriverDSLImpl.kt
#	testing/node-driver/src/main/kotlin/net/corda/testing/internal/RPCDriver.kt
#	testing/node-driver/src/main/kotlin/net/corda/testing/internal/demorun/DemoRunner.kt
#	verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt
This commit is contained in:
Shams Asari
2017-12-11 10:23:16 +00:00
75 changed files with 1911 additions and 506 deletions

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

@ -35,7 +35,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
@ -141,7 +141,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
}
}
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 */
@ -272,7 +272,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)
}
}
@ -214,7 +219,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
@ -227,10 +232,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

@ -81,13 +81,13 @@ open class NodeStartup(val args: Array<String>) {
conf0
}
banJavaSerialisation(conf)
preNetworkRegistration(conf)
if (shouldRegisterWithNetwork(cmdlineOptions, conf)) {
banJavaSerialisation(conf)
preNetworkRegistration(conf)
if (shouldRegisterWithNetwork(cmdlineOptions, conf)) {
registerWithNetwork(cmdlineOptions, conf)
return true
}
logStartupInfo(versionInfo, cmdlineOptions, conf)
logStartupInfo(versionInfo, cmdlineOptions, conf)
try {
cmdlineOptions.baseDirectory.createDirectories()

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
@ -113,14 +116,18 @@ data class NodeConfigurationImpl(
// TODO See TODO above. Rename this to nodeInfoPollingFrequency and make it of type Duration
override val additionalNodeInfoPollingFrequencyMsec: Long = 5.seconds.toMillis(),
override val sshd: SSHDConfiguration? = null,
override val database: DatabaseConfig = DatabaseConfig(initialiseSchema = devMode)
override val database: DatabaseConfig = DatabaseConfig(initialiseSchema = devMode, exportHibernateJMXStatistics = devMode)
) : NodeConfiguration {
override val exportJMXto: String get() = "http"
init {
// This is a sanity feature do not remove.
require(!useTestClock || devMode) { "Cannot use test clock outside of dev mode" }
require(devModeOptions == null || devMode) { "Cannot use devModeOptions outside of dev mode" }
require(security == null || rpcUsers.isEmpty()) {
"Cannot specify both 'rpcUsers' and 'security' in configuration"
}
}
}
@ -148,4 +155,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. */
@ -211,7 +215,12 @@ class ArtemisMessagingServer(private val config: NodeConfiguration,
addressFullMessagePolicy = AddressFullMessagePolicy.FAIL
}
)
}.configureAddressSecurity()
// JMX enablement
if (config.exportJMXto.isNotEmpty()) {isJMXManagementEnabled = true
isJMXUseBrokerName = true}
}.configureAddressSecurity()
private fun queueConfig(name: String, address: String = name, filter: String? = null, durable: Boolean): CoreQueueConfiguration {
return CoreQueueConfiguration().apply {
@ -229,13 +238,11 @@ 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))
securityRoles[RPCApi.RPC_SERVER_QUEUE_NAME] = setOf(nodeInternalRole, restrictedRole(RPC_ROLE, send = true))
// TODO: remove the NODE_USER role below once the webserver doesn't need it anymore.
securityRoles["${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.$NODE_USER.#"] = setOf(nodeInternalRole)
// Each RPC user must have its own role and its own queue. This prevents users accessing each other's queues
// and stealing RPC responses.
val rolesAdderOnLogin = RolesAdderOnLogin { username ->
@ -282,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))
}
@ -557,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
@ -567,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]!!
@ -598,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)
@ -629,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()
}
@ -357,9 +357,6 @@ class RPCServer(
observableMap.cleanUp()
}
// TODO remove this User once webserver doesn't need it
private val nodeUser = User(NODE_USER, NODE_USER, setOf())
private fun ClientMessage.context(sessionId: Trace.SessionId): RpcAuthContext {
val trace = Trace.newInstance(sessionId = sessionId)
val externalTrace = externalTrace()
@ -368,19 +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)
return if (rpcUser != null) {
Actor(Id(rpcUser.username), userService.id, targetLegalIdentity) to RpcPermissions(rpcUser.permissions)
} else if (CordaX500Name.parse(validatedUser) == nodeLegalName) {
// TODO remove this after Shell and WebServer will no longer need it
Actor(Id(nodeUser.username), userService.id, targetLegalIdentity) to RpcPermissions(nodeUser.permissions)
} else {
throw IllegalArgumentException("Validated user '$validatedUser' is not an RPC user nor the NODE user")
}
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

@ -11,6 +11,7 @@ dataSourceProperties = {
}
database = {
transactionIsolationLevel = "REPEATABLE_READ"
exportHibernateJMXStatistics = "false"
}
devMode = true
useHTTPS = false

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)
*/