Watcher add email warning if CSV attachment contains formulas (#44460) (#45557)

* Watcher add email warning if CSV attachment contains formulas (#44460)

This commit introduces a Warning message to the emails generated by 
Watcher's reporting action. This change complements Kibana's CSV 
formula notifications (see elastic/kibana#37930). 

This is implemented by reading a header (kbn-csv-contains-formulas) 
provided by Kibana to notify to attach the Warning to the email. 
The wording of the warning is borrowed from Kibana's UI and may 
be overridden by a dynamic setting
xpack.notification.reporting.warning.kbn-csv-contains-formulas.text.
This warning is enabled by default, but may be disabled via a 
dynamic setting xpack.notification.reporting.warning.enabled.
This commit is contained in:
Jake Landis 2019-08-26 08:35:33 -05:00 committed by GitHub
parent f2241a152f
commit 767f648f8e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 395 additions and 32 deletions

View File

@ -41,7 +41,7 @@ dependencies {
testCompile 'org.subethamail:subethasmtp:3.1.7' testCompile 'org.subethamail:subethasmtp:3.1.7'
// needed for subethasmtp, has @GuardedBy annotation // needed for subethasmtp, has @GuardedBy annotation
testCompile 'com.google.code.findbugs:jsr305:3.0.1' testCompile 'com.google.code.findbugs:jsr305:3.0.2'
} }
// classes are missing, e.g. com.ibm.icu.lang.UCharacter // classes are missing, e.g. com.ibm.icu.lang.UCharacter

View File

@ -288,7 +288,8 @@ public class Watcher extends Plugin implements ActionPlugin, ScriptPlugin, Reloa
Map<String, EmailAttachmentParser> emailAttachmentParsers = new HashMap<>(); Map<String, EmailAttachmentParser> emailAttachmentParsers = new HashMap<>();
emailAttachmentParsers.put(HttpEmailAttachementParser.TYPE, new HttpEmailAttachementParser(httpClient, templateEngine)); emailAttachmentParsers.put(HttpEmailAttachementParser.TYPE, new HttpEmailAttachementParser(httpClient, templateEngine));
emailAttachmentParsers.put(DataAttachmentParser.TYPE, new DataAttachmentParser()); emailAttachmentParsers.put(DataAttachmentParser.TYPE, new DataAttachmentParser());
emailAttachmentParsers.put(ReportingAttachmentParser.TYPE, new ReportingAttachmentParser(settings, httpClient, templateEngine)); emailAttachmentParsers.put(ReportingAttachmentParser.TYPE,
new ReportingAttachmentParser(settings, httpClient, templateEngine, clusterService.getClusterSettings()));
EmailAttachmentsParser emailAttachmentsParser = new EmailAttachmentsParser(emailAttachmentParsers); EmailAttachmentsParser emailAttachmentsParser = new EmailAttachmentsParser(emailAttachmentParsers);
// conditions // conditions
@ -487,8 +488,7 @@ public class Watcher extends Plugin implements ActionPlugin, ScriptPlugin, Reloa
settings.addAll(HtmlSanitizer.getSettings()); settings.addAll(HtmlSanitizer.getSettings());
settings.addAll(JiraService.getSettings()); settings.addAll(JiraService.getSettings());
settings.addAll(PagerDutyService.getSettings()); settings.addAll(PagerDutyService.getSettings());
settings.add(ReportingAttachmentParser.RETRIES_SETTING); settings.addAll(ReportingAttachmentParser.getSettings());
settings.add(ReportingAttachmentParser.INTERVAL_SETTING);
// http settings // http settings
settings.addAll(HttpSettings.getSettings()); settings.addAll(HttpSettings.getSettings());

View File

@ -24,6 +24,8 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Collections;
import java.util.Set;
import static javax.mail.Part.ATTACHMENT; import static javax.mail.Part.ATTACHMENT;
import static javax.mail.Part.INLINE; import static javax.mail.Part.INLINE;
@ -31,10 +33,17 @@ import static javax.mail.Part.INLINE;
public abstract class Attachment extends BodyPartSource { public abstract class Attachment extends BodyPartSource {
private final boolean inline; private final boolean inline;
private final Set<String> warnings;
protected Attachment(String id, String name, String contentType, boolean inline) { protected Attachment(String id, String name, String contentType, boolean inline) {
this(id, name, contentType, inline, Collections.emptySet());
}
protected Attachment(String id, String name, String contentType, boolean inline, Set<String> warnings) {
super(id, name, contentType); super(id, name, contentType);
this.inline = inline; this.inline = inline;
assert warnings != null;
this.warnings = warnings;
} }
@Override @Override
@ -53,6 +62,10 @@ public abstract class Attachment extends BodyPartSource {
return inline; return inline;
} }
public Set<String> getWarnings() {
return warnings;
}
/** /**
* intentionally not emitting path as it may come as an information leak * intentionally not emitting path as it may come as an information leak
*/ */
@ -116,15 +129,15 @@ public abstract class Attachment extends BodyPartSource {
private final byte[] bytes; private final byte[] bytes;
public Bytes(String id, byte[] bytes, String contentType, boolean inline) { public Bytes(String id, byte[] bytes, String contentType, boolean inline) {
this(id, id, bytes, contentType, inline); this(id, id, bytes, contentType, inline, Collections.emptySet());
} }
public Bytes(String id, String name, byte[] bytes, boolean inline) { public Bytes(String id, String name, byte[] bytes, boolean inline) {
this(id, name, bytes, fileTypeMap.getContentType(name), inline); this(id, name, bytes, fileTypeMap.getContentType(name), inline, Collections.emptySet());
} }
public Bytes(String id, String name, byte[] bytes, String contentType, boolean inline) { public Bytes(String id, String name, byte[] bytes, String contentType, boolean inline, Set<String> warnings) {
super(id, name, contentType, inline); super(id, name, contentType, inline, warnings);
this.bytes = bytes; this.bytes = bytes;
} }
@ -213,7 +226,7 @@ public abstract class Attachment extends BodyPartSource {
} }
protected XContent(String id, String name, ToXContent content, XContentType type) { protected XContent(String id, String name, ToXContent content, XContentType type) {
super(id, name, bytes(name, content, type), mimeType(type), false); super(id, name, bytes(name, content, type), mimeType(type), false, Collections.emptySet());
} }
static String mimeType(XContentType type) { static String mimeType(XContentType type) {

View File

@ -6,6 +6,7 @@
package org.elasticsearch.xpack.watcher.notification.email; package org.elasticsearch.xpack.watcher.notification.email;
import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.ElasticsearchParseException;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.ToXContentObject;
import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.XContentParser;
@ -16,9 +17,11 @@ import javax.mail.internet.AddressException;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set;
public class EmailTemplate implements ToXContentObject { public class EmailTemplate implements ToXContentObject {
@ -110,19 +113,46 @@ public class EmailTemplate implements ToXContentObject {
if (subject != null) { if (subject != null) {
builder.subject(engine.render(subject, model)); builder.subject(engine.render(subject, model));
} }
if (textBody != null) {
builder.textBody(engine.render(textBody, model)); Set<String> warnings = new HashSet<>(1);
}
if (attachments != null) { if (attachments != null) {
for (Attachment attachment : attachments.values()) { for (Attachment attachment : attachments.values()) {
builder.attach(attachment); builder.attach(attachment);
warnings.addAll(attachment.getWarnings());
} }
} }
String htmlWarnings = "";
String textWarnings = "";
if(warnings.isEmpty() == false){
StringBuilder textWarningBuilder = new StringBuilder();
StringBuilder htmlWarningBuilder = new StringBuilder();
warnings.forEach(w ->
{
if(Strings.isNullOrEmpty(w) == false) {
textWarningBuilder.append(w).append("\n");
htmlWarningBuilder.append(w).append("<br>");
}
});
textWarningBuilder.append("\n");
htmlWarningBuilder.append("<br>");
htmlWarnings = htmlWarningBuilder.toString();
textWarnings = textWarningBuilder.toString();
}
if (textBody != null) {
builder.textBody(textWarnings + engine.render(textBody, model));
}
if (htmlBody != null) { if (htmlBody != null) {
String renderedHtml = engine.render(htmlBody, model); String renderedHtml = htmlWarnings + engine.render(htmlBody, model);
renderedHtml = htmlSanitizer.sanitize(renderedHtml); renderedHtml = htmlSanitizer.sanitize(renderedHtml);
builder.htmlBody(renderedHtml); builder.htmlBody(renderedHtml);
} }
if(htmlBody == null && textBody == null && Strings.isNullOrEmpty(textWarnings) == false){
builder.textBody(textWarnings);
}
return builder; return builder;
} }

View File

@ -5,13 +5,16 @@
*/ */
package org.elasticsearch.xpack.watcher.notification.email.attachment; package org.elasticsearch.xpack.watcher.notification.email.attachment;
import com.google.common.collect.ImmutableMap;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.message.ParameterizedMessage;
import org.elasticsearch.ElasticsearchException; import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.common.ParseField; import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.Strings; import org.elasticsearch.common.Strings;
import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.logging.LoggerMessageFormat; import org.elasticsearch.common.logging.LoggerMessageFormat;
import org.elasticsearch.common.settings.ClusterSettings;
import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.unit.TimeValue;
@ -37,22 +40,39 @@ import org.elasticsearch.xpack.watcher.support.Variables;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.UncheckedIOException; import java.io.UncheckedIOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public class ReportingAttachmentParser implements EmailAttachmentParser<ReportingAttachment> { public class ReportingAttachmentParser implements EmailAttachmentParser<ReportingAttachment> {
public static final String TYPE = "reporting"; public static final String TYPE = "reporting";
// total polling of 10 minutes happens this way by default // total polling of 10 minutes happens this way by default
public static final Setting<TimeValue> INTERVAL_SETTING = static final Setting<TimeValue> INTERVAL_SETTING =
Setting.timeSetting("xpack.notification.reporting.interval", TimeValue.timeValueSeconds(15), Setting.Property.NodeScope); Setting.timeSetting("xpack.notification.reporting.interval", TimeValue.timeValueSeconds(15), Setting.Property.NodeScope);
public static final Setting<Integer> RETRIES_SETTING = static final Setting<Integer> RETRIES_SETTING =
Setting.intSetting("xpack.notification.reporting.retries", 40, 0, Setting.Property.NodeScope); Setting.intSetting("xpack.notification.reporting.retries", 40, 0, Setting.Property.NodeScope);
static final Setting<Boolean> REPORT_WARNING_ENABLED_SETTING =
Setting.boolSetting("xpack.notification.reporting.warning.enabled", true, Setting.Property.NodeScope, Setting.Property.Dynamic);
static final Setting.AffixSetting<String> REPORT_WARNING_TEXT =
Setting.affixKeySetting("xpack.notification.reporting.warning.", "text",
key -> Setting.simpleString(key, Setting.Property.NodeScope, Setting.Property.Dynamic));
private static final ObjectParser<Builder, AuthParseContext> PARSER = new ObjectParser<>("reporting_attachment"); private static final ObjectParser<Builder, AuthParseContext> PARSER = new ObjectParser<>("reporting_attachment");
private static final ObjectParser<KibanaReportingPayload, Void> PAYLOAD_PARSER = private static final ObjectParser<KibanaReportingPayload, Void> PAYLOAD_PARSER =
new ObjectParser<>("reporting_attachment_kibana_payload", true, null); new ObjectParser<>("reporting_attachment_kibana_payload", true, null);
static final Map<String, String> WARNINGS = ImmutableMap.of("kbn-csv-contains-formulas", "Warning: The attachment [%s] contains " +
"characters which spreadsheet applications may interpret as formulas. Please ensure that the attachment is safe prior to opening.");
static { static {
PARSER.declareInt(Builder::retries, ReportingAttachment.RETRIES); PARSER.declareInt(Builder::retries, ReportingAttachment.RETRIES);
PARSER.declareBoolean(Builder::inline, ReportingAttachment.INLINE); PARSER.declareBoolean(Builder::inline, ReportingAttachment.INLINE);
@ -63,18 +83,52 @@ public class ReportingAttachmentParser implements EmailAttachmentParser<Reportin
PAYLOAD_PARSER.declareString(KibanaReportingPayload::setPath, new ParseField("path")); PAYLOAD_PARSER.declareString(KibanaReportingPayload::setPath, new ParseField("path"));
} }
private static List<Setting<?>> getDynamicSettings() {
return Arrays.asList(REPORT_WARNING_ENABLED_SETTING, REPORT_WARNING_TEXT);
}
private static List<Setting<?>> getStaticSettings() {
return Arrays.asList(INTERVAL_SETTING, RETRIES_SETTING);
}
public static List<Setting<?>> getSettings() {
List<Setting<?>> allSettings = new ArrayList<Setting<?>>(getDynamicSettings());
allSettings.addAll(getStaticSettings());
return allSettings;
}
private final Logger logger; private final Logger logger;
private final TimeValue interval; private final TimeValue interval;
private final int retries; private final int retries;
private HttpClient httpClient; private HttpClient httpClient;
private final TextTemplateEngine templateEngine; private final TextTemplateEngine templateEngine;
private boolean warningEnabled = REPORT_WARNING_ENABLED_SETTING.getDefault(Settings.EMPTY);
private final Map<String, String> customWarnings = new ConcurrentHashMap<>(1);
public ReportingAttachmentParser(Settings settings, HttpClient httpClient, TextTemplateEngine templateEngine) { public ReportingAttachmentParser(Settings settings, HttpClient httpClient, TextTemplateEngine templateEngine,
ClusterSettings clusterSettings) {
this.interval = INTERVAL_SETTING.get(settings); this.interval = INTERVAL_SETTING.get(settings);
this.retries = RETRIES_SETTING.get(settings); this.retries = RETRIES_SETTING.get(settings);
this.httpClient = httpClient; this.httpClient = httpClient;
this.templateEngine = templateEngine; this.templateEngine = templateEngine;
this.logger = LogManager.getLogger(getClass()); this.logger = LogManager.getLogger(getClass());
clusterSettings.addSettingsUpdateConsumer(REPORT_WARNING_ENABLED_SETTING, this::setWarningEnabled);
clusterSettings.addAffixUpdateConsumer(REPORT_WARNING_TEXT, this::addWarningText, this::warningValidator);
}
void setWarningEnabled(boolean warningEnabled) {
this.warningEnabled = warningEnabled;
}
void addWarningText(String name, String value) {
customWarnings.put(name, value);
}
void warningValidator(String name, String value) {
if (WARNINGS.keySet().contains(name) == false) {
throw new IllegalArgumentException(new ParameterizedMessage(
"Warning [{}] is not supported. Only the following warnings are supported [{}]",
name, String.join(", ", WARNINGS.keySet())).getFormattedMessage());
}
} }
@Override @Override
@ -139,8 +193,24 @@ public class ReportingAttachmentParser implements EmailAttachmentParser<Reportin
"method[{}], path[{}], status[{}], body[{}]", context.watch().id(), attachment.id(), request.host(), "method[{}], path[{}], status[{}], body[{}]", context.watch().id(), attachment.id(), request.host(),
request.port(), request.method(), request.path(), response.status(), body); request.port(), request.method(), request.path(), response.status(), body);
} else if (response.status() == 200) { } else if (response.status() == 200) {
return new Attachment.Bytes(attachment.id(), BytesReference.toBytes(response.body()), Set<String> warnings = new HashSet<>(1);
response.contentType(), attachment.inline()); if (warningEnabled) {
WARNINGS.forEach((warningKey, defaultWarning) -> {
String[] text = response.header(warningKey);
if (text != null && text.length > 0) {
if (Boolean.valueOf(text[0])) {
String warning = String.format(Locale.ROOT, defaultWarning, attachment.id());
String customWarning = customWarnings.get(warningKey);
if (Strings.isNullOrEmpty(customWarning) == false) {
warning = String.format(Locale.ROOT, customWarning, attachment.id());
}
warnings.add(warning);
}
}
});
}
return new Attachment.Bytes(attachment.id(), attachment.id(), BytesReference.toBytes(response.body()),
response.contentType(), attachment.inline(), warnings);
} else { } else {
String body = response.body() != null ? response.body().utf8ToString() : null; String body = response.body() != null ? response.body().utf8ToString() : null;
String message = LoggerMessageFormat.format("", "Watch[{}] reporting[{}] Unexpected status code host[{}], port[{}], " + String message = LoggerMessageFormat.format("", "Watch[{}] reporting[{}] Unexpected status code host[{}], port[{}], " +

View File

@ -5,6 +5,8 @@
*/ */
package org.elasticsearch.xpack.watcher.notification.email; package org.elasticsearch.xpack.watcher.notification.email;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.ElasticsearchParseException;
import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentBuilder;
@ -13,18 +15,22 @@ import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.xpack.watcher.common.text.TextTemplate; import org.elasticsearch.xpack.watcher.common.text.TextTemplate;
import org.elasticsearch.xpack.watcher.test.MockTextTemplateEngine; import org.elasticsearch.xpack.watcher.test.MockTextTemplateEngine;
import org.mockito.ArgumentCaptor;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import static java.util.Collections.emptyMap; import static java.util.Collections.emptyMap;
import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.startsWith; import static org.hamcrest.Matchers.startsWith;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
public class EmailTemplateTests extends ESTestCase { public class EmailTemplateTests extends ESTestCase {
@ -130,6 +136,90 @@ public class EmailTemplateTests extends ESTestCase {
assertValidEmail("{{valid due to mustache}}, lol.com"); assertValidEmail("{{valid due to mustache}}, lol.com");
} }
public void testEmailWarning() throws Exception {
TextTemplate from = randomFrom(new TextTemplate("from@from.com"), null);
List<TextTemplate> addresses = new ArrayList<>();
for (int i = 0; i < randomIntBetween(1, 5); ++i) {
addresses.add(new TextTemplate("address" + i + "@test.com"));
}
TextTemplate[] possibleList = addresses.toArray(new TextTemplate[addresses.size()]);
TextTemplate[] replyTo = randomFrom(possibleList, null);
TextTemplate[] to = randomFrom(possibleList, null);
TextTemplate[] cc = randomFrom(possibleList, null);
TextTemplate[] bcc = randomFrom(possibleList, null);
TextTemplate priority = new TextTemplate(randomFrom(Email.Priority.values()).name());
TextTemplate subjectTemplate = new TextTemplate("Templated Subject {{foo}}");
TextTemplate textBodyTemplate = new TextTemplate("Templated Body {{foo}}");
TextTemplate htmlBodyTemplate = new TextTemplate("Templated Html Body <script>nefarious scripting</script>");
String htmlBody = "Templated Html Body <script>nefarious scripting</script>";
String sanitizedHtmlBody = "Templated Html Body";
EmailTemplate emailTemplate = new EmailTemplate(from, replyTo, priority, to, cc, bcc, subjectTemplate, textBodyTemplate,
htmlBodyTemplate);
XContentBuilder builder = XContentFactory.jsonBuilder();
emailTemplate.toXContent(builder, ToXContent.EMPTY_PARAMS);
XContentParser parser = createParser(builder);
parser.nextToken();
EmailTemplate.Parser emailTemplateParser = new EmailTemplate.Parser();
String currentFieldName = null;
XContentParser.Token token;
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
if (token == XContentParser.Token.FIELD_NAME) {
currentFieldName = parser.currentName();
} else {
assertThat(emailTemplateParser.handle(currentFieldName, parser), is(true));
}
}
EmailTemplate parsedEmailTemplate = emailTemplateParser.parsedTemplate();
Map<String, Object> model = new HashMap<>();
HtmlSanitizer htmlSanitizer = mock(HtmlSanitizer.class);
when(htmlSanitizer.sanitize(htmlBody)).thenReturn(sanitizedHtmlBody);
ArgumentCaptor<String> htmlSanitizeArguments = ArgumentCaptor.forClass(String.class);
//4 attachments, zero warning, one warning, two warnings, and one with html that should be stripped
Map<String, Attachment> attachments = ImmutableMap.of(
"one", new Attachment.Bytes("one", "one", randomByteArrayOfLength(100), randomAlphaOfLength(5), false, Collections.emptySet()),
"two", new Attachment.Bytes("two", "two", randomByteArrayOfLength(100), randomAlphaOfLength(5), false,
ImmutableSet.of("warning0")),
"thr", new Attachment.Bytes("thr", "thr", randomByteArrayOfLength(100), randomAlphaOfLength(5), false,
ImmutableSet.of("warning1", "warning2")),
"for", new Attachment.Bytes("for", "for", randomByteArrayOfLength(100), randomAlphaOfLength(5), false,
ImmutableSet.of("<script>warning3</script>")));
Email.Builder emailBuilder = parsedEmailTemplate.render(new MockTextTemplateEngine(), model, htmlSanitizer, attachments);
emailBuilder.id("_id");
Email email = emailBuilder.build();
assertThat(email.subject, equalTo(subjectTemplate.getTemplate()));
//text
int bodyStart = email.textBody.indexOf(textBodyTemplate.getTemplate());
String warnings = email.textBody.substring(0, bodyStart);
String[] warningLines = warnings.split("\n");
assertThat(warningLines.length, is(4));
for (int i = 0; i <= warningLines.length - 1; i++) {
assertThat(warnings, containsString("warning" + i));
}
//html - pull the arguments as it is run through the sanitizer
verify(htmlSanitizer).sanitize(htmlSanitizeArguments.capture());
String fullHtmlBody = htmlSanitizeArguments.getValue();
bodyStart = fullHtmlBody.indexOf(htmlBodyTemplate.getTemplate());
warnings = fullHtmlBody.substring(0, bodyStart);
warningLines = warnings.split("<br>");
assertThat(warningLines.length, is(4));
for (int i = 0; i <= warningLines.length - 1; i++) {
assertThat(warnings, containsString("warning" + i));
}
}
private void assertValidEmail(String email) { private void assertValidEmail(String email) {
EmailTemplate.Parser.validateEmailAddresses(new TextTemplate(email)); EmailTemplate.Parser.validateEmailAddresses(new TextTemplate(email));
} }

View File

@ -6,8 +6,11 @@
package org.elasticsearch.xpack.watcher.notification.email.attachment; package org.elasticsearch.xpack.watcher.notification.email.attachment;
import com.fasterxml.jackson.core.io.JsonEOFException; import com.fasterxml.jackson.core.io.JsonEOFException;
import com.google.common.collect.ImmutableSet;
import org.elasticsearch.ElasticsearchException; import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.cluster.service.ClusterService;
import org.elasticsearch.common.Strings; import org.elasticsearch.common.Strings;
import org.elasticsearch.common.settings.ClusterSettings;
import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentBuilder;
@ -38,11 +41,18 @@ import java.time.ZonedDateTime;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
import static org.elasticsearch.xpack.watcher.notification.email.attachment.ReportingAttachmentParser.INTERVAL_SETTING;
import static org.elasticsearch.xpack.watcher.notification.email.attachment.ReportingAttachmentParser.REPORT_WARNING_ENABLED_SETTING;
import static org.elasticsearch.xpack.watcher.notification.email.attachment.ReportingAttachmentParser.REPORT_WARNING_TEXT;
import static org.elasticsearch.xpack.watcher.notification.email.attachment.ReportingAttachmentParser.RETRIES_SETTING;
import static org.elasticsearch.xpack.watcher.notification.email.attachment.ReportingAttachmentParser.WARNINGS;
import static org.elasticsearch.xpack.watcher.test.WatcherTestUtils.mockExecutionContextBuilder; import static org.elasticsearch.xpack.watcher.test.WatcherTestUtils.mockExecutionContextBuilder;
import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.hasEntry; import static org.hamcrest.Matchers.hasEntry;
@ -66,12 +76,13 @@ public class ReportingAttachmentParserTests extends ESTestCase {
private ReportingAttachmentParser reportingAttachmentParser; private ReportingAttachmentParser reportingAttachmentParser;
private MockTextTemplateEngine templateEngine = new MockTextTemplateEngine(); private MockTextTemplateEngine templateEngine = new MockTextTemplateEngine();
private String dashboardUrl = "http://www.example.org/ovb/api/reporting/generate/dashboard/My-Dashboard"; private String dashboardUrl = "http://www.example.org/ovb/api/reporting/generate/dashboard/My-Dashboard";
private ClusterSettings clusterSettings;
@Before @Before
public void init() throws Exception { public void init() throws Exception {
httpClient = mock(HttpClient.class); httpClient = mock(HttpClient.class);
reportingAttachmentParser = new ReportingAttachmentParser(Settings.EMPTY, httpClient, templateEngine); clusterSettings = mockClusterService().getClusterSettings();
reportingAttachmentParser = new ReportingAttachmentParser(Settings.EMPTY, httpClient, templateEngine, clusterSettings);
attachmentParsers.put(ReportingAttachmentParser.TYPE, reportingAttachmentParser); attachmentParsers.put(ReportingAttachmentParser.TYPE, reportingAttachmentParser);
emailAttachmentsParser = new EmailAttachmentsParser(attachmentParsers); emailAttachmentsParser = new EmailAttachmentsParser(attachmentParsers);
} }
@ -165,6 +176,7 @@ public class ReportingAttachmentParserTests extends ESTestCase {
new ReportingAttachment("foo", dashboardUrl, randomBoolean(), TimeValue.timeValueMillis(1), 10, null, null); new ReportingAttachment("foo", dashboardUrl, randomBoolean(), TimeValue.timeValueMillis(1), 10, null, null);
Attachment attachment = reportingAttachmentParser.toAttachment(createWatchExecutionContext(), Payload.EMPTY, reportingAttachment); Attachment attachment = reportingAttachmentParser.toAttachment(createWatchExecutionContext(), Payload.EMPTY, reportingAttachment);
assertThat(attachment, instanceOf(Attachment.Bytes.class)); assertThat(attachment, instanceOf(Attachment.Bytes.class));
assertThat(attachment.getWarnings(), hasSize(0));
Attachment.Bytes bytesAttachment = (Attachment.Bytes) attachment; Attachment.Bytes bytesAttachment = (Attachment.Bytes) attachment;
assertThat(new String(bytesAttachment.bytes(), StandardCharsets.UTF_8), is(content)); assertThat(new String(bytesAttachment.bytes(), StandardCharsets.UTF_8), is(content));
assertThat(bytesAttachment.contentType(), is(randomContentType)); assertThat(bytesAttachment.contentType(), is(randomContentType));
@ -319,11 +331,11 @@ public class ReportingAttachmentParserTests extends ESTestCase {
.thenReturn(new HttpResponse(503)); .thenReturn(new HttpResponse(503));
ReportingAttachment attachment = new ReportingAttachment("foo", dashboardUrl, randomBoolean(), TimeValue.timeValueMillis(1), ReportingAttachment attachment = new ReportingAttachment("foo", dashboardUrl, randomBoolean(), TimeValue.timeValueMillis(1),
ReportingAttachmentParser.RETRIES_SETTING.getDefault(Settings.EMPTY), new BasicAuth("foo", "bar".toCharArray()), null); RETRIES_SETTING.getDefault(Settings.EMPTY), new BasicAuth("foo", "bar".toCharArray()), null);
expectThrows(ElasticsearchException.class, () -> expectThrows(ElasticsearchException.class, () ->
reportingAttachmentParser.toAttachment(createWatchExecutionContext(), Payload.EMPTY, attachment)); reportingAttachmentParser.toAttachment(createWatchExecutionContext(), Payload.EMPTY, attachment));
verify(httpClient, times(ReportingAttachmentParser.RETRIES_SETTING.getDefault(Settings.EMPTY) + 1)).execute(any()); verify(httpClient, times(RETRIES_SETTING.getDefault(Settings.EMPTY) + 1)).execute(any());
} }
public void testPollingDefaultCanBeOverriddenBySettings() throws Exception { public void testPollingDefaultCanBeOverriddenBySettings() throws Exception {
@ -335,11 +347,11 @@ public class ReportingAttachmentParserTests extends ESTestCase {
ReportingAttachment attachment = new ReportingAttachment("foo", dashboardUrl, randomBoolean(), null, null, null, null); ReportingAttachment attachment = new ReportingAttachment("foo", dashboardUrl, randomBoolean(), null, null, null, null);
Settings settings = Settings.builder() Settings settings = Settings.builder()
.put(ReportingAttachmentParser.INTERVAL_SETTING.getKey(), "1ms") .put(INTERVAL_SETTING.getKey(), "1ms")
.put(ReportingAttachmentParser.RETRIES_SETTING.getKey(), retries) .put(RETRIES_SETTING.getKey(), retries)
.build(); .build();
reportingAttachmentParser = new ReportingAttachmentParser(settings, httpClient, templateEngine); reportingAttachmentParser = new ReportingAttachmentParser(settings, httpClient, templateEngine, clusterSettings);
expectThrows(ElasticsearchException.class, () -> expectThrows(ElasticsearchException.class, () ->
reportingAttachmentParser.toAttachment(createWatchExecutionContext(), Payload.EMPTY, attachment)); reportingAttachmentParser.toAttachment(createWatchExecutionContext(), Payload.EMPTY, attachment));
@ -362,7 +374,7 @@ public class ReportingAttachmentParserTests extends ESTestCase {
ReportingAttachment attachment = new ReportingAttachment("foo", "http://www.example.org/REPLACEME", randomBoolean(), ReportingAttachment attachment = new ReportingAttachment("foo", "http://www.example.org/REPLACEME", randomBoolean(),
TimeValue.timeValueMillis(1), 10, new BasicAuth("foo", "bar".toCharArray()), null); TimeValue.timeValueMillis(1), 10, new BasicAuth("foo", "bar".toCharArray()), null);
reportingAttachmentParser = new ReportingAttachmentParser(Settings.EMPTY, httpClient, reportingAttachmentParser = new ReportingAttachmentParser(Settings.EMPTY, httpClient,
replaceHttpWithHttpsTemplateEngine); replaceHttpWithHttpsTemplateEngine, clusterSettings);
reportingAttachmentParser.toAttachment(createWatchExecutionContext(), Payload.EMPTY, attachment); reportingAttachmentParser.toAttachment(createWatchExecutionContext(), Payload.EMPTY, attachment);
ArgumentCaptor<HttpRequest> requestArgumentCaptor = ArgumentCaptor.forClass(HttpRequest.class); ArgumentCaptor<HttpRequest> requestArgumentCaptor = ArgumentCaptor.forClass(HttpRequest.class);
@ -379,7 +391,7 @@ public class ReportingAttachmentParserTests extends ESTestCase {
Settings invalidSettings = Settings.builder().put("xpack.notification.reporting.retries", -10).build(); Settings invalidSettings = Settings.builder().put("xpack.notification.reporting.retries", -10).build();
e = expectThrows(IllegalArgumentException.class, e = expectThrows(IllegalArgumentException.class,
() -> new ReportingAttachmentParser(invalidSettings, httpClient, templateEngine)); () -> new ReportingAttachmentParser(invalidSettings, httpClient, templateEngine, clusterSettings));
assertThat(e.getMessage(), is("Failed to parse value [-10] for setting [xpack.notification.reporting.retries] must be >= 0")); assertThat(e.getMessage(), is("Failed to parse value [-10] for setting [xpack.notification.reporting.retries] must be >= 0"));
} }
@ -405,13 +417,161 @@ public class ReportingAttachmentParserTests extends ESTestCase {
requestCaptor.getAllValues().forEach(req -> assertThat(req.proxy(), is(proxy))); requestCaptor.getAllValues().forEach(req -> assertThat(req.proxy(), is(proxy)));
} }
public void testDefaultWarnings() throws Exception {
String content = randomAlphaOfLength(200);
String path = "/ovb/api/reporting/jobs/download/iu5zfzvk15oa8990bfas9wy2";
String randomContentType = randomAlphaOfLength(20);
String reportId = randomAlphaOfLength(5);
Map<String, String[]> headers = new HashMap<>();
headers.put("Content-Type", new String[] { randomContentType });
WARNINGS.keySet().forEach((k) -> headers.put(k, new String[]{"true"}));
when(httpClient.execute(any(HttpRequest.class)))
.thenReturn(new HttpResponse(200, "{\"path\":\""+ path +"\", \"other\":\"content\"}"))
.thenReturn(new HttpResponse(200, content, headers));
ReportingAttachment reportingAttachment =
new ReportingAttachment(reportId, dashboardUrl, randomBoolean(), TimeValue.timeValueMillis(1), 10, null, null);
Attachment attachment = reportingAttachmentParser.toAttachment(createWatchExecutionContext(), Payload.EMPTY, reportingAttachment);
assertThat(attachment, instanceOf(Attachment.Bytes.class));
assertThat(attachment.getWarnings(), hasSize(WARNINGS.keySet().size()));
//parameterize the messages
assertEquals(attachment.getWarnings(), WARNINGS.values().stream().
map(s -> String.format(Locale.ROOT, s, reportId)).collect(Collectors.toSet()));
Attachment.Bytes bytesAttachment = (Attachment.Bytes) attachment;
assertThat(new String(bytesAttachment.bytes(), StandardCharsets.UTF_8), is(content));
assertThat(bytesAttachment.contentType(), is(randomContentType));
}
public void testCustomWarningsNoParams() throws Exception {
String content = randomAlphaOfLength(200);
String path = "/ovb/api/reporting/jobs/download/iu5zfzvk15oa8990bfas9wy2";
String randomContentType = randomAlphaOfLength(20);
String reportId = randomAlphaOfLength(5);
Map<String, String[]> headers = new HashMap<>();
headers.put("Content-Type", new String[] { randomContentType });
Map<String, String> customWarnings = new HashMap<>(WARNINGS.size());
WARNINGS.keySet().forEach((k) ->
{
final String warning = randomAlphaOfLength(20);
customWarnings.put(k, warning);
reportingAttachmentParser.addWarningText(k, warning);
headers.put(k, new String[]{"true"});
});
when(httpClient.execute(any(HttpRequest.class)))
.thenReturn(new HttpResponse(200, "{\"path\":\""+ path +"\", \"other\":\"content\"}"))
.thenReturn(new HttpResponse(200, content, headers));
ReportingAttachment reportingAttachment =
new ReportingAttachment(reportId, dashboardUrl, randomBoolean(), TimeValue.timeValueMillis(1), 10, null, null);
Attachment attachment = reportingAttachmentParser.toAttachment(createWatchExecutionContext(), Payload.EMPTY, reportingAttachment);
assertThat(attachment, instanceOf(Attachment.Bytes.class));
assertThat(attachment.getWarnings(), hasSize(WARNINGS.keySet().size()));
assertEquals(attachment.getWarnings(), new HashSet<>(customWarnings.values()));
Attachment.Bytes bytesAttachment = (Attachment.Bytes) attachment;
assertThat(new String(bytesAttachment.bytes(), StandardCharsets.UTF_8), is(content));
assertThat(bytesAttachment.contentType(), is(randomContentType));
}
public void testCustomWarningsWithParams() throws Exception {
String content = randomAlphaOfLength(200);
String path = "/ovb/api/reporting/jobs/download/iu5zfzvk15oa8990bfas9wy2";
String randomContentType = randomAlphaOfLength(20);
String reportId = randomAlphaOfLength(5);
Map<String, String[]> headers = new HashMap<>();
headers.put("Content-Type", new String[]{randomContentType});
Map<String, String> customWarnings = new HashMap<>(WARNINGS.size());
WARNINGS.keySet().forEach((k) ->
{
//add a parameter
final String warning = randomAlphaOfLength(20) + " %s";
customWarnings.put(k, warning);
reportingAttachmentParser.addWarningText(k, warning);
headers.put(k, new String[]{"true"});
});
when(httpClient.execute(any(HttpRequest.class)))
.thenReturn(new HttpResponse(200, "{\"path\":\"" + path + "\", \"other\":\"content\"}"))
.thenReturn(new HttpResponse(200, content, headers));
ReportingAttachment reportingAttachment =
new ReportingAttachment(reportId, dashboardUrl, randomBoolean(), TimeValue.timeValueMillis(1), 10, null, null);
Attachment attachment = reportingAttachmentParser.toAttachment(createWatchExecutionContext(), Payload.EMPTY, reportingAttachment);
assertThat(attachment, instanceOf(Attachment.Bytes.class));
assertThat(attachment.getWarnings(), hasSize(WARNINGS.keySet().size()));
//parameterize the messages
assertEquals(attachment.getWarnings(), customWarnings.values().stream().
map(s -> String.format(Locale.ROOT, s, reportId)).collect(Collectors.toSet()));
//ensure the reportId is parameterized in
attachment.getWarnings().forEach(s -> {
assertThat(s, containsString(reportId));
});
Attachment.Bytes bytesAttachment = (Attachment.Bytes) attachment;
assertThat(new String(bytesAttachment.bytes(), StandardCharsets.UTF_8), is(content));
assertThat(bytesAttachment.contentType(), is(randomContentType));
}
public void testWarningsSuppress() throws Exception {
String content = randomAlphaOfLength(200);
String path = "/ovb/api/reporting/jobs/download/iu5zfzvk15oa8990bfas9wy2";
String randomContentType = randomAlphaOfLength(20);
String reportId = randomAlphaOfLength(5);
Map<String, String[]> headers = new HashMap<>();
headers.put("Content-Type", new String[]{randomContentType});
Map<String, String> customWarnings = new HashMap<>(WARNINGS.size());
WARNINGS.keySet().forEach((k) ->
{
final String warning = randomAlphaOfLength(20);
customWarnings.put(k, warning);
reportingAttachmentParser.addWarningText(k, warning);
reportingAttachmentParser.setWarningEnabled(false);
headers.put(k, new String[]{"true"});
});
when(httpClient.execute(any(HttpRequest.class)))
.thenReturn(new HttpResponse(200, "{\"path\":\"" + path + "\", \"other\":\"content\"}"))
.thenReturn(new HttpResponse(200, content, headers));
ReportingAttachment reportingAttachment =
new ReportingAttachment(reportId, dashboardUrl, randomBoolean(), TimeValue.timeValueMillis(1), 10, null, null);
Attachment attachment = reportingAttachmentParser.toAttachment(createWatchExecutionContext(), Payload.EMPTY, reportingAttachment);
assertThat(attachment, instanceOf(Attachment.Bytes.class));
assertThat(attachment.getWarnings(), hasSize(0));
Attachment.Bytes bytesAttachment = (Attachment.Bytes) attachment;
assertThat(new String(bytesAttachment.bytes(), StandardCharsets.UTF_8), is(content));
assertThat(bytesAttachment.contentType(), is(randomContentType));
}
public void testWarningValidation() {
WARNINGS.forEach((k, v) -> {
String keyName = randomAlphaOfLength(5) + "notavalidsettingname";
IllegalArgumentException expectedException = expectThrows(IllegalArgumentException.class,
() -> reportingAttachmentParser.warningValidator(keyName, randomAlphaOfLength(10)));
assertThat(expectedException.getMessage(), containsString(keyName));
assertThat(expectedException.getMessage(), containsString("is not supported"));
});
}
private WatchExecutionContext createWatchExecutionContext() { private WatchExecutionContext createWatchExecutionContext() {
ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC); ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);
return mockExecutionContextBuilder("watch1") return mockExecutionContextBuilder("watch1")
.wid(new Wid(randomAlphaOfLength(5), now)) .wid(new Wid(randomAlphaOfLength(5), now))
.payload(new Payload.Simple()) .payload(new Payload.Simple())
.time("watch1", now) .time("watch1", now)
.metadata(Collections.emptyMap()) .metadata(Collections.emptyMap())
.buildMock(); .buildMock();
}
private ClusterService mockClusterService() {
ClusterService clusterService = mock(ClusterService.class);
ClusterSettings clusterSettings =
new ClusterSettings(Settings.EMPTY,
ImmutableSet.of(INTERVAL_SETTING, RETRIES_SETTING, REPORT_WARNING_ENABLED_SETTING, REPORT_WARNING_TEXT));
when(clusterService.getClusterSettings()).thenReturn(clusterSettings);
return clusterService;
} }
} }