Skip to content

Commit 0f16691

Browse files
Fix Watcher NotificationService's secure settings (#35610)
The NotificationService (base class for SlackService, HipchatService ...) has both dynamic cluster settings and SecureSettings and builds the clients (Account) that are used to comm with external services. This commit fixes an important bug about updating/reloading any of these settings (both Secure and dynamic cluster). Briefly the bug is due to the fact that both the secure settings as well as the dynamic node scoped ones can be updated independently, but when constructing the clients some of the settings might not be visible.
1 parent 0b939cb commit 0f16691

File tree

7 files changed

+224
-54
lines changed

7 files changed

+224
-54
lines changed

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

Lines changed: 147 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,23 @@
55
*/
66
package org.elasticsearch.xpack.watcher.notification;
77

8-
import org.elasticsearch.common.collect.Tuple;
98
import org.elasticsearch.common.component.AbstractComponent;
9+
import org.elasticsearch.common.Nullable;
1010
import org.elasticsearch.common.settings.ClusterSettings;
11+
import org.elasticsearch.common.settings.SecureSettings;
12+
import org.elasticsearch.common.settings.SecureString;
1113
import org.elasticsearch.common.settings.Setting;
1214
import org.elasticsearch.common.settings.Settings;
1315
import org.elasticsearch.common.settings.SettingsException;
1416

17+
import java.io.IOException;
18+
import java.io.InputStream;
19+
import java.security.GeneralSecurityException;
1520
import java.util.Collections;
1621
import java.util.HashMap;
1722
import java.util.List;
1823
import java.util.Map;
24+
import java.util.Set;
1925
import java.util.function.BiFunction;
2026

