Skip to content

Commit

Permalink
feat: add thing attribute variable support
Browse files Browse the repository at this point in the history
  • Loading branch information
jcosentino11 committed Jun 27, 2024
1 parent 34b9a58 commit 1fa6333
Show file tree
Hide file tree
Showing 22 changed files with 478 additions and 103 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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()));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -33,70 +34,95 @@ public class GroupConfiguration {
Map<String, GroupDefinition> definitions;
Map<String, Map<String, AuthorizationPolicyStatement>> policies;
Map<String, Set<Permission>> groupToPermissionsMap;
boolean hasDeviceAttributeVariables;

@Builder
GroupConfiguration(ConfigurationFormatVersion formatVersion, Map<String, GroupDefinition> definitions,
Map<String, Map<String, AuthorizationPolicyStatement>> 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.groupToPermissionsMap = constructor.getPermissions();
this.hasDeviceAttributeVariables = constructor.isHasDeviceAttributeVariables();
}

@JsonPOJOBuilder(withPrefix = "")
public static class GroupConfigurationBuilder {
}

private Map<String, Set<Permission>> 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<Permission> constructGroupPermission(String groupName,
Map<String, AuthorizationPolicyStatement> policyStatementMap) {
Set<Permission> permissions = new HashSet<>();
for (Map.Entry<String, AuthorizationPolicyStatement> 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<String, GroupDefinition> definitions;
private final Map<String, Map<String, AuthorizationPolicyStatement>> policies;

@Getter
private final Map<String, Set<Permission>> permissions;

@Getter
private boolean hasDeviceAttributeVariables;

private GroupPermissionConstructor(Map<String, GroupDefinition> definitions,
Map<String, Map<String, AuthorizationPolicyStatement>> policies) {
this.definitions = definitions;
this.policies = policies;
this.permissions = constructGroupPermissions();
}

private Map<String, Set<Permission>> 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<Permission> convertPolicyStatementToPermission(String groupName,
AuthorizationPolicyStatement statement) {
Set<Permission> permissions = new HashSet<>();
for (String operation : statement.getOperations()) {
if (Utils.isEmpty(operation)) {
continue;
private Set<Permission> constructGroupPermission(String groupName,
Map<String, AuthorizationPolicyStatement> policyStatementMap) {
Set<Permission> permissions = new HashSet<>();
for (Map.Entry<String, AuthorizationPolicyStatement> 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<Permission> convertPolicyStatementToPermission(String groupName,
AuthorizationPolicyStatement statement) {
Set<Permission> 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<String> findPolicyVariables(String resource) {
Matcher matcher = POLICY_VARIABLE_PATTERN.matcher(resource);
Set<String> policyVariables = new HashSet<>();
while (matcher.find()) {
String policyVariable = matcher.group(0);
policyVariables.add(policyVariable);
private Set<String> findPolicyVariables(String resource) {
Matcher matcher = POLICY_VARIABLE_PATTERN.matcher(resource);
Set<String> policyVariables = new HashSet<>();
while (matcher.find()) {
String policyVariable = matcher.group(0);
if (PolicyVariableResolver.isAttributePolicyVariable(policyVariable)) {
hasDeviceAttributeVariables = true;
}
policyVariables.add(policyVariable);
}
return policyVariables;
}
return policyVariables;
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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<PolicyVariable> 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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Pair<String,String>> policyVariableToAttributeProvider = ImmutableMap.of(
"${iot:Connection.Thing.ThingName}", new Pair<>(THING_NAMESPACE, THING_NAME_ATTRIBUTE)
);

private PolicyVariableResolver() {
}
Expand All @@ -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
*/
Expand All @@ -43,23 +36,27 @@ public static String resolvePolicyVariables(Set<String> 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);
}
}
Loading

0 comments on commit 1fa6333

Please sign in to comment.