From b4c43cd13899926d692f6dab95ad1a2aa6107cd7 Mon Sep 17 00:00:00 2001
From: Joseph Cosentino
Date: Thu, 27 Jun 2024 14:07:11 -0700
Subject: [PATCH 1/5] feat: add thing attribute variable support
---
.../benchmark/AuthorizationBenchmarks.java | 11 +-
.../integrationtests/policy/PolicyTest.java | 92 ++++++----
.../policy/thing-attribute-variable.yaml | 28 +++
.../auth/ClientDevicesAuthService.java | 31 +++-
.../clientdevices/auth/DeviceAuthClient.java | 4 +-
.../auth/configuration/ExpressionVisitor.java | 3 +-
.../configuration/GroupConfiguration.java | 116 +++++++-----
.../auth/configuration/PolicyVariable.java | 73 ++++++++
.../configuration/PolicyVariableResolver.java | 70 +++++---
.../clientdevices/auth/iot/Certificate.java | 7 +-
.../clientdevices/auth/iot/Component.java | 7 +-
.../auth/iot/IotClientFactory.java | 56 ++++++
.../clientdevices/auth/iot/IotCoreClient.java | 53 ++++++
.../clientdevices/auth/iot/Thing.java | 29 ++-
.../auth/iot/ThingAttributesCache.java | 170 ++++++++++++++++++
.../clientdevices/auth/session/Session.java | 10 +-
.../auth/session/SessionImpl.java | 14 +-
.../auth/session/attribute/Attribute.java | 32 ++++
.../auth/DeviceAuthClientTest.java | 3 +-
.../configuration/GroupDefinitionTest.java | 8 +-
.../PolicyVariableResolverTest.java | 5 +-
.../parser/RuleExpressionEvaluationTest.java | 2 +-
.../auth/iot/IotCoreClientFake.java | 39 ++++
.../auth/session/MqttSessionFactoryTest.java | 4 +-
.../auth/session/SessionImplTest.java | 5 +-
25 files changed, 726 insertions(+), 146 deletions(-)
create mode 100644 src/integrationtests/resources/com/aws/greengrass/integrationtests/policy/thing-attribute-variable.yaml
create mode 100644 src/main/java/com/aws/greengrass/clientdevices/auth/configuration/PolicyVariable.java
create mode 100644 src/main/java/com/aws/greengrass/clientdevices/auth/iot/IotClientFactory.java
create mode 100644 src/main/java/com/aws/greengrass/clientdevices/auth/iot/IotCoreClient.java
create mode 100644 src/main/java/com/aws/greengrass/clientdevices/auth/iot/ThingAttributesCache.java
create mode 100644 src/main/java/com/aws/greengrass/clientdevices/auth/session/attribute/Attribute.java
create mode 100644 src/test/java/com/aws/greengrass/clientdevices/auth/iot/IotCoreClientFake.java
diff --git a/benchmark/src/main/java/com/aws/greengrass/clientdevices/auth/benchmark/AuthorizationBenchmarks.java b/benchmark/src/main/java/com/aws/greengrass/clientdevices/auth/benchmark/AuthorizationBenchmarks.java
index d6bdcce52..606e21aad 100644
--- a/benchmark/src/main/java/com/aws/greengrass/clientdevices/auth/benchmark/AuthorizationBenchmarks.java
+++ b/benchmark/src/main/java/com/aws/greengrass/clientdevices/auth/benchmark/AuthorizationBenchmarks.java
@@ -16,6 +16,7 @@
import com.aws.greengrass.clientdevices.auth.exception.AuthorizationException;
import com.aws.greengrass.clientdevices.auth.session.Session;
import com.aws.greengrass.clientdevices.auth.session.SessionManager;
+import com.aws.greengrass.clientdevices.auth.session.attribute.Attribute;
import com.aws.greengrass.clientdevices.auth.session.attribute.AttributeProvider;
import com.aws.greengrass.clientdevices.auth.session.attribute.DeviceAttribute;
import com.aws.greengrass.clientdevices.auth.session.attribute.StringLiteralAttribute;
@@ -173,19 +174,19 @@ private FakeSession(String thingName, boolean isComponent) {
}
@Override
- public AttributeProvider getAttributeProvider(String attributeProviderNameSpace) {
+ public AttributeProvider getAttributeProvider(String namespace) {
throw new UnsupportedOperationException();
}
@Override
- public DeviceAttribute getSessionAttribute(String ns, String name) {
- if ("Component".equalsIgnoreCase(ns) && name.equalsIgnoreCase("component")) {
+ public DeviceAttribute getSessionAttribute(Attribute attribute) {
+ if ("Component".equalsIgnoreCase(attribute.getNamespace()) && attribute.getName().equalsIgnoreCase("component")) {
return isComponent ? new StringLiteralAttribute("component") : null;
}
- if ("Thing".equalsIgnoreCase(ns) && name.equalsIgnoreCase("thingName")) {
+ if ("Thing".equalsIgnoreCase(attribute.getNamespace()) && attribute.getName().equalsIgnoreCase("thingName")) {
return new WildcardSuffixAttribute(thingName);
}
- throw new UnsupportedOperationException(String.format("Attribute %s.%s not supported", ns, name));
+ throw new UnsupportedOperationException(String.format("Attribute %s.%s not supported", attribute.getNamespace(), attribute.getName()));
}
}
diff --git a/src/integrationtests/java/com/aws/greengrass/integrationtests/policy/PolicyTest.java b/src/integrationtests/java/com/aws/greengrass/integrationtests/policy/PolicyTest.java
index b9d811891..6e3f801cd 100644
--- a/src/integrationtests/java/com/aws/greengrass/integrationtests/policy/PolicyTest.java
+++ b/src/integrationtests/java/com/aws/greengrass/integrationtests/policy/PolicyTest.java
@@ -12,21 +12,21 @@
import com.aws.greengrass.clientdevices.auth.configuration.AuthorizationPolicyStatement;
import com.aws.greengrass.clientdevices.auth.configuration.GroupConfiguration;
import com.aws.greengrass.clientdevices.auth.configuration.GroupDefinition;
+import com.aws.greengrass.clientdevices.auth.exception.AuthenticationException;
import com.aws.greengrass.clientdevices.auth.exception.PolicyException;
import com.aws.greengrass.clientdevices.auth.helpers.CertificateTestHelpers;
-import com.aws.greengrass.clientdevices.auth.iot.Certificate;
-import com.aws.greengrass.clientdevices.auth.iot.CertificateRegistry;
+import com.aws.greengrass.clientdevices.auth.infra.NetworkStateProvider;
import com.aws.greengrass.clientdevices.auth.iot.IotAuthClient;
import com.aws.greengrass.clientdevices.auth.iot.IotAuthClientFake;
-import com.aws.greengrass.clientdevices.auth.iot.Thing;
-import com.aws.greengrass.clientdevices.auth.iot.infra.ThingRegistry;
+import com.aws.greengrass.clientdevices.auth.iot.IotCoreClient;
+import com.aws.greengrass.clientdevices.auth.iot.IotCoreClientFake;
+import com.aws.greengrass.clientdevices.auth.iot.NetworkStateFake;
import com.aws.greengrass.dependency.State;
import com.aws.greengrass.lifecyclemanager.Kernel;
import com.aws.greengrass.logging.impl.config.LogConfig;
import com.aws.greengrass.mqttclient.spool.SpoolerStoreException;
import com.aws.greengrass.testcommons.testutilities.GGExtension;
import com.aws.greengrass.testcommons.testutilities.UniqueRootPathExtension;
-import com.aws.greengrass.util.Pair;
import com.aws.greengrass.util.Utils;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Builder;
@@ -42,6 +42,7 @@
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.junit.jupiter.MockitoExtension;
+import software.amazon.awssdk.utils.ImmutableMap;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
@@ -65,7 +66,13 @@
@ExtendWith({GGExtension.class, UniqueRootPathExtension.class, MockitoExtension.class})
public class PolicyTest {
private static final ObjectMapper MAPPER = new ObjectMapper();
- private final Map> clients = new HashMap<>();
+ private final Map clients = new HashMap<>();
+
+ private final NetworkStateFake networkStateProvider = new NetworkStateFake();
+ private final IotAuthClientFake iotAuthClient = new IotAuthClientFake();
+ private final IotCoreClientFake iotCoreClient = new IotCoreClientFake();
+ private final Map DEFAULT_THING_ATTRIBUTES = ImmutableMap.of("myAttribute", "attribute");
+
@TempDir
Path rootDir;
Kernel kernel;
@@ -74,6 +81,8 @@ public class PolicyTest {
void beforeEach(ExtensionContext context) {
ignoreExceptionOfType(context, SpoolerStoreException.class);
ignoreExceptionOfType(context, NoSuchFileException.class); // Loading CA keystore
+ iotCoreClient.setThingAttributes(DEFAULT_THING_ATTRIBUTES);
+ networkStateProvider.goOnline();
}
@AfterEach
@@ -295,6 +304,14 @@ public static Stream authzRequests() {
.resource("mqtt:topic:hello/myThing")
.expectedResult(false)
.build()
+ )),
+ Arguments.of("thing-attribute-variable.yaml", Arrays.asList(
+ AuthZRequest.builder()
+ .thingName("myThing")
+ .operation("mqtt:publish")
+ .resource("mqtt:topic:attribute")
+ .expectedResult(true)
+ .build()
))
);
}
@@ -302,11 +319,19 @@ public static Stream authzRequests() {
@ParameterizedTest
@MethodSource("authzRequests")
void GIVEN_cda_with_policy_configuration_WHEN_client_requests_authorization_THEN_client_is_authorized(String configFile, List requests) throws Exception {
+ // register certificates and associate client devices with core BEFORE starting CDA.
+ // CDA needs this data on startup when:
+ // 1) the policy has a thing attr variable (see thing-attribute-variable.yaml)
+ Map authTokens = requests.stream()
+ .map(AuthZRequest::getThingName)
+ .distinct()
+ .collect(Collectors.toMap(thingName -> thingName, this::createOrGetClient));
+
startNucleus(configFile);
for (AuthZRequest request : requests) {
boolean actualResult = api().authorizeClientDeviceAction(AuthorizationRequest.builder()
- .sessionId(generateAuthToken(request.getThingName()))
+ .sessionId(generateAuthToken(request.getThingName(), authTokens.get(request.getThingName())))
.operation(request.getOperation())
.resource(request.getResource())
.build());
@@ -317,40 +342,43 @@ void GIVEN_cda_with_policy_configuration_WHEN_client_requests_authorization_THEN
}
@SuppressWarnings("PMD.AvoidCatchingGenericException")
- private String generateAuthToken(String thingName) throws Exception {
- Pair clientCert = clients.computeIfAbsent(thingName, k -> {
+ private String createOrGetClient(String thingName) {
+ return clients.computeIfAbsent(thingName, k -> {
try {
- Pair cert = generateClientCert();
-
- // register client within CDA
- ThingRegistry thingRegistry = kernel.getContext().get(ThingRegistry.class);
- Thing thing = thingRegistry.createThing(thingName);
- thing.attachCertificate(cert.getLeft().getCertificateId());
- thingRegistry.updateThing(thing);
-
+ String cert = generateClientCert();
+ iotAuthClient.activateCert(cert);
+ iotAuthClient.attachCertificateToThing(thingName, cert);
+ iotAuthClient.attachThingToCore(() -> thingName);
return cert;
} catch (Exception e) {
fail(e);
return null;
}
});
+ }
+
+ @SuppressWarnings("PMD.AvoidCatchingGenericException")
+ private String generateAuthToken(String thingName) {
+ return generateAuthToken(thingName, createOrGetClient(thingName));
+ }
- return api().getClientDeviceAuthToken("mqtt", Utils.immutableMap(
- "clientId", thingName,
- "certificatePem", clientCert.getRight()
- ));
+ @SuppressWarnings("PMD.AvoidCatchingGenericException")
+ private String generateAuthToken(String thingName, String cert) {
+ try {
+ assertTrue(api().verifyClientDeviceIdentity(cert)); // add cert to CDA cert registry
+ return api().getClientDeviceAuthToken("mqtt", Utils.immutableMap(
+ "clientId", thingName,
+ "certificatePem", cert
+ ));
+ } catch (AuthenticationException e) {
+ fail(e);
+ return null;
+ }
}
- private Pair generateClientCert() throws Exception {
- // create certificate to attach to thing
+ private String generateClientCert() throws Exception {
List clientCertificates = CertificateTestHelpers.createClientCertificates(1);
- String clientPem = CertificateHelper.toPem(clientCertificates.get(0));
- CertificateRegistry certificateRegistry = kernel.getContext().get(CertificateRegistry.class);
- Certificate cert = certificateRegistry.getOrCreateCertificate(clientPem);
- cert.setStatus(Certificate.Status.ACTIVE);
- // activate certificate
- certificateRegistry.updateCertificate(cert);
- return new Pair<>(cert, clientPem);
+ return CertificateHelper.toPem(clientCertificates.get(0));
}
@SuppressWarnings("unchecked")
@@ -372,7 +400,9 @@ private void startNucleus(String configFileName, State expectedState) {
// Set this property for kernel to scan its own classpath to find plugins
System.setProperty("aws.greengrass.scanSelfClasspath", "true");
kernel = new Kernel();
- kernel.getContext().put(IotAuthClient.class, new IotAuthClientFake());
+ kernel.getContext().put(IotAuthClient.class, iotAuthClient);
+ kernel.getContext().put(IotCoreClient.class, iotCoreClient);
+ kernel.getContext().put(NetworkStateProvider.class, networkStateProvider);
kernel.parseArgs("-r", rootDir.toAbsolutePath().toString(), "-i",
getClass().getResource(configFileName).toString());
Runnable mainRunning = createServiceStateChangeWaiter(kernel,
diff --git a/src/integrationtests/resources/com/aws/greengrass/integrationtests/policy/thing-attribute-variable.yaml b/src/integrationtests/resources/com/aws/greengrass/integrationtests/policy/thing-attribute-variable.yaml
new file mode 100644
index 000000000..138d42d1b
--- /dev/null
+++ b/src/integrationtests/resources/com/aws/greengrass/integrationtests/policy/thing-attribute-variable.yaml
@@ -0,0 +1,28 @@
+---
+services:
+ aws.greengrass.Nucleus:
+ configuration:
+ runWithDefault:
+ posixUser: nobody
+ windowsUser: integ-tester
+ logging:
+ level: "DEBUG"
+ aws.greengrass.clientdevices.Auth:
+ configuration:
+ deviceGroups:
+ formatVersion: "2021-03-05"
+ definitions:
+ myThing:
+ selectionRule: "thingName: myThing"
+ policyName: "publish"
+ policies:
+ publish:
+ policyStatement:
+ statementDescription: "publish"
+ operations:
+ - "mqtt:publish"
+ resources:
+ - "mqtt:topic:${iot:Connection.Thing.Attributes[myAttribute]}"
+ main:
+ dependencies:
+ - aws.greengrass.clientdevices.Auth
diff --git a/src/main/java/com/aws/greengrass/clientdevices/auth/ClientDevicesAuthService.java b/src/main/java/com/aws/greengrass/clientdevices/auth/ClientDevicesAuthService.java
index 69b079002..8c29322e7 100644
--- a/src/main/java/com/aws/greengrass/clientdevices/auth/ClientDevicesAuthService.java
+++ b/src/main/java/com/aws/greengrass/clientdevices/auth/ClientDevicesAuthService.java
@@ -25,6 +25,7 @@
import com.aws.greengrass.clientdevices.auth.connectivity.ConnectivityInfoCache;
import com.aws.greengrass.clientdevices.auth.exception.PolicyException;
import com.aws.greengrass.clientdevices.auth.infra.NetworkStateProvider;
+import com.aws.greengrass.clientdevices.auth.iot.ThingAttributesCache;
import com.aws.greengrass.clientdevices.auth.metrics.MetricsEmitter;
import com.aws.greengrass.clientdevices.auth.metrics.handlers.AuthorizeClientDeviceActionsMetricHandler;
import com.aws.greengrass.clientdevices.auth.metrics.handlers.CertificateSubscriptionEventHandler;
@@ -136,6 +137,10 @@ private void initializeInfrastructure() {
context.get(BackgroundCertificateRefresh.class).start();
context.get(MetricsEmitter.class).start(MetricsConfiguration.DEFAULT_PERIODIC_AGGREGATE_INTERVAL_SEC);
+ // make cache available during policy evaluation, which doesn't
+ // have access to context or dependency injection
+ ThingAttributesCache.setInstance(context.get(ThingAttributesCache.class));
+
// Initialize IPC thread pool
cloudCallQueueSize = DEFAULT_CLOUD_CALL_QUEUE_SIZE;
cloudCallQueueSize = getValidCloudCallQueueSize(config);
@@ -214,20 +219,33 @@ private void configChangeHandler(WhatHappened whatHappened, Node node) {
@Override
protected void startup() throws InterruptedException {
context.get(CertificateManager.class).startMonitors();
+
+ GroupConfiguration groupConfiguration;
try {
subscribeToConfigChanges();
// Validate CDA policy to force CDA to break on bad config policies before CDA reaches RUNNING
- lookupAndValidateDeviceGroups();
+ groupConfiguration = lookupAndValidateDeviceGroups();
} catch (IllegalArgumentException | PolicyException e) {
serviceErrored(e);
return;
}
+
+ // wait for device attributes to be loaded before marking CDA as STARTED,
+ // otherwise client devices will be rejected until loading is complete
+ // TODO make timeout configurable and also dependent on startup timeout
+ if (groupConfiguration.isHasDeviceAttributeVariables()
+ && !context.get(ThingAttributesCache.class).waitForInitialization(10L, TimeUnit.SECONDS)) {
+ serviceErrored("Timed out loading thing attributes from cloud during startup");
+ return;
+ }
+
super.startup();
}
@Override
protected void shutdown() throws InterruptedException {
super.shutdown();
+ context.get(ThingAttributesCache.class).stopPeriodicRefresh();
context.get(CertificateManager.class).stopMonitors();
context.get(BackgroundCertificateRefresh.class).stop();
context.get(MetricsEmitter.class).stop();
@@ -278,6 +296,17 @@ private void updateDeviceGroups() {
return;
}
+ // policy may have added or removed an attribute variable, e.g. ${iot:Connection.Thing.Attributes[myAttribute]}
+ // these attributes are fetched from the cloud periodically and cached
+ ThingAttributesCache cache = context.get(ThingAttributesCache.class);
+ if (groupConfiguration.isHasDeviceAttributeVariables()) {
+ logger.atTrace().log("enabling thing-attribute cache");
+ cache.startPeriodicRefresh();
+ } else {
+ logger.atTrace().log("disabling thing-attribute cache");
+ cache.stopPeriodicRefresh();
+ }
+
context.get(GroupManager.class).setGroupConfiguration(groupConfiguration);
}
diff --git a/src/main/java/com/aws/greengrass/clientdevices/auth/DeviceAuthClient.java b/src/main/java/com/aws/greengrass/clientdevices/auth/DeviceAuthClient.java
index 3f97ecf2c..f4e3c00a6 100644
--- a/src/main/java/com/aws/greengrass/clientdevices/auth/DeviceAuthClient.java
+++ b/src/main/java/com/aws/greengrass/clientdevices/auth/DeviceAuthClient.java
@@ -8,9 +8,9 @@
import com.aws.greengrass.clientdevices.auth.certificate.CertificateStore;
import com.aws.greengrass.clientdevices.auth.exception.AuthorizationException;
import com.aws.greengrass.clientdevices.auth.exception.InvalidSessionException;
-import com.aws.greengrass.clientdevices.auth.iot.Component;
import com.aws.greengrass.clientdevices.auth.session.Session;
import com.aws.greengrass.clientdevices.auth.session.SessionManager;
+import com.aws.greengrass.clientdevices.auth.session.attribute.Attribute;
import com.aws.greengrass.logging.api.Logger;
import com.aws.greengrass.logging.impl.LogManager;
import software.amazon.awssdk.utils.StringInputStream;
@@ -136,7 +136,7 @@ public boolean canDevicePerform(AuthorizationRequest request) throws Authorizati
}
// Allow all operations from internal components
// Keep the workaround above (ALLOW_ALL_SESSION) for Moquette since it is using the older session management
- if (session.getSessionAttribute(Component.NAMESPACE, "component") != null) {
+ if (session.getSessionAttribute(Attribute.COMPONENT) != null) {
return true;
}
diff --git a/src/main/java/com/aws/greengrass/clientdevices/auth/configuration/ExpressionVisitor.java b/src/main/java/com/aws/greengrass/clientdevices/auth/configuration/ExpressionVisitor.java
index 8b4361987..72a3bb437 100644
--- a/src/main/java/com/aws/greengrass/clientdevices/auth/configuration/ExpressionVisitor.java
+++ b/src/main/java/com/aws/greengrass/clientdevices/auth/configuration/ExpressionVisitor.java
@@ -12,6 +12,7 @@
import com.aws.greengrass.clientdevices.auth.configuration.parser.RuleExpressionVisitor;
import com.aws.greengrass.clientdevices.auth.configuration.parser.SimpleNode;
import com.aws.greengrass.clientdevices.auth.session.Session;
+import com.aws.greengrass.clientdevices.auth.session.attribute.Attribute;
import com.aws.greengrass.clientdevices.auth.session.attribute.DeviceAttribute;
public class ExpressionVisitor implements RuleExpressionVisitor {
@@ -51,7 +52,7 @@ public Object visit(ASTAnd node, Object data) {
public Object visit(ASTThing node, Object data) {
// TODO: Make ASTThing a generic node instead of hardcoding ThingName
Session session = (Session) data;
- DeviceAttribute attribute = session.getSessionAttribute("Thing", "ThingName");
+ DeviceAttribute attribute = session.getSessionAttribute(Attribute.THING_NAME);
return attribute != null && attribute.matches((String) node.jjtGetValue());
}
}
diff --git a/src/main/java/com/aws/greengrass/clientdevices/auth/configuration/GroupConfiguration.java b/src/main/java/com/aws/greengrass/clientdevices/auth/configuration/GroupConfiguration.java
index 085ba602c..5fac6f520 100644
--- a/src/main/java/com/aws/greengrass/clientdevices/auth/configuration/GroupConfiguration.java
+++ b/src/main/java/com/aws/greengrass/clientdevices/auth/configuration/GroupConfiguration.java
@@ -12,6 +12,7 @@
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
import lombok.Builder;
+import lombok.Getter;
import lombok.Value;
import java.util.Collections;
@@ -27,76 +28,99 @@
public class GroupConfiguration {
private static final Logger logger = LogManager.getLogger(GroupConfiguration.class);
- private static final Pattern POLICY_VARIABLE_PATTERN = Pattern.compile("\\$\\{.*?}");
-
ConfigurationFormatVersion formatVersion;
Map definitions;
Map> policies;
Map> groupToPermissionsMap;
+ boolean hasDeviceAttributeVariables;
@Builder
GroupConfiguration(ConfigurationFormatVersion formatVersion, Map definitions,
Map> policies) {
this.formatVersion = formatVersion == null ? ConfigurationFormatVersion.MAR_05_2021 : formatVersion;
- this.definitions = definitions == null ? Collections.emptyMap() : definitions;
- this.policies = policies == null ? Collections.emptyMap() : policies;
- this.groupToPermissionsMap = constructGroupPermissions();
+
+ GroupPermissionConstructor constructor = new GroupPermissionConstructor(definitions, policies);
+ this.definitions = constructor.getDefinitions();
+ this.policies = constructor.getPolicies();
+ this.groupToPermissionsMap = constructor.getPermissions();
+ this.hasDeviceAttributeVariables = constructor.isHasDeviceAttributeVariables();
}
@JsonPOJOBuilder(withPrefix = "")
public static class GroupConfigurationBuilder {
}
- private Map> constructGroupPermissions() {
- return definitions.entrySet().stream().collect(Collectors.toMap(
- Map.Entry::getKey,
- entry -> constructGroupPermission(
- entry.getKey(),
- policies.getOrDefault(entry.getValue().getPolicyName(),
- Collections.emptyMap()))));
- }
+ @Getter
+ private static class GroupPermissionConstructor {
- private Set constructGroupPermission(String groupName,
- Map policyStatementMap) {
- Set permissions = new HashSet<>();
- for (Map.Entry statementEntry : policyStatementMap.entrySet()) {
- AuthorizationPolicyStatement statement = statementEntry.getValue();
- // only accept 'ALLOW' effect for beta launch
- // TODO add 'DENY' effect support
- if (statement.getEffect() == AuthorizationPolicyStatement.Effect.ALLOW) {
- permissions.addAll(convertPolicyStatementToPermission(groupName, statement));
- }
+ private static final Pattern POLICY_VARIABLE_PATTERN = Pattern.compile("\\$\\{.*?}");
+
+ private final Map definitions;
+ private final Map> policies;
+ private final Map> permissions;
+ private boolean hasDeviceAttributeVariables;
+
+ GroupPermissionConstructor(Map definitions,
+ Map> policies) {
+ this.definitions = definitions == null ? Collections.emptyMap() : definitions;
+ this.policies = policies == null ? Collections.emptyMap() : policies;
+ this.permissions = constructGroupPermissions();
+ }
+
+ private Map> constructGroupPermissions() {
+ return definitions.entrySet().stream().collect(Collectors.toMap(
+ Map.Entry::getKey,
+ entry -> constructGroupPermission(
+ entry.getKey(),
+ policies.getOrDefault(entry.getValue().getPolicyName(),
+ Collections.emptyMap()))));
}
- return permissions;
- }
- private Set convertPolicyStatementToPermission(String groupName,
- AuthorizationPolicyStatement statement) {
- Set permissions = new HashSet<>();
- for (String operation : statement.getOperations()) {
- if (Utils.isEmpty(operation)) {
- continue;
+ private Set constructGroupPermission(String groupName,
+ Map policyStatementMap) {
+ Set permissions = new HashSet<>();
+ for (Map.Entry statementEntry : policyStatementMap.entrySet()) {
+ AuthorizationPolicyStatement statement = statementEntry.getValue();
+ // only accept 'ALLOW' effect for beta launch
+ // TODO add 'DENY' effect support
+ if (statement.getEffect() == AuthorizationPolicyStatement.Effect.ALLOW) {
+ permissions.addAll(convertPolicyStatementToPermission(groupName, statement));
+ }
}
- for (String resource : statement.getResources()) {
- if (Utils.isEmpty(resource)) {
+ return permissions;
+ }
+
+ private Set convertPolicyStatementToPermission(String groupName,
+ AuthorizationPolicyStatement statement) {
+ Set permissions = new HashSet<>();
+ for (String operation : statement.getOperations()) {
+ if (Utils.isEmpty(operation)) {
continue;
}
- permissions.add(
- Permission.builder().principal(groupName).operation(operation).resource(resource)
- .resourcePolicyVariables(findPolicyVariables(resource)).build());
+ for (String resource : statement.getResources()) {
+ if (Utils.isEmpty(resource)) {
+ continue;
+ }
+ permissions.add(
+ Permission.builder().principal(groupName).operation(operation).resource(resource)
+ .resourcePolicyVariables(findPolicyVariables(resource)).build());
+ }
}
+ return permissions;
}
- return permissions;
- }
- private Set findPolicyVariables(String resource) {
- Matcher matcher = POLICY_VARIABLE_PATTERN.matcher(resource);
- Set policyVariables = new HashSet<>();
- while (matcher.find()) {
- String policyVariable = matcher.group(0);
- policyVariables.add(policyVariable);
+ private Set findPolicyVariables(String resource) {
+ Matcher matcher = POLICY_VARIABLE_PATTERN.matcher(resource);
+ Set policyVariables = new HashSet<>();
+ while (matcher.find()) {
+ String policyVariable = matcher.group(0);
+ if (PolicyVariableResolver.isAttributePolicyVariable(policyVariable)) {
+ hasDeviceAttributeVariables = true;
+ }
+ policyVariables.add(policyVariable);
+ }
+ return policyVariables;
}
- return policyVariables;
}
/**
@@ -117,7 +141,7 @@ public void validate() throws PolicyException {
if (!groupToPermissionsMap.values().stream()
.flatMap(permissions -> permissions.stream().flatMap(p -> p.getResourcePolicyVariables().stream()))
- .allMatch(PolicyVariableResolver::isPolicyVariable)) {
+ .allMatch(PolicyVariableResolver::isSupportedPolicyVariable)) {
throw new PolicyException("Policy contains unknown variables");
}
}
diff --git a/src/main/java/com/aws/greengrass/clientdevices/auth/configuration/PolicyVariable.java b/src/main/java/com/aws/greengrass/clientdevices/auth/configuration/PolicyVariable.java
new file mode 100644
index 000000000..504c0f6e3
--- /dev/null
+++ b/src/main/java/com/aws/greengrass/clientdevices/auth/configuration/PolicyVariable.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.aws.greengrass.clientdevices.auth.configuration;
+
+import com.aws.greengrass.clientdevices.auth.session.attribute.Attribute;
+import lombok.Builder;
+import lombok.NonNull;
+import lombok.Value;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.Objects;
+import java.util.Optional;
+
+@Builder
+@Value
+public class PolicyVariable {
+
+ private static final String THING_NAME_PATTERN = "${iot:Connection.Thing.ThingName}";
+ private static final String THING_NAMESPACE = "Thing";
+
+ private static final String THING_ATTRS_PREFIX = "${iot:Connection.Thing.Attributes[";
+ private static final String THING_ATTRS_SUFFIX = "]}";
+
+ String originalText;
+ Attribute attribute;
+ String selector; // the part within [ ]
+
+ /**
+ * Parse a policy variable from string.
+ *
+ * @param policyVariable variable
+ * @return parsed policy variable
+ */
+ public static Optional parse(@NonNull String policyVariable) {
+ // thing name
+ if (Objects.equals(policyVariable, THING_NAME_PATTERN)) {
+ return Optional.of(PolicyVariable.builder()
+ .originalText(policyVariable)
+ .attribute(Attribute.THING_NAME)
+ .build());
+ }
+
+ // thing attributes
+ if (policyVariable.startsWith(THING_ATTRS_PREFIX) && policyVariable.endsWith(THING_ATTRS_SUFFIX)) {
+ return parseAttributePolicyVariable(policyVariable);
+ }
+
+ // unsupported variable
+ return Optional.empty();
+ }
+
+ private static Optional parseAttributePolicyVariable(@NonNull String policyVariable) {
+ int attrStart = THING_ATTRS_PREFIX.length();
+ int attrEnd = policyVariable.length() - THING_ATTRS_SUFFIX.length();
+ if (attrStart > attrEnd) {
+ return Optional.empty();
+ }
+
+ String attr = policyVariable.substring(attrStart, attrEnd);
+ if (!StringUtils.isAlphanumeric(attr)) {
+ return Optional.empty();
+ }
+
+ return Optional.of(PolicyVariable.builder()
+ .originalText(policyVariable)
+ .attribute(Attribute.THING_ATTRIBUTES)
+ .selector(attr)
+ .build());
+ }
+}
diff --git a/src/main/java/com/aws/greengrass/clientdevices/auth/configuration/PolicyVariableResolver.java b/src/main/java/com/aws/greengrass/clientdevices/auth/configuration/PolicyVariableResolver.java
index 4577c9db0..d858da04d 100644
--- a/src/main/java/com/aws/greengrass/clientdevices/auth/configuration/PolicyVariableResolver.java
+++ b/src/main/java/com/aws/greengrass/clientdevices/auth/configuration/PolicyVariableResolver.java
@@ -7,21 +7,21 @@
import com.aws.greengrass.clientdevices.auth.exception.PolicyException;
import com.aws.greengrass.clientdevices.auth.session.Session;
+import com.aws.greengrass.clientdevices.auth.session.attribute.Attribute;
+import com.aws.greengrass.clientdevices.auth.session.attribute.DeviceAttribute;
import com.aws.greengrass.util.Coerce;
-import com.aws.greengrass.util.Pair;
import org.apache.commons.lang3.StringUtils;
-import software.amazon.awssdk.utils.ImmutableMap;
-import java.util.Map;
+import java.util.Objects;
import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
public final class PolicyVariableResolver {
- private static final String THING_NAMESPACE = "Thing";
- private static final String THING_NAME_ATTRIBUTE = "ThingName";
- private static final Map> policyVariableToAttributeProvider = ImmutableMap.of(
- "${iot:Connection.Thing.ThingName}", new Pair<>(THING_NAMESPACE, THING_NAME_ATTRIBUTE)
- );
+ private static final Function NO_ATTR_FOUND_EXCEPTION = policyVariable ->
+ new PolicyException(String.format("No attribute found for policy variable %s in current session",
+ policyVariable));
private PolicyVariableResolver() {
}
@@ -32,8 +32,8 @@ private PolicyVariableResolver() {
* This method does not handle unsupported policy variables.
*
* @param policyVariables list of policy variables in permission format
- * @param format permission format to resolve
- * @param session current device session
+ * @param format permission format to resolve
+ * @param session current device session
* @return updated format
* @throws PolicyException when unable to find a policy variable value
*/
@@ -43,23 +43,49 @@ public static String resolvePolicyVariables(Set policyVariables, String
return format;
}
String substitutedFormat = format;
- for (String policyVariable : policyVariables) {
- String attributeNamespace = policyVariableToAttributeProvider.get(policyVariable).getLeft();
- String attributeName = policyVariableToAttributeProvider.get(policyVariable).getRight();
- String policyVariableValue = Coerce.toString(session.getSessionAttribute(attributeNamespace,
- attributeName));
+ for (PolicyVariable policyVariable : policyVariables.stream()
+ .map(PolicyVariable::parse).map(v -> v.orElse(null))
+ .filter(Objects::nonNull)
+ .collect(Collectors.toList())) {
+
+ DeviceAttribute attr = session.getSessionAttribute(policyVariable.getAttribute());
+ if (policyVariable.getAttribute() == Attribute.THING_ATTRIBUTES
+ && !attr.matches(policyVariable.getSelector())) {
+ throw NO_ATTR_FOUND_EXCEPTION.apply(policyVariable);
+ }
+
+ String policyVariableValue = Coerce.toString(attr);
if (policyVariableValue == null) {
- throw new PolicyException(
- String.format("No attribute found for policy variable %s in current session", policyVariable));
- } else {
- // StringUtils.replace() is faster than String.replace() since it does not use regex
- substitutedFormat = StringUtils.replace(substitutedFormat, policyVariable, policyVariableValue);
+ throw NO_ATTR_FOUND_EXCEPTION.apply(policyVariable);
}
+
+ // StringUtils.replace() is faster than String.replace() since it does not use regex
+ substitutedFormat = StringUtils.replace(substitutedFormat,
+ policyVariable.getOriginalText(), policyVariableValue);
}
return substitutedFormat;
}
- public static boolean isPolicyVariable(String variable) {
- return policyVariableToAttributeProvider.containsKey(variable);
+ /**
+ * True if the variable is a supported policy variable.
+ *
+ * @param variable variable
+ * @return true if the variable is a support policy variable
+ */
+ public static boolean isSupportedPolicyVariable(String variable) {
+ return PolicyVariable.parse(variable).isPresent();
+ }
+
+ /**
+ * True if the following variable represents a thing attribute,
+ * such as ${iot:Connection.Thing.Attributes[myAttribute]}.
+ *
+ * @param variable variable
+ * @return true if variable is a thing attribute
+ */
+ public static boolean isAttributePolicyVariable(String variable) {
+ return PolicyVariable.parse(variable)
+ .filter(var -> var.getAttribute() == Attribute.THING_ATTRIBUTES)
+ .isPresent();
}
}
diff --git a/src/main/java/com/aws/greengrass/clientdevices/auth/iot/Certificate.java b/src/main/java/com/aws/greengrass/clientdevices/auth/iot/Certificate.java
index a5cc34a84..b5e990889 100644
--- a/src/main/java/com/aws/greengrass/clientdevices/auth/iot/Certificate.java
+++ b/src/main/java/com/aws/greengrass/clientdevices/auth/iot/Certificate.java
@@ -5,6 +5,7 @@
package com.aws.greengrass.clientdevices.auth.iot;
+import com.aws.greengrass.clientdevices.auth.session.attribute.Attribute;
import com.aws.greengrass.clientdevices.auth.session.attribute.AttributeProvider;
import com.aws.greengrass.clientdevices.auth.session.attribute.DeviceAttribute;
import com.aws.greengrass.clientdevices.auth.session.attribute.StringLiteralAttribute;
@@ -29,8 +30,6 @@
@Getter
public class Certificate implements AttributeProvider {
- public static final String NAMESPACE = "Certificate";
- private static final String CERTIFICATE_ID_ATTRIBUTE = "CertificateId";
private static final AtomicInteger metadataTrustDurationMinutes =
new AtomicInteger(DEFAULT_CLIENT_DEVICE_TRUST_DURATION_MINUTES);
@@ -125,13 +124,13 @@ public boolean wasUpdatedAfter(Certificate cert) {
@Override
public String getNamespace() {
- return NAMESPACE;
+ return Attribute.Namespaces.CERTIFICATE;
}
@Override
public DeviceAttribute getDeviceAttribute(String attributeName) {
// TODO: Support other DeviceAttributes
- if (CERTIFICATE_ID_ATTRIBUTE.equals(attributeName)) {
+ if (Attribute.CERTIFICATE_ID.getName().equals(attributeName)) {
return new StringLiteralAttribute(getCertificateId());
}
return null;
diff --git a/src/main/java/com/aws/greengrass/clientdevices/auth/iot/Component.java b/src/main/java/com/aws/greengrass/clientdevices/auth/iot/Component.java
index 01f8236cd..54b9e6c26 100644
--- a/src/main/java/com/aws/greengrass/clientdevices/auth/iot/Component.java
+++ b/src/main/java/com/aws/greengrass/clientdevices/auth/iot/Component.java
@@ -5,6 +5,7 @@
package com.aws.greengrass.clientdevices.auth.iot;
+import com.aws.greengrass.clientdevices.auth.session.attribute.Attribute;
import com.aws.greengrass.clientdevices.auth.session.attribute.AttributeProvider;
import com.aws.greengrass.clientdevices.auth.session.attribute.DeviceAttribute;
import lombok.Value;
@@ -14,12 +15,12 @@
@Value
public class Component implements AttributeProvider {
- public static final String NAMESPACE = "Component";
- private static final Map ATTRIBUTES = Collections.singletonMap("component", expr -> true);
+ private static final Map ATTRIBUTES =
+ Collections.singletonMap(Attribute.COMPONENT.getName(), expr -> true);
@Override
public String getNamespace() {
- return NAMESPACE;
+ return Attribute.Namespaces.COMPONENT;
}
@Override
diff --git a/src/main/java/com/aws/greengrass/clientdevices/auth/iot/IotClientFactory.java b/src/main/java/com/aws/greengrass/clientdevices/auth/iot/IotClientFactory.java
new file mode 100644
index 000000000..9441102f8
--- /dev/null
+++ b/src/main/java/com/aws/greengrass/clientdevices/auth/iot/IotClientFactory.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.aws.greengrass.clientdevices.auth.iot;
+
+import com.aws.greengrass.deployment.DeviceConfiguration;
+import com.aws.greengrass.deployment.exceptions.DeviceConfigurationException;
+import com.aws.greengrass.util.Coerce;
+import com.aws.greengrass.util.IotSdkClientFactory;
+import com.aws.greengrass.util.Utils;
+import com.aws.greengrass.util.exceptions.InvalidEnvironmentStageException;
+import software.amazon.awssdk.services.iot.IotClient;
+
+import java.net.URISyntaxException;
+import javax.inject.Inject;
+
+public class IotClientFactory {
+
+ private final DeviceConfiguration deviceConfiguration;
+
+ @Inject
+ public IotClientFactory(DeviceConfiguration deviceConfiguration) {
+ this.deviceConfiguration = deviceConfiguration;
+ }
+
+ /**
+ * Get an IoT Client.
+ *
+ * @return client
+ * @throws DeviceConfigurationException never
+ */
+ public IotClient getClient() throws DeviceConfigurationException {
+ try {
+ String stage = Coerce.toString(deviceConfiguration.getEnvironmentStage());
+ if (stage == null) {
+ throw new DeviceConfigurationException("Environment stage not configured");
+ }
+ return IotSdkClientFactory.getIotClient(
+ getAwsRegion(deviceConfiguration),
+ IotSdkClientFactory.EnvironmentStage.fromString(stage)
+ );
+ } catch (URISyntaxException | InvalidEnvironmentStageException e) {
+ throw new DeviceConfigurationException(e);
+ }
+ }
+
+ private String getAwsRegion(DeviceConfiguration deviceConfiguration) throws DeviceConfigurationException {
+ String awsRegion = Coerce.toString(deviceConfiguration.getAWSRegion());
+ if (Utils.isEmpty(awsRegion)) {
+ throw new DeviceConfigurationException("AWS region cannot be empty");
+ }
+ return awsRegion;
+ }
+}
diff --git a/src/main/java/com/aws/greengrass/clientdevices/auth/iot/IotCoreClient.java b/src/main/java/com/aws/greengrass/clientdevices/auth/iot/IotCoreClient.java
new file mode 100644
index 000000000..75089621a
--- /dev/null
+++ b/src/main/java/com/aws/greengrass/clientdevices/auth/iot/IotCoreClient.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.aws.greengrass.clientdevices.auth.iot;
+
+import com.aws.greengrass.clientdevices.auth.exception.CloudServiceInteractionException;
+import com.aws.greengrass.deployment.DeviceConfiguration;
+import com.aws.greengrass.deployment.exceptions.DeviceConfigurationException;
+import com.aws.greengrass.tes.LazyCredentialProvider;
+import software.amazon.awssdk.services.iot.IotClient;
+import software.amazon.awssdk.services.iot.model.DescribeThingRequest;
+
+import java.util.Map;
+import javax.inject.Inject;
+
+public interface IotCoreClient {
+
+ Map getThingAttributes(String thingName) throws CloudServiceInteractionException;
+
+ class Default implements IotCoreClient {
+
+ private final DeviceConfiguration deviceConfiguration;
+ private final IotClientFactory iotClientFactory;
+ private final LazyCredentialProvider lazyCredentialProvider;
+
+ @Inject
+ Default(DeviceConfiguration deviceConfiguration,
+ IotClientFactory iotClientFactory,
+ LazyCredentialProvider lazyCredentialProvider) {
+ this.deviceConfiguration = deviceConfiguration;
+ this.iotClientFactory = iotClientFactory;
+ this.lazyCredentialProvider = lazyCredentialProvider;
+ }
+
+ @Override
+ @SuppressWarnings("PMD.AvoidCatchingGenericException")
+ public Map getThingAttributes(String thingName) throws CloudServiceInteractionException {
+ try (IotClient client = iotClientFactory.getClient()) {
+ return client.describeThing(DescribeThingRequest.builder()
+ .thingName(thingName)
+ .build())
+ .attributes();
+ } catch (DeviceConfigurationException e) {
+ throw new CloudServiceInteractionException("Failed to construct IoT Core client", e);
+ } catch (Exception e) {
+ throw new CloudServiceInteractionException(
+ String.format("Failed to get %s thing attributes", thingName), e);
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/aws/greengrass/clientdevices/auth/iot/Thing.java b/src/main/java/com/aws/greengrass/clientdevices/auth/iot/Thing.java
index cea583313..5216444ae 100644
--- a/src/main/java/com/aws/greengrass/clientdevices/auth/iot/Thing.java
+++ b/src/main/java/com/aws/greengrass/clientdevices/auth/iot/Thing.java
@@ -5,6 +5,7 @@
package com.aws.greengrass.clientdevices.auth.iot;
+import com.aws.greengrass.clientdevices.auth.session.attribute.Attribute;
import com.aws.greengrass.clientdevices.auth.session.attribute.AttributeProvider;
import com.aws.greengrass.clientdevices.auth.session.attribute.DeviceAttribute;
import com.aws.greengrass.clientdevices.auth.session.attribute.WildcardSuffixAttribute;
@@ -29,8 +30,6 @@
*/
@Getter
public final class Thing implements AttributeProvider, Cloneable {
- public static final String NAMESPACE = "Thing";
- private static final String THING_NAME_ATTRIBUTE = "ThingName";
private static final String thingNamePattern = "[a-zA-Z0-9\\-_:]+";
public static final int MAX_THING_NAME_LENGTH = 128;
private static final AtomicInteger metadataTrustDurationMinutes =
@@ -164,16 +163,34 @@ public int hashCode() {
@Override
public String getNamespace() {
- return NAMESPACE;
+ return Attribute.Namespaces.THING;
}
@Override
public DeviceAttribute getDeviceAttribute(String attributeName) {
- // TODO: Support other DeviceAttributes
- if (THING_NAME_ATTRIBUTE.equals(attributeName)) {
+ if (Attribute.THING_NAME.getName().equals(attributeName)) {
return new WildcardSuffixAttribute(thingName);
}
- return null;
+ return getCachedIotCoreDeviceAttributes();
+ }
+
+ private DeviceAttribute getCachedIotCoreDeviceAttributes() {
+ return ThingAttributesCache.instance()
+ .map(cache -> new DeviceAttribute() {
+ private String resolvedAttr;
+
+ @Override
+ public boolean matches(String attribute) {
+ resolvedAttr = cache.getAttribute(thingName, attribute).orElse(null);
+ return !Objects.equals(resolvedAttr, null);
+ }
+
+ @Override
+ public String toString() {
+ return resolvedAttr;
+ }
+ })
+ .orElse(null);
}
/**
diff --git a/src/main/java/com/aws/greengrass/clientdevices/auth/iot/ThingAttributesCache.java b/src/main/java/com/aws/greengrass/clientdevices/auth/iot/ThingAttributesCache.java
new file mode 100644
index 000000000..fbd63d400
--- /dev/null
+++ b/src/main/java/com/aws/greengrass/clientdevices/auth/iot/ThingAttributesCache.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.aws.greengrass.clientdevices.auth.iot;
+
+import com.aws.greengrass.clientdevices.auth.exception.CloudServiceInteractionException;
+import com.aws.greengrass.clientdevices.auth.infra.NetworkStateProvider;
+import com.aws.greengrass.logging.api.Logger;
+import com.aws.greengrass.logging.impl.LogManager;
+import software.amazon.awssdk.services.greengrassv2.model.AssociatedClientDevice;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
+import javax.inject.Inject;
+
+public class ThingAttributesCache {
+
+ private static final Logger logger = LogManager.getLogger(ThingAttributesCache.class);
+
+ // set once during component install
+ private static final AtomicReference INSTANCE = new AtomicReference<>();
+ private final AtomicReference initialized = new AtomicReference<>();
+
+ private final IotCoreClient iotCoreClient;
+ private final IotAuthClient iotAuthClient;
+
+ private final Map> attributesByThing = new ConcurrentHashMap<>();
+
+ private final ScheduledExecutorService ses;
+ private final NetworkStateProvider networkStateProvider;
+ private ScheduledFuture> refreshTask;
+
+ public static Optional instance() {
+ return Optional.ofNullable(INSTANCE.get());
+ }
+
+ public static void setInstance(ThingAttributesCache cache) {
+ INSTANCE.set(cache);
+ }
+
+ /**
+ * Construct a ThingAttributesCache.
+ *
+ * @param iotCoreClient iot core client
+ * @param iotAuthClient iot auth client
+ * @param networkStateProvider network state provider
+ * @param ses scheduled executor service
+ */
+ @Inject
+ public ThingAttributesCache(IotCoreClient iotCoreClient,
+ IotAuthClient iotAuthClient,
+ NetworkStateProvider networkStateProvider,
+ ScheduledExecutorService ses) {
+ this.iotCoreClient = iotCoreClient;
+ this.iotAuthClient = iotAuthClient;
+ this.networkStateProvider = networkStateProvider;
+ this.ses = ses;
+ }
+
+ /**
+ * Can be called after {@link ThingAttributesCache#startPeriodicRefresh} in order to block
+ * until this class retrieves thing attributes. If attributes have already been loaded, this will return
+ * immediately.
+ *
+ * @param time time to wait
+ * @param unit time unit
+ * @return true if initialized
+ * @throws InterruptedException interrupted while waiting
+ */
+ public boolean waitForInitialization(long time, TimeUnit unit) throws InterruptedException {
+ CountDownLatch latch = initialized.get();
+ if (latch == null) {
+ return false;
+ }
+ return latch.await(time, unit);
+ }
+
+ private void resetInitialized() {
+ initialized.set(new CountDownLatch(1));
+ }
+
+ private void markAsInitialized() {
+ CountDownLatch latch = initialized.get();
+ if (latch != null) {
+ latch.countDown();
+ }
+ }
+
+ /**
+ * Refresh client device thing attributes from cloud, periodically.
+ */
+ public void startPeriodicRefresh() {
+ stopPeriodicRefresh();
+ // TODO configurable delay
+ refreshTask = ses.scheduleWithFixedDelay(this::refresh, 0L, 1L, TimeUnit.MINUTES);
+ }
+
+ /**
+ * Stop the client device thing attribute refresh process.
+ */
+ public void stopPeriodicRefresh() {
+ if (refreshTask != null) {
+ refreshTask.cancel(true);
+ }
+ resetInitialized();
+ }
+
+ private void refresh() {
+ if (networkStateProvider.getConnectionState() == NetworkStateProvider.ConnectionState.NETWORK_DOWN) {
+ // TODO cache attributes on disk and load here, handle case if device restarts while offline
+ logger.atTrace().log("network down, unable to refresh thing-attribute cache");
+ return;
+ }
+ logger.atTrace().log("beginning thing-attribute cache refresh");
+ getAssociatedThingNames().ifPresent(thingNames -> {
+ for (String thingName : thingNames) {
+ if (Thread.currentThread().isInterrupted()) {
+ return;
+ }
+ fetchDeviceAttributes(thingName).ifPresent(attrs -> {
+ logger.atInfo().kv("thing", thingName).log("attributes refreshed for device");
+ attributesByThing.put(thingName, new ConcurrentHashMap<>(attrs));
+ });
+ }
+ // TODO handle case where some fetches fail
+ markAsInitialized();
+ });
+ }
+
+ @SuppressWarnings("PMD.AvoidCatchingGenericException")
+ private Optional> getAssociatedThingNames() {
+ try {
+ return Optional.of(iotAuthClient.getThingsAssociatedWithCoreDevice()
+ .flatMap(List::stream)
+ .map(AssociatedClientDevice::thingName)
+ .collect(Collectors.toSet()));
+ } catch (Exception e) {
+ logger.atWarn()
+ .log("Unable to find associated things");
+ return Optional.empty();
+ }
+ }
+
+ private Optional
*/
public final class RuntimeConfiguration {
@@ -229,12 +229,22 @@ public void removeCertificateV1(String certificateId) {
}
}
+ /**
+ * Persist thing association dto.
+ *
+ * @param dto dto
+ */
public void putThingAssociationV1(ThingAssociationV1DTO dto) {
Topics t = getOrRepairTopics(config, ASSOCIATIONS_KEY, ASSOCIATIONS_V1);
t.lookup(ASSOCIATIONS_PROP_KEY).withValue(new ArrayList<>(dto.getAssociatedThingNames()));
t.lookup(LAST_UPDATED_KEY).withValue(dto.getLastUpdated().toEpochSecond(ZoneOffset.UTC));
}
+ /**
+ * Get things associated with the core device.
+ *
+ * @return dto
+ */
public Optional getThingAssociationV1() {
Topics t = config.findTopics(ASSOCIATIONS_KEY, ASSOCIATIONS_V1);
if (t == null) {
@@ -256,6 +266,11 @@ public Optional getThingAssociationV1() {
return Optional.of(new ThingAssociationV1DTO(thingNames, lastFetched));
}
+ /**
+ * Persist thing description dto.
+ *
+ * @param dto dto
+ */
public void putThingDescriptionV1(ThingDescriptionV1DTO dto) {
Topics t = getOrRepairTopics(config, DESCRIPTION_KEY, DESCRIPTION_V1, dto.getThingName());
t.lookup(LAST_UPDATED_KEY).withValue(dto.getLastUpdated().toEpochSecond(ZoneOffset.UTC));
@@ -263,6 +278,12 @@ public void putThingDescriptionV1(ThingDescriptionV1DTO dto) {
getOrRepairTopics(t, ATTRIBUTES_PROP_KEY).replaceAndWait(attrs);
}
+ /**
+ * Get cached IoT describe-thing response. Currently, we only care about the attributes field.
+ *
+ * @param thingName thing name
+ * @return dto
+ */
public Optional getThingDescriptionV1(String thingName) {
Topics t = config.findTopics(DESCRIPTION_KEY, DESCRIPTION_V1, thingName);
if (t == null) {
@@ -340,8 +361,8 @@ public Stream getAllCertificatesV1() {
/**
* Put hostAddresses config.
*
- * @param source connectivity information source
- * @param hostAddresses host addresses
+ * @param source connectivity information source
+ * @param hostAddresses host addresses
*/
public void putHostAddressForSource(String source, Set hostAddresses) {
config.lookup(HOST_ADDRESSES_KEY, source)