Skip to content

Commit

Permalink
Create header files for discussion of possible flow audit api.
Browse files Browse the repository at this point in the history
Fix compile error
  • Loading branch information
Matthew Nesbit committed May 3, 2017
1 parent cfe5786 commit 7b40501
Show file tree
Hide file tree
Showing 10 changed files with 280 additions and 13 deletions.
24 changes: 22 additions & 2 deletions core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt
Expand Up @@ -142,10 +142,30 @@ abstract class FlowLogic<out T> {
return result
}

/**
* Flows can call this method ensure that the active FlowInitiator is authorised for a particular action.
* This provides fine grained control over application level permissions.
* An audit event is always recorded when this method is used.
* If the permission is not granted for the FlowInitiator a FlowException
* @param permissionName is a string representing the desired permission. Each flow is given a distinct namespace for these permissions.
* @param extraAuditData in the audit log for this permission check these extra key value pairs will be recorded
*/
@Throws(FlowException::class)
fun checkFlowPermission(permissionName: String, extraAuditData: Map<String,String>) = stateMachine.checkFlowPermission(permissionName, extraAuditData)


/**
* Flows can call this method to record application level audit events
* @param eventType is a string representing the type of event. Each flow is given a distinct namespace for these names.
* @param comment a general human readable summary of the event.
* @param extraAuditData in the audit log for this permission check these extra key value pairs will be recorded.
*/
fun recordAuditEvent(eventType: String, comment: String, extraAuditData: Map<String,String>) = stateMachine.recordAuditEvent(eventType, comment, extraAuditData)

/**
* Override this to provide a [ProgressTracker]. If one is provided and stepped, the framework will do something
* helpful with the progress reports. If this flow is invoked as a subflow of another, then the
* tracker will be made a child of the current step in the parent. If it's null, this flow doesn't track
* helpful with the progress reports e.g record to the audit service. If this flow is invoked as a subflow of another,
* then the tracker will be made a child of the current step in the parent. If it's null, this flow doesn't track
* progress.
*
* Note that this has to return a tracker before the flow is invoked. You can't change your mind half way
Expand Down
34 changes: 29 additions & 5 deletions core/src/main/kotlin/net/corda/core/flows/FlowStateMachine.kt
Expand Up @@ -13,21 +13,41 @@ import net.corda.core.utilities.UntrustworthyData
import org.slf4j.Logger
import java.util.*

/**
* Wrapper over a general authenticated user principal token. The mechanism of authentication
* is not specified. This token may be used internally to log audit information, or check access rights.
*/
interface SecurityIdentifier {
/**
* Displayable string for the user principal which will be included in the audit logs
*/
val uid: String
}

/**
* FlowInitiator holds information on who started the flow. We have different ways of doing that: via RPC [FlowInitiator.RPC],
* communication started by peer node [FlowInitiator.Peer], scheduled flows [FlowInitiator.Scheduled]
* or manual [FlowInitiator.Manual]. The last case is for all flows started in tests, shell etc. It was added
* because we can start flow directly using [StateMachineManager.add] or [ServiceHubInternal.startFlow].
*/
@CordaSerializable
sealed class FlowInitiator {
sealed class FlowInitiator : SecurityIdentifier {
/** Started using [CordaRPCOps.startFlowDynamic]. */
data class RPC(val username: String) : FlowInitiator()
data class RPC(val username: String) : FlowInitiator() {
override val uid: String get() = username
}
/** Started when we get new session initiation request. */
data class Peer(val party: Party) : FlowInitiator()
data class Peer(val party: Party) : FlowInitiator() {
override val uid: String get() = party.name
}
/** Started as scheduled activity. */
data class Scheduled(val scheduledState: ScheduledStateRef) : FlowInitiator()
object Shell : FlowInitiator() // TODO When proper ssh access enabled, add username/use RPC?
data class Scheduled(val scheduledState: ScheduledStateRef) : FlowInitiator() {
override val uid: String get() = "Scheduler"
}
// TODO When proper ssh access enabled, add username/use RPC?
object Shell : FlowInitiator() {
override val uid: String get() = "Shell User"
}
}

/**
Expand Down Expand Up @@ -62,6 +82,10 @@ interface FlowStateMachine<R> {

fun createHandle(hasProgress: Boolean): FlowHandle<R>

fun checkFlowPermission(permissionName: String, extraAuditData: Map<String,String>)

fun recordAuditEvent(eventType: String, comment: String, extraAuditData: Map<String,String>)

val serviceHub: ServiceHub
val logger: Logger
val id: StateMachineRunId
Expand Down
Expand Up @@ -55,6 +55,12 @@ class ProgressTracker(vararg steps: Step) {
open class Step(open val label: String) {
open val changes: Observable<Change> get() = Observable.empty()
open fun childProgressTracker(): ProgressTracker? = null
/**
* A flow may populate this property with flow specific context data.
* The extra data will be recorded to the audit logs when the flow progresses.
* Even if empty the basic details (i.e. label) of the step will be recorded for audit purposes.
*/
open val extraAuditData: Map<String, String> get() = emptyMap()
}

