Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,7 @@

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.deser.CreatorProperty;
import com.fasterxml.jackson.databind.deser.SettableBeanProperty;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
Expand All @@ -37,10 +35,10 @@
* skipped via {@link JsonParser#skipChildren()}, so any nested object graph is never instantiated
* and setter side effects on unauthorized properties never fire.
*
* <p>Path tracking: the wrapper pushes the full path of the current property onto the context's
* path stack before delegating, then pops in a {@code finally} block. For collection / map / array-typed
* properties, the path pushed is suffixed with {@code [0]} so nested element members produce paths like
* {@code items[0].field} — matching {@code ParametersInterceptor} depth semantics.</p>
* <p>Path tracking for nested members is done by the {@link AuthorizingValueDeserializer} wrapped
* around every property's value deserializer, so the direct path and the buffered creator path
* compute the same paths. Values Jackson assigns after construction go through {@link #set} and
* {@link #setAndReturn}, which apply the same authorization to the already-materialized value.</p>
*
* <p>When {@link ParameterAuthorizationContext#isActive()} is {@code false}, this wrapper is a
* straight pass-through to the delegate — no overhead for default-config requests.</p>
Expand All @@ -61,17 +59,17 @@ protected SettableBeanProperty withDelegate(SettableBeanProperty d) {
}

/**
* Creator-bound properties (records, {@code @JsonCreator} constructors) never reach
* {@link #deserializeAndSet}/{@link #deserializeSetAndReturn}: Jackson calls the {@code final}
* {@code SettableBeanProperty#deserialize} directly, through this property's own value deserializer.
* Wrap that deserializer with {@link AuthorizingValueDeserializer}, scoped to {@link CreatorProperty}
* so ordinary setter/field/builder properties -- already authorized below -- aren't double-checked.
* Creator-bound properties, and non-creator properties Jackson buffers while collecting creator
* parameters, never reach {@link #deserializeAndSet}/{@link #deserializeSetAndReturn}: Jackson calls
* the {@code final} {@code SettableBeanProperty#deserialize} directly, through this property's own
* value deserializer. Wrap that deserializer with {@link AuthorizingValueDeserializer} for every
* property; it owns the path push for nested members on both the direct and the buffered path.
*/
@Override
public SettableBeanProperty withValueDeserializer(JsonDeserializer<?> deser) {
JsonDeserializer<?> effective = deser;
if (delegate instanceof CreatorProperty && !(deser instanceof AuthorizingValueDeserializer)) {
effective = new AuthorizingValueDeserializer(deser, getName());
if (!(deser instanceof AuthorizingValueDeserializer)) {
effective = new AuthorizingValueDeserializer(deser, getName(), getType());
}
return _with(delegate.withValueDeserializer(effective));
}
Expand All @@ -90,12 +88,7 @@ public void deserializeAndSet(JsonParser p, DeserializationContext ctxt, Object
p.skipChildren();
return;
}
ParameterAuthorizationContext.pushPath(prefixForNested(path));
try {
delegate.deserializeAndSet(p, ctxt, instance);
} finally {
ParameterAuthorizationContext.popPath();
}
delegate.deserializeAndSet(p, ctxt, instance);
}

@Override
Expand All @@ -111,24 +104,41 @@ public Object deserializeSetAndReturn(JsonParser p, DeserializationContext ctxt,
p.skipChildren();
return instance;
}
ParameterAuthorizationContext.pushPath(prefixForNested(path));
try {
return delegate.deserializeSetAndReturn(p, ctxt, instance);
} finally {
ParameterAuthorizationContext.popPath();
return delegate.deserializeSetAndReturn(p, ctxt, instance);
}

@Override
public void set(Object instance, Object value) throws IOException {
if (isAuthorizedForSet(instance)) {
delegate.set(instance, value);
}
}

@Override
public Object setAndReturn(Object instance, Object value) throws IOException {
if (isAuthorizedForSet(instance)) {
return delegate.setAndReturn(instance, value);
}
return instance;
}

/**
* For Collection / Map / Array properties, the path to push for nested element members is
* {@code path + "[0]"} — matching {@code ParametersInterceptor} bracket-depth semantics. Scalar /
* bean properties push the path unchanged.
* Guards the already-materialized assignment path: Jackson buffers non-creator properties seen
* before the last creator parameter and assigns them after construction via
* {@code PropertyValue.Regular.assign} -> {@code set()}, which does not go through
* {@link #deserializeAndSet}.
*/
private String prefixForNested(String pathOfThisProperty) {
JavaType type = getType();
if (type != null && (type.isCollectionLikeType() || type.isMapLikeType() || type.isArrayType())) {
return pathOfThisProperty + "[0]";
private boolean isAuthorizedForSet(Object instance) {
if (!ParameterAuthorizationContext.isActive()) {
return true;
}
String path = ParameterAuthorizationContext.pathFor(getName());
if (DynamicKeyAuthorizationContext.isAuthorized(path)) {
return true;
}
return pathOfThisProperty;
LOG.warn("REST body parameter [{}] rejected by @StrutsParameter authorization on [{}]",
path, instance.getClass().getName());
ParameterAuthorizationContext.markRedacted();
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,36 +20,40 @@

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.deser.std.DelegatingDeserializer;
import com.fasterxml.jackson.databind.jsontype.TypeDeserializer;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.interceptor.parameter.ParameterAuthorizationContext;

import java.io.IOException;
import java.util.Collection;
import java.util.Map;

/**
* Enforces {@code @StrutsParameter} authorization for creator-bound properties (Java records,
* {@code @JsonCreator} constructors, {@code @ConstructorProperties}), which Jackson deserializes
* through the value deserializer directly rather than through a {@code SettableBeanProperty}.
* See {@link AuthorizingSettableBeanProperty#withValueDeserializer} for where this is installed.
* Enforces {@code @StrutsParameter} authorization on a property's value deserializer, and owns the
* path push for its nested members. It is installed on every property by
* {@link AuthorizingSettableBeanProperty#withValueDeserializer}, so the same path is computed whether
* Jackson reaches the value through {@code deserializeAndSet}, through the {@code final}
* {@code SettableBeanProperty#deserialize} used for creator parameters and buffered properties, in
* place for a setterless collection, or through a type deserializer for a polymorphic property.
*/
final class AuthorizingValueDeserializer extends DelegatingDeserializer {

private static final Logger LOG = LogManager.getLogger(AuthorizingValueDeserializer.class);

private final String propertyName;
private final JavaType propertyType;

AuthorizingValueDeserializer(JsonDeserializer<?> delegate, String propertyName) {
AuthorizingValueDeserializer(JsonDeserializer<?> delegate, String propertyName, JavaType propertyType) {
super(delegate);
this.propertyName = propertyName;
this.propertyType = propertyType;
}

@Override
protected JsonDeserializer<?> newDelegatingInstance(JsonDeserializer<?> newDelegatee) {
return new AuthorizingValueDeserializer(newDelegatee, propertyName);
return new AuthorizingValueDeserializer(newDelegatee, propertyName, propertyType);
}

@Override
Expand All @@ -58,10 +62,7 @@ public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOEx
return super.deserialize(p, ctxt);
}
String path = ParameterAuthorizationContext.pathFor(propertyName);
if (!DynamicKeyAuthorizationContext.isAuthorized(path)) {
LOG.warn("REST body parameter [{}] rejected by @StrutsParameter authorization (creator-bound property)", path);
ParameterAuthorizationContext.markRedacted();
p.skipChildren();
if (!authorize(path, p)) {
// Returning null redacts the value. For a primitive creator component this becomes the
// type default (0/false) unless FAIL_ON_NULL_FOR_PRIMITIVES is on (then construction
// fails and RedactionAwareDeserializer drops the whole object) -- either way the
Expand All @@ -76,15 +77,59 @@ public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOEx
}
}

@Override
public Object deserialize(JsonParser p, DeserializationContext ctxt, Object intoValue) throws IOException {
if (!ParameterAuthorizationContext.isActive()) {
return super.deserialize(p, ctxt, intoValue);
}
String path = ParameterAuthorizationContext.pathFor(propertyName);
if (!authorize(path, p)) {
return intoValue;
}
ParameterAuthorizationContext.pushPath(prefixForNested(path));
try {
return super.deserialize(p, ctxt, intoValue);
} finally {
ParameterAuthorizationContext.popPath();
}
}

@Override
public Object deserializeWithType(JsonParser p, DeserializationContext ctxt, TypeDeserializer typeDeserializer)
throws IOException {
if (!ParameterAuthorizationContext.isActive()) {
return super.deserializeWithType(p, ctxt, typeDeserializer);
}
String path = ParameterAuthorizationContext.pathFor(propertyName);
if (!authorize(path, p)) {
return null;
}
ParameterAuthorizationContext.pushPath(prefixForNested(path));
try {
return super.deserializeWithType(p, ctxt, typeDeserializer);
} finally {
ParameterAuthorizationContext.popPath();
}
}

private boolean authorize(String path, JsonParser p) throws IOException {
if (DynamicKeyAuthorizationContext.isAuthorized(path)) {
return true;
}
LOG.warn("REST body parameter [{}] rejected by @StrutsParameter authorization", path);
ParameterAuthorizationContext.markRedacted();
p.skipChildren();
return false;
}

/**
* For Collection / Map / Array-valued creator parameters, the path to push for nested element
* members is {@code path + "[0]"} -- matching {@code ParametersInterceptor} bracket-depth
* semantics, and {@link AuthorizingSettableBeanProperty#prefixForNested}. Scalar / bean-valued
* parameters push the path unchanged.
* For Collection / Map / Array properties, the path to push for nested element members is
* {@code path + "[0]"} -- matching {@code ParametersInterceptor} bracket-depth semantics. Scalar /
* bean-valued properties push the path unchanged.
*/
private String prefixForNested(String pathOfThisProperty) {
Class<?> type = handledType();
if (type != null && (Collection.class.isAssignableFrom(type) || Map.class.isAssignableFrom(type) || type.isArray())) {
if (propertyType != null
&& (propertyType.isCollectionLikeType() || propertyType.isMapLikeType() || propertyType.isArrayType())) {
return pathOfThisProperty + "[0]";
}
return pathOfThisProperty;
Expand Down
Loading
Loading