Skip to content

How Ihawu Works

Ihawu has a small, deliberate model. Four ideas cover almost everything.

A resource is a type you want masked, marked with @IhawuResource("name"). The name is the key policies resolve against. Types without the annotation serialize unchanged.

A policy maps a resource and a caller’s role(s) to a list of field policies. Resolution is a pure lookup: given the caller’s IhawuPrincipal and the resource name, Ihawu asks a ResourcePolicyResolver for the field policies that apply, and enforces exactly those. Policies can come from static configuration or a dynamic source (database, OPA) behind a simple provider SPI.

Each field policy carries a masking strategy, applied so the output still satisfies the field’s declared type:

  • HIDE omits the field — valid on a nullable/optional field.
  • REDACT replaces the value: a placeholder (e.g. ***-**-****) on a String, JSON null on any other nullable field.

A masked non-String field must therefore be declared nullable. A policy that cannot satisfy the type — a HIDE on a required field, or a REDACT on a non-nullable non-String — fails closed, and on Spring Boot is rejected at startup. When multiple roles apply to a field, the stricter strategy wins.

Ihawu draws a careful line between two kinds of “nothing matches”:

  • Missing identity — no verified principal on the call. Ihawu fails closed and emits {}. No identity, no data.
  • Missing policy — a principal is present, but no rule restricts a field. Ihawu fails open for that field: masking is a denylist, so unconfigured fields are public by design.

This asymmetry — fail closed on missing identity, fail open on missing policy — keeps Ihawu safe without turning it into an allowlist that would require a rule for every field just to see any data.

Telling a fully-masked response from a failure

Section titled “Telling a fully-masked response from a failure”

Because Ihawu fails closed, an empty {} is ambiguous: it can mean “fully masked for this caller” or “policy resolution failed and Ihawu fell back to masking everything” — byte-identical on the wire. Ihawu logs every fail-closed decision with the resource name (never the value), so you can tell them apart:

  • WARN — no IhawuPrincipal on the call.
  • ERROR — the resolver threw (a misconfiguration or a policy-store outage).
  • ERROR — a field’s policy could not be honoured at runtime (HIDE on a non-nullable field, or REDACT on a non-nullable non-String).

Alert on the resolver-failure ERROR: a sustained rate means your policy source is down and every resource is degrading to {} — an outage behind a 200. Since 0.4.x you can also count these drops (the ihawu.masking.failures{resource,reason} Micrometer metric) and opt into a fail-request mode that turns a resolver outage into a 5xx — see the Spring Boot starter and Ktor pages.

Ihawu enforces the decisions your policy source returns; it never evaluates authorization conditions itself. That separation is what lets it stay a small, auditable layer at the serialization boundary rather than a second, competing authorization system.

Ihawu enforces at one exit: a serializer with Ihawu’s masking installed — a Jackson ObjectMapper with IhawuModule, or a kotlinx.serialization masking serializer — writing a value with an IhawuPrincipal attached. Your object still travels through the application in full, and nothing masks it on any other path out of the process. It is not masked when it is:

  • written to a log — log.info("user=$user"), or a toString() inside an exception stack trace;
  • published to Kafka or any other event bus;
  • written to a cache such as Redis or Caffeine;
  • exported to CSV or Excel, or rendered into a server-side template;
  • serialized by a second serializer that does not have Ihawu’s masking installed.

This is inherent to enforcing at the serialization boundary, and it is a deliberate trade: masking where the response is written is what lets your controllers return whole, truthful domain objects and keeps the enforcement layer small enough to audit. But it means Ihawu masks API responses, not “sensitive data” in general. If an SSN must never appear in a log line, Ihawu is not what stops it.

Treat Ihawu as last-mile, defense-in-depth enforcement, layered with controls that act closer to the data — column-level grants, row-level security, and vendor dynamic masking, which are stronger wherever they apply because the value never reaches your JVM at all. Ihawu’s own ground is the case those cannot express: the application legitimately needs the full value, but this caller must not receive it. See Comparison for how the layers fit together.