// Sentinel objects. Overrides equals() to survive process restarts and serialization.
Expand Down
3 changes: 3 additions & 0 deletions node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt
Expand Up @@ -123,6 +123,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
override val myInfo: NodeInfo get() = info
override val schemaService: SchemaService get() = schemas
override val transactionVerifierService: TransactionVerifierService get() = txVerifierService
override val auditService: AuditService get() = auditService

// Internal only
override val monitoringService: MonitoringService = MonitoringService(MetricRegistry())
Expand Down Expand Up @@ -167,6 +168,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
lateinit var scheduler: NodeSchedulerService
lateinit var flowLogicFactory: FlowLogicRefFactory
lateinit var schemas: SchemaService
lateinit var auditService: AuditService
val customServices: ArrayList<Any> = ArrayList()
protected val runOnStop: ArrayList<Runnable> = ArrayList()
lateinit var database: Database
Expand Down Expand Up @@ -285,6 +287,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
schemas = makeSchemaService()
vault = makeVaultService(configuration.dataSourceProperties)
txVerifierService = makeTransactionVerifierService()
auditService = DummyAuditService()

info = makeInfo()
identity = makeIdentityService()
Expand Down
Expand Up @@ -15,6 +15,7 @@ interface RPCUserService {

// TODO Store passwords as salted hashes
// TODO Or ditch this and consider something like Apache Shiro
// TODO Need access to permission checks from inside flows and at other point during audit checking.
class RPCUserServiceImpl(override val users: List<User>) : RPCUserService {
override fun getUser(username: String): User? = users.find { it.username == username }
}
Expand Down
173 changes: 173 additions & 0 deletions node/src/main/kotlin/net/corda/node/services/api/AuditService.kt
@@ -0,0 +1,173 @@
package net.corda.node.services.api

import net.corda.core.flows.FlowLogic
import net.corda.core.flows.SecurityIdentifier
import net.corda.core.flows.StateMachineRunId
import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.utilities.ProgressTracker
import java.time.Instant

/**
* Minimum event specific data for any audit event to be logged. It is expected that the underlying audit service
* will enrich this to include details of the node, so that in clustered configurations the source node can be identified.
*/
interface SystemAuditEvent {
/**
* The UTC time point at which the audit event happened.
*/
val timestamp: Instant
/**
* The responsible individual, node, or subsystem to which the audit event can be mapped.
*/
val principal: SecurityIdentifier
/**
* The specific class of audit event being recorded.
*/
val auditEventType: String
/**
* A human readable description of audit event including any permission check results.
*/
val description: String
/**
* Further tagged details that should be recorded along with the common data of the audit event.
*/
val contextData: Map<String, String>
}

/**
* Simple concrete data class implementation of SystemAuditEvent
*/
data class SystemAuditEventImpl(override val timestamp: Instant,
override val principal: SecurityIdentifier,
override val auditEventType: String,
override val description: String,
override val contextData: Map<String, String>) : SystemAuditEvent

/**
* Extension of SystemAuditEvent interface to include information needed to trace an audit event back to a specific flow.
*/
interface FlowAuditEvent : SystemAuditEvent {
/**
* The concrete type of FlowLogic being referenced.
* TODO This should be replaced with the fully versioned name/signature of the flow.
*/
val flowType: Class<out FlowLogic<*>>
/**
* The stable identifier of the flow as stored with Checkpoints.
*/
val flowId: StateMachineRunId
}

/**
* Marker interface extension of FlowAuditEvent to capture the initiation of a new flow.
* The flow parameters should be captured to the context data.
*/
interface FlowStartEvent : FlowAuditEvent {
}

/**
* Extension of FlowAuditEvent to include ProgressTracker Step object whenever a change is signalled.
* The API for ProgressTracker has been extended so that the Step can contain some extra context data,
* which is copied into the contextData Map.
*/
interface FlowProgressAuditEvent : FlowAuditEvent {
val flowProgress: ProgressTracker.Step
}

/**
* Extension of FlowAuditEvent to record any FlowExceptions, or other unexpected terminations of a Flow.
*/
interface FlowErrorAuditEvent : FlowAuditEvent {
val error: Throwable
}

/**
* Extension of FlowAuditEvent to record checks on per flow permissions and the verdict of these checks
* If the permission is denied i.e. permissionGranted is false, then it is expected that the flow will be terminated immediately
* after recording the FlowPermissionAuditEvent. This may cause an extra FlowErrorAuditEvent to be recorded too.
*/
interface FlowPermissionAuditEvent : FlowAuditEvent {
val permissionRequested: String
val permissionGranted: Boolean
}

/**
* Simple concrete data class implementation of FlowAuditEvent
*/
data class FlowAuditEventImpl(override val timestamp: Instant,
override val principal: SecurityIdentifier,
override val auditEventType: String,
override val description: String,
override val contextData: Map<String, String>,
override val flowType: Class<out FlowLogic<*>>,
override val flowId: StateMachineRunId) : FlowAuditEvent

/**
* Simple concrete data class implementation of FlowStartEvent
*/
data class FlowStartEventImpl(override val timestamp: Instant,
override val principal: SecurityIdentifier,
override val auditEventType: String,
override val description: String,
override val contextData: Map<String, String>,
override val flowType: Class<out FlowLogic<*>>,
override val flowId: StateMachineRunId) : FlowStartEvent


/**
* Simple concrete data class implementation of FlowProgressAuditEvent
*/
data class FlowProgressAuditEventImpl(override val timestamp: Instant,
override val principal: SecurityIdentifier,
override val auditEventType: String,
override val description: String,
override val flowType: Class<out FlowLogic<*>>,
override val flowId: StateMachineRunId,
override val flowProgress: ProgressTracker.Step) : FlowProgressAuditEvent {
override val contextData: Map<String, String> get() = flowProgress.extraAuditData
}

/**
* Simple concrete data class implementation of FlowErrorAuditEvent
*/
data class FlowErrorAuditEventImpl(override val timestamp: Instant,
override val principal: SecurityIdentifier,
override val auditEventType: String,
override val description: String,
override val contextData: Map<String, String>,
override val flowType: Class<out FlowLogic<*>>,
override val flowId: StateMachineRunId,
override val error: Throwable) : FlowErrorAuditEvent


/**
* Simple concrete data class implementation of FlowPermissionAuditEvent
*/
data class FlowPermissionAuditEventImpl(override val timestamp: Instant,
override val principal: SecurityIdentifier,
override val auditEventType: String,
override val description: String,
override val contextData: Map<String, String>,
override val flowType: Class<out FlowLogic<*>>,
override val flowId: StateMachineRunId,
override val permissionRequested: String,
override val permissionGranted: Boolean) : FlowPermissionAuditEvent

/**
* Minimal interface for recording audit information within the system. The AuditService is assumed to be available only
* to trusted internal components via ServiceHubInternal.
*/
interface AuditService {
fun recordSystemAuditEvent(event: SystemAuditEvent)
}

/**
* Empty do nothing AuditService as placeholder.
* TODO Write a full implementation that expands all the audit events to the database.
*/
class DummyAuditService : AuditService, SingletonSerializeAsToken() {
override fun recordSystemAuditEvent(event: SystemAuditEvent) {
//TODO Implement transformation of the audit events to formal audit data
}
}

Expand Up @@ -46,6 +46,7 @@ abstract class ServiceHubInternal : PluginServiceHub {
abstract val monitoringService: MonitoringService
abstract val flowLogicRefFactory: FlowLogicRefFactory
abstract val schemaService: SchemaService
abstract val auditService: AuditService

abstract override val networkService: MessagingServiceInternal

Expand Down
Expand Up @@ -21,7 +21,7 @@ import net.corda.core.utilities.ProgressTracker
import net.corda.core.utilities.UntrustworthyData
import net.corda.core.utilities.debug
import net.corda.core.utilities.trace
import net.corda.node.services.api.ServiceHubInternal
import net.corda.node.services.api.*
import net.corda.node.utilities.StrandLocalTransactionManager
import net.corda.node.utilities.createTransaction
import org.jetbrains.exposed.sql.Database
Expand All @@ -36,6 +36,8 @@ import java.sql.SQLException
import java.util.*
import java.util.concurrent.TimeUnit

class FlowPermissionException(message: String) : FlowException(message)

class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
val logic: FlowLogic<R>,
scheduler: FiberScheduler,
Expand Down Expand Up @@ -234,6 +236,38 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
throw IllegalStateException("We were resumed after waiting for $hash but it wasn't found in our local storage")
}

// TODO Dummy implementation of access to application specific permission controls and audit logging
override fun checkFlowPermission(permissionName: String, extraAuditData: Map<String, String>) {
val permissionGranted = true // TODO define permission control service on ServiceHubInternal and actually check authorization.
val checkPermissionEvent = FlowPermissionAuditEventImpl(
serviceHub.clock.instant(),
flowInitiator,
"CheckFlowPermission.${logic.javaClass.name}.$permissionName",
"Flow Permission Required: $permissionName",
extraAuditData,
logic.javaClass,
id,
permissionName,
permissionGranted)
serviceHub.auditService.recordSystemAuditEvent(checkPermissionEvent)
if (!permissionGranted) {
throw FlowPermissionException("User $flowInitiator not permissioned for $permissionName on flow $id")
}
}

// TODO Dummy implementation of access to application specific audit logging
override fun recordAuditEvent(eventType: String, comment: String, extraAuditData: Map<String,String>) {
val flowAuditEvent = FlowAuditEventImpl(
serviceHub.clock.instant(),
flowInitiator,
"FlowAuditEvent.${logic.javaClass.name}.$eventType",
comment,
extraAuditData,
logic.javaClass,
id)
serviceHub.auditService.recordSystemAuditEvent(flowAuditEvent)
}

/**
* This method will suspend the state machine and wait for incoming session init response from other party.
*/
Expand Down
8 changes: 8 additions & 0 deletions node/src/test/kotlin/net/corda/node/InteractiveShellTest.kt
Expand Up @@ -100,5 +100,13 @@ class InteractiveShellTest {
get() = throw UnsupportedOperationException()
override val flowInitiator: FlowInitiator
get() = throw UnsupportedOperationException()

override fun checkFlowPermission(permissionName: String, extraAuditData: Map<String, String>) {
// Do nothing
}

override fun recordAuditEvent(eventType: String, comment: String, extraAuditData: Map<String, String>) {
// Do nothing
}
}
}

0 comments on commit 7b40501

Please sign in to comment.