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 fdfd350
Show file tree
Hide file tree
Showing 22 changed files with 512 additions and 105 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 @@ -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;
Expand Down Expand Up @@ -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;
}

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;

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,74 @@
/*
* 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 [ ]

/**
* Parse a policy variable from string.
*
* @param policyVariable variable
* @return parsed policy variable
*/
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)) {
return parseAttributePolicyVariable(policyVariable);
}

// unsupported variable
return Optional.empty();
}

private static Optional<PolicyVariable> 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());
}
}
Loading

0 comments on commit fdfd350

Please sign in to comment.