TransformMiddleware lets you run logic before and after every transform execution. Typical use cases include logging, request shaping, per-user concurrency limiting, shared execution context, audit trails, and OAuth token handling.
Lifecycle
Every transform execution goes through the same middleware lifecycle:
before_transform(...)runs before the transform starts.- The transform executes.
after_transform(...)runs with the output entities, final execution state, and any collected exceptions.
By default after_transform(...) also runs on failures because TransformMiddleware.call_on_failure defaults to True. Set call_on_failure = False on your middleware if you only want the after-hook for successful runs.
Timeouts
Both middleware hooks use MaltegoServerSettings.middleware_execution_timeout. This budget applies to each middleware call individually, not to the whole chain -- if one middleware's before_transform exceeds it, the SDK raises a transform exception naming that middleware and aborts the execution; other middlewares each get their own fresh budget. The transform function itself has a separate budget, transform_execution_timeout.
Where this fits in the request chain
TransformMiddleware is not the first thing that sees a request. For a transform-execution call, in order:
- Your reverse proxy/gateway (TLS, WAF, rate limiting) and the ASGI/uvicorn layer reject malformed requests and unmatched routes before any SDK code runs. See Deploying Behind a Reverse Proxy.
- If
AuthSettings.enabledis set, the SDK's authentication dependency validates the JWT/OIDC/SAML credential and populatescontext.identity/context.auth_claims. This runs before the transform is even scheduled -- instrictmode, a rejected credential meansbefore_transformnever runs at all. See Authentication. - Only then does
before_transformrun for each configured middleware, in this order: built-ins first (UserConcurrencyLimitMiddlewareif configured, thenVerifyMetadataMiddleware, thenOAuthMiddlewareif an OAuth authenticator is registered), then yourtransform_middlewaresin the order you passed them torun_server. - The transform function.
after_transformfor each middleware, same order as step 3.
Because authentication has already run by step 3, context.identity and context.auth_claims are available and trustworthy inside before_transform/after_transform -- this is what makes TransformMiddleware the right place for authorization decisions (is this already-authenticated caller allowed to run this transform), as opposed to authentication (is this credential valid at all), which belongs in AuthSettings instead. See "Example: decoupling authorization, auditing, and logging" below for a worked pattern.
Creating a middleware
from typing import Any, Dict, List, Optional, Sequence, Union
from maltego.middlewares.middlewares import TransformMiddleware
from maltego.model.context import MaltegoContext
from maltego.model.entity import MaltegoEntity
from maltego.model.graph import MaltegoGraph
from maltego.model.transform import MaltegoTransform
from maltego.model.types import ExecutionState, MaltegoSettingTypes
class LoggingMiddleware(TransformMiddleware):
async def before_transform(
self,
transform: MaltegoTransform,
transform_input: Union[MaltegoEntity, List[MaltegoEntity], MaltegoGraph[Any]],
properties: Dict[str, MaltegoSettingTypes],
context: MaltegoContext,
soft_limit: int,
hard_limit: int,
) -> None:
context.log.inform(f"Starting {transform.display_name}")
async def after_transform(
self,
transform: MaltegoTransform,
transform_input: Union[MaltegoEntity, List[MaltegoEntity], MaltegoGraph[Any]],
output_entities: List[MaltegoEntity],
context: MaltegoContext,
state: ExecutionState,
exceptions: Optional[Sequence[Exception]] = None,
) -> None:
context.log.inform(
f"Finished {transform.display_name} with state {state} and "
f"{len(output_entities)} output entities"
)Registering middlewares
Pass middleware instances to run_server:
run_server(
settings=settings,
transform_middlewares=[LoggingMiddleware()],
)run_server accepts a transform_middlewares parameter (a list of TransformMiddleware instances) alongside its other server-startup options. Configure HTTP, HTTPS, and certificate paths on ServerHTTPSettings inside MaltegoServerSettings so server startup uses a single HTTP configuration source. Middleware instances passed here are used to add additional logic right before or right after each transform runs.
Built-in middleware behavior
The SDK already wires in a few middleware behaviors for you:
VerifyMetadataMiddlewareis added by default.OAuthMiddlewareis added automatically when the first OAuth authenticator is registered.UserConcurrencyLimitMiddlewareis added whenmax_concurrent_transforms_per_useris configured.
See the Execution Runner article for how middleware execution fits into the wider transform execution lifecycle.
Example: decoupling authorization, auditing, and logging
A common request is to keep authorization, auditing, and logging out of transform business logic, and to be able to swap the systems behind them (an RBAC service, Kafka, Splunk, ...) without touching every transform.
The pattern: keep each middleware thin, and have it delegate to a small adapter class built once at startup. Swapping systems later means changing the adapter, not the middleware or the transforms:
import logging
from maltego.middlewares.middlewares import TransformMiddleware
from maltego.model.exception import MaltegoException
from maltego.server import AuthSettings, run_server
logger = logging.getLogger(__name__)
# Adapters -- the only thing that changes when you swap systems.
class PolicyChecker: # back this with OPA, your RBAC service, etc.
async def allowed(self, *, identity, claims) -> bool: ...
class AuditWriter: # today Kafka, tomorrow Splunk, same interface
async def record(self, *, identity, outcome): ...
# Thin middlewares: they read the context and delegate to an adapter.
class AuthorizationMiddleware(TransformMiddleware):
def __init__(self, policy):
self.policy = policy
async def before_transform(self, transform, transform_input, properties,
context, soft_limit, hard_limit):
# context.identity/context.auth_claims are already populated here --
# authentication ran before this middleware, not inside it.
if not await self.policy.allowed(identity=context.identity,
claims=context.auth_claims):
raise MaltegoException("Not authorised")
class AuditMiddleware(TransformMiddleware):
call_on_failure = True # explicit here for clarity; it's the default
def __init__(self, audit):
self.audit = audit
async def after_transform(self, transform, transform_input, output_entities,
context, state, exceptions=None):
logger.info("transform finished, outcome=%s", state)
await self.audit.record(identity=context.identity, outcome=state)
# Wire it once at server startup. Swap the adapter, not the middleware.
# `settings` here is the MaltegoServerSettings from earlier in this article.
run_server(
settings=settings,
auth_settings=AuthSettings(enabled=True, token_origin="sso", provider_type="oidc",
provider_url="https://id.example.com/..."),
transform_middlewares=[
AuthorizationMiddleware(PolicyChecker()),
AuditMiddleware(AuditWriter()),
],
)A few things worth calling out:
AuthorizationMiddlewareandAuditMiddlewarenever call an identity provider or parse a token themselves -- that already happened in the auth layer (see "Where this fits in the request chain" above). They only make policy/audit decisions from an already-resolved identity.AuditMiddleware.call_on_failure = Truemeans the audit record and log line are written even when the transform raises, sinceafter_transformstill runs on failure by default.- If an adapter talks to an HTTP API, the Integration Client article's client gives you retries, rate limiting, and concurrency control for it. For queues, caches, or databases, use your usual client libraries.
- Network-level concerns -- rate limiting, WAF rules, mTLS, protocol-level rejection -- belong at your reverse proxy/gateway, not in a
TransformMiddleware. See Deploying Behind a Reverse Proxy.
A runnable version of this pattern, including a demo transform that reads context.identity, ships in the generated starter project as transforms/middleware_example.py -- see Installing and Setting Up the SDK.
API reference
TransformMiddleware (maltego.middlewares.middlewares.TransformMiddleware) is an abstract base class. To create a middleware, subclass it and implement both abstract methods below.
| Member | Description |
|---|---|
call_on_failure | Class attribute, defaults to True. When True, after_transform still runs if the transform failed. |
before_transform( self, transform, transform_input, properties, context, soft_limit, hard_limit) | Abstract async method. Called before the transform executes. Receives the transform object, the transform input (a MaltegoEntity, list of MaltegoEntity, or MaltegoGraph), the client-sent settings (properties), the execution context, and the client's soft_limit/hard_limit values. Returns None. |
after_transform( self, transform, transform_input, output_entities, context, state, exceptions) | Abstract async method. Called after the transform executes. Receives the transform object, the transform input, the output_entities produced by the transform, the execution context, the final ExecutionState, and an optional sequence of exceptions collected during execution. Returns None. |
For the full class reference (including MaltegoTransform, MaltegoContext, MaltegoEntity, and MaltegoGraph), see the SDK API Reference article.