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 3da8fc0aa..636370ccf 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 @@ -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; @@ -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 { diff --git a/src/integrationtests/java/com/aws/greengrass/integrationtests/deviceauth/EvaluateClientDeviceActionsWithPolicyVariablesTest.java b/src/integrationtests/java/com/aws/greengrass/integrationtests/deviceauth/EvaluateClientDeviceActionsWithPolicyVariablesTest.java new file mode 100644 index 000000000..686d13af8 --- /dev/null +++ b/src/integrationtests/java/com/aws/greengrass/integrationtests/deviceauth/EvaluateClientDeviceActionsWithPolicyVariablesTest.java @@ -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 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() {{ + 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 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); + } +} diff --git a/src/integrationtests/resources/com/aws/greengrass/integrationtests/deviceauth/config.yaml b/src/integrationtests/resources/com/aws/greengrass/integrationtests/deviceauth/config.yaml new file mode 100644 index 000000000..b8cbef1be --- /dev/null +++ b/src/integrationtests/resources/com/aws/greengrass/integrationtests/deviceauth/config.yaml @@ -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: + - '*' 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 9d84e8033..3f97ecf2c 100644 --- a/src/main/java/com/aws/greengrass/clientdevices/auth/DeviceAuthClient.java +++ b/src/main/java/com/aws/greengrass/clientdevices/auth/DeviceAuthClient.java @@ -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; @@ -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; } /** @@ -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); } } diff --git a/src/main/java/com/aws/greengrass/clientdevices/auth/PermissionEvaluationUtils.java b/src/main/java/com/aws/greengrass/clientdevices/auth/PermissionEvaluationUtils.java index 4c79aa5aa..a37a71ee6 100644 --- a/src/main/java/com/aws/greengrass/clientdevices/auth/PermissionEvaluationUtils.java +++ b/src/main/java/com/aws/greengrass/clientdevices/auth/PermissionEvaluationUtils.java @@ -5,7 +5,10 @@ package com.aws.greengrass.clientdevices.auth; +import com.aws.greengrass.clientdevices.auth.configuration.GroupManager; import com.aws.greengrass.clientdevices.auth.configuration.Permission; +import com.aws.greengrass.clientdevices.auth.exception.PolicyException; +import com.aws.greengrass.clientdevices.auth.session.Session; import com.aws.greengrass.logging.api.Logger; import com.aws.greengrass.logging.impl.LogManager; import com.aws.greengrass.util.Utils; @@ -16,6 +19,7 @@ import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; +import javax.inject.Inject; public final class PermissionEvaluationUtils { private static final Logger logger = LogManager.getLogger(PermissionEvaluationUtils.class); @@ -32,30 +36,40 @@ public final class PermissionEvaluationUtils { private static final Pattern SERVICE_RESOURCE_PATTERN = Pattern.compile( String.format(SERVICE_RESOURCE_FORMAT, SERVICE_PATTERN_STRING, SERVICE_RESOURCE_TYPE_PATTERN_STRING, SERVICE_RESOURCE_NAME_PATTERN_STRING), Pattern.UNICODE_CHARACTER_CLASS); + private final GroupManager groupManager; - - private PermissionEvaluationUtils() { + /** + * Constructor for PermissionEvaluationUtils. + * + * @param groupManager Group Manager + */ + @Inject + public PermissionEvaluationUtils(GroupManager groupManager) { + this.groupManager = groupManager; } /** * utility method of authorizing operation to resource. * - * @param operation operation in the form of 'service:action' - * @param resource resource in the form of 'service:resourceType:resourceName' - * @param groupToPermissionsMap device matching group to permissions map - * @return whether operation to resource in authorized + * @param request Authorization Request + * @param session Session + * + * @return boolean indicating if the operation requested is authorized */ - public static boolean isAuthorized(String operation, String resource, - Map> groupToPermissionsMap) { - Operation op = parseOperation(operation); - Resource rsc = parseResource(resource); + public boolean isAuthorized(AuthorizationRequest request, Session session) { + Operation op = parseOperation(request.getOperation()); + Resource rsc = parseResource(request.getResource()); + if (!rsc.getService().equals(op.getService())) { throw new IllegalArgumentException( String.format("Operation %s service is not same as resource %s service", op, rsc)); } + + Map> groupToPermissionsMap = groupManager.getApplicablePolicyPermissions(session); + if (groupToPermissionsMap == null || groupToPermissionsMap.isEmpty()) { - logger.atDebug().kv("operation", operation).kv("resource", resource) + logger.atDebug().kv("operation", request.getOperation()).kv("resource", request.getResource()) .log("No authorization group matches, " + "deny the request"); return false; } @@ -76,7 +90,12 @@ public static boolean isAuthorized(String operation, String resource, if (!compareOperation(op, e.getOperation())) { return false; } - return compareResource(rsc, e.getResource()); + try { + return compareResource(rsc, e.getResource(session)); + } catch (PolicyException er) { + logger.atError().setCause(er).log(); + return false; + } }).findFirst().orElse(null); if (permission != null) { @@ -88,7 +107,7 @@ public static boolean isAuthorized(String operation, String resource, return false; } - private static boolean comparePrincipal(String requestPrincipal, String policyPrincipal) { + private boolean comparePrincipal(String requestPrincipal, String policyPrincipal) { if (requestPrincipal.equals(policyPrincipal)) { return true; } @@ -96,8 +115,8 @@ private static boolean comparePrincipal(String requestPrincipal, String policyPr return ANY_REGEX.equals(policyPrincipal); } - private static boolean compareOperation(Operation requestOperation, String policyOperation) { - if (requestOperation.toString().equals(policyOperation)) { + private boolean compareOperation(Operation requestOperation, String policyOperation) { + if (requestOperation.getOperationStr().equals(policyOperation)) { return true; } if (String.format(SERVICE_OPERATION_FORMAT, requestOperation.getService(), ANY_REGEX).equals(policyOperation)) { @@ -106,8 +125,8 @@ private static boolean compareOperation(Operation requestOperation, String polic return ANY_REGEX.equals(policyOperation); } - private static boolean compareResource(Resource requestResource, String policyResource) { - if (requestResource.toString().equals(policyResource)) { + private boolean compareResource(Resource requestResource, String policyResource) { + if (requestResource.getResourceStr().equals(policyResource)) { return true; } @@ -119,28 +138,32 @@ private static boolean compareResource(Resource requestResource, String policyRe return ANY_REGEX.equals(policyResource); } - private static Operation parseOperation(String operationStr) { + private Operation parseOperation(String operationStr) { if (Utils.isEmpty(operationStr)) { throw new IllegalArgumentException("Operation can't be empty"); } Matcher matcher = SERVICE_OPERATION_PATTERN.matcher(operationStr); if (matcher.matches()) { - return Operation.builder().service(matcher.group(1)).action(matcher.group(2)).build(); + return Operation.builder().operationStr(operationStr).service(matcher.group(1)).action(matcher.group(2)) + .build(); } throw new IllegalArgumentException(String.format("Operation %s is not in the form of %s", operationStr, SERVICE_OPERATION_PATTERN.pattern())); } - private static Resource parseResource(String resourceStr) { + private Resource parseResource(String resourceStr) { if (Utils.isEmpty(resourceStr)) { throw new IllegalArgumentException("Resource can't be empty"); } Matcher matcher = SERVICE_RESOURCE_PATTERN.matcher(resourceStr); if (matcher.matches()) { - return Resource.builder().service(matcher.group(1)).resourceType(matcher.group(2)) - .resourceName(matcher.group(3)).build(); + return Resource.builder().resourceStr(resourceStr) + .service(matcher.group(1)) + .resourceType(matcher.group(2)) + .resourceName(matcher.group(3)) + .build(); } throw new IllegalArgumentException( @@ -150,25 +173,17 @@ private static Resource parseResource(String resourceStr) { @Value @Builder private static class Operation { + String operationStr; String service; String action; - - @Override - public String toString() { - return String.format(SERVICE_OPERATION_FORMAT, service, action); - } } @Value @Builder private static class Resource { + String resourceStr; String service; String resourceType; String resourceName; - - @Override - public String toString() { - return String.format(SERVICE_RESOURCE_FORMAT, service, resourceType, resourceName); - } } } 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 ca18cbb04..4711be5ee 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 @@ -19,6 +19,8 @@ import java.util.HashSet; import java.util.Map; import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; @Value @JsonDeserialize(builder = GroupConfiguration.GroupConfigurationBuilder.class) @@ -36,6 +38,11 @@ public class GroupConfiguration { Map> groupToPermissionsMap; + private static final String POLICY_VARIABLE_FORMAT = "(\\$\\{[a-z]+:[a-zA-Z.]+\\})"; + + private static final Pattern POLICY_VARIABLE_PATTERN = Pattern.compile(POLICY_VARIABLE_FORMAT, + Pattern.CASE_INSENSITIVE); + @Builder GroupConfiguration(ConfigurationFormatVersion formatVersion, Map definitions, Map> policies) throws AuthorizationException { @@ -91,9 +98,20 @@ private Set convertPolicyStatementToPermission(String groupName, continue; } permissions.add( - Permission.builder().principal(groupName).operation(operation).resource(resource).build()); + Permission.builder().principal(groupName).operation(operation).resource(resource) + .resourcePolicyVariables(findPolicyVariables(resource)).build()); } } 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(1); + policyVariables.add(policyVariable); + } + return policyVariables; + } } diff --git a/src/main/java/com/aws/greengrass/clientdevices/auth/configuration/Permission.java b/src/main/java/com/aws/greengrass/clientdevices/auth/configuration/Permission.java index 8d2bc66e1..f16d8d9e8 100644 --- a/src/main/java/com/aws/greengrass/clientdevices/auth/configuration/Permission.java +++ b/src/main/java/com/aws/greengrass/clientdevices/auth/configuration/Permission.java @@ -5,10 +5,15 @@ package com.aws.greengrass.clientdevices.auth.configuration; +import com.aws.greengrass.clientdevices.auth.exception.PolicyException; +import com.aws.greengrass.clientdevices.auth.session.Session; import lombok.Builder; import lombok.NonNull; import lombok.Value; +import java.util.Collections; +import java.util.Set; + @Value @Builder public class Permission { @@ -17,4 +22,11 @@ public class Permission { @NonNull String operation; @NonNull String resource; + + @Builder.Default + Set resourcePolicyVariables = Collections.emptySet(); + + public String getResource(Session session) throws PolicyException { + return PolicyVariableResolver.resolvePolicyVariables(resourcePolicyVariables, resource, session); + } } 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 new file mode 100644 index 000000000..721813abb --- /dev/null +++ b/src/main/java/com/aws/greengrass/clientdevices/auth/configuration/PolicyVariableResolver.java @@ -0,0 +1,61 @@ +/* + * 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.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.Set; + +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() { + } + + /** + * Utility method to replace policy variables in permissions with device attributes. + * Policy variables need to be validated when reading the policy document. + * 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 + * @return updated format + * @throws PolicyException when unable to find a policy variable value + */ + public static String resolvePolicyVariables(Set policyVariables, String format, Session session) + throws PolicyException { + if (policyVariables.isEmpty()) { + 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)); + 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); + } + } + return substitutedFormat; + } +} diff --git a/src/main/java/com/aws/greengrass/clientdevices/auth/exception/PolicyException.java b/src/main/java/com/aws/greengrass/clientdevices/auth/exception/PolicyException.java new file mode 100644 index 000000000..b60ca5188 --- /dev/null +++ b/src/main/java/com/aws/greengrass/clientdevices/auth/exception/PolicyException.java @@ -0,0 +1,22 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.aws.greengrass.clientdevices.auth.exception; + +public class PolicyException extends Exception { + private static final long serialVersionUID = -1L; + + public PolicyException(String message) { + super(message); + } + + public PolicyException(Throwable e) { + super(e); + } + + public PolicyException(String message, Throwable e) { + super(message, e); + } +} 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 19795e258..a5cc34a84 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 @@ -23,8 +23,6 @@ import java.security.cert.X509Certificate; import java.time.Instant; import java.time.temporal.ChronoUnit; -import java.util.Collections; -import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import static com.aws.greengrass.clientdevices.auth.configuration.SecurityConfiguration.DEFAULT_CLIENT_DEVICE_TRUST_DURATION_MINUTES; @@ -32,6 +30,7 @@ @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); @@ -130,8 +129,12 @@ public String getNamespace() { } @Override - public Map getDeviceAttributes() { - return Collections.singletonMap("CertificateId", new StringLiteralAttribute(getCertificateId())); + public DeviceAttribute getDeviceAttribute(String attributeName) { + // TODO: Support other DeviceAttributes + if (CERTIFICATE_ID_ATTRIBUTE.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 0e6dc6a69..01f8236cd 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 @@ -23,7 +23,7 @@ public String getNamespace() { } @Override - public Map getDeviceAttributes() { - return ATTRIBUTES; + public DeviceAttribute getDeviceAttribute(String attributeName) { + return ATTRIBUTES.get(attributeName); } } 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 90cd2aaf6..c28f05369 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 @@ -12,7 +12,6 @@ import java.time.Duration; import java.time.Instant; -import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Objects; @@ -31,6 +30,7 @@ @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\\-_:]+"; private static final AtomicInteger metadataTrustDurationMinutes = new AtomicInteger(DEFAULT_CLIENT_DEVICE_TRUST_DURATION_MINUTES); @@ -164,8 +164,12 @@ public String getNamespace() { } @Override - public Map getDeviceAttributes() { - return Collections.singletonMap("ThingName", new WildcardSuffixAttribute(thingName)); + public DeviceAttribute getDeviceAttribute(String attributeName) { + // TODO: Support other DeviceAttributes + if (THING_NAME_ATTRIBUTE.equals(attributeName)) { + return new WildcardSuffixAttribute(thingName); + } + return null; } /** 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 43a901350..61aa6f406 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 @@ -41,7 +41,7 @@ public AttributeProvider getAttributeProvider(String attributeProviderNameSpace) @Override public DeviceAttribute getSessionAttribute(String attributeNamespace, String attributeName) { if (this.getAttributeProvider(attributeNamespace) != null) { - return this.getAttributeProvider(attributeNamespace).getDeviceAttributes().get(attributeName); + return this.getAttributeProvider(attributeNamespace).getDeviceAttribute(attributeName); } return null; } diff --git a/src/main/java/com/aws/greengrass/clientdevices/auth/session/attribute/AttributeProvider.java b/src/main/java/com/aws/greengrass/clientdevices/auth/session/attribute/AttributeProvider.java index efce4d03d..0da3d8645 100644 --- a/src/main/java/com/aws/greengrass/clientdevices/auth/session/attribute/AttributeProvider.java +++ b/src/main/java/com/aws/greengrass/clientdevices/auth/session/attribute/AttributeProvider.java @@ -5,10 +5,8 @@ package com.aws.greengrass.clientdevices.auth.session.attribute; -import java.util.Map; - public interface AttributeProvider { String getNamespace(); - Map getDeviceAttributes(); + DeviceAttribute getDeviceAttribute(String attributeName); } 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 8e45b2d03..6a4a66d8c 100644 --- a/src/test/java/com/aws/greengrass/clientdevices/auth/DeviceAuthClientTest.java +++ b/src/test/java/com/aws/greengrass/clientdevices/auth/DeviceAuthClientTest.java @@ -6,6 +6,9 @@ package com.aws.greengrass.clientdevices.auth; import com.aws.greengrass.clientdevices.auth.certificate.CertificateStore; +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.componentmanager.KernelConfigResolver; import com.aws.greengrass.config.Topics; import com.aws.greengrass.dependency.Context; @@ -17,16 +20,17 @@ import com.aws.greengrass.clientdevices.auth.session.SessionImpl; import com.aws.greengrass.clientdevices.auth.session.SessionManager; import com.aws.greengrass.testcommons.testutilities.GGExtension; +import com.aws.greengrass.util.Coerce; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import java.io.IOException; import java.util.Collections; +import java.util.Set; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; @@ -35,8 +39,9 @@ @ExtendWith({MockitoExtension.class, GGExtension.class}) public class DeviceAuthClientTest { - - @InjectMocks + private static final Set THING_NAME_POLICY_VARIABLE = Collections + .singleton("${iot:Connection.Thing.ThingName}"); + private static final String SESSION_ID = "sessionId"; private DeviceAuthClient authClient; @Mock @@ -45,8 +50,9 @@ public class DeviceAuthClientTest { @Mock private GroupManager groupManager; + private PermissionEvaluationUtils permissionEvaluationUtils; + @Mock - @SuppressWarnings("PMD.UnusedPrivateField") // Required for injecting into DeviceAuthClient private CertificateStore certificateStore; private Topics configurationTopics; @@ -54,6 +60,8 @@ public class DeviceAuthClientTest { @BeforeEach void beforeEach() { configurationTopics = Topics.of(new Context(), KernelConfigResolver.CONFIGURATION_CONFIG_KEY, null); + permissionEvaluationUtils = new PermissionEvaluationUtils(groupManager); + authClient = new DeviceAuthClient(sessionManager, certificateStore, permissionEvaluationUtils); } @AfterEach @@ -63,27 +71,25 @@ void afterEach() throws IOException { @Test void GIVEN_invalidSessionId_WHEN_canDevicePerform_THEN_authorizationExceptionThrown() { - String sessionId = "FAKE_SESSION"; - when(sessionManager.findSession(sessionId)).thenReturn(null); + when(sessionManager.findSession(SESSION_ID)).thenReturn(null); AuthorizationRequest authorizationRequest = - new AuthorizationRequest("mqtt:connect", "mqtt:clientId:clientId", sessionId); + new AuthorizationRequest("mqtt:connect", "mqtt:clientId:clientId", SESSION_ID); assertThrows(AuthorizationException.class, () -> authClient.canDevicePerform(authorizationRequest)); } @Test void GIVEN_missingDevicePermission_WHEN_canDevicePerform_THEN_authorizationReturnFalse() throws Exception { - String sessionId = "FAKE_SESSION"; Session session = new SessionImpl(); - when(sessionManager.findSession(sessionId)).thenReturn(session); + when(sessionManager.findSession(SESSION_ID)).thenReturn(session); AuthorizationRequest authorizationRequest = - new AuthorizationRequest("mqtt:connect", "mqtt:clientId:clientId", sessionId); + new AuthorizationRequest("mqtt:connect", "mqtt:clientId:clientId", SESSION_ID); assertThat(authClient.canDevicePerform(authorizationRequest), is(false)); } @Test void GIVEN_sessionHasPermission_WHEN_canDevicePerform_THEN_authorizationReturnTrue() throws Exception { Session session = new SessionImpl(); - when(sessionManager.findSession("sessionId")).thenReturn(session); + when(sessionManager.findSession(SESSION_ID)).thenReturn(session); when(groupManager.getApplicablePolicyPermissions(session)).thenReturn(Collections.singletonMap("group1", Collections.singleton( Permission.builder().operation("mqtt:publish").resource("mqtt:topic:foo").principal("group1") @@ -94,10 +100,28 @@ void GIVEN_sessionHasPermission_WHEN_canDevicePerform_THEN_authorizationReturnTr assertThat(authorized, is(true)); } + @Test + void GIVEN_sessionHasPolicyVariablesPermission_WHEN_canDevicePerform_THEN_authorizationReturnTrue() throws Exception { + Certificate cert = CertificateFake.of("FAKE_CERT_ID"); + Thing thing = Thing.of("b"); + Session session = new SessionImpl(cert, thing); + when(sessionManager.findSession(SESSION_ID)).thenReturn(session); + + String thingName = Coerce.toString(session.getSessionAttribute("Thing", "ThingName")); + when(groupManager.getApplicablePolicyPermissions(session)).thenReturn(Collections.singletonMap("group1", + Collections.singleton(Permission.builder().operation("mqtt:publish") + .resource("mqtt:topic:${iot:Connection.Thing.ThingName}").principal("group1") + .resourcePolicyVariables(THING_NAME_POLICY_VARIABLE).build()))); + + boolean authorized = authClient.canDevicePerform(constructPolicyVariableAuthorizationRequest(thingName)); + + assertThat(authorized, is(true)); + } + @Test void GIVEN_internalClientSession_WHEN_canDevicePerform_THEN_authorizationReturnTrue() throws Exception { Session session = new SessionImpl(new Component()); - when(sessionManager.findSession("sessionId")).thenReturn(session); + when(sessionManager.findSession(SESSION_ID)).thenReturn(session); boolean authorized = authClient.canDevicePerform(constructAuthorizationRequest()); @@ -105,7 +129,12 @@ void GIVEN_internalClientSession_WHEN_canDevicePerform_THEN_authorizationReturnT } private AuthorizationRequest constructAuthorizationRequest() { - return AuthorizationRequest.builder().sessionId("sessionId").operation("mqtt:publish") + return AuthorizationRequest.builder().sessionId(SESSION_ID).operation("mqtt:publish") .resource("mqtt:topic:foo").build(); } + + private AuthorizationRequest constructPolicyVariableAuthorizationRequest(String thingName) { + return AuthorizationRequest.builder().sessionId(SESSION_ID).operation("mqtt:publish") + .resource(String.format("mqtt:topic:%s", thingName)).build(); + } } diff --git a/src/test/java/com/aws/greengrass/clientdevices/auth/PermissionEvaluationUtilsTest.java b/src/test/java/com/aws/greengrass/clientdevices/auth/PermissionEvaluationUtilsTest.java index a9e0d6e55..0d1f6d727 100644 --- a/src/test/java/com/aws/greengrass/clientdevices/auth/PermissionEvaluationUtilsTest.java +++ b/src/test/java/com/aws/greengrass/clientdevices/auth/PermissionEvaluationUtilsTest.java @@ -5,10 +5,20 @@ package com.aws.greengrass.clientdevices.auth; +import com.aws.greengrass.clientdevices.auth.configuration.GroupManager; import com.aws.greengrass.clientdevices.auth.configuration.Permission; +import com.aws.greengrass.clientdevices.auth.exception.PolicyException; +import com.aws.greengrass.clientdevices.auth.iot.Certificate; +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.Session; +import com.aws.greengrass.clientdevices.auth.session.SessionImpl; import com.aws.greengrass.testcommons.testutilities.GGExtension; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import java.util.Arrays; @@ -19,38 +29,194 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; @ExtendWith({MockitoExtension.class, GGExtension.class}) class PermissionEvaluationUtilsTest { + private static final String FAKE_CERT_ID = "FAKE_CERT_ID"; + private static final String THING_NAME = "b"; + private static final String SESSION_ID = "sessionId"; + private static final Set THING_NAME_POLICY_VARIABLE = Collections. + singleton("${iot:Connection.Thing.ThingName}"); + private Certificate cert; + private Thing thing; + private Session session; + private PermissionEvaluationUtils permissionEvaluationUtils; + @Mock + private GroupManager groupManager; + + @BeforeEach + void beforeEach() throws InvalidCertificateException { + cert = CertificateFake.of(FAKE_CERT_ID); + thing = Thing.of(THING_NAME); + session = new SessionImpl(cert, thing); + permissionEvaluationUtils = new PermissionEvaluationUtils(groupManager); + } + + @Test + void GIVEN_single_permission_with_policy_variable_WHEN_get_permission_resource_THEN_return_updated_permission_resource() + throws PolicyException { + Permission policyVariablePermission = + Permission.builder().principal("some-principal").operation("some-operation") + .resource("/msg/${iot:Connection.Thing.ThingName}").resourcePolicyVariables(THING_NAME_POLICY_VARIABLE).build(); + + Permission permission = Permission.builder().principal("some-principal").operation("some-operation") + .resource("/msg/b").build(); + assertThat(policyVariablePermission.getResource(session).equals(permission.getResource()), is(true)); + } + + @Test + void GIVEN_single_permission_with_invalid_policy_variable_WHEN_get_permission_resource_THEN_return_updated_permission_resource() + throws PolicyException { + Permission policyVariablePermission = + Permission.builder().principal("some-principal").operation("some-operation") + .resource("/msg/${iot:Connection.Thing.ThingName/}").build(); + + Permission permission = Permission.builder().principal("some-principal").operation("some-operation") + .resource("/msg/b").build(); + + assertThat(policyVariablePermission.getResource(session).equals(policyVariablePermission.getResource()), is(true)); + assertThat(policyVariablePermission.getResource(session).equals(permission.getResource()), is(false)); + } + + @Test + void GIVEN_single_permission_with_nonexistent_policy_variable_WHEN_get_permission_resource_THEN_return_updated_permission_resource() + throws PolicyException { + Permission policyVariablePermission = + Permission.builder().principal("some-principal").operation("some-operation") + .resource("/msg/${iot:Connection.Thing.RealThing}").build(); + + Permission permission = Permission.builder().principal("some-principal").operation("some-operation") + .resource("/msg/b").build(); + + assertThat(policyVariablePermission.getResource(session).equals(policyVariablePermission.getResource()), is(true)); + assertThat(policyVariablePermission.getResource(session).equals(permission.getResource()), is(false)); + } + + @Test + void GIVEN_single_permission_with_multiple_policy_variables_WHEN_get_permission_resource_THEN_return_updated_permission_resource() + throws PolicyException { + Permission policyVariablePermission = + Permission.builder().principal("some-principal").operation("some-operation") + .resource("/msg/${iot:Connection.Thing.ThingName}/${iot:Connection.Thing.ThingName}/src") + .resourcePolicyVariables(THING_NAME_POLICY_VARIABLE).build(); + + Permission permission = Permission.builder().principal("some-principal").operation("some-operation") + .resource("/msg/b/b/src").build(); + + assertThat(policyVariablePermission.getResource(session).equals(policyVariablePermission.getResource()), is(false)); + assertThat(policyVariablePermission.getResource(session).equals(permission.getResource()), is(true)); + } + + @Test + void GIVEN_single_permission_with_multiple_invalid_policy_variables_WHEN_get_permission_resource_THEN_return_updated_permission_resource() + throws PolicyException { + Permission policyVariablePermission = + Permission.builder().principal("some-principal").operation("some-operation") + .resource("/msg/${iot:Connection.Thing.ThingName}/${iot:Connection}.Thing.RealThing}/src") + .resourcePolicyVariables(THING_NAME_POLICY_VARIABLE).build(); + + Permission permission = Permission.builder().principal("some-principal").operation("some-operation") + .resource("/msg/b/b/src").build(); + + Permission expectedPermission = Permission.builder().principal("some-principal").operation("some-operation") + .resource("/msg/b/${iot:Connection}.Thing.RealThing}/src").build(); + + assertThat(policyVariablePermission.getResource(session).equals(policyVariablePermission.getResource()), is(false)); + assertThat(policyVariablePermission.getResource(session).equals(permission.getResource()), is(false)); + assertThat(policyVariablePermission.getResource(session).equals(expectedPermission.getResource()), is(true)); + } + + @Test + void GIVEN_single_group_permission_with_variable_WHEN_evaluate_operation_permission_THEN_return_decision() { + when(groupManager.getApplicablePolicyPermissions(any(Session.class))) + .thenReturn(prepareGroupVariablePermissionsData()); + + AuthorizationRequest request = AuthorizationRequest.builder().operation("mqtt:publish") + .resource("mqtt:topic:a").sessionId(SESSION_ID).build(); + boolean authorized = permissionEvaluationUtils.isAuthorized(request, session); + assertThat(authorized, is(true)); + + request = AuthorizationRequest.builder().operation("mqtt:publish").resource("mqtt:topic:b") + .sessionId(SESSION_ID).build(); + authorized = permissionEvaluationUtils.isAuthorized(request, session); + assertThat(authorized, is(true)); + + request = AuthorizationRequest.builder().operation("mqtt:subscribe").resource("mqtt:topic:b") + .sessionId(SESSION_ID).build(); + authorized = permissionEvaluationUtils.isAuthorized(request, session); + assertThat(authorized, is(true)); + + request = AuthorizationRequest.builder().operation("mqtt:connect").resource("mqtt:broker:localBroker") + .sessionId(SESSION_ID).build(); + authorized = permissionEvaluationUtils.isAuthorized(request, session); + assertThat(authorized, is(true)); + + request = AuthorizationRequest.builder().operation("mqtt:subscribe") + .resource("mqtt:topic:device:${iot:Connection.FakeThing.ThingName}").sessionId(SESSION_ID).build(); + authorized = permissionEvaluationUtils.isAuthorized(request, session); + assertThat(authorized, is(true)); + + request = AuthorizationRequest.builder().operation("mqtt:publish").resource("mqtt:topic:d") + .sessionId(SESSION_ID).build(); + authorized = permissionEvaluationUtils.isAuthorized(request, session); + assertThat(authorized, is(false)); + + request = AuthorizationRequest.builder().operation("mqtt:subscribe").resource("mqtt:message:a") + .sessionId(SESSION_ID).build(); + authorized = permissionEvaluationUtils.isAuthorized(request, session); + assertThat(authorized, is(false)); + + request = AuthorizationRequest.builder().operation("mqtt:subscribe").resource("mqtt:topic:device:b") + .sessionId(SESSION_ID).build(); + authorized = permissionEvaluationUtils.isAuthorized(request, session); + assertThat(authorized, is(false)); + } + @Test void GIVEN_single_group_permission_WHEN_evaluate_operation_permission_THEN_return_decision() { - Map> groupPermissions = prepareGroupPermissionsData(); - boolean authorized = PermissionEvaluationUtils.isAuthorized("mqtt:publish", "mqtt:topic:a", groupPermissions); + when(groupManager.getApplicablePolicyPermissions(any(Session.class))).thenReturn(prepareGroupPermissionsData()); + + AuthorizationRequest request = AuthorizationRequest.builder().operation("mqtt:publish") + .resource("mqtt:topic:a").sessionId(SESSION_ID).build(); + boolean authorized = permissionEvaluationUtils.isAuthorized(request, session); assertThat(authorized, is(true)); - authorized = PermissionEvaluationUtils.isAuthorized("mqtt:publish", "mqtt:topic:b", groupPermissions); + request = AuthorizationRequest.builder().operation("mqtt:publish").resource("mqtt:topic:b") + .sessionId(SESSION_ID).build(); + authorized = permissionEvaluationUtils.isAuthorized(request, session); assertThat(authorized, is(true)); - authorized = PermissionEvaluationUtils.isAuthorized("mqtt:subscribe", "mqtt:topic:b", groupPermissions); + request = AuthorizationRequest.builder().operation("mqtt:subscribe").resource("mqtt:topic:b") + .sessionId(SESSION_ID).build(); + authorized = permissionEvaluationUtils.isAuthorized(request, session); assertThat(authorized, is(true)); - authorized = - PermissionEvaluationUtils.isAuthorized("mqtt:subscribe", "mqtt:topic:$foo/bar/+/baz", groupPermissions); + request = AuthorizationRequest.builder().operation("mqtt:subscribe").resource("mqtt:topic:$foo/bar/+/baz") + .sessionId(SESSION_ID).build(); + authorized = permissionEvaluationUtils.isAuthorized(request, session); assertThat(authorized, is(true)); - authorized = PermissionEvaluationUtils.isAuthorized("mqtt:subscribe", "mqtt:topic:$foo .10bar/導À-baz/#", - groupPermissions); + request = AuthorizationRequest.builder().operation("mqtt:subscribe") + .resource("mqtt:topic:$foo .10bar/導À-baz/#").sessionId(SESSION_ID).build(); + authorized = permissionEvaluationUtils.isAuthorized(request, session); assertThat(authorized, is(true)); - authorized = - PermissionEvaluationUtils.isAuthorized("mqtt:connect", "mqtt:broker:localBroker", groupPermissions); + request = AuthorizationRequest.builder().operation("mqtt:connect").resource("mqtt:broker:localBroker") + .sessionId(SESSION_ID).build(); + authorized = permissionEvaluationUtils.isAuthorized(request, session); assertThat(authorized, is(true)); - authorized = PermissionEvaluationUtils.isAuthorized("mqtt:publish", "mqtt:topic:d", groupPermissions); + request = AuthorizationRequest.builder().operation("mqtt:publish").resource("mqtt:topic:d") + .sessionId(SESSION_ID).build(); + authorized = permissionEvaluationUtils.isAuthorized(request, session); assertThat(authorized, is(false)); - authorized = PermissionEvaluationUtils.isAuthorized("mqtt:subscribe", "mqtt:message:a", groupPermissions); + request = AuthorizationRequest.builder().operation("mqtt:subscribe").resource("mqtt:message:a") + .sessionId(SESSION_ID).build(); + authorized = permissionEvaluationUtils.isAuthorized(request, session); assertThat(authorized, is(false)); } @@ -64,4 +230,15 @@ private Map> prepareGroupPermissionsData() { return Collections.singletonMap("sensor", new HashSet<>(Arrays.asList(sensorPermission))); } + private Map> prepareGroupVariablePermissionsData() { + Permission[] sensorPermission = + {Permission.builder().principal("sensor").operation("mqtt:publish").resource("mqtt:topic:a").build(), + Permission.builder().principal("sensor").operation("mqtt:*").resource("mqtt:topic:${iot:Connection.Thing.ThingName}") + .resourcePolicyVariables(THING_NAME_POLICY_VARIABLE).build(), + Permission.builder().principal("sensor").operation("mqtt:subscribe") + .resource("mqtt:topic:device:${iot:Connection.FakeThing.ThingName}").build(), + Permission.builder().principal("sensor").operation("mqtt:connect").resource("*").build(),}; + return Collections.singletonMap("sensor", new HashSet<>(Arrays.asList(sensorPermission))); + } + } diff --git a/src/test/java/com/aws/greengrass/clientdevices/auth/configuration/GroupManagerTest.java b/src/test/java/com/aws/greengrass/clientdevices/auth/configuration/GroupManagerTest.java index 4e40dff07..f973b8c7e 100644 --- a/src/test/java/com/aws/greengrass/clientdevices/auth/configuration/GroupManagerTest.java +++ b/src/test/java/com/aws/greengrass/clientdevices/auth/configuration/GroupManagerTest.java @@ -61,7 +61,8 @@ void GIVEN_sessionInSingleGroup_WHEN_getApplicablePolicyPermissions_THEN_returnG .build(); GroupManager groupManager = new GroupManager(); Map> permissionsMap = new HashMap<>(Collections.singletonMap("group1", - new HashSet<>(Collections.singleton(new Permission("group1", "connect", "clientId"))))); + new HashSet<>(Collections.singleton(Permission.builder().principal("group1").operation("connect") + .resource("clientId").build())))); groupManager.setGroupConfiguration(groupConfiguration); @@ -86,9 +87,11 @@ void GIVEN_sessionInMultipleGroups_WHEN_getApplicablePolicyPermissions_THEN_retu Map> permissionsMap = new HashMap<>(); permissionsMap.put("group1", - new HashSet<>(Collections.singleton(new Permission("group1", "connect", "clientId")))); + new HashSet<>(Collections.singleton(Permission.builder().principal("group1").operation("connect") + .resource("clientId").build()))); permissionsMap.put("group2", - new HashSet<>(Collections.singleton(new Permission("group2", "publish", "topic")))); + new HashSet<>(Collections.singleton(Permission.builder().principal("group2").operation("publish") + .resource("topic").build()))); groupManager.setGroupConfiguration(groupConfiguration); 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 new file mode 100644 index 000000000..b72d4fbb1 --- /dev/null +++ b/src/test/java/com/aws/greengrass/clientdevices/auth/configuration/PolicyVariableResolverTest.java @@ -0,0 +1,83 @@ +/* + * 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.exception.PolicyException; +import com.aws.greengrass.clientdevices.auth.iot.Certificate; +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.Session; +import com.aws.greengrass.clientdevices.auth.session.SessionImpl; +import com.aws.greengrass.clientdevices.auth.session.attribute.WildcardSuffixAttribute; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; + +import java.util.Collections; +import java.util.Set; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +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; + @Mock + private Session mockSession; + @Mock + private WildcardSuffixAttribute wildcardSuffixAttribute; + private static final Set POLICY_VARIABLES = Collections.singleton("${iot:Connection.Thing.ThingName}"); + + @BeforeEach + void beforeEach() throws InvalidCertificateException { + cert = CertificateFake.of(FAKE_CERT_ID); + thing = Thing.of(THING_NAME); + session = new SessionImpl(cert, thing); + mockSession = mock(Session.class); + wildcardSuffixAttribute = mock(WildcardSuffixAttribute.class); + } + + @Test + void GIVEN_valid_resource_and_policy_variables_WHEN_resolve_policy_variables_THEN_return_updated_resource() + throws PolicyException { + String resource = "msg/${iot:Connection.Thing.ThingName}/test"; + String expected = String.format("msg/%s/test", THING_NAME); + String actual = PolicyVariableResolver.resolvePolicyVariables(POLICY_VARIABLES, resource, session); + assertThat(expected.equals(actual), is(true)); + } + + @Test + void GIVEN_invalid_resource_and_policy_variables_WHEN_resolve_policy_variables_THEN_return_original_resource() + throws PolicyException { + String resource = "msg/${iot:Connection.Thing/ThingName}/test"; + String expected = "msg/${iot:Connection.Thing/ThingName}/test"; + String actual = PolicyVariableResolver.resolvePolicyVariables(POLICY_VARIABLES, resource, session); + assertThat(expected.equals(actual), is(true)); + } + + @Test + 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(wildcardSuffixAttribute.toString()).thenReturn(null); + + assertThrows( + PolicyException.class, () -> PolicyVariableResolver.resolvePolicyVariables(POLICY_VARIABLES, resource, + mockSession)); + } + +} 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 8b66b19a7..6717523bd 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 @@ -26,8 +26,8 @@ public void GIVEN_sessionWithThingAndCert_WHEN_getSessionAttributes_THEN_attribu Session session = new SessionImpl(cert, thing); Assertions.assertEquals(session.getSessionAttribute("Certificate", "CertificateId").toString(), - cert.getDeviceAttributes().get("CertificateId").toString()); + cert.getDeviceAttribute("CertificateId").toString()); Assertions.assertEquals(session.getSessionAttribute("Thing", "ThingName").toString(), - thing.getDeviceAttributes().get("ThingName").toString()); + thing.getDeviceAttribute("ThingName").toString()); } }