Skip to content

WW-5626 per-property authorization for Jackson REST handlers (Approach C)#1674

Draft
lukaszlenart wants to merge 13 commits intoWW-5626-cleanupfrom
WW-5626-approach-c
Draft

WW-5626 per-property authorization for Jackson REST handlers (Approach C)#1674
lukaszlenart wants to merge 13 commits intoWW-5626-cleanupfrom
WW-5626-approach-c

Conversation

@lukaszlenart
Copy link
Copy Markdown
Member

@lukaszlenart lukaszlenart commented May 4, 2026

Fixes WW-5626.

Part 2 of 2. Builds on the cleanup PR #1673 (do not merge until that one lands; base will be retargeted to main afterwards).

What this changes

Replaces the post-hoc reflection-based property filtering in ContentTypeInterceptor with per-property authorization owned by the handlers themselves, for all Apache-shipped REST handlers.

Architecture

  • ParameterAuthorizationContext (core) — ThreadLocal holder for per-request (authorizer, target, action, pathStack). The interceptor binds it before invoking authorization-aware handlers and unbinds in a finally block.
  • AuthorizationAwareContentTypeHandler (rest) — marker interface. Handlers implementing it signal that their toObject honors ParameterAuthorizationContext for per-property @StrutsParameter enforcement.
  • ParameterAuthorizingModule + AuthorizingSettableBeanProperty (rest) — Jackson plumbing: BeanDeserializerModifier wraps every SettableBeanProperty, checks ParameterAuthorizationContext.isAuthorized(path) before delegating, calls JsonParser.skipChildren() for unauthorized values, manages the path stack with [0] suffix for collection/map/array-typed properties.
  • ContentTypeInterceptor — when requireAnnotations=true AND handler instanceof AuthorizationAwareContentTypeHandler: binds context, calls handler.toObject(...) directly. Otherwise: legacy two-phase copy (preserved as-is).

Handler coverage

Handler Status Mechanism
JacksonJsonHandler (default json) Authorization-aware BeanDeserializerModifier + skipChildren() — strongest guarantee: unauthorized nested subtrees are never instantiated
JacksonXmlHandler (default xml) Authorization-aware Same as JSON (XmlMapper extends ObjectMapper)
JuneauXmlHandler (opt-in) Authorization-aware Post-parse walk in handler; functionally equivalent to the legacy two-phase copy but handler-owned and using ParameterAuthorizationContext. Setter side effects on transient unauthorized nested objects can fire — they're then discarded (a known limitation due to Juneau's inside-out parse order, see commit fc2f54155)
XStreamHandler (opt-in) @Deprecated(since="7.2.0", forRemoval=true) Falls back to legacy two-phase copy in ContentTypeInterceptor. Deprecation cites XStream's CVE history and the per-class allowlist requirement; users pointed to JacksonXmlHandler

Why this is better than the cleanup PR's two-phase copy

  • No no-arg constructor requirement for Jackson handlers
  • Stronger security for Jackson: rejecting at the parent property means the unauthorized JSON subtree is discarded entirely — Jackson never instantiates nested objects, so setter side effects on unauthorized properties never fire
  • No reflection-based deep copy for Jackson handlers — bypasses ~250 lines of post-hoc filtering
  • Juneau also bypasses the legacy fallback — both handlers it ships now use ParameterAuthorizationContext directly
  • No fragile isNestedBeanType package-name heuristic for Jackson — Jackson's JavaType introspection handles type detection correctly

Out of scope (preserved as legacy fallback)

The legacy two-phase copy in ContentTypeInterceptor is not removed — XStreamHandler and any custom third-party handlers without the marker interface continue to use it. A future PR migrating those (or removing XStream entirely after the deprecation period) can finally delete the dead code.

Spike

A spike that validated the Jackson BeanDeserializerModifier mechanism is in commit 6110b8f6b and removed in commit 0166c6a0b (replaced by the production tests in ParameterAuthorizingModuleTest). A second spike for Juneau's BeanInterceptor confirmed why parser-level interception isn't viable for that library (inside-out call order); the post-parse walk approach was chosen instead.

Test plan

  • mvn test -DskipAssembly -pl coreParameterAuthorizationContextTest 12 new tests pass; full core suite zero regressions
  • mvn test -DskipAssembly -pl plugins/restParameterAuthorizingModuleTest 8 new tests pass; ContentTypeInterceptorIntegrationTest 7 tests pass (5 from cleanup PR now silently exercise the new path + 2 new); JuneauXmlHandlerIntegrationTest 5 new tests pass
  • mvn test -DskipAssembly -pl '!plugins/bean-validation,!plugins/tiles' — full multi-module BUILD SUCCESS (the two excluded modules fail identically on main for unrelated reasons: AnnotationFormatError from Hibernate Validator on a test model, and missing Velocity dependencies in the tiles plugin)

lukaszlenart and others added 13 commits May 4, 2026 13:40
Validates that the Approach C design is feasible before committing to a detailed
implementation plan. Wraps each SettableBeanProperty via BeanDeserializerModifier;
intercepts deserializeAndSet to authorize against a path built from a ThreadLocal
Deque; uses skipChildren() to discard unauthorized values; uses [0] suffix for
collection/map/array elements to match ParametersInterceptor depth semantics.

Findings:
- Delegating base class via 'protected delegate' field is the right pattern
- addOrReplaceProperty(prop, true) is the correct builder API
- Reject-at-parent skips all nested deserialization (better security than two-phase
  copy: setter side effects on unauthorized properties never fire)
- JavaType#isCollectionLikeType/isMapLikeType/isArrayType detects the indexed-path case

Spike is kept under .../spike/ as a learning artifact; it will be replaced by
production code + tests in subsequent commits.
…orization

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements AuthorizationAwareContentTypeHandler. When ParameterAuthorizationContext
is active (set by ContentTypeInterceptor when requireAnnotations=true), the handler
walks the parsed result tree and copies only authorized properties to the target,
descending into nested beans/collections/maps/arrays with indexed-path semantics
([0] suffix) for parity with ParametersInterceptor.

Note: Juneau parses the entire result tree before our walk runs, so setter side
effects on transient nested objects can fire even for unauthorized properties —
those transient objects are then discarded. This is functionally equivalent to the
legacy two-phase copy in ContentTypeInterceptor; only the Jackson handlers achieve
the stronger guarantee where unauthorized subtrees are never instantiated at all
(they use Jackson's BeanDeserializerModifier + skipChildren).

When no context is bound (default config), behavior is unchanged: parser.parse +
BeanUtils.copyProperties.
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented May 5, 2026

Quality Gate Failed Quality Gate failed

Failed conditions
65.2% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant