Skip to content

Commit cf514c0

Browse files
author
Nelson Ochoa
committed
feat: recover from configuration failures
1 parent 95e4fb3 commit cf514c0

File tree

5 files changed

+183
-42
lines changed

5 files changed

+183
-42
lines changed

src/integrationtests/java/com/aws/greengrass/integrationtests/certificateauthority/CustomCaConfigurationTest.java

+63-6
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import com.aws.greengrass.clientdevices.auth.connectivity.CISShadowMonitor;
1919
import com.aws.greengrass.clientdevices.auth.exception.CertificateChainLoadingException;
2020
import com.aws.greengrass.clientdevices.auth.exception.CertificateGenerationException;
21+
import com.aws.greengrass.clientdevices.auth.exception.InvalidConfigurationException;
2122
import com.aws.greengrass.clientdevices.auth.helpers.CertificateTestHelpers;
2223
import com.aws.greengrass.config.Topics;
2324
import com.aws.greengrass.dependency.State;
@@ -52,16 +53,19 @@
5253
import java.net.URISyntaxException;
5354
import java.nio.file.Path;
5455
import java.security.KeyPair;
56+
import java.security.KeyStoreException;
5557
import java.security.NoSuchAlgorithmException;
5658
import java.security.cert.CertificateException;
5759
import java.security.cert.X509Certificate;
5860
import java.util.Collections;
5961
import java.util.List;
62+
import java.util.Objects;
6063
import java.util.concurrent.CompletableFuture;
6164
import java.util.concurrent.CountDownLatch;
6265
import java.util.concurrent.ExecutionException;
6366
import java.util.concurrent.TimeUnit;
6467
import java.util.concurrent.TimeoutException;
68+
import java.util.concurrent.atomic.AtomicBoolean;
6569
import java.util.concurrent.atomic.AtomicReference;
6670
import java.util.function.Consumer;
6771

@@ -74,10 +78,12 @@
7478
import static org.hamcrest.Matchers.is;
7579
import static org.junit.jupiter.api.Assertions.assertEquals;
7680
import static org.junit.jupiter.api.Assertions.assertTrue;
81+
import static org.mockito.ArgumentMatchers.any;
7782
import static org.mockito.Mockito.atLeastOnce;
7883
import static org.mockito.Mockito.doReturn;
7984
import static org.mockito.Mockito.lenient;
8085
import static org.mockito.Mockito.spy;
86+
import static org.mockito.Mockito.times;
8187
import static org.mockito.Mockito.verify;
8288
import static org.mockito.Mockito.when;
8389

@@ -128,21 +134,29 @@ void cleanup() {
128134
kernel.shutdown();
129135
}
130136

