Merge remote-tracking branch 'open/master' into shams-os-merge-040118

# Conflicts:
#	node/src/integration-test/kotlin/net/corda/node/AuthDBTests.kt
#	node/src/integration-test/kotlin/net/corda/node/SSHServerTest.kt
This commit is contained in:
Shams Asari 2018-01-05 14:13:36 +00:00
commit 74c2eb8a0a
4 changed files with 144 additions and 103 deletions

View File

@ -1,4 +1,4 @@
package net.corda.node.services package net.corda.node
import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.fibers.Suspendable
import net.corda.client.rpc.CordaRPCClient import net.corda.client.rpc.CordaRPCClient
@ -12,6 +12,7 @@ import net.corda.finance.flows.CashIssueFlow
import net.corda.node.internal.Node import net.corda.node.internal.Node
import net.corda.node.internal.StartedNode import net.corda.node.internal.StartedNode
import net.corda.node.services.config.AuthDataSourceType import net.corda.node.services.config.AuthDataSourceType
import net.corda.node.services.Permissions
import net.corda.node.services.config.PasswordEncryption import net.corda.node.services.config.PasswordEncryption
import net.corda.node.services.config.SecurityConfiguration import net.corda.node.services.config.SecurityConfiguration
import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.config.User
@ -21,23 +22,89 @@ import net.corda.testing.internal.IntegrationTestSchemas
import net.corda.testing.node.internal.NodeBasedTest import net.corda.testing.node.internal.NodeBasedTest
import net.corda.testing.internal.toDatabaseSchemaName import net.corda.testing.internal.toDatabaseSchemaName
import org.apache.activemq.artemis.api.core.ActiveMQSecurityException import org.apache.activemq.artemis.api.core.ActiveMQSecurityException
import org.apache.shiro.authc.credential.DefaultPasswordService
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.ClassRule import org.junit.ClassRule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import java.sql.DriverManager import java.sql.DriverManager
import java.sql.Statement import java.sql.Statement
import java.util.*
import kotlin.test.assertFailsWith import kotlin.test.assertFailsWith
abstract class UserAuthServiceTest : NodeBasedTest() { /*
* Starts Node's instance configured to load clients credentials and permissions from an external DB, then
* check authentication/authorization of RPC connections.
*/
@RunWith(Parameterized::class)
class AuthDBTests : NodeBasedTest() {
companion object { companion object {
@ClassRule @JvmField @ClassRule @JvmField
val databaseSchemas = IntegrationTestSchemas(ALICE_NAME.toDatabaseSchemaName()) val databaseSchemas = IntegrationTestSchemas(ALICE_NAME.toDatabaseSchemaName())
} }
protected lateinit var node: StartedNode<Node> private lateinit var node: StartedNode<Node>
protected lateinit var client: CordaRPCClient private lateinit var client: CordaRPCClient
private lateinit var db: UsersDB
companion object {
private val cacheExpireAfterSecs: Long = 1
@JvmStatic
@Parameterized.Parameters(name = "password encryption format = {0}")
fun encFormats() = arrayOf(PasswordEncryption.NONE, PasswordEncryption.SHIRO_1_CRYPT)
}
@Parameterized.Parameter
lateinit var passwordEncryption: PasswordEncryption
@Before
fun setup() {
db = UsersDB(
name = "SecurityDataSourceTestDB",
users = listOf(UserAndRoles(username = "user",
password = encodePassword("foo", passwordEncryption),
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")
)))
val securityConfig = mapOf(
"security" to mapOf(
"authService" to mapOf(
"dataSource" to mapOf(
"type" to "DB",
"passwordEncryption" to passwordEncryption.toString(),
"connection" to mapOf(
"jdbcUrl" to db.jdbcUrl,
"username" to "",
"password" to "",
"driverClassName" to "org.h2.Driver"
)
)
),
"options" to mapOf(
"cache" to mapOf(
"expireAfterSecs" to cacheExpireAfterSecs,
"maxEntries" to 50
)
)
)
)
node = startNode(ALICE_NAME, rpcUsers = emptyList(), configOverrides = securityConfig)
client = CordaRPCClient(node.internals.configuration.rpcAddress!!)
}
@Test @Test
fun `login with correct credentials`() { fun `login with correct credentials`() {
@ -65,7 +132,7 @@ abstract class UserAuthServiceTest : NodeBasedTest() {
val proxy = it.proxy val proxy = it.proxy
proxy.startFlowDynamic(DummyFlow::class.java) proxy.startFlowDynamic(DummyFlow::class.java)
proxy.startTrackedFlowDynamic(DummyFlow::class.java) proxy.startTrackedFlowDynamic(DummyFlow::class.java)
proxy.startFlow(::DummyFlow) proxy.startFlow(AuthDBTests::DummyFlow)
assertFailsWith( assertFailsWith(
PermissionException::class, PermissionException::class,
"This user should not be authorized to start flow `CashIssueFlow`") { "This user should not be authorized to start flow `CashIssueFlow`") {
@ -92,77 +159,8 @@ abstract class UserAuthServiceTest : NodeBasedTest() {
} }
} }
@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 @Test
fun `Add new users on-the-fly`() { fun `Add new users dynamically`() {
assertFailsWith( assertFailsWith(
ActiveMQSecurityException::class, ActiveMQSecurityException::class,
"Login with incorrect password should fail") { "Login with incorrect password should fail") {
@ -171,7 +169,7 @@ class UserAuthServiceTestsJDBC : UserAuthServiceTest() {
db.insert(UserAndRoles( db.insert(UserAndRoles(
username = "user2", username = "user2",
password = "bar", password = encodePassword("bar"),
roles = listOf("default"))) roles = listOf("default")))
client.start("user2", "bar") client.start("user2", "bar")
@ -181,10 +179,9 @@ class UserAuthServiceTestsJDBC : UserAuthServiceTest() {
fun `Modify user permissions during RPC session`() { fun `Modify user permissions during RPC session`() {
db.insert(UserAndRoles( db.insert(UserAndRoles(
username = "user3", username = "user3",
password = "bar", password = encodePassword("bar"),
roles = emptyList())) roles = emptyList()))
client.start("user3", "bar").use { client.start("user3", "bar").use {
val proxy = it.proxy val proxy = it.proxy
assertFailsWith( assertFailsWith(
@ -193,6 +190,7 @@ class UserAuthServiceTestsJDBC : UserAuthServiceTest() {
proxy.stateMachinesFeed() proxy.stateMachinesFeed()
} }
db.addRoleToUser("user3", "default") db.addRoleToUser("user3", "default")
Thread.sleep(1500)
proxy.stateMachinesFeed() proxy.stateMachinesFeed()
} }
} }
@ -201,13 +199,14 @@ class UserAuthServiceTestsJDBC : UserAuthServiceTest() {
fun `Revoke user permissions during RPC session`() { fun `Revoke user permissions during RPC session`() {
db.insert(UserAndRoles( db.insert(UserAndRoles(
username = "user4", username = "user4",
password = "test", password = encodePassword("test"),
roles = listOf("default"))) roles = listOf("default")))
client.start("user4", "test").use { client.start("user4", "test").use {
val proxy = it.proxy val proxy = it.proxy
proxy.stateMachinesFeed() proxy.stateMachinesFeed()
db.deleteUser("user4") db.deleteUser("user4")
Thread.sleep(1500)
assertFailsWith( assertFailsWith(
PermissionException::class, PermissionException::class,
"This user should not be authorized to call 'nodeInfo'") { "This user should not be authorized to call 'nodeInfo'") {
@ -216,15 +215,27 @@ class UserAuthServiceTestsJDBC : UserAuthServiceTest() {
} }
} }
@StartableByRPC
@InitiatingFlow
class DummyFlow : FlowLogic<Unit>() {
@Suspendable
override fun call() = Unit
}
@After @After
override fun tearDown() { override fun tearDown() {
db.close() db.close()
} }
private fun encodePassword(s: String) = encodePassword(s, passwordEncryption)
} }
private data class UserAndRoles(val username: String, val password: String, val roles: List<String>) 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 data class RoleAndPermissions(val role: String, val permissions: List<String>)
/*
* Manage in-memory DB mocking a users database with the schema expected by Node's security manager
*/
private class UsersDB : AutoCloseable { private class UsersDB : AutoCloseable {
val jdbcUrl: String val jdbcUrl: String
@ -261,12 +272,6 @@ private class UsersDB : AutoCloseable {
} }
} }
fun deleteRole(role: String) {
session {
it.execute("DELETE FROM role_permissions WHERE role_name = '$role'")
}
}
fun deleteUser(username: String) { fun deleteUser(username: String) {
session { session {
it.execute("DELETE FROM users WHERE username = '$username'") it.execute("DELETE FROM users WHERE username = '$username'")
@ -308,3 +313,21 @@ private class UsersDB : AutoCloseable {
} }
} }
} }
/*
* Sample of hardcoded hashes to watch for format backward compatibility
*/
private val hashedPasswords = mapOf(
PasswordEncryption.SHIRO_1_CRYPT to mapOf(
"foo" to "\$shiro1\$SHA-256$500000\$WSiEVj6q8d02sFcCk1dkoA==\$MBkU/ghdD9ovoDerdzNfkXdP9Bdhmok7tidvVIqGzcA=",
"bar" to "\$shiro1\$SHA-256$500000\$Q6dmdY1uVMm0LYAWaOHtCA==\$u7NbFaj9tHf2RTW54jedLPiOiGjJv0RVEPIjVquJuYY=",
"test" to "\$shiro1\$SHA-256$500000\$F6CWSFDDxGTlzvREwih8Gw==\$DQhyAPoUw3RdvNYJ1aubCnzEIXm+szGQ3HplaG+euz8="))
/*
* A functional object for producing password encoded according to the given scheme.
*/
private fun encodePassword(s: String, format: PasswordEncryption) = when (format) {
PasswordEncryption.NONE -> s
PasswordEncryption.SHIRO_1_CRYPT -> hashedPasswords[format]!![s] ?:
DefaultPasswordService().encryptPassword(s.toCharArray())
}

View File

@ -26,6 +26,9 @@ import java.net.ConnectException
import java.util.regex.Pattern import java.util.regex.Pattern
import kotlin.test.assertTrue import kotlin.test.assertTrue
import kotlin.test.fail import kotlin.test.fail
import org.assertj.core.api.Assertions.assertThat
import org.junit.Ignore
import java.util.regex.Pattern
class SSHServerTest : IntegrationTest() { class SSHServerTest : IntegrationTest() {
companion object { companion object {
@ -33,6 +36,7 @@ class SSHServerTest : IntegrationTest() {
val databaseSchemas = IntegrationTestSchemas(ALICE_NAME.toDatabaseSchemaName()) val databaseSchemas = IntegrationTestSchemas(ALICE_NAME.toDatabaseSchemaName())
} }
@Ignore("Test has undeterministic capacity to hang, ignore till fixed")
@Test() @Test()
fun `ssh server does not start be default`() { fun `ssh server does not start be default`() {
val user = User("u", "p", setOf()) val user = User("u", "p", setOf())
@ -54,6 +58,7 @@ class SSHServerTest : IntegrationTest() {
} }
} }
@Ignore("Test has undeterministic capacity to hang, ignore till fixed")
@Test @Test
fun `ssh server starts when configured`() { fun `ssh server starts when configured`() {
val user = User("u", "p", setOf()) val user = User("u", "p", setOf())
@ -74,6 +79,7 @@ class SSHServerTest : IntegrationTest() {
} }
@Ignore("Test has undeterministic capacity to hang, ignore till fixed")
@Test @Test
fun `ssh server verify credentials`() { fun `ssh server verify credentials`() {
val user = User("u", "p", setOf()) val user = User("u", "p", setOf())
@ -97,6 +103,7 @@ class SSHServerTest : IntegrationTest() {
} }
} }
@Ignore("Test has undeterministic capacity to hang, ignore till fixed")
@Test @Test
fun `ssh respects permissions`() { fun `ssh respects permissions`() {
val user = User("u", "p", setOf(startFlow<FlowICanRun>())) val user = User("u", "p", setOf(startFlow<FlowICanRun>()))
@ -127,6 +134,7 @@ class SSHServerTest : IntegrationTest() {
} }
} }
@Ignore("Test has undeterministic capacity to hang, ignore till fixed")
@Test @Test
fun `ssh runs flows`() { fun `ssh runs flows`() {
val user = User("u", "p", setOf(startFlow<FlowICanRun>())) val user = User("u", "p", setOf(startFlow<FlowICanRun>()))

View File

@ -203,8 +203,16 @@ data class SecurityConfiguration(val authService: SecurityConfiguration.AuthServ
data class Options(val cache: Options.Cache?) { data class Options(val cache: Options.Cache?) {
// Cache parameters // Cache parameters
data class Cache(val expireAfterSecs: Long, val maxEntries: Long) data class Cache(val expireAfterSecs: Long, val maxEntries: Long) {
init {
require(expireAfterSecs >= 0) {
"Expected positive value for 'cache.expireAfterSecs'"
}
require(maxEntries > 0) {
"Expected positive value for 'cache.maxEntries'"
}
}
}
} }
// Provider of users credentials and permissions data // Provider of users credentials and permissions data
@ -228,11 +236,12 @@ data class SecurityConfiguration(val authService: SecurityConfiguration.AuthServ
AuthDataSourceType.DB -> AuthServiceId("REMOTE_DATABASE") AuthDataSourceType.DB -> AuthServiceId("REMOTE_DATABASE")
} }
fun fromUsers(users: List<User>) = AuthService( fun fromUsers(users: List<User>, encryption: PasswordEncryption = PasswordEncryption.NONE) =
AuthService(
dataSource = DataSource( dataSource = DataSource(
type = AuthDataSourceType.INMEMORY, type = AuthDataSourceType.INMEMORY,
users = users, users = users,
passwordEncryption = PasswordEncryption.NONE), passwordEncryption = encryption),
id = AuthServiceId("NODE_CONFIG")) id = AuthServiceId("NODE_CONFIG"))
} }
} }

View File

@ -7,6 +7,7 @@ import net.corda.node.internal.security.Password
import net.corda.node.internal.security.RPCSecurityManagerImpl import net.corda.node.internal.security.RPCSecurityManagerImpl
import net.corda.node.internal.security.tryAuthenticate import net.corda.node.internal.security.tryAuthenticate
import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.config.User
import net.corda.node.services.config.SecurityConfiguration
import org.assertj.core.api.Assertions.assertThatThrownBy import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Test import org.junit.Test
import javax.security.auth.login.FailedLoginException import javax.security.auth.login.FailedLoginException
@ -26,7 +27,7 @@ class RPCSecurityManagerTest {
@Test @Test
fun `Generic RPC call authorization`() { fun `Generic RPC call authorization`() {
checkUserPermissions( checkUserActions(
permitted = setOf(arrayListOf("nodeInfo"), arrayListOf("notaryIdentities")), permitted = setOf(arrayListOf("nodeInfo"), arrayListOf("notaryIdentities")),
permissions = setOf( permissions = setOf(
Permissions.invokeRpc(CordaRPCOps::nodeInfo), Permissions.invokeRpc(CordaRPCOps::nodeInfo),
@ -35,7 +36,7 @@ class RPCSecurityManagerTest {
@Test @Test
fun `Flow invocation authorization`() { fun `Flow invocation authorization`() {
checkUserPermissions( checkUserActions(
permissions = setOf(Permissions.startFlow<DummyFlow>()), permissions = setOf(Permissions.startFlow<DummyFlow>()),
permitted = setOf( permitted = setOf(
arrayListOf("startTrackedFlowDynamic", "net.corda.node.services.RPCSecurityManagerTest\$DummyFlow"), arrayListOf("startTrackedFlowDynamic", "net.corda.node.services.RPCSecurityManagerTest\$DummyFlow"),
@ -44,21 +45,21 @@ class RPCSecurityManagerTest {
@Test @Test
fun `Check startFlow RPC permission implies startFlowDynamic`() { fun `Check startFlow RPC permission implies startFlowDynamic`() {
checkUserPermissions( checkUserActions(
permissions = setOf(Permissions.invokeRpc("startFlow")), permissions = setOf(Permissions.invokeRpc("startFlow")),
permitted = setOf(arrayListOf("startFlow"), arrayListOf("startFlowDynamic"))) permitted = setOf(arrayListOf("startFlow"), arrayListOf("startFlowDynamic")))
} }
@Test @Test
fun `Check startTrackedFlow RPC permission implies startTrackedFlowDynamic`() { fun `Check startTrackedFlow RPC permission implies startTrackedFlowDynamic`() {
checkUserPermissions( checkUserActions(
permitted = setOf(arrayListOf("startTrackedFlow"), arrayListOf("startTrackedFlowDynamic")), permitted = setOf(arrayListOf("startTrackedFlow"), arrayListOf("startTrackedFlowDynamic")),
permissions = setOf(Permissions.invokeRpc("startTrackedFlow"))) permissions = setOf(Permissions.invokeRpc("startTrackedFlow")))
} }
@Test @Test
fun `Admin authorization`() { fun `Admin authorization`() {
checkUserPermissions( checkUserActions(
permissions = setOf("all"), permissions = setOf("all"),
permitted = allActions.map { arrayListOf(it) }.toSet()) permitted = allActions.map { arrayListOf(it) }.toSet())
} }
@ -118,9 +119,9 @@ class RPCSecurityManagerTest {
users = listOf(User(username, "password", setOf())), id = AuthServiceId("TEST")) users = listOf(User(username, "password", setOf())), id = AuthServiceId("TEST"))
} }
private fun checkUserPermissions(permissions: Set<String>, permitted: Set<ArrayList<String>>) { private fun checkUserActions(permissions: Set<String>, permitted: Set<ArrayList<String>>) {
val user = User(username = "user", password = "password", permissions = permissions) val user = User(username = "user", password = "password", permissions = permissions)
val userRealms = RPCSecurityManagerImpl.fromUserList(users = listOf(user), id = AuthServiceId("TEST")) val userRealms = RPCSecurityManagerImpl(SecurityConfiguration.AuthService.fromUsers(listOf(user)))
val disabled = allActions.filter { !permitted.contains(listOf(it)) } val disabled = allActions.filter { !permitted.contains(listOf(it)) }
for (subject in listOf( for (subject in listOf(
userRealms.authenticate("user", Password("password")), userRealms.authenticate("user", Password("password")),