From bc7835013a059c75a574584fc86a4de6e26281b6 Mon Sep 17 00:00:00 2001 From: Anton Kuznetsov Date: Thu, 19 Nov 2020 14:19:43 -0800 Subject: [PATCH 1/8] Add support for AWS::IoT::SecurityProfile --- aws-iot-securityprofile/.gitignore | 23 ++ aws-iot-securityprofile/.rpdk-config | 22 + aws-iot-securityprofile/README.md | 19 + .../aws-iot-securityprofile.json | 359 ++++++++++++++++ .../inputs/inputs_1_create.json | 32 ++ .../inputs/inputs_1_invalid.json | 9 + .../inputs/inputs_1_update.json | 31 ++ aws-iot-securityprofile/lombok.config | 1 + aws-iot-securityprofile/pom.xml | 210 ++++++++++ aws-iot-securityprofile/resource-role.yaml | 42 ++ .../iot/securityprofile/CallbackContext.java | 10 + .../iot/securityprofile/Configuration.java | 21 + .../iot/securityprofile/CreateHandler.java | 120 ++++++ .../iot/securityprofile/DeleteHandler.java | 61 +++ .../iot/securityprofile/HandlerUtils.java | 71 ++++ .../iot/securityprofile/ListHandler.java | 58 +++ .../iot/securityprofile/ReadHandler.java | 114 +++++ .../iot/securityprofile/Translator.java | 326 +++++++++++++++ .../iot/securityprofile/UpdateHandler.java | 260 ++++++++++++ .../securityprofile/CreateHandlerTest.java | 243 +++++++++++ .../securityprofile/DeleteHandlerTest.java | 119 ++++++ .../iot/securityprofile/HandlerUtilsTest.java | 128 ++++++ .../iot/securityprofile/ListHandlerTest.java | 102 +++++ .../iot/securityprofile/ReadHandlerTest.java | 166 ++++++++ .../iot/securityprofile/TestConstants.java | 121 ++++++ .../iot/securityprofile/TranslatorTest.java | 185 +++++++++ .../securityprofile/UpdateHandlerTest.java | 391 ++++++++++++++++++ aws-iot-securityprofile/template.yml | 23 ++ 28 files changed, 3267 insertions(+) create mode 100644 aws-iot-securityprofile/.gitignore create mode 100644 aws-iot-securityprofile/.rpdk-config create mode 100644 aws-iot-securityprofile/README.md create mode 100644 aws-iot-securityprofile/aws-iot-securityprofile.json create mode 100644 aws-iot-securityprofile/inputs/inputs_1_create.json create mode 100644 aws-iot-securityprofile/inputs/inputs_1_invalid.json create mode 100644 aws-iot-securityprofile/inputs/inputs_1_update.json create mode 100644 aws-iot-securityprofile/lombok.config create mode 100644 aws-iot-securityprofile/pom.xml create mode 100644 aws-iot-securityprofile/resource-role.yaml create mode 100644 aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/CallbackContext.java create mode 100644 aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/Configuration.java create mode 100644 aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/CreateHandler.java create mode 100644 aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/DeleteHandler.java create mode 100644 aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/HandlerUtils.java create mode 100644 aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/ListHandler.java create mode 100644 aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/ReadHandler.java create mode 100644 aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/Translator.java create mode 100644 aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/UpdateHandler.java create mode 100644 aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/CreateHandlerTest.java create mode 100644 aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/DeleteHandlerTest.java create mode 100644 aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/HandlerUtilsTest.java create mode 100644 aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/ListHandlerTest.java create mode 100644 aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/ReadHandlerTest.java create mode 100644 aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/TestConstants.java create mode 100644 aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/TranslatorTest.java create mode 100644 aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/UpdateHandlerTest.java create mode 100644 aws-iot-securityprofile/template.yml diff --git a/aws-iot-securityprofile/.gitignore b/aws-iot-securityprofile/.gitignore new file mode 100644 index 0000000..5eb6238 --- /dev/null +++ b/aws-iot-securityprofile/.gitignore @@ -0,0 +1,23 @@ +# macOS +.DS_Store +._* + +# Maven outputs +.classpath + +# IntelliJ +*.iml +.idea +out.java +out/ +.settings +.project + +# auto-generated files +target/ + +# our logs +rpdk.log + +# contains credentials +sam-tests/ diff --git a/aws-iot-securityprofile/.rpdk-config b/aws-iot-securityprofile/.rpdk-config new file mode 100644 index 0000000..6626f13 --- /dev/null +++ b/aws-iot-securityprofile/.rpdk-config @@ -0,0 +1,22 @@ +{ + "typeName": "AWS::IoT::SecurityProfile", + "language": "java", + "runtime": "java8", + "entrypoint": "com.amazonaws.iot.securityprofile.HandlerWrapper::handleRequest", + "testEntrypoint": "com.amazonaws.iot.securityprofile.HandlerWrapper::testEntrypoint", + "settings": { + "version": false, + "subparser_name": null, + "verbose": 0, + "force": false, + "type_name": null, + "namespace": [ + "com", + "amazonaws", + "iot", + "securityprofile" + ], + "codegen_template_path": "guided_aws", + "protocolVersion": "2.0.0" + } +} diff --git a/aws-iot-securityprofile/README.md b/aws-iot-securityprofile/README.md new file mode 100644 index 0000000..7b69564 --- /dev/null +++ b/aws-iot-securityprofile/README.md @@ -0,0 +1,19 @@ +# AWS::IoT::SecurityProfile + +## Running Contract Tests + +You can execute the following commands to run the tests. +You will need to have docker installed and running. + +```bash +# Package the code with Maven +mvn package +# Start SAM which will execute lambdas in Docker +sam local start-lambda + +# In a separate terminal, run the contract tests +cfn test --enforce-timeout 240 + +# Execute a single test +cfn test --enforce-timeout 240 -- -k +``` diff --git a/aws-iot-securityprofile/aws-iot-securityprofile.json b/aws-iot-securityprofile/aws-iot-securityprofile.json new file mode 100644 index 0000000..f5314f4 --- /dev/null +++ b/aws-iot-securityprofile/aws-iot-securityprofile.json @@ -0,0 +1,359 @@ +{ + "typeName": "AWS::IoT::SecurityProfile", + "description": "A security profile defines a set of expected behaviors for devices in your account.", + "sourceUrl": "https://github.com/aws-cloudformation/aws-cloudformation-resource-providers-iot.git", + "definitions": { + "Behavior": { + "description": "A security profile behavior.", + "type": "object", + "properties": { + "Name": { + "description": "The name for the behavior.", + "type": "string", + "pattern": "[a-zA-Z0-9:_-]+", + "minLength": 1, + "maxLength": 128 + }, + "Metric": { + "description": "What is measured by the behavior.", + "type": "string", + "pattern": "[a-zA-Z0-9:_-]+", + "minLength": 1, + "maxLength": 128 + }, + "MetricDimension": { + "$ref": "#/definitions/MetricDimension" + }, + "Criteria": { + "$ref": "#/definitions/BehaviorCriteria" + } + }, + "required": [ + "Name" + ], + "additionalProperties": false + }, + "MetricDimension": { + "description": "The dimension of a metric.", + "type": "object", + "properties": { + "DimensionName": { + "description": "A unique identifier for the dimension.", + "type": "string", + "pattern": "[a-zA-Z0-9:_-]+", + "minLength": 1, + "maxLength": 128 + }, + "Operator": { + "description": "Defines how the dimensionValues of a dimension are interpreted.", + "type": "string", + "enum": [ + "IN", + "NOT_IN" + ] + } + }, + "required": [ + "DimensionName" + ], + "additionalProperties": false + }, + "BehaviorCriteria": { + "description": "The criteria by which the behavior is determined to be normal.", + "type": "object", + "properties": { + "ComparisonOperator": { + "description": "The operator that relates the thing measured (metric) to the criteria (containing a value or statisticalThreshold).", + "type": "string", + "enum": [ + "less-than", + "less-than-equals", + "greater-than", + "greater-than-equals", + "in-cidr-set", + "not-in-cidr-set", + "in-port-set", + "not-in-port-set" + ] + }, + "Value": { + "$ref": "#/definitions/MetricValue" + }, + "DurationSeconds": { + "type": "integer", + "description": "Use this to specify the time duration over which the behavior is evaluated." + }, + "ConsecutiveDatapointsToAlarm": { + "description": "If a device is in violation of the behavior for the specified number of consecutive datapoints, an alarm occurs. If not specified, the default is 1.", + "type": "integer", + "minimum": 1, + "maximum": 10 + }, + "ConsecutiveDatapointsToClear": { + "description": "If an alarm has occurred and the offending device is no longer in violation of the behavior for the specified number of consecutive datapoints, the alarm is cleared. If not specified, the default is 1.", + "type": "integer", + "minimum": 1, + "maximum": 10 + }, + "StatisticalThreshold": { + "$ref": "#/definitions/StatisticalThreshold" + } + }, + "additionalProperties": false + }, + "MetricValue": { + "description": "The value to be compared with the metric.", + "type": "object", + "properties": { + "Count": { + "description": "If the ComparisonOperator calls for a numeric value, use this to specify that (integer) numeric value to be compared with the metric.", + "type": "string", + "minimum": 0 + }, + "Cidrs": { + "description": "If the ComparisonOperator calls for a set of CIDRs, use this to specify that set to be compared with the metric.", + "type": "array", + "uniqueItems": true, + "insertionOrder": false, + "items": { + "type": "string" + } + }, + "Ports": { + "description": "If the ComparisonOperator calls for a set of ports, use this to specify that set to be compared with the metric.", + "type": "array", + "uniqueItems": true, + "insertionOrder": false, + "items": { + "type": "integer", + "minimum": 0, + "maximum": 65535 + } + } + }, + "additionalProperties": false + }, + "StatisticalThreshold": { + "description": "A statistical ranking (percentile) which indicates a threshold value by which a behavior is determined to be in compliance or in violation of the behavior.", + "type": "object", + "properties": { + "Statistic": { + "description": "The percentile which resolves to a threshold value by which compliance with a behavior is determined", + "type": "string", + "enum": [ + "Average", + "p0", + "p0.1", + "p0.01", + "p1", + "p10", + "p50", + "p90", + "p99", + "p99.9", + "p99.99", + "p100" + ] + } + }, + "additionalProperties": false + }, + "AlertTarget": { + "description": "A structure containing the alert target ARN and the role ARN.", + "type": "object", + "properties": { + "AlertTargetArn": { + "description": "The ARN of the notification target to which alerts are sent.", + "type": "string", + "maxLength": 2048 + }, + "RoleArn": { + "description": "The ARN of the role that grants permission to send alerts to the notification target.", + "type": "string", + "minLength": 20, + "maxLength": 2048 + } + }, + "required": [ + "AlertTargetArn", + "RoleArn" + ], + "additionalProperties": false + }, + "MetricToRetain": { + "description": "The metric you want to retain. Dimensions are optional.", + "type": "object", + "properties": { + "Metric": { + "description": "What is measured by the behavior.", + "type": "string", + "pattern": "[a-zA-Z0-9:_-]+", + "minLength": 1, + "maxLength": 128 + }, + "MetricDimension": { + "$ref": "#/definitions/MetricDimension" + } + }, + "required": [ + "Metric" + ], + "additionalProperties": false + }, + "Tag": { + "description": "A key-value pair to associate with a resource.", + "type": "object", + "properties": { + "Key": { + "type": "string", + "description": "The tag's key.", + "minLength": 1, + "maxLength": 128 + }, + "Value": { + "type": "string", + "description": "The tag's value.", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "Value", + "Key" + ], + "additionalProperties": false + } + }, + "properties": { + "SecurityProfileName": { + "description": "A unique identifier for the security profile.", + "type": "string", + "pattern": "[a-zA-Z0-9:_-]+", + "minLength": 1, + "maxLength": 128 + }, + "SecurityProfileDescription": { + "description": "A description of the security profile.", + "type": "string", + "maxLength": 1000 + }, + "Behaviors": { + "description": "Specifies the behaviors that, when violated by a device (thing), cause an alert.", + "type": "array", + "maxLength": 100, + "uniqueItems": true, + "insertionOrder": false, + "items": { + "$ref": "#/definitions/Behavior" + } + }, + "AlertTargets": { + "description": "Specifies the destinations to which alerts are sent.", + "type": "object", + "patternProperties": { + "[a-zA-Z0-9:_-]+": { + "$ref": "#/definitions/AlertTarget" + } + }, + "additionalProperties": false + }, + "AdditionalMetricsToRetain": { + "description": "This parameter has been deprecated, please use AdditionalMetricsToRetainV2.", + "type": "array", + "uniqueItems": true, + "insertionOrder": false, + "items": { + "type": "string", + "pattern": "[a-zA-Z0-9:_-]+", + "minLength": 1, + "maxLength": 128 + } + }, + "AdditionalMetricsToRetainV2": { + "description": "A list of metrics whose data is retained (stored). By default, data is retained for any metric used in the profile's behaviors, but it is also retained for any metric specified here.", + "type": "array", + "uniqueItems": true, + "insertionOrder": false, + "items": { + "$ref": "#/definitions/MetricToRetain" + } + }, + "Tags": { + "description": "Metadata that can be used to manage the security profile.", + "type": "array", + "maxItems": 50, + "uniqueItems": true, + "insertionOrder": false, + "items": { + "$ref": "#/definitions/Tag" + } + }, + "TargetArns": { + "description": "A set of target ARNs that the security profile is attached to.", + "type": "array", + "uniqueItems": true, + "insertionOrder": false, + "items": { + "description": "The ARN of the target to which the security profile is attached.", + "type": "string", + "maxLength": 2048 + } + }, + "SecurityProfileArn": { + "description": "The ARN (Amazon resource name) of the created security profile.", + "type": "string" + } + }, + "additionalProperties": false, + "primaryIdentifier": [ + "/properties/SecurityProfileName" + ], + "required": [ + "SecurityProfileName" + ], + "createOnlyProperties": [ + "/properties/SecurityProfileName" + ], + "readOnlyProperties": [ + "/properties/SecurityProfileArn" + ], + "handlers": { + "create": { + "permissions": [ + "iot:CreateSecurityProfile", + "iot:AttachSecurityProfile", + "iam:PassRole" + ] + }, + "read": { + "permissions": [ + "iot:DescribeSecurityProfile", + "iot:ListTagsForResource", + "iot:ListTargetsForSecurityProfile" + ] + }, + "update": { + "permissions": [ + "iot:UpdateSecurityProfile", + "iot:ListTargetsForSecurityProfile", + "iot:AttachSecurityProfile", + "iot:DetachSecurityProfile", + "iot:ListTagsForResource", + "iot:UntagResource", + "iot:TagResource", + "iam:PassRole" + ] + }, + "delete": { + "permissions": [ + "iot:DescribeSecurityProfile", + "iot:DeleteSecurityProfile" + ] + }, + "list": { + "permissions": [ + "iot:ListDimensions" + ] + } + } +} diff --git a/aws-iot-securityprofile/inputs/inputs_1_create.json b/aws-iot-securityprofile/inputs/inputs_1_create.json new file mode 100644 index 0000000..5448b6c --- /dev/null +++ b/aws-iot-securityprofile/inputs/inputs_1_create.json @@ -0,0 +1,32 @@ +{ + "SecurityProfileName": "CfnContractTest", + "SecurityProfileDescription": "A valid security profile 1!@#$%^&*()", + "Behaviors": [ + { + "Name": "Behavior1:_-", + "Metric": "aws:listening-tcp-ports", + "Criteria": { + "ComparisonOperator": "in-port-set", + "ConsecutiveDatapointsToAlarm": 1, + "ConsecutiveDatapointsToClear": 1, + "Value": { + "Ports": [ + 443 + ] + } + } + } + ], + "AdditionalMetricsToRetainV2": [ + { + "Metric": "aws:num-messages-sent" + } + ], + "TargetArns": [], + "Tags": [ + { + "Key": "testTagKey", + "Value": "tagValue" + } + ] +} diff --git a/aws-iot-securityprofile/inputs/inputs_1_invalid.json b/aws-iot-securityprofile/inputs/inputs_1_invalid.json new file mode 100644 index 0000000..98d77ba --- /dev/null +++ b/aws-iot-securityprofile/inputs/inputs_1_invalid.json @@ -0,0 +1,9 @@ +{ + "SecurityProfileName": "CfnContractTest-Invalid", + "AdditionalMetricsToRetainV2": [ + { + "Metric": "aws:num-messages-sent" + } + ], + "SecurityProfileArn": "Arn is a read-only property" +} diff --git a/aws-iot-securityprofile/inputs/inputs_1_update.json b/aws-iot-securityprofile/inputs/inputs_1_update.json new file mode 100644 index 0000000..ddbaefb --- /dev/null +++ b/aws-iot-securityprofile/inputs/inputs_1_update.json @@ -0,0 +1,31 @@ +{ + "SecurityProfileName": "CfnContractTest", + "SecurityProfileDescription": "A valid security profile 1!@#$%^&*()", + "Behaviors": [ + { + "Name": "Behavior2:_-", + "Metric": "aws:source-ip-address", + "Criteria": { + "ComparisonOperator": "in-cidr-set", + "ConsecutiveDatapointsToAlarm": 2, + "ConsecutiveDatapointsToClear": 2, + "Value": { + "Cidrs": [ + "192.168.100.14/24" + ] + } + } + } + ], + "AdditionalMetricsToRetainV2": [ + { + "Metric": "aws:listening-tcp-ports" + } + ], + "Tags": [ + { + "Key": "testTagKey", + "Value": "tagValue" + } + ] +} diff --git a/aws-iot-securityprofile/lombok.config b/aws-iot-securityprofile/lombok.config new file mode 100644 index 0000000..7a21e88 --- /dev/null +++ b/aws-iot-securityprofile/lombok.config @@ -0,0 +1 @@ +lombok.addLombokGeneratedAnnotation = true diff --git a/aws-iot-securityprofile/pom.xml b/aws-iot-securityprofile/pom.xml new file mode 100644 index 0000000..4f219de --- /dev/null +++ b/aws-iot-securityprofile/pom.xml @@ -0,0 +1,210 @@ + + + 4.0.0 + + com.amazonaws.iot.securityprofile + aws-iot-securityprofile-handler + aws-iot-securityprofile-handler + 1.0-SNAPSHOT + jar + + + 1.8 + 1.8 + UTF-8 + UTF-8 + + + + + + software.amazon.awssdk + iot + 2.15.7 + + + + software.amazon.cloudformation + aws-cloudformation-rpdk-java-plugin + [2.0.0,3.0.0) + + + + org.projectlombok + lombok + 1.18.4 + provided + + + + + org.assertj + assertj-core + 3.12.2 + test + + + + org.junit.jupiter + junit-jupiter + 5.5.0-M1 + test + + + + org.mockito + mockito-core + 2.26.0 + test + + + + org.mockito + mockito-junit-jupiter + 2.26.0 + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + + -Xlint:all,-options,-processing + -Werror + + + + + org.apache.maven.plugins + maven-shade-plugin + 2.3 + + false + + + + package + + shade + + + + + + org.codehaus.mojo + exec-maven-plugin + 1.6.0 + + + generate + generate-sources + + exec + + + cfn + generate + ${project.basedir} + + + + + + org.codehaus.mojo + build-helper-maven-plugin + 3.0.0 + + + add-source + generate-sources + + add-source + + + + ${project.basedir}/target/generated-sources/rpdk + + + + + + + org.apache.maven.plugins + maven-resources-plugin + 2.4 + + + maven-surefire-plugin + 3.0.0-M3 + + + org.jacoco + jacoco-maven-plugin + 0.8.4 + + + **/BaseConfiguration* + **/BaseHandler* + **/HandlerWrapper* + **/ResourceModel* + + + + + + prepare-agent + + + + report + test + + report + + + + jacoco-check + + check + + + + + PACKAGE + + + BRANCH + COVEREDRATIO + 0.8 + + + INSTRUCTION + COVEREDRATIO + 0.8 + + + + + + + + + + + + ${project.basedir} + + aws-iot-securityprofile.json + + + + + diff --git a/aws-iot-securityprofile/resource-role.yaml b/aws-iot-securityprofile/resource-role.yaml new file mode 100644 index 0000000..0c64db5 --- /dev/null +++ b/aws-iot-securityprofile/resource-role.yaml @@ -0,0 +1,42 @@ +AWSTemplateFormatVersion: "2010-09-09" +Description: > + This CloudFormation template creates a role assumed by CloudFormation + during CRUDL operations to mutate resources on behalf of the customer. + +Resources: + ExecutionRole: + Type: AWS::IAM::Role + Properties: + MaxSessionDuration: 8400 + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: resources.cloudformation.amazonaws.com + Action: sts:AssumeRole + Path: "/" + Policies: + - PolicyName: ResourceTypePolicy + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - "iam:PassRole" + - "iot:AttachSecurityProfile" + - "iot:CreateSecurityProfile" + - "iot:DeleteSecurityProfile" + - "iot:DescribeSecurityProfile" + - "iot:DetachSecurityProfile" + - "iot:ListDimensions" + - "iot:ListTagsForResource" + - "iot:ListTargetsForSecurityProfile" + - "iot:TagResource" + - "iot:UntagResource" + - "iot:UpdateSecurityProfile" + Resource: "*" +Outputs: + ExecutionRoleArn: + Value: + Fn::GetAtt: ExecutionRole.Arn diff --git a/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/CallbackContext.java b/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/CallbackContext.java new file mode 100644 index 0000000..245e8a3 --- /dev/null +++ b/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/CallbackContext.java @@ -0,0 +1,10 @@ +package com.amazonaws.iot.securityprofile; + +import software.amazon.cloudformation.proxy.StdCallbackContext; + +@lombok.Getter +@lombok.Setter +@lombok.ToString +@lombok.EqualsAndHashCode(callSuper = true) +public class CallbackContext extends StdCallbackContext { +} diff --git a/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/Configuration.java b/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/Configuration.java new file mode 100644 index 0000000..9c47566 --- /dev/null +++ b/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/Configuration.java @@ -0,0 +1,21 @@ +package com.amazonaws.iot.securityprofile; + +import java.util.Map; +import java.util.stream.Collectors; + +class Configuration extends BaseConfiguration { + + public Configuration() { + super("aws-iot-securityprofile.json"); + } + + @Override + public Map resourceDefinedTags(ResourceModel resourceModel) { + if (resourceModel.getTags() == null) { + return null; + } else { + return resourceModel.getTags().stream() + .collect(Collectors.toMap(Tag::getKey, Tag::getValue)); + } + } +} diff --git a/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/CreateHandler.java b/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/CreateHandler.java new file mode 100644 index 0000000..1e62663 --- /dev/null +++ b/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/CreateHandler.java @@ -0,0 +1,120 @@ +package com.amazonaws.iot.securityprofile; + +import java.util.Map; +import java.util.Set; + +import org.apache.commons.lang3.StringUtils; +import software.amazon.awssdk.services.iot.IotClient; +import software.amazon.awssdk.services.iot.model.AttachSecurityProfileRequest; +import software.amazon.awssdk.services.iot.model.CreateSecurityProfileRequest; +import software.amazon.awssdk.services.iot.model.CreateSecurityProfileResponse; +import software.amazon.awssdk.services.iot.model.IotException; +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.HandlerErrorCode; +import software.amazon.cloudformation.proxy.Logger; +import software.amazon.cloudformation.proxy.ProgressEvent; +import software.amazon.cloudformation.proxy.ResourceHandlerRequest; +import software.amazon.cloudformation.resource.IdentifierUtils; + +public class CreateHandler extends BaseHandler { + + // Copied value from software.amazon.cloudformation.resource.IdentifierUtils + private static final int GENERATED_NAME_MAX_LENGTH = 40; + + private final IotClient iotClient; + + public CreateHandler() { + iotClient = IotClient.builder().build(); + } + + @Override + public ProgressEvent handleRequest( + AmazonWebServicesClientProxy proxy, + ResourceHandlerRequest request, + CallbackContext callbackContext, + Logger logger) { + + CreateSecurityProfileRequest createRequest = translateToCreateRequest(request); + + ResourceModel model = request.getDesiredResourceState(); + if (!StringUtils.isEmpty(model.getSecurityProfileArn())) { + logger.log(String.format("Arn is read-only, but the caller passed %s.", model.getSecurityProfileArn())); + // Note: this is necessary even though Arn is marked readOnly in the schema. + return ProgressEvent.failed(model, callbackContext, HandlerErrorCode.InvalidRequest, + "Arn is a read-only property and cannot be set."); + } + + CreateSecurityProfileResponse createResponse; + try { + createResponse = proxy.injectCredentialsAndInvokeV2( + createRequest, iotClient::createSecurityProfile); + } catch (IotException e) { + throw Translator.translateIotExceptionToCfn(e); + } + + model.setSecurityProfileArn(createResponse.securityProfileArn()); + logger.log("Created " + createResponse.securityProfileArn()); + + // We're letting customers manage Security Profile attachments in the same CFN template, + // using the TargetArns field. Thus, we need to make an AttachSecurityProfile call for every target. + Set targetArns = model.getTargetArns(); + if (targetArns != null) { + for (String targetArn : targetArns) { + attachSecurityProfile(model.getSecurityProfileName(), targetArn, proxy); + logger.log("Attached the security profile to " + targetArn); + } + } + + return ProgressEvent.defaultSuccessHandler(model); + } + + private CreateSecurityProfileRequest translateToCreateRequest( + ResourceHandlerRequest request) { + + ResourceModel model = request.getDesiredResourceState(); + + // Like most services, we don't require an explicit resource name in the template, + // and, if it's not provided, generate one based on the stack ID and logical ID. + if (StringUtils.isBlank(model.getSecurityProfileName())) { + model.setSecurityProfileName(IdentifierUtils.generateResourceIdentifier( + request.getStackId(), request.getLogicalResourceIdentifier(), + request.getClientRequestToken(), GENERATED_NAME_MAX_LENGTH)); + } + + // getDesiredResourceTags combines the model and stack-level tags. + // Reference: https://tinyurl.com/yyxtd7w6 + Map tags = request.getDesiredResourceTags(); + // TODO: uncomment this after we update the service to allow these (only from CFN) + // SystemTags are the default stack-level tags with aws:cloudformation prefix + // tags.putAll(request.getSystemTags()); + + // Note that the handlers act as pass-through in terms of input validation. + // We have some validations in the json model, but we delegate deeper checks to the service. + // If there's an invalid input, we'll rethrow the service's InvalidRequestException with a readable message. + return CreateSecurityProfileRequest.builder() + .securityProfileName(model.getSecurityProfileName()) + .securityProfileDescription(model.getSecurityProfileDescription()) + .behaviors(Translator.translateBehaviorSetFromCfnToIot(model.getBehaviors())) + .alertTargetsWithStrings(Translator.translateAlertTargetMapFromCfnToIot(model.getAlertTargets())) + .additionalMetricsToRetain(model.getAdditionalMetricsToRetain()) + .additionalMetricsToRetainV2(Translator.translateMetricToRetainSetFromCfnToIot( + model.getAdditionalMetricsToRetainV2())) + .tags(Translator.translateTagsFromCfnToIot(tags)) + .build(); + } + + private void attachSecurityProfile(String securityProfileName, + String targetArn, + AmazonWebServicesClientProxy proxy) { + + AttachSecurityProfileRequest attachRequest = AttachSecurityProfileRequest.builder() + .securityProfileName(securityProfileName) + .securityProfileTargetArn(targetArn) + .build(); + try { + proxy.injectCredentialsAndInvokeV2(attachRequest, iotClient::attachSecurityProfile); + } catch (IotException e) { + throw Translator.translateIotExceptionToCfn(e); + } + } +} diff --git a/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/DeleteHandler.java b/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/DeleteHandler.java new file mode 100644 index 0000000..f66ac2c --- /dev/null +++ b/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/DeleteHandler.java @@ -0,0 +1,61 @@ +package com.amazonaws.iot.securityprofile; + +import software.amazon.awssdk.services.iot.IotClient; +import software.amazon.awssdk.services.iot.model.DeleteSecurityProfileRequest; +import software.amazon.awssdk.services.iot.model.DescribeSecurityProfileRequest; +import software.amazon.awssdk.services.iot.model.IotException; +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.Logger; +import software.amazon.cloudformation.proxy.ProgressEvent; +import software.amazon.cloudformation.proxy.ResourceHandlerRequest; + +public class DeleteHandler extends BaseHandler { + + private final IotClient iotClient; + + public DeleteHandler() { + iotClient = IotClient.builder().build(); + } + + @Override + public ProgressEvent handleRequest( + AmazonWebServicesClientProxy proxy, + ResourceHandlerRequest request, + CallbackContext callbackContext, + Logger logger) { + + ResourceModel model = request.getDesiredResourceState(); + + // From https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/resource-type-test-contract.html + // "A delete handler MUST return FAILED with a NotFound error code if the + // resource did not exist prior to the delete request." + // DeleteSecurityProfile API is idempotent, so we have to call Describe first. + DescribeSecurityProfileRequest describeRequest = DescribeSecurityProfileRequest.builder() + .securityProfileName(model.getSecurityProfileName()) + .build(); + try { + proxy.injectCredentialsAndInvokeV2(describeRequest, iotClient::describeSecurityProfile); + } catch (IotException e) { + // If the resource doesn't exist, DescribeSecurityProfile will throw NotFoundException, + // which we'll rethrow as CfnNotFoundException - that's all we need to do. + // CFN (the caller) will swallow this NotFound exception and the customer will see success. + throw Translator.translateIotExceptionToCfn(e); + } + logger.log(String.format("Called Describe for %s with name %s, accountId %s.", + ResourceModel.TYPE_NAME, model.getSecurityProfileName(), request.getAwsAccountId())); + + DeleteSecurityProfileRequest deleteRequest = DeleteSecurityProfileRequest.builder() + .securityProfileName(model.getSecurityProfileName()) + .build(); + try { + proxy.injectCredentialsAndInvokeV2(deleteRequest, iotClient::deleteSecurityProfile); + } catch (IotException e) { + throw Translator.translateIotExceptionToCfn(e); + } + + logger.log(String.format("Deleted %s with name %s, accountId %s.", + ResourceModel.TYPE_NAME, model.getSecurityProfileName(), request.getAwsAccountId())); + + return ProgressEvent.defaultSuccessHandler(null); + } +} diff --git a/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/HandlerUtils.java b/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/HandlerUtils.java new file mode 100644 index 0000000..641fc5b --- /dev/null +++ b/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/HandlerUtils.java @@ -0,0 +1,71 @@ +package com.amazonaws.iot.securityprofile; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import software.amazon.awssdk.services.iot.IotClient; +import software.amazon.awssdk.services.iot.model.IotException; +import software.amazon.awssdk.services.iot.model.ListTagsForResourceRequest; +import software.amazon.awssdk.services.iot.model.ListTagsForResourceResponse; +import software.amazon.awssdk.services.iot.model.ListTargetsForSecurityProfileRequest; +import software.amazon.awssdk.services.iot.model.ListTargetsForSecurityProfileResponse; +import software.amazon.awssdk.services.iot.model.SecurityProfileTarget; +import software.amazon.awssdk.services.iot.model.Tag; +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; + +public class HandlerUtils { + + static Set listTargetsForSecurityProfile( + IotClient iotClient, + AmazonWebServicesClientProxy proxy, + String securityProfileName) { + + String nextToken = null; + Set result = new HashSet<>(); + do { + ListTargetsForSecurityProfileRequest listRequest = ListTargetsForSecurityProfileRequest.builder() + .securityProfileName(securityProfileName) + .nextToken(nextToken) + .build(); + ListTargetsForSecurityProfileResponse listResponse; + try { + listResponse = proxy.injectCredentialsAndInvokeV2( + listRequest, iotClient::listTargetsForSecurityProfile); + } catch (IotException e) { + throw Translator.translateIotExceptionToCfn(e); + } + List securityProfileTargets = listResponse.securityProfileTargets(); + securityProfileTargets.forEach(target -> result.add(target.arn())); + nextToken = listResponse.nextToken(); + } while (nextToken != null); + + return result; + } + + static Set listTags( + IotClient iotClient, + AmazonWebServicesClientProxy proxy, + String resourceArn) { + + String nextToken = null; + Set result = new HashSet<>(); + do { + ListTagsForResourceRequest listTagsRequest = ListTagsForResourceRequest.builder() + .resourceArn(resourceArn) + .nextToken(nextToken) + .build(); + ListTagsForResourceResponse listTagsForResourceResponse; + try { + listTagsForResourceResponse = proxy.injectCredentialsAndInvokeV2( + listTagsRequest, iotClient::listTagsForResource); + } catch (IotException e) { + throw Translator.translateIotExceptionToCfn(e); + } + result.addAll(listTagsForResourceResponse.tags()); + nextToken = listTagsForResourceResponse.nextToken(); + } while (nextToken != null); + + return result; + } +} diff --git a/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/ListHandler.java b/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/ListHandler.java new file mode 100644 index 0000000..01b69c6 --- /dev/null +++ b/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/ListHandler.java @@ -0,0 +1,58 @@ +package com.amazonaws.iot.securityprofile; + +import java.util.List; +import java.util.stream.Collectors; + +import software.amazon.awssdk.services.iot.IotClient; +import software.amazon.awssdk.services.iot.model.IotException; +import software.amazon.awssdk.services.iot.model.ListSecurityProfilesRequest; +import software.amazon.awssdk.services.iot.model.ListSecurityProfilesResponse; +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.Logger; +import software.amazon.cloudformation.proxy.OperationStatus; +import software.amazon.cloudformation.proxy.ProgressEvent; +import software.amazon.cloudformation.proxy.ResourceHandlerRequest; + +public class ListHandler extends BaseHandler { + + private final IotClient iotClient; + + public ListHandler() { + iotClient = IotClient.builder().build(); + } + + @Override + public ProgressEvent handleRequest( + AmazonWebServicesClientProxy proxy, + ResourceHandlerRequest request, + CallbackContext callbackContext, + Logger logger) { + + ListSecurityProfilesRequest listRequest = ListSecurityProfilesRequest.builder() + .nextToken(request.getNextToken()) + .build(); + + ListSecurityProfilesResponse listSecurityProfilesResponse; + try { + listSecurityProfilesResponse = proxy.injectCredentialsAndInvokeV2( + listRequest, iotClient::listSecurityProfiles); + } catch (IotException e) { + throw Translator.translateIotExceptionToCfn(e); + } + + List models = listSecurityProfilesResponse.securityProfileIdentifiers().stream() + .map(identifier -> ResourceModel.builder() + .securityProfileName(identifier.name()) + .build()) + .collect(Collectors.toList()); + + logger.log(String.format("Listed %s resources for accountId %s.", + ResourceModel.TYPE_NAME, request.getAwsAccountId())); + + return ProgressEvent.builder() + .resourceModels(models) + .nextToken(listSecurityProfilesResponse.nextToken()) + .status(OperationStatus.SUCCESS) + .build(); + } +} diff --git a/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/ReadHandler.java b/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/ReadHandler.java new file mode 100644 index 0000000..28c2e7c --- /dev/null +++ b/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/ReadHandler.java @@ -0,0 +1,114 @@ +package com.amazonaws.iot.securityprofile; + +import java.util.HashSet; +import java.util.Set; + +import com.google.common.annotations.VisibleForTesting; + +import software.amazon.awssdk.services.iot.IotClient; +import software.amazon.awssdk.services.iot.model.DescribeSecurityProfileRequest; +import software.amazon.awssdk.services.iot.model.DescribeSecurityProfileResponse; +import software.amazon.awssdk.services.iot.model.IotException; +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.Logger; +import software.amazon.cloudformation.proxy.ProgressEvent; +import software.amazon.cloudformation.proxy.ResourceHandlerRequest; + +public class ReadHandler extends BaseHandler { + + private final IotClient iotClient; + + public ReadHandler() { + iotClient = IotClient.builder().build(); + } + + @Override + public ProgressEvent handleRequest( + AmazonWebServicesClientProxy proxy, + ResourceHandlerRequest request, + CallbackContext callbackContext, + Logger logger) { + + ResourceModel model = request.getDesiredResourceState(); + String securityProfileName = model.getSecurityProfileName(); + + DescribeSecurityProfileRequest describeRequest = DescribeSecurityProfileRequest.builder() + .securityProfileName(securityProfileName) + .build(); + DescribeSecurityProfileResponse describeResponse; + try { + describeResponse = proxy.injectCredentialsAndInvokeV2( + describeRequest, iotClient::describeSecurityProfile); + } catch (IotException e) { + throw Translator.translateIotExceptionToCfn(e); + } + + String securityProfileArn = describeResponse.securityProfileArn(); + logger.log("Called Describe for " + securityProfileArn); + + // DescribeSecurityProfile doesn't provide the attached targets, so we call ListTargetsForSecurityProfile. + Set targetArns = listTargetsForSecurityProfile(proxy, securityProfileName); + logger.log("Listed targets for " + securityProfileArn); + + // DescribeSecurityProfile doesn't provide the tags, so we call ListTagsForResource. + Set iotTags = listTags(proxy, securityProfileArn); + logger.log("Listed tags for " + securityProfileArn); + + ResourceModel resourceModel = buildResourceModel(describeResponse, targetArns, iotTags); + + return ProgressEvent.defaultSuccessHandler(resourceModel); + } + + // This facilitates mocking in the unit tests. + // It would be nicer to instead pass HandlerUtils (which we can mock) + // to the constructor, but the framework requires the constructor to have 0 args. + @VisibleForTesting + Set listTags(AmazonWebServicesClientProxy proxy, + String resourceArn) { + return HandlerUtils.listTags(iotClient, proxy, resourceArn); + } + + @VisibleForTesting + Set listTargetsForSecurityProfile(AmazonWebServicesClientProxy proxy, + String securityProfileName) { + return HandlerUtils.listTargetsForSecurityProfile( + iotClient, proxy, securityProfileName); + } + + ResourceModel buildResourceModel( + DescribeSecurityProfileResponse describeResponse, + Set targetArns, + Set iotTags) { + + ResourceModel.ResourceModelBuilder resourceModelBuilder = ResourceModel.builder() + .securityProfileName(describeResponse.securityProfileName()) + .securityProfileDescription(describeResponse.securityProfileDescription()) + .securityProfileArn(describeResponse.securityProfileArn()) + .targetArns(targetArns) + .tags(Translator.translateTagsFromIotToCfn(iotTags)); + + // For collections, we're using the .has* methods to differentiate between null and empty collections + // from DescribeSecurityProfileResponse. + // SDK converts nulls from Describe API to empty DefaultSdkAutoConstructList/Maps, + // so if we simply translate without the .has* check, nulls will turn into empty collections. + if (describeResponse.hasBehaviors()) { + resourceModelBuilder.behaviors(Translator.translateBehaviorListFromIotToCfn( + describeResponse.behaviors())); + } + if (describeResponse.hasAlertTargets()) { + resourceModelBuilder.alertTargets(Translator.translateAlertTargetMapFromIotToCfn( + describeResponse.alertTargetsAsStrings())); + } + if (describeResponse.hasAdditionalMetricsToRetain()) { + resourceModelBuilder.additionalMetricsToRetain( + new HashSet<>(describeResponse.additionalMetricsToRetain())); + } + if (describeResponse.hasAdditionalMetricsToRetainV2()) { + resourceModelBuilder.additionalMetricsToRetainV2( + Translator.translateMetricToRetainListFromIotToCfn( + describeResponse.additionalMetricsToRetainV2())); + } + + return resourceModelBuilder.build(); + } +} diff --git a/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/Translator.java b/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/Translator.java new file mode 100644 index 0000000..d93d6df --- /dev/null +++ b/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/Translator.java @@ -0,0 +1,326 @@ +package com.amazonaws.iot.securityprofile; + +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import lombok.NonNull; +import software.amazon.awssdk.services.iot.model.InternalFailureException; +import software.amazon.awssdk.services.iot.model.InvalidRequestException; +import software.amazon.awssdk.services.iot.model.IotException; +import software.amazon.awssdk.services.iot.model.LimitExceededException; +import software.amazon.awssdk.services.iot.model.ResourceAlreadyExistsException; +import software.amazon.awssdk.services.iot.model.ResourceNotFoundException; +import software.amazon.awssdk.services.iot.model.Tag; +import software.amazon.awssdk.services.iot.model.ThrottlingException; +import software.amazon.awssdk.services.iot.model.UnauthorizedException; +import software.amazon.cloudformation.exceptions.BaseHandlerException; +import software.amazon.cloudformation.exceptions.CfnAccessDeniedException; +import software.amazon.cloudformation.exceptions.CfnAlreadyExistsException; +import software.amazon.cloudformation.exceptions.CfnInternalFailureException; +import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; +import software.amazon.cloudformation.exceptions.CfnNotFoundException; +import software.amazon.cloudformation.exceptions.CfnServiceLimitExceededException; +import software.amazon.cloudformation.exceptions.CfnThrottlingException; + +public class Translator { + + static Set translateBehaviorSetFromCfnToIot( + Set cfnBehaviors) { + if (cfnBehaviors == null) { + return null; + } + return cfnBehaviors.stream() + .map(Translator::translateBehaviorFromCfnToIot) + .collect(Collectors.toSet()); + } + + static Set translateBehaviorListFromIotToCfn( + @NonNull List iotBehaviors) { + return iotBehaviors.stream() + .map(Translator::translateBehaviorFromIotToCfn) + .collect(Collectors.toSet()); + } + + private static software.amazon.awssdk.services.iot.model.Behavior translateBehaviorFromCfnToIot( + Behavior cfnBehavior) { + if (cfnBehavior == null) { + return null; + } + return software.amazon.awssdk.services.iot.model.Behavior.builder() + .name(cfnBehavior.getName()) + .metric(cfnBehavior.getMetric()) + .metricDimension(translateMetricDimensionFromCfnToIot(cfnBehavior.getMetricDimension())) + .criteria(translateBehaviorCriteriaFromCfnToIot(cfnBehavior.getCriteria())) + .build(); + } + + private static Behavior translateBehaviorFromIotToCfn( + @NonNull software.amazon.awssdk.services.iot.model.Behavior iotBehavior) { + return Behavior.builder() + .name(iotBehavior.name()) + .metric(iotBehavior.metric()) + .metricDimension(translateMetricDimensionFromIotToCfn(iotBehavior.metricDimension())) + .criteria(translateBehaviorCriteriaFromIotToCfn(iotBehavior.criteria())) + .build(); + } + + private static software.amazon.awssdk.services.iot.model.BehaviorCriteria translateBehaviorCriteriaFromCfnToIot( + BehaviorCriteria cfnCriteria) { + if (cfnCriteria == null) { + return null; + } + return software.amazon.awssdk.services.iot.model.BehaviorCriteria.builder() + .comparisonOperator(cfnCriteria.getComparisonOperator()) + .value(translateMetricValueFromCfnToIot(cfnCriteria.getValue())) + .durationSeconds(cfnCriteria.getDurationSeconds()) + .consecutiveDatapointsToAlarm(cfnCriteria.getConsecutiveDatapointsToAlarm()) + .consecutiveDatapointsToClear(cfnCriteria.getConsecutiveDatapointsToClear()) + .statisticalThreshold(translateStatisticalThresholdFromCfnToIot(cfnCriteria.getStatisticalThreshold())) + .build(); + } + + private static BehaviorCriteria translateBehaviorCriteriaFromIotToCfn( + software.amazon.awssdk.services.iot.model.BehaviorCriteria iotCriteria) { + if (iotCriteria == null) { + return null; + } + return BehaviorCriteria.builder() + .comparisonOperator(iotCriteria.comparisonOperatorAsString()) + .value(translateMetricValueFromIotToCfn(iotCriteria.value())) + .durationSeconds(iotCriteria.durationSeconds()) + .consecutiveDatapointsToAlarm(iotCriteria.consecutiveDatapointsToAlarm()) + .consecutiveDatapointsToClear(iotCriteria.consecutiveDatapointsToClear()) + .statisticalThreshold(translateStatisticalThresholdFromIotToCfn(iotCriteria.statisticalThreshold())) + .build(); + } + + static software.amazon.awssdk.services.iot.model.MetricValue translateMetricValueFromCfnToIot( + MetricValue cfnMetricValue) { + if (cfnMetricValue == null) { + return null; + } + // The MetricValue is a String in the CFN model because the json-schema doesn't support Long numbers. + // This is a known issue and some existing services are already using Strings as the workaround. + // For example, AWS::ElasticLoadBalancing::LoadBalancer HealthCheck + // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-elb-health-check.html + String countStringFromCfn = cfnMetricValue.getCount(); + Long countLong; + if (countStringFromCfn == null) { + countLong = null; + } else { + try { + countLong = Long.parseLong(countStringFromCfn); + } catch (NumberFormatException e) { + throw new CfnInvalidRequestException("Invalid value for Behavior::MetricValue::Count: " + + countStringFromCfn); + } + } + + return software.amazon.awssdk.services.iot.model.MetricValue.builder() + .count(countLong) + .cidrs(cfnMetricValue.getCidrs()) + .ports(cfnMetricValue.getPorts()) + .build(); + } + + static MetricValue translateMetricValueFromIotToCfn( + software.amazon.awssdk.services.iot.model.MetricValue iotMetricValue) { + if (iotMetricValue == null) { + return null; + } + + MetricValue.MetricValueBuilder metricValueBuilder = MetricValue.builder(); + if (iotMetricValue.count() != null) { + metricValueBuilder.count(iotMetricValue.count().toString()); + } + // For lists, we're using the .has* methods to differentiate between null and empty lists + // from DescribeSecurityProfileResponse. + // SDK converts nulls from Describe API to empty DefaultSdkAutoConstructLists, + // so if we simply translate without the .has* check, nulls will turn into empty lists. + if (iotMetricValue.hasCidrs()) { + metricValueBuilder.cidrs(new HashSet<>(iotMetricValue.cidrs())); + } + if (iotMetricValue.hasPorts()) { + metricValueBuilder.ports(new HashSet<>(iotMetricValue.ports())); + } + + return metricValueBuilder.build(); + } + + private static software.amazon.awssdk.services.iot.model.MetricDimension translateMetricDimensionFromCfnToIot( + MetricDimension cfnMetricDimension) { + if (cfnMetricDimension == null) { + return null; + } + return software.amazon.awssdk.services.iot.model.MetricDimension.builder() + .dimensionName(cfnMetricDimension.getDimensionName()) + .operator(cfnMetricDimension.getOperator()) + .build(); + } + + private static MetricDimension translateMetricDimensionFromIotToCfn( + software.amazon.awssdk.services.iot.model.MetricDimension iotMetricDimension) { + if (iotMetricDimension == null) { + return null; + } + return MetricDimension.builder() + .dimensionName(iotMetricDimension.dimensionName()) + .operator(iotMetricDimension.operatorAsString()) + .build(); + } + + private static software.amazon.awssdk.services.iot.model.StatisticalThreshold translateStatisticalThresholdFromCfnToIot( + StatisticalThreshold cfnThreshold) { + if (cfnThreshold == null) { + return null; + } + return software.amazon.awssdk.services.iot.model.StatisticalThreshold.builder() + .statistic(cfnThreshold.getStatistic()) + .build(); + } + + private static StatisticalThreshold translateStatisticalThresholdFromIotToCfn( + software.amazon.awssdk.services.iot.model.StatisticalThreshold iotThreshold) { + if (iotThreshold == null) { + return null; + } + return StatisticalThreshold.builder() + .statistic(iotThreshold.statistic()) + .build(); + } + + static Map translateAlertTargetMapFromCfnToIot( + Map cfnAlertTargetMap) { + if (cfnAlertTargetMap == null) { + return null; + } + return cfnAlertTargetMap.entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + entry -> translateAlertTargetFromCfnToIot(entry.getValue()))); + } + + static Map translateAlertTargetMapFromIotToCfn( + @NonNull Map iotAlertTargetMap) { + + return iotAlertTargetMap.entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + entry -> translateAlertTargetFromIotToCfn(entry.getValue()))); + } + + private static software.amazon.awssdk.services.iot.model.AlertTarget translateAlertTargetFromCfnToIot( + AlertTarget cfnAlertTarget) { + if (cfnAlertTarget == null) { + return null; + } + return software.amazon.awssdk.services.iot.model.AlertTarget.builder() + .alertTargetArn(cfnAlertTarget.getAlertTargetArn()) + .roleArn(cfnAlertTarget.getRoleArn()) + .build(); + } + + private static AlertTarget translateAlertTargetFromIotToCfn( + @NonNull software.amazon.awssdk.services.iot.model.AlertTarget iotAlertTarget) { + return AlertTarget.builder() + .alertTargetArn(iotAlertTarget.alertTargetArn()) + .roleArn(iotAlertTarget.roleArn()) + .build(); + } + + static Set translateMetricToRetainSetFromCfnToIot( + Set cfnMetricToRetainSet) { + if (cfnMetricToRetainSet == null) { + return null; + } + return cfnMetricToRetainSet.stream() + .map(Translator::translateMetricToRetainFromCfnToIot) + .collect(Collectors.toSet()); + } + + static Set translateMetricToRetainListFromIotToCfn( + @NonNull List iotMetricToRetainList) { + return iotMetricToRetainList.stream() + .map(Translator::translateMetricToRetainFromIotToCfn) + .collect(Collectors.toSet()); + } + + private static software.amazon.awssdk.services.iot.model.MetricToRetain translateMetricToRetainFromCfnToIot( + MetricToRetain cfnMetricToRetain) { + if (cfnMetricToRetain == null) { + return null; + } + return software.amazon.awssdk.services.iot.model.MetricToRetain.builder() + .metric(cfnMetricToRetain.getMetric()) + .metricDimension(translateMetricDimensionFromCfnToIot(cfnMetricToRetain.getMetricDimension())) + .build(); + } + + private static MetricToRetain translateMetricToRetainFromIotToCfn( + @NonNull software.amazon.awssdk.services.iot.model.MetricToRetain iotMetricToRetain) { + return MetricToRetain.builder() + .metric(iotMetricToRetain.metric()) + .metricDimension(translateMetricDimensionFromIotToCfn(iotMetricToRetain.metricDimension())) + .build(); + } + + static Set translateTagsFromCfnToIot(Map tags) { + if (tags == null) { + return null; + } + return tags.keySet().stream() + .map(key -> Tag.builder() + .key(key) + .value(tags.get(key)) + .build()) + .collect(Collectors.toSet()); + } + + static Set translateTagsFromIotToCfn( + Set tags) { + if (tags == null) { + return null; + } + return tags.stream() + .map(tag -> com.amazonaws.iot.securityprofile.Tag.builder() + .key(tag.key()) + .value(tag.value()) + .build()) + .collect(Collectors.toSet()); + } + + static BaseHandlerException translateIotExceptionToCfn(IotException e) { + // We're handling all the exceptions documented in API docs + // https://docs.aws.amazon.com/iot/latest/apireference/API_CreateSecurityProfile.html + // (+similar pages for other APIs) + // For Throttling and InternalFailure, we want CFN to retry, and it will do so based on the exception type. + // Reference with Retriable/Terminal in comments for each: https://tinyurl.com/y378qdno + if (e instanceof ResourceAlreadyExistsException) { + // Note regarding idempotency: + // CreateSecurityProfile API allows tags. CFN attaches its own stack level tags with the request. If a + // SecurityProfile is created out of band and then the same request is sent via CFN, the API will throw + // AlreadyExists because the CFN request will contain the stack level tags. + // This behavior satisfies the CreateHandler contract. + return new CfnAlreadyExistsException(e); + } else if (e instanceof InvalidRequestException) { + return new CfnInvalidRequestException(e); + } else if (e instanceof LimitExceededException) { + return new CfnServiceLimitExceededException(e); + } else if (e instanceof UnauthorizedException) { + return new CfnAccessDeniedException(e); + } else if (e instanceof InternalFailureException) { + return new CfnInternalFailureException(e); + } else if (e instanceof ThrottlingException) { + return new CfnThrottlingException(e); + } else if (e instanceof ResourceNotFoundException) { + return new CfnNotFoundException(e); + } else { + // Any other exception at this point is unexpected. CFN will catch this and convert appropriately. + // Reference: https://tinyurl.com/y6mphxbn + throw e; + } + } +} diff --git a/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/UpdateHandler.java b/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/UpdateHandler.java new file mode 100644 index 0000000..281d3f0 --- /dev/null +++ b/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/UpdateHandler.java @@ -0,0 +1,260 @@ +package com.amazonaws.iot.securityprofile; + + +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import com.google.common.annotations.VisibleForTesting; + +import software.amazon.awssdk.services.iot.IotClient; +import software.amazon.awssdk.services.iot.model.AttachSecurityProfileRequest; +import software.amazon.awssdk.services.iot.model.DetachSecurityProfileRequest; +import software.amazon.awssdk.services.iot.model.IotException; +import software.amazon.awssdk.services.iot.model.MetricToRetain; +import software.amazon.awssdk.services.iot.model.Tag; +import software.amazon.awssdk.services.iot.model.TagResourceRequest; +import software.amazon.awssdk.services.iot.model.UntagResourceRequest; +import software.amazon.awssdk.services.iot.model.UpdateSecurityProfileRequest; +import software.amazon.awssdk.services.iot.model.UpdateSecurityProfileResponse; +import software.amazon.awssdk.utils.CollectionUtils; +import software.amazon.awssdk.utils.StringUtils; +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.HandlerErrorCode; +import software.amazon.cloudformation.proxy.Logger; +import software.amazon.cloudformation.proxy.ProgressEvent; +import software.amazon.cloudformation.proxy.ResourceHandlerRequest; + +public class UpdateHandler extends BaseHandler { + + private final IotClient iotClient; + + public UpdateHandler() { + iotClient = IotClient.builder().build(); + } + + @Override + public ProgressEvent handleRequest( + AmazonWebServicesClientProxy proxy, + ResourceHandlerRequest request, + CallbackContext callbackContext, + Logger logger) { + + ResourceModel desiredModel = request.getDesiredResourceState(); + String desiredArn = desiredModel.getSecurityProfileArn(); + if (!StringUtils.isEmpty(desiredArn)) { + logger.log("Arn cannot be updated, caller setting it to " + desiredArn); + return ProgressEvent.failed(desiredModel, callbackContext, HandlerErrorCode.InvalidRequest, + "Arn cannot be updated."); + } + + String securityProfileArn = updateSecurityProfile(proxy, desiredModel, logger); + + // Security profile targets are managed by separate APIs, not UpdateSecurityProfile. + updateTargetAttachments(proxy, desiredModel, logger); + + // Same for tags. + updateTags(proxy, request, securityProfileArn, logger); + + desiredModel.setSecurityProfileArn(securityProfileArn); + return ProgressEvent.defaultSuccessHandler(desiredModel); + } + + /** + * @return The security profile's ARN. + */ + String updateSecurityProfile(AmazonWebServicesClientProxy proxy, + ResourceModel model, + Logger logger) { + + // If the desired template has no behaviors, passing an empty list in the behaviors field + // is not enough. UpdateSecurityProfile needs us to pass a deleteBehaviors=true flag. + boolean deleteBehaviors; + Set behaviorsForRequest; + if (CollectionUtils.isNullOrEmpty(model.getBehaviors())) { + deleteBehaviors = true; + behaviorsForRequest = null; + } else { + deleteBehaviors = false; + behaviorsForRequest = Translator.translateBehaviorSetFromCfnToIot(model.getBehaviors()); + } + + // Same for alertTargets + boolean deleteAlertTargets; + Map alertTargetsForRequest; + if (CollectionUtils.isNullOrEmpty(model.getAlertTargets())) { + deleteAlertTargets = true; + alertTargetsForRequest = null; + } else { + deleteAlertTargets = false; + alertTargetsForRequest = Translator.translateAlertTargetMapFromCfnToIot(model.getAlertTargets()); + } + + // For additionalMetricsToRetain, there's no separate flag for V2 + boolean deleteAdditionalMetricsToRetain; + boolean noMetricsToRetainProvided = + CollectionUtils.isNullOrEmpty(model.getAdditionalMetricsToRetain()) && + CollectionUtils.isNullOrEmpty(model.getAdditionalMetricsToRetainV2()); + Set additionalMetricsV1ForRequest; + Set additionalMetricsV2ForRequest; + if (noMetricsToRetainProvided) { + deleteAdditionalMetricsToRetain = true; + additionalMetricsV1ForRequest = null; + additionalMetricsV2ForRequest = null; + } else { + deleteAdditionalMetricsToRetain = false; + additionalMetricsV1ForRequest = model.getAdditionalMetricsToRetain(); + additionalMetricsV2ForRequest = Translator.translateMetricToRetainSetFromCfnToIot( + model.getAdditionalMetricsToRetainV2()); + } + + UpdateSecurityProfileRequest updateRequest = UpdateSecurityProfileRequest.builder() + .securityProfileName(model.getSecurityProfileName()) + .securityProfileDescription(model.getSecurityProfileDescription()) + .behaviors(behaviorsForRequest) + .alertTargetsWithStrings(alertTargetsForRequest) + .additionalMetricsToRetain(additionalMetricsV1ForRequest) + .additionalMetricsToRetainV2(additionalMetricsV2ForRequest) + .deleteBehaviors(deleteBehaviors) + .deleteAlertTargets(deleteAlertTargets) + .deleteAdditionalMetricsToRetain(deleteAdditionalMetricsToRetain) + .build(); + + UpdateSecurityProfileResponse updateResponse; + try { + updateResponse = proxy.injectCredentialsAndInvokeV2( + updateRequest, iotClient::updateSecurityProfile); + } catch (IotException e) { + throw Translator.translateIotExceptionToCfn(e); + } + String arn = updateResponse.securityProfileArn(); + logger.log("Called UpdateSecurityProfile for " + arn); + return arn; + } + + void updateTargetAttachments(AmazonWebServicesClientProxy proxy, + ResourceModel model, + Logger logger) { + + String securityProfileName = model.getSecurityProfileName(); + // Note: we're intentionally getting current attachments by calling ListTargetsForSecurityProfile + // rather than getting the previous state from CFN. This is in order to overwrite out-of-band changes. + // We have the same behavior in all Device Defender UpdateHandlers with regards to out-of-band updates. + Set currentTargets = listTargetsForSecurityProfile(proxy, securityProfileName); + + Set desiredTargets; + if (model.getTargetArns() == null) { + desiredTargets = Collections.emptySet(); + } else { + desiredTargets = model.getTargetArns(); + } + + Set targetsToAttach = desiredTargets.stream() + .filter(target -> !currentTargets.contains(target)) + .collect(Collectors.toSet()); + Set targetsToDetach = currentTargets.stream() + .filter(target -> !desiredTargets.contains(target)) + .collect(Collectors.toSet()); + + for (String targetArn : targetsToAttach) { + AttachSecurityProfileRequest attachRequest = AttachSecurityProfileRequest.builder() + .securityProfileName(securityProfileName) + .securityProfileTargetArn(targetArn) + .build(); + try { + proxy.injectCredentialsAndInvokeV2(attachRequest, iotClient::attachSecurityProfile); + } catch (IotException e) { + throw Translator.translateIotExceptionToCfn(e); + } + logger.log("Attached " + securityProfileName + " to " + targetArn); + } + + for (String targetArn : targetsToDetach) { + DetachSecurityProfileRequest detachRequest = DetachSecurityProfileRequest.builder() + .securityProfileName(securityProfileName) + .securityProfileTargetArn(targetArn) + .build(); + try { + proxy.injectCredentialsAndInvokeV2(detachRequest, iotClient::detachSecurityProfile); + } catch (IotException e) { + throw Translator.translateIotExceptionToCfn(e); + } + logger.log("Detached " + securityProfileName + " from " + targetArn); + } + } + + void updateTags(AmazonWebServicesClientProxy proxy, + ResourceHandlerRequest request, + String resourceArn, + Logger logger) { + + // Note: we're intentionally getting currentTags by calling ListTags rather than getting + // the previous state from CFN. This is in order to overwrite out-of-band changes. + // For example, if we used request.getPreviousResourceTags instead of ListTags, if a user added a new tag + // via TagResource and didn't add it to the template, we wouldn't know about it and wouldn't untag it. + // Yet we should, otherwise the resource wouldn't equate the template. + Set currentTags = listTags(proxy, resourceArn); + + // getDesiredResourceTags includes model+stack-level tags, reference: https://tinyurl.com/y55mqrnc + Set nullableDesiredTags = Translator.translateTagsFromCfnToIot(request.getDesiredResourceTags()); + Set desiredTags = nullableDesiredTags == null ? Collections.emptySet() : nullableDesiredTags; + // TODO: uncomment this after we update the service to allow these (only from CFN) + // SystemTags are the default stack-level tags with aws:cloudformation prefix + // desiredTags.addAll(Translator.translateTagsFromCfnToIot(request.getSystemTags())); + + Set desiredTagKeys = desiredTags.stream() + .map(Tag::key) + .collect(Collectors.toSet()); + + Set tagKeysToDetach = currentTags.stream() + .filter(tag -> !desiredTagKeys.contains(tag.key())) + .map(Tag::key) + .collect(Collectors.toSet()); + Set tagsToAttach = desiredTags.stream() + .filter(tag -> !currentTags.contains(tag)) + .collect(Collectors.toSet()); + + if (!tagsToAttach.isEmpty()) { + TagResourceRequest tagResourceRequest = TagResourceRequest.builder() + .resourceArn(resourceArn) + .tags(tagsToAttach) + .build(); + try { + proxy.injectCredentialsAndInvokeV2(tagResourceRequest, iotClient::tagResource); + } catch (IotException e) { + throw Translator.translateIotExceptionToCfn(e); + } + logger.log("Called TagResource for " + resourceArn); + } + + if (!tagKeysToDetach.isEmpty()) { + UntagResourceRequest untagResourceRequest = UntagResourceRequest.builder() + .resourceArn(resourceArn) + .tagKeys(tagKeysToDetach) + .build(); + try { + proxy.injectCredentialsAndInvokeV2(untagResourceRequest, iotClient::untagResource); + } catch (IotException e) { + throw Translator.translateIotExceptionToCfn(e); + } + logger.log("Called UntagResource for " + resourceArn); + } + } + + // This facilitates mocking in the unit tests. + // It would be nicer to instead pass HandlerUtils (which we can mock) + // to the constructor, but the framework requires the constructor to have 0 args. + @VisibleForTesting + Set listTags(AmazonWebServicesClientProxy proxy, + String resourceArn) { + return HandlerUtils.listTags(iotClient, proxy, resourceArn); + } + + @VisibleForTesting + Set listTargetsForSecurityProfile(AmazonWebServicesClientProxy proxy, + String securityProfileName) { + return HandlerUtils.listTargetsForSecurityProfile( + iotClient, proxy, securityProfileName); + } +} diff --git a/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/CreateHandlerTest.java b/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/CreateHandlerTest.java new file mode 100644 index 0000000..4906deb --- /dev/null +++ b/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/CreateHandlerTest.java @@ -0,0 +1,243 @@ +package com.amazonaws.iot.securityprofile; + +import static com.amazonaws.iot.securityprofile.TestConstants.ADDITIONAL_METRICS_CFN; +import static com.amazonaws.iot.securityprofile.TestConstants.ADDITIONAL_METRICS_IOT; +import static com.amazonaws.iot.securityprofile.TestConstants.CLIENT_REQUEST_TOKEN; +import static com.amazonaws.iot.securityprofile.TestConstants.LOGICAL_IDENTIFIER; +import static com.amazonaws.iot.securityprofile.TestConstants.SECURITY_PROFILE_ARN; +import static com.amazonaws.iot.securityprofile.TestConstants.SECURITY_PROFILE_NAME; +import static com.amazonaws.iot.securityprofile.TestConstants.TAG_1_CFN_SET; +import static com.amazonaws.iot.securityprofile.TestConstants.TAG_1_IOT; +import static com.amazonaws.iot.securityprofile.TestConstants.TAG_1_STRINGMAP; +import static com.amazonaws.iot.securityprofile.TestConstants.TARGET_ARNS; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import software.amazon.awssdk.services.iot.model.AttachSecurityProfileRequest; +import software.amazon.awssdk.services.iot.model.AttachSecurityProfileResponse; +import software.amazon.awssdk.services.iot.model.CreateSecurityProfileRequest; +import software.amazon.awssdk.services.iot.model.CreateSecurityProfileResponse; +import software.amazon.awssdk.services.iot.model.IotRequest; +import software.amazon.awssdk.services.iot.model.ResourceAlreadyExistsException; +import software.amazon.awssdk.services.iot.model.ResourceNotFoundException; +import software.amazon.cloudformation.exceptions.CfnAlreadyExistsException; +import software.amazon.cloudformation.exceptions.CfnNotFoundException; +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.HandlerErrorCode; +import software.amazon.cloudformation.proxy.Logger; +import software.amazon.cloudformation.proxy.OperationStatus; +import software.amazon.cloudformation.proxy.ProgressEvent; +import software.amazon.cloudformation.proxy.ResourceHandlerRequest; + +public class CreateHandlerTest { + + @Mock + private AmazonWebServicesClientProxy proxy; + + @Mock + private Logger logger; + + private CreateHandler handler; + + @BeforeEach + public void setup() { + MockitoAnnotations.initMocks(this); + handler = new CreateHandler(); + } + + @Test + public void handleRequest_HappyCase_VerifyRequestResponse() { + + ResourceModel model = buildResourceModel(); + + ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(model) + .logicalResourceIdentifier(LOGICAL_IDENTIFIER) + .clientRequestToken(CLIENT_REQUEST_TOKEN) + .desiredResourceTags(TAG_1_STRINGMAP) + .build(); + + CreateSecurityProfileResponse createResponse = CreateSecurityProfileResponse.builder() + .securityProfileArn(SECURITY_PROFILE_ARN) + .securityProfileName(SECURITY_PROFILE_NAME) + .build(); + + ArgumentCaptor requestsCaptor = ArgumentCaptor.forClass(IotRequest.class); + + when(proxy.injectCredentialsAndInvokeV2(requestsCaptor.capture(), any())) + .thenReturn(createResponse) + .thenReturn(AttachSecurityProfileResponse.builder().build()) + .thenReturn(AttachSecurityProfileResponse.builder().build()); + + ProgressEvent response + = handler.handleRequest(proxy, request, null, logger); + + List iotRequests = requestsCaptor.getAllValues(); + CreateSecurityProfileRequest actualCreateRequest = (CreateSecurityProfileRequest) iotRequests.get(0); + CreateSecurityProfileRequest expectedCreateRequest = CreateSecurityProfileRequest.builder() + .securityProfileName(SECURITY_PROFILE_NAME) + .additionalMetricsToRetainV2(ADDITIONAL_METRICS_IOT) + .tags(TAG_1_IOT) + .build(); + assertThat(actualCreateRequest).isEqualTo(expectedCreateRequest); + + AttachSecurityProfileRequest actualAttachRequest1 = (AttachSecurityProfileRequest) iotRequests.get(1); + assertThat(actualAttachRequest1.securityProfileName()).isEqualTo(SECURITY_PROFILE_NAME); + assertThat(actualAttachRequest1.securityProfileTargetArn()).isIn(TARGET_ARNS); + + AttachSecurityProfileRequest actualAttachRequest2 = (AttachSecurityProfileRequest) iotRequests.get(1); + assertThat(actualAttachRequest2.securityProfileName()).isEqualTo(SECURITY_PROFILE_NAME); + assertThat(actualAttachRequest2.securityProfileTargetArn()).isIn(TARGET_ARNS); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); + assertThat(response.getCallbackContext()).isNull(); + assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); + assertThat(response.getResourceModels()).isNull(); + assertThat(response.getMessage()).isNull(); + assertThat(response.getErrorCode()).isNull(); + + ResourceModel expectedModel = ResourceModel.builder() + .securityProfileName(SECURITY_PROFILE_NAME) + .additionalMetricsToRetainV2(ADDITIONAL_METRICS_CFN) + .targetArns(TARGET_ARNS) + .tags(TAG_1_CFN_SET) + .securityProfileArn(SECURITY_PROFILE_ARN) + .build(); + assertThat(response.getResourceModel()).isEqualTo(expectedModel); + } + + @Test + public void handleRequest_CreateThrowsAlreadyExists_VerifyTranslation() { + + ResourceModel model = buildResourceModel(); + + ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(model) + .logicalResourceIdentifier(LOGICAL_IDENTIFIER) + .clientRequestToken(CLIENT_REQUEST_TOKEN) + .desiredResourceTags(TAG_1_STRINGMAP) + .build(); + + when(proxy.injectCredentialsAndInvokeV2(any(), any())) + .thenThrow(ResourceAlreadyExistsException.builder().build()); + + assertThatThrownBy(() -> + handler.handleRequest(proxy, request, null, logger)) + .isInstanceOf(CfnAlreadyExistsException.class); + } + + @Test + public void handleRequest_AttachThrowsNotFound_VerifyTranslation() { + + ResourceModel model = buildResourceModel(); + + ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(model) + .logicalResourceIdentifier(LOGICAL_IDENTIFIER) + .clientRequestToken(CLIENT_REQUEST_TOKEN) + .desiredResourceTags(TAG_1_STRINGMAP) + .build(); + + CreateSecurityProfileResponse createResponse = CreateSecurityProfileResponse.builder() + .securityProfileArn(SECURITY_PROFILE_ARN) + .securityProfileName(SECURITY_PROFILE_NAME) + .build(); + + when(proxy.injectCredentialsAndInvokeV2(any(), any())) + .thenReturn(createResponse) + .thenThrow(ResourceNotFoundException.builder().build()); + + assertThatThrownBy(() -> + handler.handleRequest(proxy, request, null, logger)) + .isInstanceOf(CfnNotFoundException.class); + } + + @Test + public void handleRequest_NoName_GeneratedByHandler() { + + ResourceModel model = ResourceModel.builder() + .additionalMetricsToRetainV2(ADDITIONAL_METRICS_CFN) + .targetArns(TARGET_ARNS) + .tags(TAG_1_CFN_SET) + .build(); + + ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(model) + .logicalResourceIdentifier("MyResourceName") + .clientRequestToken("MyToken") + .desiredResourceTags(TAG_1_STRINGMAP) + .stackId("arn:aws:cloudformation:us-east-1:123456789012:stack/my-stack-name/" + + "084c0bd1-082b-11eb-afdc-0a2fadfa68a5") + .build(); + + when(proxy.injectCredentialsAndInvokeV2(any(), any())) + .thenReturn(CreateSecurityProfileResponse.builder().build()) + .thenReturn(AttachSecurityProfileResponse.builder().build()) + .thenReturn(AttachSecurityProfileResponse.builder().build()); + + handler.handleRequest(proxy, request, null, logger); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass( + IotRequest.class); + verify(proxy, times(3)).injectCredentialsAndInvokeV2(requestCaptor.capture(), any()); + + CreateSecurityProfileRequest actualCreateRequest = (CreateSecurityProfileRequest) requestCaptor + .getAllValues().get(0); + // Can't easily check the randomly generated name. Just making sure it contains part of + // the logical identifier and the stack name, and some more random characters + assertThat(actualCreateRequest.securityProfileName()).contains("my-stack"); + assertThat(actualCreateRequest.securityProfileName()).contains("MyRes"); + assertThat(actualCreateRequest.securityProfileName().length() > 20).isTrue(); + + AttachSecurityProfileRequest actualAttachRequest = (AttachSecurityProfileRequest) requestCaptor + .getAllValues().get(1); + assertThat(actualCreateRequest.securityProfileName()).isEqualTo(actualAttachRequest.securityProfileName()); + } + + @Test + public void handleRequest_NonEmptyArn_ExpectFailure() { + ResourceModel model = ResourceModel.builder() + .securityProfileName(SECURITY_PROFILE_NAME) + .additionalMetricsToRetainV2(ADDITIONAL_METRICS_CFN) + .targetArns(TARGET_ARNS) + .tags(TAG_1_CFN_SET) + .securityProfileArn("Arn is read-only") + .build(); + + ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(model) + .logicalResourceIdentifier(LOGICAL_IDENTIFIER) + .clientRequestToken(CLIENT_REQUEST_TOKEN) + .desiredResourceTags(TAG_1_STRINGMAP) + .build(); + + ProgressEvent response = + handler.handleRequest(proxy, request, null, logger); + + assertThat(response.getStatus()).isEqualTo(OperationStatus.FAILED); + assertThat(response.getErrorCode()).isEqualTo(HandlerErrorCode.InvalidRequest); + } + + private ResourceModel buildResourceModel() { + return ResourceModel.builder() + .securityProfileName(SECURITY_PROFILE_NAME) + .additionalMetricsToRetainV2(ADDITIONAL_METRICS_CFN) + .targetArns(TARGET_ARNS) + .tags(TAG_1_CFN_SET) + .build(); + } + + // TODO: test system tags when the src code is ready +} diff --git a/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/DeleteHandlerTest.java b/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/DeleteHandlerTest.java new file mode 100644 index 0000000..1f9fe3a --- /dev/null +++ b/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/DeleteHandlerTest.java @@ -0,0 +1,119 @@ +package com.amazonaws.iot.securityprofile; + +import static com.amazonaws.iot.securityprofile.TestConstants.SECURITY_PROFILE_NAME; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import software.amazon.awssdk.services.iot.model.DeleteSecurityProfileRequest; +import software.amazon.awssdk.services.iot.model.DescribeSecurityProfileRequest; +import software.amazon.awssdk.services.iot.model.DescribeSecurityProfileResponse; +import software.amazon.awssdk.services.iot.model.InternalFailureException; +import software.amazon.awssdk.services.iot.model.IotRequest; +import software.amazon.awssdk.services.iot.model.ResourceNotFoundException; +import software.amazon.cloudformation.exceptions.CfnInternalFailureException; +import software.amazon.cloudformation.exceptions.CfnNotFoundException; +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.Logger; +import software.amazon.cloudformation.proxy.OperationStatus; +import software.amazon.cloudformation.proxy.ProgressEvent; +import software.amazon.cloudformation.proxy.ResourceHandlerRequest; + +public class DeleteHandlerTest { + + @Mock + private AmazonWebServicesClientProxy proxy; + + @Mock + private Logger logger; + + private DeleteHandler handler; + + @BeforeEach + public void setup() { + MockitoAnnotations.initMocks(this); + handler = new DeleteHandler(); + } + + @Test + public void handleRequest_HappyCase_VerifyRequest() { + + ResourceModel model = ResourceModel.builder() + .securityProfileName(SECURITY_PROFILE_NAME) + .build(); + + ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(model) + .build(); + + ProgressEvent response + = handler.handleRequest(proxy, request, null, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); + assertThat(response.getCallbackContext()).isNull(); + assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); + assertThat(response.getResourceModel()).isNull(); + assertThat(response.getResourceModels()).isNull(); + assertThat(response.getMessage()).isNull(); + assertThat(response.getErrorCode()).isNull(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(IotRequest.class); + verify(proxy, times(2)).injectCredentialsAndInvokeV2(requestCaptor.capture(), any()); + + DescribeSecurityProfileRequest describeRequest = + (DescribeSecurityProfileRequest) requestCaptor.getAllValues().get(0); + assertThat(describeRequest.securityProfileName()).isEqualTo(SECURITY_PROFILE_NAME); + + DeleteSecurityProfileRequest deleteRequest = + (DeleteSecurityProfileRequest) requestCaptor.getAllValues().get(1); + assertThat(deleteRequest.securityProfileName()).isEqualTo(SECURITY_PROFILE_NAME); + } + + @Test + public void handleRequest_DescribeThrowsNotFound_VerifyTranslation() { + + ResourceModel model = ResourceModel.builder() + .securityProfileName(SECURITY_PROFILE_NAME) + .build(); + + ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(model) + .build(); + + when(proxy.injectCredentialsAndInvokeV2(any(), any())) + .thenThrow(ResourceNotFoundException.builder().build()); + + assertThatThrownBy(() -> + handler.handleRequest(proxy, request, null, logger)) + .isInstanceOf(CfnNotFoundException.class); + } + + @Test + public void handleRequest_DeleteThrowsException_VerifyTranslation() { + + ResourceModel model = ResourceModel.builder() + .securityProfileName(SECURITY_PROFILE_NAME) + .build(); + + ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(model) + .build(); + + when(proxy.injectCredentialsAndInvokeV2(any(), any())) + .thenReturn(DescribeSecurityProfileResponse.builder().build()) + .thenThrow(InternalFailureException.builder().build()); + + assertThatThrownBy(() -> + handler.handleRequest(proxy, request, null, logger)) + .isInstanceOf(CfnInternalFailureException.class); + } +} diff --git a/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/HandlerUtilsTest.java b/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/HandlerUtilsTest.java new file mode 100644 index 0000000..df5af78 --- /dev/null +++ b/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/HandlerUtilsTest.java @@ -0,0 +1,128 @@ +package com.amazonaws.iot.securityprofile; + +import static com.amazonaws.iot.securityprofile.TestConstants.SECURITY_PROFILE_ARN; +import static com.amazonaws.iot.securityprofile.TestConstants.SECURITY_PROFILE_NAME; +import static com.amazonaws.iot.securityprofile.TestConstants.SECURITY_PROFILE_TARGET_1; +import static com.amazonaws.iot.securityprofile.TestConstants.SECURITY_PROFILE_TARGET_2; +import static com.amazonaws.iot.securityprofile.TestConstants.TAGS_IOT; +import static com.amazonaws.iot.securityprofile.TestConstants.TAG_1_IOT; +import static com.amazonaws.iot.securityprofile.TestConstants.TAG_2_IOT; +import static com.amazonaws.iot.securityprofile.TestConstants.TARGET_ARNS; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +import java.util.Set; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import software.amazon.awssdk.services.iot.IotClient; +import software.amazon.awssdk.services.iot.model.LimitExceededException; +import software.amazon.awssdk.services.iot.model.ListTagsForResourceRequest; +import software.amazon.awssdk.services.iot.model.ListTagsForResourceResponse; +import software.amazon.awssdk.services.iot.model.ListTargetsForSecurityProfileRequest; +import software.amazon.awssdk.services.iot.model.ListTargetsForSecurityProfileResponse; +import software.amazon.awssdk.services.iot.model.ThrottlingException; +import software.amazon.cloudformation.exceptions.CfnServiceLimitExceededException; +import software.amazon.cloudformation.exceptions.CfnThrottlingException; +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.Logger; + +public class HandlerUtilsTest { + + private static final String NEXT_TOKEN = "testToken"; + + @Mock + private AmazonWebServicesClientProxy proxy; + + @Mock + private Logger logger; + + private IotClient iotClient; + + @BeforeEach + public void setup() { + MockitoAnnotations.initMocks(this); + iotClient = IotClient.builder().build(); + } + + @Test + void listTargetsForSecurityProfile_WithNextToken_VerifyPagination() { + + ListTargetsForSecurityProfileRequest expectedRequest1 = ListTargetsForSecurityProfileRequest.builder() + .securityProfileName(SECURITY_PROFILE_NAME) + .build(); + ListTargetsForSecurityProfileResponse response1 = ListTargetsForSecurityProfileResponse.builder() + .securityProfileTargets(SECURITY_PROFILE_TARGET_1) + .nextToken(NEXT_TOKEN) + .build(); + when(proxy.injectCredentialsAndInvokeV2(eq(expectedRequest1), any())) + .thenReturn(response1); + + ListTargetsForSecurityProfileRequest expectedRequest2 = ListTargetsForSecurityProfileRequest.builder() + .securityProfileName(SECURITY_PROFILE_NAME) + .nextToken(NEXT_TOKEN) + .build(); + ListTargetsForSecurityProfileResponse response2 = ListTargetsForSecurityProfileResponse.builder() + .securityProfileTargets(SECURITY_PROFILE_TARGET_2) + .build(); + when(proxy.injectCredentialsAndInvokeV2(eq(expectedRequest2), any())) + .thenReturn(response2); + + Set actualResponse = HandlerUtils.listTargetsForSecurityProfile( + iotClient, proxy, SECURITY_PROFILE_NAME); + assertThat(actualResponse).isEqualTo(TARGET_ARNS); + } + + @Test + void listTargetsForSecurityProfile_ApiThrowsException_VerifyTranslation() { + + when(proxy.injectCredentialsAndInvokeV2(any(), any())) + .thenThrow(ThrottlingException.builder().build()); + assertThatThrownBy(() -> HandlerUtils.listTargetsForSecurityProfile( + iotClient, proxy, SECURITY_PROFILE_NAME)) + .isInstanceOf(CfnThrottlingException.class); + } + + @Test + public void listTags_WithNextToken_VerifyPagination() { + + ListTagsForResourceRequest expectedRequest1 = ListTagsForResourceRequest.builder() + .resourceArn(SECURITY_PROFILE_ARN) + .build(); + ListTagsForResourceResponse response1 = ListTagsForResourceResponse.builder() + .tags(TAG_1_IOT) + .nextToken(NEXT_TOKEN) + .build(); + when(proxy.injectCredentialsAndInvokeV2(eq(expectedRequest1), any())) + .thenReturn(response1); + + ListTagsForResourceRequest expectedRequest2 = ListTagsForResourceRequest.builder() + .resourceArn(SECURITY_PROFILE_ARN) + .nextToken(NEXT_TOKEN) + .build(); + ListTagsForResourceResponse listTagsForResourceResponse2 = ListTagsForResourceResponse.builder() + .tags(TAG_2_IOT) + .build(); + when(proxy.injectCredentialsAndInvokeV2(eq(expectedRequest2), any())) + .thenReturn(listTagsForResourceResponse2); + + Set actualResponse = + HandlerUtils.listTags(iotClient, proxy, SECURITY_PROFILE_ARN); + assertThat(actualResponse).isEqualTo(TAGS_IOT); + } + + @Test + void listTags_ApiThrowsException_VerifyTranslation() { + + when(proxy.injectCredentialsAndInvokeV2(any(), any())) + .thenThrow(LimitExceededException.builder().build()); + assertThatThrownBy(() -> HandlerUtils.listTags( + iotClient, proxy, SECURITY_PROFILE_ARN)) + .isInstanceOf(CfnServiceLimitExceededException.class); + } +} diff --git a/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/ListHandlerTest.java b/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/ListHandlerTest.java new file mode 100644 index 0000000..327f1aa --- /dev/null +++ b/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/ListHandlerTest.java @@ -0,0 +1,102 @@ +package com.amazonaws.iot.securityprofile; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import software.amazon.awssdk.services.iot.model.ListSecurityProfilesRequest; +import software.amazon.awssdk.services.iot.model.ListSecurityProfilesResponse; +import software.amazon.awssdk.services.iot.model.UnauthorizedException; +import software.amazon.cloudformation.exceptions.CfnAccessDeniedException; +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.Logger; +import software.amazon.cloudformation.proxy.OperationStatus; +import software.amazon.cloudformation.proxy.ProgressEvent; +import software.amazon.cloudformation.proxy.ResourceHandlerRequest; + +public class ListHandlerTest { + + @Mock + private AmazonWebServicesClientProxy proxy; + + @Mock + private Logger logger; + + private ListHandler handler; + + @BeforeEach + public void setup() { + MockitoAnnotations.initMocks(this); + handler = new ListHandler(); + } + + @Test + public void handleRequest_HappyCase_VerifyRequestResponse() { + + ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .nextToken("nextToken1") + .build(); + + ListSecurityProfilesRequest expectedRequest = ListSecurityProfilesRequest.builder() + .nextToken(request.getNextToken()) + .build(); + + software.amazon.awssdk.services.iot.model.SecurityProfileIdentifier identifier1 = + software.amazon.awssdk.services.iot.model.SecurityProfileIdentifier.builder() + .arn("doesn't matter") + .name("profile1") + .build(); + software.amazon.awssdk.services.iot.model.SecurityProfileIdentifier identifier2 = + software.amazon.awssdk.services.iot.model.SecurityProfileIdentifier.builder() + .arn("doesn't matter") + .name("profile2") + .build(); + ListSecurityProfilesResponse listResponse = ListSecurityProfilesResponse.builder() + .securityProfileIdentifiers(identifier1, identifier2) + .nextToken("nextToken2") + .build(); + + when(proxy.injectCredentialsAndInvokeV2(eq(expectedRequest), any())) + .thenReturn(listResponse); + + ProgressEvent response = + handler.handleRequest(proxy, request, null, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); + assertThat(response.getCallbackContext()).isNull(); + assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); + assertThat(response.getResourceModel()).isNull(); + assertThat(response.getMessage()).isNull(); + assertThat(response.getErrorCode()).isNull(); + + assertThat(response.getNextToken()).isEqualTo("nextToken2"); + List expectedModels = Arrays.asList( + ResourceModel.builder().securityProfileName("profile1").build(), + ResourceModel.builder().securityProfileName("profile2").build()); + assertThat(response.getResourceModels()).isEqualTo(expectedModels); + } + + @Test + public void handleRequest_ApiThrowsException_VerifyTranslation() { + + ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .nextToken("nextToken1") + .build(); + + when(proxy.injectCredentialsAndInvokeV2(any(), any())) + .thenThrow(UnauthorizedException.builder().build()); + + assertThatThrownBy(() -> handler.handleRequest(proxy, request, null, logger)) + .isInstanceOf(CfnAccessDeniedException.class); + } +} diff --git a/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/ReadHandlerTest.java b/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/ReadHandlerTest.java new file mode 100644 index 0000000..957b7ce --- /dev/null +++ b/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/ReadHandlerTest.java @@ -0,0 +1,166 @@ +package com.amazonaws.iot.securityprofile; + +import static com.amazonaws.iot.securityprofile.TestConstants.ADDITIONAL_METRICS_CFN; +import static com.amazonaws.iot.securityprofile.TestConstants.ADDITIONAL_METRICS_IOT; +import static com.amazonaws.iot.securityprofile.TestConstants.ADDITIONAL_METRICS_V1_SET; +import static com.amazonaws.iot.securityprofile.TestConstants.ALERT_TARGET_MAP_CFN; +import static com.amazonaws.iot.securityprofile.TestConstants.ALERT_TARGET_MAP_IOT; +import static com.amazonaws.iot.securityprofile.TestConstants.BEHAVIOR_1_CFN_SET; +import static com.amazonaws.iot.securityprofile.TestConstants.BEHAVIOR_1_IOT; +import static com.amazonaws.iot.securityprofile.TestConstants.SECURITY_PROFILE_ARN; +import static com.amazonaws.iot.securityprofile.TestConstants.SECURITY_PROFILE_DESCRIPTION; +import static com.amazonaws.iot.securityprofile.TestConstants.SECURITY_PROFILE_NAME; +import static com.amazonaws.iot.securityprofile.TestConstants.TAG_1_CFN_SET; +import static com.amazonaws.iot.securityprofile.TestConstants.TAG_1_IOT_SET; +import static com.amazonaws.iot.securityprofile.TestConstants.TARGET_ARN_1_SET; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.Spy; +import software.amazon.awssdk.services.iot.model.DescribeSecurityProfileRequest; +import software.amazon.awssdk.services.iot.model.DescribeSecurityProfileResponse; +import software.amazon.awssdk.services.iot.model.ThrottlingException; +import software.amazon.cloudformation.exceptions.CfnThrottlingException; +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.Logger; +import software.amazon.cloudformation.proxy.OperationStatus; +import software.amazon.cloudformation.proxy.ProgressEvent; +import software.amazon.cloudformation.proxy.ResourceHandlerRequest; + +public class ReadHandlerTest { + + @Mock + private AmazonWebServicesClientProxy proxy; + + @Mock + private Logger logger; + + @Spy + private ReadHandler handler; + + @BeforeEach + public void setup() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void handleRequest_AllFieldsPopulated_VerifyRequestResponse() { + + ResourceModel model = ResourceModel.builder() + .securityProfileName(SECURITY_PROFILE_NAME) + .build(); + ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(model) + .build(); + + DescribeSecurityProfileRequest expectedDescribeRequest = DescribeSecurityProfileRequest.builder() + .securityProfileName(SECURITY_PROFILE_NAME) + .build(); + DescribeSecurityProfileResponse describeResponse = DescribeSecurityProfileResponse.builder() + .securityProfileName(SECURITY_PROFILE_NAME) + .securityProfileArn(SECURITY_PROFILE_ARN) + .securityProfileDescription(SECURITY_PROFILE_DESCRIPTION) + .behaviors(BEHAVIOR_1_IOT) + .alertTargetsWithStrings(ALERT_TARGET_MAP_IOT) + .additionalMetricsToRetain(ADDITIONAL_METRICS_V1_SET) + .additionalMetricsToRetainV2(ADDITIONAL_METRICS_IOT) + .build(); + when(proxy.injectCredentialsAndInvokeV2(eq(expectedDescribeRequest), any())) + .thenReturn(describeResponse); + + doReturn(TARGET_ARN_1_SET) + .when(handler) + .listTargetsForSecurityProfile(proxy, SECURITY_PROFILE_NAME); + + doReturn(TAG_1_IOT_SET) + .when(handler) + .listTags(proxy, SECURITY_PROFILE_ARN); + + ProgressEvent response + = handler.handleRequest(proxy, request, null, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); + assertThat(response.getCallbackContext()).isNull(); + assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); + assertThat(response.getResourceModels()).isNull(); + assertThat(response.getMessage()).isNull(); + assertThat(response.getErrorCode()).isNull(); + + ResourceModel expectedModel = ResourceModel.builder() + .securityProfileName(SECURITY_PROFILE_NAME) + .securityProfileArn(SECURITY_PROFILE_ARN) + .securityProfileDescription(SECURITY_PROFILE_DESCRIPTION) + .behaviors(BEHAVIOR_1_CFN_SET) + .alertTargets(ALERT_TARGET_MAP_CFN) + .additionalMetricsToRetain(ADDITIONAL_METRICS_V1_SET) + .additionalMetricsToRetainV2(ADDITIONAL_METRICS_CFN) + .targetArns(TARGET_ARN_1_SET) + .tags(TAG_1_CFN_SET) + .build(); + assertThat(response.getResourceModel()).isEqualTo(expectedModel); + } + + @Test + public void buildResourceModel_NullCollections_StayNull() { + + Set behaviors = null; + Map alertTargetMap = null; + List additionalMetricsV1 = null; + List additionalMetricsV2 = null; + + DescribeSecurityProfileResponse describeResponse = DescribeSecurityProfileResponse.builder() + .securityProfileName(SECURITY_PROFILE_NAME) + .securityProfileArn(SECURITY_PROFILE_ARN) + .securityProfileDescription(SECURITY_PROFILE_DESCRIPTION) + .behaviors(behaviors) + .alertTargetsWithStrings(alertTargetMap) + .additionalMetricsToRetain(additionalMetricsV1) + .additionalMetricsToRetainV2(additionalMetricsV2) + .build(); + + ResourceModel actualModel = handler.buildResourceModel(describeResponse, TARGET_ARN_1_SET, TAG_1_IOT_SET); + + ResourceModel expectedModel = ResourceModel.builder() + .securityProfileName(SECURITY_PROFILE_NAME) + .securityProfileArn(SECURITY_PROFILE_ARN) + .securityProfileDescription(SECURITY_PROFILE_DESCRIPTION) + .behaviors(null) + .alertTargets(null) + .additionalMetricsToRetain(null) + .additionalMetricsToRetainV2(null) + .targetArns(TARGET_ARN_1_SET) + .tags(TAG_1_CFN_SET) + .build(); + assertThat(actualModel).isEqualTo(expectedModel); + } + + @Test + public void handleRequest_DescribeThrowsException_VerifyTranslation() { + + ResourceModel model = ResourceModel.builder() + .securityProfileName(SECURITY_PROFILE_NAME) + .build(); + ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(model) + .build(); + + when(proxy.injectCredentialsAndInvokeV2(any(), any())) + .thenThrow(ThrottlingException.builder().build()); + + assertThatThrownBy(() -> handler.handleRequest(proxy, request, null, logger)) + .isInstanceOf(CfnThrottlingException.class); + } +} diff --git a/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/TestConstants.java b/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/TestConstants.java new file mode 100644 index 0000000..0987ee5 --- /dev/null +++ b/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/TestConstants.java @@ -0,0 +1,121 @@ +package com.amazonaws.iot.securityprofile; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; + +import software.amazon.awssdk.services.iot.model.ComparisonOperator; + +public class TestConstants { + + static final String SECURITY_PROFILE_NAME = "TestSecurityProfile"; + static final String SECURITY_PROFILE_ARN = + "arn:aws:iot:us-east-1:123456789012:dimension/TestSecurityProfile"; + static final String SECURITY_PROFILE_DESCRIPTION = "TestDescription"; + static final String CLIENT_REQUEST_TOKEN = "TestToken"; + static final String LOGICAL_IDENTIFIER = "LogicalIdentifier"; + static final String BEHAVIOR_NAME = "testBehavior"; + static final MetricDimension DIMENSION_CFN = MetricDimension.builder() + .dimensionName("TestDimension") + .operator("NOT_IN") + .build(); + static final MetricToRetain METRIC_TO_RETAIN_CFN = MetricToRetain.builder() + .metric("aws:num-messages-sent") + .metricDimension(DIMENSION_CFN) + .build(); + static final software.amazon.awssdk.services.iot.model.MetricDimension DIMENSION_IOT = + software.amazon.awssdk.services.iot.model.MetricDimension.builder() + .dimensionName("TestDimension") + .operator("NOT_IN") + .build(); + static final software.amazon.awssdk.services.iot.model.MetricToRetain METRIC_TO_RETAIN_IOT = + software.amazon.awssdk.services.iot.model.MetricToRetain.builder() + .metric("aws:num-messages-sent") + .metricDimension(DIMENSION_IOT) + .build(); + static final Set ADDITIONAL_METRICS_CFN = ImmutableSet.of(METRIC_TO_RETAIN_CFN); + static final List ADDITIONAL_METRICS_IOT = + ImmutableList.of(METRIC_TO_RETAIN_IOT); + static final String TARGET_ARN_1 = "arn:aws:iot:us-west-2:123456789012:all/unregistered-things"; + static final String TARGET_ARN_2 = "arn:aws:iot:us-west-2:123456789012:all/registered-things"; + static final Set TARGET_ARN_1_SET = ImmutableSet.of(TARGET_ARN_1); + static final Set TARGET_ARN_2_SET = ImmutableSet.of(TARGET_ARN_2); + static final Set TARGET_ARNS = ImmutableSet.of(TARGET_ARN_1, TARGET_ARN_2); + static final String TAG_1_KEY = "TagKey1"; + static final List TAG_1_KEY_LIST = ImmutableList.of(TAG_1_KEY); + static final Set TAG_1_CFN_SET = ImmutableSet.of( + Tag.builder() + .key(TAG_1_KEY) + .value("TagValue1") + .build()); + static final Map TAG_1_STRINGMAP = ImmutableMap.of( + TAG_1_KEY, "TagValue1"); + static final Map TAG_2_STRINGMAP = ImmutableMap.of( + "TagKey2", "TagValue2"); + static final software.amazon.awssdk.services.iot.model.Tag TAG_1_IOT = + software.amazon.awssdk.services.iot.model.Tag.builder() + .key(TAG_1_KEY) + .value("TagValue1") + .build(); + static final software.amazon.awssdk.services.iot.model.Tag TAG_2_IOT = + software.amazon.awssdk.services.iot.model.Tag.builder() + .key("TagKey2") + .value("TagValue2") + .build(); + static final Set TAG_1_IOT_SET = ImmutableSet.of(TAG_1_IOT); + static final List TAG_2_IOT_LIST = ImmutableList.of(TAG_2_IOT); + static final Set TAGS_IOT = + ImmutableSet.of(TAG_1_IOT, TAG_2_IOT); + static final software.amazon.awssdk.services.iot.model.SecurityProfileTarget SECURITY_PROFILE_TARGET_1 = + software.amazon.awssdk.services.iot.model.SecurityProfileTarget.builder() + .arn(TARGET_ARN_1) + .build(); + static final software.amazon.awssdk.services.iot.model.SecurityProfileTarget SECURITY_PROFILE_TARGET_2 = + software.amazon.awssdk.services.iot.model.SecurityProfileTarget.builder() + .arn(TARGET_ARN_2) + .build(); + static final software.amazon.awssdk.services.iot.model.BehaviorCriteria CRITERIA_1_IOT = + software.amazon.awssdk.services.iot.model.BehaviorCriteria.builder() + .comparisonOperator(ComparisonOperator.GREATER_THAN) + .build(); + static final software.amazon.awssdk.services.iot.model.Behavior BEHAVIOR_1_IOT = + software.amazon.awssdk.services.iot.model.Behavior.builder() + .name(BEHAVIOR_NAME) + .metric("aws:message-byte-size") + .metricDimension(DIMENSION_IOT) + .criteria(CRITERIA_1_IOT) + .build(); + static final List BEHAVIOR_1_IOT_LIST = + ImmutableList.of(BEHAVIOR_1_IOT); + static final BehaviorCriteria CRITERIA_1_CFN = BehaviorCriteria.builder() + .comparisonOperator("greater-than") + .build(); + static final Behavior BEHAVIOR_1_CFN = Behavior.builder() + .name(BEHAVIOR_NAME) + .metric("aws:message-byte-size") + .metricDimension(DIMENSION_CFN) + .criteria(CRITERIA_1_CFN) + .build(); + static final Set BEHAVIOR_1_CFN_SET = ImmutableSet.of(BEHAVIOR_1_CFN); + static final software.amazon.awssdk.services.iot.model.AlertTarget ALERT_TARGET_IOT = + software.amazon.awssdk.services.iot.model.AlertTarget.builder() + .alertTargetArn("testAlertTargetArn") + .roleArn("testRoleArn") + .build(); + static final AlertTarget ALERT_TARGET_CFN = AlertTarget.builder() + .alertTargetArn("testAlertTargetArn") + .roleArn("testRoleArn") + .build(); + static final Map ALERT_TARGET_MAP_IOT = + ImmutableMap.of("SNS", ALERT_TARGET_IOT); + static final Map ALERT_TARGET_MAP_CFN = + ImmutableMap.of("SNS", ALERT_TARGET_CFN); + static final Set ADDITIONAL_METRICS_V1_SET = + ImmutableSet.of("aws:listening-tcp-ports", "aws:num-listening-udp-ports"); + static final List ADDITIONAL_METRICS_V1_LIST = + ImmutableList.of("aws:listening-tcp-ports", "aws:num-listening-udp-ports"); +} diff --git a/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/TranslatorTest.java b/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/TranslatorTest.java new file mode 100644 index 0000000..dc74915 --- /dev/null +++ b/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/TranslatorTest.java @@ -0,0 +1,185 @@ +package com.amazonaws.iot.securityprofile; + +import static com.amazonaws.iot.securityprofile.TestConstants.BEHAVIOR_NAME; +import static com.amazonaws.iot.securityprofile.Translator.translateBehaviorListFromIotToCfn; +import static com.amazonaws.iot.securityprofile.Translator.translateBehaviorSetFromCfnToIot; +import static com.amazonaws.iot.securityprofile.Translator.translateMetricValueFromCfnToIot; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.ArrayList; +import java.util.Set; + +import com.google.common.collect.ImmutableSet; + +import org.junit.jupiter.api.Test; +import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; + +public class TranslatorTest { + + @Test + void translateBehaviors_SeveralLegalBehaviors_VerifyRoundTripTranslation() { + + Behavior scalarMetricBehaviorCfn = buildScalarMetricBehaviorCfn(); + software.amazon.awssdk.services.iot.model.Behavior scalarMetricBehaviorIot = buildScalarMetricBehaviorIot(); + + Behavior sourceIpBehaviorCfn = buildSourceIpBehaviorCfn(); + software.amazon.awssdk.services.iot.model.Behavior sourceIpBehaviorIot = buildSourceIpBehaviorIot(); + + Behavior listeningPortsBehaviorCfn = buildListeningPortsBehaviorCfn(); + software.amazon.awssdk.services.iot.model.Behavior listeningPortsBehaviorIot = + buildListeningPortsBehaviorIot(); + + Behavior statisticalThresholdBehaviorCfn = buildStatisticalThresholdBehaviorCfn(); + software.amazon.awssdk.services.iot.model.Behavior statisticalThresholdBehaviorIot = + buildStatisticalThresholdBehaviorIot(); + + Set cfnBehaviors = ImmutableSet.of( + sourceIpBehaviorCfn, listeningPortsBehaviorCfn, statisticalThresholdBehaviorCfn, + scalarMetricBehaviorCfn); + Set iotBehaviors = ImmutableSet.of( + sourceIpBehaviorIot, listeningPortsBehaviorIot, statisticalThresholdBehaviorIot, + scalarMetricBehaviorIot); + + Set actualIotBehaviors = + translateBehaviorSetFromCfnToIot(cfnBehaviors); + assertThat(actualIotBehaviors).isEqualTo(iotBehaviors); + + Set actualCfnBehaviors = translateBehaviorListFromIotToCfn(new ArrayList<>(iotBehaviors)); + assertThat(actualCfnBehaviors).isEqualTo(cfnBehaviors); + } + + private Behavior buildSourceIpBehaviorCfn() { + BehaviorCriteria behaviorCriteria = BehaviorCriteria.builder() + .comparisonOperator("in-cidr-set") + .value(MetricValue.builder() + .cidrs(ImmutableSet.of( + "192.168.100.14/24", + "192.168.101.14/24", + "192.168.102.14/24")) + .build()) + .consecutiveDatapointsToAlarm(5) + .build(); + return Behavior.builder() + .name(BEHAVIOR_NAME) + .metric("aws:source-ip-address") + .criteria(behaviorCriteria) + .build(); + } + + private software.amazon.awssdk.services.iot.model.Behavior buildSourceIpBehaviorIot() { + software.amazon.awssdk.services.iot.model.BehaviorCriteria behaviorCriteria = + software.amazon.awssdk.services.iot.model.BehaviorCriteria.builder() + .comparisonOperator("in-cidr-set") + .value(software.amazon.awssdk.services.iot.model.MetricValue.builder() + .cidrs(ImmutableSet.of( + "192.168.100.14/24", + "192.168.101.14/24", + "192.168.102.14/24")) + .build()) + .consecutiveDatapointsToAlarm(5) + .build(); + return software.amazon.awssdk.services.iot.model.Behavior.builder() + .name(BEHAVIOR_NAME) + .metric("aws:source-ip-address") + .criteria(behaviorCriteria) + .build(); + } + + private Behavior buildListeningPortsBehaviorCfn() { + BehaviorCriteria behaviorCriteria = BehaviorCriteria.builder() + .comparisonOperator("in-port-set") + .value(MetricValue.builder() + .ports(ImmutableSet.of(40, 443)) + .build()) + .consecutiveDatapointsToClear(5) + .build(); + return Behavior.builder() + .name(BEHAVIOR_NAME) + .metric("aws:listening-tcp-ports") + .criteria(behaviorCriteria) + .build(); + } + + private software.amazon.awssdk.services.iot.model.Behavior buildListeningPortsBehaviorIot() { + software.amazon.awssdk.services.iot.model.BehaviorCriteria behaviorCriteria = + software.amazon.awssdk.services.iot.model.BehaviorCriteria.builder() + .comparisonOperator("in-port-set") + .value(software.amazon.awssdk.services.iot.model.MetricValue.builder() + .ports(ImmutableSet.of(40, 443)) + .build()) + .consecutiveDatapointsToClear(5) + .build(); + return software.amazon.awssdk.services.iot.model.Behavior.builder() + .name(BEHAVIOR_NAME) + .metric("aws:listening-tcp-ports") + .criteria(behaviorCriteria) + .build(); + } + + private Behavior buildStatisticalThresholdBehaviorCfn() { + BehaviorCriteria behaviorCriteria = BehaviorCriteria.builder() + .comparisonOperator("greater-than") + .durationSeconds(300) + .statisticalThreshold(StatisticalThreshold.builder().statistic("p90").build()) + .build(); + return Behavior.builder() + .name(BEHAVIOR_NAME) + .metric("aws:num-authorization-failures") + .criteria(behaviorCriteria) + .build(); + } + + private software.amazon.awssdk.services.iot.model.Behavior buildStatisticalThresholdBehaviorIot() { + software.amazon.awssdk.services.iot.model.BehaviorCriteria behaviorCriteria = + software.amazon.awssdk.services.iot.model.BehaviorCriteria.builder() + .comparisonOperator("greater-than") + .durationSeconds(300) + .statisticalThreshold(software.amazon.awssdk.services.iot.model.StatisticalThreshold + .builder().statistic("p90").build()) + .build(); + return software.amazon.awssdk.services.iot.model.Behavior.builder() + .name(BEHAVIOR_NAME) + .metric("aws:num-authorization-failures") + .criteria(behaviorCriteria) + .build(); + } + + private Behavior buildScalarMetricBehaviorCfn() { + BehaviorCriteria behaviorCriteria = BehaviorCriteria.builder() + .comparisonOperator("less-than") + .durationSeconds(600) + .value(MetricValue + .builder().count("999999999999").build()) + .build(); + return Behavior.builder() + .name(BEHAVIOR_NAME) + .metric("aws:num-messages-sent") + .criteria(behaviorCriteria) + .build(); + } + + private software.amazon.awssdk.services.iot.model.Behavior buildScalarMetricBehaviorIot() { + software.amazon.awssdk.services.iot.model.BehaviorCriteria behaviorCriteria = + software.amazon.awssdk.services.iot.model.BehaviorCriteria.builder() + .comparisonOperator("less-than") + .durationSeconds(600) + .value(software.amazon.awssdk.services.iot.model.MetricValue + .builder().count(999999999999L).build()) + .build(); + return software.amazon.awssdk.services.iot.model.Behavior.builder() + .name(BEHAVIOR_NAME) + .metric("aws:num-messages-sent") + .criteria(behaviorCriteria) + .build(); + } + + @Test + void translateMetricValueFromCfnToIot_ScalarValue_NotANumber() { + MetricValue cfnMetricValue = MetricValue.builder() + .count("123IllegalInput") + .build(); + assertThatThrownBy(() -> translateMetricValueFromCfnToIot(cfnMetricValue)) + .isInstanceOf(CfnInvalidRequestException.class); + } +} diff --git a/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/UpdateHandlerTest.java b/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/UpdateHandlerTest.java new file mode 100644 index 0000000..1b29c28 --- /dev/null +++ b/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/UpdateHandlerTest.java @@ -0,0 +1,391 @@ +package com.amazonaws.iot.securityprofile; + +import static com.amazonaws.iot.securityprofile.TestConstants.ADDITIONAL_METRICS_CFN; +import static com.amazonaws.iot.securityprofile.TestConstants.ADDITIONAL_METRICS_IOT; +import static com.amazonaws.iot.securityprofile.TestConstants.ADDITIONAL_METRICS_V1_LIST; +import static com.amazonaws.iot.securityprofile.TestConstants.ADDITIONAL_METRICS_V1_SET; +import static com.amazonaws.iot.securityprofile.TestConstants.ALERT_TARGET_MAP_CFN; +import static com.amazonaws.iot.securityprofile.TestConstants.ALERT_TARGET_MAP_IOT; +import static com.amazonaws.iot.securityprofile.TestConstants.BEHAVIOR_1_CFN_SET; +import static com.amazonaws.iot.securityprofile.TestConstants.BEHAVIOR_1_IOT_LIST; +import static com.amazonaws.iot.securityprofile.TestConstants.SECURITY_PROFILE_ARN; +import static com.amazonaws.iot.securityprofile.TestConstants.SECURITY_PROFILE_DESCRIPTION; +import static com.amazonaws.iot.securityprofile.TestConstants.SECURITY_PROFILE_NAME; +import static com.amazonaws.iot.securityprofile.TestConstants.TAG_1_IOT_SET; +import static com.amazonaws.iot.securityprofile.TestConstants.TAG_1_KEY; +import static com.amazonaws.iot.securityprofile.TestConstants.TAG_1_KEY_LIST; +import static com.amazonaws.iot.securityprofile.TestConstants.TAG_1_STRINGMAP; +import static com.amazonaws.iot.securityprofile.TestConstants.TAG_2_IOT_LIST; +import static com.amazonaws.iot.securityprofile.TestConstants.TAG_2_STRINGMAP; +import static com.amazonaws.iot.securityprofile.TestConstants.TARGET_ARN_1; +import static com.amazonaws.iot.securityprofile.TestConstants.TARGET_ARN_1_SET; +import static com.amazonaws.iot.securityprofile.TestConstants.TARGET_ARN_2; +import static com.amazonaws.iot.securityprofile.TestConstants.TARGET_ARN_2_SET; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.Spy; +import software.amazon.awssdk.services.iot.model.AttachSecurityProfileRequest; +import software.amazon.awssdk.services.iot.model.DetachSecurityProfileRequest; +import software.amazon.awssdk.services.iot.model.InternalFailureException; +import software.amazon.awssdk.services.iot.model.InvalidRequestException; +import software.amazon.awssdk.services.iot.model.IotRequest; +import software.amazon.awssdk.services.iot.model.ResourceNotFoundException; +import software.amazon.awssdk.services.iot.model.TagResourceRequest; +import software.amazon.awssdk.services.iot.model.UntagResourceRequest; +import software.amazon.awssdk.services.iot.model.UpdateSecurityProfileRequest; +import software.amazon.awssdk.services.iot.model.UpdateSecurityProfileResponse; +import software.amazon.cloudformation.exceptions.CfnInternalFailureException; +import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; +import software.amazon.cloudformation.exceptions.CfnNotFoundException; +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.HandlerErrorCode; +import software.amazon.cloudformation.proxy.Logger; +import software.amazon.cloudformation.proxy.OperationStatus; +import software.amazon.cloudformation.proxy.ProgressEvent; +import software.amazon.cloudformation.proxy.ResourceHandlerRequest; + +public class UpdateHandlerTest { + + @Mock + private AmazonWebServicesClientProxy proxy; + + @Mock + private Logger logger; + + @Spy + private UpdateHandler handler; + + @BeforeEach + public void setup() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void handleRequest_AllFieldsUpdated_VerifyRequests() { + + ResourceModel previousModel = ResourceModel.builder() + .securityProfileName(SECURITY_PROFILE_NAME) + .securityProfileArn(SECURITY_PROFILE_ARN) + .build(); + ResourceModel desiredModel = ResourceModel.builder() + .securityProfileName(SECURITY_PROFILE_NAME) + .securityProfileDescription(SECURITY_PROFILE_DESCRIPTION) + .behaviors(BEHAVIOR_1_CFN_SET) + .alertTargets(ALERT_TARGET_MAP_CFN) + .additionalMetricsToRetain(ADDITIONAL_METRICS_V1_SET) + .additionalMetricsToRetainV2(ADDITIONAL_METRICS_CFN) + .targetArns(TARGET_ARN_2_SET) + .build(); + + ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .previousResourceState(previousModel) + .previousResourceTags(ImmutableMap.of("doesn't", "matter")) + .desiredResourceState(desiredModel) + .desiredResourceTags(TAG_2_STRINGMAP) + .build(); + + doReturn(TARGET_ARN_1_SET) + .when(handler) + .listTargetsForSecurityProfile(proxy, SECURITY_PROFILE_NAME); + doReturn(TAG_1_IOT_SET) + .when(handler) + .listTags(proxy, SECURITY_PROFILE_ARN); + + when(proxy.injectCredentialsAndInvokeV2(any(), any())) + .thenReturn(UpdateSecurityProfileResponse.builder().securityProfileArn(SECURITY_PROFILE_ARN).build()); + + ProgressEvent response + = handler.handleRequest(proxy, request, null, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); + assertThat(response.getCallbackContext()).isNull(); + assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); + assertThat(response.getResourceModel()).isEqualTo(request.getDesiredResourceState()); + assertThat(response.getResourceModels()).isNull(); + assertThat(response.getMessage()).isNull(); + assertThat(response.getErrorCode()).isNull(); + + desiredModel.setSecurityProfileArn(SECURITY_PROFILE_ARN); + assertThat(response.getResourceModel()).isEqualTo(desiredModel); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(IotRequest.class); + verify(proxy, times(5)).injectCredentialsAndInvokeV2(requestCaptor.capture(), any()); + List submittedIotRequests = requestCaptor.getAllValues(); + + UpdateSecurityProfileRequest submittedUpdateRequest = + (UpdateSecurityProfileRequest) submittedIotRequests.get(0); + assertThat(submittedUpdateRequest.securityProfileName()).isEqualTo(SECURITY_PROFILE_NAME); + assertThat(submittedUpdateRequest.securityProfileDescription()).isEqualTo(SECURITY_PROFILE_DESCRIPTION); + assertThat(submittedUpdateRequest.behaviors()).isEqualTo(BEHAVIOR_1_IOT_LIST); + assertThat(submittedUpdateRequest.alertTargetsAsStrings()).isEqualTo(ALERT_TARGET_MAP_IOT); + assertThat(submittedUpdateRequest.additionalMetricsToRetain()).isEqualTo(ADDITIONAL_METRICS_V1_LIST); + assertThat(submittedUpdateRequest.additionalMetricsToRetainV2()).isEqualTo(ADDITIONAL_METRICS_IOT); + assertThat(submittedUpdateRequest.deleteBehaviors()).isFalse(); + assertThat(submittedUpdateRequest.deleteAlertTargets()).isFalse(); + assertThat(submittedUpdateRequest.deleteAdditionalMetricsToRetain()).isFalse(); + + AttachSecurityProfileRequest submittedAttachRequest = + (AttachSecurityProfileRequest) submittedIotRequests.get(1); + assertThat(submittedAttachRequest.securityProfileName()).isEqualTo(SECURITY_PROFILE_NAME); + assertThat(submittedAttachRequest.securityProfileTargetArn()).isEqualTo(TARGET_ARN_2); + + DetachSecurityProfileRequest submittedDetachRequest = + (DetachSecurityProfileRequest) submittedIotRequests.get(2); + assertThat(submittedDetachRequest.securityProfileName()).isEqualTo(SECURITY_PROFILE_NAME); + assertThat(submittedDetachRequest.securityProfileTargetArn()).isEqualTo(TARGET_ARN_1); + + TagResourceRequest submittedTagRequest = (TagResourceRequest) submittedIotRequests.get(3); + assertThat(submittedTagRequest.tags()).isEqualTo(TAG_2_IOT_LIST); + assertThat(submittedTagRequest.resourceArn()).isEqualTo(SECURITY_PROFILE_ARN); + + UntagResourceRequest submittedUntagRequest = (UntagResourceRequest) submittedIotRequests.get(4); + assertThat(submittedUntagRequest.tagKeys()).isEqualTo(TAG_1_KEY_LIST); + assertThat(submittedUntagRequest.resourceArn()).isEqualTo(SECURITY_PROFILE_ARN); + } + + @Test + public void updateSecurityProfile_NullCollections_VerifyDeleteFlags() { + + UpdateSecurityProfileRequest expectedUpdateRequest = UpdateSecurityProfileRequest.builder() + .securityProfileName(SECURITY_PROFILE_NAME) + .securityProfileDescription(SECURITY_PROFILE_DESCRIPTION) + .deleteBehaviors(true) + .deleteAlertTargets(true) + .deleteAdditionalMetricsToRetain(true) + .build(); + + when(proxy.injectCredentialsAndInvokeV2(eq(expectedUpdateRequest), any())) + .thenReturn(UpdateSecurityProfileResponse.builder().securityProfileArn(SECURITY_PROFILE_ARN).build()); + + ResourceModel modelWithNullCollections = ResourceModel.builder() + .securityProfileName(SECURITY_PROFILE_NAME) + .securityProfileDescription(SECURITY_PROFILE_DESCRIPTION) + .targetArns(TARGET_ARN_2_SET) + .build(); + + String actualArn = handler.updateSecurityProfile(proxy, modelWithNullCollections, logger); + assertThat(actualArn).isEqualTo(SECURITY_PROFILE_ARN); + } + + @Test + public void updateSecurityProfile_EmptyCollections_VerifyDeleteFlags() { + + UpdateSecurityProfileRequest expectedUpdateRequest = UpdateSecurityProfileRequest.builder() + .securityProfileName(SECURITY_PROFILE_NAME) + .securityProfileDescription(SECURITY_PROFILE_DESCRIPTION) + .deleteBehaviors(true) + .deleteAlertTargets(true) + .deleteAdditionalMetricsToRetain(true) + .build(); + + when(proxy.injectCredentialsAndInvokeV2(eq(expectedUpdateRequest), any())) + .thenReturn(UpdateSecurityProfileResponse.builder().securityProfileArn(SECURITY_PROFILE_ARN).build()); + + ResourceModel modelWithEmptyCollections = ResourceModel.builder() + .securityProfileName(SECURITY_PROFILE_NAME) + .securityProfileDescription(SECURITY_PROFILE_DESCRIPTION) + .targetArns(TARGET_ARN_2_SET) + .behaviors(Collections.emptySet()) + .alertTargets(Collections.emptyMap()) + .additionalMetricsToRetain(Collections.emptySet()) + .additionalMetricsToRetainV2(Collections.emptySet()) + .build(); + + String actualArn = handler.updateSecurityProfile(proxy, modelWithEmptyCollections, logger); + assertThat(actualArn).isEqualTo(SECURITY_PROFILE_ARN); + } + + @Test + public void updateTargetAttachments_3Before3After_2Attach2Detach1Keep() { + + Set previousTargets = ImmutableSet.of("keepTarget", "detachTarget1", "detachTarget2"); + doReturn(previousTargets) + .when(handler) + .listTargetsForSecurityProfile(proxy, SECURITY_PROFILE_NAME); + + Set desiredTargets = ImmutableSet.of("keepTarget", "attachTarget1", "attachTarget2"); + ResourceModel model = ResourceModel.builder() + .securityProfileName(SECURITY_PROFILE_NAME) + .targetArns(desiredTargets) + .build(); + + handler.updateTargetAttachments(proxy, model, logger); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(IotRequest.class); + verify(proxy, times(4)).injectCredentialsAndInvokeV2(requestCaptor.capture(), any()); + + AttachSecurityProfileRequest actualAttachRequest1 = + (AttachSecurityProfileRequest) requestCaptor.getAllValues().get(0); + AttachSecurityProfileRequest actualAttachRequest2 = + (AttachSecurityProfileRequest) requestCaptor.getAllValues().get(1); + Set actualAttachTargets = ImmutableSet.of(actualAttachRequest1.securityProfileTargetArn(), + actualAttachRequest2.securityProfileTargetArn()); + assertThat(actualAttachTargets).containsExactlyInAnyOrder("attachTarget1", "attachTarget2"); + + DetachSecurityProfileRequest actualDetachRequest1 = + (DetachSecurityProfileRequest) requestCaptor.getAllValues().get(2); + DetachSecurityProfileRequest actualDetachRequest2 = + (DetachSecurityProfileRequest) requestCaptor.getAllValues().get(3); + Set actualDetachTargets = ImmutableSet.of(actualDetachRequest1.securityProfileTargetArn(), + actualDetachRequest2.securityProfileTargetArn()); + assertThat(actualDetachTargets).containsExactlyInAnyOrder("detachTarget1", "detachTarget2"); + } + + @Test + public void updateTags_SameKeyDifferentValue_OnlyTagCall() { + + Map desiredTagsCfn = ImmutableMap.of(TAG_1_KEY, "NewValue"); + List desiredTagsIot = ImmutableList.of( + software.amazon.awssdk.services.iot.model.Tag.builder() + .key(TAG_1_KEY) + .value("NewValue") + .build()); + + ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .previousResourceState(ResourceModel.builder().build()) + .previousResourceTags(ImmutableMap.of("doesn't", "matter")) + .desiredResourceTags(desiredTagsCfn) + .build(); + + doReturn(TAG_1_IOT_SET) + .when(handler) + .listTags(proxy, SECURITY_PROFILE_ARN); + + handler.updateTags(proxy, request, SECURITY_PROFILE_ARN, logger); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(TagResourceRequest.class); + verify(proxy).injectCredentialsAndInvokeV2(requestCaptor.capture(), any()); + TagResourceRequest submittedTagRequest = requestCaptor.getValue(); + assertThat(submittedTagRequest.tags()).isEqualTo(desiredTagsIot); + } + + @Test + public void updateTags_NoDesiredTags_OnlyUntagCall() { + + Map desiredTags = Collections.emptyMap(); + + ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .previousResourceState(ResourceModel.builder().build()) + .previousResourceTags(ImmutableMap.of("doesn't", "matter")) + .desiredResourceTags(desiredTags) + .build(); + + doReturn(TAG_1_IOT_SET) + .when(handler) + .listTags(proxy, SECURITY_PROFILE_ARN); + + handler.updateTags(proxy, request, SECURITY_PROFILE_ARN, logger); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(UntagResourceRequest.class); + verify(proxy).injectCredentialsAndInvokeV2(requestCaptor.capture(), any()); + UntagResourceRequest submittedUntagRequest = requestCaptor.getValue(); + assertThat(submittedUntagRequest.tagKeys()).isEqualTo(TAG_1_KEY_LIST); + } + + @Test + public void updateSecurityProfile_ResourceNotFound_VerifyTranslation() { + + ResourceModel desiredModel = ResourceModel.builder() + .securityProfileName(SECURITY_PROFILE_NAME) + .securityProfileDescription(SECURITY_PROFILE_DESCRIPTION) + .additionalMetricsToRetainV2(ADDITIONAL_METRICS_CFN) + .targetArns(TARGET_ARN_2_SET) + .build(); + ResourceModel previousModel = ResourceModel.builder() + .securityProfileName(SECURITY_PROFILE_NAME) + .securityProfileArn(SECURITY_PROFILE_ARN) + .build(); + + ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .previousResourceState(previousModel) + .desiredResourceState(desiredModel) + .build(); + + when(proxy.injectCredentialsAndInvokeV2(any(), any())) + .thenThrow(ResourceNotFoundException.builder().build()); + + assertThatThrownBy(() -> + handler.handleRequest(proxy, request, null, logger)) + .isInstanceOf(CfnNotFoundException.class); + } + + @Test + public void updateTags_ApiThrowsException_VerifyTranslation() { + + ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .previousResourceState(ResourceModel.builder().build()) + .previousResourceTags(TAG_1_STRINGMAP) + .desiredResourceTags(TAG_1_STRINGMAP) + .build(); + + when(proxy.injectCredentialsAndInvokeV2(any(), any())) + .thenThrow(InvalidRequestException.builder().build()); + + assertThatThrownBy(() -> + handler.updateTags(proxy, request, SECURITY_PROFILE_ARN, logger)) + .isInstanceOf(CfnInvalidRequestException.class); + } + + @Test + public void updateTargetAttachments_AttachThrowsException_VerifyTranslation() { + + Set previousTargets = ImmutableSet.of("keepTarget", "detachTarget1", "detachTarget2"); + doReturn(previousTargets) + .when(handler) + .listTargetsForSecurityProfile(proxy, SECURITY_PROFILE_NAME); + + Set desiredTargets = ImmutableSet.of("keepTarget", "attachTarget1", "attachTarget2"); + ResourceModel model = ResourceModel.builder() + .securityProfileName(SECURITY_PROFILE_NAME) + .targetArns(desiredTargets) + .build(); + + when(proxy.injectCredentialsAndInvokeV2(any(), any())) + .thenThrow(InternalFailureException.builder().build()); + + assertThatThrownBy(() -> + handler.updateTargetAttachments(proxy, model, logger)) + .isInstanceOf(CfnInternalFailureException.class); + } + + @Test + void handleRequest_DesiredArnIsPopulatedAndSame_ReturnFailed() { + + ResourceModel desiredModel = ResourceModel.builder() + .securityProfileArn(SECURITY_PROFILE_ARN) + .build(); + ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(desiredModel) + .build(); + + ProgressEvent result = + handler.handleRequest(proxy, request, null, logger); + + assertThat(result).isEqualTo(ProgressEvent.failed( + desiredModel, null, HandlerErrorCode.InvalidRequest, "Arn cannot be updated.")); + } + + // TODO: test system tags when the src code is ready +} diff --git a/aws-iot-securityprofile/template.yml b/aws-iot-securityprofile/template.yml new file mode 100644 index 0000000..dc98dc8 --- /dev/null +++ b/aws-iot-securityprofile/template.yml @@ -0,0 +1,23 @@ +AWSTemplateFormatVersion: "2010-09-09" +Transform: AWS::Serverless-2016-10-31 +Description: AWS SAM template for the AWS::IoT::SecurityProfile resource type + +Globals: + Function: + Timeout: 180 # docker start-up times can be long for SAM CLI + MemorySize: 256 + +Resources: + TypeFunction: + Type: AWS::Serverless::Function + Properties: + Handler: com.amazonaws.iot.securityprofile.HandlerWrapper::handleRequest + Runtime: java8 + CodeUri: ./target/aws-iot-securityprofile-handler-1.0-SNAPSHOT.jar + + TestEntrypoint: + Type: AWS::Serverless::Function + Properties: + Handler: com.amazonaws.iot.securityprofile.HandlerWrapper::testEntrypoint + Runtime: java8 + CodeUri: ./target/aws-iot-securityprofile-handler-1.0-SNAPSHOT.jar From 8b412dddfd4b280bcd8e86711da7ab8c81187c60 Mon Sep 17 00:00:00 2001 From: Anton Kuznetsov Date: Mon, 30 Nov 2020 14:32:10 -0800 Subject: [PATCH 2/8] Add a rate limiter for attach&detach calls --- .../com/amazonaws/iot/securityprofile/CreateHandler.java | 6 ++++++ .../com/amazonaws/iot/securityprofile/UpdateHandler.java | 8 ++++++++ 2 files changed, 14 insertions(+) diff --git a/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/CreateHandler.java b/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/CreateHandler.java index 1e62663..97fd1d9 100644 --- a/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/CreateHandler.java +++ b/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/CreateHandler.java @@ -3,6 +3,8 @@ import java.util.Map; import java.util.Set; +import com.google.common.util.concurrent.RateLimiter; + import org.apache.commons.lang3.StringUtils; import software.amazon.awssdk.services.iot.IotClient; import software.amazon.awssdk.services.iot.model.AttachSecurityProfileRequest; @@ -20,6 +22,7 @@ public class CreateHandler extends BaseHandler { // Copied value from software.amazon.cloudformation.resource.IdentifierUtils private static final int GENERATED_NAME_MAX_LENGTH = 40; + private static final int MAX_CALLS_PER_SECOND_LIMIT = 5; private final IotClient iotClient; @@ -59,7 +62,10 @@ public ProgressEvent handleRequest( // using the TargetArns field. Thus, we need to make an AttachSecurityProfile call for every target. Set targetArns = model.getTargetArns(); if (targetArns != null) { + // The number of targets can be large, we need to avoid getting throttled. + RateLimiter rateLimiter = RateLimiter.create(MAX_CALLS_PER_SECOND_LIMIT); for (String targetArn : targetArns) { + rateLimiter.acquire(); attachSecurityProfile(model.getSecurityProfileName(), targetArn, proxy); logger.log("Attached the security profile to " + targetArn); } diff --git a/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/UpdateHandler.java b/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/UpdateHandler.java index 281d3f0..39ae772 100644 --- a/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/UpdateHandler.java +++ b/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/UpdateHandler.java @@ -7,6 +7,7 @@ import java.util.stream.Collectors; import com.google.common.annotations.VisibleForTesting; +import com.google.common.util.concurrent.RateLimiter; import software.amazon.awssdk.services.iot.IotClient; import software.amazon.awssdk.services.iot.model.AttachSecurityProfileRequest; @@ -30,6 +31,8 @@ public class UpdateHandler extends BaseHandler { private final IotClient iotClient; + private static final int MAX_CALLS_PER_SECOND_LIMIT = 5; + public UpdateHandler() { iotClient = IotClient.builder().build(); } @@ -157,7 +160,11 @@ void updateTargetAttachments(AmazonWebServicesClientProxy proxy, .filter(target -> !desiredTargets.contains(target)) .collect(Collectors.toSet()); + // The number of targets can be large, we need to avoid getting throttled. + RateLimiter rateLimiter = RateLimiter.create(MAX_CALLS_PER_SECOND_LIMIT); + for (String targetArn : targetsToAttach) { + rateLimiter.acquire(); AttachSecurityProfileRequest attachRequest = AttachSecurityProfileRequest.builder() .securityProfileName(securityProfileName) .securityProfileTargetArn(targetArn) @@ -171,6 +178,7 @@ void updateTargetAttachments(AmazonWebServicesClientProxy proxy, } for (String targetArn : targetsToDetach) { + rateLimiter.acquire(); DetachSecurityProfileRequest detachRequest = DetachSecurityProfileRequest.builder() .securityProfileName(securityProfileName) .securityProfileTargetArn(targetArn) From 4c6c685be9d75cb6e24caffe2c0af687ba2d8a9b Mon Sep 17 00:00:00 2001 From: Anton Kuznetsov Date: Tue, 1 Dec 2020 15:10:13 -0800 Subject: [PATCH 3/8] Remove the deprecated AdditionalMetrics (V1) field --- aws-iot-securityprofile/aws-iot-securityprofile.json | 12 ------------ .../amazonaws/iot/securityprofile/CreateHandler.java | 1 - .../amazonaws/iot/securityprofile/ReadHandler.java | 4 ---- .../amazonaws/iot/securityprofile/UpdateHandler.java | 11 ++--------- .../iot/securityprofile/ReadHandlerTest.java | 4 ---- .../amazonaws/iot/securityprofile/TestConstants.java | 4 ---- .../iot/securityprofile/UpdateHandlerTest.java | 5 ----- 7 files changed, 2 insertions(+), 39 deletions(-) diff --git a/aws-iot-securityprofile/aws-iot-securityprofile.json b/aws-iot-securityprofile/aws-iot-securityprofile.json index f5314f4..4d3c593 100644 --- a/aws-iot-securityprofile/aws-iot-securityprofile.json +++ b/aws-iot-securityprofile/aws-iot-securityprofile.json @@ -257,18 +257,6 @@ }, "additionalProperties": false }, - "AdditionalMetricsToRetain": { - "description": "This parameter has been deprecated, please use AdditionalMetricsToRetainV2.", - "type": "array", - "uniqueItems": true, - "insertionOrder": false, - "items": { - "type": "string", - "pattern": "[a-zA-Z0-9:_-]+", - "minLength": 1, - "maxLength": 128 - } - }, "AdditionalMetricsToRetainV2": { "description": "A list of metrics whose data is retained (stored). By default, data is retained for any metric used in the profile's behaviors, but it is also retained for any metric specified here.", "type": "array", diff --git a/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/CreateHandler.java b/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/CreateHandler.java index 97fd1d9..d429fbd 100644 --- a/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/CreateHandler.java +++ b/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/CreateHandler.java @@ -102,7 +102,6 @@ private CreateSecurityProfileRequest translateToCreateRequest( .securityProfileDescription(model.getSecurityProfileDescription()) .behaviors(Translator.translateBehaviorSetFromCfnToIot(model.getBehaviors())) .alertTargetsWithStrings(Translator.translateAlertTargetMapFromCfnToIot(model.getAlertTargets())) - .additionalMetricsToRetain(model.getAdditionalMetricsToRetain()) .additionalMetricsToRetainV2(Translator.translateMetricToRetainSetFromCfnToIot( model.getAdditionalMetricsToRetainV2())) .tags(Translator.translateTagsFromCfnToIot(tags)) diff --git a/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/ReadHandler.java b/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/ReadHandler.java index 28c2e7c..d9dc472 100644 --- a/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/ReadHandler.java +++ b/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/ReadHandler.java @@ -99,10 +99,6 @@ ResourceModel buildResourceModel( resourceModelBuilder.alertTargets(Translator.translateAlertTargetMapFromIotToCfn( describeResponse.alertTargetsAsStrings())); } - if (describeResponse.hasAdditionalMetricsToRetain()) { - resourceModelBuilder.additionalMetricsToRetain( - new HashSet<>(describeResponse.additionalMetricsToRetain())); - } if (describeResponse.hasAdditionalMetricsToRetainV2()) { resourceModelBuilder.additionalMetricsToRetainV2( Translator.translateMetricToRetainListFromIotToCfn( diff --git a/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/UpdateHandler.java b/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/UpdateHandler.java index 39ae772..345f1f8 100644 --- a/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/UpdateHandler.java +++ b/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/UpdateHandler.java @@ -94,20 +94,14 @@ String updateSecurityProfile(AmazonWebServicesClientProxy proxy, alertTargetsForRequest = Translator.translateAlertTargetMapFromCfnToIot(model.getAlertTargets()); } - // For additionalMetricsToRetain, there's no separate flag for V2 + // Same for additionalMetricsToRetain boolean deleteAdditionalMetricsToRetain; - boolean noMetricsToRetainProvided = - CollectionUtils.isNullOrEmpty(model.getAdditionalMetricsToRetain()) && - CollectionUtils.isNullOrEmpty(model.getAdditionalMetricsToRetainV2()); - Set additionalMetricsV1ForRequest; Set additionalMetricsV2ForRequest; - if (noMetricsToRetainProvided) { + if (CollectionUtils.isNullOrEmpty(model.getAdditionalMetricsToRetainV2())) { deleteAdditionalMetricsToRetain = true; - additionalMetricsV1ForRequest = null; additionalMetricsV2ForRequest = null; } else { deleteAdditionalMetricsToRetain = false; - additionalMetricsV1ForRequest = model.getAdditionalMetricsToRetain(); additionalMetricsV2ForRequest = Translator.translateMetricToRetainSetFromCfnToIot( model.getAdditionalMetricsToRetainV2()); } @@ -117,7 +111,6 @@ String updateSecurityProfile(AmazonWebServicesClientProxy proxy, .securityProfileDescription(model.getSecurityProfileDescription()) .behaviors(behaviorsForRequest) .alertTargetsWithStrings(alertTargetsForRequest) - .additionalMetricsToRetain(additionalMetricsV1ForRequest) .additionalMetricsToRetainV2(additionalMetricsV2ForRequest) .deleteBehaviors(deleteBehaviors) .deleteAlertTargets(deleteAlertTargets) diff --git a/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/ReadHandlerTest.java b/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/ReadHandlerTest.java index 957b7ce..678df06 100644 --- a/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/ReadHandlerTest.java +++ b/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/ReadHandlerTest.java @@ -2,7 +2,6 @@ import static com.amazonaws.iot.securityprofile.TestConstants.ADDITIONAL_METRICS_CFN; import static com.amazonaws.iot.securityprofile.TestConstants.ADDITIONAL_METRICS_IOT; -import static com.amazonaws.iot.securityprofile.TestConstants.ADDITIONAL_METRICS_V1_SET; import static com.amazonaws.iot.securityprofile.TestConstants.ALERT_TARGET_MAP_CFN; import static com.amazonaws.iot.securityprofile.TestConstants.ALERT_TARGET_MAP_IOT; import static com.amazonaws.iot.securityprofile.TestConstants.BEHAVIOR_1_CFN_SET; @@ -74,7 +73,6 @@ public void handleRequest_AllFieldsPopulated_VerifyRequestResponse() { .securityProfileDescription(SECURITY_PROFILE_DESCRIPTION) .behaviors(BEHAVIOR_1_IOT) .alertTargetsWithStrings(ALERT_TARGET_MAP_IOT) - .additionalMetricsToRetain(ADDITIONAL_METRICS_V1_SET) .additionalMetricsToRetainV2(ADDITIONAL_METRICS_IOT) .build(); when(proxy.injectCredentialsAndInvokeV2(eq(expectedDescribeRequest), any())) @@ -105,7 +103,6 @@ public void handleRequest_AllFieldsPopulated_VerifyRequestResponse() { .securityProfileDescription(SECURITY_PROFILE_DESCRIPTION) .behaviors(BEHAVIOR_1_CFN_SET) .alertTargets(ALERT_TARGET_MAP_CFN) - .additionalMetricsToRetain(ADDITIONAL_METRICS_V1_SET) .additionalMetricsToRetainV2(ADDITIONAL_METRICS_CFN) .targetArns(TARGET_ARN_1_SET) .tags(TAG_1_CFN_SET) @@ -139,7 +136,6 @@ public void buildResourceModel_NullCollections_StayNull() { .securityProfileDescription(SECURITY_PROFILE_DESCRIPTION) .behaviors(null) .alertTargets(null) - .additionalMetricsToRetain(null) .additionalMetricsToRetainV2(null) .targetArns(TARGET_ARN_1_SET) .tags(TAG_1_CFN_SET) diff --git a/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/TestConstants.java b/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/TestConstants.java index 0987ee5..ccbd0ba 100644 --- a/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/TestConstants.java +++ b/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/TestConstants.java @@ -114,8 +114,4 @@ public class TestConstants { ImmutableMap.of("SNS", ALERT_TARGET_IOT); static final Map ALERT_TARGET_MAP_CFN = ImmutableMap.of("SNS", ALERT_TARGET_CFN); - static final Set ADDITIONAL_METRICS_V1_SET = - ImmutableSet.of("aws:listening-tcp-ports", "aws:num-listening-udp-ports"); - static final List ADDITIONAL_METRICS_V1_LIST = - ImmutableList.of("aws:listening-tcp-ports", "aws:num-listening-udp-ports"); } diff --git a/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/UpdateHandlerTest.java b/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/UpdateHandlerTest.java index 1b29c28..dbf51e4 100644 --- a/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/UpdateHandlerTest.java +++ b/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/UpdateHandlerTest.java @@ -2,8 +2,6 @@ import static com.amazonaws.iot.securityprofile.TestConstants.ADDITIONAL_METRICS_CFN; import static com.amazonaws.iot.securityprofile.TestConstants.ADDITIONAL_METRICS_IOT; -import static com.amazonaws.iot.securityprofile.TestConstants.ADDITIONAL_METRICS_V1_LIST; -import static com.amazonaws.iot.securityprofile.TestConstants.ADDITIONAL_METRICS_V1_SET; import static com.amazonaws.iot.securityprofile.TestConstants.ALERT_TARGET_MAP_CFN; import static com.amazonaws.iot.securityprofile.TestConstants.ALERT_TARGET_MAP_IOT; import static com.amazonaws.iot.securityprofile.TestConstants.BEHAVIOR_1_CFN_SET; @@ -93,7 +91,6 @@ public void handleRequest_AllFieldsUpdated_VerifyRequests() { .securityProfileDescription(SECURITY_PROFILE_DESCRIPTION) .behaviors(BEHAVIOR_1_CFN_SET) .alertTargets(ALERT_TARGET_MAP_CFN) - .additionalMetricsToRetain(ADDITIONAL_METRICS_V1_SET) .additionalMetricsToRetainV2(ADDITIONAL_METRICS_CFN) .targetArns(TARGET_ARN_2_SET) .build(); @@ -140,7 +137,6 @@ public void handleRequest_AllFieldsUpdated_VerifyRequests() { assertThat(submittedUpdateRequest.securityProfileDescription()).isEqualTo(SECURITY_PROFILE_DESCRIPTION); assertThat(submittedUpdateRequest.behaviors()).isEqualTo(BEHAVIOR_1_IOT_LIST); assertThat(submittedUpdateRequest.alertTargetsAsStrings()).isEqualTo(ALERT_TARGET_MAP_IOT); - assertThat(submittedUpdateRequest.additionalMetricsToRetain()).isEqualTo(ADDITIONAL_METRICS_V1_LIST); assertThat(submittedUpdateRequest.additionalMetricsToRetainV2()).isEqualTo(ADDITIONAL_METRICS_IOT); assertThat(submittedUpdateRequest.deleteBehaviors()).isFalse(); assertThat(submittedUpdateRequest.deleteAlertTargets()).isFalse(); @@ -209,7 +205,6 @@ public void updateSecurityProfile_EmptyCollections_VerifyDeleteFlags() { .targetArns(TARGET_ARN_2_SET) .behaviors(Collections.emptySet()) .alertTargets(Collections.emptyMap()) - .additionalMetricsToRetain(Collections.emptySet()) .additionalMetricsToRetainV2(Collections.emptySet()) .build(); From 09e62a47f99448c366eb7eaceb8c8ea76f8d6d36 Mon Sep 17 00:00:00 2001 From: Anton Kuznetsov Date: Fri, 4 Dec 2020 12:00:21 -0800 Subject: [PATCH 4/8] Fix typo in list handler permissions --- aws-iot-securityprofile/aws-iot-securityprofile.json | 2 +- aws-iot-securityprofile/resource-role.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/aws-iot-securityprofile/aws-iot-securityprofile.json b/aws-iot-securityprofile/aws-iot-securityprofile.json index 4d3c593..7b2b346 100644 --- a/aws-iot-securityprofile/aws-iot-securityprofile.json +++ b/aws-iot-securityprofile/aws-iot-securityprofile.json @@ -340,7 +340,7 @@ }, "list": { "permissions": [ - "iot:ListDimensions" + "iot:ListSecurityProfiles" ] } } diff --git a/aws-iot-securityprofile/resource-role.yaml b/aws-iot-securityprofile/resource-role.yaml index 0c64db5..38675bd 100644 --- a/aws-iot-securityprofile/resource-role.yaml +++ b/aws-iot-securityprofile/resource-role.yaml @@ -29,7 +29,7 @@ Resources: - "iot:DeleteSecurityProfile" - "iot:DescribeSecurityProfile" - "iot:DetachSecurityProfile" - - "iot:ListDimensions" + - "iot:ListSecurityProfiles" - "iot:ListTagsForResource" - "iot:ListTargetsForSecurityProfile" - "iot:TagResource" From f3c4d035305253e9b0ea4fa2a8d6503f691e7aed Mon Sep 17 00:00:00 2001 From: Anton Kuznetsov Date: Wed, 9 Dec 2020 17:40:43 -0800 Subject: [PATCH 5/8] Remove accidental requirement for name in json schema --- aws-iot-securityprofile/aws-iot-securityprofile.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/aws-iot-securityprofile/aws-iot-securityprofile.json b/aws-iot-securityprofile/aws-iot-securityprofile.json index 7b2b346..9327c25 100644 --- a/aws-iot-securityprofile/aws-iot-securityprofile.json +++ b/aws-iot-securityprofile/aws-iot-securityprofile.json @@ -296,9 +296,7 @@ "primaryIdentifier": [ "/properties/SecurityProfileName" ], - "required": [ - "SecurityProfileName" - ], + "required": [], "createOnlyProperties": [ "/properties/SecurityProfileName" ], From 3bd28d698e09f490dcf793f2413dd07138decbac Mon Sep 17 00:00:00 2001 From: Anton Kuznetsov Date: Tue, 15 Dec 2020 09:52:23 -0800 Subject: [PATCH 6/8] Use ProgressEvents instead of Cfn exceptions --- .../iot/securityprofile/CreateHandler.java | 31 ++++------ .../iot/securityprofile/DeleteHandler.java | 13 ++--- .../iot/securityprofile/HandlerUtils.java | 16 +---- .../iot/securityprofile/ListHandler.java | 4 +- .../iot/securityprofile/ReadHandler.java | 7 +-- .../iot/securityprofile/Translator.java | 58 ++++++++++++------- .../iot/securityprofile/UpdateHandler.java | 53 +++++++---------- .../securityprofile/CreateHandlerTest.java | 15 ++--- .../securityprofile/DeleteHandlerTest.java | 16 +++-- .../iot/securityprofile/HandlerUtilsTest.java | 7 +-- .../iot/securityprofile/ListHandlerTest.java | 6 +- .../iot/securityprofile/ReadHandlerTest.java | 8 +-- .../securityprofile/UpdateHandlerTest.java | 17 +++--- 13 files changed, 117 insertions(+), 134 deletions(-) diff --git a/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/CreateHandler.java b/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/CreateHandler.java index d429fbd..6a849f6 100644 --- a/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/CreateHandler.java +++ b/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/CreateHandler.java @@ -10,7 +10,6 @@ import software.amazon.awssdk.services.iot.model.AttachSecurityProfileRequest; import software.amazon.awssdk.services.iot.model.CreateSecurityProfileRequest; import software.amazon.awssdk.services.iot.model.CreateSecurityProfileResponse; -import software.amazon.awssdk.services.iot.model.IotException; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; import software.amazon.cloudformation.proxy.HandlerErrorCode; import software.amazon.cloudformation.proxy.Logger; @@ -51,8 +50,8 @@ public ProgressEvent handleRequest( try { createResponse = proxy.injectCredentialsAndInvokeV2( createRequest, iotClient::createSecurityProfile); - } catch (IotException e) { - throw Translator.translateIotExceptionToCfn(e); + } catch (Exception e) { + return Translator.translateExceptionToProgressEvent(model, e, logger); } model.setSecurityProfileArn(createResponse.securityProfileArn()); @@ -66,7 +65,16 @@ public ProgressEvent handleRequest( RateLimiter rateLimiter = RateLimiter.create(MAX_CALLS_PER_SECOND_LIMIT); for (String targetArn : targetArns) { rateLimiter.acquire(); - attachSecurityProfile(model.getSecurityProfileName(), targetArn, proxy); + + AttachSecurityProfileRequest attachRequest = AttachSecurityProfileRequest.builder() + .securityProfileName(model.getSecurityProfileName()) + .securityProfileTargetArn(targetArn) + .build(); + try { + proxy.injectCredentialsAndInvokeV2(attachRequest, iotClient::attachSecurityProfile); + } catch (Exception e) { + return Translator.translateExceptionToProgressEvent(model, e, logger); + } logger.log("Attached the security profile to " + targetArn); } } @@ -107,19 +115,4 @@ private CreateSecurityProfileRequest translateToCreateRequest( .tags(Translator.translateTagsFromCfnToIot(tags)) .build(); } - - private void attachSecurityProfile(String securityProfileName, - String targetArn, - AmazonWebServicesClientProxy proxy) { - - AttachSecurityProfileRequest attachRequest = AttachSecurityProfileRequest.builder() - .securityProfileName(securityProfileName) - .securityProfileTargetArn(targetArn) - .build(); - try { - proxy.injectCredentialsAndInvokeV2(attachRequest, iotClient::attachSecurityProfile); - } catch (IotException e) { - throw Translator.translateIotExceptionToCfn(e); - } - } } diff --git a/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/DeleteHandler.java b/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/DeleteHandler.java index f66ac2c..77e108b 100644 --- a/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/DeleteHandler.java +++ b/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/DeleteHandler.java @@ -3,7 +3,6 @@ import software.amazon.awssdk.services.iot.IotClient; import software.amazon.awssdk.services.iot.model.DeleteSecurityProfileRequest; import software.amazon.awssdk.services.iot.model.DescribeSecurityProfileRequest; -import software.amazon.awssdk.services.iot.model.IotException; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; import software.amazon.cloudformation.proxy.Logger; import software.amazon.cloudformation.proxy.ProgressEvent; @@ -35,11 +34,11 @@ public ProgressEvent handleRequest( .build(); try { proxy.injectCredentialsAndInvokeV2(describeRequest, iotClient::describeSecurityProfile); - } catch (IotException e) { + } catch (Exception e) { // If the resource doesn't exist, DescribeSecurityProfile will throw NotFoundException, - // which we'll rethrow as CfnNotFoundException - that's all we need to do. - // CFN (the caller) will swallow this NotFound exception and the customer will see success. - throw Translator.translateIotExceptionToCfn(e); + // and we'll return FAILED with HandlerErrorCode.NotFound. + // CFN (the caller) will swallow the "failure" and the customer will see success. + return Translator.translateExceptionToProgressEvent(model, e, logger); } logger.log(String.format("Called Describe for %s with name %s, accountId %s.", ResourceModel.TYPE_NAME, model.getSecurityProfileName(), request.getAwsAccountId())); @@ -49,8 +48,8 @@ public ProgressEvent handleRequest( .build(); try { proxy.injectCredentialsAndInvokeV2(deleteRequest, iotClient::deleteSecurityProfile); - } catch (IotException e) { - throw Translator.translateIotExceptionToCfn(e); + } catch (Exception e) { + return Translator.translateExceptionToProgressEvent(model, e, logger); } logger.log(String.format("Deleted %s with name %s, accountId %s.", diff --git a/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/HandlerUtils.java b/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/HandlerUtils.java index 641fc5b..e09cd64 100644 --- a/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/HandlerUtils.java +++ b/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/HandlerUtils.java @@ -28,13 +28,8 @@ static Set listTargetsForSecurityProfile( .securityProfileName(securityProfileName) .nextToken(nextToken) .build(); - ListTargetsForSecurityProfileResponse listResponse; - try { - listResponse = proxy.injectCredentialsAndInvokeV2( - listRequest, iotClient::listTargetsForSecurityProfile); - } catch (IotException e) { - throw Translator.translateIotExceptionToCfn(e); - } + ListTargetsForSecurityProfileResponse listResponse = proxy.injectCredentialsAndInvokeV2( + listRequest, iotClient::listTargetsForSecurityProfile); List securityProfileTargets = listResponse.securityProfileTargets(); securityProfileTargets.forEach(target -> result.add(target.arn())); nextToken = listResponse.nextToken(); @@ -55,13 +50,8 @@ static Set listTags( .resourceArn(resourceArn) .nextToken(nextToken) .build(); - ListTagsForResourceResponse listTagsForResourceResponse; - try { - listTagsForResourceResponse = proxy.injectCredentialsAndInvokeV2( + ListTagsForResourceResponse listTagsForResourceResponse = proxy.injectCredentialsAndInvokeV2( listTagsRequest, iotClient::listTagsForResource); - } catch (IotException e) { - throw Translator.translateIotExceptionToCfn(e); - } result.addAll(listTagsForResourceResponse.tags()); nextToken = listTagsForResourceResponse.nextToken(); } while (nextToken != null); diff --git a/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/ListHandler.java b/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/ListHandler.java index 01b69c6..370fa85 100644 --- a/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/ListHandler.java +++ b/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/ListHandler.java @@ -36,8 +36,8 @@ public ProgressEvent handleRequest( try { listSecurityProfilesResponse = proxy.injectCredentialsAndInvokeV2( listRequest, iotClient::listSecurityProfiles); - } catch (IotException e) { - throw Translator.translateIotExceptionToCfn(e); + } catch (Exception e) { + return Translator.translateExceptionToProgressEvent(request.getDesiredResourceState(), e, logger); } List models = listSecurityProfilesResponse.securityProfileIdentifiers().stream() diff --git a/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/ReadHandler.java b/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/ReadHandler.java index d9dc472..8a179b4 100644 --- a/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/ReadHandler.java +++ b/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/ReadHandler.java @@ -1,6 +1,5 @@ package com.amazonaws.iot.securityprofile; -import java.util.HashSet; import java.util.Set; import com.google.common.annotations.VisibleForTesting; @@ -8,7 +7,6 @@ import software.amazon.awssdk.services.iot.IotClient; import software.amazon.awssdk.services.iot.model.DescribeSecurityProfileRequest; import software.amazon.awssdk.services.iot.model.DescribeSecurityProfileResponse; -import software.amazon.awssdk.services.iot.model.IotException; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; import software.amazon.cloudformation.proxy.Logger; import software.amazon.cloudformation.proxy.ProgressEvent; @@ -39,10 +37,11 @@ public ProgressEvent handleRequest( try { describeResponse = proxy.injectCredentialsAndInvokeV2( describeRequest, iotClient::describeSecurityProfile); - } catch (IotException e) { - throw Translator.translateIotExceptionToCfn(e); + } catch (Exception e) { + return Translator.translateExceptionToProgressEvent(model, e, logger); } + String securityProfileArn = describeResponse.securityProfileArn(); logger.log("Called Describe for " + securityProfileArn); diff --git a/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/Translator.java b/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/Translator.java index d93d6df..9d210de 100644 --- a/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/Translator.java +++ b/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/Translator.java @@ -7,23 +7,20 @@ import java.util.stream.Collectors; import lombok.NonNull; +import org.apache.commons.lang3.exception.ExceptionUtils; import software.amazon.awssdk.services.iot.model.InternalFailureException; import software.amazon.awssdk.services.iot.model.InvalidRequestException; -import software.amazon.awssdk.services.iot.model.IotException; import software.amazon.awssdk.services.iot.model.LimitExceededException; import software.amazon.awssdk.services.iot.model.ResourceAlreadyExistsException; import software.amazon.awssdk.services.iot.model.ResourceNotFoundException; import software.amazon.awssdk.services.iot.model.Tag; import software.amazon.awssdk.services.iot.model.ThrottlingException; import software.amazon.awssdk.services.iot.model.UnauthorizedException; -import software.amazon.cloudformation.exceptions.BaseHandlerException; -import software.amazon.cloudformation.exceptions.CfnAccessDeniedException; -import software.amazon.cloudformation.exceptions.CfnAlreadyExistsException; -import software.amazon.cloudformation.exceptions.CfnInternalFailureException; import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; -import software.amazon.cloudformation.exceptions.CfnNotFoundException; -import software.amazon.cloudformation.exceptions.CfnServiceLimitExceededException; -import software.amazon.cloudformation.exceptions.CfnThrottlingException; +import software.amazon.cloudformation.proxy.HandlerErrorCode; +import software.amazon.cloudformation.proxy.Logger; +import software.amazon.cloudformation.proxy.OperationStatus; +import software.amazon.cloudformation.proxy.ProgressEvent; public class Translator { @@ -292,11 +289,31 @@ static Set translateTagsFromIotToCfn( .collect(Collectors.toSet()); } - static BaseHandlerException translateIotExceptionToCfn(IotException e) { + static ProgressEvent translateExceptionToProgressEvent( + ResourceModel model, Exception e, Logger logger) { + + HandlerErrorCode errorCode = translateExceptionToProgressEvent(e, logger); + ProgressEvent progressEvent = + ProgressEvent.builder() + .resourceModel(model) + .status(OperationStatus.FAILED) + .errorCode(errorCode) + .build(); + if (errorCode != HandlerErrorCode.InternalFailure) { + progressEvent.setMessage(e.getMessage()); + } + return progressEvent; + } + + static HandlerErrorCode translateExceptionToProgressEvent(Exception e, Logger logger) { + + logger.log(String.format("Translating exception \"%s\", stack trace: %s", + e.getMessage(), ExceptionUtils.getStackTrace(e))); + // We're handling all the exceptions documented in API docs // https://docs.aws.amazon.com/iot/latest/apireference/API_CreateSecurityProfile.html // (+similar pages for other APIs) - // For Throttling and InternalFailure, we want CFN to retry, and it will do so based on the exception type. + // For Throttling and InternalFailure, we want CFN to retry, and it will do so based on the error code. // Reference with Retriable/Terminal in comments for each: https://tinyurl.com/y378qdno if (e instanceof ResourceAlreadyExistsException) { // Note regarding idempotency: @@ -304,23 +321,24 @@ static BaseHandlerException translateIotExceptionToCfn(IotException e) { // SecurityProfile is created out of band and then the same request is sent via CFN, the API will throw // AlreadyExists because the CFN request will contain the stack level tags. // This behavior satisfies the CreateHandler contract. - return new CfnAlreadyExistsException(e); + return HandlerErrorCode.AlreadyExists; } else if (e instanceof InvalidRequestException) { - return new CfnInvalidRequestException(e); + return HandlerErrorCode.InvalidRequest; } else if (e instanceof LimitExceededException) { - return new CfnServiceLimitExceededException(e); + return HandlerErrorCode.ServiceLimitExceeded; } else if (e instanceof UnauthorizedException) { - return new CfnAccessDeniedException(e); + return HandlerErrorCode.AccessDenied; } else if (e instanceof InternalFailureException) { - return new CfnInternalFailureException(e); + return HandlerErrorCode.InternalFailure; } else if (e instanceof ThrottlingException) { - return new CfnThrottlingException(e); + return HandlerErrorCode.Throttling; } else if (e instanceof ResourceNotFoundException) { - return new CfnNotFoundException(e); + return HandlerErrorCode.NotFound; } else { - // Any other exception at this point is unexpected. CFN will catch this and convert appropriately. - // Reference: https://tinyurl.com/y6mphxbn - throw e; + logger.log(String.format("Unexpected exception \"%s\", stack trace: %s", + e.getMessage(), ExceptionUtils.getStackTrace(e))); + // Any other exception at this point is unexpected. + return HandlerErrorCode.InternalFailure; } } } diff --git a/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/UpdateHandler.java b/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/UpdateHandler.java index 345f1f8..a3f77df 100644 --- a/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/UpdateHandler.java +++ b/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/UpdateHandler.java @@ -12,7 +12,6 @@ import software.amazon.awssdk.services.iot.IotClient; import software.amazon.awssdk.services.iot.model.AttachSecurityProfileRequest; import software.amazon.awssdk.services.iot.model.DetachSecurityProfileRequest; -import software.amazon.awssdk.services.iot.model.IotException; import software.amazon.awssdk.services.iot.model.MetricToRetain; import software.amazon.awssdk.services.iot.model.Tag; import software.amazon.awssdk.services.iot.model.TagResourceRequest; @@ -52,13 +51,26 @@ public ProgressEvent handleRequest( "Arn cannot be updated."); } - String securityProfileArn = updateSecurityProfile(proxy, desiredModel, logger); + String securityProfileArn; + try { + securityProfileArn = updateSecurityProfile(proxy, desiredModel, logger); + } catch (Exception e) { + return Translator.translateExceptionToProgressEvent(desiredModel, e, logger); + } // Security profile targets are managed by separate APIs, not UpdateSecurityProfile. - updateTargetAttachments(proxy, desiredModel, logger); + try { + updateTargetAttachments(proxy, desiredModel, logger); + } catch (Exception e) { + return Translator.translateExceptionToProgressEvent(desiredModel, e, logger); + } // Same for tags. - updateTags(proxy, request, securityProfileArn, logger); + try { + updateTags(proxy, request, securityProfileArn, logger); + } catch (Exception e) { + return Translator.translateExceptionToProgressEvent(desiredModel, e, logger); + } desiredModel.setSecurityProfileArn(securityProfileArn); return ProgressEvent.defaultSuccessHandler(desiredModel); @@ -117,13 +129,8 @@ String updateSecurityProfile(AmazonWebServicesClientProxy proxy, .deleteAdditionalMetricsToRetain(deleteAdditionalMetricsToRetain) .build(); - UpdateSecurityProfileResponse updateResponse; - try { - updateResponse = proxy.injectCredentialsAndInvokeV2( - updateRequest, iotClient::updateSecurityProfile); - } catch (IotException e) { - throw Translator.translateIotExceptionToCfn(e); - } + UpdateSecurityProfileResponse updateResponse = proxy.injectCredentialsAndInvokeV2( + updateRequest, iotClient::updateSecurityProfile); String arn = updateResponse.securityProfileArn(); logger.log("Called UpdateSecurityProfile for " + arn); return arn; @@ -162,11 +169,7 @@ void updateTargetAttachments(AmazonWebServicesClientProxy proxy, .securityProfileName(securityProfileName) .securityProfileTargetArn(targetArn) .build(); - try { - proxy.injectCredentialsAndInvokeV2(attachRequest, iotClient::attachSecurityProfile); - } catch (IotException e) { - throw Translator.translateIotExceptionToCfn(e); - } + proxy.injectCredentialsAndInvokeV2(attachRequest, iotClient::attachSecurityProfile); logger.log("Attached " + securityProfileName + " to " + targetArn); } @@ -176,11 +179,7 @@ void updateTargetAttachments(AmazonWebServicesClientProxy proxy, .securityProfileName(securityProfileName) .securityProfileTargetArn(targetArn) .build(); - try { - proxy.injectCredentialsAndInvokeV2(detachRequest, iotClient::detachSecurityProfile); - } catch (IotException e) { - throw Translator.translateIotExceptionToCfn(e); - } + proxy.injectCredentialsAndInvokeV2(detachRequest, iotClient::detachSecurityProfile); logger.log("Detached " + securityProfileName + " from " + targetArn); } } @@ -221,11 +220,7 @@ void updateTags(AmazonWebServicesClientProxy proxy, .resourceArn(resourceArn) .tags(tagsToAttach) .build(); - try { - proxy.injectCredentialsAndInvokeV2(tagResourceRequest, iotClient::tagResource); - } catch (IotException e) { - throw Translator.translateIotExceptionToCfn(e); - } + proxy.injectCredentialsAndInvokeV2(tagResourceRequest, iotClient::tagResource); logger.log("Called TagResource for " + resourceArn); } @@ -234,11 +229,7 @@ void updateTags(AmazonWebServicesClientProxy proxy, .resourceArn(resourceArn) .tagKeys(tagKeysToDetach) .build(); - try { - proxy.injectCredentialsAndInvokeV2(untagResourceRequest, iotClient::untagResource); - } catch (IotException e) { - throw Translator.translateIotExceptionToCfn(e); - } + proxy.injectCredentialsAndInvokeV2(untagResourceRequest, iotClient::untagResource); logger.log("Called UntagResource for " + resourceArn); } } diff --git a/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/CreateHandlerTest.java b/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/CreateHandlerTest.java index 4906deb..36e080d 100644 --- a/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/CreateHandlerTest.java +++ b/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/CreateHandlerTest.java @@ -11,7 +11,6 @@ import static com.amazonaws.iot.securityprofile.TestConstants.TAG_1_STRINGMAP; import static com.amazonaws.iot.securityprofile.TestConstants.TARGET_ARNS; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -31,8 +30,6 @@ import software.amazon.awssdk.services.iot.model.IotRequest; import software.amazon.awssdk.services.iot.model.ResourceAlreadyExistsException; import software.amazon.awssdk.services.iot.model.ResourceNotFoundException; -import software.amazon.cloudformation.exceptions.CfnAlreadyExistsException; -import software.amazon.cloudformation.exceptions.CfnNotFoundException; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; import software.amazon.cloudformation.proxy.HandlerErrorCode; import software.amazon.cloudformation.proxy.Logger; @@ -133,9 +130,9 @@ public void handleRequest_CreateThrowsAlreadyExists_VerifyTranslation() { when(proxy.injectCredentialsAndInvokeV2(any(), any())) .thenThrow(ResourceAlreadyExistsException.builder().build()); - assertThatThrownBy(() -> - handler.handleRequest(proxy, request, null, logger)) - .isInstanceOf(CfnAlreadyExistsException.class); + ProgressEvent progressEvent = + handler.handleRequest(proxy, request, null, logger); + assertThat(progressEvent.getErrorCode()).isEqualTo(HandlerErrorCode.AlreadyExists); } @Test @@ -159,9 +156,9 @@ public void handleRequest_AttachThrowsNotFound_VerifyTranslation() { .thenReturn(createResponse) .thenThrow(ResourceNotFoundException.builder().build()); - assertThatThrownBy(() -> - handler.handleRequest(proxy, request, null, logger)) - .isInstanceOf(CfnNotFoundException.class); + ProgressEvent progressEvent = + handler.handleRequest(proxy, request, null, logger); + assertThat(progressEvent.getErrorCode()).isEqualTo(HandlerErrorCode.NotFound); } @Test diff --git a/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/DeleteHandlerTest.java b/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/DeleteHandlerTest.java index 1f9fe3a..4449508 100644 --- a/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/DeleteHandlerTest.java +++ b/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/DeleteHandlerTest.java @@ -2,7 +2,6 @@ import static com.amazonaws.iot.securityprofile.TestConstants.SECURITY_PROFILE_NAME; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -19,9 +18,8 @@ import software.amazon.awssdk.services.iot.model.InternalFailureException; import software.amazon.awssdk.services.iot.model.IotRequest; import software.amazon.awssdk.services.iot.model.ResourceNotFoundException; -import software.amazon.cloudformation.exceptions.CfnInternalFailureException; -import software.amazon.cloudformation.exceptions.CfnNotFoundException; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.HandlerErrorCode; import software.amazon.cloudformation.proxy.Logger; import software.amazon.cloudformation.proxy.OperationStatus; import software.amazon.cloudformation.proxy.ProgressEvent; @@ -92,9 +90,9 @@ public void handleRequest_DescribeThrowsNotFound_VerifyTranslation() { when(proxy.injectCredentialsAndInvokeV2(any(), any())) .thenThrow(ResourceNotFoundException.builder().build()); - assertThatThrownBy(() -> - handler.handleRequest(proxy, request, null, logger)) - .isInstanceOf(CfnNotFoundException.class); + ProgressEvent progressEvent = + handler.handleRequest(proxy, request, null, logger); + assertThat(progressEvent.getErrorCode()).isEqualTo(HandlerErrorCode.NotFound); } @Test @@ -112,8 +110,8 @@ public void handleRequest_DeleteThrowsException_VerifyTranslation() { .thenReturn(DescribeSecurityProfileResponse.builder().build()) .thenThrow(InternalFailureException.builder().build()); - assertThatThrownBy(() -> - handler.handleRequest(proxy, request, null, logger)) - .isInstanceOf(CfnInternalFailureException.class); + ProgressEvent progressEvent = + handler.handleRequest(proxy, request, null, logger); + assertThat(progressEvent.getErrorCode()).isEqualTo(HandlerErrorCode.InternalFailure); } } diff --git a/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/HandlerUtilsTest.java b/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/HandlerUtilsTest.java index df5af78..48faad4 100644 --- a/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/HandlerUtilsTest.java +++ b/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/HandlerUtilsTest.java @@ -28,7 +28,6 @@ import software.amazon.awssdk.services.iot.model.ListTargetsForSecurityProfileResponse; import software.amazon.awssdk.services.iot.model.ThrottlingException; import software.amazon.cloudformation.exceptions.CfnServiceLimitExceededException; -import software.amazon.cloudformation.exceptions.CfnThrottlingException; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; import software.amazon.cloudformation.proxy.Logger; @@ -79,13 +78,13 @@ void listTargetsForSecurityProfile_WithNextToken_VerifyPagination() { } @Test - void listTargetsForSecurityProfile_ApiThrowsException_VerifyTranslation() { + void listTargetsForSecurityProfile_ApiThrowsException_BubbleUp() { when(proxy.injectCredentialsAndInvokeV2(any(), any())) .thenThrow(ThrottlingException.builder().build()); assertThatThrownBy(() -> HandlerUtils.listTargetsForSecurityProfile( iotClient, proxy, SECURITY_PROFILE_NAME)) - .isInstanceOf(CfnThrottlingException.class); + .isInstanceOf(ThrottlingException.class); } @Test @@ -116,7 +115,7 @@ public void listTags_WithNextToken_VerifyPagination() { assertThat(actualResponse).isEqualTo(TAGS_IOT); } - @Test +// @Test void listTags_ApiThrowsException_VerifyTranslation() { when(proxy.injectCredentialsAndInvokeV2(any(), any())) diff --git a/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/ListHandlerTest.java b/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/ListHandlerTest.java index 327f1aa..8916632 100644 --- a/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/ListHandlerTest.java +++ b/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/ListHandlerTest.java @@ -18,6 +18,7 @@ import software.amazon.awssdk.services.iot.model.UnauthorizedException; import software.amazon.cloudformation.exceptions.CfnAccessDeniedException; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.HandlerErrorCode; import software.amazon.cloudformation.proxy.Logger; import software.amazon.cloudformation.proxy.OperationStatus; import software.amazon.cloudformation.proxy.ProgressEvent; @@ -96,7 +97,8 @@ public void handleRequest_ApiThrowsException_VerifyTranslation() { when(proxy.injectCredentialsAndInvokeV2(any(), any())) .thenThrow(UnauthorizedException.builder().build()); - assertThatThrownBy(() -> handler.handleRequest(proxy, request, null, logger)) - .isInstanceOf(CfnAccessDeniedException.class); + ProgressEvent progressEvent = + handler.handleRequest(proxy, request, null, logger); + assertThat(progressEvent.getErrorCode()).isEqualTo(HandlerErrorCode.AccessDenied); } } diff --git a/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/ReadHandlerTest.java b/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/ReadHandlerTest.java index 678df06..2d3a5c5 100644 --- a/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/ReadHandlerTest.java +++ b/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/ReadHandlerTest.java @@ -13,7 +13,6 @@ import static com.amazonaws.iot.securityprofile.TestConstants.TAG_1_IOT_SET; import static com.amazonaws.iot.securityprofile.TestConstants.TARGET_ARN_1_SET; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; @@ -31,8 +30,8 @@ import software.amazon.awssdk.services.iot.model.DescribeSecurityProfileRequest; import software.amazon.awssdk.services.iot.model.DescribeSecurityProfileResponse; import software.amazon.awssdk.services.iot.model.ThrottlingException; -import software.amazon.cloudformation.exceptions.CfnThrottlingException; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.HandlerErrorCode; import software.amazon.cloudformation.proxy.Logger; import software.amazon.cloudformation.proxy.OperationStatus; import software.amazon.cloudformation.proxy.ProgressEvent; @@ -156,7 +155,8 @@ public void handleRequest_DescribeThrowsException_VerifyTranslation() { when(proxy.injectCredentialsAndInvokeV2(any(), any())) .thenThrow(ThrottlingException.builder().build()); - assertThatThrownBy(() -> handler.handleRequest(proxy, request, null, logger)) - .isInstanceOf(CfnThrottlingException.class); + ProgressEvent progressEvent = + handler.handleRequest(proxy, request, null, logger); + assertThat(progressEvent.getErrorCode()).isEqualTo(HandlerErrorCode.Throttling); } } diff --git a/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/UpdateHandlerTest.java b/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/UpdateHandlerTest.java index dbf51e4..06101cd 100644 --- a/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/UpdateHandlerTest.java +++ b/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/UpdateHandlerTest.java @@ -53,9 +53,6 @@ import software.amazon.awssdk.services.iot.model.UntagResourceRequest; import software.amazon.awssdk.services.iot.model.UpdateSecurityProfileRequest; import software.amazon.awssdk.services.iot.model.UpdateSecurityProfileResponse; -import software.amazon.cloudformation.exceptions.CfnInternalFailureException; -import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; -import software.amazon.cloudformation.exceptions.CfnNotFoundException; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; import software.amazon.cloudformation.proxy.HandlerErrorCode; import software.amazon.cloudformation.proxy.Logger; @@ -321,13 +318,13 @@ public void updateSecurityProfile_ResourceNotFound_VerifyTranslation() { when(proxy.injectCredentialsAndInvokeV2(any(), any())) .thenThrow(ResourceNotFoundException.builder().build()); - assertThatThrownBy(() -> - handler.handleRequest(proxy, request, null, logger)) - .isInstanceOf(CfnNotFoundException.class); + ProgressEvent progressEvent = + handler.handleRequest(proxy, request, null, logger); + assertThat(progressEvent.getErrorCode()).isEqualTo(HandlerErrorCode.NotFound); } @Test - public void updateTags_ApiThrowsException_VerifyTranslation() { + public void updateTags_ApiThrowsException_BubbleUp() { ResourceHandlerRequest request = ResourceHandlerRequest.builder() .previousResourceState(ResourceModel.builder().build()) @@ -340,11 +337,11 @@ public void updateTags_ApiThrowsException_VerifyTranslation() { assertThatThrownBy(() -> handler.updateTags(proxy, request, SECURITY_PROFILE_ARN, logger)) - .isInstanceOf(CfnInvalidRequestException.class); + .isInstanceOf(InvalidRequestException.class); } @Test - public void updateTargetAttachments_AttachThrowsException_VerifyTranslation() { + public void updateTargetAttachments_AttachThrowsException_BubbleUp() { Set previousTargets = ImmutableSet.of("keepTarget", "detachTarget1", "detachTarget2"); doReturn(previousTargets) @@ -362,7 +359,7 @@ public void updateTargetAttachments_AttachThrowsException_VerifyTranslation() { assertThatThrownBy(() -> handler.updateTargetAttachments(proxy, model, logger)) - .isInstanceOf(CfnInternalFailureException.class); + .isInstanceOf(InternalFailureException.class); } @Test From f386181373b311d91300c6166a50c9052c93c6b3 Mon Sep 17 00:00:00 2001 From: Anton Kuznetsov Date: Tue, 15 Dec 2020 10:03:18 -0800 Subject: [PATCH 7/8] Fix accidentally commented out test --- .../com/amazonaws/iot/securityprofile/HandlerUtilsTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/HandlerUtilsTest.java b/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/HandlerUtilsTest.java index 48faad4..a51609d 100644 --- a/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/HandlerUtilsTest.java +++ b/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/HandlerUtilsTest.java @@ -115,13 +115,13 @@ public void listTags_WithNextToken_VerifyPagination() { assertThat(actualResponse).isEqualTo(TAGS_IOT); } -// @Test - void listTags_ApiThrowsException_VerifyTranslation() { + @Test + void listTags_ApiThrowsException_BubbleUp() { when(proxy.injectCredentialsAndInvokeV2(any(), any())) .thenThrow(LimitExceededException.builder().build()); assertThatThrownBy(() -> HandlerUtils.listTags( iotClient, proxy, SECURITY_PROFILE_ARN)) - .isInstanceOf(CfnServiceLimitExceededException.class); + .isInstanceOf(LimitExceededException.class); } } From 279a5beb9f12eef9edf027670ab90c1d4f313bb0 Mon Sep 17 00:00:00 2001 From: Anton Kuznetsov Date: Tue, 15 Dec 2020 17:46:18 -0800 Subject: [PATCH 8/8] Add minor improvements --- .../java/com/amazonaws/iot/securityprofile/HandlerUtils.java | 3 +-- .../java/com/amazonaws/iot/securityprofile/ListHandler.java | 1 - .../java/com/amazonaws/iot/securityprofile/Translator.java | 2 +- .../com/amazonaws/iot/securityprofile/HandlerUtilsTest.java | 1 - .../com/amazonaws/iot/securityprofile/ListHandlerTest.java | 2 -- 5 files changed, 2 insertions(+), 7 deletions(-) diff --git a/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/HandlerUtils.java b/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/HandlerUtils.java index e09cd64..4683a4e 100644 --- a/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/HandlerUtils.java +++ b/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/HandlerUtils.java @@ -5,7 +5,6 @@ import java.util.Set; import software.amazon.awssdk.services.iot.IotClient; -import software.amazon.awssdk.services.iot.model.IotException; import software.amazon.awssdk.services.iot.model.ListTagsForResourceRequest; import software.amazon.awssdk.services.iot.model.ListTagsForResourceResponse; import software.amazon.awssdk.services.iot.model.ListTargetsForSecurityProfileRequest; @@ -51,7 +50,7 @@ static Set listTags( .nextToken(nextToken) .build(); ListTagsForResourceResponse listTagsForResourceResponse = proxy.injectCredentialsAndInvokeV2( - listTagsRequest, iotClient::listTagsForResource); + listTagsRequest, iotClient::listTagsForResource); result.addAll(listTagsForResourceResponse.tags()); nextToken = listTagsForResourceResponse.nextToken(); } while (nextToken != null); diff --git a/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/ListHandler.java b/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/ListHandler.java index 370fa85..82a9e8b 100644 --- a/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/ListHandler.java +++ b/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/ListHandler.java @@ -4,7 +4,6 @@ import java.util.stream.Collectors; import software.amazon.awssdk.services.iot.IotClient; -import software.amazon.awssdk.services.iot.model.IotException; import software.amazon.awssdk.services.iot.model.ListSecurityProfilesRequest; import software.amazon.awssdk.services.iot.model.ListSecurityProfilesResponse; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; diff --git a/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/Translator.java b/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/Translator.java index 9d210de..bd286f8 100644 --- a/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/Translator.java +++ b/aws-iot-securityprofile/src/main/java/com/amazonaws/iot/securityprofile/Translator.java @@ -305,7 +305,7 @@ static ProgressEvent translateExceptionToProgres return progressEvent; } - static HandlerErrorCode translateExceptionToProgressEvent(Exception e, Logger logger) { + private static HandlerErrorCode translateExceptionToProgressEvent(Exception e, Logger logger) { logger.log(String.format("Translating exception \"%s\", stack trace: %s", e.getMessage(), ExceptionUtils.getStackTrace(e))); diff --git a/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/HandlerUtilsTest.java b/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/HandlerUtilsTest.java index a51609d..3e650e6 100644 --- a/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/HandlerUtilsTest.java +++ b/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/HandlerUtilsTest.java @@ -27,7 +27,6 @@ import software.amazon.awssdk.services.iot.model.ListTargetsForSecurityProfileRequest; import software.amazon.awssdk.services.iot.model.ListTargetsForSecurityProfileResponse; import software.amazon.awssdk.services.iot.model.ThrottlingException; -import software.amazon.cloudformation.exceptions.CfnServiceLimitExceededException; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; import software.amazon.cloudformation.proxy.Logger; diff --git a/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/ListHandlerTest.java b/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/ListHandlerTest.java index 8916632..f1fc292 100644 --- a/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/ListHandlerTest.java +++ b/aws-iot-securityprofile/src/test/java/com/amazonaws/iot/securityprofile/ListHandlerTest.java @@ -1,7 +1,6 @@ package com.amazonaws.iot.securityprofile; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; @@ -16,7 +15,6 @@ import software.amazon.awssdk.services.iot.model.ListSecurityProfilesRequest; import software.amazon.awssdk.services.iot.model.ListSecurityProfilesResponse; import software.amazon.awssdk.services.iot.model.UnauthorizedException; -import software.amazon.cloudformation.exceptions.CfnAccessDeniedException; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; import software.amazon.cloudformation.proxy.HandlerErrorCode; import software.amazon.cloudformation.proxy.Logger;