131-
// TODO: Consolidate this test helpers with ClientDevicesAuthServiceTest
132-
private void givenNucleusRunningWithConfig(String configFileName) throws InterruptedException {
137+
private void givenNucleusRunningWithConfig(String configFileName, Consumer<State> consumer) throws InterruptedException {
133138
CountDownLatch authServiceRunning = new CountDownLatch(1);
134139
kernel.parseArgs("-r", rootDir.toAbsolutePath().toString(), "-i",
135-
getClass().getResource(configFileName).toString());
140+
Objects.requireNonNull(getClass().getResource(configFileName)).toString());
136141
kernel.getContext().addGlobalStateChangeListener((service, was, newState) -> {
137-
if (ClientDevicesAuthService.CLIENT_DEVICES_AUTH_SERVICE_NAME.equals(service.getName()) && service.getState()
138-
.equals(State.RUNNING)) {
139-
authServiceRunning.countDown();
142+
if (ClientDevicesAuthService.CLIENT_DEVICES_AUTH_SERVICE_NAME.equals(service.getName())) {
143+
State serviceState = service.getState();
144+
consumer.accept(serviceState);
145+
146+
if (serviceState.equals(State.RUNNING)) {
147+
authServiceRunning.countDown();
148+
}
140149
}
141150
});
142151
kernel.launch();
152+
143153
assertThat(authServiceRunning.await(30L, TimeUnit.SECONDS), is(true));
144154
}
145155

156+
private void givenNucleusRunningWithConfig(String configFileName) throws InterruptedException {
157+
givenNucleusRunningWithConfig(configFileName, (State s) -> {});
158+
}
159+
146160
private static Pair<X509Certificate[], KeyPair[]> givenRootAndIntermediateCA() throws NoSuchAlgorithmException,
147161
CertificateException,
148162
OperatorCreationException, CertIOException {
@@ -317,4 +331,47 @@ void GIVEN_managedCAConfiguration_WHEN_updatedToCustomCAConfiguration_THEN_serve
317331
CertificateUpdateEvent event = eventRef.get();
318332
assertTrue(CertificateTestHelpers.wasCertificateIssuedBy(intermediateCA, event.getCertificate()));
319333
}
334+
335+
@Test
336+
void GIVEN_invalidConfigServiceBroken_WHEN_whenCorrected_THEN_serviceCanRecover(ExtensionContext context)
337+
throws CertificateException, NoSuchAlgorithmException, OperatorCreationException, CertIOException,
338+
URISyntaxException, InterruptedException, KeyLoadingException, ServiceUnavailableException,
339+
CertificateChainLoadingException, ServiceLoadException, KeyStoreException {
340+
ignoreExceptionOfType(context, InvalidConfigurationException.class);
341+
ignoreExceptionOfType(context, URISyntaxException.class);
342+
Pair<X509Certificate[], KeyPair[]> credentials = givenRootAndIntermediateCA();
343+
X509Certificate[] chain = credentials.getLeft();
344+
KeyPair[] certificateKeys = credentials.getRight();
345+
KeyPair intermediateKeyPair = certificateKeys[0];
346+
347+
CountDownLatch authServiceBroken = new CountDownLatch(1);
348+
CountDownLatch recoveredFromBroken = new CountDownLatch(1);
349+
AtomicBoolean wasBroken = new AtomicBoolean(false);
350+
Consumer<State> serviceStateChangeListener = (State s) -> {
351+
if (s.equals(State.BROKEN)) {
352+
wasBroken.getAndSet(true);
353+
authServiceBroken.countDown();
354+
}
355+
356+
if (wasBroken.get() && s.equals(State.RUNNING)) {
357+
recoveredFromBroken.countDown();
358+
}
359+
};
360+
361+
362+
givenNucleusRunningWithConfig("config.yaml", serviceStateChangeListener);
363+
verify(certificateStoreSpy, times(1)).setCaKeyAndCertificateChain(any(), any(), any());
364+
365+
// Do enough bad operations until the service goes belly up
366+
givenCDAWithCustomCertificateAuthority(new URI("file:///private.key"), new URI(""));
367+
assertThat(authServiceBroken.await(10L, TimeUnit.SECONDS), is(true));
368+
369+
// Do the right thing
370+
URI privateKeyUri = new URI("file:///private.key");
371+
URI certificateUri = new URI("file:///certificate.pem");
372+
when(securityServiceMock.getKeyPair(privateKeyUri, certificateUri)).thenReturn(intermediateKeyPair);
373+
doReturn(chain).when(certificateStoreSpy).loadCaCertificateChain(privateKeyUri, certificateUri);
374+
givenCDAWithCustomCertificateAuthority(privateKeyUri, certificateUri);
375+
assertThat(recoveredFromBroken.await(10L, TimeUnit.SECONDS), is(true));
376+
}
320377
}

src/main/java/com/aws/greengrass/clientdevices/auth/ClientDevicesAuthService.java