2127
/**
@@ -24,25 +30,68 @@
2430
public abstract class NotificationService<Account> extends AbstractComponent {
2531

2632
private final String type;
27-
// both are guarded by this
28-
private Map<String, Account> accounts;
29-
private Account defaultAccount;
30-
31-
public NotificationService(String type,
32-
ClusterSettings clusterSettings, List<Setting<?>> pluginSettings) {
33-
this(type);
34-
clusterSettings.addSettingsUpdateConsumer(this::reload, pluginSettings);
33+
private final Settings bootSettings;
34+
private final List<Setting<?>> pluginSecureSettings;
35+
// all are guarded by this
36+
private volatile Map<String, Account> accounts;
37+
private volatile Account defaultAccount;
38+
// cached cluster setting, required when recreating the notification clients
39+
// using the new "reloaded" secure settings
40+
private volatile Settings cachedClusterSettings;
41+
// cached secure settings, required when recreating the notification clients
42+
// using the new updated cluster settings
43+
private volatile SecureSettings cachedSecureSettings;
44+
45+
public NotificationService(String type, Settings settings, ClusterSettings clusterSettings, List<Setting<?>> pluginDynamicSettings,
46+
List<Setting<?>> pluginSecureSettings) {
47+
this(type, settings, pluginSecureSettings);
48+
// register a grand updater for the whole group, as settings are usable together
49+
clusterSettings.addSettingsUpdateConsumer(this::clusterSettingsConsumer, pluginDynamicSettings);
3550
}
3651

3752
// Used for testing only
38-
NotificationService(String type) {
53+
NotificationService(String type, Settings settings, List<Setting<?>> pluginSecureSettings) {
3954
this.type = type;
55+
this.bootSettings = settings;
56+
this.pluginSecureSettings = pluginSecureSettings;
57+
}
58+
59+
private synchronized void clusterSettingsConsumer(Settings settings) {
60+
// update cached cluster settings
61+
this.cachedClusterSettings = settings;
62+
// use these new dynamic cluster settings together with the previously cached
63+
// secure settings
64+
buildAccounts();
4065
}
4166

4267
public synchronized void reload(Settings settings) {
43-
Tuple<Map<String, Account>, Account> accounts = buildAccounts(settings, this::createAccount);
44-
this.accounts = Collections.unmodifiableMap(accounts.v1());
45-
this.defaultAccount = accounts.v2();
68+
// `SecureSettings` are available here! cache them as they will be needed
69+
// whenever dynamic cluster settings change and we have to rebuild the accounts
70+
try {
71+
this.cachedSecureSettings = extractSecureSettings(settings, pluginSecureSettings);
72+
} catch (GeneralSecurityException e) {
73+
logger.error("Keystore exception while reloading watcher notification service", e);
74+
return;
75+
}
76+
// use these new secure settings together with the previously cached dynamic
77+
// cluster settings
78+
buildAccounts();
79+
}
80+
81+
private void buildAccounts() {
82+
// build complete settings combining cluster and secure settings
83+
final Settings.Builder completeSettingsBuilder = Settings.builder().put(bootSettings, false);
84+
if (this.cachedClusterSettings != null) {
85+
completeSettingsBuilder.put(this.cachedClusterSettings, false);
86+
}
87+
if (this.cachedSecureSettings != null) {
88+
completeSettingsBuilder.setSecureSettings(this.cachedSecureSettings);
89+
}
90+
final Settings completeSettings = completeSettingsBuilder.build();
91+
// obtain account names and create accounts
92+
final Set<String> accountNames = getAccountNames(completeSettings);
93+
this.accounts = createAccounts(completeSettings, accountNames, this::createAccount);
94+
this.defaultAccount = findDefaultAccountOrNull(completeSettings, this.accounts);
4695
}
4796

4897
protected abstract Account createAccount(String name, Settings accountSettings);
@@ -67,31 +116,100 @@ public Account getAccount(String name) {
67116
return theAccount;
68117
}
69118

70-
private <A> Tuple<Map<String, A>, A> buildAccounts(Settings settings, BiFunction<String, Settings, A> accountFactory) {
71-
Settings accountsSettings = settings.getByPrefix("xpack.notification." + type + ".").getAsSettings("account");
72-
Map<String, A> accounts = new HashMap<>();
73-
for (String name : accountsSettings.names()) {
74-
Settings accountSettings = accountsSettings.getAsSettings(name);
75-
A account = accountFactory.apply(name, accountSettings);
76-
accounts.put(name, account);
119+
private String getNotificationsAccountPrefix() {
120+
return "xpack.notification." + type + ".account.";
121+
}
122+
123+
private Set<String> getAccountNames(Settings settings) {
124+
// secure settings are not responsible for the client names
125+
final Settings noSecureSettings = Settings.builder().put(settings, false).build();
126+
return noSecureSettings.getByPrefix(getNotificationsAccountPrefix()).names();
127+
}
128+
129+
private @Nullable String getDefaultAccountName(Settings settings) {
130+
return settings.get("xpack.notification." + type + ".default_account");
131+
}
132+
133+
private Map<String, Account> createAccounts(Settings settings, Set<String> accountNames,
134+
BiFunction<String, Settings, Account> accountFactory) {
135+
final Map<String, Account> accounts = new HashMap<>();
136+
for (final String accountName : accountNames) {
137+
final Settings accountSettings = settings.getAsSettings(getNotificationsAccountPrefix() + accountName);
138+
final Account account = accountFactory.apply(accountName, accountSettings);
139+
accounts.put(accountName, account);
77140
}
141+
return Collections.unmodifiableMap(accounts);
142+
}
78143

79-
final String defaultAccountName = settings.get("xpack.notification." + type + ".default_account");
80-
A defaultAccount;
144+
private @Nullable Account findDefaultAccountOrNull(Settings settings, Map<String, Account> accounts) {
145+
final String defaultAccountName = getDefaultAccountName(settings);
81146
if (defaultAccountName == null) {
82147
if (accounts.isEmpty()) {
83-
defaultAccount = null;
148+
return null;
84149
} else {
85-
A account = accounts.values().iterator().next();
86-
defaultAccount = account;
87-
150+
return accounts.values().iterator().next();
88151
}
89152
} else {
90-
defaultAccount = accounts.get(defaultAccountName);
91-
if (defaultAccount == null) {
153+
final Account account = accounts.get(defaultAccountName);
154+
if (account == null) {
92155
throw new SettingsException("could not find default account [" + defaultAccountName + "]");
93156
}
157+
return account;
158+
}
159+
}
160+
161+
/**
162+
* Extracts the {@link SecureSettings}` out of the passed in {@link Settings} object. The {@code Setting} argument has to have the
163+
* {@code SecureSettings} open/available. Normally {@code SecureSettings} are available only under specific callstacks (eg. during node
164+
* initialization or during a `reload` call). The returned copy can be reused freely as it will never be closed (this is a bit of
165+
* cheating, but it is necessary in this specific circumstance). Only works for secure settings of type string (not file).
166+
*
167+
* @param source
168+
* A {@code Settings} object with its {@code SecureSettings} open/available.
169+
* @param securePluginSettings
170+
* The list of settings to copy.
171+
* @return A copy of the {@code SecureSettings} of the passed in {@code Settings} argument.
172+
*/
173+
private static SecureSettings extractSecureSettings(Settings source, List<Setting<?>> securePluginSettings)
174+
throws GeneralSecurityException {
175+
// get the secure settings out
176+
final SecureSettings sourceSecureSettings = Settings.builder().put(source, true).getSecureSettings();
177+
// filter and cache them...
178+
final Map<String, SecureString> cache = new HashMap<>();
179+
if (sourceSecureSettings != null && securePluginSettings != null) {
180+
for (final String settingKey : sourceSecureSettings.getSettingNames()) {
181+
for (final Setting<?> secureSetting : securePluginSettings) {
182+
if (secureSetting.match(settingKey)) {
183+
cache.put(settingKey, sourceSecureSettings.getString(settingKey));
184+
}
185+
}
186+
}
94187
}
95-
return new Tuple<>(accounts, defaultAccount);
188+
return new SecureSettings() {
189+
190+
@Override
191+
public boolean isLoaded() {
192+
return true;
193+
}
194+
195+
@Override
196+
public SecureString getString(String setting) throws GeneralSecurityException {
197+
return cache.get(setting);
198+
}
199+
200+
@Override
201+
public Set<String> getSettingNames() {
202+
return cache.keySet();
203+
}
204+
205+
@Override
206+
public InputStream getFile(String setting) throws GeneralSecurityException {
207+
throw new IllegalStateException("A NotificationService setting cannot be File.");
208+
}
209+
210+
@Override
211+
public void close() throws IOException {
212+
}
213+
};
96214
}
97215
}

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

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
import org.elasticsearch.xpack.watcher.notification.NotificationService;
1818

