diff --git a/codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/AddBuiltinPlugins.java b/codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/AddBuiltinPlugins.java index ac8872a81b826..9e7cb91db642c 100644 --- a/codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/AddBuiltinPlugins.java +++ b/codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/AddBuiltinPlugins.java @@ -41,6 +41,13 @@ public class AddBuiltinPlugins implements TypeScriptIntegration { private static final Set ROUTE_53_ID_MEMBERS = SetUtils.of("DelegationSetId", "HostedZoneId", "Id"); + private static final Set RDS_PRESIGNED_URL_OPERATIONS = SetUtils.of( + "CopyDBSnapshot", + "CreateDBInstanceReadReplica", + "CreateDBCluster", + "CopyDBClusterSnapshot" + ); + private static final Set S3_MD5_OPERATIONS = SetUtils.of( "DeleteObjects", "PutBucketCors", @@ -145,6 +152,12 @@ && testServiceId(s, "S3")) .operationPredicate((m, s, o) -> o.getId().getName().equals("ChangeResourceRecordSets") && testServiceId(s, "Route 53")) .build(), + RuntimeClientPlugin.builder() + .withConventions(AwsDependency.RDS_MIDDLEWARE.dependency, "CrossRegionPresignedUrl", + HAS_MIDDLEWARE) + .operationPredicate((m, s, o) -> RDS_PRESIGNED_URL_OPERATIONS.contains(o.getId().getName()) + && testServiceId(s, "RDS")) + .build(), RuntimeClientPlugin.builder() .withConventions(AwsDependency.ROUTE53_MIDDLEWARE.dependency, "IdNormalizer", HAS_MIDDLEWARE) diff --git a/codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/AwsDependency.java b/codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/AwsDependency.java index a4710b86012d5..1033596530060 100644 --- a/codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/AwsDependency.java +++ b/codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/AwsDependency.java @@ -39,6 +39,7 @@ public enum AwsDependency implements SymbolDependencyContainer { MACHINELEARNING_MIDDLEWARE(NORMAL_DEPENDENCY, "@aws-sdk/middleware-sdk-machinelearning", "^1.0.0-alpha.0"), S3_CONTROL_MIDDLEWARE(NORMAL_DEPENDENCY, "@aws-sdk/middleware-sdk-s3-control", "^1.0.0-alpha.0"), SSEC_MIDDLEWARE(NORMAL_DEPENDENCY, "@aws-sdk/middleware-ssec", "^1.0.0-alpha.0"), + RDS_MIDDLEWARE(NORMAL_DEPENDENCY, "@aws-sdk/middleware-sdk-rds", "^1.0.0-alpha.0"), LOCATION_CONSTRAINT(NORMAL_DEPENDENCY, "@aws-sdk/middleware-location-constraint", "^1.0.0-alpha.0"), MD5_BROWSER(NORMAL_DEPENDENCY, "@aws-sdk/md5-js", "^1.0.0-alpha.0"), STREAM_HASHER_NODE(NORMAL_DEPENDENCY, "@aws-sdk/hash-stream-node", "^1.0.0-alpha.0"), diff --git a/packages/middleware-sdk-rds/.gitignore b/packages/middleware-sdk-rds/.gitignore new file mode 100644 index 0000000000000..3d1714c9806ef --- /dev/null +++ b/packages/middleware-sdk-rds/.gitignore @@ -0,0 +1,8 @@ +/node_modules/ +/build/ +/coverage/ +/docs/ +*.tsbuildinfo +*.tgz +*.log +package-lock.json diff --git a/packages/middleware-sdk-rds/.npmignore b/packages/middleware-sdk-rds/.npmignore new file mode 100644 index 0000000000000..4b9fe3abf33a6 --- /dev/null +++ b/packages/middleware-sdk-rds/.npmignore @@ -0,0 +1,13 @@ +/src/ +/coverage/ +/docs/ +tsconfig.test.json +*.tsbuildinfo + +*.spec.js +*.spec.d.ts +*.spec.js.map + +*.fixture.js +*.fixture.d.ts +*.fixture.js.map diff --git a/packages/middleware-sdk-rds/CHANGELOG.md b/packages/middleware-sdk-rds/CHANGELOG.md new file mode 100644 index 0000000000000..ffc74e87e7470 --- /dev/null +++ b/packages/middleware-sdk-rds/CHANGELOG.md @@ -0,0 +1,82 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [0.1.0-preview.7](https://github.com/aws/aws-sdk-js-v3/compare/@aws-sdk/middleware-rds-presignedurl@0.1.0-preview.3...@aws-sdk/middleware-rds-presignedurl@0.1.0-preview.7) (2019-09-19) + + + +# 0.3.0 (2019-09-09) + + +### Features + +* commit all clients ([#324](https://github.com/aws/aws-sdk-js-v3/issues/324)) ([cb268ed](https://github.com/aws/aws-sdk-js-v3/commit/cb268ed)) + + + +# 0.2.0 (2019-07-12) + + +### Features + +* add npm badges for individual packages ([#251](https://github.com/aws/aws-sdk-js-v3/issues/251)) ([8adc10c](https://github.com/aws/aws-sdk-js-v3/commit/8adc10c)) +* update jest v20 to v24 ([#243](https://github.com/aws/aws-sdk-js-v3/issues/243)) ([1e156ab](https://github.com/aws/aws-sdk-js-v3/commit/1e156ab)) + + + +# 0.1.0 (2019-04-19) + + + + + +# [0.1.0-preview.6](https://github.com/aws/aws-sdk-js-v3/compare/@aws-sdk/middleware-rds-presignedurl@0.1.0-preview.3...@aws-sdk/middleware-rds-presignedurl@0.1.0-preview.6) (2019-09-09) + + +### Features + +* commit all clients ([#324](https://github.com/aws/aws-sdk-js-v3/issues/324)) ([cb268ed](https://github.com/aws/aws-sdk-js-v3/commit/cb268ed)) + + + +# 0.2.0 (2019-07-12) + + +### Features + +* add npm badges for individual packages ([#251](https://github.com/aws/aws-sdk-js-v3/issues/251)) ([8adc10c](https://github.com/aws/aws-sdk-js-v3/commit/8adc10c)) +* update jest v20 to v24 ([#243](https://github.com/aws/aws-sdk-js-v3/issues/243)) ([1e156ab](https://github.com/aws/aws-sdk-js-v3/commit/1e156ab)) + + + +# 0.1.0 (2019-04-19) + + + + + +# [0.1.0-preview.5](https://github.com/aws/aws-sdk-js-v3/compare/@aws-sdk/middleware-rds-presignedurl@0.1.0-preview.3...@aws-sdk/middleware-rds-presignedurl@0.1.0-preview.5) (2019-07-12) + + +### Features + +* add npm badges for individual packages ([#251](https://github.com/aws/aws-sdk-js-v3/issues/251)) ([8adc10c](https://github.com/aws/aws-sdk-js-v3/commit/8adc10c)) +* update jest v20 to v24 ([#243](https://github.com/aws/aws-sdk-js-v3/issues/243)) ([1e156ab](https://github.com/aws/aws-sdk-js-v3/commit/1e156ab)) + + + +# 0.1.0 (2019-04-19) + + + + + +# [0.1.0-preview.4](https://github.com/aws/aws-sdk-js-v3/compare/@aws-sdk/middleware-rds-presignedurl@0.1.0-preview.3...@aws-sdk/middleware-rds-presignedurl@0.1.0-preview.4) (2019-04-19) + +**Note:** Version bump only for package @aws-sdk/middleware-rds-presignedurl + +# [0.1.0-preview.3](https://github.com/aws/aws-sdk-js-v3/compare/@aws-sdk/middleware-rds-presignedurl@0.1.0-preview.2...@aws-sdk/middleware-rds-presignedurl@0.1.0-preview.3) (2019-03-27) + +**Note:** Version bump only for package @aws-sdk/middleware-rds-presignedurl diff --git a/packages/middleware-sdk-rds/LICENSE b/packages/middleware-sdk-rds/LICENSE new file mode 100644 index 0000000000000..b7d2463d8cc07 --- /dev/null +++ b/packages/middleware-sdk-rds/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/middleware-sdk-rds/README.md b/packages/middleware-sdk-rds/README.md new file mode 100644 index 0000000000000..c36da6eff94df --- /dev/null +++ b/packages/middleware-sdk-rds/README.md @@ -0,0 +1,4 @@ +# @aws-sdk/middleware-sdk-rds + +[![NPM version](https://img.shields.io/npm/v/@aws-sdk/middleware-sdk-rds/preview.svg)](https://www.npmjs.com/package/@aws-sdk/middleware-sdk-rds) +[![NPM downloads](https://img.shields.io/npm/dm/@aws-sdk/middleware-sdk-rds.svg)](https://www.npmjs.com/package/@aws-sdk/middleware-sdk-rds) diff --git a/packages/middleware-sdk-rds/jest.config.js b/packages/middleware-sdk-rds/jest.config.js new file mode 100644 index 0000000000000..498ea8304467c --- /dev/null +++ b/packages/middleware-sdk-rds/jest.config.js @@ -0,0 +1,5 @@ +const base = require("../../jest.config.base.js"); + +module.exports = { + ...base +}; diff --git a/packages/middleware-sdk-rds/package.json b/packages/middleware-sdk-rds/package.json new file mode 100644 index 0000000000000..4c45fb2188b2a --- /dev/null +++ b/packages/middleware-sdk-rds/package.json @@ -0,0 +1,28 @@ +{ + "name": "@aws-sdk/middleware-sdk-rds", + "version": "1.0.0-alpha.0", + "scripts": { + "prepublishOnly": "tsc", + "pretest": "tsc -p tsconfig.test.json", + "test": "jest" + }, + "main": "./build/index.js", + "types": "./build/index.d.ts", + "author": { + "name": "AWS SDK for JavaScript Team", + "url": "https://aws.amazon.com/javascript/" + }, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/protocol-http": "^1.0.0-alpha.3", + "@aws-sdk/signature-v4": "^1.0.0-alpha.3", + "@aws-sdk/types": "^1.0.0-alpha.2", + "@aws-sdk/util-format-url": "^1.0.0-alpha.2", + "tslib": "^1.8.0" + }, + "devDependencies": { + "@types/jest": "^24.0.12", + "jest": "^24.7.1", + "typescript": "~3.4.0" + } +} diff --git a/packages/middleware-sdk-rds/src/fixture.ts b/packages/middleware-sdk-rds/src/fixture.ts new file mode 100644 index 0000000000000..502527d1544b9 --- /dev/null +++ b/packages/middleware-sdk-rds/src/fixture.ts @@ -0,0 +1,25 @@ +import { SourceData } from "@aws-sdk/types"; + +export class MockSha256 { + constructor(secret?: string | ArrayBuffer | ArrayBufferView) {} + update(data?: SourceData) {} + digest() { + return Promise.resolve(new Uint8Array(5)); + } +} + +export const region = () => Promise.resolve("mock-region"); + +export const endpoint = () => + Promise.resolve({ + protocol: "https:", + path: "/", + hostname: "ec2.mock-region.amazonaws.com" + }); + +export const credentials = () => + Promise.resolve({ + accessKeyId: "akid", + secretAccessKey: "secret", + sessionToken: "session" + }); diff --git a/packages/middleware-sdk-rds/src/index.spec.ts b/packages/middleware-sdk-rds/src/index.spec.ts new file mode 100644 index 0000000000000..08c5d0f511c90 --- /dev/null +++ b/packages/middleware-sdk-rds/src/index.spec.ts @@ -0,0 +1,198 @@ +import { crossRegionPresignedUrlMiddleware } from "./"; +import { MockSha256, region, credentials, endpoint } from "./fixture"; + +const nextHandler = jest.fn(); +const arn = "arn:aws:rds:src-region:000000000000:src-snapshot:dist-snapshot"; +const arnSameRegion = + "arn:aws:rds:mock-region:000000000000:src-snapshot:dist-snapshot"; +const sourceIdentifier = "src-snapshot"; + +const handler = crossRegionPresignedUrlMiddleware({ + region, + credentials, + endpoint, + sha256: MockSha256, + signingEscapePath: true +})(nextHandler, {} as any); + +describe("middleware-sdk-rds", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("should build CopyDBSnapshot cross origin presigned url correctly ", async () => { + const params = { + SourceDBSnapshotIdentifier: arn, + TargetDBSnapshotIdentifier: "target-id", + KmsKeyId: "000-111" + }; + await handler({ input: params }); + expect(nextHandler.mock.calls.length).toBe(1); + const middlewareOutput = nextHandler.mock.calls[0][0]; + expect(middlewareOutput.input.SourceDBSnapshotIdentifier).toEqual( + params.SourceDBSnapshotIdentifier + ); + expect(middlewareOutput.input.TargetDBSnapshotIdentifier).toEqual( + params.TargetDBSnapshotIdentifier + ); + expect(middlewareOutput.input.KmsKeyId).toEqual(params.KmsKeyId); + const presignedUrl = middlewareOutput.input.PreSignedUrl; + expect(presignedUrl).toMatch( + /https\:\/\/rds\.src\-region\.amazonaws\.com\/\?/ + ); + expect(presignedUrl).toMatch(/Action\=CopyDBSnapshot/); + expect(presignedUrl).toMatch(/Version\=2014\-10\-31/); + expect(presignedUrl).toMatch(/X\-Amz\-Security\-Token\=session/); + expect(presignedUrl).toMatch(/X\-Amz\-Algorithm\=AWS4\-HMAC\-SHA256/); + expect(presignedUrl).toMatch(/X\-Amz\-SignedHeaders\=host/); + expect(presignedUrl).toMatch(/X\-Amz\-Credential\=/); + expect(presignedUrl).toMatch(/X\-Amz\-Date\=/); + expect(presignedUrl).toMatch(/X-Amz-Expires=([\d]+)/); + expect(presignedUrl).toMatch(/X-Amz-Signature=000000/); + }); + + it("should build CreateDBInstanceReadReplica cross origin presigned url correctly ", async () => { + const params = { + SourceDBInstanceIdentifier: arn, + DBInstanceIdentifier: "target-id", + KmsKeyId: "000-111" + }; + await handler({ input: params }); + expect(nextHandler.mock.calls.length).toBe(1); + const middlewareOutput = nextHandler.mock.calls[0][0]; + expect(middlewareOutput.input.SourceDBInstanceIdentifier).toEqual( + params.SourceDBInstanceIdentifier + ); + expect(middlewareOutput.input.DBInstanceIdentifier).toEqual( + params.DBInstanceIdentifier + ); + expect(middlewareOutput.input.KmsKeyId).toEqual(params.KmsKeyId); + const presignedUrl = middlewareOutput.input.PreSignedUrl; + expect(presignedUrl).toMatch( + /https\:\/\/rds\.src\-region\.amazonaws\.com\/\?/ + ); + expect(presignedUrl).toMatch(/Action\=CreateDBInstanceReadReplica/); + expect(presignedUrl).toMatch(/Version\=2014\-10\-31/); + expect(presignedUrl).toMatch(/X\-Amz\-Security\-Token\=session/); + expect(presignedUrl).toMatch(/X\-Amz\-Algorithm\=AWS4\-HMAC\-SHA256/); + expect(presignedUrl).toMatch(/X\-Amz\-SignedHeaders\=host/); + expect(presignedUrl).toMatch(/X\-Amz\-Credential\=/); + expect(presignedUrl).toMatch(/X\-Amz\-Date\=/); + expect(presignedUrl).toMatch(/X-Amz-Expires=([\d]+)/); + expect(presignedUrl).toMatch(/X-Amz-Signature=000000/); + }); + + it("should build CreateDBCluster cross origin presigned url correctly ", async () => { + const params = { + ReplicationSourceIdentifier: arn, + DBClusterIdentifier: "target-id", + KmsKeyId: "000-111" + }; + await handler({ input: params }); + expect(nextHandler.mock.calls.length).toBe(1); + const middlewareOutput = nextHandler.mock.calls[0][0]; + expect(middlewareOutput.input.ReplicationSourceIdentifier).toEqual( + params.ReplicationSourceIdentifier + ); + expect(middlewareOutput.input.DBClusterIdentifier).toEqual( + params.DBClusterIdentifier + ); + expect(middlewareOutput.input.KmsKeyId).toEqual(params.KmsKeyId); + const presignedUrl = middlewareOutput.input.PreSignedUrl; + expect(presignedUrl).toMatch( + /https\:\/\/rds\.src\-region\.amazonaws\.com\/\?/ + ); + expect(presignedUrl).toMatch(/Action\=CreateDBCluster/); + expect(presignedUrl).toMatch(/Version\=2014\-10\-31/); + expect(presignedUrl).toMatch(/X\-Amz\-Security\-Token\=session/); + expect(presignedUrl).toMatch(/X\-Amz\-Algorithm\=AWS4\-HMAC\-SHA256/); + expect(presignedUrl).toMatch(/X\-Amz\-SignedHeaders\=host/); + expect(presignedUrl).toMatch(/X\-Amz\-Credential\=/); + expect(presignedUrl).toMatch(/X\-Amz\-Date\=/); + expect(presignedUrl).toMatch(/X-Amz-Expires=([\d]+)/); + expect(presignedUrl).toMatch(/X-Amz-Signature=000000/); + }); + + it("should build CopyDBClusterSnapshot cross origin presigned url correctly ", async () => { + const params = { + SourceDBClusterSnapshotIdentifier: arn, + TargetDBClusterSnapshotIdentifier: "target-id", + KmsKeyId: "000-111" + }; + await handler({ input: params }); + expect(nextHandler.mock.calls.length).toBe(1); + const middlewareOutput = nextHandler.mock.calls[0][0]; + expect(middlewareOutput.input.SourceDBClusterSnapshotIdentifier).toEqual( + params.SourceDBClusterSnapshotIdentifier + ); + expect(middlewareOutput.input.TargetDBClusterSnapshotIdentifier).toEqual( + params.TargetDBClusterSnapshotIdentifier + ); + expect(middlewareOutput.input.KmsKeyId).toEqual(params.KmsKeyId); + const presignedUrl = middlewareOutput.input.PreSignedUrl; + expect(presignedUrl).toMatch( + /https\:\/\/rds\.src\-region\.amazonaws\.com\/\?/ + ); + expect(presignedUrl).toMatch(/Action\=CopyDBClusterSnapshot/); + expect(presignedUrl).toMatch(/Version\=2014\-10\-31/); + expect(presignedUrl).toMatch(/X\-Amz\-Security\-Token\=session/); + expect(presignedUrl).toMatch(/X\-Amz\-Algorithm\=AWS4\-HMAC\-SHA256/); + expect(presignedUrl).toMatch(/X\-Amz\-SignedHeaders\=host/); + expect(presignedUrl).toMatch(/X\-Amz\-Credential\=/); + expect(presignedUrl).toMatch(/X\-Amz\-Date\=/); + expect(presignedUrl).toMatch(/X-Amz-Expires=([\d]+)/); + expect(presignedUrl).toMatch(/X-Amz-Signature=000000/); + }); + + it("should not generate PreSignedUrl if source identifier is not ARN", async () => { + const params = { + SourceDBClusterSnapshotIdentifier: sourceIdentifier, + TargetDBClusterSnapshotIdentifier: "target-id", + KmsKeyId: "000-111" + }; + await handler({ input: params }); + expect(nextHandler.mock.calls.length).toBe(1); + const middlewareOutput = nextHandler.mock.calls[0][0]; + expect(middlewareOutput.input.SourceDBClusterSnapshotIdentifier).toEqual( + params.SourceDBClusterSnapshotIdentifier + ); + expect(middlewareOutput.input.TargetDBClusterSnapshotIdentifier).toEqual( + params.TargetDBClusterSnapshotIdentifier + ); + expect(middlewareOutput.input.KmsKeyId).toEqual(params.KmsKeyId); + expect(middlewareOutput.input.PreSignedUrl).not.toBeDefined(); + }); + + it("should not generate PreSignedUrl if source identifier is from the same region", async () => { + const params = { + SourceDBClusterSnapshotIdentifier: arnSameRegion, + TargetDBClusterSnapshotIdentifier: "target-id", + KmsKeyId: "000-111" + }; + await handler({ input: params }); + expect(nextHandler.mock.calls.length).toBe(1); + const middlewareOutput = nextHandler.mock.calls[0][0]; + expect(middlewareOutput.input.SourceDBClusterSnapshotIdentifier).toEqual( + params.SourceDBClusterSnapshotIdentifier + ); + expect(middlewareOutput.input.TargetDBClusterSnapshotIdentifier).toEqual( + params.TargetDBClusterSnapshotIdentifier + ); + expect(middlewareOutput.input.KmsKeyId).toEqual(params.KmsKeyId); + expect(middlewareOutput.input.PreSignedUrl).not.toBeDefined(); + }); + + it("should leave input shape unchanged", async () => { + const params = { + SourceDBClusterSnapshotIdentifier: arn, + TargetDBClusterSnapshotIdentifier: "target-id", + KmsKeyId: "000-111" + }; + await handler({ input: params }); + expect(params).toEqual({ + SourceDBClusterSnapshotIdentifier: arn, + TargetDBClusterSnapshotIdentifier: "target-id", + KmsKeyId: "000-111" + }); + }); +}); diff --git a/packages/middleware-sdk-rds/src/index.ts b/packages/middleware-sdk-rds/src/index.ts new file mode 100644 index 0000000000000..66fec28b9752b --- /dev/null +++ b/packages/middleware-sdk-rds/src/index.ts @@ -0,0 +1,147 @@ +import { + Credentials, + DateInput, + Endpoint, + HashConstructor, + InitializeHandler, + InitializeMiddleware, + InitializeHandlerArguments, + InitializeHandlerOptions, + InitializeHandlerOutput, + MetadataBearer, + Pluggable, + Provider +} from "@aws-sdk/types"; +import { formatUrl } from "@aws-sdk/util-format-url"; +import { HttpRequest } from "@aws-sdk/protocol-http"; +import { SignatureV4 } from "@aws-sdk/signature-v4"; + +const regARN = /arn:[\w+=/,.@-]+:[\w+=/,.@-]+:([\w+=/,.@-]*)?:[0-9]+:[\w+=/,.@-]+(:[\w+=/,.@-]+)?(:[\w+=/,.@-]+)?/; + +const sourceIds: string[] = [ + "SourceDBSnapshotIdentifier", + "SourceDBInstanceIdentifier", + "ReplicationSourceIdentifier", + "SourceDBClusterSnapshotIdentifier" +]; + +const sourceIdToCommandKeyMap: { [key: string]: string } = { + SourceDBSnapshotIdentifier: "CopyDBSnapshot", + SourceDBInstanceIdentifier: "CreateDBInstanceReadReplica", + ReplicationSourceIdentifier: "CreateDBCluster", + SourceDBClusterSnapshotIdentifier: "CopyDBClusterSnapshot" +}; + +const version = "2014-10-31"; + +interface PreviouslyResolved { + credentials: Provider; + endpoint: Provider; + region: Provider; + sha256: HashConstructor; + signingEscapePath: boolean; +} + +/** + * Config of the middleware to automatically add presigned URL to request. + * The presigned URL is generated by sigV4 + */ + +export function crossRegionPresignedUrlMiddleware( + options: PreviouslyResolved +): InitializeMiddleware { + return ( + next: InitializeHandler + ): InitializeHandler => async ( + args: InitializeHandlerArguments + ): Promise> => { + const { input } = args; + const region = await options.region(); + let command, sourceId; + for (const id of sourceIds) { + if (input.hasOwnProperty(id)) { + sourceId = id; + command = sourceIdToCommandKeyMap[id]; + } + } + if (!sourceId) { + throw new Error("Source identifier key not set"); + } + if ( + !input.PreSignedUrl && + isARN(input[sourceId]) && + region !== getEndpointFromARN(input[sourceId]) + ) { + const sourceRegion = getEndpointFromARN(input[sourceId]); + const resolvedEndpoint = await options.endpoint(); + resolvedEndpoint.hostname = `rds.${sourceRegion}.amazonaws.com`; + const request = new HttpRequest({ + ...resolvedEndpoint, + protocol: "https", + headers: { + host: resolvedEndpoint.hostname + }, + query: { + Action: command, + Version: version, + KmsKeyId: input.KmsKeyId, + DestinationRegion: region, + [sourceId]: input[sourceId] + } + }); + const signer = new SignatureV4({ + credentials: options.credentials, + region: sourceRegion, + service: "rds", + sha256: options.sha256, + uriEscapePath: options.signingEscapePath + }); + const presignedRequest = await signer.presignRequest( + request, + expirationTime(3600) + ); + args = { + ...args, + input: { + ...args.input, + PreSignedUrl: formatUrl(presignedRequest) + } + }; + } + return next(args); + }; +} + +export const crossRegionPresignedUrlMiddlewareOptions: InitializeHandlerOptions = { + step: "initialize", + tags: ["CROSS_REGION_PRESIGNED_URL"], + name: "crossRegionPresignedUrlMiddleware" +}; + +export const getCrossRegionPresignedUrlPlugin = ( + config: PreviouslyResolved +): Pluggable => ({ + applyToStack: clientStack => { + clientStack.add( + crossRegionPresignedUrlMiddleware(config), + crossRegionPresignedUrlMiddlewareOptions + ); + } +}); + +function expirationTime(durationSeconds: number): DateInput { + return Math.round((new Date().valueOf() + durationSeconds * 1000) / 1000); +} + +function isARN(id: string | undefined): boolean { + if (!id) return false; + return regARN.test(id); +} + +function getEndpointFromARN(arn: string): string { + const arnArr = arn.split(":"); + if (arnArr.length < 4) { + throw new Error(`Cannot infer endpoint from '${arn}'`); + } + return arnArr[3]; +} diff --git a/packages/middleware-sdk-rds/tsconfig.json b/packages/middleware-sdk-rds/tsconfig.json new file mode 100644 index 0000000000000..38b94cda274ec --- /dev/null +++ b/packages/middleware-sdk-rds/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "es5", + "module": "commonjs", + "declaration": true, + "strict": true, + "sourceMap": true, + "downlevelIteration": true, + "importHelpers": true, + "noEmitHelpers": true, + "lib": [ + "es5", + "es2015.promise", + "es2015.collection", + "es2015.iterable", + "es2015.symbol.wellknown" + ], + "rootDir": "./src", + "outDir": "./build", + "incremental": true + } +} diff --git a/packages/middleware-sdk-rds/tsconfig.test.json b/packages/middleware-sdk-rds/tsconfig.test.json new file mode 100644 index 0000000000000..17d0f1b7321fb --- /dev/null +++ b/packages/middleware-sdk-rds/tsconfig.test.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "sourceMap": false, + "inlineSourceMap": true, + "inlineSources": true, + "rootDir": "./src", + "outDir": "./build", + "incremental": true + } +}