diff --git a/server/src/main/java/org/elasticsearch/ingest/ConditionalProcessor.java b/server/src/main/java/org/elasticsearch/ingest/ConditionalProcessor.java index 9582279bfbe93..fdadc82497a35 100644 --- a/server/src/main/java/org/elasticsearch/ingest/ConditionalProcessor.java +++ b/server/src/main/java/org/elasticsearch/ingest/ConditionalProcessor.java @@ -15,18 +15,8 @@ import org.elasticsearch.script.ScriptService; import org.elasticsearch.script.ScriptType; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; -import java.util.ListIterator; -import java.util.Map; -import java.util.Set; import java.util.function.BiConsumer; import java.util.function.LongSupplier; -import java.util.stream.Collectors; import static org.elasticsearch.ingest.ConfigurationUtils.newConfigurationException; @@ -130,7 +120,7 @@ boolean evaluate(IngestDocument ingestDocument) { if (factory == null) { factory = scriptService.compile(condition, IngestConditionalScript.CONTEXT); } - return factory.newInstance(condition.getParams(), new UnmodifiableIngestData(ingestDocument.getSourceAndMetadata())).execute(); + return factory.newInstance(condition.getParams(), ingestDocument.getUnmodifiableSourceAndMetadata()).execute(); } public Processor getInnerProcessor() { @@ -149,402 +139,4 @@ public String getType() { public String getCondition() { return condition.getIdOrCode(); } - - @SuppressWarnings("unchecked") - private static Object wrapUnmodifiable(Object raw) { - // Wraps all mutable types that the JSON parser can create by immutable wrappers. - // Any inputs not wrapped are assumed to be immutable - if (raw instanceof Map) { - return new UnmodifiableIngestData((Map) raw); - } else if (raw instanceof List) { - return new UnmodifiableIngestList((List) raw); - } else if (raw instanceof Set rawSet) { - return new UnmodifiableIngestSet((Set) rawSet); - } else if (raw instanceof byte[] bytes) { - return bytes.clone(); - } - return raw; - } - - private static UnsupportedOperationException unmodifiableException() { - return new UnsupportedOperationException("Mutating ingest documents in conditionals is not supported"); - } - - private static final class UnmodifiableIngestData implements Map { - - private final Map data; - - UnmodifiableIngestData(Map data) { - this.data = data; - } - - @Override - public int size() { - return data.size(); - } - - @Override - public boolean isEmpty() { - return data.isEmpty(); - } - - @Override - public boolean containsKey(final Object key) { - return data.containsKey(key); - } - - @Override - public boolean containsValue(final Object value) { - return data.containsValue(value); - } - - @Override - public Object get(final Object key) { - return wrapUnmodifiable(data.get(key)); - } - - @Override - public Object put(final String key, final Object value) { - throw unmodifiableException(); - } - - @Override - public Object remove(final Object key) { - throw unmodifiableException(); - } - - @Override - public void putAll(final Map m) { - throw unmodifiableException(); - } - - @Override - public void clear() { - throw unmodifiableException(); - } - - @Override - public Set keySet() { - return Collections.unmodifiableSet(data.keySet()); - } - - @Override - public Collection values() { - return new UnmodifiableIngestList(new ArrayList<>(data.values())); - } - - @Override - public Set> entrySet() { - return data.entrySet().stream().map(entry -> new Entry() { - @Override - public String getKey() { - return entry.getKey(); - } - - @Override - public Object getValue() { - return wrapUnmodifiable(entry.getValue()); - } - - @Override - public Object setValue(final Object value) { - throw unmodifiableException(); - } - - @Override - public boolean equals(final Object o) { - return entry.equals(o); - } - - @Override - public int hashCode() { - return entry.hashCode(); - } - }).collect(Collectors.toSet()); - } - } - - private static final class UnmodifiableIngestList implements List { - - private final List data; - - UnmodifiableIngestList(List data) { - this.data = data; - } - - @Override - public int size() { - return data.size(); - } - - @Override - public boolean isEmpty() { - return data.isEmpty(); - } - - @Override - public boolean contains(final Object o) { - return data.contains(o); - } - - @Override - public Iterator iterator() { - return new UnmodifiableIterator(data.iterator()); - } - - @Override - public Object[] toArray() { - Object[] wrapped = data.toArray(new Object[0]); - for (int i = 0; i < wrapped.length; i++) { - wrapped[i] = wrapUnmodifiable(wrapped[i]); - } - return wrapped; - } - - @Override - @SuppressWarnings("unchecked") - public T[] toArray(final T[] a) { - Object[] raw = data.toArray(new Object[0]); - T[] wrapped = (T[]) Arrays.copyOf(raw, a.length, a.getClass()); - for (int i = 0; i < wrapped.length; i++) { - wrapped[i] = (T) wrapUnmodifiable(wrapped[i]); - } - return wrapped; - } - - @Override - public boolean add(final Object o) { - throw unmodifiableException(); - } - - @Override - public boolean remove(final Object o) { - throw unmodifiableException(); - } - - @Override - public boolean containsAll(final Collection c) { - return data.contains(c); - } - - @Override - public boolean addAll(final Collection c) { - throw unmodifiableException(); - } - - @Override - public boolean addAll(final int index, final Collection c) { - throw unmodifiableException(); - } - - @Override - public boolean removeAll(final Collection c) { - throw unmodifiableException(); - } - - @Override - public boolean retainAll(final Collection c) { - throw unmodifiableException(); - } - - @Override - public void clear() { - throw unmodifiableException(); - } - - @Override - public Object get(final int index) { - return wrapUnmodifiable(data.get(index)); - } - - @Override - public Object set(final int index, final Object element) { - throw unmodifiableException(); - } - - @Override - public void add(final int index, final Object element) { - throw unmodifiableException(); - } - - @Override - public Object remove(final int index) { - throw unmodifiableException(); - } - - @Override - public int indexOf(final Object o) { - return data.indexOf(o); - } - - @Override - public int lastIndexOf(final Object o) { - return data.lastIndexOf(o); - } - - @Override - public ListIterator listIterator() { - return new UnmodifiableListIterator(data.listIterator()); - } - - @Override - public ListIterator listIterator(final int index) { - return new UnmodifiableListIterator(data.listIterator(index)); - } - - @Override - public List subList(final int fromIndex, final int toIndex) { - return new UnmodifiableIngestList(data.subList(fromIndex, toIndex)); - } - - private static final class UnmodifiableListIterator implements ListIterator { - - private final ListIterator data; - - UnmodifiableListIterator(ListIterator data) { - this.data = data; - } - - @Override - public boolean hasNext() { - return data.hasNext(); - } - - @Override - public Object next() { - return wrapUnmodifiable(data.next()); - } - - @Override - public boolean hasPrevious() { - return data.hasPrevious(); - } - - @Override - public Object previous() { - return wrapUnmodifiable(data.previous()); - } - - @Override - public int nextIndex() { - return data.nextIndex(); - } - - @Override - public int previousIndex() { - return data.previousIndex(); - } - - @Override - public void remove() { - throw unmodifiableException(); - } - - @Override - public void set(final Object o) { - throw unmodifiableException(); - } - - @Override - public void add(final Object o) { - throw unmodifiableException(); - } - } - } - - private static final class UnmodifiableIngestSet implements Set { - private final Set data; - - UnmodifiableIngestSet(Set data) { - this.data = data; - } - - @Override - public int size() { - return data.size(); - } - - @Override - public boolean isEmpty() { - return data.isEmpty(); - } - - @Override - public boolean contains(Object o) { - return data.contains(o); - } - - @Override - public Iterator iterator() { - return new UnmodifiableIterator(data.iterator()); - } - - @Override - public Object[] toArray() { - return data.toArray(); - } - - @Override - public T[] toArray(T[] a) { - return data.toArray(a); - } - - @Override - public boolean add(Object o) { - throw unmodifiableException(); - } - - @Override - public boolean remove(Object o) { - throw unmodifiableException(); - } - - @Override - public boolean containsAll(Collection c) { - return data.containsAll(c); - } - - @Override - public boolean addAll(Collection c) { - throw unmodifiableException(); - } - - @Override - public boolean retainAll(Collection c) { - throw unmodifiableException(); - } - - @Override - public boolean removeAll(Collection c) { - throw unmodifiableException(); - } - - @Override - public void clear() { - throw unmodifiableException(); - } - } - - private static final class UnmodifiableIterator implements Iterator { - private final Iterator it; - - UnmodifiableIterator(Iterator it) { - this.it = it; - } - - @Override - public boolean hasNext() { - return it.hasNext(); - } - - @Override - public Object next() { - return wrapUnmodifiable(it.next()); - } - - @Override - public void remove() { - throw unmodifiableException(); - } - } } diff --git a/server/src/main/java/org/elasticsearch/ingest/IngestDocument.java b/server/src/main/java/org/elasticsearch/ingest/IngestDocument.java index f1ff163721004..55c767e80cd0a 100644 --- a/server/src/main/java/org/elasticsearch/ingest/IngestDocument.java +++ b/server/src/main/java/org/elasticsearch/ingest/IngestDocument.java @@ -36,8 +36,10 @@ import java.util.Date; import java.util.Deque; import java.util.HashMap; +import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; +import java.util.ListIterator; import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -1006,6 +1008,18 @@ public Map getSourceAndMetadata() { return ctxMap; } + /* + * This returns the same information as getSourceAndMetadata(), but in an unmodifiable map that is safe to send into a script that is + * not supposed to be modifying the data. If an attempt is made to modify this Map, or a Map, List, or Set nested within it, an + * UnsupportedOperationException is thrown. If an attempt is made to modify a byte[] within this Map, the attempt succeeds, but the + * results are not reflected on this IngestDocument. If a user has put any other mutable Object into the IngestDocument, this method + * makes no attempt to make it immutable. This method just protects users against accidentally modifying the most common types of + * Objects found in IngestDocuments. + */ + public Map getUnmodifiableSourceAndMetadata() { + return new UnmodifiableIngestData(ctxMap); + } + /** * Get the CtxMap */ @@ -1557,4 +1571,407 @@ private static String invalidPath(String fullPath) { return "path [" + fullPath + "] is not valid"; } } + + @SuppressWarnings("unchecked") + private static Object wrapUnmodifiable(Object raw) { + /* + * This method makes an attempt to make the raw Object and its children immutable, if it is one of a known set of classes. If raw + * is a Map, List, or Set, an immutable version will be returned, and an UnsupportedOperationException will be thrown if an attempt + * to modify it is made. All the Objects in those collections are also made unmodifiable by this method. If raw is a byte[], a copy + * of the byte[] will be returned so that changes to it will not be reflected in the original data. No exception will be thrown if + * a user modifies it though. + */ + if (raw instanceof Map rawMap) { + return new UnmodifiableIngestData((Map) rawMap); + } else if (raw instanceof List) { + return new UnmodifiableIngestList((List) raw); + } else if (raw instanceof Set rawSet) { + return new UnmodifiableIngestSet((Set) rawSet); + } else if (raw instanceof byte[] bytes) { + return bytes.clone(); + } + return raw; + } + + private static UnsupportedOperationException unmodifiableException() { + return new UnsupportedOperationException("Mutating ingest documents in conditionals is not supported"); + } + + private static final class UnmodifiableIngestData implements Map { + + private final Map data; + + UnmodifiableIngestData(Map data) { + this.data = data; + } + + @Override + public int size() { + return data.size(); + } + + @Override + public boolean isEmpty() { + return data.isEmpty(); + } + + @Override + public boolean containsKey(final Object key) { + return data.containsKey(key); + } + + @Override + public boolean containsValue(final Object value) { + return data.containsValue(value); + } + + @Override + public Object get(final Object key) { + return wrapUnmodifiable(data.get(key)); + } + + @Override + public Object put(final String key, final Object value) { + throw unmodifiableException(); + } + + @Override + public Object remove(final Object key) { + throw unmodifiableException(); + } + + @Override + public void putAll(final Map m) { + throw unmodifiableException(); + } + + @Override + public void clear() { + throw unmodifiableException(); + } + + @Override + public Set keySet() { + return Collections.unmodifiableSet(data.keySet()); + } + + @Override + public Collection values() { + return new UnmodifiableIngestList(new ArrayList<>(data.values())); + } + + @Override + public Set> entrySet() { + return data.entrySet().stream().map(entry -> new Entry() { + @Override + public String getKey() { + return entry.getKey(); + } + + @Override + public Object getValue() { + return wrapUnmodifiable(entry.getValue()); + } + + @Override + public Object setValue(final Object value) { + throw unmodifiableException(); + } + + @Override + public boolean equals(final Object o) { + return entry.equals(o); + } + + @Override + public int hashCode() { + return entry.hashCode(); + } + }).collect(Collectors.toSet()); + } + } + + private static final class UnmodifiableIngestList implements List { + + private final List data; + + UnmodifiableIngestList(List data) { + this.data = data; + } + + @Override + public int size() { + return data.size(); + } + + @Override + public boolean isEmpty() { + return data.isEmpty(); + } + + @Override + public boolean contains(final Object o) { + return data.contains(o); + } + + @Override + public Iterator iterator() { + return new UnmodifiableIterator(data.iterator()); + } + + @Override + public Object[] toArray() { + Object[] wrapped = data.toArray(new Object[0]); + for (int i = 0; i < wrapped.length; i++) { + wrapped[i] = wrapUnmodifiable(wrapped[i]); + } + return wrapped; + } + + @Override + @SuppressWarnings("unchecked") + public T[] toArray(final T[] a) { + Object[] raw = data.toArray(new Object[0]); + T[] wrapped = (T[]) Arrays.copyOf(raw, a.length, a.getClass()); + for (int i = 0; i < wrapped.length; i++) { + wrapped[i] = (T) wrapUnmodifiable(wrapped[i]); + } + return wrapped; + } + + @Override + public boolean add(final Object o) { + throw unmodifiableException(); + } + + @Override + public boolean remove(final Object o) { + throw unmodifiableException(); + } + + @Override + public boolean containsAll(final Collection c) { + return data.contains(c); + } + + @Override + public boolean addAll(final Collection c) { + throw unmodifiableException(); + } + + @Override + public boolean addAll(final int index, final Collection c) { + throw unmodifiableException(); + } + + @Override + public boolean removeAll(final Collection c) { + throw unmodifiableException(); + } + + @Override + public boolean retainAll(final Collection c) { + throw unmodifiableException(); + } + + @Override + public void clear() { + throw unmodifiableException(); + } + + @Override + public Object get(final int index) { + return wrapUnmodifiable(data.get(index)); + } + + @Override + public Object set(final int index, final Object element) { + throw unmodifiableException(); + } + + @Override + public void add(final int index, final Object element) { + throw unmodifiableException(); + } + + @Override + public Object remove(final int index) { + throw unmodifiableException(); + } + + @Override + public int indexOf(final Object o) { + return data.indexOf(o); + } + + @Override + public int lastIndexOf(final Object o) { + return data.lastIndexOf(o); + } + + @Override + public ListIterator listIterator() { + return new UnmodifiableListIterator(data.listIterator()); + } + + @Override + public ListIterator listIterator(final int index) { + return new UnmodifiableListIterator(data.listIterator(index)); + } + + @Override + public List subList(final int fromIndex, final int toIndex) { + return new UnmodifiableIngestList(data.subList(fromIndex, toIndex)); + } + + private static final class UnmodifiableListIterator implements ListIterator { + + private final ListIterator data; + + UnmodifiableListIterator(ListIterator data) { + this.data = data; + } + + @Override + public boolean hasNext() { + return data.hasNext(); + } + + @Override + public Object next() { + return wrapUnmodifiable(data.next()); + } + + @Override + public boolean hasPrevious() { + return data.hasPrevious(); + } + + @Override + public Object previous() { + return wrapUnmodifiable(data.previous()); + } + + @Override + public int nextIndex() { + return data.nextIndex(); + } + + @Override + public int previousIndex() { + return data.previousIndex(); + } + + @Override + public void remove() { + throw unmodifiableException(); + } + + @Override + public void set(final Object o) { + throw unmodifiableException(); + } + + @Override + public void add(final Object o) { + throw unmodifiableException(); + } + } + } + + private static final class UnmodifiableIngestSet implements Set { + private final Set data; + + UnmodifiableIngestSet(Set data) { + this.data = data; + } + + @Override + public int size() { + return data.size(); + } + + @Override + public boolean isEmpty() { + return data.isEmpty(); + } + + @Override + public boolean contains(Object o) { + return data.contains(o); + } + + @Override + public Iterator iterator() { + return new UnmodifiableIterator(data.iterator()); + } + + @Override + public Object[] toArray() { + return data.toArray(); + } + + @Override + public T[] toArray(T[] a) { + return data.toArray(a); + } + + @Override + public boolean add(Object o) { + throw unmodifiableException(); + } + + @Override + public boolean remove(Object o) { + throw unmodifiableException(); + } + + @Override + public boolean containsAll(Collection c) { + return data.containsAll(c); + } + + @Override + public boolean addAll(Collection c) { + throw unmodifiableException(); + } + + @Override + public boolean retainAll(Collection c) { + throw unmodifiableException(); + } + + @Override + public boolean removeAll(Collection c) { + throw unmodifiableException(); + } + + @Override + public void clear() { + throw unmodifiableException(); + } + } + + private static final class UnmodifiableIterator implements Iterator { + private final Iterator it; + + UnmodifiableIterator(Iterator it) { + this.it = it; + } + + @Override + public boolean hasNext() { + return it.hasNext(); + } + + @Override + public Object next() { + return wrapUnmodifiable(it.next()); + } + + @Override + public void remove() { + throw unmodifiableException(); + } + } } diff --git a/server/src/test/java/org/elasticsearch/ingest/IngestDocumentTests.java b/server/src/test/java/org/elasticsearch/ingest/IngestDocumentTests.java index f8b6e85bd08f7..95c0fc5b72c24 100644 --- a/server/src/test/java/org/elasticsearch/ingest/IngestDocumentTests.java +++ b/server/src/test/java/org/elasticsearch/ingest/IngestDocumentTests.java @@ -23,6 +23,7 @@ import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -2162,4 +2163,47 @@ void doTestNestedAccessPatternPropagation(int level, int maxCallDepth, IngestDoc } logger.debug("LEVEL {}/{}: COMPLETE", level, maxCallDepth); } + + @SuppressWarnings("unchecked") + public void testGetUnmodifiableSourceAndMetadata() { + assertMutatingThrows(ctx -> ctx.remove("foo")); + assertMutatingThrows(ctx -> ctx.put("foo", "bar")); + assertMutatingThrows(ctx -> ((List) ctx.get("listField")).add("bar")); + assertMutatingThrows(ctx -> ((List) ctx.get("listField")).remove("bar")); + assertMutatingThrows(ctx -> ((Set) ctx.get("setField")).add("bar")); + assertMutatingThrows(ctx -> ((Set) ctx.get("setField")).remove("bar")); + assertMutatingThrows(ctx -> ((Map) ctx.get("mapField")).put("bar", "baz")); + assertMutatingThrows(ctx -> ((Map) ctx.get("mapField")).remove("bar")); + assertMutatingThrows(ctx -> ((List) ((Set) ctx.get("setField")).iterator().next()).add("bar")); + assertMutatingThrows( + ctx -> ((List) ((List) ((Set) ctx.get("setField")).iterator().next()).iterator().next()).add("bar") + ); + + /* + * The source can also have a byte array. But we do not throw an UnsupportedOperationException when a byte array is changed -- + * we just ignore the change. + */ + Map document = new HashMap<>(); + IngestDocument ingestDocument = RandomDocumentPicks.randomIngestDocument(random(), document); + ingestDocument.setFieldValue("byteArrayField", randomByteArrayOfLength(10)); + Map unmodifiableDocument = ingestDocument.getUnmodifiableSourceAndMetadata(); + byte originalByteValue = ((byte[]) unmodifiableDocument.get("byteArrayField"))[0]; + ((byte[]) unmodifiableDocument.get("byteArrayField"))[0] = (byte) (originalByteValue + 1); + assertThat(((byte[]) unmodifiableDocument.get("byteArrayField"))[0], equalTo(originalByteValue)); + } + + @SuppressWarnings("unchecked") + public void assertMutatingThrows(Consumer> mutation) { + Map document = new HashMap<>(); + IngestDocument ingestDocument = RandomDocumentPicks.randomIngestDocument(random(), document); + ingestDocument.setFieldValue("listField", new ArrayList<>()); + ingestDocument.setFieldValue("mapField", new HashMap<>()); + ingestDocument.setFieldValue("setField", new HashSet<>()); + List listWithinSet = new ArrayList<>(); + listWithinSet.add(new ArrayList<>()); + ingestDocument.getFieldValue("setField", Set.class).add(listWithinSet); + Map unmodifiableDocument = ingestDocument.getUnmodifiableSourceAndMetadata(); + assertThrows(UnsupportedOperationException.class, () -> mutation.accept(unmodifiableDocument)); + mutation.accept(ingestDocument.getSourceAndMetadata()); // no exception expected + } }