diff --git a/packages/@aws-cdk/aws-wafv2/README.md b/packages/@aws-cdk/aws-wafv2/README.md index 9405ace0f26a3..e36334d4a4e02 100644 --- a/packages/@aws-cdk/aws-wafv2/README.md +++ b/packages/@aws-cdk/aws-wafv2/README.md @@ -9,23 +9,43 @@ > > [CFN Resources]: https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib +![cdk-constructs: Experimental](https://img.shields.io/badge/cdk--constructs-experimental-important.svg?style=for-the-badge) + +> The APIs of higher level constructs in this module are experimental and under active development. +> They are subject to non-backward compatible changes or removal in any future version. These are +> not subject to the [Semantic Versioning](https://semver.org/) model and breaking changes will be +> announced in the release notes. This means that while you may use them, you may need to update +> your source code when upgrading to a newer version of this package. + --- -This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. +## Installation -```ts nofixture -import * as wafv2 from '@aws-cdk/aws-wafv2'; +Install the module: + +```console +$ npm i @aws-cdk/aws-wafv2 ``` - +Import it into your code: -There are no hand-written ([L2](https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib)) constructs for this service yet. -However, you can still use the automatically generated [L1](https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_l1_using) constructs, and use this service exactly as you would using CloudFormation directly. +```ts +import * as wafv2 from '@aws-cdk/aws-wafv2'; +``` + +## `WebAcl` -For more information on the resources and properties available for this service, see the [CloudFormation documentation for AWS::WAFv2](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/AWS_WAFv2.html). +Create a web ACL that gives you fine-grained control over all of the HTTP(S) web requests that your protected resource responds to. +You can create a web ACL as following: -(Read the [CDK Contributing Guide](https://github.com/aws/aws-cdk/blob/master/CONTRIBUTING.md) if you are interested in contributing to this construct library.) +```ts +import * as wafv2 from '@aws-cdk/aws-wafv2'; - +new wafv2.WebAcl(this, 'WebAcl', { + webAclName: 'my-web-acl-name', // OPTIONAL + scope: wafv2.Scope.CLOUDFRONT, + defaultAction: wafv2.DefaultAction.block(), +}); +``` diff --git a/packages/@aws-cdk/aws-wafv2/lib/default-action.ts b/packages/@aws-cdk/aws-wafv2/lib/default-action.ts new file mode 100644 index 0000000000000..603d08d326f13 --- /dev/null +++ b/packages/@aws-cdk/aws-wafv2/lib/default-action.ts @@ -0,0 +1,56 @@ +import { Construct } from 'constructs'; +import { CfnWebACL } from './wafv2.generated'; + +/** + * The type returned from the `bind()` method in {@link DefaultAction}. + */ +export interface DefaultActionConfig { + /** + * The configuration for this default action. + */ + readonly configuration: CfnWebACL.DefaultActionProperty; +} + +/** + * The action to perform if none of the Rules contained in the WebACL match. + */ +export abstract class DefaultAction { + /** + * Specifies that AWS WAF should allow requests by default. + */ + public static allow(): DefaultAction { + return new AllowAction(); + } + + /** + * Specifies that AWS WAF should block requests by default. + */ + public static block(): DefaultAction { + return new BlockAction(); + } + + /** + * Returns the DefaultAction configuration. + */ + public abstract bind(scope: Construct): DefaultActionConfig +} + +class AllowAction extends DefaultAction { + bind(_scope: Construct) { + return { + configuration: { + allow: {}, + }, + }; + } +}; + +class BlockAction extends DefaultAction { + bind(_scope: Construct) { + return { + configuration: { + block: {}, + }, + }; + } +}; diff --git a/packages/@aws-cdk/aws-wafv2/lib/index.ts b/packages/@aws-cdk/aws-wafv2/lib/index.ts index dc251f24ea749..20efb153299c7 100644 --- a/packages/@aws-cdk/aws-wafv2/lib/index.ts +++ b/packages/@aws-cdk/aws-wafv2/lib/index.ts @@ -1,2 +1,5 @@ +export * from './default-action'; +export * from './web-acl'; + // AWS::WAFv2 CloudFormation Resources: export * from './wafv2.generated'; diff --git a/packages/@aws-cdk/aws-wafv2/lib/web-acl.ts b/packages/@aws-cdk/aws-wafv2/lib/web-acl.ts new file mode 100644 index 0000000000000..cb620ea3408eb --- /dev/null +++ b/packages/@aws-cdk/aws-wafv2/lib/web-acl.ts @@ -0,0 +1,104 @@ +import { Resource, Names } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { DefaultAction } from './default-action'; +import { CfnWebACL } from './wafv2.generated'; + +/** + * Specifies whether this is for an Amazon CloudFront distribution or for a regional application. + * A regional application can be an Application Load Balancer (ALB), an Amazon API Gateway REST API, + * or an AWS AppSync GraphQL API. + */ +export enum Scope { + /** + * For regional application + */ + REGIONAL = 'REGIONAL', + + /** + * For Amazon CloudFront distribution + */ + CLOUDFRONT = 'CLOUDFRONT', +} + +/** + * Properties for defining an AWS WAF web ACL + */ +export interface WebAclProps { + /** + * The descriptive name of the web ACL. You cannot change the name of a web ACL after you create it. + * @default None + */ + readonly webAclName?: string; + + /** + * Specifies whether this is for an Amazon CloudFront distribution or for a regional application. + */ + readonly scope: Scope; + + /** + * The action to perform if none of the Rules contained in the WebACL match. + */ + readonly defaultAction: DefaultAction; +} + +/** + * Defines an AWS WAF web ACL in this stack. + */ +export class WebAcl extends Resource { + /** + * Name of this web ACL rule + * @attribute + */ + public readonly webAclName: string; + + /** + * The Amazon Resource Name (ARN) of the web ACL. + * @attribute + */ + public readonly webAclArn: string; + + /** + * The current web ACL capacity (WCU) usage by the web ACL. + * @attribute + */ + public readonly webAclCapacity: number; + + /** + * The ID of the web ACL. + * @attribute + */ + public readonly webAclId: string; + + /** + * The label namespace prefix for this web ACL. All labels added by rules in this web ACL have this prefix. + * @attribute + */ + public readonly webAclLabelNamespace: string; + + constructor(scope: Construct, id: string, props: WebAclProps) { + super(scope, id, { + physicalName: props.webAclName, + }); + + const resource = new CfnWebACL(this, 'Resource', { + name: this.physicalName, + scope: props.scope, + defaultAction: props.defaultAction.bind(this).configuration, + visibilityConfig: { + cloudWatchMetricsEnabled: true, + metricName: props.webAclName || Names.uniqueId(this), + sampledRequestsEnabled: true, + }, + }); + + this.webAclName = this.getResourceNameAttribute(resource.ref); + this.webAclArn = this.getResourceArnAttribute(resource.attrArn, { + service: 'wafv2', + resource: 'webacl', + resourceName: this.physicalName, + }); + this.webAclCapacity = resource.attrCapacity; + this.webAclId = resource.attrId; + this.webAclLabelNamespace = resource.attrLabelNamespace; + } +} diff --git a/packages/@aws-cdk/aws-wafv2/package.json b/packages/@aws-cdk/aws-wafv2/package.json index 3f6fa8ac9d3ff..7a28153c1133b 100644 --- a/packages/@aws-cdk/aws-wafv2/package.json +++ b/packages/@aws-cdk/aws-wafv2/package.json @@ -83,9 +83,11 @@ "devDependencies": { "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", + "@aws-cdk/cdk-integ-tools": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.4.1" + "@types/jest": "^27.4.1", + "jest": "^27.5.1" }, "dependencies": { "@aws-cdk/core": "0.0.0", @@ -99,7 +101,7 @@ "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", - "maturity": "cfn-only", + "maturity": "experimental", "awscdkio": { "announce": false }, diff --git a/packages/@aws-cdk/aws-wafv2/test/integ.web-acl.expected.json b/packages/@aws-cdk/aws-wafv2/test/integ.web-acl.expected.json new file mode 100644 index 0000000000000..737b5b27c915e --- /dev/null +++ b/packages/@aws-cdk/aws-wafv2/test/integ.web-acl.expected.json @@ -0,0 +1,19 @@ +{ + "Resources": { + "WebAclE76B067C": { + "Type": "AWS::WAFv2::WebACL", + "Properties": { + "DefaultAction": { + "Block": {} + }, + "Scope": "CLOUDFRONT", + "VisibilityConfig": { + "CloudWatchMetricsEnabled": true, + "MetricName": "my-web-acl-name", + "SampledRequestsEnabled": true + }, + "Name": "my-web-acl-name" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-wafv2/test/integ.web-acl.ts b/packages/@aws-cdk/aws-wafv2/test/integ.web-acl.ts new file mode 100644 index 0000000000000..b0a26f2f5ecdb --- /dev/null +++ b/packages/@aws-cdk/aws-wafv2/test/integ.web-acl.ts @@ -0,0 +1,18 @@ +import * as cdk from '@aws-cdk/core'; +import * as wafv2 from '../lib'; + +class TestStack extends cdk.Stack { + constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + new wafv2.WebAcl(this, 'WebAcl', { + webAclName: 'my-web-acl-name', + scope: wafv2.Scope.CLOUDFRONT, + defaultAction: wafv2.DefaultAction.block(), + }); + } +} + +const app = new cdk.App(); +new TestStack(app, 'web-acl-integ-stack'); +app.synth(); diff --git a/packages/@aws-cdk/aws-wafv2/test/wafv2.test.ts b/packages/@aws-cdk/aws-wafv2/test/wafv2.test.ts deleted file mode 100644 index 465c7bdea0693..0000000000000 --- a/packages/@aws-cdk/aws-wafv2/test/wafv2.test.ts +++ /dev/null @@ -1,6 +0,0 @@ -import '@aws-cdk/assertions'; -import {} from '../lib'; - -test('No tests are specified for this package', () => { - expect(true).toBe(true); -}); diff --git a/packages/@aws-cdk/aws-wafv2/test/web-acl.test.ts b/packages/@aws-cdk/aws-wafv2/test/web-acl.test.ts new file mode 100644 index 0000000000000..772a47b8b64f9 --- /dev/null +++ b/packages/@aws-cdk/aws-wafv2/test/web-acl.test.ts @@ -0,0 +1,128 @@ +import { Template, Match } from '@aws-cdk/assertions'; +import * as cdk from '@aws-cdk/core'; +import * as wafv2 from '../lib'; + +test('Default property', () => { + const stack = new cdk.Stack(); + + // WHEN + new wafv2.WebAcl(stack, 'MyWebAcl', { + scope: wafv2.Scope.REGIONAL, + defaultAction: wafv2.DefaultAction.allow(), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::WAFv2::WebACL', { + DefaultAction: { + Allow: {}, + }, + Scope: 'REGIONAL', + VisibilityConfig: { + CloudWatchMetricsEnabled: true, + MetricName: 'MyWebAcl', + SampledRequestsEnabled: true, + }, + }); +}); + +test('can get web ACL name', () => { + const stack = new cdk.Stack(); + // GIVEN + const webAcl = new wafv2.WebAcl(stack, 'MyWebAcl', { + scope: wafv2.Scope.REGIONAL, + defaultAction: wafv2.DefaultAction.allow(), + }); + + // WHEN + new cdk.CfnResource(stack, 'Res', { + type: 'Test::Resource', + properties: { + WebAclName: webAcl.webAclName, + }, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('Test::Resource', { + WebAclName: { Ref: 'MyWebAcl2C5E4DE6' }, + }); +}); + +test.each([ + ['Arn', 'webAclArn'], + ['Capacity', 'webAclCapacity'], + ['Id', 'webAclId'], + ['LabelNamespace', 'webAclLabelNamespace'], +] as const)('can get web ACL %s', (attrName, propName) => { + const stack = new cdk.Stack(); + // GIVEN + const webAcl = new wafv2.WebAcl(stack, 'MyWebAcl', { + scope: wafv2.Scope.REGIONAL, + defaultAction: wafv2.DefaultAction.allow(), + }); + + // WHEN + new cdk.CfnResource(stack, 'Res', { + type: 'Test::Resource', + properties: { + [`WebAcl${attrName}`]: webAcl[propName], + }, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('Test::Resource', { + [`WebAcl${attrName}`]: { + 'Fn::GetAtt': ['MyWebAcl2C5E4DE6', attrName], + }, + }); +}); + +test('can set scope', () => { + const stack = new cdk.Stack(); + + // WHEN + new wafv2.WebAcl(stack, 'MyWebAcl', { + scope: wafv2.Scope.CLOUDFRONT, + defaultAction: wafv2.DefaultAction.allow(), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::WAFv2::WebACL', { + Scope: 'CLOUDFRONT', + }); +}); + +test('can set defaultAction', () => { + const stack = new cdk.Stack(); + + // WHEN + new wafv2.WebAcl(stack, 'MyWebAcl', { + scope: wafv2.Scope.REGIONAL, + defaultAction: wafv2.DefaultAction.block(), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::WAFv2::WebACL', { + DefaultAction: { + Block: {}, + }, + }); +}); + +test('can set physical name', () => { + const stack = new cdk.Stack(); + + // WHEN + new wafv2.WebAcl(stack, 'MyWebAcl', { + webAclName: 'test-WebAcl', + scope: wafv2.Scope.REGIONAL, + defaultAction: wafv2.DefaultAction.allow(), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::WAFv2::WebACL', { + Name: 'test-WebAcl', + VisibilityConfig: Match.objectLike({ + MetricName: 'test-WebAcl', + }), + }); +});