+32-2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import com.aws.greengrass.clientdevices.auth.configuration.GroupConfiguration;
1717
import com.aws.greengrass.clientdevices.auth.configuration.GroupManager;
1818
import com.aws.greengrass.clientdevices.auth.connectivity.CISShadowMonitor;
19+
import com.aws.greengrass.clientdevices.auth.exception.InvalidConfigurationException;
1920
import com.aws.greengrass.clientdevices.auth.infra.NetworkState;
2021
import com.aws.greengrass.clientdevices.auth.session.MqttSessionFactory;
2122
import com.aws.greengrass.clientdevices.auth.session.SessionConfig;
@@ -26,6 +27,7 @@
2627
import com.aws.greengrass.config.Topics;
2728
import com.aws.greengrass.config.WhatHappened;
2829
import com.aws.greengrass.dependency.ImplementsService;
30+
import com.aws.greengrass.dependency.State;
2931
import com.aws.greengrass.ipc.AuthorizeClientDeviceActionOperationHandler;
3032
import com.aws.greengrass.ipc.GetClientDeviceAuthTokenOperationHandler;
3133
import com.aws.greengrass.ipc.SubscribeToCertificateUpdatesOperationHandler;
@@ -71,6 +73,7 @@ public class ClientDevicesAuthService extends PluginService {
7173
private ThreadPoolExecutor cloudCallThreadPool;
7274
private int cloudCallQueueSize;
7375
private CDAConfiguration cdaConfiguration;
76+
private boolean configurationErrored = false;
7477

7578

7679
/**
@@ -136,9 +139,31 @@ private void subscribeToConfigChanges() {
136139
}
137140

138141
private void onConfigurationChanged() {
142+
// Note: The nucleus emits multiple configuration changed events, one per key that changed. It will also
143+
// keep emitting them regardless of the state it is current in. If the configuration was incorrect, we want the
144+
// service to error, but we don't want to check again until the nucleus has run the remediation steps (when the
145+
// service errors, the nucleus will try to call shutdown -> install -> startup).
146+
if (configurationErrored && !inState(State.BROKEN)) {
147+
return;
148+
}
149+
139150
try {
140-
cdaConfiguration = CDAConfiguration.from(cdaConfiguration, getConfig());
141-
} catch (URISyntaxException e) {
151+
CDAConfiguration configuration = CDAConfiguration.from(cdaConfiguration, getConfig());
152+
153+
if (configuration.isEqual(cdaConfiguration)) {
154+
return;
155+
}
156+
157+
cdaConfiguration = configuration;
158+
159+
// Good configuration and was previously broken
160+
if (inState(State.BROKEN)) {
161+
logger.info("Service is {} and configuration changed. Attempting to reinstall.", State.BROKEN);
162+
configurationErrored = false;
163+
requestReinstall();
164+
}
165+
} catch (URISyntaxException | InvalidConfigurationException e) {
166+
configurationErrored = true;
142167
serviceErrored(e);
143168
}
144169
}
@@ -183,6 +208,11 @@ private void configChangeHandler(WhatHappened whatHappened, Node node) {
183208
protected void startup() throws InterruptedException {
184209
context.get(CertificateManager.class).startMonitors();
185210
super.startup();
211+
212+
if (configurationErrored) {
213+
configurationErrored = false;
214+
onConfigurationChanged();
215+
}
186216
}
187217

188218
@Override

src/main/java/com/aws/greengrass/clientdevices/auth/configuration/CAConfiguration.java

+38-3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
package com.aws.greengrass.clientdevices.auth.configuration;
77

88
import com.aws.greengrass.clientdevices.auth.certificate.CertificateStore;
9+
import com.aws.greengrass.clientdevices.auth.exception.InvalidConfigurationException;
910
import com.aws.greengrass.config.Topic;
1011
import com.aws.greengrass.config.Topics;
1112
import com.aws.greengrass.logging.api.Logger;
@@ -16,8 +17,10 @@
1617

1718
import java.net.URI;
1819
import java.net.URISyntaxException;
20+
import java.text.MessageFormat;
1921
import java.util.Collections;
2022
import java.util.List;
23+
import java.util.Objects;
2124
import java.util.Optional;
2225

2326
/**
@@ -60,15 +63,31 @@ private CAConfiguration(List<String> caTypes, CertificateStore.CAType caType,
6063
* @param configurationTopics the configuration key of the service configuration
6164
*
6265
* @throws URISyntaxException if invalid certificateUri or privateKeyUri provided.
66+
* @throws InvalidConfigurationException if provided privateKeyUri but not certificateUri
6367
*/
64-
public static CAConfiguration from(Topics configurationTopics) throws URISyntaxException {
68+
public static CAConfiguration from(Topics configurationTopics) throws URISyntaxException,
69+
InvalidConfigurationException {
6570
Topics certAuthorityTopic = configurationTopics.lookupTopics(CERTIFICATE_AUTHORITY_TOPIC);
6671

72+
Optional<URI> privateKeyUri = getCaPrivateKeyUriFromConfiguration(certAuthorityTopic);
73+
Optional<URI> certificateUri = getCaCertificateUriFromConfiguration(certAuthorityTopic);
74+
75+
if (privateKeyUri.isPresent() != certificateUri.isPresent()) {
76+
throw new InvalidConfigurationException(
77+
MessageFormat.format(
78+
"{0} and {1} must have a value. Provided {0}:{2} and {1}:{3}",
79+
CA_PRIVATE_KEY_URI, CA_CERTIFICATE_URI,
80+
privateKeyUri.orElse(new URI("")),
81+
certificateUri.orElse(new URI(""))
82+
)
83+
);
84+
}
85+
6786
return new CAConfiguration(
6887
getCaTypeListFromConfiguration(configurationTopics),
6988
getCaTypeFromConfiguration(configurationTopics),
70-
getCaPrivateKeyUriFromConfiguration(certAuthorityTopic),
71-
getCaCertificateUriFromConfiguration(certAuthorityTopic)
89+
privateKeyUri,
90+
certificateUri
7291
);
7392
}
7493

@@ -145,4 +164,20 @@ private static Optional<URI> getCaCertificateUriFromConfiguration(Topics certAut
145164

146165
return Optional.of(getUri(certificateUri));
147166
}
167+
168+
/**
169+
* Checks if the CAConfiguration is equal to another.
170+
*
171+
* @param config a CAConfiguration
172+
*/
173+
public boolean isEqual(CAConfiguration config) {
174+
if (config == null) {
175+
return false;
176+
}
177+
178+
return Objects.equals(config.getCertificateUri(), certificateUri)
179+
&& Objects.equals(config.getPrivateKeyUri(), privateKeyUri)
180+
&& Objects.equals(config.getCaType(), caType);
181+
}
182+
148183
}

src/main/java/com/aws/greengrass/clientdevices/auth/configuration/CDAConfiguration.java

+25-21
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@
88
import com.aws.greengrass.clientdevices.auth.api.DomainEvents;
99
import com.aws.greengrass.clientdevices.auth.certificate.CertificateStore;
1010
import com.aws.greengrass.clientdevices.auth.certificate.events.CAConfigurationChanged;
11+
import com.aws.greengrass.clientdevices.auth.exception.InvalidConfigurationException;
1112
import com.aws.greengrass.config.Topics;
13+
import lombok.Getter;
1214

1315
import java.net.URI;
1416
import java.net.URISyntaxException;
1517
import java.util.List;
16-
import java.util.Objects;
1718
import java.util.Optional;
1819

1920
import static com.aws.greengrass.componentmanager.KernelConfigResolver.CONFIGURATION_CONFIG_KEY;
@@ -47,24 +48,27 @@
4748
public final class CDAConfiguration {
4849

4950
private final RuntimeConfiguration runtime;
50-
private final CAConfiguration ca;
51+
@Getter
52+
private final CAConfiguration caConfig;
5153
private final DomainEvents domainEvents;
5254

5355
private CDAConfiguration(DomainEvents domainEvents, RuntimeConfiguration runtime, CAConfiguration ca) {
5456
this.domainEvents = domainEvents;
5557
this.runtime = runtime;
56-
this.ca = ca;
58+
this.caConfig = ca;
5759
}
5860

5961
/**
60-
* Creates the CDA (Client Device Auth) Service configuration. And allows it to be available in the context
61-
* with the updated values
62+
* Creates the CDA (Client Device Auth) Service configuration.
6263
*
6364
* @param existingConfig an existing version of the CDAConfiguration
6465
* @param topics configuration topics from GG
66+
*
6567
* @throws URISyntaxException if invalid URI inside the configuration
68+
* @throws InvalidConfigurationException if a part of the configuration is invalid
6669
*/
67-
public static CDAConfiguration from(CDAConfiguration existingConfig, Topics topics) throws URISyntaxException {
70+
public static CDAConfiguration from(CDAConfiguration existingConfig, Topics topics) throws URISyntaxException,
71+
InvalidConfigurationException {
6872
Topics runtimeTopics = topics.lookupTopics(RUNTIME_STORE_NAMESPACE_TOPIC);
6973
Topics serviceConfiguration = topics.lookupTopics(CONFIGURATION_CONFIG_KEY);
7074

@@ -85,20 +89,22 @@ public static CDAConfiguration from(CDAConfiguration existingConfig, Topics topi
8589
* Creates the CDA (Client Device Auth) Service configuration.
8690
*
8791
* @param topics configuration topics from GG
92+
*
8893
* @throws URISyntaxException if invalid URI inside the configuration
94+
* @throws InvalidConfigurationException if a part of the configuration is invalid
8995
*/
90-
public static CDAConfiguration from(Topics topics) throws URISyntaxException {
96+
public static CDAConfiguration from(Topics topics) throws URISyntaxException, InvalidConfigurationException {
9197
return from(null, topics);
9298
}
9399

94100
private void triggerChanges(CDAConfiguration current, CDAConfiguration prev) {
95-
if (hasCAConfigurationChanged(prev)) {
101+
if (prev == null || !caConfig.isEqual(prev.getCaConfig())) {
96102
domainEvents.emit(new CAConfigurationChanged(current));
97103
}
98104
}
99105

100106
public boolean isUsingCustomCA() {
101-
return ca.isUsingCustomCA();
107+
return caConfig.isUsingCustomCA();
102108
}
103109

104110
public String getCaPassphrase() {
@@ -114,30 +120,28 @@ public void updateCACertificates(List<String> caCertificates) {
114120
}
115121

116122
public CertificateStore.CAType getCaType() {
117-
return ca.getCaType();
123+
return caConfig.getCaType();
118124
}
119125

120126
public Optional<URI> getPrivateKeyUri() {
121-
return ca.getPrivateKeyUri();
127+
return caConfig.getPrivateKeyUri();
122128
}
123129

124130
public Optional<URI> getCertificateUri() {
125-
return ca.getCertificateUri();
131+
return caConfig.getCertificateUri();
126132
}
127133

128134
/**
129-
* Verifies if the configuration for the certificateAuthority has changed, given a previous
130-
* configuration.
135+
* Verifies if the configuration for the certificateAuthority is equal to another CDA configuration.
131136
*
132-
* @param config CDAConfiguration
137+
* @param configuration CDAConfiguration
133138
*/
134-
private boolean hasCAConfigurationChanged(CDAConfiguration config) {
135-
if (config == null) {
136-
return true;
139+
public boolean isEqual(CDAConfiguration configuration) {
140+
if (configuration == null) {
141+
return false;
137142
}
138143

139-
return !Objects.equals(config.getCertificateUri(), getCertificateUri())
140-
|| !Objects.equals(config.getPrivateKeyUri(), getPrivateKeyUri())
141-
|| !Objects.equals(config.getCaType(), getCaType());
144+
// TODO: As we add more configurations here we should change the equality comparison.
145+
return caConfig.isEqual(configuration.getCaConfig());
142146
}
143147
}

0 commit comments

Comments
 (0)