Skip to content

Commit fc0ae41

Browse files
lukewhitingalexey-ivanov-es
authored andcommitted
Introduce Email Address Allow Lists For Watcher (elastic#116672)
* New setting plus mutual exclusiveness validation * New domain list checking * Email service tests * Documentation updates * PR Changes Fix comment
1 parent f35d8b2 commit fc0ae41

File tree

3 files changed

+415
-13
lines changed

3 files changed

+415
-13
lines changed

docs/reference/settings/notification-settings.asciidoc

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,17 @@ If you configure multiple email accounts, you must either configure this setting
118118
or specify the email account to use in the <<actions-email,`email`>> action. See
119119
<<configuring-email>>.
120120

121+
`xpack.notification.email.recipient_allowlist`::
122+
(<<dynamic-cluster-setting,Dynamic>>)
123+
Specifies addresses to which emails are allowed to be sent.
124+
Emails with recipients (`To:`, `Cc:`, or `Bcc:`) outside of these patterns will be rejected and an
125+
error thrown. This setting defaults to `["*"]` which means all recipients are allowed.
126+
Simple globbing is supported, such as `list-*@company.com` in the list of allowed recipients.
127+
128+
NOTE: This setting can't be used at the same time as `xpack.notification.email.account.domain_allowlist`
129+
and an error will be thrown if both are set at the same time. This setting can be used to specify domains
130+
to allow by using a wildcard pattern such as `*@company.com`.
131+
121132
`xpack.notification.email.account`::
122133
Specifies account information for sending notifications via email. You
123134
can specify the following email account attributes:
@@ -129,6 +140,10 @@ Specifies domains to which emails are allowed to be sent. Emails with recipients
129140
`Bcc:`) outside of these domains will be rejected and an error thrown. This setting defaults to
130141
`["*"]` which means all domains are allowed. Simple globbing is supported, such as `*.company.com`
131142
in the list of allowed domains.
143+
144+
NOTE: This setting can't be used at the same time as `xpack.notification.email.recipient_allowlist`
145+
and an error will be thrown if both are set at the same time.
146+
132147
--
133148
[[email-account-attributes]]
134149

x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/notification/email/EmailService.java

Lines changed: 115 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@
2626
import java.util.ArrayList;
2727
import java.util.Arrays;
2828
import java.util.HashSet;
29+
import java.util.Iterator;
2930
import java.util.List;
31+
import java.util.Map;
3032
import java.util.Optional;
3133
import java.util.Set;
3234
import java.util.function.Predicate;
@@ -56,9 +58,72 @@ public class EmailService extends NotificationService<Account> {
5658
(key) -> Setting.simpleString(key, Property.Dynamic, Property.NodeScope)
5759
);
5860

