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> fetchDeviceAttributes(String thingName) { + try { + return Optional.ofNullable(iotCoreClient.getThingAttributes(thingName)); + } catch (CloudServiceInteractionException e) { + logger.atWarn() + .kv("thing", thingName) + .log("Unable to get thing attributes"); + return Optional.empty(); + } + } + + public Optional getAttribute(String thingName, String attribute) { + return Optional.ofNullable(attributesByThing.get(thingName)) + .map(attrs -> attrs.get(attribute)); + } +} diff --git a/src/main/java/com/aws/greengrass/clientdevices/auth/session/Session.java b/src/main/java/com/aws/greengrass/clientdevices/auth/session/Session.java index 93c067696..b1b448199 100644 --- a/src/main/java/com/aws/greengrass/clientdevices/auth/session/Session.java +++ b/src/main/java/com/aws/greengrass/clientdevices/auth/session/Session.java @@ -5,6 +5,7 @@ package com.aws.greengrass.clientdevices.auth.session; +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; @@ -13,17 +14,16 @@ public interface Session { /** * Get attribute provider by namespace. * - * @param attributeProviderNameSpace Attribute namespace + * @param namespace attribute provider namespace * @return Attribute provider */ - AttributeProvider getAttributeProvider(String attributeProviderNameSpace); + AttributeProvider getAttributeProvider(String namespace); /** * Get session attribute. * - * @param attributeNamespace Attribute namespace - * @param attributeName Attribute name + * @param attribute attribute * @return Session attribute */ - DeviceAttribute getSessionAttribute(String attributeNamespace, String attributeName); + DeviceAttribute getSessionAttribute(Attribute attribute); } diff --git a/src/main/java/com/aws/greengrass/clientdevices/auth/session/SessionImpl.java b/src/main/java/com/aws/greengrass/clientdevices/auth/session/SessionImpl.java index 61aa6f406..a35774794 100644 --- a/src/main/java/com/aws/greengrass/clientdevices/auth/session/SessionImpl.java +++ b/src/main/java/com/aws/greengrass/clientdevices/auth/session/SessionImpl.java @@ -5,6 +5,7 @@ package com.aws.greengrass.clientdevices.auth.session; +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; @@ -27,21 +28,20 @@ public SessionImpl(AttributeProvider... providers) { } @Override - public AttributeProvider getAttributeProvider(String attributeProviderNameSpace) { - return this.get(attributeProviderNameSpace); + public AttributeProvider getAttributeProvider(String namespace) { + return this.get(namespace); } /** * Get session attribute. * - * @param attributeNamespace Attribute namespace - * @param attributeName Attribute name + * @param attribute attribute * @return Session attribute */ @Override - public DeviceAttribute getSessionAttribute(String attributeNamespace, String attributeName) { - if (this.getAttributeProvider(attributeNamespace) != null) { - return this.getAttributeProvider(attributeNamespace).getDeviceAttribute(attributeName); + public DeviceAttribute getSessionAttribute(Attribute attribute) { + if (this.getAttributeProvider(attribute.getNamespace()) != null) { + return this.getAttributeProvider(attribute.getNamespace()).getDeviceAttribute(attribute.getName()); } return null; } diff --git a/src/main/java/com/aws/greengrass/clientdevices/auth/session/attribute/Attribute.java b/src/main/java/com/aws/greengrass/clientdevices/auth/session/attribute/Attribute.java new file mode 100644 index 000000000..321985991 --- /dev/null +++ b/src/main/java/com/aws/greengrass/clientdevices/auth/session/attribute/Attribute.java @@ -0,0 +1,32 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.aws.greengrass.clientdevices.auth.session.attribute; + + +import lombok.Getter; + +@Getter +public enum Attribute { + + THING_NAME(Namespaces.THING, "ThingName"), + THING_ATTRIBUTES(Namespaces.THING, "ThingAttributes"), + CERTIFICATE_ID(Namespaces.CERTIFICATE, "CertificateId"), + COMPONENT(Namespaces.COMPONENT, "component"); + + private final String namespace; + private final String name; + + Attribute(String namespace, String name) { + this.namespace = namespace; + this.name = name; + } + + public static class Namespaces { + public static final String THING = "Thing"; + public static final String CERTIFICATE = "Certificate"; + public static final String COMPONENT = "Component"; + } +} diff --git a/src/test/java/com/aws/greengrass/clientdevices/auth/DeviceAuthClientTest.java b/src/test/java/com/aws/greengrass/clientdevices/auth/DeviceAuthClientTest.java index 6a4a66d8c..7ec930889 100644 --- a/src/test/java/com/aws/greengrass/clientdevices/auth/DeviceAuthClientTest.java +++ b/src/test/java/com/aws/greengrass/clientdevices/auth/DeviceAuthClientTest.java @@ -9,6 +9,7 @@ import com.aws.greengrass.clientdevices.auth.iot.Certificate; import com.aws.greengrass.clientdevices.auth.iot.CertificateFake; import com.aws.greengrass.clientdevices.auth.iot.Thing; +import com.aws.greengrass.clientdevices.auth.session.attribute.Attribute; import com.aws.greengrass.componentmanager.KernelConfigResolver; import com.aws.greengrass.config.Topics; import com.aws.greengrass.dependency.Context; @@ -107,7 +108,7 @@ void GIVEN_sessionHasPolicyVariablesPermission_WHEN_canDevicePerform_THEN_author Session session = new SessionImpl(cert, thing); when(sessionManager.findSession(SESSION_ID)).thenReturn(session); - String thingName = Coerce.toString(session.getSessionAttribute("Thing", "ThingName")); + String thingName = Coerce.toString(session.getSessionAttribute(Attribute.THING_NAME)); when(groupManager.getApplicablePolicyPermissions(session)).thenReturn(Collections.singletonMap("group1", Collections.singleton(Permission.builder().operation("mqtt:publish") .resource("mqtt:topic:${iot:Connection.Thing.ThingName}").principal("group1") diff --git a/src/test/java/com/aws/greengrass/clientdevices/auth/configuration/GroupDefinitionTest.java b/src/test/java/com/aws/greengrass/clientdevices/auth/configuration/GroupDefinitionTest.java index 82ab28bec..a085b253e 100644 --- a/src/test/java/com/aws/greengrass/clientdevices/auth/configuration/GroupDefinitionTest.java +++ b/src/test/java/com/aws/greengrass/clientdevices/auth/configuration/GroupDefinitionTest.java @@ -28,7 +28,7 @@ void GIVEN_groupDefinitionAndMatchingSession_WHEN_containsSession_THEN_returnsTr GroupDefinition groupDefinition = new GroupDefinition("thingName: thing", "Policy1"); Session session = Mockito.mock(Session.class); DeviceAttribute attribute = new WildcardSuffixAttribute("thing"); - Mockito.when(session.getSessionAttribute(any(), any())).thenReturn(attribute); + Mockito.when(session.getSessionAttribute(any())).thenReturn(attribute); assertThat(groupDefinition.containsClientDevice(session), is(true)); } @@ -38,7 +38,7 @@ void GIVEN_groupDefinitionWithTrailingWildcardAndMatchingSession_WHEN_containsSe GroupDefinition groupDefinition = new GroupDefinition("thingName: thing*", "Policy1"); Session session = Mockito.mock(Session.class); DeviceAttribute attribute = new WildcardSuffixAttribute("thing-A"); - Mockito.when(session.getSessionAttribute(any(), any())).thenReturn(attribute); + Mockito.when(session.getSessionAttribute(any())).thenReturn(attribute); assertThat(groupDefinition.containsClientDevice(session), is(true)); } @@ -48,7 +48,7 @@ void GIVEN_groupDefinitionWithLeadingWildcardAndMatchingSession_WHEN_containsSes GroupDefinition groupDefinition = new GroupDefinition("thingName: *thing", "Policy1"); Session session = Mockito.mock(Session.class); DeviceAttribute attribute = new WildcardSuffixAttribute("A-thing"); - Mockito.when(session.getSessionAttribute(any(), any())).thenReturn(attribute); + Mockito.when(session.getSessionAttribute(any())).thenReturn(attribute); assertThat(groupDefinition.containsClientDevice(session), is(true)); } @@ -58,7 +58,7 @@ void GIVEN_groupDefinitionWithLeadingAndTrailingWildcardAndMatchingSession_WHEN_ GroupDefinition groupDefinition = new GroupDefinition("thingName: *thing*", "Policy1"); Session session = Mockito.mock(Session.class); DeviceAttribute attribute = new WildcardSuffixAttribute("A-thing-B"); - Mockito.when(session.getSessionAttribute(any(), any())).thenReturn(attribute); + Mockito.when(session.getSessionAttribute(any())).thenReturn(attribute); assertThat(groupDefinition.containsClientDevice(session), is(true)); } diff --git a/src/test/java/com/aws/greengrass/clientdevices/auth/configuration/PolicyVariableResolverTest.java b/src/test/java/com/aws/greengrass/clientdevices/auth/configuration/PolicyVariableResolverTest.java index b72d4fbb1..70f4d7c88 100644 --- a/src/test/java/com/aws/greengrass/clientdevices/auth/configuration/PolicyVariableResolverTest.java +++ b/src/test/java/com/aws/greengrass/clientdevices/auth/configuration/PolicyVariableResolverTest.java @@ -12,6 +12,7 @@ import com.aws.greengrass.clientdevices.auth.iot.Thing; import com.aws.greengrass.clientdevices.auth.session.Session; import com.aws.greengrass.clientdevices.auth.session.SessionImpl; +import com.aws.greengrass.clientdevices.auth.session.attribute.Attribute; import com.aws.greengrass.clientdevices.auth.session.attribute.WildcardSuffixAttribute; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -29,8 +30,6 @@ public class PolicyVariableResolverTest { private static final String FAKE_CERT_ID = "FAKE_CERT_ID"; private static final String THING_NAME = "b"; - private static final String THING_NAMESPACE = "Thing"; - private static final String THING_NAME_ATTRIBUTE = "ThingName"; private Certificate cert; private Thing thing; private Session session; @@ -71,7 +70,7 @@ void GIVEN_invalid_resource_and_policy_variables_WHEN_resolve_policy_variables_T void GIVEN_valid_resource_and_policy_variables_WHEN_no_session_attribute_THEN_throw_exception() { String resource = "msg/${iot:Connection.Thing.ThingName}/test"; - when(mockSession.getSessionAttribute(THING_NAMESPACE, THING_NAME_ATTRIBUTE)).thenReturn(wildcardSuffixAttribute); + when(mockSession.getSessionAttribute(Attribute.THING_NAME)).thenReturn(wildcardSuffixAttribute); when(wildcardSuffixAttribute.toString()).thenReturn(null); diff --git a/src/test/java/com/aws/greengrass/clientdevices/auth/configuration/parser/RuleExpressionEvaluationTest.java b/src/test/java/com/aws/greengrass/clientdevices/auth/configuration/parser/RuleExpressionEvaluationTest.java index c91355ded..440e411d3 100644 --- a/src/test/java/com/aws/greengrass/clientdevices/auth/configuration/parser/RuleExpressionEvaluationTest.java +++ b/src/test/java/com/aws/greengrass/clientdevices/auth/configuration/parser/RuleExpressionEvaluationTest.java @@ -31,7 +31,7 @@ ASTStart getTree(String expressionString) throws ParseException { Session getSessionWithThing(String thingName) { Session session = Mockito.mock(Session.class); DeviceAttribute attribute = new WildcardSuffixAttribute(thingName); - Mockito.when(session.getSessionAttribute(any(), any())).thenReturn(attribute); + Mockito.when(session.getSessionAttribute(any())).thenReturn(attribute); return session; } diff --git a/src/test/java/com/aws/greengrass/clientdevices/auth/iot/IotCoreClientFake.java b/src/test/java/com/aws/greengrass/clientdevices/auth/iot/IotCoreClientFake.java new file mode 100644 index 000000000..b0c717156 --- /dev/null +++ b/src/test/java/com/aws/greengrass/clientdevices/auth/iot/IotCoreClientFake.java @@ -0,0 +1,39 @@ +/* + * 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 java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class IotCoreClientFake implements IotCoreClient { + + private final Map attributes = new ConcurrentHashMap<>(); + private volatile boolean down; + + public void setThingAttributes(Map attributes) { + this.attributes.clear(); + this.attributes.putAll(attributes); + } + + public void down() { + down = true; + } + + public void up() { + down = false; + } + + @Override + public Map getThingAttributes(String thingName) throws CloudServiceInteractionException { + if (down) { + throw new CloudServiceInteractionException("service is down"); + } + return new HashMap<>(attributes); + } +} diff --git a/src/test/java/com/aws/greengrass/clientdevices/auth/session/MqttSessionFactoryTest.java b/src/test/java/com/aws/greengrass/clientdevices/auth/session/MqttSessionFactoryTest.java index d1a40db5b..c745c373a 100644 --- a/src/test/java/com/aws/greengrass/clientdevices/auth/session/MqttSessionFactoryTest.java +++ b/src/test/java/com/aws/greengrass/clientdevices/auth/session/MqttSessionFactoryTest.java @@ -12,13 +12,13 @@ import com.aws.greengrass.clientdevices.auth.infra.NetworkStateProvider; import com.aws.greengrass.clientdevices.auth.iot.CertificateFake; import com.aws.greengrass.clientdevices.auth.iot.CertificateRegistry; -import com.aws.greengrass.clientdevices.auth.iot.Component; import com.aws.greengrass.clientdevices.auth.iot.InvalidCertificateException; import com.aws.greengrass.clientdevices.auth.iot.IotAuthClient; import com.aws.greengrass.clientdevices.auth.iot.Thing; import com.aws.greengrass.clientdevices.auth.iot.infra.ThingRegistry; import com.aws.greengrass.clientdevices.auth.iot.usecases.CreateIoTThingSession; import com.aws.greengrass.clientdevices.auth.iot.usecases.VerifyThingAttachedToCertificate; +import com.aws.greengrass.clientdevices.auth.session.attribute.Attribute; import com.aws.greengrass.dependency.Context; import com.aws.greengrass.testcommons.testutilities.GGExtension; import com.aws.greengrass.util.Pair; @@ -146,6 +146,6 @@ void GIVEN_componentWithValidClientId_WHEN_createSession_THEN_returnsSession() t Session session = mqttSessionFactory.createSession(credentialMap); assertThat(session, is(IsNull.notNullValue())); - assertThat(session.getSessionAttribute(Component.NAMESPACE, "component"), notNullValue()); + assertThat(session.getSessionAttribute(Attribute.COMPONENT), notNullValue()); } } diff --git a/src/test/java/com/aws/greengrass/clientdevices/auth/session/SessionImplTest.java b/src/test/java/com/aws/greengrass/clientdevices/auth/session/SessionImplTest.java index 6717523bd..14cd67003 100644 --- a/src/test/java/com/aws/greengrass/clientdevices/auth/session/SessionImplTest.java +++ b/src/test/java/com/aws/greengrass/clientdevices/auth/session/SessionImplTest.java @@ -9,6 +9,7 @@ import com.aws.greengrass.clientdevices.auth.iot.CertificateFake; import com.aws.greengrass.clientdevices.auth.iot.InvalidCertificateException; import com.aws.greengrass.clientdevices.auth.iot.Thing; +import com.aws.greengrass.clientdevices.auth.session.attribute.Attribute; import com.aws.greengrass.testcommons.testutilities.GGExtension; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -25,9 +26,9 @@ public void GIVEN_sessionWithThingAndCert_WHEN_getSessionAttributes_THEN_attribu Thing thing = Thing.of("MyThing"); Session session = new SessionImpl(cert, thing); - Assertions.assertEquals(session.getSessionAttribute("Certificate", "CertificateId").toString(), + Assertions.assertEquals(session.getSessionAttribute(Attribute.CERTIFICATE_ID).toString(), cert.getDeviceAttribute("CertificateId").toString()); - Assertions.assertEquals(session.getSessionAttribute("Thing", "ThingName").toString(), + Assertions.assertEquals(session.getSessionAttribute(Attribute.THING_NAME).toString(), thing.getDeviceAttribute("ThingName").toString()); } } From 631cd15ce8d1849bf6055d4da40ca2976e5d5cb0 Mon Sep 17 00:00:00 2001 From: Joseph Cosentino Date: Fri, 28 Jun 2024 15:38:27 -0700 Subject: [PATCH 2/5] chore: start adding cache support --- .../configuration/RuntimeConfiguration.java | 18 ++++ .../clientdevices/auth/iot/IotCoreClient.java | 4 +- .../auth/iot/ThingAttributesCache.java | 86 ++++++++++++++++--- .../auth/iot/dto/ThingAssociationV1DTO.java | 19 ++++ .../auth/iot/dto/ThingDescriptionV1DTO.java | 20 +++++ 5 files changed, 134 insertions(+), 13 deletions(-) create mode 100644 src/main/java/com/aws/greengrass/clientdevices/auth/iot/dto/ThingAssociationV1DTO.java create mode 100644 src/main/java/com/aws/greengrass/clientdevices/auth/iot/dto/ThingDescriptionV1DTO.java diff --git a/src/main/java/com/aws/greengrass/clientdevices/auth/configuration/RuntimeConfiguration.java b/src/main/java/com/aws/greengrass/clientdevices/auth/configuration/RuntimeConfiguration.java index 1948efcec..85215498f 100644 --- a/src/main/java/com/aws/greengrass/clientdevices/auth/configuration/RuntimeConfiguration.java +++ b/src/main/java/com/aws/greengrass/clientdevices/auth/configuration/RuntimeConfiguration.java @@ -7,6 +7,8 @@ import com.aws.greengrass.clientdevices.auth.connectivity.HostAddress; import com.aws.greengrass.clientdevices.auth.iot.dto.CertificateV1DTO; +import com.aws.greengrass.clientdevices.auth.iot.dto.ThingAssociationV1DTO; +import com.aws.greengrass.clientdevices.auth.iot.dto.ThingDescriptionV1DTO; import com.aws.greengrass.clientdevices.auth.iot.dto.ThingV1DTO; import com.aws.greengrass.config.Node; import com.aws.greengrass.config.Topic; @@ -204,6 +206,22 @@ public void removeCertificateV1(String certificateId) { } } + public void putThingAssociationV1(ThingAssociationV1DTO thingAssociationV1DTO) { + // TODO + } + + public ThingAssociationV1DTO getThingAssociationV1() { + return null; // TODO + } + + public void putThingDescriptionV1(ThingDescriptionV1DTO thingDescriptionV1DTO) { + // TODO + } + + public ThingDescriptionV1DTO getThingDescriptionV1(String thingName) { + return null; // TODO + } + private Topics getOrRepairTopics(Topics root, String... path) { try { return root.lookupTopics(path); 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 index 75089621a..3a8bccdc8 100644 --- a/src/main/java/com/aws/greengrass/clientdevices/auth/iot/IotCoreClient.java +++ b/src/main/java/com/aws/greengrass/clientdevices/auth/iot/IotCoreClient.java @@ -12,6 +12,7 @@ import software.amazon.awssdk.services.iot.IotClient; import software.amazon.awssdk.services.iot.model.DescribeThingRequest; +import java.util.Collections; import java.util.Map; import javax.inject.Inject; @@ -38,10 +39,11 @@ class Default implements IotCoreClient { @SuppressWarnings("PMD.AvoidCatchingGenericException") public Map getThingAttributes(String thingName) throws CloudServiceInteractionException { try (IotClient client = iotClientFactory.getClient()) { - return client.describeThing(DescribeThingRequest.builder() + Map attributes = client.describeThing(DescribeThingRequest.builder() .thingName(thingName) .build()) .attributes(); + return attributes == null ? Collections.emptyMap() : attributes; } catch (DeviceConfigurationException e) { throw new CloudServiceInteractionException("Failed to construct IoT Core client", e); } catch (Exception e) { 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 index fbd63d400..7a284bb74 100644 --- a/src/main/java/com/aws/greengrass/clientdevices/auth/iot/ThingAttributesCache.java +++ b/src/main/java/com/aws/greengrass/clientdevices/auth/iot/ThingAttributesCache.java @@ -5,12 +5,16 @@ package com.aws.greengrass.clientdevices.auth.iot; +import com.aws.greengrass.clientdevices.auth.configuration.RuntimeConfiguration; import com.aws.greengrass.clientdevices.auth.exception.CloudServiceInteractionException; import com.aws.greengrass.clientdevices.auth.infra.NetworkStateProvider; +import com.aws.greengrass.clientdevices.auth.iot.dto.ThingAssociationV1DTO; +import com.aws.greengrass.clientdevices.auth.iot.dto.ThingDescriptionV1DTO; import com.aws.greengrass.logging.api.Logger; import com.aws.greengrass.logging.impl.LogManager; import software.amazon.awssdk.services.greengrassv2.model.AssociatedClientDevice; +import java.time.LocalDateTime; import java.util.List; import java.util.Map; import java.util.Optional; @@ -28,6 +32,13 @@ public class ThingAttributesCache { private static final Logger logger = LogManager.getLogger(ThingAttributesCache.class); + private static final long DEFAULT_REFRESH_DELAY_SECONDS = + TimeUnit.MINUTES.toSeconds(1); + private static final long DEFAULT_THING_ASSOCIATION_TRUST_DURATION_SECONDS = + TimeUnit.MINUTES.toSeconds(5); + private static final long DEFAULT_THING_DESCRIPTION_TRUST_DURATION_SECONDS = + TimeUnit.MINUTES.toSeconds(10); + // set once during component install private static final AtomicReference INSTANCE = new AtomicReference<>(); private final AtomicReference initialized = new AtomicReference<>(); @@ -41,6 +52,8 @@ public class ThingAttributesCache { private final NetworkStateProvider networkStateProvider; private ScheduledFuture refreshTask; + private final RuntimeConfiguration runtimeConfiguration; + public static Optional instance() { return Optional.ofNullable(INSTANCE.get()); } @@ -55,16 +68,19 @@ public static void setInstance(ThingAttributesCache cache) { * @param iotCoreClient iot core client * @param iotAuthClient iot auth client * @param networkStateProvider network state provider + * @param runtimeConfiguration runtime configuration * @param ses scheduled executor service */ @Inject public ThingAttributesCache(IotCoreClient iotCoreClient, IotAuthClient iotAuthClient, NetworkStateProvider networkStateProvider, + RuntimeConfiguration runtimeConfiguration, ScheduledExecutorService ses) { this.iotCoreClient = iotCoreClient; this.iotAuthClient = iotAuthClient; this.networkStateProvider = networkStateProvider; + this.runtimeConfiguration = runtimeConfiguration; this.ses = ses; } @@ -102,8 +118,9 @@ private void markAsInitialized() { */ public void startPeriodicRefresh() { stopPeriodicRefresh(); - // TODO configurable delay - refreshTask = ses.scheduleWithFixedDelay(this::refresh, 0L, 1L, TimeUnit.MINUTES); + // TODO pull from configuration + refreshTask = ses.scheduleWithFixedDelay(this::refresh, 0L, + DEFAULT_REFRESH_DELAY_SECONDS, TimeUnit.SECONDS); } /** @@ -117,29 +134,49 @@ public void stopPeriodicRefresh() { } 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 -> { + getThingAttributes(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 + // TODO it's currently possible that not all thing attributes were successfully + // fetched at this point, meaning that CDA will have started and devices + // will be rejected until subsequent refreshes as executed successfully. markAsInitialized(); }); } - @SuppressWarnings("PMD.AvoidCatchingGenericException") private Optional> getAssociatedThingNames() { + // use cached value, provided it's not stale + ThingAssociationV1DTO dto = runtimeConfiguration.getThingAssociationV1(); + // TODO pull from configuration + if (dto != null && dto.getLastFetched().plusSeconds(DEFAULT_THING_ASSOCIATION_TRUST_DURATION_SECONDS) + .isBefore(LocalDateTime.now())) { + logger.atTrace().log("Using locally cached thing associations"); + return Optional.ofNullable(dto.getAssociatedThingNames()); + } + + if (networkStateProvider.getConnectionState() == NetworkStateProvider.ConnectionState.NETWORK_DOWN) { + logger.atTrace().log("Network down, unable to fetch thing associations from cloud"); + return Optional.empty(); + } + + // otherwise fetch new associations from cloud and write to cache + logger.atTrace().log("Fetching thing associations from cloud"); + Optional> associatedThingNames = fetchAssociatedThingNames(); + associatedThingNames.ifPresent(names -> + runtimeConfiguration.putThingAssociationV1(new ThingAssociationV1DTO(names, LocalDateTime.now()))); + return associatedThingNames; + } + + @SuppressWarnings("PMD.AvoidCatchingGenericException") + private Optional> fetchAssociatedThingNames() { try { return Optional.of(iotAuthClient.getThingsAssociatedWithCoreDevice() .flatMap(List::stream) @@ -147,16 +184,41 @@ private Optional> getAssociatedThingNames() { .collect(Collectors.toSet())); } catch (Exception e) { logger.atWarn() + .cause(e) .log("Unable to find associated things"); return Optional.empty(); } } - private Optional> fetchDeviceAttributes(String thingName) { + private Optional> getThingAttributes(String thingName) { + // use cached value, provided it's not stale + ThingDescriptionV1DTO dto = runtimeConfiguration.getThingDescriptionV1(thingName); + // TODO pull from configuration + if (dto != null && dto.getLastFetched().plusSeconds(DEFAULT_THING_DESCRIPTION_TRUST_DURATION_SECONDS) + .isBefore(LocalDateTime.now())) { + logger.atTrace().log("Using locally cached thing description"); + return Optional.ofNullable(dto.getAttributes()); + } + + if (networkStateProvider.getConnectionState() == NetworkStateProvider.ConnectionState.NETWORK_DOWN) { + logger.atTrace().log("Network down, unable to fetch thing description from cloud"); + return Optional.empty(); + } + + // otherwise fetch new description from cloud and write to cache + logger.atTrace().log("Fetching thing description from cloud"); + Optional> attributes = fetchThingAttributes(thingName); + attributes.ifPresent(attrs -> + runtimeConfiguration.putThingDescriptionV1(new ThingDescriptionV1DTO(thingName, attrs, LocalDateTime.now()))); + return attributes; + } + + private Optional> fetchThingAttributes(String thingName) { try { - return Optional.ofNullable(iotCoreClient.getThingAttributes(thingName)); + return Optional.of(iotCoreClient.getThingAttributes(thingName)); } catch (CloudServiceInteractionException e) { logger.atWarn() + .cause(e) .kv("thing", thingName) .log("Unable to get thing attributes"); return Optional.empty(); diff --git a/src/main/java/com/aws/greengrass/clientdevices/auth/iot/dto/ThingAssociationV1DTO.java b/src/main/java/com/aws/greengrass/clientdevices/auth/iot/dto/ThingAssociationV1DTO.java new file mode 100644 index 000000000..1ba7343c4 --- /dev/null +++ b/src/main/java/com/aws/greengrass/clientdevices/auth/iot/dto/ThingAssociationV1DTO.java @@ -0,0 +1,19 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.aws.greengrass.clientdevices.auth.iot.dto; + +import lombok.AllArgsConstructor; +import lombok.Value; + +import java.time.LocalDateTime; +import java.util.Set; + +@Value +@AllArgsConstructor +public class ThingAssociationV1DTO { + Set associatedThingNames; + LocalDateTime lastFetched; +} diff --git a/src/main/java/com/aws/greengrass/clientdevices/auth/iot/dto/ThingDescriptionV1DTO.java b/src/main/java/com/aws/greengrass/clientdevices/auth/iot/dto/ThingDescriptionV1DTO.java new file mode 100644 index 000000000..8c05c4598 --- /dev/null +++ b/src/main/java/com/aws/greengrass/clientdevices/auth/iot/dto/ThingDescriptionV1DTO.java @@ -0,0 +1,20 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.aws.greengrass.clientdevices.auth.iot.dto; + +import lombok.AllArgsConstructor; +import lombok.Value; + +import java.time.LocalDateTime; +import java.util.Map; + +@Value +@AllArgsConstructor +public class ThingDescriptionV1DTO { + String thingName; + Map attributes; + LocalDateTime lastFetched; +} From c686e1f7dcf7445ded15b4c2741d4c60f7d587c6 Mon Sep 17 00:00:00 2001 From: Joseph Cosentino Date: Fri, 28 Jun 2024 16:16:22 -0700 Subject: [PATCH 3/5] chore: finish cache impl --- .../configuration/RuntimeConfiguration.java | 78 +++++++++++++++++-- .../auth/iot/ThingAttributesCache.java | 12 +-- .../auth/iot/dto/ThingAssociationV1DTO.java | 2 +- .../auth/iot/dto/ThingDescriptionV1DTO.java | 2 +- 4 files changed, 78 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/aws/greengrass/clientdevices/auth/configuration/RuntimeConfiguration.java b/src/main/java/com/aws/greengrass/clientdevices/auth/configuration/RuntimeConfiguration.java index 85215498f..c6c6720bd 100644 --- a/src/main/java/com/aws/greengrass/clientdevices/auth/configuration/RuntimeConfiguration.java +++ b/src/main/java/com/aws/greengrass/clientdevices/auth/configuration/RuntimeConfiguration.java @@ -16,6 +16,11 @@ import com.aws.greengrass.util.Coerce; import lombok.NonNull; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -46,6 +51,16 @@ * | |---- certificateId: * | |---- "s": status * | |---- "l": lastUpdated + * | |---- "clientDeviceThingAssociations": + * | |---- "v1": + * | |---- associations: [...] + * | |---- "l": lastUpdated + * | |---- "clientDeviceThingDescription": + * | |---- "v1": + * | |---- thingName: + * | |---- "attributes": + * | |---- k:v + * | |---- "l": lastUpdated * | |---- "hostAddresses": * |---- : * |---- [...] @@ -64,6 +79,14 @@ public final class RuntimeConfiguration { static final String CERTS_STATUS_UPDATED_KEY = "l"; private static final String HOST_ADDRESSES_KEY = "hostAddresses"; + private static final String ASSOCIATIONS_KEY = "clientDeviceThingAssociations"; + private static final String ASSOCIATIONS_V1 = "v1"; + private static final String DESCRIPTION_KEY = "clientDeviceThingDescription"; + private static final String DESCRIPTION_V1 = "v1"; + private static final String LAST_UPDATED_KEY = "l"; + private static final String ASSOCIATIONS_PROP_KEY = "associations"; + private static final String ATTRIBUTES_PROP_KEY = "attributes"; + private final Topics config; @@ -206,20 +229,59 @@ public void removeCertificateV1(String certificateId) { } } - public void putThingAssociationV1(ThingAssociationV1DTO thingAssociationV1DTO) { - // TODO + 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)); } - public ThingAssociationV1DTO getThingAssociationV1() { - return null; // TODO + public Optional getThingAssociationV1() { + Topics t = config.findTopics(ASSOCIATIONS_KEY, ASSOCIATIONS_V1); + if (t == null) { + return Optional.empty(); + } + + Set thingNames = new HashSet<>(); + Topic associationsTopic = t.find(ASSOCIATIONS_PROP_KEY); + if (associationsTopic != null) { + thingNames.addAll(Coerce.toStringList(associationsTopic)); + } + + LocalDateTime lastFetched = null; + Topic lastFetchedTopic = t.find(LAST_UPDATED_KEY); + if (lastFetchedTopic != null) { + lastFetched = LocalDateTime.ofInstant(Instant.ofEpochMilli(Coerce.toLong(lastFetchedTopic)), + ZoneId.of("UTC")); + } + return Optional.of(new ThingAssociationV1DTO(thingNames, lastFetched)); } - public void putThingDescriptionV1(ThingDescriptionV1DTO thingDescriptionV1DTO) { - // TODO + 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)); + Map attrs = new HashMap<>(dto.getAttributes()); + getOrRepairTopics(t, ATTRIBUTES_PROP_KEY).replaceAndWait(attrs); } - public ThingDescriptionV1DTO getThingDescriptionV1(String thingName) { - return null; // TODO + public Optional getThingDescriptionV1(String thingName) { + Topics t = config.findTopics(DESCRIPTION_KEY, DESCRIPTION_V1, thingName); + if (t == null) { + return Optional.empty(); + } + + Map attrs = new HashMap<>(); + Topics attributesTopic = t.findTopics(ATTRIBUTES_PROP_KEY); + if (attributesTopic != null && !attributesTopic.isEmpty()) { + attributesTopic.forEach(node -> attrs.put(node.getName(), Coerce.toString(node))); + } + + LocalDateTime lastFetched = null; + Topic lastFetchedTopic = t.find(LAST_UPDATED_KEY); + if (lastFetchedTopic != null) { + lastFetched = LocalDateTime.ofInstant(Instant.ofEpochMilli(Coerce.toLong(lastFetchedTopic)), + ZoneId.of("UTC")); + } + return Optional.of(new ThingDescriptionV1DTO(thingName, attrs, lastFetched)); } private Topics getOrRepairTopics(Topics root, String... path) { 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 index 7a284bb74..15d9778f7 100644 --- a/src/main/java/com/aws/greengrass/clientdevices/auth/iot/ThingAttributesCache.java +++ b/src/main/java/com/aws/greengrass/clientdevices/auth/iot/ThingAttributesCache.java @@ -154,12 +154,12 @@ private void refresh() { private Optional> getAssociatedThingNames() { // use cached value, provided it's not stale - ThingAssociationV1DTO dto = runtimeConfiguration.getThingAssociationV1(); + Optional dto = runtimeConfiguration.getThingAssociationV1(); // TODO pull from configuration - if (dto != null && dto.getLastFetched().plusSeconds(DEFAULT_THING_ASSOCIATION_TRUST_DURATION_SECONDS) + if (dto.isPresent() && dto.get().getLastUpdated().plusSeconds(DEFAULT_THING_ASSOCIATION_TRUST_DURATION_SECONDS) .isBefore(LocalDateTime.now())) { logger.atTrace().log("Using locally cached thing associations"); - return Optional.ofNullable(dto.getAssociatedThingNames()); + return dto.map(ThingAssociationV1DTO::getAssociatedThingNames); } if (networkStateProvider.getConnectionState() == NetworkStateProvider.ConnectionState.NETWORK_DOWN) { @@ -192,12 +192,12 @@ private Optional> fetchAssociatedThingNames() { private Optional> getThingAttributes(String thingName) { // use cached value, provided it's not stale - ThingDescriptionV1DTO dto = runtimeConfiguration.getThingDescriptionV1(thingName); + Optional dto = runtimeConfiguration.getThingDescriptionV1(thingName); // TODO pull from configuration - if (dto != null && dto.getLastFetched().plusSeconds(DEFAULT_THING_DESCRIPTION_TRUST_DURATION_SECONDS) + if (dto.isPresent() && dto.get().getLastUpdated().plusSeconds(DEFAULT_THING_DESCRIPTION_TRUST_DURATION_SECONDS) .isBefore(LocalDateTime.now())) { logger.atTrace().log("Using locally cached thing description"); - return Optional.ofNullable(dto.getAttributes()); + return dto.map(ThingDescriptionV1DTO::getAttributes); } if (networkStateProvider.getConnectionState() == NetworkStateProvider.ConnectionState.NETWORK_DOWN) { diff --git a/src/main/java/com/aws/greengrass/clientdevices/auth/iot/dto/ThingAssociationV1DTO.java b/src/main/java/com/aws/greengrass/clientdevices/auth/iot/dto/ThingAssociationV1DTO.java index 1ba7343c4..219398383 100644 --- a/src/main/java/com/aws/greengrass/clientdevices/auth/iot/dto/ThingAssociationV1DTO.java +++ b/src/main/java/com/aws/greengrass/clientdevices/auth/iot/dto/ThingAssociationV1DTO.java @@ -15,5 +15,5 @@ @AllArgsConstructor public class ThingAssociationV1DTO { Set associatedThingNames; - LocalDateTime lastFetched; + LocalDateTime lastUpdated; } diff --git a/src/main/java/com/aws/greengrass/clientdevices/auth/iot/dto/ThingDescriptionV1DTO.java b/src/main/java/com/aws/greengrass/clientdevices/auth/iot/dto/ThingDescriptionV1DTO.java index 8c05c4598..66400032e 100644 --- a/src/main/java/com/aws/greengrass/clientdevices/auth/iot/dto/ThingDescriptionV1DTO.java +++ b/src/main/java/com/aws/greengrass/clientdevices/auth/iot/dto/ThingDescriptionV1DTO.java @@ -16,5 +16,5 @@ public class ThingDescriptionV1DTO { String thingName; Map attributes; - LocalDateTime lastFetched; + LocalDateTime lastUpdated; } From 6eb5c0228be5309c93ccff05fd320e9e428f86d8 Mon Sep 17 00:00:00 2001 From: Joseph Cosentino Date: Fri, 28 Jun 2024 16:17:17 -0700 Subject: [PATCH 4/5] chore: checkstyle --- .../clientdevices/auth/iot/ThingAttributesCache.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 index 15d9778f7..dcb5a08ad 100644 --- a/src/main/java/com/aws/greengrass/clientdevices/auth/iot/ThingAttributesCache.java +++ b/src/main/java/com/aws/greengrass/clientdevices/auth/iot/ThingAttributesCache.java @@ -209,7 +209,8 @@ private Optional> getThingAttributes(String thingName) { logger.atTrace().log("Fetching thing description from cloud"); Optional> attributes = fetchThingAttributes(thingName); attributes.ifPresent(attrs -> - runtimeConfiguration.putThingDescriptionV1(new ThingDescriptionV1DTO(thingName, attrs, LocalDateTime.now()))); + runtimeConfiguration.putThingDescriptionV1( + new ThingDescriptionV1DTO(thingName, attrs, LocalDateTime.now()))); return attributes; } From 3642a32ab3c704ee3596d41d57ed6e5abb865579 Mon Sep 17 00:00:00 2001 From: Joseph Cosentino Date: Fri, 28 Jun 2024 16:20:46 -0700 Subject: [PATCH 5/5] chore: javadocs --- .../configuration/RuntimeConfiguration.java | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/aws/greengrass/clientdevices/auth/configuration/RuntimeConfiguration.java b/src/main/java/com/aws/greengrass/clientdevices/auth/configuration/RuntimeConfiguration.java index c6c6720bd..1ef85e5c6 100644 --- a/src/main/java/com/aws/greengrass/clientdevices/auth/configuration/RuntimeConfiguration.java +++ b/src/main/java/com/aws/greengrass/clientdevices/auth/configuration/RuntimeConfiguration.java @@ -62,8 +62,8 @@ * | |---- k:v * | |---- "l": lastUpdated * | |---- "hostAddresses": - * |---- : - * |---- [...] + * |---- : + * |---- [...] *

*/ 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)