mirror of
https://github.com/corda/corda.git
synced 2024-12-28 16:58:55 +00:00
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:
commit
74c2eb8a0a
@ -1,4 +1,4 @@
|
||||
package net.corda.node.services
|
||||
package net.corda.node
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
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.StartedNode
|
||||
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.SecurityConfiguration
|
||||
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.internal.toDatabaseSchemaName
|
||||
import org.apache.activemq.artemis.api.core.ActiveMQSecurityException
|
||||
import org.apache.shiro.authc.credential.DefaultPasswordService
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.ClassRule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.Parameterized
|
||||
import java.sql.DriverManager
|
||||
import java.sql.Statement
|
||||
import java.util.*
|
||||
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 {
|
||||
@ClassRule @JvmField
|
||||
val databaseSchemas = IntegrationTestSchemas(ALICE_NAME.toDatabaseSchemaName())
|
||||
}
|
||||
|
||||
protected lateinit var node: StartedNode<Node>
|
||||
protected lateinit var client: CordaRPCClient
|
||||
private lateinit var node: StartedNode<Node>
|
||||
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
|
||||
fun `login with correct credentials`() {
|
||||
@ -65,7 +132,7 @@ abstract class UserAuthServiceTest : NodeBasedTest() {
|
||||
val proxy = it.proxy
|
||||
proxy.startFlowDynamic(DummyFlow::class.java)
|
||||
proxy.startTrackedFlowDynamic(DummyFlow::class.java)
|
||||
proxy.startFlow(::DummyFlow)
|
||||
proxy.startFlow(AuthDBTests::DummyFlow)
|
||||
assertFailsWith(
|
||||
PermissionException::class,
|
||||
"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
|
||||
fun `Add new users on-the-fly`() {
|
||||
fun `Add new users dynamically`() {
|
||||
assertFailsWith(
|
||||
ActiveMQSecurityException::class,
|
||||
"Login with incorrect password should fail") {
|
||||
@ -171,7 +169,7 @@ class UserAuthServiceTestsJDBC : UserAuthServiceTest() {
|
||||
|
||||
db.insert(UserAndRoles(
|
||||
username = "user2",
|
||||
password = "bar",
|
||||
password = encodePassword("bar"),
|
||||
roles = listOf("default")))
|
||||
|
||||
client.start("user2", "bar")
|
||||
@ -181,10 +179,9 @@ class UserAuthServiceTestsJDBC : UserAuthServiceTest() {
|
||||
fun `Modify user permissions during RPC session`() {
|
||||
db.insert(UserAndRoles(
|
||||
username = "user3",
|
||||
password = "bar",
|
||||
password = encodePassword("bar"),
|
||||
roles = emptyList()))
|
||||
|
||||
|
||||
client.start("user3", "bar").use {
|
||||
val proxy = it.proxy
|
||||
assertFailsWith(
|
||||
@ -193,6 +190,7 @@ class UserAuthServiceTestsJDBC : UserAuthServiceTest() {
|
||||
proxy.stateMachinesFeed()
|
||||
}
|
||||
db.addRoleToUser("user3", "default")
|
||||
Thread.sleep(1500)
|
||||
proxy.stateMachinesFeed()
|
||||
}
|
||||
}
|
||||
@ -201,13 +199,14 @@ class UserAuthServiceTestsJDBC : UserAuthServiceTest() {
|
||||
fun `Revoke user permissions during RPC session`() {
|
||||
db.insert(UserAndRoles(
|
||||
username = "user4",
|
||||
password = "test",
|
||||
password = encodePassword("test"),
|
||||
roles = listOf("default")))
|
||||
|
||||
client.start("user4", "test").use {
|
||||
val proxy = it.proxy
|
||||
proxy.stateMachinesFeed()
|
||||
db.deleteUser("user4")
|
||||
Thread.sleep(1500)
|
||||
assertFailsWith(
|
||||
PermissionException::class,
|
||||
"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
|
||||
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 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 {
|
||||
|
||||
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) {
|
||||
session {
|
||||
it.execute("DELETE FROM users WHERE username = '$username'")
|
||||
@ -307,4 +312,22 @@ 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())
|
||||
}
|
@ -26,6 +26,9 @@ import java.net.ConnectException
|
||||
import java.util.regex.Pattern
|
||||
import kotlin.test.assertTrue
|
||||
import kotlin.test.fail
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.Ignore
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class SSHServerTest : IntegrationTest() {
|
||||
companion object {
|
||||
@ -33,6 +36,7 @@ class SSHServerTest : IntegrationTest() {
|
||||
val databaseSchemas = IntegrationTestSchemas(ALICE_NAME.toDatabaseSchemaName())
|
||||
}
|
||||
|
||||
@Ignore("Test has undeterministic capacity to hang, ignore till fixed")
|
||||
@Test()
|
||||
fun `ssh server does not start be default`() {
|
||||
val user = User("u", "p", setOf())
|
||||
@ -54,6 +58,7 @@ class SSHServerTest : IntegrationTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Ignore("Test has undeterministic capacity to hang, ignore till fixed")
|
||||
@Test
|
||||
fun `ssh server starts when configured`() {
|
||||
val user = User("u", "p", setOf())
|
||||
@ -74,6 +79,7 @@ class SSHServerTest : IntegrationTest() {
|
||||
}
|
||||
|
||||
|
||||
@Ignore("Test has undeterministic capacity to hang, ignore till fixed")
|
||||
@Test
|
||||
fun `ssh server verify credentials`() {
|
||||
val user = User("u", "p", setOf())
|
||||
@ -97,6 +103,7 @@ class SSHServerTest : IntegrationTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Ignore("Test has undeterministic capacity to hang, ignore till fixed")
|
||||
@Test
|
||||
fun `ssh respects permissions`() {
|
||||
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
|
||||
fun `ssh runs flows`() {
|
||||
val user = User("u", "p", setOf(startFlow<FlowICanRun>()))
|
||||
|
@ -203,8 +203,16 @@ data class SecurityConfiguration(val authService: SecurityConfiguration.AuthServ
|
||||
data class Options(val cache: Options.Cache?) {
|
||||
|
||||
// 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
|
||||
@ -228,12 +236,13 @@ data class SecurityConfiguration(val authService: SecurityConfiguration.AuthServ
|
||||
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"))
|
||||
fun fromUsers(users: List<User>, encryption: PasswordEncryption = PasswordEncryption.NONE) =
|
||||
AuthService(
|
||||
dataSource = DataSource(
|
||||
type = AuthDataSourceType.INMEMORY,
|
||||
users = users,
|
||||
passwordEncryption = encryption),
|
||||
id = AuthServiceId("NODE_CONFIG"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import net.corda.node.internal.security.Password
|
||||
import net.corda.node.internal.security.RPCSecurityManagerImpl
|
||||
import net.corda.node.internal.security.tryAuthenticate
|
||||
import net.corda.nodeapi.internal.config.User
|
||||
import net.corda.node.services.config.SecurityConfiguration
|
||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||
import org.junit.Test
|
||||
import javax.security.auth.login.FailedLoginException
|
||||
@ -26,7 +27,7 @@ class RPCSecurityManagerTest {
|
||||
|
||||
@Test
|
||||
fun `Generic RPC call authorization`() {
|
||||
checkUserPermissions(
|
||||
checkUserActions(
|
||||
permitted = setOf(arrayListOf("nodeInfo"), arrayListOf("notaryIdentities")),
|
||||
permissions = setOf(
|
||||
Permissions.invokeRpc(CordaRPCOps::nodeInfo),
|
||||
@ -35,7 +36,7 @@ class RPCSecurityManagerTest {
|
||||
|
||||
@Test
|
||||
fun `Flow invocation authorization`() {
|
||||
checkUserPermissions(
|
||||
checkUserActions(
|
||||
permissions = setOf(Permissions.startFlow<DummyFlow>()),
|
||||
permitted = setOf(
|
||||
arrayListOf("startTrackedFlowDynamic", "net.corda.node.services.RPCSecurityManagerTest\$DummyFlow"),
|
||||
@ -44,21 +45,21 @@ class RPCSecurityManagerTest {
|
||||
|
||||
@Test
|
||||
fun `Check startFlow RPC permission implies startFlowDynamic`() {
|
||||
checkUserPermissions(
|
||||
checkUserActions(
|
||||
permissions = setOf(Permissions.invokeRpc("startFlow")),
|
||||
permitted = setOf(arrayListOf("startFlow"), arrayListOf("startFlowDynamic")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Check startTrackedFlow RPC permission implies startTrackedFlowDynamic`() {
|
||||
checkUserPermissions(
|
||||
checkUserActions(
|
||||
permitted = setOf(arrayListOf("startTrackedFlow"), arrayListOf("startTrackedFlowDynamic")),
|
||||
permissions = setOf(Permissions.invokeRpc("startTrackedFlow")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Admin authorization`() {
|
||||
checkUserPermissions(
|
||||
checkUserActions(
|
||||
permissions = setOf("all"),
|
||||
permitted = allActions.map { arrayListOf(it) }.toSet())
|
||||
}
|
||||
@ -118,9 +119,9 @@ class RPCSecurityManagerTest {
|
||||
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 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)) }
|
||||
for (subject in listOf(
|
||||
userRealms.authenticate("user", Password("password")),
|
||||
|
Loading…
Reference in New Issue
Block a user