* 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:
parent
f2241a152f
commit
767f648f8e
|
@ -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
|
||||
|
|
|
@ -288,7 +288,8 @@ public class Watcher extends Plugin implements ActionPlugin, ScriptPlugin, Reloa
|
|||
Map<String, EmailAttachmentParser> 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
|
||||
|
@ -487,8 +488,7 @@ public class Watcher extends Plugin implements ActionPlugin, ScriptPlugin, Reloa
|
|||
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());
|
||||
|
|
|
@ -24,6 +24,8 @@ import java.io.IOException;
|
|||
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 @@ import static javax.mail.Part.INLINE;
|
|||
public abstract class Attachment extends BodyPartSource {
|
||||
|
||||
private final boolean inline;
|
||||
private final Set<String> 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<String> warnings) {
|
||||
super(id, name, contentType);
|
||||
this.inline = inline;
|
||||
assert warnings != null;
|
||||
this.warnings = warnings;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -53,6 +62,10 @@ public abstract class Attachment extends BodyPartSource {
|
|||
return inline;
|
||||
}
|
||||
|
||||
public Set<String> getWarnings() {
|
||||
return warnings;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
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<String> warnings) {
|
||||
super(id, name, contentType, inline, warnings);
|
||||
this.bytes = bytes;
|
||||
}
|
||||
|
||||
|
@ -213,7 +226,7 @@ public abstract class Attachment extends BodyPartSource {
|
|||
}
|
||||
|
||||
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) {
|
||||
|
|
|
@ -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 javax.mail.internet.AddressException;
|
|||
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 class EmailTemplate implements ToXContentObject {
|
|||
if (subject != null) {
|
||||
builder.subject(engine.render(subject, model));
|
||||
}
|
||||
if (textBody != null) {
|
||||
builder.textBody(engine.render(textBody, model));
|
||||
}
|
||||
|
||||
Set<String> 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("<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) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
@ -5,13 +5,16 @@
|
|||
*/
|
||||
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.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 +40,39 @@ import org.elasticsearch.xpack.watcher.support.Variables;
|
|||
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<ReportingAttachment> {
|
||||
|
||||
public static final String TYPE = "reporting";
|
||||
|
||||
// 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);
|
||||
public static final Setting<Integer> RETRIES_SETTING =
|
||||
static final Setting<Integer> RETRIES_SETTING =
|
||||
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<KibanaReportingPayload, Void> PAYLOAD_PARSER =
|
||||
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 {
|
||||
PARSER.declareInt(Builder::retries, ReportingAttachment.RETRIES);
|
||||
PARSER.declareBoolean(Builder::inline, ReportingAttachment.INLINE);
|
||||
|
@ -63,18 +83,52 @@ public class ReportingAttachmentParser implements EmailAttachmentParser<Reportin
|
|||
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 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<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.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 +193,24 @@ public class ReportingAttachmentParser implements EmailAttachmentParser<Reportin
|
|||
"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<String> 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[{}], " +
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
*/
|
||||
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.common.xcontent.ToXContent;
|
||||
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||
|
@ -13,18 +15,22 @@ import org.elasticsearch.common.xcontent.XContentParser;
|
|||
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 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 +136,90 @@ public class EmailTemplateTests extends ESTestCase {
|
|||
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) {
|
||||
EmailTemplate.Parser.validateEmailAddresses(new TextTemplate(email));
|
||||
}
|
||||
|
|
|
@ -6,8 +6,11 @@
|
|||
package org.elasticsearch.xpack.watcher.notification.email.attachment;
|
||||
|
||||
import com.fasterxml.jackson.core.io.JsonEOFException;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
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 +41,18 @@ import java.time.ZonedDateTime;
|
|||
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.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 class ReportingAttachmentParserTests extends ESTestCase {
|
|||
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 class ReportingAttachmentParserTests extends ESTestCase {
|
|||
.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 class ReportingAttachmentParserTests extends ESTestCase {
|
|||
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 class ReportingAttachmentParserTests extends ESTestCase {
|
|||
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<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();
|
||||
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 class ReportingAttachmentParserTests extends ESTestCase {
|
|||
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() {
|
||||
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,
|
||||
ImmutableSet.of(INTERVAL_SETTING, RETRIES_SETTING, REPORT_WARNING_ENABLED_SETTING, REPORT_WARNING_TEXT));
|
||||
when(clusterService.getClusterSettings()).thenReturn(clusterSettings);
|
||||
return clusterService;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue