diff --git a/x-pack/plugin/watcher/build.gradle b/x-pack/plugin/watcher/build.gradle index bfd447adc26fe..c887506f69ad1 100644 --- a/x-pack/plugin/watcher/build.gradle +++ b/x-pack/plugin/watcher/build.gradle @@ -41,7 +41,7 @@ dependencies { testCompile 'org.subethamail:subethasmtp:3.1.7' // 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 diff --git a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/Watcher.java b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/Watcher.java index ee4ebec0b0b03..0682160b0118a 100644 --- a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/Watcher.java +++ b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/Watcher.java @@ -287,7 +287,8 @@ public Collection createComponents(Client client, ClusterService cluster Map emailAttachmentParsers = new HashMap<>(); emailAttachmentParsers.put(HttpEmailAttachementParser.TYPE, new HttpEmailAttachementParser(httpClient, templateEngine)); 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); // conditions @@ -486,8 +487,7 @@ public List> getSettings() { settings.addAll(HtmlSanitizer.getSettings()); settings.addAll(JiraService.getSettings()); settings.addAll(PagerDutyService.getSettings()); - settings.add(ReportingAttachmentParser.RETRIES_SETTING); - settings.add(ReportingAttachmentParser.INTERVAL_SETTING); + settings.addAll(ReportingAttachmentParser.getSettings()); // http settings settings.addAll(HttpSettings.getSettings()); diff --git a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/notification/email/Attachment.java b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/notification/email/Attachment.java index cc21ba9f03f44..7f177dc86ab95 100644 --- a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/notification/email/Attachment.java +++ b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/notification/email/Attachment.java @@ -24,6 +24,8 @@ import java.io.InputStream; import java.io.OutputStream; import java.nio.file.Path; +import java.util.Collections; +import java.util.Set; import static javax.mail.Part.ATTACHMENT; import static javax.mail.Part.INLINE; @@ -31,10 +33,17 @@ public abstract class Attachment extends BodyPartSource { private final boolean inline; + private final Set warnings; 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 warnings) { super(id, name, contentType); this.inline = inline; + assert warnings != null; + this.warnings = warnings; } @Override @@ -53,6 +62,10 @@ public boolean isInline() { return inline; } + public Set getWarnings() { + return warnings; + } + /** * intentionally not emitting path as it may come as an information leak */ @@ -116,15 +129,15 @@ public static class Bytes extends Attachment { private final byte[] bytes; 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) { - 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) { - super(id, name, contentType, inline); + public Bytes(String id, String name, byte[] bytes, String contentType, boolean inline, Set warnings) { + super(id, name, contentType, inline, warnings); this.bytes = bytes; } @@ -213,7 +226,7 @@ protected XContent(String id, 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) { diff --git a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/notification/email/EmailTemplate.java b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/notification/email/EmailTemplate.java index 05d8ccb240093..dc48dd2b9f23f 100644 --- a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/notification/email/EmailTemplate.java +++ b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/notification/email/EmailTemplate.java @@ -6,6 +6,7 @@ package org.elasticsearch.xpack.watcher.notification.email; import org.elasticsearch.ElasticsearchParseException; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; @@ -16,9 +17,11 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; public class EmailTemplate implements ToXContentObject { @@ -110,19 +113,46 @@ public Email.Builder render(TextTemplateEngine engine, Map model if (subject != null) { builder.subject(engine.render(subject, model)); } - if (textBody != null) { - builder.textBody(engine.render(textBody, model)); - } + + Set warnings = new HashSet<>(1); if (attachments != null) { for (Attachment attachment : attachments.values()) { 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("
"); + } + }); + textWarningBuilder.append("\n"); + htmlWarningBuilder.append("
"); + htmlWarnings = htmlWarningBuilder.toString(); + textWarnings = textWarningBuilder.toString(); + } + if (textBody != null) { + builder.textBody(textWarnings + engine.render(textBody, model)); + } + if (htmlBody != null) { - String renderedHtml = engine.render(htmlBody, model); + String renderedHtml = htmlWarnings + engine.render(htmlBody, model); renderedHtml = htmlSanitizer.sanitize(renderedHtml); builder.htmlBody(renderedHtml); } + + if(htmlBody == null && textBody == null && Strings.isNullOrEmpty(textWarnings) == false){ + builder.textBody(textWarnings); + } + return builder; } diff --git a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/notification/email/attachment/ReportingAttachmentParser.java b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/notification/email/attachment/ReportingAttachmentParser.java index d0d9ecc78104c..83c9027add419 100644 --- a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/notification/email/attachment/ReportingAttachmentParser.java +++ b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/notification/email/attachment/ReportingAttachmentParser.java @@ -7,11 +7,13 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.message.ParameterizedMessage; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.logging.LoggerMessageFormat; +import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; @@ -37,22 +39,39 @@ import java.io.IOException; import java.io.InputStream; 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.Set; +import java.util.concurrent.ConcurrentHashMap; public class ReportingAttachmentParser implements EmailAttachmentParser { public static final String TYPE = "reporting"; // total polling of 10 minutes happens this way by default - public static final Setting INTERVAL_SETTING = + static final Setting INTERVAL_SETTING = Setting.timeSetting("xpack.notification.reporting.interval", TimeValue.timeValueSeconds(15), Setting.Property.NodeScope); - public static final Setting RETRIES_SETTING = + static final Setting RETRIES_SETTING = Setting.intSetting("xpack.notification.reporting.retries", 40, 0, Setting.Property.NodeScope); + static final Setting REPORT_WARNING_ENABLED_SETTING = + Setting.boolSetting("xpack.notification.reporting.warning.enabled", true, Setting.Property.NodeScope, Setting.Property.Dynamic); + + static final Setting.AffixSetting REPORT_WARNING_TEXT = + Setting.affixKeySetting("xpack.notification.reporting.warning.", "text", + key -> Setting.simpleString(key, Setting.Property.NodeScope, Setting.Property.Dynamic)); + private static final ObjectParser PARSER = new ObjectParser<>("reporting_attachment"); private static final ObjectParser PAYLOAD_PARSER = new ObjectParser<>("reporting_attachment_kibana_payload", true, null); + static final Map WARNINGS = Map.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 { PARSER.declareInt(Builder::retries, ReportingAttachment.RETRIES); PARSER.declareBoolean(Builder::inline, ReportingAttachment.INLINE); @@ -63,18 +82,52 @@ public class ReportingAttachmentParser implements EmailAttachmentParser> getDynamicSettings() { + return Arrays.asList(REPORT_WARNING_ENABLED_SETTING, REPORT_WARNING_TEXT); + } + + private static List> getStaticSettings() { + return Arrays.asList(INTERVAL_SETTING, RETRIES_SETTING); + } + + public static List> getSettings() { + List> allSettings = new ArrayList>(getDynamicSettings()); + allSettings.addAll(getStaticSettings()); + return allSettings; + } private final Logger logger; private final TimeValue interval; private final int retries; private HttpClient httpClient; private final TextTemplateEngine templateEngine; + private boolean warningEnabled = REPORT_WARNING_ENABLED_SETTING.getDefault(Settings.EMPTY); + private final Map 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.retries = RETRIES_SETTING.get(settings); this.httpClient = httpClient; this.templateEngine = templateEngine; 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 @@ -139,8 +192,24 @@ public Attachment toAttachment(WatchExecutionContext context, Payload payload, R "method[{}], path[{}], status[{}], body[{}]", context.watch().id(), attachment.id(), request.host(), request.port(), request.method(), request.path(), response.status(), body); } else if (response.status() == 200) { - return new Attachment.Bytes(attachment.id(), BytesReference.toBytes(response.body()), - response.contentType(), attachment.inline()); + Set warnings = new HashSet<>(1); + 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 { String body = response.body() != null ? response.body().utf8ToString() : null; String message = LoggerMessageFormat.format("", "Watch[{}] reporting[{}] Unexpected status code host[{}], port[{}], " + diff --git a/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/notification/email/EmailTemplateTests.java b/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/notification/email/EmailTemplateTests.java index 489ad7f6883e6..100c4edb0127f 100644 --- a/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/notification/email/EmailTemplateTests.java +++ b/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/notification/email/EmailTemplateTests.java @@ -13,18 +13,23 @@ import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xpack.watcher.common.text.TextTemplate; import org.elasticsearch.xpack.watcher.test.MockTextTemplateEngine; +import org.mockito.ArgumentCaptor; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import static java.util.Collections.emptyMap; import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.startsWith; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; public class EmailTemplateTests extends ESTestCase { @@ -130,6 +135,89 @@ public void testEmailValidation() { assertValidEmail("{{valid due to mustache}}, lol.com"); } + public void testEmailWarning() throws Exception { + TextTemplate from = randomFrom(new TextTemplate("from@from.com"), null); + List 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 "); + String htmlBody = "Templated Html Body "; + 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 model = new HashMap<>(); + + HtmlSanitizer htmlSanitizer = mock(HtmlSanitizer.class); + when(htmlSanitizer.sanitize(htmlBody)).thenReturn(sanitizedHtmlBody); + ArgumentCaptor htmlSanitizeArguments = ArgumentCaptor.forClass(String.class); + + //4 attachments, zero warning, one warning, two warnings, and one with html that should be stripped + Map attachments = Map.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, Set.of("warning0")), + "thr", new Attachment.Bytes("thr", "thr", randomByteArrayOfLength(100), randomAlphaOfLength(5), false, + Set.of("warning1", "warning2")), + "for", new Attachment.Bytes("for", "for", randomByteArrayOfLength(100), randomAlphaOfLength(5), false, + Set.of(""))); + 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("
"); + assertThat(warningLines.length, is(4)); + for (int i = 0; i <= warningLines.length - 1; i++) { + assertThat(warnings, containsString("warning" + i)); + } + } + private void assertValidEmail(String email) { EmailTemplate.Parser.validateEmailAddresses(new TextTemplate(email)); } diff --git a/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/notification/email/attachment/ReportingAttachmentParserTests.java b/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/notification/email/attachment/ReportingAttachmentParserTests.java index 17b0498dc89fa..8061df4791e63 100644 --- a/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/notification/email/attachment/ReportingAttachmentParserTests.java +++ b/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/notification/email/attachment/ReportingAttachmentParserTests.java @@ -7,7 +7,9 @@ import com.fasterxml.jackson.core.io.JsonEOFException; import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.Strings; +import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.XContentBuilder; @@ -38,11 +40,19 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; 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.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.hasEntry; @@ -66,12 +76,13 @@ public class ReportingAttachmentParserTests extends ESTestCase { private ReportingAttachmentParser reportingAttachmentParser; private MockTextTemplateEngine templateEngine = new MockTextTemplateEngine(); private String dashboardUrl = "http://www.example.org/ovb/api/reporting/generate/dashboard/My-Dashboard"; + private ClusterSettings clusterSettings; @Before public void init() throws Exception { 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); emailAttachmentsParser = new EmailAttachmentsParser(attachmentParsers); } @@ -165,6 +176,7 @@ public void testGoodCase() throws Exception { new ReportingAttachment("foo", 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)); @@ -319,11 +331,11 @@ public void testPollingDefaultsRetries() throws Exception { .thenReturn(new HttpResponse(503)); 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, () -> 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 { @@ -335,11 +347,11 @@ public void testPollingDefaultCanBeOverriddenBySettings() throws Exception { ReportingAttachment attachment = new ReportingAttachment("foo", dashboardUrl, randomBoolean(), null, null, null, null); Settings settings = Settings.builder() - .put(ReportingAttachmentParser.INTERVAL_SETTING.getKey(), "1ms") - .put(ReportingAttachmentParser.RETRIES_SETTING.getKey(), retries) + .put(INTERVAL_SETTING.getKey(), "1ms") + .put(RETRIES_SETTING.getKey(), retries) .build(); - reportingAttachmentParser = new ReportingAttachmentParser(settings, httpClient, templateEngine); + reportingAttachmentParser = new ReportingAttachmentParser(settings, httpClient, templateEngine, clusterSettings); expectThrows(ElasticsearchException.class, () -> reportingAttachmentParser.toAttachment(createWatchExecutionContext(), Payload.EMPTY, attachment)); @@ -362,7 +374,7 @@ public String render(TextTemplate textTemplate, Map model) { ReportingAttachment attachment = new ReportingAttachment("foo", "http://www.example.org/REPLACEME", randomBoolean(), TimeValue.timeValueMillis(1), 10, new BasicAuth("foo", "bar".toCharArray()), null); reportingAttachmentParser = new ReportingAttachmentParser(Settings.EMPTY, httpClient, - replaceHttpWithHttpsTemplateEngine); + replaceHttpWithHttpsTemplateEngine, clusterSettings); reportingAttachmentParser.toAttachment(createWatchExecutionContext(), Payload.EMPTY, attachment); ArgumentCaptor requestArgumentCaptor = ArgumentCaptor.forClass(HttpRequest.class); @@ -379,7 +391,7 @@ public void testRetrySettingCannotBeNegative() throws Exception { Settings invalidSettings = Settings.builder().put("xpack.notification.reporting.retries", -10).build(); 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")); } @@ -405,13 +417,161 @@ public void testHttpProxy() throws Exception { 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 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 headers = new HashMap<>(); + headers.put("Content-Type", new String[] { randomContentType }); + Map 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 headers = new HashMap<>(); + headers.put("Content-Type", new String[]{randomContentType}); + Map 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 headers = new HashMap<>(); + headers.put("Content-Type", new String[]{randomContentType}); + Map 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() { ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC); return mockExecutionContextBuilder("watch1") - .wid(new Wid(randomAlphaOfLength(5), now)) - .payload(new Payload.Simple()) - .time("watch1", now) - .metadata(Collections.emptyMap()) - .buildMock(); + .wid(new Wid(randomAlphaOfLength(5), now)) + .payload(new Payload.Simple()) + .time("watch1", now) + .metadata(Collections.emptyMap()) + .buildMock(); + } + + + private ClusterService mockClusterService() { + ClusterService clusterService = mock(ClusterService.class); + ClusterSettings clusterSettings = + new ClusterSettings(Settings.EMPTY, + Set.of(INTERVAL_SETTING, RETRIES_SETTING, REPORT_WARNING_ENABLED_SETTING, REPORT_WARNING_TEXT)); + when(clusterService.getClusterSettings()).thenReturn(clusterSettings); + return clusterService; } }