Skip to content

Ktor

ihawu-ktor is the Ktor server adapter — the same zero-wiring experience the Spring Boot starter gives Spring. A single install(IhawuKtor) masks every @IhawuResource response for the caller who receives it. It builds on ihawu-kotlinx (the kotlinx.serialization backend), so it pulls ihawu-kotlinx and ihawu-core transitively — it is the only dependency you add.

implementation("org.ihawu:ihawu-ktor:0.4.1")
install(IhawuKtor) {
// Map the authenticated call to a caller. No principal -> the response fails closed to {}.
resolvePrincipal = { it.ihawuPrincipal() }
// Static role-based rules (or supply your own ResourcePolicyResolver via policyResolver(...)).
policies(
ResourcePolicy(
"employee",
mapOf("MANAGER" to listOf(FieldPolicy("ssn", MaskingStrategy.REDACT, "***-**-****"))),
),
)
// Register each @IhawuResource serializer under its resource name so it masks when serialized.
resources(Employee.serializer() to "employee")
}

The one install folds in the ContentNegotiation registration — do not install it separately.

  • Response masking — a custom ContentConverter wraps the kotlinx masking serializer, so every @IhawuResource response is masked as it serializes. Non-resource responses pass through untouched.
  • Identity bridgeresolvePrincipal maps each call to an IhawuPrincipal; the default ApplicationCall.ihawuPrincipal() reads one straight from Ktor Authentication. The masking context is installed on the send pipeline, so it resolves after route-level authenticate { }.
  • Per-call caching — each (principal, resource) resolves at most once per call, for free, via the engine’s request-scoped memoization.
  • Fail-closed — no principal → {}, matching the other adapters.

A fail-closed {} (HTTP 200) is ambiguous — fully masked for this caller or policy resolution failed — so a policy-store outage can go unnoticed. Two opt-in knobs make it visible:

Failure metrics. Point onFailClosed at a Micrometer registry to count every fail-closed drop as ihawu.masking.failures{resource,reason} — the same metric the Spring Boot starter emits. Alert on reason=RESOLVER_ERROR.

install(IhawuKtor) {
onFailClosed = MicrometerMaskingFailureSink(appMeterRegistry)
// …or log AND count: CompositeMaskingFailureSink(myLogSink, MicrometerMaskingFailureSink(registry))
}

Fail the request instead of masking. By default a resolver outage masks to {} behind a 200. Set onPolicyFailure = ResolverErrorMode.FAIL_REQUEST and it instead surfaces as a 500, so the outage is not silent (only the resolver-error path — a missing principal still masks). Add Ktor’s StatusPages if you want a custom error body. Best-effort: a resource nested past the point the response is already committed can’t become a clean 500.

install(IhawuKtor) {
onPolicyFailure = ResolverErrorMode.FAIL_REQUEST
}

Polymorphic @IhawuResource responses mask too — the concrete subtype is masked and the class discriminator preserved, in both the default and array-polymorphism encodings, nested and in collections.

  • Sealed hierarchies work out of the box.
  • OPEN (non-sealed abstract/interface) hierarchies additionally require registering their subtypes on the Json’s SerializersModule — pass that Json to the plugin via json = … and the converter threads its module through.

The runnable sample shows both (/payment sealed, /alert OPEN).

Ktor masking runs on the ihawu-kotlinx backend, which materialises the response tree and rewrites it — a different cost profile from the Jackson streaming path (see the backend comparison).

For a working reference, the runnable sample shows the plugin against a live Netty endpoint with two roles and a testApplication test that pins each role’s masked JSON.

Full API on the Dokka reference for ihawu-ktor.