From 1fa63339ecb82ee05fdf4f3b4d12979505858769 Mon Sep 17 00:00:00 2001 From: Joseph Cosentino Date: Thu, 27 Jun 2024 14:07:11 -0700 Subject: [PATCH] feat: add thing attribute variable support --- .../benchmark/AuthorizationBenchmarks.java | 11 +- .../auth/ClientDevicesAuthService.java | 13 ++ .../clientdevices/auth/DeviceAuthClient.java | 3 +- .../auth/configuration/ExpressionVisitor.java | 3 +- .../configuration/GroupConfiguration.java | 108 ++++++++++------ .../auth/configuration/PolicyVariable.java | 60 +++++++++ .../configuration/PolicyVariableResolver.java | 33 +++-- .../clientdevices/auth/iot/Certificate.java | 7 +- .../clientdevices/auth/iot/Component.java | 6 +- .../auth/iot/IotClientFactory.java | 46 +++++++ .../clientdevices/auth/iot/IotCoreClient.java | 56 ++++++++ .../clientdevices/auth/iot/Thing.java | 33 ++++- .../auth/iot/ThingAttributesCache.java | 120 ++++++++++++++++++ .../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/session/MqttSessionFactoryTest.java | 3 +- .../auth/session/SessionImplTest.java | 5 +- 22 files changed, 478 insertions(+), 103 deletions(-) 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 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/main/java/com/aws/greengrass/clientdevices/auth/ClientDevicesAuthService.java b/src/main/java/com/aws/greengrass/clientdevices/auth/ClientDevicesAuthService.java index 69b079002..659156594 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; @@ -130,6 +131,8 @@ private void initializeInfrastructure() { RuntimeConfiguration runtimeConfiguration = RuntimeConfiguration.from(getRuntimeConfig()); context.put(RuntimeConfiguration.class, runtimeConfiguration); context.get(ConnectivityInfoCache.class).setRuntimeConfiguration(runtimeConfiguration); + ThingAttributesCache thingAttributesCache = context.get(ThingAttributesCache.class); + ThingAttributesCache.setInstance(thingAttributesCache); NetworkStateProvider networkState = context.get(NetworkStateProvider.class); networkState.registerHandler(context.get(CISShadowMonitor.class)); networkState.registerHandler(context.get(BackgroundCertificateRefresh.class)); @@ -228,6 +231,7 @@ protected void startup() throws InterruptedException { @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 +282,15 @@ private void updateDeviceGroups() { return; } + // periodically refresh device attributes from cloud, for usage in policy variables + if (groupConfiguration.isHasDeviceAttributeVariables()) { + logger.atTrace().log("enabling thing-attribute cache"); + ThingAttributesCache.instance().ifPresent(ThingAttributesCache::startPeriodicRefresh); + } else { + logger.atTrace().log("disabling thing-attribute cache"); + ThingAttributesCache.instance().ifPresent(ThingAttributesCache::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..079ce1929 100644 --- a/src/main/java/com/aws/greengrass/clientdevices/auth/DeviceAuthClient.java +++ b/src/main/java/com/aws/greengrass/clientdevices/auth/DeviceAuthClient.java @@ -11,6 +11,7 @@ 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 +137,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..8765b695e 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; @@ -33,6 +34,7 @@ public class GroupConfiguration { Map definitions; Map> policies; Map> groupToPermissionsMap; + boolean hasDeviceAttributeVariables; @Builder GroupConfiguration(ConfigurationFormatVersion formatVersion, Map definitions, @@ -40,63 +42,87 @@ public class GroupConfiguration { 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.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())))); - } + 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 final Map definitions; + private final Map> policies; + + @Getter + private final Map> permissions; + + @Getter + private boolean hasDeviceAttributeVariables; + + private GroupPermissionConstructor(Map definitions, + Map> policies) { + this.definitions = definitions; + this.policies = 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; } /** 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..571a4c053 --- /dev/null +++ b/src/main/java/com/aws/greengrass/clientdevices/auth/configuration/PolicyVariable.java @@ -0,0 +1,60 @@ +/* + * 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; + boolean isThingAttribute; + Attribute attribute; + String selector; // the part within [ ] + + 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)) { + int attrStart = THING_ATTRS_PREFIX.length(); + int attrEnd = policyVariable.length() - THING_ATTRS_SUFFIX.length(); + if (attrStart <= attrEnd) { + String attr = policyVariable.substring(attrStart, attrEnd); + if (StringUtils.isAlphanumeric(attr)) { + return Optional.of(PolicyVariable.builder() + .originalText(policyVariable) + .attribute(Attribute.THING_ATTRIBUTES) + .selector(attr) + .build()); + } + } + } + + // unsupported variable + return Optional.empty(); + } +} 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..8704f59c1 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 @@ -8,20 +8,13 @@ import com.aws.greengrass.clientdevices.auth.exception.PolicyException; import com.aws.greengrass.clientdevices.auth.session.Session; 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.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 PolicyVariableResolver() { } @@ -32,8 +25,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 +36,27 @@ 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())) { + String policyVariableValue = Coerce.toString(session.getSessionAttribute(policyVariable.getAttribute())); 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); + substitutedFormat = StringUtils.replace(substitutedFormat, policyVariable.getOriginalText(), policyVariableValue); } } return substitutedFormat; } public static boolean isPolicyVariable(String variable) { - return policyVariableToAttributeProvider.containsKey(variable); + return PolicyVariable.parse(variable).isPresent(); + } + + public static boolean isAttributePolicyVariable(String variable) { + return PolicyVariable.parse(variable).map(PolicyVariable::isThingAttribute).orElse(false); } } 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..477dcfad2 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,11 @@ @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..303cfef28 --- /dev/null +++ b/src/main/java/com/aws/greengrass/clientdevices/auth/iot/IotClientFactory.java @@ -0,0 +1,46 @@ +/* + * 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 software.amazon.awssdk.services.iot.IotClient; + +import javax.inject.Inject; +import java.net.URISyntaxException; + +public class IotClientFactory { + + private final DeviceConfiguration deviceConfiguration; + + @Inject + public IotClientFactory(DeviceConfiguration deviceConfiguration) { + this.deviceConfiguration = deviceConfiguration; + } + + public IotClient getClient() throws DeviceConfigurationException { + try { + return IotSdkClientFactory.getIotClient( + getAwsRegion(deviceConfiguration), + IotSdkClientFactory.EnvironmentStage.PROD // TODO + ); + } catch (URISyntaxException unused) { + // this will never execute. exception can only throw when stage != EnvironmentStage.PROD + throw new DeviceConfigurationException(""); + } + } + + 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..c9456b4ed --- /dev/null +++ b/src/main/java/com/aws/greengrass/clientdevices/auth/iot/IotCoreClient.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.clientdevices.auth.exception.CloudServiceInteractionException; +import com.aws.greengrass.deployment.DeviceConfiguration; +import com.aws.greengrass.deployment.exceptions.DeviceConfigurationException; +import com.aws.greengrass.logging.api.Logger; +import com.aws.greengrass.logging.impl.LogManager; +import com.aws.greengrass.tes.LazyCredentialProvider; +import software.amazon.awssdk.services.iot.IotClient; +import software.amazon.awssdk.services.iot.model.DescribeThingRequest; + +import javax.inject.Inject; +import java.util.Map; + +public interface IotCoreClient { + + Map getThingAttributes(String thingName) throws CloudServiceInteractionException; + + class Default implements IotCoreClient { + + private static final Logger logger = LogManager.getLogger(IotAuthClient.Default.class); + + 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 + 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..8b679f76e 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,8 @@ package com.aws.greengrass.clientdevices.auth.iot; +import com.aws.greengrass.clientdevices.auth.configuration.PolicyVariable; +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 +31,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 +164,37 @@ 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 static final String NO_ATTR = ""; + private String resolvedAttr; + + @Override + public boolean matches(String expr) { + resolvedAttr = PolicyVariable.parse(expr) + .flatMap(v -> cache.getAttribute(thingName, v.getSelector())) + .orElse(NO_ATTR); + return !Objects.equals(resolvedAttr, NO_ATTR); + } + + @Override + public String toString() { + return resolvedAttr == null ? NO_ATTR : 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..4aff634ba --- /dev/null +++ b/src/main/java/com/aws/greengrass/clientdevices/auth/iot/ThingAttributesCache.java @@ -0,0 +1,120 @@ +/* + * 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 javax.inject.Inject; +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.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +public class ThingAttributesCache { + + private static final Logger logger = LogManager.getLogger(ThingAttributesCache.class); + + // set once during component install + private static final AtomicReference INSTANCE = new AtomicReference<>(); + + public static Optional instance() { + return Optional.ofNullable(INSTANCE.get()); + } + + public static void setInstance(ThingAttributesCache cache) { + INSTANCE.compareAndSet(null, cache); + } + + 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; + + @Inject + public ThingAttributesCache(IotCoreClient iotCoreClient, + IotAuthClient iotAuthClient, + NetworkStateProvider networkStateProvider, + ScheduledExecutorService ses) { + this.iotCoreClient = iotCoreClient; + this.iotAuthClient = iotAuthClient; + this.networkStateProvider = networkStateProvider; + this.ses = ses; + } + + public void startPeriodicRefresh() { + stopPeriodicRefresh(); + // TODO configurable delay + refreshTask = ses.scheduleWithFixedDelay(this::refresh, 0L, 1L, TimeUnit.MINUTES); + } + + public void stopPeriodicRefresh() { + if (refreshTask != null) { + refreshTask.cancel(true); + } + } + + private void refresh() { + if (networkStateProvider.getConnectionState() == NetworkStateProvider.ConnectionState.NETWORK_DOWN) { + 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)); + }); + } + }); + } + + 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/session/MqttSessionFactoryTest.java b/src/test/java/com/aws/greengrass/clientdevices/auth/session/MqttSessionFactoryTest.java index d1a40db5b..f359ba76c 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 @@ -19,6 +19,7 @@ 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 +147,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()); } }