Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add thing attribute variable support #434

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -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;
Expand All @@ -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;
Expand All @@ -65,7 +66,13 @@
@ExtendWith({GGExtension.class, UniqueRootPathExtension.class, MockitoExtension.class})
public class PolicyTest {
private static final ObjectMapper MAPPER = new ObjectMapper();
private final Map<String, Pair<Certificate, String>> clients = new HashMap<>();
private final Map<String, String> clients = new HashMap<>();

private final NetworkStateFake networkStateProvider = new NetworkStateFake();
private final IotAuthClientFake iotAuthClient = new IotAuthClientFake();
private final IotCoreClientFake iotCoreClient = new IotCoreClientFake();
private final Map<String, String> DEFAULT_THING_ATTRIBUTES = ImmutableMap.of("myAttribute", "attribute");

@TempDir
Path rootDir;
Kernel kernel;
Expand All @@ -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
Expand Down Expand Up @@ -295,18 +304,34 @@ public static Stream<Arguments> 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()
))
);
}

@ParameterizedTest
@MethodSource("authzRequests")
void GIVEN_cda_with_policy_configuration_WHEN_client_requests_authorization_THEN_client_is_authorized(String configFile, List<AuthZRequest> 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<String, String> 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());
Expand All @@ -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<Certificate, String> clientCert = clients.computeIfAbsent(thingName, k -> {
private String createOrGetClient(String thingName) {
return clients.computeIfAbsent(thingName, k -> {
try {
Pair<Certificate, String> 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<Certificate, String> generateClientCert() throws Exception {
// create certificate to attach to thing
private String generateClientCert() throws Exception {
List<X509Certificate> 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")
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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
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 @@ -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);
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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);
}

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());
}
}
Loading
Loading