Skip to content

Commit

Permalink
feat: add policy variable support (#415)
Browse files Browse the repository at this point in the history
* feat: utility method for policy variable support (#339)

* feat: utility method for updating permissions for device (#344)

* fix: add support for replacing multiple policy variables (#346)

* feat: allow device actions to be evaluated based on policy variable permissions (#349)

* feat: add integration test for policy variable authorization (#355)

* feat: make PermissionEvaluationUtils non static

* fix: check style

* fix: pmd

* fix: refactor PermissionEvaluationUtils

* chore: check style

* fix: remove device auth client mocks

* fix: remove permission caching

* fix: case insensitive matching

* fix: pmd

* fix: update benchmark

* fix: update benchmark

* fix: typo

* fix: exception handling

* fix: remove ipc from integration tests and add benchmark for policy vars

* chore: parameterize integration tests

* feat: create foundation to support more policy variables

* feat: identify policy variables during config time

* feat: performance improvements

* chore: renamed attribute provider exception to policy exception

* chore: use set instead of list to store user policy variables

* chore: fix policy exception logging and use coerce to string to avoid npe

---------

Co-authored-by: Cynthia Yan <[email protected]>
Co-authored-by: Robert Manning <[email protected]>
  • Loading branch information
3 people authored Feb 1, 2024
1 parent 4c7a61a commit 1a7a07a
Show file tree
Hide file tree
Showing 19 changed files with 765 additions and 87 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import com.aws.greengrass.clientdevices.auth.AuthorizationRequest;
import com.aws.greengrass.clientdevices.auth.DeviceAuthClient;
import com.aws.greengrass.clientdevices.auth.PermissionEvaluationUtils;
import com.aws.greengrass.clientdevices.auth.configuration.AuthorizationPolicyStatement;
import com.aws.greengrass.clientdevices.auth.configuration.GroupConfiguration;
import com.aws.greengrass.clientdevices.auth.configuration.GroupDefinition;
Expand Down Expand Up @@ -72,15 +73,51 @@ public void doSetup() throws ParseException, AuthorizationException {
}
}

@State(Scope.Thread)
public static class PolicyVariableAuthRequest extends PolicyTestState {

final AuthorizationRequest thingNameRequest = AuthorizationRequest.builder()
.operation("mqtt:publish")
.resource("mqtt:topic:MyThingName/humidity")
.sessionId("sessionId")
.build();

@Setup
public void doSetup() throws ParseException, AuthorizationException {
sessionManager.registerSession("sessionId", FakeSession.forDevice("MyThingName"));
groupManager.setGroupConfiguration(GroupConfiguration.builder()
.definitions(Collections.singletonMap(
"group1", GroupDefinition.builder()
.selectionRule("thingName: " + "MyThingName")
.policyName("policy1")
.build()))
.policies(Collections.singletonMap(
"policy1", Collections.singletonMap(
"Statement1", AuthorizationPolicyStatement.builder()
.statementDescription("Policy description")
.effect(AuthorizationPolicyStatement.Effect.ALLOW)
.resources(new HashSet<>(Collections.singleton("mqtt:topic:${iot:Connection.Thing.ThingName}/humidity")))
.operations(new HashSet<>(Collections.singleton("mqtt:publish")))
.build())))
.build());
}
}

@Benchmark
public boolean GIVEN_single_group_permission_WHEN_simple_auth_request_THEN_successful_auth(SimpleAuthRequest state) throws Exception {
return state.deviceAuthClient.canDevicePerform(state.basicRequest);
}

@Benchmark
public boolean GIVEN_policy_with_thing_name_variable_WHEN_auth_request_THEN_successful_auth(PolicyVariableAuthRequest state) throws Exception {
return state.deviceAuthClient.canDevicePerform(state.thingNameRequest);
}

static abstract class PolicyTestState {
final FakeSessionManager sessionManager = new FakeSessionManager();
final GroupManager groupManager = new GroupManager();
final DeviceAuthClient deviceAuthClient = new DeviceAuthClient(sessionManager, groupManager, null);
final PermissionEvaluationUtils permissionEvaluationUtils = new PermissionEvaluationUtils(groupManager);
final DeviceAuthClient deviceAuthClient = new DeviceAuthClient(sessionManager, null, permissionEvaluationUtils);
}

static class FakeSession implements Session {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

package com.aws.greengrass.integrationtests.deviceauth;

import com.aws.greengrass.clientdevices.auth.AuthorizationRequest;
import com.aws.greengrass.clientdevices.auth.ClientDevicesAuthService;
import com.aws.greengrass.clientdevices.auth.api.ClientDevicesAuthServiceApi;
import com.aws.greengrass.clientdevices.auth.certificate.CertificateHelper;
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.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.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 org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.junit.jupiter.MockitoExtension;

import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.security.cert.X509Certificate;
import java.util.HashMap;
import java.util.List;
import java.util.stream.Stream;

import static com.aws.greengrass.testcommons.testutilities.ExceptionLogProtector.ignoreExceptionOfType;
import static com.aws.greengrass.testcommons.testutilities.TestUtils.createServiceStateChangeWaiter;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;

@SuppressWarnings("PMD.UnusedPrivateMethod")
@ExtendWith({GGExtension.class, UniqueRootPathExtension.class, MockitoExtension.class})
public class EvaluateClientDeviceActionsWithPolicyVariablesTest {
@TempDir
Path rootDir;
private Kernel kernel;

private Certificate certificate;

private String clientPem;
private ClientDevicesAuthServiceApi api;

@BeforeEach
void beforeEach(ExtensionContext context) throws Exception {
ignoreExceptionOfType(context, SpoolerStoreException.class);
ignoreExceptionOfType(context, NoSuchFileException.class); // Loading CA keystore

// Set this property for kernel to scan its own classpath to find plugins
System.setProperty("aws.greengrass.scanSelfClasspath", "true");
kernel = new Kernel();

// Set up Iot auth client
IotAuthClientFake iotAuthClientFake = new IotAuthClientFake();
kernel.getContext().put(IotAuthClient.class, iotAuthClientFake);

// start CDA service with configuration
startNucleusWithConfig("config.yaml");

// create certificate that client devices can use
setClientDeviceCertificatePem();

this.api = kernel.getContext().get(ClientDevicesAuthServiceApi.class);
}

private void setClientDeviceCertificatePem() throws Exception{
// create certificate to attach to thing
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);
this.certificate = cert;
this.clientPem = clientPem;
}

private String getClientDeviceSessionAuthToken(String thingName, String clientPem) throws Exception {
// create thing - needed for api call to validate thing + certificate
ThingRegistry thingRegistry = kernel.getContext().get(ThingRegistry.class);
Thing MyThing = thingRegistry.createThing(thingName);
MyThing.attachCertificate(certificate.getCertificateId());
thingRegistry.updateThing(MyThing);

// create client device session and get token
return api.getClientDeviceAuthToken("mqtt", new HashMap<String, String>() {{
put("clientId", thingName);
put("certificatePem", clientPem);
put("username", "foo");
put("password", "bar");
}});
}

private void startNucleusWithConfig(String configFileName) {
kernel.parseArgs("-r", rootDir.toAbsolutePath().toString(), "-i",
getClass().getResource(configFileName).toString());
Runnable mainRunning = createServiceStateChangeWaiter(kernel,
ClientDevicesAuthService.CLIENT_DEVICES_AUTH_SERVICE_NAME, 30, State.RUNNING);
kernel.launch();
mainRunning.run();
}

private void authzClientDeviceAction(AuthorizationRequest request, Boolean authorized) throws Exception {
assertThat(api.authorizeClientDeviceAction(request), is(authorized));
}

@AfterEach
void afterEach() {
LogConfig.getRootLogConfig().reset();
kernel.shutdown();
}

private static Stream<Arguments> authzRequests () {
return Stream.of(
// GIVEN_permissiveGroupPolicyWithThingNameVariable_WHEN_ClientAuthorizesWithThingNameValidResource_THEN_ClientAuthorized
Arguments.of("myThing", "mqtt:connect", "mqtt:myThing:foo", true),
Arguments.of("myThing", "mqtt:publish", "mqtt:topic:myThing", true),
// GIVEN_permissiveGroupPolicyWithThingNameVariable_WHEN_ClientAuthorizesWithThingNameInvalidResource_THEN_ClientNotAuthorized
Arguments.of("myThing", "mqtt:connect", "mqtt:MyCoolThing:foo", false),
Arguments.of("myThing", "mqtt:publish", "mqtt:topic:SomeThing", false),
// GIVEN_permissiveGroupPolicyWithThingNameVariable_WHEN_ClientAuthorizesWithThingNameResourceInvalidAction_THEN_ClientNotAuthorized
Arguments.of("myThing", "mqtt:connect", "mqtt:topic:myThing", false),
Arguments.of("myThing", "mqtt:publish", "mqtt:myThing:foo", false),
// GIVEN_permissiveGroupPolicyWithThingNameVariable_WHEN_ClientAuthorizesWithInvalidThingNameResource_THEN_ClientNotAuthorized
Arguments.of("SomeThing", "mqtt:connect", "mqtt:myThing:foo", false),
Arguments.of("SomeThing", "mqtt:publish", "mqtt:topic:myThing", false)
);
}

@ParameterizedTest
@MethodSource("authzRequests")
void GIVEN_permissiveGroupPolicyWithThingNameVariable_WHEN_ClientAuthorizesWithThingName_THEN_ClientAuthorized(
String thingName, String operation, String resource, Boolean result) throws Exception {
String deviceToken = getClientDeviceSessionAuthToken(thingName, clientPem);

AuthorizationRequest request = AuthorizationRequest.builder().sessionId(deviceToken)
.operation(operation).resource(resource).build();

authzClientDeviceAction(request, result);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
---
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: "thingAccessPolicy"
policies:
thingAccessPolicy:
policyStatement1:
statementDescription: "mqtt connect"
effect: ALLOW
operations:
- "mqtt:connect"
resources:
- "mqtt:${iot:Connection.Thing.ThingName}:foo"
policyStatement2:
statementDescription: "mqtt publish"
operations:
- "mqtt:publish"
resources:
- "mqtt:topic:${iot:Connection.Thing.ThingName}"
main:
dependencies:
- BrokerWithGetClientDeviceAuthTokenPermission
- BrokerWithAuthorizeClientDeviceActionPermission
BrokerWithGetClientDeviceAuthTokenPermission:
dependencies:
- aws.greengrass.clientdevices.Auth
configuration:
accessControl:
aws.greengrass.clientdevices.Auth:
GetClientDeviceAuthTokenPolicy:
policyDescription: access to certificate updates
operations:
- 'aws.greengrass#GetClientDeviceAuthToken'
resources:
- '*'
BrokerWithAuthorizeClientDeviceActionPermission:
dependencies:
- aws.greengrass.clientdevices.Auth
configuration:
accessControl:
aws.greengrass.clientdevices.Auth:
BrokerWithAuthorizeClientDeviceActionPermission:
policyDescription: access to certificate updates
operations:
- 'aws.greengrass#AuthorizeClientDeviceAction'
resources:
- '*'
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
package com.aws.greengrass.clientdevices.auth;

import com.aws.greengrass.clientdevices.auth.certificate.CertificateStore;
import com.aws.greengrass.clientdevices.auth.configuration.GroupManager;
import com.aws.greengrass.clientdevices.auth.exception.AuthorizationException;
import com.aws.greengrass.clientdevices.auth.exception.InvalidSessionException;
import com.aws.greengrass.clientdevices.auth.iot.Component;
Expand Down Expand Up @@ -40,22 +39,22 @@ public class DeviceAuthClient {
private static final Logger logger = LogManager.getLogger(DeviceAuthClient.class);

private final SessionManager sessionManager;
private final GroupManager groupManager;
private final CertificateStore certificateStore;
private final PermissionEvaluationUtils permissionEvaluationUtils;

/**
* Constructor.
*
* @param sessionManager Session manager
* @param groupManager Group manager
* @param certificateStore Certificate store
* @param sessionManager Session manager
* @param certificateStore Certificate store
* @param permissionEvaluationUtils Permission Evaluation Utils
*/
@Inject
public DeviceAuthClient(SessionManager sessionManager, GroupManager groupManager,
CertificateStore certificateStore) {
public DeviceAuthClient(SessionManager sessionManager, CertificateStore certificateStore,
PermissionEvaluationUtils permissionEvaluationUtils) {
this.sessionManager = sessionManager;
this.groupManager = groupManager;
this.certificateStore = certificateStore;
this.permissionEvaluationUtils = permissionEvaluationUtils;
}

/**
Expand Down Expand Up @@ -141,7 +140,6 @@ public boolean canDevicePerform(AuthorizationRequest request) throws Authorizati
return true;
}

return PermissionEvaluationUtils.isAuthorized(request.getOperation(), request.getResource(),
groupManager.getApplicablePolicyPermissions(session));
return permissionEvaluationUtils.isAuthorized(request, session);
}
}
Loading

0 comments on commit 1a7a07a

Please sign in to comment.