Skip to main content

Module masking

Module masking 

Source
Expand description

PII masking — classification-driven redaction at every data sink.

§Why this is load-bearing for the rest of the framework

The compliance machinery shipped so far makes data durable on purpose: the audit trail is hash-chained append-only, outbox rows are committed, idempotency caches replay responses for hours, and the DLQ parks full payloads. Every one of those becomes a permanent PII leak if a card number or patient name reaches it raw — you cannot delete from a sealed ledger without destroying the very tamper-evidence it exists to provide. Masking therefore happens at the sink, before durability.

§Zero-lock mechanics

The rule set lives behind one ArcSwap<MaskingPolicy> (the proven pattern from secrets / tenants / policies): applying masks is one atomic pointer load plus a pure walk over the JSON tree against paths that were parsed at load time — no regex, no I/O, no locks on any hot path.

§Usage

// boot — global rules by data classification:
ctx.provide(Masker::new(
    MaskingPolicy::new(1)
        .field("email")                      // Redact (default)
        .field("payment.card.number:last4")  // PCI PAN
        .field("ssn:drop")
        .field("items.*.patient_name:hash"), // joinable, unreadable
));

// route — extra fields for this endpoint only:
#[Get("/:id", security("bearer"))]
#[MaskFields("notes.*.body", "phone:last4")]
async fn get_patient(/* ... */) -> Result<Json<Patient>, HttpException> { /* ... */ }

No Masker in the DI container → every sink passes data through untouched (dev mode needs zero configuration).

Structs§

MaskRule
A field rule: where + how.
Masker
Hot-swappable redaction point. Provide via ctx.provide(Masker::new(…)).
MaskingPolicy
Versioned, immutable rule set (hot-swapped whole, like PolicySet).

Enums§

MaskStrategy
How a matched field is transformed.
PathSeg
One segment of a compiled field path. * matches every array element (or every object value), enabling rules like items.*.email.