Security via NodePolicies
NodePolicies enforce which DataStateMachine handler a node can access, which scope levels it can write to, and which tenant contexts it operates in — all evaluated in the execution pipeline before the handler is ever resolved.
Where Policies Sit in the Pipeline
Node Execution Request
│
▼
NodePolicyEvaluator ◄─── BEFORE DataStateMachineFactory is called
│
├── Is this node allowed to use handler "blockchain-state"?
├── Is this node allowed to write at "App" scope?
├── Does the requesting tenant have access to this handler?
│
├── [All checks pass] → continue to DataStateMachineFactory
│
└── [Any check fails] → execution blocked, audit log entry created
What NodePolicies Control
| Policy Dimension | What It Controls | Example |
|---|---|---|
| Handler Access | Which HandlerKey values the node may use | "built-in" only; or "blockchain-state" allowed |
| Scope Ceiling | Maximum scope level the node may declare | Cannot exceed "Element" scope |
| Tenant Isolation | Which tenant IDs the node can act on behalf of | Node can only write for its own tenant |
| Capability Permissions | Which NodeCapabilities the node can expose | Can expose "entity" but not "datasource" |
| Audit Requirements | Whether all state writes must be audit-logged | Mandatory for financial nodes |
Policy Configuration
Policies are declared in the NodeCapabilityPolicy entity (managed by the ProcessSecurity module) and evaluated by the NodePolicyEvaluator before the DataStateMachineFactory creates the handler instance.
{
"nodeType": "InvoiceDispatchExecutor",
"dataStateMachine": {
"allowedHandlers": ["built-in"],
"maxScope": "Element",
"requireAuditLog": true,
"tenantIsolation": "strict"
},
"capabilities": {
"allowed": ["entity", "businessService"],
"denied": ["datasource", "rules"]
}
}
Privilege Keys
Individual operations can require privilege keys. A node must be granted the key before it can perform the operation. This allows fine-grained control within a single handler.
// NodePolicyEvaluator — privilege key enforcement
if (!await _policyService.HasPrivilegeAsync(
nodeType: "InvoiceDispatchExecutor",
privilegeKey: "state:write:blockchain",
tenantId: ctx.TenantID,
ct))
{
_auditLog.Write(AuditLevel.Warning,
$"Node {nodeType} attempted blocked privilege: state:write:blockchain");
throw new NodePolicyViolationException("Privilege 'state:write:blockchain' not granted");
}
Audit Trail
Every state write by a node that has requireAuditLog: true generates a SecurityAuditLog entry. The entry records the node type, tenant, handler used, scope, item keys, status transition, and the calling user/service identity.
| Field | Description |
|---|---|
| NodeType | The executor class name |
| HandlerKey | Which handler was invoked |
| Scope | The scope level used |
| ItemKey / ItemEntityKey | What was processed |
| StatusFrom / StatusTo | The state transition |
| TenantID | Tenant context |
| ActorID | User or service identity |
| Timestamp | When the transition occurred |
| PolicyDecision | Allowed / Denied |
Tenant Isolation
Strict tenant isolation means a node can only read and write state records for the tenant it is executing under. The DataStateMachineFactory always binds TenantID from the workflow thread context. NodePolicies add a second enforcement layer: if the node config specifies a different tenantId, strict isolation blocks it before the factory is called.
Never configure a node with tenantIsolation: 'relaxed' in production without explicit approval from the security team. Relaxed mode allows cross-tenant state reads, which can expose data across organisational boundaries.
Use requireAuditLog: true for all nodes that process financial transactions, compliance records, healthcare data, or personal information. The audit trail is your evidence in regulatory inquiries.