Skip to content

WW-5725 Authorize the buffered creator path in AuthorizingSettableBeanProperty - #1916

Draft
lukaszlenart wants to merge 1 commit into
mainfrom
WW-5725-rest-buffered-set
Draft

WW-5725 Authorize the buffered creator path in AuthorizingSettableBeanProperty#1916
lukaszlenart wants to merge 1 commit into
mainfrom
WW-5725-rest-buffered-set

Conversation

@lukaszlenart

Copy link
Copy Markdown
Member

Fixes WW-5725

AuthorizingSettableBeanProperty authorized a property in deserializeAndSet / deserializeSetAndReturn and wrapped the value deserializer only for creator-bound properties. Jackson takes neither route for a non-creator property it buffers during property-based creation: a setter property that appears in the body before the last creator parameter is read through the final SettableBeanProperty.deserialize() with no authorization and no path push, and assigned after construction through SettableBeanProperty.set(), which the Delegating base forwarded unchecked. The same property after the last creator parameter, or on a setter-only type, was already rejected, so member order alone decided whether the check applied, and the members of a buffered bean-valued property were checked one level too shallow.

Three gates now cover the paths between them:

  • deserializeAndSet / deserializeSetAndReturn keep authorizing the direct path and skipping the value on rejection, so the setter never fires, but no longer push the path themselves.
  • AuthorizingValueDeserializer wraps every property's value deserializer, not only creator properties, and owns the path push for nested members. It covers all three of Jackson's entry points — deserialize(p, ctxt), the in-place deserialize(p, ctxt, intoValue) used for setterless collections, and deserializeWithType for polymorphic properties — and classifies the [0] element prefix on the property's declared type. On the direct path it re-checks a path deserializeAndSet already accepted; the authorizer is stateless, so the answer is the same.
  • set / setAndReturn authorize the already-materialized assignment, which also covers a buffered null (Jackson skips the value deserializer for a null token) and the other callers of set() in jackson-databind: @JsonMerge, @JsonManagedReference, inner-class valued properties, EXTERNAL_PROPERTY type ids and the @JsonIdentityInfo id property, none of which were authorized before.

All gates go through DynamicKeyAuthorizationContext, so a dynamic-key scope authorizes by depth on every path.

Tests (all in ParameterAuthorizingModuleTest): setter before and after the last creator parameter, a setter-only type with the same member order, a nested creator, a creator-plus-setter type inside a dynamic-key scope, a buffered bean-valued setter whose members must be authorized at their own depth, a setterless collection, and a polymorphic property. The last two are written in the direction that matters — a sibling grant on the enclosing bean must not authorize a collapsed nested path — and both failed against an intermediate version of this change that had moved the push but not yet covered the 3-arg and typed entry points.

Behaviour notes. No configuration or API change. A request that previously had an unauthorized property assigned only because of its position in the body now has it dropped, consistent with every other position. Properties assigned through the other set() callers listed above are now subject to the same authorization as ordinary setters — most visibly, an @JsonIdentityInfo id property now needs @StrutsParameter like any other. A buffered rejection logs twice (once at the read, once at the assignment); the assignment-side warning is kept because it is the only one for the other set() callers.

🤖 Generated with Claude Code

…SettableBeanProperty

AuthorizingSettableBeanProperty authorized a property in deserializeAndSet
and deserializeSetAndReturn and wrapped the value deserializer only for
creator-bound properties. Jackson takes neither route for a non-creator
property it buffers during property-based creation: a setter property
that appears in the body before the last creator parameter is read
through the final SettableBeanProperty.deserialize(), with no
authorization and no path push, and assigned after construction through
PropertyValue.Regular.assign -> SettableBeanProperty.set(), which the
Delegating base forwarded unchecked. The same property after the last
creator parameter, or on a setter-only type, was already rejected, so
member order alone decided whether the check applied, and the members
of a buffered bean-valued property were checked one level too shallow.

Three gates now cover the paths between them:

- deserializeAndSet / deserializeSetAndReturn keep authorizing the
  direct path and skipping the value on rejection, so the setter never
  fires, but no longer push the path themselves.
- AuthorizingValueDeserializer wraps every property's value
  deserializer, not only creator properties, and owns the path push for
  nested members. It now covers all three of Jackson's entry points --
  deserialize(p, ctxt), the in-place deserialize(p, ctxt, intoValue)
  used for setterless collections, and deserializeWithType for
  polymorphic properties -- and classifies the [0] element prefix on
  the property's declared type rather than the deserializer's handled
  type. On the direct path it re-checks a path deserializeAndSet
  already accepted; the authorizer call is stateless, so the answer is
  the same.
- set / setAndReturn authorize the already-materialized assignment,
  which also covers a buffered null (Jackson skips the value
  deserializer for a null token) and the other callers of set() in
  jackson-databind: @JsonMerge, @JsonManagedReference, inner-class
  valued properties, EXTERNAL_PROPERTY type ids and the
  @JsonIdentityInfo id property, none of which were authorized before.

All gates go through DynamicKeyAuthorizationContext so a dynamic-key
scope authorizes by depth on every path.

Tests cover the setter before and after the last creator parameter, a
setter-only type with the same member order, a nested creator, a
creator-plus-setter type inside a dynamic-key scope, a buffered
bean-valued setter whose members must be authorized at their own
depth, a setterless collection, and a polymorphic property -- the last
two in the direction that matters: a sibling grant on the enclosing
bean must not authorize a collapsed nested path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
78.4% 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