55 */
66package org .elasticsearch .xpack .watcher .notification ;
77
8- import org .elasticsearch .common .collect .Tuple ;
98import org .elasticsearch .common .component .AbstractComponent ;
9+ import org .elasticsearch .common .Nullable ;
1010import org .elasticsearch .common .settings .ClusterSettings ;
11+ import org .elasticsearch .common .settings .SecureSettings ;
12+ import org .elasticsearch .common .settings .SecureString ;
1113import org .elasticsearch .common .settings .Setting ;
1214import org .elasticsearch .common .settings .Settings ;
1315import org .elasticsearch .common .settings .SettingsException ;
1416
17+ import java .io .IOException ;
18+ import java .io .InputStream ;
19+ import java .security .GeneralSecurityException ;
1520import java .util .Collections ;
1621import java .util .HashMap ;
1722import java .util .List ;
1823import java .util .Map ;
24+ import java .util .Set ;
1925import java .util .function .BiFunction ;
2026
2127/**
2430public 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}
0 commit comments