Repository navigation
Description
Hiding a field reached into the live object and nulled it by reflection. The object being
modified belongs to the application — it is whatever resolveAttributes(...) returned — and
nothing in the contract requires that to be freshly built. A resource returning a cached DTO, a
shared constant, or a JPA entity would have the field permanently nulled, so the next request
from any caller, including one with full access, saw it empty. Returning a fresh attributes
object was not even sufficient, because the recursion descends and would corrupt a shared
nested object inside a fresh wrapper.
Hiding now produces a copy with the denied fields blanked and leaves the original untouched.
Copies are made only along the path to a hidden field, and only when something is actually
hidden — when a caller may see everything, the original instance is passed straight through and
nothing is allocated. Building the copy does not invoke the application's constructor, so
constructors that validate their arguments, normalise them, or take them in a different order
than the fields are declared all work; records, whose components cannot be written after
construction, are rebuilt through their canonical constructor.
This also fixes a real defect rather than only a latent one: records could not be used as
attributes classes at all. The old path wrote fields reflectively, which record components
forbid, so field-level access control on a record failed at request time — and only for denied
callers, so it passed every happy-path test. Anonymization also had no test coverage of any
kind before this change.
Three declared-but-ineffective cases now warn once when the model is built, rather than failing
late or not at all: access control on a primitive field (which can never hold an absence),
on a collection, array or map element class (not enforced — only hiding the container field
itself works), and on a static field (never serialized). The element case is the one that fails
open, which is why it is worth saying out loud until element traversal exists.
The Quarkus extension now registers redactable classes for native image, since building a copy
instantiates the class reflectively. Note that Quarkus indexes the application module but not
its dependencies, so attributes classes in a separate jar need quarkus.index-dependency; the
build warns when access control is enabled and the index contains no @AccessControl. Native
image support for the framework as a whole remains unverified.
Adds a compile dependency on Objenesis, scoped to the access control plugin rather than to
jsonapi4j-base, so only users who opt into access control inherit it.
Activity
#147: copy attributes objects when hiding fields instead of mutating …