1919
import javax.mail.MessagingException;
20+
21+
import java.util.ArrayList;
2022
import java.util.Arrays;
2123
import java.util.List;
2224

@@ -104,7 +106,7 @@ public class EmailService extends NotificationService<Account> {
104106
private final CryptoService cryptoService;
105107

106108
public EmailService(Settings settings, @Nullable CryptoService cryptoService, ClusterSettings clusterSettings) {
107-
super("email", clusterSettings, EmailService.getSettings());
109+
super("email", settings, clusterSettings, EmailService.getDynamicSettings(), EmailService.getSecureSettings());
108110
this.cryptoService = cryptoService;
109111
// ensure logging of setting changes
110112
clusterSettings.addSettingsUpdateConsumer(SETTING_DEFAULT_ACCOUNT, (s) -> {});
@@ -117,7 +119,6 @@ public EmailService(Settings settings, @Nullable CryptoService cryptoService, Cl
117119
clusterSettings.addAffixUpdateConsumer(SETTING_SMTP_PORT, (s, o) -> {}, (s, o) -> {});
118120
clusterSettings.addAffixUpdateConsumer(SETTING_SMTP_USER, (s, o) -> {}, (s, o) -> {});
119121
clusterSettings.addAffixUpdateConsumer(SETTING_SMTP_PASSWORD, (s, o) -> {}, (s, o) -> {});
120-
clusterSettings.addAffixUpdateConsumer(SETTING_SECURE_PASSWORD, (s, o) -> {}, (s, o) -> {});
121122
clusterSettings.addAffixUpdateConsumer(SETTING_SMTP_TIMEOUT, (s, o) -> {}, (s, o) -> {});
122123
clusterSettings.addAffixUpdateConsumer(SETTING_SMTP_CONNECTION_TIMEOUT, (s, o) -> {}, (s, o) -> {});
123124
clusterSettings.addAffixUpdateConsumer(SETTING_SMTP_WRITE_TIMEOUT, (s, o) -> {}, (s, o) -> {});
@@ -175,12 +176,21 @@ public Email email() {
175176
}
176177
}
177178

178-
public static List<Setting<?>> getSettings() {
179+
private static List<Setting<?>> getDynamicSettings() {
179180
return Arrays.asList(SETTING_DEFAULT_ACCOUNT, SETTING_PROFILE, SETTING_EMAIL_DEFAULTS, SETTING_SMTP_AUTH, SETTING_SMTP_HOST,
180181
SETTING_SMTP_PASSWORD, SETTING_SMTP_PORT, SETTING_SMTP_STARTTLS_ENABLE, SETTING_SMTP_USER, SETTING_SMTP_STARTTLS_REQUIRED,
181182
SETTING_SMTP_TIMEOUT, SETTING_SMTP_CONNECTION_TIMEOUT, SETTING_SMTP_WRITE_TIMEOUT, SETTING_SMTP_LOCAL_ADDRESS,
182-
SETTING_SMTP_LOCAL_PORT, SETTING_SMTP_SEND_PARTIAL, SETTING_SMTP_WAIT_ON_QUIT, SETTING_SMTP_SSL_TRUST_ADDRESS,
183-
SETTING_SECURE_PASSWORD);
183+
SETTING_SMTP_LOCAL_PORT, SETTING_SMTP_SEND_PARTIAL, SETTING_SMTP_WAIT_ON_QUIT, SETTING_SMTP_SSL_TRUST_ADDRESS);
184+
}
185+
186+
private static List<Setting<?>> getSecureSettings() {
187+
return Arrays.asList(SETTING_SECURE_PASSWORD);
188+
}
189+
190+
public static List<Setting<?>> getSettings() {
191+
List<Setting<?>> allSettings = new ArrayList<Setting<?>>(EmailService.getDynamicSettings());
192+
allSettings.addAll(EmailService.getSecureSettings());
193+
return allSettings;
184194
}
185195

186196
}

x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/notification/hipchat/HipChatService.java

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import org.elasticsearch.xpack.watcher.common.http.HttpClient;
1515
import org.elasticsearch.xpack.watcher.notification.NotificationService;
1616

17+
import java.util.ArrayList;
1718
import java.util.Arrays;
1819
import java.util.List;
1920

@@ -64,20 +65,19 @@ public class HipChatService extends NotificationService<HipChatAccount> {
6465
private HipChatServer defaultServer;
6566

6667
public HipChatService(Settings settings, HttpClient httpClient, ClusterSettings clusterSettings) {
67-
super("hipchat", clusterSettings, HipChatService.getSettings());
68+
super("hipchat", settings, clusterSettings, HipChatService.getDynamicSettings(), HipChatService.getSecureSettings());
6869
this.httpClient = httpClient;
6970
// ensure logging of setting changes
7071
clusterSettings.addSettingsUpdateConsumer(SETTING_DEFAULT_ACCOUNT, (s) -> {});
7172
clusterSettings.addSettingsUpdateConsumer(SETTING_DEFAULT_HOST, (s) -> {});
7273
clusterSettings.addSettingsUpdateConsumer(SETTING_DEFAULT_PORT, (s) -> {});
7374
clusterSettings.addAffixUpdateConsumer(SETTING_AUTH_TOKEN, (s, o) -> {}, (s, o) -> {});
74-
clusterSettings.addAffixUpdateConsumer(SETTING_AUTH_TOKEN_SECURE, (s, o) -> {}, (s, o) -> {});
7575
clusterSettings.addAffixUpdateConsumer(SETTING_PROFILE, (s, o) -> {}, (s, o) -> {});
7676
clusterSettings.addAffixUpdateConsumer(SETTING_ROOM, (s, o) -> {}, (s, o) -> {});
7777
clusterSettings.addAffixUpdateConsumer(SETTING_HOST, (s, o) -> {}, (s, o) -> {});
7878
clusterSettings.addAffixUpdateConsumer(SETTING_PORT, (s, o) -> {}, (s, o) -> {});
7979
clusterSettings.addAffixUpdateConsumer(SETTING_MESSAGE_DEFAULTS, (s, o) -> {}, (s, o) -> {});
80-
80+
// do an initial load
8181
reload(settings);
8282
}
8383

@@ -96,8 +96,18 @@ protected HipChatAccount createAccount(String name, Settings accountSettings) {
9696
return profile.createAccount(name, accountSettings, defaultServer, httpClient, logger);
9797
}
9898

99+
private static List<Setting<?>> getDynamicSettings() {
100+
return Arrays.asList(SETTING_DEFAULT_ACCOUNT, SETTING_AUTH_TOKEN, SETTING_PROFILE, SETTING_ROOM, SETTING_MESSAGE_DEFAULTS,
101+
SETTING_DEFAULT_HOST, SETTING_DEFAULT_PORT, SETTING_HOST, SETTING_PORT);
102+
}
103+
104+
private static List<Setting<?>> getSecureSettings() {
105+
return Arrays.asList(SETTING_AUTH_TOKEN_SECURE);
106+
}
107+
99108
public static List<Setting<?>> getSettings() {
100-
return Arrays.asList(SETTING_DEFAULT_ACCOUNT, SETTING_AUTH_TOKEN, SETTING_AUTH_TOKEN_SECURE, SETTING_PROFILE, SETTING_ROOM,
101-
SETTING_MESSAGE_DEFAULTS, SETTING_DEFAULT_HOST, SETTING_DEFAULT_PORT, SETTING_HOST, SETTING_PORT);
109+
List<Setting<?>> allSettings = new ArrayList<Setting<?>>(getDynamicSettings());
110+
allSettings.addAll(getSecureSettings());
111+
return allSettings;
102112
}
103113
}

x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/notification/jira/JiraService.java

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import org.elasticsearch.xpack.watcher.common.http.HttpClient;
1515
import org.elasticsearch.xpack.watcher.notification.NotificationService;
1616

17+
import java.util.ArrayList;
1718
import java.util.Arrays;
1819
import java.util.List;
1920

@@ -62,17 +63,14 @@ public class JiraService extends NotificationService<JiraAccount> {
6263
private final HttpClient httpClient;
6364

6465
public JiraService(Settings settings, HttpClient httpClient, ClusterSettings clusterSettings) {
65-
super("jira", clusterSettings, JiraService.getSettings());
66+
super("jira", settings, clusterSettings, JiraService.getDynamicSettings(), JiraService.getSecureSettings());
6667
this.httpClient = httpClient;
6768
// ensure logging of setting changes
6869
clusterSettings.addSettingsUpdateConsumer(SETTING_DEFAULT_ACCOUNT, (s) -> {});
6970
clusterSettings.addAffixUpdateConsumer(SETTING_ALLOW_HTTP, (s, o) -> {}, (s, o) -> {});
7071
clusterSettings.addAffixUpdateConsumer(SETTING_URL, (s, o) -> {}, (s, o) -> {});
7172
clusterSettings.addAffixUpdateConsumer(SETTING_USER, (s, o) -> {}, (s, o) -> {});
7273
clusterSettings.addAffixUpdateConsumer(SETTING_PASSWORD, (s, o) -> {}, (s, o) -> {});
73-
clusterSettings.addAffixUpdateConsumer(SETTING_SECURE_USER, (s, o) -> {}, (s, o) -> {});
74-
clusterSettings.addAffixUpdateConsumer(SETTING_SECURE_URL, (s, o) -> {}, (s, o) -> {});
75-
clusterSettings.addAffixUpdateConsumer(SETTING_SECURE_PASSWORD, (s, o) -> {}, (s, o) -> {});
7674
clusterSettings.addAffixUpdateConsumer(SETTING_DEFAULTS, (s, o) -> {}, (s, o) -> {});
7775
// do an initial load
7876
reload(settings);
@@ -83,8 +81,17 @@ protected JiraAccount createAccount(String name, Settings settings) {
8381
return new JiraAccount(name, settings, httpClient);
8482
}
8583

84+
private static List<Setting<?>> getDynamicSettings() {
85+
return Arrays.asList(SETTING_DEFAULT_ACCOUNT, SETTING_ALLOW_HTTP, SETTING_URL, SETTING_USER, SETTING_PASSWORD, SETTING_DEFAULTS);
86+
}
87+
88+
private static List<Setting<?>> getSecureSettings() {
89+
return Arrays.asList(SETTING_SECURE_USER, SETTING_SECURE_PASSWORD, SETTING_SECURE_URL);
90+
}
91+
8692
public static List<Setting<?>> getSettings() {
87-
return Arrays.asList(SETTING_ALLOW_HTTP, SETTING_URL, SETTING_USER, SETTING_PASSWORD, SETTING_SECURE_USER,
88-
SETTING_SECURE_PASSWORD, SETTING_SECURE_URL, SETTING_DEFAULTS, SETTING_DEFAULT_ACCOUNT);
93+
List<Setting<?>> allSettings = new ArrayList<Setting<?>>(getDynamicSettings());
94+
allSettings.addAll(getSecureSettings());
95+
return allSettings;
8996
}
9097
}

0 commit comments

Comments
 (0)