Portal Community

Node Configuration

{
  "nodeType": "PermissionCheck",
  "name": "assertCanApprove",
  "config": {
    "userId": "$context.actorId",
    "permission": "invoices:approve",
    "resource": "$output.createInvoice.invoiceId",
    "failMessage": "Actor does not have invoices:approve permission"
  }
}

Configuration Fields

FieldTypeDescription
userIdstring / exprThe user ID to check. Commonly $context.actorId or $output.fetchApprover.userId.
permissionstringThe permission key to assert. Format: resource:action (e.g., invoices:approve).
resourcestring / exprOptional resource ID for resource-scoped permission checks. If omitted, checks global permission.
failMessagestringThe error message written to the error port output when the check fails.

Routing Pattern

The PermissionCheckNode produces no data output — it is a gate. Connect the main port to the protected path and the error port to an access-denied handler:


PermissionCheckNode (assertCanApprove)
    │
    ├── [main] ──→ Protected workflow steps
    │
    └── [error] ─→ SendEmail (access denied notification)
                   └──→ EndNode (terminate)
  

Error Port Output

{
  "error": "Actor does not have invoices:approve permission",
  "permission": "invoices:approve",
  "userId": "usr-a1b2c3",
  "resource": "inv-99887",
  "checkedAt": "2026-05-25T10:00:00Z"
}

PermissionCheckExecutor

public class PermissionCheckExecutor : BaseNodeExecutor<PermissionCheckConfig>
{
    protected override async Task<NodeExecutionResult> ExecuteAsync(
        PermissionCheckConfig config,
        NodeDataContext ctx,
        CancellationToken ct)
    {
        var userId = _evaluator.Evaluate<string>(config.UserId, ctx);
        var resource = config.Resource != null
            ? _evaluator.Evaluate<string>(config.Resource, ctx)
            : null;

        var hasPermission = await _passport.CheckPermissionAsync(new PermissionCheckRequest
        {
            UserId = userId,
            Permission = config.Permission,
            Resource = resource,
            TenantId = ctx.TenantId
        }, ct);

        if (!hasPermission)
            return NodeExecutionResult.Fail(new PermissionDeniedException(
                config.FailMessage ?? $"Permission denied: {config.Permission}"));

        return NodeExecutionResult.Success(null);
    }
}
Defense in depth: The PermissionCheckNode is a workflow-level gate — useful for business logic enforcement. It does not replace API-level authorization in the backend. Critical operations should be protected at both layers.