Skip to content

Commit 017c954

Browse files
vaibhavmurkuteVaibhav Murkute
andauthored
feat: add thing registry (#103)
Adds ThingRegistry that registers thing <-> certId association and returns locally registered results when cloud is unreachable. Co-authored-by: Vaibhav Murkute <murkutv@amazon.com>
1 parent 7d32c4d commit 017c954

File tree

8 files changed

+192
-23
lines changed

8 files changed

+192
-23
lines changed

src/main/java/com/aws/greengrass/clientdevices/auth/api/ClientDevicesAuthServiceApi.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import com.aws.greengrass.clientdevices.auth.exception.AuthenticationException;
1212
import com.aws.greengrass.clientdevices.auth.exception.AuthorizationException;
1313
import com.aws.greengrass.clientdevices.auth.exception.CertificateGenerationException;
14-
import com.aws.greengrass.clientdevices.auth.iot.CertificateRegistry;
14+
import com.aws.greengrass.clientdevices.auth.iot.registry.CertificateRegistry;
1515
import com.aws.greengrass.clientdevices.auth.session.SessionManager;
1616

1717
import java.util.Map;

src/main/java/com/aws/greengrass/clientdevices/auth/iot/CertificateRegistry.java renamed to src/main/java/com/aws/greengrass/clientdevices/auth/iot/registry/CertificateRegistry.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
package com.aws.greengrass.clientdevices.auth.iot;
6+
package com.aws.greengrass.clientdevices.auth.iot.registry;
77

8+
import com.aws.greengrass.clientdevices.auth.iot.IotAuthClient;
89
import com.aws.greengrass.logging.api.Logger;
910
import com.aws.greengrass.logging.impl.LogManager;
1011
import com.aws.greengrass.util.Digest;
@@ -20,14 +21,13 @@
2021

2122
public class CertificateRegistry {
2223
private static final Logger logger = LogManager.getLogger(CertificateRegistry.class);
23-
public static final int REGISTRY_CACHE_SIZE = 50;
2424
// holds mapping of certificateHash (SHA-256 hash of certificatePem) to IoT Certificate Id;
2525
// size-bound by default cache size, evicts oldest written entry if the max size is reached
2626
private static final Map<String, String> certificateHashToIdMap = Collections.synchronizedMap(
27-
new LinkedHashMap<String, String>(REGISTRY_CACHE_SIZE, 0.75f, false) {
27+
new LinkedHashMap<String, String>(RegistryConfig.REGISTRY_CACHE_SIZE, 0.75f, false) {
2828
@Override
2929
protected boolean removeEldestEntry(Map.Entry eldest) {
30-
return size() > REGISTRY_CACHE_SIZE;
30+
return size() > RegistryConfig.REGISTRY_CACHE_SIZE;
3131
}
3232
});
3333

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package com.aws.greengrass.clientdevices.auth.iot.registry;
7+
8+
public class RegistryConfig {
9+
public static final int REGISTRY_CACHE_SIZE = 50;
10+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package com.aws.greengrass.clientdevices.auth.iot.registry;
7+
8+
import com.aws.greengrass.clientdevices.auth.exception.CloudServiceInteractionException;
9+
import com.aws.greengrass.clientdevices.auth.iot.Certificate;
10+
import com.aws.greengrass.clientdevices.auth.iot.IotAuthClient;
11+
import com.aws.greengrass.clientdevices.auth.iot.Thing;
12+
13+
import java.util.Collections;
14+
import java.util.LinkedHashMap;
15+
import java.util.Map;
16+
import java.util.Optional;
17+
import javax.inject.Inject;
18+
19+
public class ThingRegistry {
20+
// holds mapping of thingName to IoT Certificate ID;
21+
// size-bound by default cache size, evicts oldest written entry if the max size is reached
22+
static final Map<String, String> registry = Collections.synchronizedMap(
23+
new LinkedHashMap<String, String>(RegistryConfig.REGISTRY_CACHE_SIZE, 0.75f, false) {
24+
@Override
25+
protected boolean removeEldestEntry(Map.Entry eldest) {
26+
return size() > RegistryConfig.REGISTRY_CACHE_SIZE;
27+
}
28+
});
29+
30+
private final IotAuthClient iotAuthClient;
31+
32+
@Inject
33+
public ThingRegistry(IotAuthClient iotAuthClient) {
34+
this.iotAuthClient = iotAuthClient;
35+
}
36+
37+
/**
38+
* Returns whether the Thing is associated to the given IoT Certificate.
39+
* Returns locally registered result when IoT Core cannot be reached.
40+
* TODO: add a separate refreshable caching layer for offline auth
41+
*
42+
* @param thing IoT Thing
43+
* @param certificate IoT Certificate
44+
* @return whether thing is attached to the certificate
45+
*/
46+
public boolean isThingAttachedToCertificate(Thing thing, Certificate certificate) {
47+
try {
48+
if (iotAuthClient.isThingAttachedToCertificate(thing, certificate)) {
49+
registerCertificateForThing(thing, certificate);
50+
return true;
51+
} else {
52+
clearRegistryForThing(thing);
53+
}
54+
} catch (CloudServiceInteractionException e) {
55+
return isCertificateRegisteredForThing(thing, certificate);
56+
}
57+
return false;
58+
}
59+
60+
private void registerCertificateForThing(Thing thing, Certificate certificate) {
61+
registry.put(thing.getThingName(), certificate.getIotCertificateId());
62+
}
63+
64+
private void clearRegistryForThing(Thing thing) {
65+
registry.remove(thing.getThingName());
66+
}
67+
68+
private boolean isCertificateRegisteredForThing(Thing thing, Certificate certificate) {
69+
return Optional.ofNullable(registry.get(thing.getThingName()))
70+
.filter(certId -> certId.equals(certificate.getIotCertificateId()))
71+
.isPresent();
72+
}
73+
}

src/main/java/com/aws/greengrass/clientdevices/auth/session/MqttSessionFactory.java

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,34 +9,34 @@
99
import com.aws.greengrass.clientdevices.auth.exception.AuthenticationException;
1010
import com.aws.greengrass.clientdevices.auth.exception.CloudServiceInteractionException;
1111
import com.aws.greengrass.clientdevices.auth.iot.Certificate;
12-
import com.aws.greengrass.clientdevices.auth.iot.CertificateRegistry;
1312
import com.aws.greengrass.clientdevices.auth.iot.Component;
14-
import com.aws.greengrass.clientdevices.auth.iot.IotAuthClient;
1513
import com.aws.greengrass.clientdevices.auth.iot.Thing;
14+
import com.aws.greengrass.clientdevices.auth.iot.registry.CertificateRegistry;
15+
import com.aws.greengrass.clientdevices.auth.iot.registry.ThingRegistry;
1616

1717
import java.util.Map;
1818
import java.util.Optional;
1919
import javax.inject.Inject;
2020

2121
public class MqttSessionFactory implements SessionFactory {
22-
private final IotAuthClient iotAuthClient;
2322
private final DeviceAuthClient deviceAuthClient;
2423
private final CertificateRegistry certificateRegistry;
24+
private final ThingRegistry thingRegistry;
2525

2626
/**
2727
* Constructor.
2828
*
29-
* @param iotAuthClient Iot auth client
3029
* @param deviceAuthClient Device auth client
3130
* @param certificateRegistry device Certificate registry
31+
* @param thingRegistry thing registry
3232
*/
3333
@Inject
34-
public MqttSessionFactory(IotAuthClient iotAuthClient,
35-
DeviceAuthClient deviceAuthClient,
36-
CertificateRegistry certificateRegistry) {
37-
this.iotAuthClient = iotAuthClient;
34+
public MqttSessionFactory(DeviceAuthClient deviceAuthClient,
35+
CertificateRegistry certificateRegistry,
36+
ThingRegistry thingRegistry) {
3837
this.deviceAuthClient = deviceAuthClient;
3938
this.certificateRegistry = certificateRegistry;
39+
this.thingRegistry = thingRegistry;
4040
}
4141

4242
@Override
@@ -61,7 +61,7 @@ private Session createIotThingSession(MqttCredential mqttCredential) throws Auth
6161
}
6262
Thing thing = new Thing(mqttCredential.clientId);
6363
Certificate cert = new Certificate(certificateId.get());
64-
if (!iotAuthClient.isThingAttachedToCertificate(thing, cert)) {
64+
if (!thingRegistry.isThingAttachedToCertificate(thing, cert)) {
6565
throw new AuthenticationException("unable to authenticate device");
6666
}
6767
Session session = new SessionImpl(cert);

src/test/java/com/aws/greengrass/clientdevices/auth/iot/CertificateRegistryTest.java renamed to src/test/java/com/aws/greengrass/clientdevices/auth/iot/registry/CertificateRegistryTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
package com.aws.greengrass.clientdevices.auth.iot;
6+
package com.aws.greengrass.clientdevices.auth.iot.registry;
77

8+
import com.aws.greengrass.clientdevices.auth.iot.IotAuthClient;
89
import com.aws.greengrass.testcommons.testutilities.GGExtension;
910
import org.junit.jupiter.api.AfterEach;
1011
import org.junit.jupiter.api.BeforeEach;
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package com.aws.greengrass.clientdevices.auth.iot.registry;
7+
8+
9+
import com.aws.greengrass.clientdevices.auth.exception.CloudServiceInteractionException;
10+
import com.aws.greengrass.clientdevices.auth.iot.Certificate;
11+
import com.aws.greengrass.clientdevices.auth.iot.IotAuthClient;
12+
import com.aws.greengrass.clientdevices.auth.iot.Thing;
13+
import com.aws.greengrass.testcommons.testutilities.GGExtension;
14+
import org.junit.jupiter.api.BeforeEach;
15+
import org.junit.jupiter.api.Test;
16+
import org.junit.jupiter.api.extension.ExtendWith;
17+
import org.mockito.Mock;
18+
import org.mockito.junit.jupiter.MockitoExtension;
19+
20+
import static org.junit.jupiter.api.Assertions.assertFalse;
21+
import static org.junit.jupiter.api.Assertions.assertNull;
22+
import static org.junit.jupiter.api.Assertions.assertTrue;
23+
import static org.mockito.Mockito.any;
24+
import static org.mockito.Mockito.doThrow;
25+
import static org.mockito.Mockito.reset;
26+
import static org.mockito.Mockito.times;
27+
import static org.mockito.Mockito.verify;
28+
import static org.mockito.Mockito.when;
29+
30+
@ExtendWith({MockitoExtension.class, GGExtension.class})
31+
class ThingRegistryTest {
32+
private static final Thing mockThing = new Thing("mock-thing");
33+
private static final Certificate mockCertificate = new Certificate("mock-certificateId");
34+
35+
@Mock
36+
private IotAuthClient mockIotAuthClient;
37+
38+
private ThingRegistry registry;
39+
40+
@BeforeEach
41+
void beforeEach() {
42+
registry = new ThingRegistry(mockIotAuthClient);
43+
}
44+
45+
@Test
46+
void GIVEN_valid_thing_and_certificate_WHEN_isThingAttachedToCertificate_THEN_pass() {
47+
// positive result
48+
when(mockIotAuthClient.isThingAttachedToCertificate(any(Thing.class), any(Certificate.class))).thenReturn(true);
49+
assertTrue(registry.isThingAttachedToCertificate(mockThing, mockCertificate));
50+
verify(mockIotAuthClient, times(1)).isThingAttachedToCertificate(any(), any());
51+
52+
// negative result
53+
reset(mockIotAuthClient);
54+
when(mockIotAuthClient.isThingAttachedToCertificate(any(Thing.class), any(Certificate.class))).thenReturn(false);
55+
assertFalse(registry.isThingAttachedToCertificate(mockThing, mockCertificate));
56+
// registry should be cleared for negative result
57+
assertNull(ThingRegistry.registry.get(mockThing.getThingName()));
58+
}
59+
60+
@Test
61+
void GIVEN_unreachable_cloud_WHEN_isThingAttachedToCertificate_THEN_return_cached_result() {
62+
// cache result before going offline
63+
when(mockIotAuthClient.isThingAttachedToCertificate(any(Thing.class), any(Certificate.class))).thenReturn(true);
64+
assertTrue(registry.isThingAttachedToCertificate(mockThing, mockCertificate));
65+
66+
// go offline
67+
reset(mockIotAuthClient);
68+
doThrow(CloudServiceInteractionException.class)
69+
.when(mockIotAuthClient).isThingAttachedToCertificate(any(), any());
70+
71+
// verify cached result
72+
assertTrue(registry.isThingAttachedToCertificate(mockThing, mockCertificate));
73+
verify(mockIotAuthClient, times(1)).isThingAttachedToCertificate(any(), any());
74+
}
75+
76+
@Test
77+
void GIVEN_offline_initialization_WHEN_isThingAttachedToCertificate_THEN_return_false_by_default() {
78+
doThrow(CloudServiceInteractionException.class)
79+
.when(mockIotAuthClient).isThingAttachedToCertificate(any(), any());
80+
81+
assertFalse(registry.isThingAttachedToCertificate(mockThing, mockCertificate));
82+
verify(mockIotAuthClient, times(1)).isThingAttachedToCertificate(any(), any());
83+
}
84+
85+
}

src/test/java/com/aws/greengrass/clientdevices/auth/session/MqttSessionFactoryTest.java

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88
import com.aws.greengrass.clientdevices.auth.DeviceAuthClient;
99
import com.aws.greengrass.clientdevices.auth.exception.AuthenticationException;
1010
import com.aws.greengrass.clientdevices.auth.exception.CloudServiceInteractionException;
11-
import com.aws.greengrass.clientdevices.auth.iot.CertificateRegistry;
11+
import com.aws.greengrass.clientdevices.auth.iot.registry.CertificateRegistry;
1212
import com.aws.greengrass.clientdevices.auth.iot.Component;
13-
import com.aws.greengrass.clientdevices.auth.iot.IotAuthClient;
13+
import com.aws.greengrass.clientdevices.auth.iot.registry.ThingRegistry;
1414
import com.aws.greengrass.testcommons.testutilities.GGExtension;
1515
import org.hamcrest.core.IsNull;
1616
import org.junit.jupiter.api.Assertions;
@@ -33,12 +33,12 @@
3333

3434
@ExtendWith({MockitoExtension.class, GGExtension.class})
3535
public class MqttSessionFactoryTest {
36-
@Mock
37-
private IotAuthClient mockIotAuthClient;
3836
@Mock
3937
private DeviceAuthClient mockDeviceAuthClient;
4038
@Mock
4139
private CertificateRegistry mockCertificateRegistry;
40+
@Mock
41+
private ThingRegistry mockThingRegistry;
4242
private MqttSessionFactory mqttSessionFactory;
4343
private final Map<String, String> credentialMap = ImmutableMap.of(
4444
"certificatePem", "PEM",
@@ -49,13 +49,13 @@ public class MqttSessionFactoryTest {
4949

5050
@BeforeEach
5151
void beforeEach() {
52-
mqttSessionFactory = new MqttSessionFactory(mockIotAuthClient, mockDeviceAuthClient, mockCertificateRegistry);
52+
mqttSessionFactory = new MqttSessionFactory(mockDeviceAuthClient, mockCertificateRegistry, mockThingRegistry);
5353
}
5454

5555
@Test
5656
void GIVEN_credentialsWithUnknownClientId_WHEN_createSession_THEN_throwsAuthenticationException() {
5757
when(mockCertificateRegistry.getIotCertificateIdForPem(any())).thenReturn(Optional.of("id"));
58-
when(mockIotAuthClient.isThingAttachedToCertificate(any(), any())).thenReturn(false);
58+
when(mockThingRegistry.isThingAttachedToCertificate(any(), any())).thenReturn(false);
5959

6060
Assertions.assertThrows(AuthenticationException.class,
6161
() -> mqttSessionFactory.createSession(credentialMap));
@@ -71,7 +71,7 @@ void GIVEN_credentialsWithInvalidCertificate_WHEN_createSession_THEN_throwsAuthe
7171
@Test
7272
void GIVEN_credentialsWithCertificate_WHEN_createSession_AND_cloudError_THEN_throwsAuthenticationException() {
7373
when(mockCertificateRegistry.getIotCertificateIdForPem(any())).thenReturn(Optional.of("id"));
74-
when(mockIotAuthClient.isThingAttachedToCertificate(any(), any()))
74+
when(mockThingRegistry.isThingAttachedToCertificate(any(), any()))
7575
.thenThrow(CloudServiceInteractionException.class);
7676
Assertions.assertThrows(AuthenticationException.class,
7777
() -> mqttSessionFactory.createSession(credentialMap));
@@ -80,7 +80,7 @@ void GIVEN_credentialsWithCertificate_WHEN_createSession_AND_cloudError_THEN_thr
8080
@Test
8181
void GIVEN_credentialsWithValidClientId_WHEN_createSession_THEN_returnsSession() throws AuthenticationException {
8282
when(mockCertificateRegistry.getIotCertificateIdForPem(any())).thenReturn(Optional.of("id"));
83-
when(mockIotAuthClient.isThingAttachedToCertificate(any(), any())).thenReturn(true);
83+
when(mockThingRegistry.isThingAttachedToCertificate(any(), any())).thenReturn(true);
8484

8585
Session session = mqttSessionFactory.createSession(credentialMap);
8686
assertThat(session, is(IsNull.notNullValue()));

0 commit comments

Comments
 (0)