diff --git a/hooks/S3_AccessControl/.gitignore b/hooks/S3_AccessControl/.gitignore new file mode 100644 index 00000000..929637fb --- /dev/null +++ b/hooks/S3_AccessControl/.gitignore @@ -0,0 +1,29 @@ +# 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/ + +# tests-related +.hypothesis/ + +# generated archive +*.zip diff --git a/hooks/S3_AccessControl/.rpdk-config b/hooks/S3_AccessControl/.rpdk-config new file mode 100644 index 00000000..13f09df7 --- /dev/null +++ b/hooks/S3_AccessControl/.rpdk-config @@ -0,0 +1,29 @@ +{ + "artifact_type": "HOOK", + "typeName": "AwsCommunity::S3::AccessControl", + "language": "java", + "runtime": "java8", + "entrypoint": "com.awscommunity.s3.accesscontrol.HookHandlerWrapper::handleRequest", + "testEntrypoint": "com.awscommunity.s3.accesscontrol.HookHandlerWrapper::testEntrypoint", + "settings": { + "version": false, + "subparser_name": null, + "verbose": 0, + "force": false, + "type_name": null, + "artifact_type": null, + "endpoint_url": null, + "region": null, + "target_schemas": [], + "profile": null, + "namespace": [ + "com", + "awscommunity", + "s3", + "accesscontrol" + ], + "codegen_template_path": "guided_aws", + "protocolVersion": "2.0.0" + }, + "executableEntrypoint": "com.awscommunity.s3.accesscontrol.HookHandlerWrapperExecutable" +} diff --git a/hooks/S3_AccessControl/README.md b/hooks/S3_AccessControl/README.md new file mode 100644 index 00000000..0ed325ef --- /dev/null +++ b/hooks/S3_AccessControl/README.md @@ -0,0 +1,147 @@ +# AwsCommunity::S3::AccessControl + + +- [Overview](#Overview) +- [Usage](#Usage) +- [Example templates](#example-templates) +- [Tests](#Tests) + - [Unit tests](#Unit-tests) + - [Contract tests](#Contract-tests) +- [Hook development notes](#Hook-development-notes) + + +## Overview +This hook for [AWS CloudFormation](https://aws.amazon.com/cloudformation/) validates that the legacy `AccessControl` [property](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-s3-bucket.html#cfn-s3-bucket-accesscontrol) for an `AWS::S3::Bucket` [resource](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-s3-bucket.html) is either set to `Private`, or is not present. + + +## Usage +This hook is written in [Kotlin](https://kotlinlang.org/). To build it on your machine, install [Apache Maven](https://maven.apache.org/install.html), and a JDK (8, or 11). For more information, see [Prerequisites for developing hooks](https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/hooks-walkthrough-java.html#prerequisites-developing-hooks-java). + +Next, you'll need to install the [CloudFormation CLI](https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/what-is-cloudformation-cli.html), that you'll use to run contract tests for this hook, and to [submit](https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/resource-type-cli-submit.html) this hook to the CloudFormation registry, as a private extension, in the AWS account and Region(s) of your choice. + +The following example shows how to build and submit the hook to the registry, in the `us-east-1` region for the account you use: + +```shell +cfn generate && mvn clean verify && cfn submit --set-default --region us-east-1 +``` + +After you submit the hook to the registry, you'll need to configure it. One of the ways of doing this is to first create a `type_config.json` file as shown next (for more information on `Properties` defined for this hook, see [Configuration options](#Configuration-options)): + +```shell +cat < type_config.json +{ + "CloudFormationConfiguration": { + "HookConfiguration": { + "TargetStacks": "ALL", + "FailureMode": "FAIL", + "Properties": { + } + } + } +} +EOF +``` + +and then submit the hook configuration using the [AWS Command Line Interface (AWS CLI)](https://aws.amazon.com/cli/). First, get the [Amazon Resource Name](https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html) (ARN) for this hook, as follows (examples are for the `us-east-1` region): + +```shell +aws cloudformation list-types \ + --type HOOK \ + --filters TypeNamePrefix=AwsCommunity::S3::AccessControl \ + --query 'TypeSummaries[?TypeName==`AwsCommunity::S3::AccessControl`].TypeArn' \ + --output text \ + --region us-east-1 +``` + +Next, find the ARN value string in the output of the command above, and use it with this command by replacing the placeholder text below in upper case characters for `YOUR_HOOK_ARN`: + +```shell +aws cloudformation set-type-configuration \ + --configuration file://type_config.json \ + --type-arn 'YOUR_HOOK_ARN' \ + --region us-east-1 +``` + + +# Example templates + +Templates in this section are marked as: +- _Non-compliant_: this hook will find the given template to be non-compliant, and +- _Compliant_: the template will be found to be compliant, and should deploy successfully. + +Note that you'll also find templates called `integ-succeed.yml` and `integ-fail.yml` in the `test` directory, that can be used to create stacks for integration testing. + +Non-compliant: the `AccessControl` [property](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-s3-bucket.html#cfn-s3-bucket-accesscontrol) for the `AWS::S3::Bucket` [resource](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-s3-bucket.html) is present in the template, and with a value other than `Private`: + +```yaml +AWSTemplateFormatVersion: "2010-09-09" + +Description: Test-only template that describes an Amazon S3 bucket for integration tests. + +Resources: + S3Bucket: + Type: AWS::S3::Bucket + Properties: + AccessControl: PublicRead +``` + +Compliant, example 1: `AccessControl` is present in the template, and with a value of `Private`: + +```yaml +AWSTemplateFormatVersion: "2010-09-09" + +Description: Test-only template that describes an Amazon S3 bucket for integration tests. + +Resources: + S3Bucket: + Type: AWS::S3::Bucket + Properties: + AccessControl: Private +``` + +Compliant, example 2: `AccessControl` is not present in the template: + +```yaml +AWSTemplateFormatVersion: "2010-09-09" + +Description: Test-only template that describes an Amazon S3 bucket for integration tests. + +Resources: + S3Bucket: + Type: AWS::S3::Bucket +``` + +## Tests + + +### Unit tests +Run unit tests, and verify code coverage with: + +```shell +mvn clean verify +``` + + +### Contract tests +Contract tests help you validate hooks you develop work as expected, and are required to pass when you submit a CloudFormation extension such as a hook to the public registry. It is recommended to strive to pass contract tests also when you write a private extension, to help discover potential issues. + +To run contract tests, open a new terminal window and run, from the root directory of this project: + +```shell +sam local start-lambda +``` + +For more information, see [Testing resource types locally using AWS SAM](https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/resource-type-test.html#resource-type-develop-test). + +Open a another terminal window, build this hook, and run contract tests: + +```shell +cfn generate && mvn clean verify && cfn test -v --enforce-timeout 90 +``` + +## Hook development notes +When you build this hook's project via Maven, the correct hook input model from the schema will be automatically generated. You can also do this manually with the following command: `cfn generate`. + +> Please don't modify files under `target/generated-sources/rpdk`, as they will be automatically overwritten. + +The generated code uses [Lombok](https://projectlombok.org/) to annotate Java classes, including classes in the `target/generated-sources/rpdk` path mentioned above. This hook -that is written in Kotlin- needs to consume such Lombok-annotated Java classes: one way to do this is to use [Delombok](https://projectlombok.org/features/delombok) to delombok relevant source files. The `pom.xml` file, for this `AwsCommunity::S3::AccessControl` hook, uses the `delombok` goal for the [lombok.maven plugin](https://github.com/awhitford/lombok.maven) to delombok, during the `process-sources` phase, the `target/generated-sources/rpdk` generated classes into the `target/generated-sources/delombok` target directory that, in turn, is then added as a source with the `add-source` goal for the `build-helper-maven-plugin`. diff --git a/hooks/S3_AccessControl/awscommunity-s3-accesscontrol.json b/hooks/S3_AccessControl/awscommunity-s3-accesscontrol.json new file mode 100644 index 00000000..84fe44fa --- /dev/null +++ b/hooks/S3_AccessControl/awscommunity-s3-accesscontrol.json @@ -0,0 +1,27 @@ +{ + "typeName": "AwsCommunity::S3::AccessControl", + "description": "This hook for AWS CloudFormation validates that the legacy AccessControl property for an AWS::S3::Bucket resource is either set to Private, or is not present.", + "sourceUrl": "https://github.com/aws-cloudformation/community-registry-extensions/tree/main/hooks/S3_AccessControl", + "documentationUrl": "https://github.com/aws-cloudformation/community-registry-extensions/blob/main/hooks/S3_AccessControl/README.md", + "typeConfiguration": { + "properties": { + }, + "additionalProperties": false + }, + "required": [], + "handlers": { + "preCreate": { + "targetNames": [ + "AWS::S3::Bucket" + ], + "permissions": [] + }, + "preUpdate": { + "targetNames": [ + "AWS::S3::Bucket" + ], + "permissions": [] + } + }, + "additionalProperties": false +} diff --git a/hooks/S3_AccessControl/docs/README.md b/hooks/S3_AccessControl/docs/README.md new file mode 100644 index 00000000..16df8429 --- /dev/null +++ b/hooks/S3_AccessControl/docs/README.md @@ -0,0 +1,33 @@ +# AwsCommunity::S3::AccessControl + +## Activation + +To activate a hook in your account, use the following JSON as the `Configuration` request parameter for [`SetTypeConfiguration`](https://docs.aws.amazon.com/AWSCloudFormation/latest/APIReference/API_SetTypeConfiguration.html) API request. + +### Configuration + +
+{
+    "CloudFormationConfiguration": {
+        "HookConfiguration": {
+            "TargetStacks":  "ALL" | "NONE",
+            "FailureMode": "FAIL" | "WARN" ,
+            "Properties" : {
+            }
+        }
+    }
+}
+
+ +--- + +## Targets + +* `AWS::S3::Bucket` + +--- + +

