[CORDA-827] Improved unit tests coverage and documentation (#2229)

* Extend unit test on RPCSecurityManager
* Fix corner cases in permission parsing and bug in tryAuthenticate
* Rework docsite page
* Add missing ChangeLog entry
This commit is contained in:
igor nitto
2017-12-13 17:09:09 +00:00
committed by GitHub
parent 929341e7ee
commit 5720697b0d
10 changed files with 309 additions and 155 deletions

View File

@ -34,7 +34,7 @@ fun RPCSecurityManager.tryAuthenticate(principal: String, password: Password): A
password.use {
return try {
authenticate(principal, password)
} catch (e: AuthenticationException) {
} catch (e: FailedLoginException) {
null
}
}

View File

@ -95,8 +95,8 @@ class RPCSecurityManagerImpl(config: AuthServiceConfig) : RPCSecurityManager {
// Setup optional cache layer if configured
it.cacheManager = config.options?.cache?.let {
GuavaCacheManager(
timeToLiveSeconds = it.expiryTimeInSecs,
maxSize = it.capacity)
timeToLiveSeconds = it.expireAfterSecs,
maxSize = it.maxEntries)
}
}
}
@ -149,22 +149,29 @@ private object RPCPermissionResolver : PermissionResolver {
private val ACTION_START_FLOW = "startflow"
private val ACTION_INVOKE_RPC = "invokerpc"
private val ACTION_ALL = "all"
private val FLOW_RPC_CALLS = setOf("startFlowDynamic", "startTrackedFlowDynamic")
private val FLOW_RPC_CALLS = setOf(
"startFlowDynamic",
"startTrackedFlowDynamic",
"startFlow",
"startTrackedFlow")
override fun resolvePermission(representation: String): Permission {
val action = representation.substringBefore(SEPARATOR).toLowerCase()
val action = representation.substringBefore(SEPARATOR).toLowerCase()
when (action) {
ACTION_INVOKE_RPC -> {
val rpcCall = representation.substringAfter(SEPARATOR)
require(representation.count { it == SEPARATOR } == 1) {
val rpcCall = representation.substringAfter(SEPARATOR, "")
require(representation.count { it == SEPARATOR } == 1 && !rpcCall.isEmpty()) {
"Malformed permission string"
}
return RPCPermission(setOf(rpcCall))
val permitted = when(rpcCall) {
"startFlow" -> setOf("startFlowDynamic", rpcCall)
"startTrackedFlow" -> setOf("startTrackedFlowDynamic", rpcCall)
else -> setOf(rpcCall)
}
return RPCPermission(permitted)
}
ACTION_START_FLOW -> {
val targetFlow = representation.substringAfter(SEPARATOR)
val targetFlow = representation.substringAfter(SEPARATOR, "")
require(targetFlow.isNotEmpty()) {
"Missing target flow after StartFlow"
}

View File

@ -196,7 +196,7 @@ data class SecurityConfiguration(val authService: SecurityConfiguration.AuthServ
data class Options(val cache: Options.Cache?) {
// Cache parameters
data class Cache(val expiryTimeInSecs: Long, val capacity: Long)
data class Cache(val expireAfterSecs: Long, val maxEntries: Long)
}

View File

@ -1,10 +1,19 @@
package net.corda.node.services
import net.corda.core.context.AuthServiceId
import net.corda.core.flows.FlowLogic
import net.corda.core.messaging.CordaRPCOps
import net.corda.node.internal.security.Password
import net.corda.node.internal.security.RPCSecurityManagerImpl
import net.corda.node.internal.security.tryAuthenticate
import net.corda.nodeapi.internal.config.User
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Test
import javax.security.auth.login.FailedLoginException
import kotlin.reflect.KFunction
import kotlin.test.assertFails
import kotlin.test.assertFailsWith
import kotlin.test.assertNull
class RPCSecurityManagerTest {
@ -15,7 +24,147 @@ class RPCSecurityManagerTest {
assertThatThrownBy { configWithRPCUsername("user#1") }.hasMessageContaining("#")
}
private fun configWithRPCUsername(username: String) {
RPCSecurityManagerImpl.fromUserList(users = listOf(User(username, "password", setOf())), id = AuthServiceId("TEST"))
@Test
fun `Generic RPC call authorization`() {
checkUserPermissions(
permitted = setOf(arrayListOf("nodeInfo"), arrayListOf("notaryIdentities")),
permissions = setOf(
Permissions.invokeRpc(CordaRPCOps::nodeInfo),
Permissions.invokeRpc(CordaRPCOps::notaryIdentities)))
}
@Test
fun `Flow invocation authorization`() {
checkUserPermissions(
permissions = setOf(Permissions.startFlow<DummyFlow>()),
permitted = setOf(
arrayListOf("startTrackedFlowDynamic", "net.corda.node.services.RPCSecurityManagerTest\$DummyFlow"),
arrayListOf("startFlowDynamic", "net.corda.node.services.RPCSecurityManagerTest\$DummyFlow")))
}
@Test
fun `Check startFlow RPC permission implies startFlowDynamic`() {
checkUserPermissions(
permissions = setOf(Permissions.invokeRpc("startFlow")),
permitted = setOf(arrayListOf("startFlow"), arrayListOf("startFlowDynamic")))
}
@Test
fun `Check startTrackedFlow RPC permission implies startTrackedFlowDynamic`() {
checkUserPermissions(
permitted = setOf(arrayListOf("startTrackedFlow"), arrayListOf("startTrackedFlowDynamic")),
permissions = setOf(Permissions.invokeRpc("startTrackedFlow")))
}
@Test
fun `Admin authorization`() {
checkUserPermissions(
permissions = setOf("all"),
permitted = allActions.map { arrayListOf(it) }.toSet())
}
@Test
fun `Malformed permission strings`() {
assertMalformedPermission("bar")
assertMalformedPermission("InvokeRpc.nodeInfo.XXX")
assertMalformedPermission("")
assertMalformedPermission(".")
assertMalformedPermission("..")
assertMalformedPermission("startFlow")
assertMalformedPermission("startFlow.")
}
@Test
fun `Login with unknown user`() {
val userRealm = RPCSecurityManagerImpl.fromUserList(
users = listOf(User("user", "xxxx", emptySet())),
id = AuthServiceId("TEST"))
userRealm.authenticate("user", Password("xxxx"))
assertFailsWith(FailedLoginException::class, "Login with wrong password should fail") {
userRealm.authenticate("foo", Password("xxxx"))
}
assertNull(userRealm.tryAuthenticate("foo", Password("wrong")),
"Login with wrong password should fail")
}
@Test
fun `Login with wrong credentials`() {
val userRealm = RPCSecurityManagerImpl.fromUserList(
users = listOf(User("user", "password", emptySet())),
id = AuthServiceId("TEST"))
userRealm.authenticate("user", Password("password"))
assertFailsWith(FailedLoginException::class, "Login with wrong password should fail") {
userRealm.authenticate("user", Password("wrong"))
}
assertNull(userRealm.tryAuthenticate("user", Password("wrong")),
"Login with wrong password should fail")
}
@Test
fun `Build invalid subject`() {
val userRealm = RPCSecurityManagerImpl.fromUserList(
users = listOf(User("user", "password", emptySet())),
id = AuthServiceId("TEST"))
val subject = userRealm.buildSubject("foo")
for (action in allActions) {
assert(!subject.isPermitted(action)) {
"Invalid subject should not be allowed to call $action"
}
}
}
private fun configWithRPCUsername(username: String) {
RPCSecurityManagerImpl.fromUserList(
users = listOf(User(username, "password", setOf())), id = AuthServiceId("TEST"))
}
private fun checkUserPermissions(permissions: Set<String>, permitted: Set<ArrayList<String>>) {
val user = User(username = "user", password = "password", permissions = permissions)
val userRealms = RPCSecurityManagerImpl.fromUserList(users = listOf(user), id = AuthServiceId("TEST"))
val disabled = allActions.filter { !permitted.contains(listOf(it)) }
for (subject in listOf(
userRealms.authenticate("user", Password("password")),
userRealms.tryAuthenticate("user", Password("password"))!!,
userRealms.buildSubject("user"))) {
for (request in permitted) {
val call = request.first()
val args = request.drop(1).toTypedArray()
assert(subject.isPermitted(request.first(), *args)) {
"User ${subject.principal} should be permitted ${call} with target '${request.toList()}'"
}
if (args.isEmpty()) {
assert(subject.isPermitted(request.first(), "XXX")) {
"User ${subject.principal} should be permitted ${call} with any target"
}
}
}
disabled.forEach {
assert(!subject.isPermitted(it)) {
"Permissions $permissions should not allow to call $it"
}
}
disabled.filter { !permitted.contains(listOf(it, "foo")) }.forEach {
assert(!subject.isPermitted(it, "foo")) {
"Permissions $permissions should not allow to call $it with argument 'foo'"
}
}
}
}
private fun assertMalformedPermission(permission: String) {
assertFails {
RPCSecurityManagerImpl.fromUserList(
users = listOf(User("x", "x", setOf(permission))),
id = AuthServiceId("TEST"))
}
}
companion object {
private val allActions = CordaRPCOps::class.members.filterIsInstance<KFunction<*>>().map { it.name }.toSet() +
setOf("startFlow", "startTrackedFlow")
}
private abstract class DummyFlow : FlowLogic<Unit>()
}