61+
private static final List<String> ALLOW_ALL_DEFAULT = List.of("*");
62+
5963
private static final Setting<List<String>> SETTING_DOMAIN_ALLOWLIST = Setting.stringListSetting(
6064
"xpack.notification.email.account.domain_allowlist",
61-
List.of("*"),
65+
ALLOW_ALL_DEFAULT,
66+
new Setting.Validator<>() {
67+
@Override
68+
public void validate(List<String> value) {
69+
// Ignored
70+
}
71+
72+
@Override
73+
@SuppressWarnings("unchecked")
74+
public void validate(List<String> value, Map<Setting<?>, Object> settings) {
75+
List<String> recipientAllowPatterns = (List<String>) settings.get(SETTING_RECIPIENT_ALLOW_PATTERNS);
76+
if (value.equals(ALLOW_ALL_DEFAULT) == false && recipientAllowPatterns.equals(ALLOW_ALL_DEFAULT) == false) {
77+
throw new IllegalArgumentException(
78+
"Cannot set both ["
79+
+ SETTING_RECIPIENT_ALLOW_PATTERNS.getKey()
80+
+ "] and ["
81+
+ SETTING_DOMAIN_ALLOWLIST.getKey()
82+
+ "] to a non [\"*\"] value at the same time."
83+
);
84+
}
85+
}
86+
87+
@Override
88+
public Iterator<Setting<?>> settings() {
89+
List<Setting<?>> settingRecipientAllowPatterns = List.of(SETTING_RECIPIENT_ALLOW_PATTERNS);
90+
return settingRecipientAllowPatterns.iterator();
91+
}
92+
},
93+
Property.Dynamic,
94+
Property.NodeScope
95+
);
96+
97+
private static final Setting<List<String>> SETTING_RECIPIENT_ALLOW_PATTERNS = Setting.stringListSetting(
98+
"xpack.notification.email.recipient_allowlist",
99+
ALLOW_ALL_DEFAULT,
100+
new Setting.Validator<>() {
101+
@Override
102+
public void validate(List<String> value) {
103+
// Ignored
104+
}
105+
106+
@Override
107+
@SuppressWarnings("unchecked")
108+
public void validate(List<String> value, Map<Setting<?>, Object> settings) {
109+
List<String> domainAllowList = (List<String>) settings.get(SETTING_DOMAIN_ALLOWLIST);
110+
if (value.equals(ALLOW_ALL_DEFAULT) == false && domainAllowList.equals(ALLOW_ALL_DEFAULT) == false) {
111+
throw new IllegalArgumentException(
112+
"Connect set both ["
113+
+ SETTING_RECIPIENT_ALLOW_PATTERNS.getKey()
114+
+ "] and ["
115+
+ SETTING_DOMAIN_ALLOWLIST.getKey()
116+
+ "] to a non [\"*\"] value at the same time."
117+
);
118+
}
119+
}
120+
121+
@Override
122+
public Iterator<Setting<?>> settings() {
123+
List<Setting<?>> settingDomainAllowlist = List.of(SETTING_DOMAIN_ALLOWLIST);
124+
return settingDomainAllowlist.iterator();
125+
}
126+
},
62127
Property.Dynamic,
63128
Property.NodeScope
64129
);
@@ -167,6 +232,7 @@ public class EmailService extends NotificationService<Account> {
167232
private final CryptoService cryptoService;
168233
private final SSLService sslService;
169234
private volatile Set<String> allowedDomains;
235+
private volatile Set<String> allowedRecipientPatterns;
170236

171237
@SuppressWarnings("this-escape")
172238
public EmailService(Settings settings, @Nullable CryptoService cryptoService, SSLService sslService, ClusterSettings clusterSettings) {
@@ -192,7 +258,9 @@ public EmailService(Settings settings, @Nullable CryptoService cryptoService, SS
192258
clusterSettings.addAffixUpdateConsumer(SETTING_SMTP_SEND_PARTIAL, (s, o) -> {}, (s, o) -> {});
193259
clusterSettings.addAffixUpdateConsumer(SETTING_SMTP_WAIT_ON_QUIT, (s, o) -> {}, (s, o) -> {});
194260
this.allowedDomains = new HashSet<>(SETTING_DOMAIN_ALLOWLIST.get(settings));
261+
this.allowedRecipientPatterns = new HashSet<>(SETTING_RECIPIENT_ALLOW_PATTERNS.get(settings));
195262
clusterSettings.addSettingsUpdateConsumer(SETTING_DOMAIN_ALLOWLIST, this::updateAllowedDomains);
263+
clusterSettings.addSettingsUpdateConsumer(SETTING_RECIPIENT_ALLOW_PATTERNS, this::updateAllowedRecipientPatterns);
196264
// do an initial load
197265
reload(settings);
198266
}
@@ -201,6 +269,10 @@ void updateAllowedDomains(List<String> newDomains) {
201269
this.allowedDomains = new HashSet<>(newDomains);
202270
}
203271

272+
void updateAllowedRecipientPatterns(List<String> newPatterns) {
273+
this.allowedRecipientPatterns = new HashSet<>(newPatterns);
274+
}
275+
204276
@Override
205277
protected Account createAccount(String name, Settings accountSettings) {
206278
Account.Config config = new Account.Config(name, accountSettings, getSmtpSslSocketFactory(), logger);
@@ -228,46 +300,77 @@ public EmailSent send(Email email, Authentication auth, Profile profile, String
228300
"failed to send email with subject ["
229301
+ email.subject()
230302
+ "] and recipient domains "
231-
+ getRecipientDomains(email)
303+
+ getRecipients(email, true)
232304
+ ", one or more recipients is not specified in the domain allow list setting ["
233305
+ SETTING_DOMAIN_ALLOWLIST.getKey()
234306
+ "]."
235307
);
236308
}
309+
if (recipientAddressInAllowList(email, this.allowedRecipientPatterns) == false) {
310+
throw new IllegalArgumentException(
311+
"failed to send email with subject ["
312+
+ email.subject()
313+
+ "] and recipients "
314+
+ getRecipients(email, false)
315+
+ ", one or more recipients is not specified in the domain allow list setting ["
316+
+ SETTING_RECIPIENT_ALLOW_PATTERNS.getKey()
317+
+ "]."
318+
);
319+
}
237320
return send(email, auth, profile, account);
238321
}
239322

240323
// Visible for testing
241-
static Set<String> getRecipientDomains(Email email) {
242-
return Stream.concat(
324+
static Set<String> getRecipients(Email email, boolean domainsOnly) {
325+
var stream = Stream.concat(
243326
Optional.ofNullable(email.to()).map(addrs -> Arrays.stream(addrs.toArray())).orElse(Stream.empty()),
244327
Stream.concat(
245328
Optional.ofNullable(email.cc()).map(addrs -> Arrays.stream(addrs.toArray())).orElse(Stream.empty()),
246329
Optional.ofNullable(email.bcc()).map(addrs -> Arrays.stream(addrs.toArray())).orElse(Stream.empty())
247330
)
248-
)
249-
.map(InternetAddress::getAddress)
250-
// Pull out only the domain of the email address, so [email protected] -> bar.com
251-
.map(emailAddress -> emailAddress.substring(emailAddress.lastIndexOf('@') + 1))
252-
.collect(Collectors.toSet());
331+
).map(InternetAddress::getAddress);
332+
333+
if (domainsOnly) {
334+
// Pull out only the domain of the email address, so [email protected] becomes bar.com
335+
stream = stream.map(emailAddress -> emailAddress.substring(emailAddress.lastIndexOf('@') + 1));
336+
}
337+
338+
return stream.collect(Collectors.toSet());
253339
}
254340

255341
// Visible for testing
256342
static boolean recipientDomainsInAllowList(Email email, Set<String> allowedDomainSet) {
257-
if (allowedDomainSet.size() == 0) {
343+
if (allowedDomainSet.isEmpty()) {
258344
// Nothing is allowed
259345
return false;
260346
}
261347
if (allowedDomainSet.contains("*")) {
262348
// Don't bother checking, because there is a wildcard all
263349
return true;
264350
}
265-
final Set<String> domains = getRecipientDomains(email);
351+
final Set<String> domains = getRecipients(email, true);
266352
final Predicate<String> matchesAnyAllowedDomain = domain -> allowedDomainSet.stream()
267353
.anyMatch(allowedDomain -> Regex.simpleMatch(allowedDomain, domain, true));
268354
return domains.stream().allMatch(matchesAnyAllowedDomain);
269355
}
270356

357+
// Visible for testing
358+
static boolean recipientAddressInAllowList(Email email, Set<String> allowedRecipientPatterns) {
359+
if (allowedRecipientPatterns.isEmpty()) {
360+
// Nothing is allowed
361+
return false;
362+
}
363+
if (allowedRecipientPatterns.contains("*")) {
364+
// Don't bother checking, because there is a wildcard all
365+
return true;
366+
}
367+
368+
final Set<String> recipients = getRecipients(email, false);
369+
final Predicate<String> matchesAnyAllowedRecipient = recipient -> allowedRecipientPatterns.stream()
370+
.anyMatch(pattern -> Regex.simpleMatch(pattern, recipient, true));
371+
return recipients.stream().allMatch(matchesAnyAllowedRecipient);
372+
}
373+
271374
private static EmailSent send(Email email, Authentication auth, Profile profile, Account account) throws MessagingException {
272375
assert account != null;
273376
try {
@@ -304,6 +407,7 @@ private static List<Setting<?>> getDynamicSettings() {
304407
return Arrays.asList(
305408
SETTING_DEFAULT_ACCOUNT,
306409
SETTING_DOMAIN_ALLOWLIST,
410+
SETTING_RECIPIENT_ALLOW_PATTERNS,
307411
SETTING_PROFILE,
308412
SETTING_EMAIL_DEFAULTS,
309413
SETTING_SMTP_AUTH,

0 commit comments

Comments
 (0)