Please note that the enum values for +TargetStacks and FailureMode +might go out of date, please refer to their official documentation page for up-to-date values.

+ diff --git a/hooks/S3_AccessControl/hook-role-prod.yaml b/hooks/S3_AccessControl/hook-role-prod.yaml new file mode 100644 index 00000000..9494ba48 --- /dev/null +++ b/hooks/S3_AccessControl/hook-role-prod.yaml @@ -0,0 +1,40 @@ +AWSTemplateFormatVersion: "2010-09-09" +Description: > + This CloudFormation template creates a role assumed by CloudFormation + during Hook operations on behalf of the customer. + +Resources: + ExecutionRole: + Type: AWS::IAM::Role + Properties: + MaxSessionDuration: 8400 + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: + - hooks.cloudformation.amazonaws.com + - resources.cloudformation.amazonaws.com + Action: sts:AssumeRole + Condition: + StringEquals: + aws:SourceAccount: + Ref: AWS::AccountId + StringLike: + aws:SourceArn: + Fn::Sub: arn:${AWS::Partition}:cloudformation:${AWS::Region}:${AWS::AccountId}:type/hook/AwsCommunity-S3-AccessControl/* + Path: "/" + Policies: + - PolicyName: HookTypePolicy + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Deny + Action: + - "*" + Resource: "*" +Outputs: + ExecutionRoleArn: + Value: + Fn::GetAtt: ExecutionRole.Arn diff --git a/hooks/S3_AccessControl/hook-role.yaml b/hooks/S3_AccessControl/hook-role.yaml new file mode 100644 index 00000000..9494ba48 --- /dev/null +++ b/hooks/S3_AccessControl/hook-role.yaml @@ -0,0 +1,40 @@ +AWSTemplateFormatVersion: "2010-09-09" +Description: > + This CloudFormation template creates a role assumed by CloudFormation + during Hook operations on behalf of the customer. + +Resources: + ExecutionRole: + Type: AWS::IAM::Role + Properties: + MaxSessionDuration: 8400 + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: + - hooks.cloudformation.amazonaws.com + - resources.cloudformation.amazonaws.com + Action: sts:AssumeRole + Condition: + StringEquals: + aws:SourceAccount: + Ref: AWS::AccountId + StringLike: + aws:SourceArn: + Fn::Sub: arn:${AWS::Partition}:cloudformation:${AWS::Region}:${AWS::AccountId}:type/hook/AwsCommunity-S3-AccessControl/* + Path: "/" + Policies: + - PolicyName: HookTypePolicy + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Deny + Action: + - "*" + Resource: "*" +Outputs: + ExecutionRoleArn: + Value: + Fn::GetAtt: ExecutionRole.Arn diff --git a/hooks/S3_AccessControl/inputs/inputs_1_invalid.json b/hooks/S3_AccessControl/inputs/inputs_1_invalid.json new file mode 100644 index 00000000..a7ea60dc --- /dev/null +++ b/hooks/S3_AccessControl/inputs/inputs_1_invalid.json @@ -0,0 +1,7 @@ +{ + "AWS::S3::Bucket": { + "resourceProperties": { + "AccessControl": "PublicRead" + } + } +} diff --git a/hooks/S3_AccessControl/inputs/inputs_1_pre_create.json b/hooks/S3_AccessControl/inputs/inputs_1_pre_create.json new file mode 100644 index 00000000..9aa690cb --- /dev/null +++ b/hooks/S3_AccessControl/inputs/inputs_1_pre_create.json @@ -0,0 +1,5 @@ +{ + "AWS::S3::Bucket": { + "resourceProperties": {} + } +} diff --git a/hooks/S3_AccessControl/inputs/inputs_1_pre_update.json b/hooks/S3_AccessControl/inputs/inputs_1_pre_update.json new file mode 100644 index 00000000..fa77c73c --- /dev/null +++ b/hooks/S3_AccessControl/inputs/inputs_1_pre_update.json @@ -0,0 +1,7 @@ +{ + "AWS::S3::Bucket": { + "resourceProperties": { + "AccessControl": "Private" + } + } +} diff --git a/hooks/S3_AccessControl/lombok.config b/hooks/S3_AccessControl/lombok.config new file mode 100644 index 00000000..7a21e880 --- /dev/null +++ b/hooks/S3_AccessControl/lombok.config @@ -0,0 +1 @@ +lombok.addLombokGeneratedAnnotation = true diff --git a/hooks/S3_AccessControl/pom.xml b/hooks/S3_AccessControl/pom.xml new file mode 100644 index 00000000..be097b85 --- /dev/null +++ b/hooks/S3_AccessControl/pom.xml @@ -0,0 +1,379 @@ + + + 4.0.0 + + com.awscommunity.s3.accesscontrol + awscommunity-s3-accesscontrol-handler + 1.0-SNAPSHOT + jar + awscommunity-s3-accesscontrol-handler + + + 1.9.10 + 1.8 + 1.8 + UTF-8 + UTF-8 + + + + + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + ${kotlin.version} + + + + software.amazon.cloudformation + aws-cloudformation-rpdk-java-plugin + [2.0.0,3.0.0) + + + + org.projectlombok + lombok + 1.18.24 + provided + + + + org.apache.logging.log4j + log4j-api + 2.17.2 + + + + org.apache.logging.log4j + log4j-core + 2.17.2 + + + + org.apache.logging.log4j + log4j-slf4j-impl + 2.17.2 + + + + + org.jetbrains.kotlin + kotlin-test-junit + ${kotlin.version} + test + + + + org.assertj + assertj-core + 3.12.2 + test + + + + org.junit.jupiter + junit-jupiter + 5.8.2 + test + + + + io.mockk + mockk + 1.11.0 + test + + + + + + + ${project.basedir} + + awscommunity-s3-accesscontrol.json + + + + ${project.basedir}/target/loaded-target-schemas + + **/*.json + + + + + + com.diffplug.spotless + spotless-maven-plugin + 2.28.0 + + + + + + + true + + + *.md + *.json + *.xml + *.yaml + *.yml + .gitignore + + + + + + + true + 4 + + + + + 4 + + + pom.xml + + + + + + check + + check + + validate + + + + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + compile + + compile + + compile + + + src/main/kotlin + + + + + test-compile + + test-compile + + test-compile + + + src/test/kotlin + + + + + + + org.projectlombok + lombok-maven-plugin + 1.18.12.0 + + + + delombok + + process-sources + + ${project.basedir}/target/generated-sources/rpdk + ${project.basedir}/target/generated-sources/delombok + + + + + + org.codehaus.mojo + build-helper-maven-plugin + 3.4.0 + + + add-source + + add-source + + process-sources + + + ${project.basedir}/target/generated-sources/delombok + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + + -Xlint:all,-options,-processing + -Werror + + + + + default-compile + none + + + default-testCompile + none + + + java-compile + + compile + + compile + + + java-test-compile + + testCompile + + test-compile + + + + + org.jetbrains.dokka + dokka-maven-plugin + 1.9.0 + + + ${project.basedir}/src/main/kotlin + + + + + + dokka + + pre-site + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.4 + + false + + + *:* + + **/Log4j2Plugins.dat + + + + + + + + shade + + package + + + + + org.codehaus.mojo + exec-maven-plugin + 1.6.0 + + + generate + + exec + + generate-sources + + cfn + generate ${cfn.generate.args} + ${project.basedir} + + + + + + org.apache.maven.plugins + maven-resources-plugin + 2.4 + + + maven-surefire-plugin + 3.1.2 + + + org.jacoco + jacoco-maven-plugin + 0.8.10 + + + **/model/** + **/BaseHookConfiguration* + **/HookHandlerWrapper* + **/Configuration* + + + + + + prepare-agent + + + + report + + report + + test + + + jacoco-check + + check + + + + + PACKAGE + + + BRANCH + COVEREDRATIO + 1 + + + INSTRUCTION + COVEREDRATIO + 1 + + + + + + + + + + + diff --git a/hooks/S3_AccessControl/requirements-dev.txt b/hooks/S3_AccessControl/requirements-dev.txt new file mode 100644 index 00000000..847a4f2f --- /dev/null +++ b/hooks/S3_AccessControl/requirements-dev.txt @@ -0,0 +1,4 @@ +cloudformation-cli>=0.2.33 +cloudformation-cli-java-plugin>=2.0.14 +cloudformation-cli-python-lib>=2.1.15 +pytest>=7.2.0 diff --git a/hooks/S3_AccessControl/requirements.txt b/hooks/S3_AccessControl/requirements.txt new file mode 100644 index 00000000..14af4a8b --- /dev/null +++ b/hooks/S3_AccessControl/requirements.txt @@ -0,0 +1,2 @@ +cloudformation-cli-python-lib>=2.1.15 +pytest>=7.2.0 diff --git a/hooks/S3_AccessControl/src/main/kotlin/com/awscommunity/s3/accesscontrol/BaseHookHandlerStd.kt b/hooks/S3_AccessControl/src/main/kotlin/com/awscommunity/s3/accesscontrol/BaseHookHandlerStd.kt new file mode 100644 index 00000000..e14bc4cd --- /dev/null +++ b/hooks/S3_AccessControl/src/main/kotlin/com/awscommunity/s3/accesscontrol/BaseHookHandlerStd.kt @@ -0,0 +1,50 @@ +package com.awscommunity.s3.accesscontrol + +import com.awscommunity.s3.accesscontrol.model.aws.s3.bucket.AwsS3BucketTargetModel +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.hook.HookHandlerRequest +import software.amazon.cloudformation.proxy.hook.targetmodel.HookTargetModel + +/** + * This class is used by both pre-create and pre-update handlers for this hook, + * and it contains common business logic for validation of target(s). + */ +abstract class BaseHookHandlerStd : BaseHookHandler() { + + /** + * Common entry point for pre-create and pre-update validation operations. + * + * @param proxy AmazonWebServicesClientProxy + * @param request HookHandlerRequest + * @param callbackContext CallbackContext + * @param logger Logger + * @param typeConfiguration TypeConfigurationModel + */ + fun handlePreCreatePreUpdateRequests( + proxy: AmazonWebServicesClientProxy, + request: HookHandlerRequest, + callbackContext: CallbackContext?, + logger: Logger, + typeConfiguration: TypeConfigurationModel + ): ProgressEvent { + val targetModel = request.hookContext.getTargetModel(AwsS3BucketTargetModel::class.java) + val resourceProperties = targetModel.resourceProperties + val accessControl: String? = resourceProperties.accessControl + if (accessControl != null && accessControl != "Private") { + return ProgressEvent.builder() + .status(OperationStatus.FAILED) + .errorCode(HandlerErrorCode.NonCompliant) + .message("The legacy AccessControl property is present in the resource's configuration, and it is not set to Private. Remove the legacy AccessControl property, or set it to Private.") + .build() + } + + return ProgressEvent.builder() + .status(OperationStatus.SUCCESS) + .message("The legacy AccessControl property is either not present in the resource's configuration, or it is set to Private.") + .build() + } +} diff --git a/hooks/S3_AccessControl/src/main/kotlin/com/awscommunity/s3/accesscontrol/CallbackContext.kt b/hooks/S3_AccessControl/src/main/kotlin/com/awscommunity/s3/accesscontrol/CallbackContext.kt new file mode 100644 index 00000000..0d832a7d --- /dev/null +++ b/hooks/S3_AccessControl/src/main/kotlin/com/awscommunity/s3/accesscontrol/CallbackContext.kt @@ -0,0 +1,27 @@ +package com.awscommunity.s3.accesscontrol + +import software.amazon.cloudformation.proxy.StdCallbackContext + +/** + * Callback context class, that uses a POJO model (that is, a `data` + * class in Kotlin). If needed, this class can be used to persist, + * across invocations for a given handler (such as, PreCreate for + * example) information you need, so that you can consume this + * information from your handler code by using a `ProgressEvent` object, + * to which you pass an `OperationStatus.IN_PROGRESS` status and an + * instance of this CallbackContext class with information (properties + * and values) you need. + * + * This hook's scope does not use this functionality at this time; if + * this were not to be the case, you could have prepended `data` to the + * class declaration below and you'd have added properties, that you need to + * persist across invocations, to `CallbackContext`; you could have then + * been able to access and set values for such properties from within a given + * handler's (e.g., a PreCreate handler) business logic. + * + * As an example, you could have declared this class as follows to set the + * `example` property in the callback context: + * + * data class CallbackContext(val example: String?) : StdCallbackContext() + */ +class CallbackContext : StdCallbackContext() diff --git a/hooks/S3_AccessControl/src/main/kotlin/com/awscommunity/s3/accesscontrol/Configuration.kt b/hooks/S3_AccessControl/src/main/kotlin/com/awscommunity/s3/accesscontrol/Configuration.kt new file mode 100644 index 00000000..d9a2a9fc --- /dev/null +++ b/hooks/S3_AccessControl/src/main/kotlin/com/awscommunity/s3/accesscontrol/Configuration.kt @@ -0,0 +1,6 @@ +package com.awscommunity.s3.accesscontrol + +/** + * Hook configuration class. + */ +class Configuration : BaseHookConfiguration("awscommunity-s3-accesscontrol.json") diff --git a/hooks/S3_AccessControl/src/main/kotlin/com/awscommunity/s3/accesscontrol/PreCreateHookHandler.kt b/hooks/S3_AccessControl/src/main/kotlin/com/awscommunity/s3/accesscontrol/PreCreateHookHandler.kt new file mode 100644 index 00000000..0ad485c7 --- /dev/null +++ b/hooks/S3_AccessControl/src/main/kotlin/com/awscommunity/s3/accesscontrol/PreCreateHookHandler.kt @@ -0,0 +1,32 @@ +package com.awscommunity.s3.accesscontrol + +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy +import software.amazon.cloudformation.proxy.Logger +import software.amazon.cloudformation.proxy.ProgressEvent +import software.amazon.cloudformation.proxy.hook.HookHandlerRequest +import software.amazon.cloudformation.proxy.hook.targetmodel.HookTargetModel + +/** + * Class for validation operations on pre-create. + */ +open class PreCreateHookHandler : BaseHookHandlerStd() { + + /** + * Function for pre-create validation operations. + * + * @param proxy AmazonWebServicesClientProxy + * @param request HookHandlerRequest + * @param callbackContext CallbackContext + * @param logger Logger + * @param typeConfiguration TypeConfigurationModel + */ + override fun handleRequest( + proxy: AmazonWebServicesClientProxy, + request: HookHandlerRequest, + callbackContext: CallbackContext?, + logger: Logger, + typeConfiguration: TypeConfigurationModel + ): ProgressEvent { + return handlePreCreatePreUpdateRequests(proxy, request, callbackContext, logger, typeConfiguration) + } +} diff --git a/hooks/S3_AccessControl/src/main/kotlin/com/awscommunity/s3/accesscontrol/PreUpdateHookHandler.kt b/hooks/S3_AccessControl/src/main/kotlin/com/awscommunity/s3/accesscontrol/PreUpdateHookHandler.kt new file mode 100644 index 00000000..7da1f514 --- /dev/null +++ b/hooks/S3_AccessControl/src/main/kotlin/com/awscommunity/s3/accesscontrol/PreUpdateHookHandler.kt @@ -0,0 +1,32 @@ +package com.awscommunity.s3.accesscontrol + +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy +import software.amazon.cloudformation.proxy.Logger +import software.amazon.cloudformation.proxy.ProgressEvent +import software.amazon.cloudformation.proxy.hook.HookHandlerRequest +import software.amazon.cloudformation.proxy.hook.targetmodel.HookTargetModel + +/** + * Class for validation operations on pre-update. + */ +open class PreUpdateHookHandler : BaseHookHandlerStd() { + + /** + * Function for pre-update validation operations. + * + * @param proxy AmazonWebServicesClientProxy + * @param request HookHandlerRequest + * @param callbackContext CallbackContext + * @param logger Logger + * @param typeConfiguration TypeConfigurationModel + */ + override fun handleRequest( + proxy: AmazonWebServicesClientProxy, + request: HookHandlerRequest, + callbackContext: CallbackContext?, + logger: Logger, + typeConfiguration: TypeConfigurationModel + ): ProgressEvent { + return handlePreCreatePreUpdateRequests(proxy, request, callbackContext, logger, typeConfiguration) + } +} diff --git a/hooks/S3_AccessControl/src/resources/log4j2.xml b/hooks/S3_AccessControl/src/resources/log4j2.xml new file mode 100644 index 00000000..5657dafe --- /dev/null +++ b/hooks/S3_AccessControl/src/resources/log4j2.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/hooks/S3_AccessControl/src/test/kotlin/com/awscommunity/s3/accesscontrol/AbstractTestBase.kt b/hooks/S3_AccessControl/src/test/kotlin/com/awscommunity/s3/accesscontrol/AbstractTestBase.kt new file mode 100644 index 00000000..5ce14297 --- /dev/null +++ b/hooks/S3_AccessControl/src/test/kotlin/com/awscommunity/s3/accesscontrol/AbstractTestBase.kt @@ -0,0 +1,72 @@ +package com.awscommunity.s3.accesscontrol + +import io.mockk.impl.annotations.MockK +import org.assertj.core.api.Assertions.assertThat +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy +import software.amazon.cloudformation.proxy.HandlerErrorCode +import software.amazon.cloudformation.proxy.OperationStatus +import software.amazon.cloudformation.proxy.ProgressEvent +import software.amazon.cloudformation.proxy.hook.HookContext +import software.amazon.cloudformation.proxy.hook.HookHandlerRequest +import software.amazon.cloudformation.proxy.hook.targetmodel.HookTargetModel + +abstract class AbstractTestBase { + + @MockK + protected lateinit var proxy: AmazonWebServicesClientProxy + + protected fun getMockTargetModelWithoutAccessControl(): MutableMap { + val targetModel: MutableMap = HashMap() + val resourceProperties: MutableMap = HashMap() + targetModel["resourceProperties"] = resourceProperties + return targetModel + } + + protected fun getMockTargetModelWithAccessControlSetToPrivate(): MutableMap { + val targetModel: MutableMap = HashMap() + val resourceProperties: MutableMap = HashMap() + resourceProperties["AccessControl"] = "Private" + targetModel["resourceProperties"] = resourceProperties + return targetModel + } + + protected fun getMockTargetModelWithAccessControlSetToPublicRead(): MutableMap { + val targetModel: MutableMap = HashMap() + val resourceProperties: MutableMap = HashMap() + resourceProperties["AccessControl"] = "PublicRead" + targetModel["resourceProperties"] = resourceProperties + return targetModel + } + + protected fun getMockHookHandlerRequest(targetName: String, targetLogicalId: String, targetModel: MutableMap): HookHandlerRequest { + return HookHandlerRequest.builder().hookContext( + HookContext.builder().targetName(targetName) + .targetLogicalId(targetLogicalId).targetModel(HookTargetModel.of(targetModel)).build() + ) + .build() + } + + protected fun testNonCompliant( + response: ProgressEvent, + message: String + ) { + assertThat(response).isNotNull() + assertThat(response.message).isEqualTo(message) + assertThat(response.status).isEqualTo(OperationStatus.FAILED) + assertThat(response.errorCode).isEqualTo(HandlerErrorCode.NonCompliant) + assertThat(response.callbackContext).isNull() + assertThat(response.callbackDelaySeconds).isEqualTo(0) + } + + protected fun testCompliant( + response: ProgressEvent, + message: String + ) { + assertThat(response).isNotNull() + assertThat(response.message).isEqualTo(message) + assertThat(response.status).isEqualTo(OperationStatus.SUCCESS) + assertThat(response.errorCode).isNull() + assertThat(response.callbackContext).isNull() + assertThat(response.callbackDelaySeconds).isEqualTo(0) + } +} diff --git a/hooks/S3_AccessControl/src/test/kotlin/com/awscommunity/s3/accesscontrol/PreCreateHookHandlerTest.kt b/hooks/S3_AccessControl/src/test/kotlin/com/awscommunity/s3/accesscontrol/PreCreateHookHandlerTest.kt new file mode 100644 index 00000000..c08040b5 --- /dev/null +++ b/hooks/S3_AccessControl/src/test/kotlin/com/awscommunity/s3/accesscontrol/PreCreateHookHandlerTest.kt @@ -0,0 +1,42 @@ +package com.awscommunity.s3.accesscontrol + +import io.mockk.impl.annotations.MockK +import io.mockk.junit5.MockKExtension +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import software.amazon.cloudformation.proxy.Logger + +@ExtendWith(MockKExtension::class) +class PreCreateHookHandlerTest : PreCreatePreUpdateHookHandlerCommonTests() { + + private var handler: BaseHookHandlerStd = PreCreateHookHandler() + + @MockK + lateinit var logger: Logger + + @BeforeEach + fun setup() { + handler = PreCreateHookHandler() + } + + @Test + fun testCallbackContextIsInstanceOfStdCallbackContext() { + callbackContextIsInstanceOfStdCallbackContext() + } + + @Test + fun testAnS3BucketWithoutTheAccessControlPropertyShouldSucceed() { + anS3BucketWithoutTheAccessControlPropertyShouldSucceed(proxy, logger, handler) + } + + @Test + fun testAnS3BucketWithTheAccessControlPropertySetToPrivateShouldSucceed() { + anS3BucketWithTheAccessControlPropertySetToPrivateShouldSucceed(proxy, logger, handler) + } + + @Test + fun testAnS3BucketWithTheAccessControlPropertySetToAValueDifferentThanPrivateShouldFail() { + anS3BucketWithTheAccessControlPropertySetToAValueDifferentThanPrivateShouldFail(proxy, logger, handler) + } +} diff --git a/hooks/S3_AccessControl/src/test/kotlin/com/awscommunity/s3/accesscontrol/PreCreatePreUpdateHookHandlerCommonTests.kt b/hooks/S3_AccessControl/src/test/kotlin/com/awscommunity/s3/accesscontrol/PreCreatePreUpdateHookHandlerCommonTests.kt new file mode 100644 index 00000000..c14786f6 --- /dev/null +++ b/hooks/S3_AccessControl/src/test/kotlin/com/awscommunity/s3/accesscontrol/PreCreatePreUpdateHookHandlerCommonTests.kt @@ -0,0 +1,82 @@ +package com.awscommunity.s3.accesscontrol + +import io.mockk.mockk +import org.assertj.core.api.Assertions.assertThat +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy +import software.amazon.cloudformation.proxy.Logger +import software.amazon.cloudformation.proxy.ProgressEvent +import software.amazon.cloudformation.proxy.StdCallbackContext +import software.amazon.cloudformation.proxy.hook.HookHandlerRequest +import software.amazon.cloudformation.proxy.hook.targetmodel.HookTargetModel + +open class PreCreatePreUpdateHookHandlerCommonTests : AbstractTestBase() { + + protected fun callbackContextIsInstanceOfStdCallbackContext() { + val callbackContext = CallbackContext() + assertThat(callbackContext).isInstanceOf(CallbackContext::class.java) + assertThat(callbackContext).isInstanceOf(StdCallbackContext::class.java) + } + + protected fun anS3BucketWithoutTheAccessControlPropertyShouldSucceed( + proxy: AmazonWebServicesClientProxy, + logger: Logger, + handler: BaseHookHandlerStd + ) { + val typeConfiguration: TypeConfigurationModel = mockk() + val targetModel: MutableMap = getMockTargetModelWithoutAccessControl() + val request: HookHandlerRequest = getMockHookHandlerRequest("AWS::S3::Bucket", "Example", targetModel) + val response: ProgressEvent = handler.handleRequest( + proxy, + request, + null, + logger, + typeConfiguration + ) + testCompliant( + response, + "The legacy AccessControl property is either not present in the resource's configuration, or it is set to Private." + ) + } + + protected fun anS3BucketWithTheAccessControlPropertySetToPrivateShouldSucceed( + proxy: AmazonWebServicesClientProxy, + logger: Logger, + handler: BaseHookHandlerStd + ) { + val typeConfiguration: TypeConfigurationModel = mockk() + val targetModel: MutableMap = getMockTargetModelWithAccessControlSetToPrivate() + val request: HookHandlerRequest = getMockHookHandlerRequest("AWS::S3::Bucket", "Example", targetModel) + val response: ProgressEvent = handler.handleRequest( + proxy, + request, + null, + logger, + typeConfiguration + ) + testCompliant( + response, + "The legacy AccessControl property is either not present in the resource's configuration, or it is set to Private." + ) + } + + protected fun anS3BucketWithTheAccessControlPropertySetToAValueDifferentThanPrivateShouldFail( + proxy: AmazonWebServicesClientProxy, + logger: Logger, + handler: BaseHookHandlerStd + ) { + val typeConfiguration: TypeConfigurationModel = mockk() + val targetModel: MutableMap = getMockTargetModelWithAccessControlSetToPublicRead() + val request: HookHandlerRequest = getMockHookHandlerRequest("AWS::S3::Bucket", "Example", targetModel) + val response: ProgressEvent = handler.handleRequest( + proxy, + request, + null, + logger, + typeConfiguration + ) + testNonCompliant( + response, + "The legacy AccessControl property is present in the resource's configuration, and it is not set to Private. Remove the legacy AccessControl property, or set it to Private." + ) + } +} diff --git a/hooks/S3_AccessControl/src/test/kotlin/com/awscommunity/s3/accesscontrol/PreUpdateHookHandlerTest.kt b/hooks/S3_AccessControl/src/test/kotlin/com/awscommunity/s3/accesscontrol/PreUpdateHookHandlerTest.kt new file mode 100644 index 00000000..07650de2 --- /dev/null +++ b/hooks/S3_AccessControl/src/test/kotlin/com/awscommunity/s3/accesscontrol/PreUpdateHookHandlerTest.kt @@ -0,0 +1,42 @@ +package com.awscommunity.s3.accesscontrol + +import io.mockk.impl.annotations.MockK +import io.mockk.junit5.MockKExtension +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import software.amazon.cloudformation.proxy.Logger + +@ExtendWith(MockKExtension::class) +class PreUpdateHookHandlerTest : PreCreatePreUpdateHookHandlerCommonTests() { + + private var handler: BaseHookHandlerStd = PreUpdateHookHandler() + + @MockK + lateinit var logger: Logger + + @BeforeEach + fun setup() { + handler = PreUpdateHookHandler() + } + + @Test + fun testCallbackContextIsInstanceOfStdCallbackContext() { + callbackContextIsInstanceOfStdCallbackContext() + } + + @Test + fun testAnS3BucketWithoutTheAccessControlPropertyShouldSucceed() { + anS3BucketWithoutTheAccessControlPropertyShouldSucceed(proxy, logger, handler) + } + + @Test + fun testAnS3BucketWithTheAccessControlPropertySetToPrivateShouldSucceed() { + anS3BucketWithTheAccessControlPropertySetToPrivateShouldSucceed(proxy, logger, handler) + } + + @Test + fun testAnS3BucketWithTheAccessControlPropertySetToAValueDifferentThanPrivateShouldFail() { + anS3BucketWithTheAccessControlPropertySetToAValueDifferentThanPrivateShouldFail(proxy, logger, handler) + } +} diff --git a/hooks/S3_AccessControl/template.yml b/hooks/S3_AccessControl/template.yml new file mode 100644 index 00000000..9e50403a --- /dev/null +++ b/hooks/S3_AccessControl/template.yml @@ -0,0 +1,23 @@ +AWSTemplateFormatVersion: "2010-09-09" +Transform: AWS::Serverless-2016-10-31 +Description: AWS SAM template for the AwsCommunity::S3::AccessControl resource type + +Globals: + Function: + Timeout: 180 # docker start-up times can be long for SAM CLI + MemorySize: 512 + +Resources: + TypeFunction: + Type: AWS::Serverless::Function + Properties: + Handler: com.awscommunity.s3.accesscontrol.HookHandlerWrapper::handleRequest + Runtime: java8 + CodeUri: ./target/awscommunity-s3-accesscontrol-handler-1.0-SNAPSHOT.jar + + TestEntrypoint: + Type: AWS::Serverless::Function + Properties: + Handler: com.awscommunity.s3.accesscontrol.HookHandlerWrapper::testEntrypoint + Runtime: java8 + CodeUri: ./target/awscommunity-s3-accesscontrol-handler-1.0-SNAPSHOT.jar diff --git a/hooks/S3_AccessControl/test/integ-fail.yml b/hooks/S3_AccessControl/test/integ-fail.yml new file mode 100644 index 00000000..3c208c1b --- /dev/null +++ b/hooks/S3_AccessControl/test/integ-fail.yml @@ -0,0 +1,9 @@ +AWSTemplateFormatVersion: "2010-09-09" + +Description: Test-only template that describes an Amazon S3 bucket for integration tests. + +Resources: + S3Bucket: + Type: AWS::S3::Bucket + Properties: + AccessControl: PublicRead diff --git a/hooks/S3_AccessControl/test/integ-succeed.yml b/hooks/S3_AccessControl/test/integ-succeed.yml new file mode 100644 index 00000000..7472d4fe --- /dev/null +++ b/hooks/S3_AccessControl/test/integ-succeed.yml @@ -0,0 +1,7 @@ +AWSTemplateFormatVersion: "2010-09-09" + +Description: Test-only template that describes an Amazon S3 bucket for integration tests. + +Resources: + S3Bucket: + Type: AWS::S3::Bucket diff --git a/release/awscommunity/cicd.yml b/release/awscommunity/cicd.yml index 594a82f2..c8752ccb 100644 --- a/release/awscommunity/cicd.yml +++ b/release/awscommunity/cicd.yml @@ -114,6 +114,19 @@ Resources: ManagedPolicyArns: - Fn::ImportValue: !Sub "cep-${Env}-common-build-project-policy" + S3AccessControlBuildProjectRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Action: sts:AssumeRole + Effect: Allow + Principal: + Service: codebuild.amazonaws.com + Version: '2012-10-17' + ManagedPolicyArns: + - Fn::ImportValue: !Sub "cep-${Env}-common-build-project-policy" + HookEC2SecurityGroupRestrictedSSHBuildProjectRole: Type: AWS::IAM::Role Properties: @@ -826,7 +839,7 @@ Resources: - iam:UpdateRoleDescription Resource: !Sub 'arn:${AWS::Partition}:iam::${AWS::AccountId}:role/TrailS3Cleanup-integ-*-awscommunity-kms-encryptionsettings' - Effect: Allow - Action: + Action: - iam:CreateServiceLinkedRole - iam:DeleteServiceLinkedRole - iam:GetServiceLinkedRoleDeletionStatus @@ -836,6 +849,20 @@ Resources: Roles: - !Ref KMSEncryptionSettingsBuildProjectRole + S3AccessControlBuildProjectPolicy: + Type: AWS::IAM::Policy + Properties: + PolicyDocument: + Statement: + - Effect: Allow + Action: + - s3:* + Resource: "*" + Version: '2012-10-17' + PolicyName: s3-accesscontrol-build-project-policy + Roles: + - !Ref S3AccessControlBuildProjectRole + ApplicationAutoscalingScheduledActionBuildProjectRolePolicy: Type: AWS::IAM::Policy Properties: @@ -1292,6 +1319,28 @@ Resources: BuildSpec: !Sub "hooks/${Env}-buildspec-java.yml" TimeoutInMinutes: 480 + S3AccessControlBuildProject: + Type: AWS::CodeBuild::Project + Properties: + Name: !Sub "${PrefixLower}-${Env}-s3-accesscontrol" + Artifacts: + Type: CODEPIPELINE + Environment: + ComputeType: BUILD_GENERAL1_LARGE + Image: !Sub "${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/cep-cicd:latest" + ImagePullCredentialsType: SERVICE_ROLE + PrivilegedMode: true + Type: LINUX_CONTAINER + EnvironmentVariables: + - Name: HOOK_PATH + Type: PLAINTEXT + Value: "placeholder-for-path-to-hook" + ServiceRole: !GetAtt S3AccessControlBuildProjectRole.Arn + Source: + Type: CODEPIPELINE + BuildSpec: !Sub "hooks/${Env}-buildspec-java.yml" + TimeoutInMinutes: 480 + ApplicationAutoscalingScheduledActionBuildProject: Type: AWS::CodeBuild::Project Properties: @@ -1392,6 +1441,7 @@ Resources: - !GetAtt CloudFrontS3WebsiteModuleBuildProject.Arn - !GetAtt AlternateContactBuildProject.Arn - !GetAtt KMSEncryptionSettingsBuildProject.Arn + - !GetAtt S3AccessControlBuildProject.Arn - !GetAtt ApplicationAutoscalingScheduledActionBuildProject.Arn - Action: - kms:* @@ -1462,6 +1512,7 @@ Resources: - !GetAtt S3BucketModuleBuildProjectRole.Arn - !GetAtt CloudFrontS3WebsiteModuleBuildProjectRole.Arn - !GetAtt KMSEncryptionSettingsBuildProjectRole.Arn + - !GetAtt S3AccessControlBuildProjectRole.Arn - !GetAtt ApplicationAutoscalingScheduledActionBuildProjectRole.Arn Resource: "*" MultiRegion: true @@ -1777,6 +1828,25 @@ Resources: } ] RunOrder: 5 + - Name: S3AccessControl + InputArtifacts: + - Name: extensions-source + ActionTypeId: + Category: Build + Owner: AWS + Provider: CodeBuild + Version: 1 + Configuration: + ProjectName: !Ref S3AccessControlBuildProject + EnvironmentVariables: |- + [ + { + "name": "HOOK_PATH", + "type": "PLAINTEXT", + "value": "hooks/S3_AccessControl" + } + ] + RunOrder: 4 - Name: ApplicationAutoscalingScheduledAction InputArtifacts: - Name: extensions-source