Skip to content

Commit

Permalink
Enforce read-only semantics in SpEL's SimpleEvaluationContext
Browse files Browse the repository at this point in the history
SimpleEvaluationContext.forReadOnlyDataBinding() documents that it
creates a SimpleEvaluationContext for read-only access to public
properties; however, prior to this commit write access was not disabled
for indexed structures when using the assignment operator, the
increment operator, or the decrement operator.

In order to better align with the documented contract for
forReadOnlyDataBinding(), this commit makes it possible to disable
assignment in general in order to enforce read-only semantics for
SpEL's SimpleEvaluationContext when created via the
forReadOnlyDataBinding() factory method. Specifically:

- This commit introduces a new isAssignmentEnabled() "default" method
  in the EvaluationContext API, which returns true by default.

- SimpleEvaluationContext overrides isAssignmentEnabled(), returning
  false if the context was created via the forReadOnlyDataBinding()
  factory method.

- The Assign, OpDec, and OpInc AST nodes -- representing the assignment
  (=), increment (++), and decrement (--) operators, respectively --
  now throw a SpelEvaluationException if assignment is disabled for the
  current EvaluationContext.

See gh-33319
Closes gh-33320

(cherry picked from commit e1ab306)
  • Loading branch information
sbrannen committed Aug 6, 2024
1 parent 7e39078 commit 26f2dad
Show file tree
Hide file tree
Showing 9 changed files with 687 additions and 109 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -132,4 +132,18 @@ default TypedValue assignVariable(String name, Supplier<TypedValue> valueSupplie
@Nullable
Object lookupVariable(String name);

/**
* Determine if assignment is enabled within expressions evaluated by this evaluation
* context.
* <p>If this method returns {@code false}, the assignment ({@code =}), increment
* ({@code ++}), and decrement ({@code --}) operators are disabled.
* <p>By default, this method returns {@code true}. Concrete implementations may override
* this <em>default</em> method to disable assignment.
* @return {@code true} if assignment is enabled; {@code false} otherwise
* @since 5.3.38
*/
default boolean isAssignmentEnabled() {
return true;
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -19,6 +19,8 @@
import org.springframework.expression.EvaluationException;
import org.springframework.expression.TypedValue;
import org.springframework.expression.spel.ExpressionState;
import org.springframework.expression.spel.SpelEvaluationException;
import org.springframework.expression.spel.SpelMessage;

/**
* Represents assignment. An alternative to calling {@code setValue}
Expand All @@ -39,6 +41,9 @@ public Assign(int startPos, int endPos, SpelNodeImpl... operands) {

@Override
public TypedValue getValueInternal(ExpressionState state) throws EvaluationException {
if (!state.getEvaluationContext().isAssignmentEnabled()) {
throw new SpelEvaluationException(getStartPosition(), SpelMessage.NOT_ASSIGNABLE, toStringAST());
}
return this.children[0].setValueInternal(state, () -> this.children[1].getValueInternal(state));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -34,6 +34,7 @@
* @author Andy Clement
* @author Juergen Hoeller
* @author Giovanni Dall'Oglio Risso
* @author Sam Brannen
* @since 3.2
*/
public class OpDec extends Operator {
Expand All @@ -50,6 +51,10 @@ public OpDec(int startPos, int endPos, boolean postfix, SpelNodeImpl... operands

@Override
public TypedValue getValueInternal(ExpressionState state) throws EvaluationException {
if (!state.getEvaluationContext().isAssignmentEnabled()) {
throw new SpelEvaluationException(getStartPosition(), SpelMessage.OPERAND_NOT_DECREMENTABLE, toStringAST());
}

SpelNodeImpl operand = getLeftOperand();

// The operand is going to be read and then assigned to, we don't want to evaluate it twice.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -34,6 +34,7 @@
* @author Andy Clement
* @author Juergen Hoeller
* @author Giovanni Dall'Oglio Risso
* @author Sam Brannen
* @since 3.2
*/
public class OpInc extends Operator {
Expand All @@ -50,6 +51,10 @@ public OpInc(int startPos, int endPos, boolean postfix, SpelNodeImpl... operands

@Override
public TypedValue getValueInternal(ExpressionState state) throws EvaluationException {
if (!state.getEvaluationContext().isAssignmentEnabled()) {
throw new SpelEvaluationException(getStartPosition(), SpelMessage.OPERAND_NOT_INCREMENTABLE, toStringAST());
}

SpelNodeImpl operand = getLeftOperand();
ValueRef valueRef = operand.getValueRef(state);

Expand Down Expand Up @@ -104,7 +109,7 @@ else if (op1 instanceof Byte) {
}
}

// set the name value
// set the new value
try {
valueRef.setValue(newValue.getValue());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -51,25 +51,25 @@
* SpEL language syntax, e.g. excluding references to Java types, constructors,
* and bean references.
*
* <p>When creating a {@code SimpleEvaluationContext} you need to choose the
* level of support that you need for property access in SpEL expressions:
* <p>When creating a {@code SimpleEvaluationContext} you need to choose the level of
* support that you need for data binding in SpEL expressions:
* <ul>
* <li>A custom {@code PropertyAccessor} (typically not reflection-based),
* potentially combined with a {@link DataBindingPropertyAccessor}</li>
* <li>Data binding properties for read-only access</li>
* <li>Data binding properties for read and write</li>
* <li>Data binding for read-only access</li>
* <li>Data binding for read and write access</li>
* <li>A custom {@code PropertyAccessor} (typically not reflection-based), potentially
* combined with a {@link DataBindingPropertyAccessor}</li>
* </ul>
*
* <p>Conveniently, {@link SimpleEvaluationContext#forReadOnlyDataBinding()}
* enables read access to properties via {@link DataBindingPropertyAccessor};
* same for {@link SimpleEvaluationContext#forReadWriteDataBinding()} when
* write access is needed as well. Alternatively, configure custom accessors
* via {@link SimpleEvaluationContext#forPropertyAccessors}, and potentially
* activate method resolution and/or a type converter through the builder.
* <p>Conveniently, {@link SimpleEvaluationContext#forReadOnlyDataBinding()} enables
* read-only access to properties via {@link DataBindingPropertyAccessor}. Similarly,
* {@link SimpleEvaluationContext#forReadWriteDataBinding()} enables read and write access
* to properties. Alternatively, configure custom accessors via
* {@link SimpleEvaluationContext#forPropertyAccessors} and potentially activate method
* resolution and/or a type converter through the builder.
*
* <p>Note that {@code SimpleEvaluationContext} is typically not configured
* with a default root object. Instead it is meant to be created once and
* used repeatedly through {@code getValue} calls on a pre-compiled
* used repeatedly through {@code getValue} calls on a predefined
* {@link org.springframework.expression.Expression} with both an
* {@code EvaluationContext} and a root object as arguments:
* {@link org.springframework.expression.Expression#getValue(EvaluationContext, Object)}.
Expand All @@ -81,9 +81,9 @@
* @author Juergen Hoeller
* @author Sam Brannen
* @since 4.3.15
* @see #forPropertyAccessors
* @see #forReadOnlyDataBinding()
* @see #forReadWriteDataBinding()
* @see #forPropertyAccessors
* @see StandardEvaluationContext
* @see StandardTypeConverter
* @see DataBindingPropertyAccessor
Expand All @@ -109,14 +109,17 @@ public final class SimpleEvaluationContext implements EvaluationContext {

private final Map<String, Object> variables = new HashMap<>();

private final boolean assignmentEnabled;


private SimpleEvaluationContext(List<PropertyAccessor> accessors, List<MethodResolver> resolvers,
@Nullable TypeConverter converter, @Nullable TypedValue rootObject) {
@Nullable TypeConverter converter, @Nullable TypedValue rootObject, boolean assignmentEnabled) {

this.propertyAccessors = accessors;
this.methodResolvers = resolvers;
this.typeConverter = (converter != null ? converter : new StandardTypeConverter());
this.rootObject = (rootObject != null ? rootObject : TypedValue.NULL);
this.assignmentEnabled = assignmentEnabled;
}


Expand Down Expand Up @@ -224,15 +227,33 @@ public Object lookupVariable(String name) {
return this.variables.get(name);
}

/**
* Determine if assignment is enabled within expressions evaluated by this evaluation
* context.
* <p>If this method returns {@code false}, the assignment ({@code =}), increment
* ({@code ++}), and decrement ({@code --}) operators are disabled.
* @return {@code true} if assignment is enabled; {@code false} otherwise
* @since 5.3.38
* @see #forPropertyAccessors(PropertyAccessor...)
* @see #forReadOnlyDataBinding()
* @see #forReadWriteDataBinding()
*/
@Override
public boolean isAssignmentEnabled() {
return this.assignmentEnabled;
}

/**
* Create a {@code SimpleEvaluationContext} for the specified {@link PropertyAccessor}
* delegates: typically a custom {@code PropertyAccessor} specific to a use case
* (e.g. attribute resolution in a custom data structure), potentially combined with
* a {@link DataBindingPropertyAccessor} if property dereferences are needed as well.
* <p>Assignment is enabled within expressions evaluated by the context created via
* this factory method.
* @param accessors the accessor delegates to use
* @see DataBindingPropertyAccessor#forReadOnlyAccess()
* @see DataBindingPropertyAccessor#forReadWriteAccess()
* @see #isAssignmentEnabled()
*/
public static Builder forPropertyAccessors(PropertyAccessor... accessors) {
for (PropertyAccessor accessor : accessors) {
Expand All @@ -241,34 +262,40 @@ public static Builder forPropertyAccessors(PropertyAccessor... accessors) {
"ReflectivePropertyAccessor. Consider using DataBindingPropertyAccessor or a custom subclass.");
}
}
return new Builder(accessors);
return new Builder(true, accessors);
}

/**
* Create a {@code SimpleEvaluationContext} for read-only access to
* public properties via {@link DataBindingPropertyAccessor}.
* <p>Assignment is disabled within expressions evaluated by the context created via
* this factory method.
* @see DataBindingPropertyAccessor#forReadOnlyAccess()
* @see #forPropertyAccessors
* @see #isAssignmentEnabled()
*/
public static Builder forReadOnlyDataBinding() {
return new Builder(DataBindingPropertyAccessor.forReadOnlyAccess());
return new Builder(false, DataBindingPropertyAccessor.forReadOnlyAccess());
}

/**
* Create a {@code SimpleEvaluationContext} for read-write access to
* public properties via {@link DataBindingPropertyAccessor}.
* <p>Assignment is enabled within expressions evaluated by the context created via
* this factory method.
* @see DataBindingPropertyAccessor#forReadWriteAccess()
* @see #forPropertyAccessors
* @see #isAssignmentEnabled()
*/
public static Builder forReadWriteDataBinding() {
return new Builder(DataBindingPropertyAccessor.forReadWriteAccess());
return new Builder(true, DataBindingPropertyAccessor.forReadWriteAccess());
}


/**
* Builder for {@code SimpleEvaluationContext}.
*/
public static class Builder {
public static final class Builder {

private final List<PropertyAccessor> accessors;

Expand All @@ -280,10 +307,15 @@ public static class Builder {
@Nullable
private TypedValue rootObject;

public Builder(PropertyAccessor... accessors) {
private final boolean assignmentEnabled;


private Builder(boolean assignmentEnabled, PropertyAccessor... accessors) {
this.assignmentEnabled = assignmentEnabled;
this.accessors = Arrays.asList(accessors);
}


/**
* Register the specified {@link MethodResolver} delegates for
* a combination of property access and method resolution.
Expand Down Expand Up @@ -315,7 +347,6 @@ public Builder withInstanceMethods() {
return this;
}


/**
* Register a custom {@link ConversionService}.
* <p>By default a {@link StandardTypeConverter} backed by a
Expand All @@ -327,6 +358,7 @@ public Builder withConversionService(ConversionService conversionService) {
this.typeConverter = new StandardTypeConverter(conversionService);
return this;
}

/**
* Register a custom {@link TypeConverter}.
* <p>By default a {@link StandardTypeConverter} backed by a
Expand Down Expand Up @@ -362,7 +394,8 @@ public Builder withTypedRootObject(Object rootObject, TypeDescriptor typeDescrip
}

public SimpleEvaluationContext build() {
return new SimpleEvaluationContext(this.accessors, this.resolvers, this.typeConverter, this.rootObject);
return new SimpleEvaluationContext(this.accessors, this.resolvers, this.typeConverter, this.rootObject,
this.assignmentEnabled);
}
}

Expand Down
Loading

0 comments on commit 26f2dad

Please sign in to comment.