mirror of
https://github.com/corda/corda.git
synced 2025-06-19 07:38:22 +00:00
[CORDA-758]: Permissions are now checked for each RPC method. (#1985)
* Permissions are now checked for each RPC method. * Fixed NodeMonitorModelTest * Fixed IRSDemoTest
This commit is contained in:
committed by
GitHub
parent
a21d361df8
commit
d882f8871e
@ -7,7 +7,7 @@ import net.corda.core.internal.div
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.node.internal.NodeStartup
|
||||
import net.corda.node.services.FlowPermissions.Companion.startFlowPermission
|
||||
import net.corda.node.services.Permissions.Companion.startFlow
|
||||
import net.corda.nodeapi.User
|
||||
import net.corda.nodeapi.internal.ServiceInfo
|
||||
import net.corda.nodeapi.internal.ServiceType
|
||||
@ -29,7 +29,7 @@ class BootTests {
|
||||
@Test
|
||||
fun `java deserialization is disabled`() {
|
||||
driver {
|
||||
val user = User("u", "p", setOf(startFlowPermission<ObjectInputStreamFlow>()))
|
||||
val user = User("u", "p", setOf(startFlow<ObjectInputStreamFlow>()))
|
||||
val future = startNode(rpcUsers = listOf(user)).getOrThrow().rpcClientToNode().
|
||||
start(user.username, user.password).proxy.startFlow(::ObjectInputStreamFlow).returnValue
|
||||
assertThatThrownBy { future.getOrThrow() }.isInstanceOf(InvalidClassException::class.java).hasMessage("filter status: REJECTED")
|
||||
|
@ -7,7 +7,7 @@ import net.corda.core.internal.concurrent.transpose
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.unwrap
|
||||
import net.corda.node.services.FlowPermissions.Companion.startFlowPermission
|
||||
import net.corda.node.services.Permissions.Companion.startFlow
|
||||
import net.corda.nodeapi.User
|
||||
import net.corda.testing.ALICE
|
||||
import net.corda.testing.BOB
|
||||
@ -19,7 +19,7 @@ import org.junit.Test
|
||||
class CordappScanningDriverTest {
|
||||
@Test
|
||||
fun `sub-classed initiated flow pointing to the same initiating flow as its super-class`() {
|
||||
val user = User("u", "p", setOf(startFlowPermission<ReceiveFlow>()))
|
||||
val user = User("u", "p", setOf(startFlow<ReceiveFlow>()))
|
||||
// The driver will automatically pick up the annotated flows below
|
||||
driver {
|
||||
val (alice, bob) = listOf(
|
||||
|
@ -12,7 +12,7 @@ import net.corda.core.utilities.minutes
|
||||
import net.corda.finance.DOLLARS
|
||||
import net.corda.finance.flows.CashIssueFlow
|
||||
import net.corda.finance.flows.CashPaymentFlow
|
||||
import net.corda.node.services.FlowPermissions.Companion.startFlowPermission
|
||||
import net.corda.node.services.Permissions.Companion.startFlow
|
||||
import net.corda.nodeapi.User
|
||||
import net.corda.testing.DUMMY_NOTARY
|
||||
import net.corda.testing.chooseIdentity
|
||||
@ -59,7 +59,7 @@ class NodePerformanceTests {
|
||||
@Test
|
||||
fun `empty flow per second`() {
|
||||
driver(startNodesInProcess = true) {
|
||||
val a = startNode(rpcUsers = listOf(User("A", "A", setOf(startFlowPermission<EmptyFlow>())))).get()
|
||||
val a = startNode(rpcUsers = listOf(User("A", "A", setOf(startFlow<EmptyFlow>())))).get()
|
||||
|
||||
a.rpcClientToNode().use("A", "A") { connection ->
|
||||
val timings = Collections.synchronizedList(ArrayList<Long>())
|
||||
@ -89,7 +89,7 @@ class NodePerformanceTests {
|
||||
@Test
|
||||
fun `empty flow rate`() {
|
||||
driver(startNodesInProcess = true) {
|
||||
val a = startNode(rpcUsers = listOf(User("A", "A", setOf(startFlowPermission<EmptyFlow>())))).get()
|
||||
val a = startNode(rpcUsers = listOf(User("A", "A", setOf(startFlow<EmptyFlow>())))).get()
|
||||
a as NodeHandle.InProcess
|
||||
val metricRegistry = startReporter(shutdownManager, a.node.services.monitoringService.metrics)
|
||||
a.rpcClientToNode().use("A", "A") { connection ->
|
||||
@ -105,7 +105,7 @@ class NodePerformanceTests {
|
||||
driver(startNodesInProcess = true, extraCordappPackagesToScan = listOf("net.corda.finance")) {
|
||||
val a = startNotaryNode(
|
||||
DUMMY_NOTARY.name,
|
||||
rpcUsers = listOf(User("A", "A", setOf(startFlowPermission<CashIssueFlow>(), startFlowPermission<CashPaymentFlow>())))
|
||||
rpcUsers = listOf(User("A", "A", setOf(startFlow<CashIssueFlow>(), startFlow<CashPaymentFlow>())))
|
||||
).getOrThrow()
|
||||
a as NodeHandle.InProcess
|
||||
val metricRegistry = startReporter(shutdownManager, a.node.services.monitoringService.metrics)
|
||||
|
@ -11,7 +11,8 @@ import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.finance.POUNDS
|
||||
import net.corda.finance.flows.CashIssueFlow
|
||||
import net.corda.finance.flows.CashPaymentFlow
|
||||
import net.corda.node.services.FlowPermissions.Companion.startFlowPermission
|
||||
import net.corda.node.services.Permissions.Companion.startFlow
|
||||
import net.corda.node.services.Permissions.Companion.invokeRpc
|
||||
import net.corda.node.services.transactions.RaftValidatingNotaryService
|
||||
import net.corda.nodeapi.User
|
||||
import net.corda.testing.*
|
||||
@ -34,8 +35,10 @@ class DistributedServiceTests : DriverBasedTest() {
|
||||
// Start Alice and 3 notaries in a RAFT cluster
|
||||
val clusterSize = 3
|
||||
val testUser = User("test", "test", permissions = setOf(
|
||||
startFlowPermission<CashIssueFlow>(),
|
||||
startFlowPermission<CashPaymentFlow>())
|
||||
startFlow<CashIssueFlow>(),
|
||||
startFlow<CashPaymentFlow>(),
|
||||
invokeRpc(CordaRPCOps::nodeInfo),
|
||||
invokeRpc(CordaRPCOps::stateMachinesFeed))
|
||||
)
|
||||
val aliceFuture = startNode(providedName = ALICE.name, rpcUsers = listOf(testUser))
|
||||
val notariesFuture = startNotaryCluster(
|
||||
@ -137,4 +140,4 @@ class DistributedServiceTests : DriverBasedTest() {
|
||||
private fun paySelf(amount: Amount<Currency>) {
|
||||
aliceProxy.startFlow(::CashPaymentFlow, amount, alice.nodeInfo.chooseIdentity()).returnValue.getOrThrow()
|
||||
}
|
||||
}
|
||||
}
|
@ -17,7 +17,8 @@ import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.node.services.FlowPermissions
|
||||
import net.corda.node.services.Permissions.Companion.startFlow
|
||||
import net.corda.node.services.Permissions.Companion.invokeRpc
|
||||
import net.corda.nodeapi.User
|
||||
import net.corda.testing.DUMMY_NOTARY
|
||||
import net.corda.testing.chooseIdentity
|
||||
@ -39,7 +40,7 @@ class NodeStatePersistenceTests {
|
||||
// More investigation is needed to establish why.
|
||||
Assume.assumeFalse(System.getProperty("os.name").toLowerCase().startsWith("win"))
|
||||
|
||||
val user = User("mark", "dadada", setOf(FlowPermissions.startFlowPermission<SendMessageFlow>()))
|
||||
val user = User("mark", "dadada", setOf(startFlow<SendMessageFlow>(), invokeRpc("vaultQuery")))
|
||||
val message = Message("Hello world!")
|
||||
driver(isDebug = true, startNodesInProcess = isQuasarAgentSpecified()) {
|
||||
val (nodeName, notaryNodeHandle) = {
|
||||
|
@ -154,7 +154,7 @@ abstract class AbstractNode(config: NodeConfiguration,
|
||||
|
||||
/** The implementation of the [CordaRPCOps] interface used by this node. */
|
||||
open fun makeRPCOps(flowStarter: FlowStarter): CordaRPCOps {
|
||||
return CordaRPCOpsImpl(services, smm, database, flowStarter)
|
||||
return SecureCordaRPCOps(services, smm, database, flowStarter)
|
||||
}
|
||||
|
||||
private fun saveOwnNodeInfo() {
|
||||
|
@ -20,11 +20,9 @@ import net.corda.core.node.services.vault.QueryCriteria
|
||||
import net.corda.core.node.services.vault.Sort
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.node.services.FlowPermissions.Companion.startFlowPermission
|
||||
import net.corda.node.services.api.FlowStarter
|
||||
import net.corda.node.services.api.ServiceHubInternal
|
||||
import net.corda.node.services.messaging.getRpcContext
|
||||
import net.corda.node.services.messaging.requirePermission
|
||||
import net.corda.node.services.messaging.rpcContext
|
||||
import net.corda.node.services.statemachine.StateMachineManager
|
||||
import net.corda.node.utilities.CordaPersistence
|
||||
import rx.Observable
|
||||
@ -36,7 +34,7 @@ import java.time.Instant
|
||||
* Server side implementations of RPCs available to MQ based client tools. Execution takes place on the server
|
||||
* thread (i.e. serially). Arguments are serialised and deserialised automatically.
|
||||
*/
|
||||
class CordaRPCOpsImpl(
|
||||
internal class CordaRPCOpsImpl(
|
||||
private val services: ServiceHubInternal,
|
||||
private val smm: StateMachineManager,
|
||||
private val database: CordaPersistence,
|
||||
@ -149,9 +147,7 @@ class CordaRPCOpsImpl(
|
||||
|
||||
private fun <T> startFlow(logicType: Class<out FlowLogic<T>>, args: Array<out Any?>): FlowStateMachine<T> {
|
||||
require(logicType.isAnnotationPresent(StartableByRPC::class.java)) { "${logicType.name} was not designed for RPC" }
|
||||
val rpcContext = getRpcContext()
|
||||
rpcContext.requirePermission(startFlowPermission(logicType))
|
||||
val currentUser = FlowInitiator.RPC(rpcContext.currentUser.username)
|
||||
val currentUser = FlowInitiator.RPC(rpcContext().currentUser.username)
|
||||
// TODO RPC flows should have mapping user -> identity that should be resolved automatically on starting flow.
|
||||
return flowStarter.invokeFlowAsync(logicType, currentUser, *args).getOrThrow()
|
||||
}
|
||||
@ -221,6 +217,38 @@ class CordaRPCOpsImpl(
|
||||
}
|
||||
}
|
||||
|
||||
override fun <T : ContractState> vaultQuery(contractStateType: Class<out T>): Vault.Page<T> {
|
||||
return vaultQueryBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), contractStateType)
|
||||
}
|
||||
|
||||
override fun <T : ContractState> vaultQueryByCriteria(criteria: QueryCriteria, contractStateType: Class<out T>): Vault.Page<T> {
|
||||
return vaultQueryBy(criteria, PageSpecification(), Sort(emptySet()), contractStateType)
|
||||
}
|
||||
|
||||
override fun <T : ContractState> vaultQueryByWithPagingSpec(contractStateType: Class<out T>, criteria: QueryCriteria, paging: PageSpecification): Vault.Page<T> {
|
||||
return vaultQueryBy(criteria, paging, Sort(emptySet()), contractStateType)
|
||||
}
|
||||
|
||||
override fun <T : ContractState> vaultQueryByWithSorting(contractStateType: Class<out T>, criteria: QueryCriteria, sorting: Sort): Vault.Page<T> {
|
||||
return vaultQueryBy(criteria, PageSpecification(), sorting, contractStateType)
|
||||
}
|
||||
|
||||
override fun <T : ContractState> vaultTrack(contractStateType: Class<out T>): DataFeed<Vault.Page<T>, Vault.Update<T>> {
|
||||
return vaultTrackBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), contractStateType)
|
||||
}
|
||||
|
||||
override fun <T : ContractState> vaultTrackByCriteria(contractStateType: Class<out T>, criteria: QueryCriteria): DataFeed<Vault.Page<T>, Vault.Update<T>> {
|
||||
return vaultTrackBy(criteria, PageSpecification(), Sort(emptySet()), contractStateType)
|
||||
}
|
||||
|
||||
override fun <T : ContractState> vaultTrackByWithPagingSpec(contractStateType: Class<out T>, criteria: QueryCriteria, paging: PageSpecification): DataFeed<Vault.Page<T>, Vault.Update<T>> {
|
||||
return vaultTrackBy(criteria, paging, Sort(emptySet()), contractStateType)
|
||||
}
|
||||
|
||||
override fun <T : ContractState> vaultTrackByWithSorting(contractStateType: Class<out T>, criteria: QueryCriteria, sorting: Sort): DataFeed<Vault.Page<T>, Vault.Update<T>> {
|
||||
return vaultTrackBy(criteria, PageSpecification(), sorting, contractStateType)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private fun stateMachineInfoFromFlowLogic(flowLogic: FlowLogic<*>): StateMachineInfo {
|
||||
return StateMachineInfo(flowLogic.runId, flowLogic.javaClass.name, flowLogic.stateMachine.flowInitiator, flowLogic.track())
|
||||
@ -233,6 +261,4 @@ class CordaRPCOpsImpl(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package net.corda.node.internal
|
||||
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.node.services.Permissions.Companion.all
|
||||
import net.corda.node.services.Permissions.Companion.startFlow
|
||||
import net.corda.node.services.Permissions.Companion.invokeRpc
|
||||
import kotlin.reflect.KVisibility
|
||||
import kotlin.reflect.full.declaredMemberFunctions
|
||||
|
||||
object DefaultCordaRpcPermissions {
|
||||
|
||||
private val invokePermissions = CordaRPCOps::class.declaredMemberFunctions.filter { it.visibility == KVisibility.PUBLIC }.associate { it.name to setOf(invokeRpc(it), all()) }
|
||||
private val startFlowPermissions = setOf("startFlow", "startFlowDynamic", "startTrackedFlow", "startTrackedFlowDynamic").associate { it to this::startFlowPermission }
|
||||
|
||||
fun permissionsAllowing(methodName: String, args: List<Any?>): Set<String> {
|
||||
|
||||
val invoke = invokePermissions[methodName] ?: emptySet()
|
||||
val start = startFlowPermissions[methodName]?.invoke(args)
|
||||
return if (start != null) invoke + start else invoke
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun startFlowPermission(args: List<Any?>): String = if (args[0] is Class<*>) startFlow(args[0] as Class<FlowLogic<*>>) else startFlow(args[0] as String)
|
||||
}
|
@ -0,0 +1,158 @@
|
||||
package net.corda.node.internal
|
||||
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.messaging.DataFeed
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.node.services.NetworkMapCache
|
||||
import net.corda.core.node.services.Vault
|
||||
import net.corda.core.node.services.vault.PageSpecification
|
||||
import net.corda.core.node.services.vault.QueryCriteria
|
||||
import net.corda.core.node.services.vault.Sort
|
||||
import net.corda.node.services.messaging.RpcContext
|
||||
import net.corda.node.services.messaging.requireEitherPermission
|
||||
import java.io.InputStream
|
||||
import java.security.PublicKey
|
||||
|
||||
// TODO change to KFunction reference after Kotlin fixes https://youtrack.jetbrains.com/issue/KT-12140
|
||||
class RpcAuthorisationProxy(private val implementation: CordaRPCOps, private val context: () -> RpcContext, private val permissionsAllowing: (methodName: String, args: List<Any?>) -> Set<String>) : CordaRPCOps {
|
||||
|
||||
override fun stateMachinesSnapshot() = guard("stateMachinesSnapshot") {
|
||||
implementation.stateMachinesSnapshot()
|
||||
}
|
||||
|
||||
override fun stateMachinesFeed() = guard("stateMachinesFeed") {
|
||||
implementation.stateMachinesFeed()
|
||||
}
|
||||
|
||||
override fun <T : ContractState> vaultQueryBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort, contractStateType: Class<out T>) = guard("vaultQueryBy") {
|
||||
implementation.vaultQueryBy(criteria, paging, sorting, contractStateType)
|
||||
}
|
||||
|
||||
override fun <T : ContractState> vaultTrackBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort, contractStateType: Class<out T>) = guard("vaultTrackBy") {
|
||||
implementation.vaultTrackBy(criteria, paging, sorting, contractStateType)
|
||||
}
|
||||
|
||||
override fun internalVerifiedTransactionsSnapshot() = guard("internalVerifiedTransactionsSnapshot", implementation::internalVerifiedTransactionsSnapshot)
|
||||
|
||||
override fun internalVerifiedTransactionsFeed() = guard("internalVerifiedTransactionsFeed", implementation::internalVerifiedTransactionsFeed)
|
||||
|
||||
override fun stateMachineRecordedTransactionMappingSnapshot() = guard("stateMachineRecordedTransactionMappingSnapshot", implementation::stateMachineRecordedTransactionMappingSnapshot)
|
||||
|
||||
override fun stateMachineRecordedTransactionMappingFeed() = guard("stateMachineRecordedTransactionMappingFeed", implementation::stateMachineRecordedTransactionMappingFeed)
|
||||
|
||||
override fun networkMapSnapshot(): List<NodeInfo> = guard("networkMapSnapshot", implementation::networkMapSnapshot)
|
||||
|
||||
override fun networkMapFeed(): DataFeed<List<NodeInfo>, NetworkMapCache.MapChange> = guard("networkMapFeed", implementation::networkMapFeed)
|
||||
|
||||
override fun <T> startFlowDynamic(logicType: Class<out FlowLogic<T>>, vararg args: Any?) = guard("startFlowDynamic", listOf(logicType)) {
|
||||
implementation.startFlowDynamic(logicType, *args)
|
||||
}
|
||||
|
||||
override fun <T> startTrackedFlowDynamic(logicType: Class<out FlowLogic<T>>, vararg args: Any?) = guard("startTrackedFlowDynamic", listOf(logicType)) {
|
||||
implementation.startTrackedFlowDynamic(logicType, *args)
|
||||
}
|
||||
|
||||
override fun nodeInfo(): NodeInfo = guard("nodeInfo", implementation::nodeInfo)
|
||||
|
||||
override fun notaryIdentities(): List<Party> = guard("notaryIdentities", implementation::notaryIdentities)
|
||||
|
||||
override fun addVaultTransactionNote(txnId: SecureHash, txnNote: String) = guard("addVaultTransactionNote") {
|
||||
implementation.addVaultTransactionNote(txnId, txnNote)
|
||||
}
|
||||
|
||||
override fun getVaultTransactionNotes(txnId: SecureHash): Iterable<String> = guard("getVaultTransactionNotes") {
|
||||
implementation.getVaultTransactionNotes(txnId)
|
||||
}
|
||||
|
||||
override fun attachmentExists(id: SecureHash) = guard("attachmentExists") {
|
||||
implementation.attachmentExists(id)
|
||||
}
|
||||
|
||||
override fun openAttachment(id: SecureHash) = guard("openAttachment") {
|
||||
implementation.openAttachment(id)
|
||||
}
|
||||
|
||||
override fun uploadAttachment(jar: InputStream) = guard("uploadAttachment") {
|
||||
implementation.uploadAttachment(jar)
|
||||
}
|
||||
|
||||
override fun currentNodeTime() = guard("currentNodeTime", implementation::currentNodeTime)
|
||||
|
||||
override fun waitUntilNetworkReady() = guard("waitUntilNetworkReady", implementation::waitUntilNetworkReady)
|
||||
|
||||
override fun wellKnownPartyFromAnonymous(party: AbstractParty) = guard("wellKnownPartyFromAnonymous") {
|
||||
implementation.wellKnownPartyFromAnonymous(party)
|
||||
}
|
||||
|
||||
override fun partyFromKey(key: PublicKey) = guard("partyFromKey") {
|
||||
implementation.partyFromKey(key)
|
||||
}
|
||||
|
||||
override fun wellKnownPartyFromX500Name(x500Name: CordaX500Name) = guard("wellKnownPartyFromX500Name") {
|
||||
implementation.wellKnownPartyFromX500Name(x500Name)
|
||||
}
|
||||
|
||||
override fun notaryPartyFromX500Name(x500Name: CordaX500Name) = guard("notaryPartyFromX500Name") {
|
||||
implementation.notaryPartyFromX500Name(x500Name)
|
||||
}
|
||||
|
||||
override fun partiesFromName(query: String, exactMatch: Boolean) = guard("partiesFromName") {
|
||||
implementation.partiesFromName(query, exactMatch)
|
||||
}
|
||||
|
||||
override fun registeredFlows() = guard("registeredFlows", implementation::registeredFlows)
|
||||
|
||||
override fun nodeInfoFromParty(party: AbstractParty) = guard("nodeInfoFromParty") {
|
||||
implementation.nodeInfoFromParty(party)
|
||||
}
|
||||
|
||||
override fun clearNetworkMapCache() = guard("clearNetworkMapCache", implementation::clearNetworkMapCache)
|
||||
|
||||
override fun <T : ContractState> vaultQuery(contractStateType: Class<out T>): Vault.Page<T> = guard("vaultQuery") {
|
||||
implementation.vaultQuery(contractStateType)
|
||||
}
|
||||
|
||||
override fun <T : ContractState> vaultQueryByCriteria(criteria: QueryCriteria, contractStateType: Class<out T>): Vault.Page<T> = guard("vaultQueryByCriteria") {
|
||||
implementation.vaultQueryByCriteria(criteria, contractStateType)
|
||||
}
|
||||
|
||||
override fun <T : ContractState> vaultQueryByWithPagingSpec(contractStateType: Class<out T>, criteria: QueryCriteria, paging: PageSpecification): Vault.Page<T> = guard("vaultQueryByWithPagingSpec") {
|
||||
implementation.vaultQueryByWithPagingSpec(contractStateType, criteria, paging)
|
||||
}
|
||||
|
||||
override fun <T : ContractState> vaultQueryByWithSorting(contractStateType: Class<out T>, criteria: QueryCriteria, sorting: Sort): Vault.Page<T> = guard("vaultQueryByWithSorting") {
|
||||
implementation.vaultQueryByWithSorting(contractStateType, criteria, sorting)
|
||||
}
|
||||
|
||||
override fun <T : ContractState> vaultTrack(contractStateType: Class<out T>): DataFeed<Vault.Page<T>, Vault.Update<T>> = guard("vaultTrack") {
|
||||
implementation.vaultTrack(contractStateType)
|
||||
}
|
||||
|
||||
override fun <T : ContractState> vaultTrackByCriteria(contractStateType: Class<out T>, criteria: QueryCriteria): DataFeed<Vault.Page<T>, Vault.Update<T>> = guard("vaultTrackByCriteria") {
|
||||
implementation.vaultTrackByCriteria(contractStateType, criteria)
|
||||
}
|
||||
|
||||
override fun <T : ContractState> vaultTrackByWithPagingSpec(contractStateType: Class<out T>, criteria: QueryCriteria, paging: PageSpecification): DataFeed<Vault.Page<T>, Vault.Update<T>> = guard("vaultTrackByWithPagingSpec") {
|
||||
implementation.vaultTrackByWithPagingSpec(contractStateType, criteria, paging)
|
||||
}
|
||||
|
||||
override fun <T : ContractState> vaultTrackByWithSorting(contractStateType: Class<out T>, criteria: QueryCriteria, sorting: Sort): DataFeed<Vault.Page<T>, Vault.Update<T>> = guard("vaultTrackByWithSorting") {
|
||||
implementation.vaultTrackByWithSorting(contractStateType, criteria, sorting)
|
||||
}
|
||||
|
||||
// TODO change to KFunction reference after Kotlin fixes https://youtrack.jetbrains.com/issue/KT-12140
|
||||
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.invoke().requireEitherPermission(permissionsAllowing.invoke(methodName, args))
|
||||
return action.invoke()
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package net.corda.node.internal
|
||||
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.node.services.api.FlowStarter
|
||||
import net.corda.node.services.api.ServiceHubInternal
|
||||
import net.corda.node.services.messaging.rpcContext
|
||||
import net.corda.node.services.statemachine.StateMachineManager
|
||||
import net.corda.node.utilities.CordaPersistence
|
||||
|
||||
/**
|
||||
* Implementation of [CordaRPCOps] that checks authorisation.
|
||||
*/
|
||||
class SecureCordaRPCOps(services: ServiceHubInternal,
|
||||
smm: StateMachineManager,
|
||||
database: CordaPersistence,
|
||||
flowStarter: FlowStarter,
|
||||
val unsafe: CordaRPCOps = CordaRPCOpsImpl(services, smm, database, flowStarter)) : CordaRPCOps by RpcAuthorisationProxy(unsafe, ::rpcContext, DefaultCordaRpcPermissions::permissionsAllowing) {
|
||||
|
||||
/**
|
||||
* Returns the RPC protocol version, which is the same the node's Platform Version. Exists since version 1 so guaranteed
|
||||
* to be present.
|
||||
*/
|
||||
override val protocolVersion: Int get() = unsafe.nodeInfo().platformVersion
|
||||
}
|
59
node/src/main/kotlin/net/corda/node/services/Permissions.kt
Normal file
59
node/src/main/kotlin/net/corda/node/services/Permissions.kt
Normal file
@ -0,0 +1,59 @@
|
||||
package net.corda.node.services
|
||||
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import kotlin.reflect.KFunction
|
||||
|
||||
/**
|
||||
* Helper class for creating permissions.
|
||||
*/
|
||||
class Permissions {
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* Global admin permissions.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun all() = "ALL"
|
||||
|
||||
/**
|
||||
* Creates the flow permission string of the format "StartFlow.{ClassName}".
|
||||
*
|
||||
* @param className a flow class name for which permission is created.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun startFlow(className: String) = "StartFlow.$className"
|
||||
|
||||
/**
|
||||
* An overload for the [startFlow]
|
||||
*
|
||||
* @param clazz a class for which permission is created.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun <P : FlowLogic<*>> startFlow(clazz: Class<P>) = startFlow(clazz.name)
|
||||
|
||||
/**
|
||||
* An overload for the [startFlow].
|
||||
*
|
||||
* @param P a class for which permission is created.
|
||||
*/
|
||||
@JvmStatic
|
||||
inline fun <reified P : FlowLogic<*>> startFlow(): String = startFlow(P::class.java)
|
||||
|
||||
/**
|
||||
* Creates a permission string with format "InvokeRpc.{MethodName}".
|
||||
*
|
||||
* @param methodName a RPC method name for which permission is created.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun invokeRpc(methodName: String) = "InvokeRpc.$methodName"
|
||||
|
||||
/**
|
||||
* Creates a permission string with format "InvokeRpc.{method.name}".
|
||||
*
|
||||
* @param method a RPC [KFunction] for which permission is created.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun invokeRpc(method: KFunction<*>) = invokeRpc(method.name)
|
||||
}
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
package net.corda.node.services
|
||||
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.nodeapi.User
|
||||
|
||||
/**
|
||||
@ -26,36 +25,3 @@ class RPCUserServiceImpl(override val users: List<User>) : RPCUserService {
|
||||
|
||||
override fun getUser(username: String): User? = users.find { it.username == username }
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class for creating flow class permissions.
|
||||
*/
|
||||
class FlowPermissions {
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* Creates the flow permission string of the format "StartFlow.{ClassName}".
|
||||
*
|
||||
* @param className a flow class name for which permission is created.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun startFlowPermission(className: String) = "StartFlow.$className"
|
||||
|
||||
/**
|
||||
* An overload for the [startFlowPermission]
|
||||
*
|
||||
* @param clazz a class for which permission is created.
|
||||
*
|
||||
*/
|
||||
@JvmStatic
|
||||
fun <P : FlowLogic<*>> startFlowPermission(clazz: Class<P>) = startFlowPermission(clazz.name)
|
||||
|
||||
/**
|
||||
* An overload for the [startFlowPermission].
|
||||
*
|
||||
* @param P a class for which permission is created.
|
||||
*/
|
||||
@JvmStatic
|
||||
inline fun <reified P : FlowLogic<*>> startFlowPermission(): String = startFlowPermission(P::class.java)
|
||||
}
|
||||
}
|
||||
|
@ -373,7 +373,7 @@ internal val CURRENT_RPC_CONTEXT: ThreadLocal<RpcContext> = ThreadLocal()
|
||||
* throw. If you'd like to use the context outside of the call (e.g. in another thread) then pass the returned reference
|
||||
* around explicitly.
|
||||
*/
|
||||
fun getRpcContext(): RpcContext = CURRENT_RPC_CONTEXT.get()
|
||||
fun rpcContext(): RpcContext = CURRENT_RPC_CONTEXT.get()
|
||||
|
||||
/**
|
||||
* @param currentUser This is available to RPC implementations to query the validated [User] that is calling it. Each
|
||||
|
@ -3,13 +3,18 @@
|
||||
package net.corda.node.services.messaging
|
||||
|
||||
import net.corda.client.rpc.PermissionException
|
||||
import net.corda.node.services.Permissions.Companion.all
|
||||
import net.corda.nodeapi.ArtemisMessagingComponent
|
||||
|
||||
/** Helper method which checks that the current RPC user is entitled for the given permission. Throws a [PermissionException] otherwise. */
|
||||
fun RpcContext.requirePermission(permission: String) {
|
||||
fun RpcContext.requirePermission(permission: String): RpcContext = requireEitherPermission(setOf(permission))
|
||||
|
||||
/** Helper method which checks that the current RPC user is entitled with any of the given permissions. Throws a [PermissionException] otherwise. */
|
||||
fun RpcContext.requireEitherPermission(permissions: Set<String>): RpcContext {
|
||||
// TODO remove the NODE_USER condition once webserver doesn't need it
|
||||
val currentUserPermissions = currentUser.permissions
|
||||
if (currentUser.username != ArtemisMessagingComponent.NODE_USER && currentUserPermissions.intersect(listOf(permission, "ALL")).isEmpty()) {
|
||||
throw PermissionException("User not permissioned for $permission, permissions are $currentUserPermissions")
|
||||
if (currentUser.username != ArtemisMessagingComponent.NODE_USER && currentUserPermissions.intersect(permissions + all()).isEmpty()) {
|
||||
throw PermissionException("User not permissioned with any of $permissions, permissions are $currentUserPermissions")
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
@ -23,9 +23,10 @@ import net.corda.finance.USD
|
||||
import net.corda.finance.contracts.asset.Cash
|
||||
import net.corda.finance.flows.CashIssueFlow
|
||||
import net.corda.finance.flows.CashPaymentFlow
|
||||
import net.corda.node.internal.CordaRPCOpsImpl
|
||||
import net.corda.node.internal.SecureCordaRPCOps
|
||||
import net.corda.node.internal.StartedNode
|
||||
import net.corda.node.services.FlowPermissions.Companion.startFlowPermission
|
||||
import net.corda.node.services.Permissions.Companion.startFlow
|
||||
import net.corda.node.services.Permissions.Companion.invokeRpc
|
||||
import net.corda.node.services.messaging.CURRENT_RPC_CONTEXT
|
||||
import net.corda.node.services.messaging.RpcContext
|
||||
import net.corda.nodeapi.User
|
||||
@ -63,19 +64,20 @@ class CordaRPCOpsImplTest {
|
||||
lateinit var transactions: Observable<SignedTransaction>
|
||||
lateinit var vaultTrackCash: Observable<Vault.Update<Cash.State>>
|
||||
|
||||
private val user = User("user", "pwd", permissions = emptySet())
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
mockNet = MockNetwork(cordappPackages = listOf("net.corda.finance.contracts.asset"))
|
||||
aliceNode = mockNet.createNode()
|
||||
notaryNode = mockNet.createNotaryNode(validating = false)
|
||||
rpc = CordaRPCOpsImpl(aliceNode.services, aliceNode.smm, aliceNode.database, aliceNode.services)
|
||||
CURRENT_RPC_CONTEXT.set(RpcContext(User("user", "pwd", permissions = setOf(
|
||||
startFlowPermission<CashIssueFlow>(),
|
||||
startFlowPermission<CashPaymentFlow>()
|
||||
))))
|
||||
rpc = SecureCordaRPCOps(aliceNode.services, aliceNode.smm, aliceNode.database, aliceNode.services)
|
||||
CURRENT_RPC_CONTEXT.set(RpcContext(user))
|
||||
|
||||
mockNet.runNetwork()
|
||||
notary = rpc.notaryIdentities().first()
|
||||
withPermissions(invokeRpc(CordaRPCOps::notaryIdentities)) {
|
||||
notary = rpc.notaryIdentities().first()
|
||||
}
|
||||
}
|
||||
|
||||
@After
|
||||
@ -85,170 +87,185 @@ class CordaRPCOpsImplTest {
|
||||
|
||||
@Test
|
||||
fun `cash issue accepted`() {
|
||||
aliceNode.database.transaction {
|
||||
stateMachineUpdates = rpc.stateMachinesFeed().updates
|
||||
vaultTrackCash = rpc.vaultTrackBy<Cash.State>().updates
|
||||
}
|
||||
|
||||
val quantity = 1000L
|
||||
val ref = OpaqueBytes(ByteArray(1) { 1 })
|
||||
withPermissions(invokeRpc("vaultTrackBy"), invokeRpc("vaultQueryBy"), invokeRpc(CordaRPCOps::stateMachinesFeed), startFlow<CashIssueFlow>()) {
|
||||
|
||||
// Check the monitoring service wallet is empty
|
||||
aliceNode.database.transaction {
|
||||
assertFalse(aliceNode.services.vaultService.queryBy<ContractState>().totalStatesAvailable > 0)
|
||||
}
|
||||
aliceNode.database.transaction {
|
||||
stateMachineUpdates = rpc.stateMachinesFeed().updates
|
||||
vaultTrackCash = rpc.vaultTrackBy<Cash.State>().updates
|
||||
}
|
||||
|
||||
// Tell the monitoring service node to issue some cash
|
||||
val result = rpc.startFlow(::CashIssueFlow, Amount(quantity, GBP), ref, notary)
|
||||
mockNet.runNetwork()
|
||||
val quantity = 1000L
|
||||
val ref = OpaqueBytes(ByteArray(1) { 1 })
|
||||
|
||||
var issueSmId: StateMachineRunId? = null
|
||||
stateMachineUpdates.expectEvents {
|
||||
sequence(
|
||||
// ISSUE
|
||||
expect { add: StateMachineUpdate.Added ->
|
||||
issueSmId = add.id
|
||||
},
|
||||
expect { remove: StateMachineUpdate.Removed ->
|
||||
require(remove.id == issueSmId)
|
||||
}
|
||||
)
|
||||
}
|
||||
// Check the monitoring service wallet is empty
|
||||
aliceNode.database.transaction {
|
||||
assertFalse(aliceNode.services.vaultService.queryBy<ContractState>().totalStatesAvailable > 0)
|
||||
}
|
||||
|
||||
val anonymisedRecipient = result.returnValue.getOrThrow().recipient!!
|
||||
val expectedState = Cash.State(Amount(quantity,
|
||||
Issued(aliceNode.info.chooseIdentity().ref(ref), GBP)),
|
||||
anonymisedRecipient)
|
||||
// Tell the monitoring service node to issue some cash
|
||||
val result = rpc.startFlow(::CashIssueFlow, Amount(quantity, GBP), ref, notary)
|
||||
mockNet.runNetwork()
|
||||
|
||||
// Query vault via RPC
|
||||
val cash = rpc.vaultQueryBy<Cash.State>()
|
||||
assertEquals(expectedState, cash.states.first().state.data)
|
||||
var issueSmId: StateMachineRunId? = null
|
||||
stateMachineUpdates.expectEvents {
|
||||
sequence(
|
||||
// ISSUE
|
||||
expect { add: StateMachineUpdate.Added ->
|
||||
issueSmId = add.id
|
||||
},
|
||||
expect { remove: StateMachineUpdate.Removed ->
|
||||
require(remove.id == issueSmId)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
vaultTrackCash.expectEvents {
|
||||
expect { update ->
|
||||
val actual = update.produced.single().state.data
|
||||
assertEquals(expectedState, actual)
|
||||
val anonymisedRecipient = result.returnValue.getOrThrow().recipient!!
|
||||
val expectedState = Cash.State(Amount(quantity,
|
||||
Issued(aliceNode.info.chooseIdentity().ref(ref), GBP)),
|
||||
anonymisedRecipient)
|
||||
|
||||
// Query vault via RPC
|
||||
val cash = rpc.vaultQueryBy<Cash.State>()
|
||||
assertEquals(expectedState, cash.states.first().state.data)
|
||||
|
||||
vaultTrackCash.expectEvents {
|
||||
expect { update ->
|
||||
val actual = update.produced.single().state.data
|
||||
assertEquals(expectedState, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `issue and move`() {
|
||||
aliceNode.database.transaction {
|
||||
stateMachineUpdates = rpc.stateMachinesFeed().updates
|
||||
transactions = rpc.internalVerifiedTransactionsFeed().updates
|
||||
vaultTrackCash = rpc.vaultTrackBy<Cash.State>().updates
|
||||
}
|
||||
|
||||
val result = rpc.startFlow(::CashIssueFlow,
|
||||
100.DOLLARS,
|
||||
OpaqueBytes(ByteArray(1, { 1 })),
|
||||
notary
|
||||
)
|
||||
withPermissions(invokeRpc(CordaRPCOps::stateMachinesFeed),
|
||||
invokeRpc(CordaRPCOps::internalVerifiedTransactionsFeed),
|
||||
invokeRpc("vaultTrackBy"),
|
||||
startFlow<CashIssueFlow>(),
|
||||
startFlow<CashPaymentFlow>()) {
|
||||
aliceNode.database.transaction {
|
||||
stateMachineUpdates = rpc.stateMachinesFeed().updates
|
||||
transactions = rpc.internalVerifiedTransactionsFeed().updates
|
||||
vaultTrackCash = rpc.vaultTrackBy<Cash.State>().updates
|
||||
}
|
||||
|
||||
mockNet.runNetwork()
|
||||
|
||||
rpc.startFlow(::CashPaymentFlow, 100.DOLLARS, aliceNode.info.chooseIdentity())
|
||||
|
||||
mockNet.runNetwork()
|
||||
|
||||
var issueSmId: StateMachineRunId? = null
|
||||
var moveSmId: StateMachineRunId? = null
|
||||
stateMachineUpdates.expectEvents {
|
||||
sequence(
|
||||
// ISSUE
|
||||
expect { add: StateMachineUpdate.Added ->
|
||||
issueSmId = add.id
|
||||
},
|
||||
expect { remove: StateMachineUpdate.Removed ->
|
||||
require(remove.id == issueSmId)
|
||||
},
|
||||
// MOVE
|
||||
expect { add: StateMachineUpdate.Added ->
|
||||
moveSmId = add.id
|
||||
},
|
||||
expect { remove: StateMachineUpdate.Removed ->
|
||||
require(remove.id == moveSmId)
|
||||
}
|
||||
val result = rpc.startFlow(::CashIssueFlow,
|
||||
100.DOLLARS,
|
||||
OpaqueBytes(ByteArray(1, { 1 })),
|
||||
notary
|
||||
)
|
||||
}
|
||||
|
||||
result.returnValue.getOrThrow()
|
||||
transactions.expectEvents {
|
||||
sequence(
|
||||
// ISSUE
|
||||
expect { stx ->
|
||||
require(stx.tx.inputs.isEmpty())
|
||||
require(stx.tx.outputs.size == 1)
|
||||
val signaturePubKeys = stx.sigs.map { it.by }.toSet()
|
||||
// Only Alice signed, as issuer
|
||||
val aliceKey = aliceNode.info.chooseIdentity().owningKey
|
||||
require(signaturePubKeys.size <= aliceKey.keys.size)
|
||||
require(aliceKey.isFulfilledBy(signaturePubKeys))
|
||||
},
|
||||
// MOVE
|
||||
expect { stx ->
|
||||
require(stx.tx.inputs.size == 1)
|
||||
require(stx.tx.outputs.size == 1)
|
||||
val signaturePubKeys = stx.sigs.map { it.by }.toSet()
|
||||
// Alice and Notary signed
|
||||
require(aliceNode.services.keyManagementService.filterMyKeys(signaturePubKeys).toList().isNotEmpty())
|
||||
require(notary.owningKey.isFulfilledBy(signaturePubKeys))
|
||||
}
|
||||
)
|
||||
}
|
||||
mockNet.runNetwork()
|
||||
|
||||
vaultTrackCash.expectEvents {
|
||||
sequence(
|
||||
// ISSUE
|
||||
expect { (consumed, produced) ->
|
||||
require(consumed.isEmpty()) { consumed.size }
|
||||
require(produced.size == 1) { produced.size }
|
||||
},
|
||||
// MOVE
|
||||
expect { (consumed, produced) ->
|
||||
require(consumed.size == 1) { consumed.size }
|
||||
require(produced.size == 1) { produced.size }
|
||||
}
|
||||
)
|
||||
rpc.startFlow(::CashPaymentFlow, 100.DOLLARS, aliceNode.info.chooseIdentity())
|
||||
|
||||
mockNet.runNetwork()
|
||||
|
||||
var issueSmId: StateMachineRunId? = null
|
||||
var moveSmId: StateMachineRunId? = null
|
||||
stateMachineUpdates.expectEvents {
|
||||
sequence(
|
||||
// ISSUE
|
||||
expect { add: StateMachineUpdate.Added ->
|
||||
issueSmId = add.id
|
||||
},
|
||||
expect { remove: StateMachineUpdate.Removed ->
|
||||
require(remove.id == issueSmId)
|
||||
},
|
||||
// MOVE
|
||||
expect { add: StateMachineUpdate.Added ->
|
||||
moveSmId = add.id
|
||||
},
|
||||
expect { remove: StateMachineUpdate.Removed ->
|
||||
require(remove.id == moveSmId)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
result.returnValue.getOrThrow()
|
||||
transactions.expectEvents {
|
||||
sequence(
|
||||
// ISSUE
|
||||
expect { stx ->
|
||||
require(stx.tx.inputs.isEmpty())
|
||||
require(stx.tx.outputs.size == 1)
|
||||
val signaturePubKeys = stx.sigs.map { it.by }.toSet()
|
||||
// Only Alice signed, as issuer
|
||||
val aliceKey = aliceNode.info.chooseIdentity().owningKey
|
||||
require(signaturePubKeys.size <= aliceKey.keys.size)
|
||||
require(aliceKey.isFulfilledBy(signaturePubKeys))
|
||||
},
|
||||
// MOVE
|
||||
expect { stx ->
|
||||
require(stx.tx.inputs.size == 1)
|
||||
require(stx.tx.outputs.size == 1)
|
||||
val signaturePubKeys = stx.sigs.map { it.by }.toSet()
|
||||
// Alice and Notary signed
|
||||
require(aliceNode.services.keyManagementService.filterMyKeys(signaturePubKeys).toList().isNotEmpty())
|
||||
require(notary.owningKey.isFulfilledBy(signaturePubKeys))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
vaultTrackCash.expectEvents {
|
||||
sequence(
|
||||
// ISSUE
|
||||
expect { (consumed, produced) ->
|
||||
require(consumed.isEmpty()) { consumed.size }
|
||||
require(produced.size == 1) { produced.size }
|
||||
},
|
||||
// MOVE
|
||||
expect { (consumed, produced) ->
|
||||
require(consumed.size == 1) { consumed.size }
|
||||
require(produced.size == 1) { produced.size }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `cash command by user not permissioned for cash`() {
|
||||
CURRENT_RPC_CONTEXT.set(RpcContext(User("user", "pwd", permissions = emptySet())))
|
||||
assertThatExceptionOfType(PermissionException::class.java).isThrownBy {
|
||||
rpc.startFlow(::CashIssueFlow, Amount(100, USD), OpaqueBytes(ByteArray(1, { 1 })), notary)
|
||||
withoutAnyPermissions {
|
||||
assertThatExceptionOfType(PermissionException::class.java).isThrownBy {
|
||||
rpc.startFlow(::CashIssueFlow, Amount(100, USD), OpaqueBytes(ByteArray(1, { 1 })), notary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `can upload an attachment`() {
|
||||
val inputJar = Thread.currentThread().contextClassLoader.getResourceAsStream(testJar)
|
||||
val secureHash = rpc.uploadAttachment(inputJar)
|
||||
assertTrue(rpc.attachmentExists(secureHash))
|
||||
withPermissions(invokeRpc(CordaRPCOps::uploadAttachment), invokeRpc(CordaRPCOps::attachmentExists)) {
|
||||
val inputJar = Thread.currentThread().contextClassLoader.getResourceAsStream(testJar)
|
||||
val secureHash = rpc.uploadAttachment(inputJar)
|
||||
assertTrue(rpc.attachmentExists(secureHash))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `can download an uploaded attachment`() {
|
||||
val inputJar = Thread.currentThread().contextClassLoader.getResourceAsStream(testJar)
|
||||
val secureHash = rpc.uploadAttachment(inputJar)
|
||||
val bufferFile = ByteArrayOutputStream()
|
||||
val bufferRpc = ByteArrayOutputStream()
|
||||
withPermissions(invokeRpc(CordaRPCOps::uploadAttachment), invokeRpc(CordaRPCOps::openAttachment)) {
|
||||
val inputJar = Thread.currentThread().contextClassLoader.getResourceAsStream(testJar)
|
||||
val secureHash = rpc.uploadAttachment(inputJar)
|
||||
val bufferFile = ByteArrayOutputStream()
|
||||
val bufferRpc = ByteArrayOutputStream()
|
||||
|
||||
IOUtils.copy(Thread.currentThread().contextClassLoader.getResourceAsStream(testJar), bufferFile)
|
||||
IOUtils.copy(rpc.openAttachment(secureHash), bufferRpc)
|
||||
IOUtils.copy(Thread.currentThread().contextClassLoader.getResourceAsStream(testJar), bufferFile)
|
||||
IOUtils.copy(rpc.openAttachment(secureHash), bufferRpc)
|
||||
|
||||
assertArrayEquals(bufferFile.toByteArray(), bufferRpc.toByteArray())
|
||||
assertArrayEquals(bufferFile.toByteArray(), bufferRpc.toByteArray())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `attempt to start non-RPC flow`() {
|
||||
CURRENT_RPC_CONTEXT.set(RpcContext(User("user", "pwd", permissions = setOf(
|
||||
startFlowPermission<NonRPCFlow>()
|
||||
))))
|
||||
assertThatExceptionOfType(IllegalArgumentException::class.java).isThrownBy {
|
||||
rpc.startFlow(::NonRPCFlow)
|
||||
withPermissions(startFlow<NonRPCFlow>()) {
|
||||
assertThatExceptionOfType(IllegalArgumentException::class.java).isThrownBy {
|
||||
rpc.startFlow(::NonRPCFlow)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -259,12 +276,11 @@ class CordaRPCOpsImplTest {
|
||||
|
||||
@Test
|
||||
fun `attempt to start RPC flow with void return`() {
|
||||
CURRENT_RPC_CONTEXT.set(RpcContext(User("user", "pwd", permissions = setOf(
|
||||
startFlowPermission<VoidRPCFlow>()
|
||||
))))
|
||||
val result = rpc.startFlow(::VoidRPCFlow)
|
||||
mockNet.runNetwork()
|
||||
assertNull(result.returnValue.getOrThrow())
|
||||
withPermissions(startFlow<VoidRPCFlow>()) {
|
||||
val result = rpc.startFlow(::VoidRPCFlow)
|
||||
mockNet.runNetwork()
|
||||
assertNull(result.returnValue.getOrThrow())
|
||||
}
|
||||
}
|
||||
|
||||
@StartableByRPC
|
||||
@ -272,4 +288,17 @@ class CordaRPCOpsImplTest {
|
||||
@Suspendable
|
||||
override fun call(): Void? = null
|
||||
}
|
||||
}
|
||||
|
||||
private fun withPermissions(vararg permissions: String, action: () -> Unit) {
|
||||
|
||||
val previous = CURRENT_RPC_CONTEXT.get()
|
||||
try {
|
||||
CURRENT_RPC_CONTEXT.set(RpcContext(user.copy(permissions = permissions.toSet())))
|
||||
action.invoke()
|
||||
} finally {
|
||||
CURRENT_RPC_CONTEXT.set(previous)
|
||||
}
|
||||
}
|
||||
|
||||
private fun withoutAnyPermissions(action: () -> Unit) = withPermissions(action = action)
|
||||
}
|
Reference in New Issue
Block a user