From b35ad804df29e4ade9e1e8f0f89b876eb69ae338 Mon Sep 17 00:00:00 2001 From: David Pilato Date: Tue, 20 Aug 2013 18:31:06 +0200 Subject: [PATCH] Ignore encrypted documents Original request: I am sending multiple pdf, word etc. attachments in one documents to be indexed. Some of them (pdf) are encrypted and I am getting a MapperParsingException caused by org.apache.tika.exception.TikaException: Unable to extract PDF content cause by org.apache.pdfbox.exceptions.WrappedIOException: Error decrypting document. I was wondering if the attachment mapper could expose some switch to ignore the documents it can not extract? As we now have option `ignore_errors`, we can support it. See #38 relative to this option. Closes #18. --- .../mapper/attachment/AttachmentMapper.java | 4 +- .../xcontent/EncryptedDocMapperTest.java | 130 ++++++++++++++++++ .../MultipleAttachmentIntegrationTests.java | 125 +++++++++++++++++ .../mapper/multipledocs/test-mapping.json | 12 ++ .../index/mapper/xcontent/encrypted.pdf | Bin 0 -> 14682 bytes 5 files changed, 270 insertions(+), 1 deletion(-) create mode 100644 src/test/java/org/elasticsearch/index/mapper/xcontent/EncryptedDocMapperTest.java create mode 100644 src/test/java/org/elasticsearch/plugin/mapper/attachments/test/MultipleAttachmentIntegrationTests.java create mode 100644 src/test/resources/org/elasticsearch/index/mapper/multipledocs/test-mapping.json create mode 100644 src/test/resources/org/elasticsearch/index/mapper/xcontent/encrypted.pdf diff --git a/src/main/java/org/elasticsearch/index/mapper/attachment/AttachmentMapper.java b/src/main/java/org/elasticsearch/index/mapper/attachment/AttachmentMapper.java index 304cd4edc63..ecf17e0ce15 100644 --- a/src/main/java/org/elasticsearch/index/mapper/attachment/AttachmentMapper.java +++ b/src/main/java/org/elasticsearch/index/mapper/attachment/AttachmentMapper.java @@ -356,7 +356,9 @@ public class AttachmentMapper implements Mapper { // Set the maximum length of strings returned by the parseToString method, -1 sets no limit parsedContent = tika().parseToString(new BytesStreamInput(content, false), metadata, indexedChars); } catch (Throwable e) { - throw new MapperParsingException("Failed to extract [" + indexedChars + "] characters of text for [" + name + "]", e); + // #18: we could ignore errors when Tika does not parse data + if (!ignoreErrors) throw new MapperParsingException("Failed to extract [" + indexedChars + "] characters of text for [" + name + "]", e); + return; } context.externalValue(parsedContent); diff --git a/src/test/java/org/elasticsearch/index/mapper/xcontent/EncryptedDocMapperTest.java b/src/test/java/org/elasticsearch/index/mapper/xcontent/EncryptedDocMapperTest.java new file mode 100644 index 00000000000..7b7679f6ba4 --- /dev/null +++ b/src/test/java/org/elasticsearch/index/mapper/xcontent/EncryptedDocMapperTest.java @@ -0,0 +1,130 @@ +package org.elasticsearch.index.mapper.xcontent; + +import org.apache.lucene.document.Document; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.settings.ImmutableSettings; +import org.elasticsearch.index.Index; +import org.elasticsearch.index.analysis.AnalysisService; +import org.elasticsearch.index.mapper.DocumentMapper; +import org.elasticsearch.index.mapper.DocumentMapperParser; +import org.elasticsearch.index.mapper.MapperParsingException; +import org.elasticsearch.index.mapper.attachment.AttachmentMapper; +import org.testng.annotations.Test; + +import java.io.IOException; + +import static org.elasticsearch.common.io.Streams.copyToBytesFromClasspath; +import static org.elasticsearch.common.io.Streams.copyToStringFromClasspath; +import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +/** + * Test for https://github.com/elasticsearch/elasticsearch-mapper-attachments/issues/18 + * Note that we have converted /org/elasticsearch/index/mapper/xcontent/testContentLength.txt + * to a /org/elasticsearch/index/mapper/xcontent/encrypted.pdf with password `12345678`. + */ +public class EncryptedDocMapperTest { + + @Test + public void testMultipleDocsEncryptedLast() throws IOException { + DocumentMapperParser mapperParser = new DocumentMapperParser(new Index("test"), new AnalysisService(new Index("test")), null, null); + mapperParser.putTypeParser(AttachmentMapper.CONTENT_TYPE, new AttachmentMapper.TypeParser()); + + String mapping = copyToStringFromClasspath("/org/elasticsearch/index/mapper/multipledocs/test-mapping.json"); + DocumentMapper docMapper = mapperParser.parse(mapping); + byte[] html = copyToBytesFromClasspath("/org/elasticsearch/index/mapper/xcontent/htmlWithValidDateMeta.html"); + byte[] pdf = copyToBytesFromClasspath("/org/elasticsearch/index/mapper/xcontent/encrypted.pdf"); + + BytesReference json = jsonBuilder() + .startObject() + .field("_id", 1) + .field("file1", html) + .field("file2", pdf) + .endObject().bytes(); + + Document doc = docMapper.parse(json).rootDoc(); + assertThat(doc.get(docMapper.mappers().smartName("file1").mapper().names().indexName()), containsString("World")); + assertThat(doc.get(docMapper.mappers().smartName("file1.title").mapper().names().indexName()), equalTo("Hello")); + assertThat(doc.get(docMapper.mappers().smartName("file1.author").mapper().names().indexName()), equalTo("kimchy")); + assertThat(doc.get(docMapper.mappers().smartName("file1.keywords").mapper().names().indexName()), equalTo("elasticsearch,cool,bonsai")); + assertThat(doc.get(docMapper.mappers().smartName("file1.content_type").mapper().names().indexName()), equalTo("text/html; charset=ISO-8859-1")); + assertThat(doc.getField(docMapper.mappers().smartName("file1.content_length").mapper().names().indexName()).numericValue().longValue(), is(344L)); + + assertThat(doc.get(docMapper.mappers().smartName("file2").mapper().names().indexName()), nullValue()); + assertThat(doc.get(docMapper.mappers().smartName("file2.title").mapper().names().indexName()), nullValue()); + assertThat(doc.get(docMapper.mappers().smartName("file2.author").mapper().names().indexName()), nullValue()); + assertThat(doc.get(docMapper.mappers().smartName("file2.keywords").mapper().names().indexName()), nullValue()); + assertThat(doc.get(docMapper.mappers().smartName("file2.content_type").mapper().names().indexName()), nullValue()); + assertThat(doc.getField(docMapper.mappers().smartName("file2.content_length").mapper().names().indexName()), nullValue()); + } + + @Test + public void testMultipleDocsEncryptedFirst() throws IOException { + DocumentMapperParser mapperParser = new DocumentMapperParser(new Index("test"), new AnalysisService(new Index("test")), null, null); + mapperParser.putTypeParser(AttachmentMapper.CONTENT_TYPE, new AttachmentMapper.TypeParser()); + + String mapping = copyToStringFromClasspath("/org/elasticsearch/index/mapper/multipledocs/test-mapping.json"); + DocumentMapper docMapper = mapperParser.parse(mapping); + byte[] html = copyToBytesFromClasspath("/org/elasticsearch/index/mapper/xcontent/htmlWithValidDateMeta.html"); + byte[] pdf = copyToBytesFromClasspath("/org/elasticsearch/index/mapper/xcontent/encrypted.pdf"); + + BytesReference json = jsonBuilder() + .startObject() + .field("_id", 1) + .field("file1", pdf) + .field("file2", html) + .endObject().bytes(); + + Document doc = docMapper.parse(json).rootDoc(); + assertThat(doc.get(docMapper.mappers().smartName("file1").mapper().names().indexName()), nullValue()); + assertThat(doc.get(docMapper.mappers().smartName("file1.title").mapper().names().indexName()), nullValue()); + assertThat(doc.get(docMapper.mappers().smartName("file1.author").mapper().names().indexName()), nullValue()); + assertThat(doc.get(docMapper.mappers().smartName("file1.keywords").mapper().names().indexName()), nullValue()); + assertThat(doc.get(docMapper.mappers().smartName("file1.content_type").mapper().names().indexName()), nullValue()); + assertThat(doc.getField(docMapper.mappers().smartName("file1.content_length").mapper().names().indexName()), nullValue()); + + assertThat(doc.get(docMapper.mappers().smartName("file2").mapper().names().indexName()), containsString("World")); + assertThat(doc.get(docMapper.mappers().smartName("file2.title").mapper().names().indexName()), equalTo("Hello")); + assertThat(doc.get(docMapper.mappers().smartName("file2.author").mapper().names().indexName()), equalTo("kimchy")); + assertThat(doc.get(docMapper.mappers().smartName("file2.keywords").mapper().names().indexName()), equalTo("elasticsearch,cool,bonsai")); + assertThat(doc.get(docMapper.mappers().smartName("file2.content_type").mapper().names().indexName()), equalTo("text/html; charset=ISO-8859-1")); + assertThat(doc.getField(docMapper.mappers().smartName("file2.content_length").mapper().names().indexName()).numericValue().longValue(), is(344L)); + } + + @Test(expectedExceptions = MapperParsingException.class) + public void testMultipleDocsEncryptedNotIgnoringErrors() throws IOException { + DocumentMapperParser mapperParser = new DocumentMapperParser(new Index("test"), + ImmutableSettings.builder().put("index.mapping.attachment.ignore_errors", false).build(), + new AnalysisService(new Index("test")), null, null); + mapperParser.putTypeParser(AttachmentMapper.CONTENT_TYPE, new AttachmentMapper.TypeParser()); + + String mapping = copyToStringFromClasspath("/org/elasticsearch/index/mapper/multipledocs/test-mapping.json"); + DocumentMapper docMapper = mapperParser.parse(mapping); + byte[] html = copyToBytesFromClasspath("/org/elasticsearch/index/mapper/xcontent/htmlWithValidDateMeta.html"); + byte[] pdf = copyToBytesFromClasspath("/org/elasticsearch/index/mapper/xcontent/encrypted.pdf"); + + BytesReference json = jsonBuilder() + .startObject() + .field("_id", 1) + .field("file1", pdf) + .field("file2", html) + .endObject().bytes(); + + Document doc = docMapper.parse(json).rootDoc(); + assertThat(doc.get(docMapper.mappers().smartName("file1").mapper().names().indexName()), nullValue()); + assertThat(doc.get(docMapper.mappers().smartName("file1.title").mapper().names().indexName()), nullValue()); + assertThat(doc.get(docMapper.mappers().smartName("file1.author").mapper().names().indexName()), nullValue()); + assertThat(doc.get(docMapper.mappers().smartName("file1.keywords").mapper().names().indexName()), nullValue()); + assertThat(doc.get(docMapper.mappers().smartName("file1.content_type").mapper().names().indexName()), nullValue()); + assertThat(doc.getField(docMapper.mappers().smartName("file1.content_length").mapper().names().indexName()), nullValue()); + + assertThat(doc.get(docMapper.mappers().smartName("file2").mapper().names().indexName()), containsString("World")); + assertThat(doc.get(docMapper.mappers().smartName("file2.title").mapper().names().indexName()), equalTo("Hello")); + assertThat(doc.get(docMapper.mappers().smartName("file2.author").mapper().names().indexName()), equalTo("kimchy")); + assertThat(doc.get(docMapper.mappers().smartName("file2.keywords").mapper().names().indexName()), equalTo("elasticsearch,cool,bonsai")); + assertThat(doc.get(docMapper.mappers().smartName("file2.content_type").mapper().names().indexName()), equalTo("text/html; charset=ISO-8859-1")); + assertThat(doc.getField(docMapper.mappers().smartName("file2.content_length").mapper().names().indexName()).numericValue().longValue(), is(344L)); + } + +} diff --git a/src/test/java/org/elasticsearch/plugin/mapper/attachments/test/MultipleAttachmentIntegrationTests.java b/src/test/java/org/elasticsearch/plugin/mapper/attachments/test/MultipleAttachmentIntegrationTests.java new file mode 100644 index 00000000000..3dc94afb7c3 --- /dev/null +++ b/src/test/java/org/elasticsearch/plugin/mapper/attachments/test/MultipleAttachmentIntegrationTests.java @@ -0,0 +1,125 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.plugin.mapper.attachments.test; + +import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse; +import org.elasticsearch.action.admin.cluster.health.ClusterHealthStatus; +import org.elasticsearch.action.count.CountResponse; +import org.elasticsearch.common.logging.ESLogger; +import org.elasticsearch.common.logging.Loggers; +import org.elasticsearch.common.network.NetworkUtils; +import org.elasticsearch.common.settings.ImmutableSettings; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.index.mapper.MapperParsingException; +import org.elasticsearch.node.Node; +import org.testng.annotations.AfterClass; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import static org.elasticsearch.client.Requests.*; +import static org.elasticsearch.common.io.Streams.copyToBytesFromClasspath; +import static org.elasticsearch.common.io.Streams.copyToStringFromClasspath; +import static org.elasticsearch.common.settings.ImmutableSettings.settingsBuilder; +import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; +import static org.elasticsearch.index.query.QueryBuilders.fieldQuery; +import static org.elasticsearch.node.NodeBuilder.nodeBuilder; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; + +/** + * Test case for issue https://github.com/elasticsearch/elasticsearch-mapper-attachments/issues/18 + */ +@Test +public class MultipleAttachmentIntegrationTests { + + private final ESLogger logger = Loggers.getLogger(getClass()); + + private Node node; + + @BeforeClass + public void setupServer() { + node = nodeBuilder().local(true).settings(settingsBuilder() + .put("path.data", "target/data") + .put("cluster.name", "test-cluster-" + NetworkUtils.getLocalAddress()) + .put("gateway.type", "none")).node(); + } + + @AfterClass + public void closeServer() { + node.close(); + } + + private void createIndex(Settings settings) { + logger.info("creating index [test]"); + node.client().admin().indices().create(createIndexRequest("test").settings(settingsBuilder().put("index.numberOfReplicas", 0).put(settings))).actionGet(); + logger.info("Running Cluster Health"); + ClusterHealthResponse clusterHealth = node.client().admin().cluster().health(clusterHealthRequest().waitForGreenStatus()).actionGet(); + logger.info("Done Cluster Health, status " + clusterHealth.getStatus()); + assertThat(clusterHealth.isTimedOut(), equalTo(false)); + assertThat(clusterHealth.getStatus(), equalTo(ClusterHealthStatus.GREEN)); + } + + @AfterMethod + public void deleteIndex() { + logger.info("deleting index [test]"); + node.client().admin().indices().delete(deleteIndexRequest("test")).actionGet(); + } + + /** + * When we want to ignore errors (default) + */ + @Test + public void testMultipleAttachmentsWithEncryptedDoc() throws Exception { + createIndex(ImmutableSettings.builder().build()); + String mapping = copyToStringFromClasspath("/org/elasticsearch/index/mapper/multipledocs/test-mapping.json"); + byte[] html = copyToBytesFromClasspath("/org/elasticsearch/index/mapper/xcontent/htmlWithValidDateMeta.html"); + byte[] pdf = copyToBytesFromClasspath("/org/elasticsearch/index/mapper/xcontent/encrypted.pdf"); + + node.client().admin().indices().putMapping(putMappingRequest("test").type("person").source(mapping)).actionGet(); + + node.client().index(indexRequest("test").type("person") + .source(jsonBuilder().startObject().field("file1", html).field("file2", pdf).field("hello","world").endObject())).actionGet(); + node.client().admin().indices().refresh(refreshRequest()).actionGet(); + + CountResponse countResponse = node.client().count(countRequest("test").query(fieldQuery("file1", "World"))).actionGet(); + assertThat(countResponse.getCount(), equalTo(1l)); + + countResponse = node.client().count(countRequest("test").query(fieldQuery("hello", "World"))).actionGet(); + assertThat(countResponse.getCount(), equalTo(1l)); + } + + /** + * When we don't want to ignore errors + */ + @Test(expectedExceptions = MapperParsingException.class) + public void testMultipleAttachmentsWithEncryptedDocNotIgnoringErrors() throws Exception { + createIndex(ImmutableSettings.builder().put("index.mapping.attachment.ignore_errors", false).build()); + String mapping = copyToStringFromClasspath("/org/elasticsearch/index/mapper/multipledocs/test-mapping.json"); + byte[] html = copyToBytesFromClasspath("/org/elasticsearch/index/mapper/xcontent/htmlWithValidDateMeta.html"); + byte[] pdf = copyToBytesFromClasspath("/org/elasticsearch/index/mapper/xcontent/encrypted.pdf"); + + node.client().admin().indices() + .putMapping(putMappingRequest("test").type("person").source(mapping)).actionGet(); + + node.client().index(indexRequest("test").type("person") + .source(jsonBuilder().startObject().field("file1", html).field("file2", pdf).field("hello","world").endObject())).actionGet(); + } +} diff --git a/src/test/resources/org/elasticsearch/index/mapper/multipledocs/test-mapping.json b/src/test/resources/org/elasticsearch/index/mapper/multipledocs/test-mapping.json new file mode 100644 index 00000000000..7dc796c2b17 --- /dev/null +++ b/src/test/resources/org/elasticsearch/index/mapper/multipledocs/test-mapping.json @@ -0,0 +1,12 @@ +{ + "person":{ + "properties":{ + "file1":{ + "type":"attachment" + }, + "file2":{ + "type":"attachment" + } + } + } +} diff --git a/src/test/resources/org/elasticsearch/index/mapper/xcontent/encrypted.pdf b/src/test/resources/org/elasticsearch/index/mapper/xcontent/encrypted.pdf new file mode 100644 index 0000000000000000000000000000000000000000..569a904a3157476d6134c8f39d2081b09c6873af GIT binary patch literal 14682 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

0Va3>) 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 literal 0 HcmV?d00001