mirror of
https://github.com/corda/corda.git
synced 2025-06-13 04:38:19 +00:00
[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:
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
}
|
||||
|
||||
|
@ -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>()
|
||||
}
|
Reference in New Issue
Block a user