From 04c98bf421e097bd5d05d6eacca32f66c5c6ff83 Mon Sep 17 00:00:00 2001 From: Chan Yoo <55515281+sichanyoo@users.noreply.github.com> Date: Tue, 27 Feb 2024 16:58:53 -0800 Subject: [PATCH] feat!: SRA Identity & Auth (#665) * chore: Add new identity protocols. (#594) * Add new identity protocols. --------- Co-authored-by: Sichan Yoo <chanyoo@amazon.com> * chore: Add signer protocol. (#598) * Add signer protocol & refactor HttpContext --------- Co-authored-by: Sichan Yoo <chanyoo@amazon.com> * feat: Auth scheme changes (#601) * Add signer protocol. * Add http context changes. --------- Co-authored-by: Sichan Yoo <chanyoo@amazon.com> * feat: middleware changes (#605) * Add middlewares - AuthSchemeMiddleware and SignerMiddleware * Provide hook in auth scheme for signing properties customization. --------- Co-authored-by: Sichan Yoo <chanyoo@amazon.com> * feat: codegen changes (#609) * Add codegen for service specific auth scheme resolver protocol, service specific default auth scheme resolver struct, and service specific auth scheme resolver parameters struct. * Make ASR throw if passed in ASR params doesn't have region field for SigV4 auth scheme & fix ktlint issues. * Clean up middlewares. * Remove auth scheme and signing middlewares from operation stack of protocol tests. * Update test cases to include new middlewares. * Codegen more descriptive comment for empty service specific auth scheme resolver protocol. * Add codegen test for auth scheme resolver generation. * Move region in middleware context from sdk side to smithy side. * Remove AWSClientRuntime dependency - signingProperties will be set in auth scheme customization hooks instead. * Move auth schemes from service specific config to general AWS config. --------- Co-authored-by: Sichan Yoo <chanyoo@amazon.com> * chore: update epic branch with new changes in main (#617) * chore: Require Swift 5.7, fix deprecation warnings (#600) * feat: support initial-response in RPC based event streams (#597) * chore: Updates version to 0.32.0 * chore: Add newline to README.md (#602) * feat: add limited support in smithy-swift for visionOS (#606) * feat: add support for requiresLength trait and Transfer-Encoding: Chunked (#604) * chore: Update to aws-crt-swift 0.15.0 (#607) * fix: content-length middleware should not error on event streams (#608) * chore: Updates version to 0.33.0 * chore: Improved downstream task (#568) * chore: Convert idempotency token middleware from closure to reusable type (#610) * fix: Update aws-crt-swift dependency to 0.17.0 (#612) * chore: Updates version to 0.34.0 * fix: Endpoint url should be nil if host or scheme is missing (#614) * fix: Pool HTTP connections based on scheme, host, and port (#615) * add default log level to initialize method (#616) * feat: add utility method for converting SdkHttpRequest to URLRequest. (#613) * Add extension constructor to URLRequest to convert SDKHttpRequest * Add preprocessor conditional import functionality to SwiftWriter. --------- Co-authored-by: Sichan Yoo <chanyoo@amazon.com> * chore: Updates version to 0.35.0 * Update test cases added from main to reflect sra identity & auth fields in middleware context & sra identity & auth middlewares in operation stack. --------- Co-authored-by: Josh Elkins <jbelkins@users.noreply.github.com> Co-authored-by: David Yaffe <dayaffe@amazon.com> Co-authored-by: AWS SDK Swift Automation <github-aws-sdk-swift-automation@amazon.com> Co-authored-by: Cyprien Ricque <48893621+CyprienRicque@users.noreply.github.com> Co-authored-by: Sichan Yoo <chanyoo@amazon.com> * feat: customizations (#618) * Add customizations to auth resolve process. * Add signEvent API to Signer protocol, and rename sign to signRequest. * Resolve PR comments. --------- Co-authored-by: Sichan Yoo <chanyoo@amazon.com> * chore: merge most recent main into I&A project branch (#638) * chore: Require Swift 5.7, fix deprecation warnings (#600) * feat: support initial-response in RPC based event streams (#597) * chore: Updates version to 0.32.0 * chore: Add newline to README.md (#602) * feat: add limited support in smithy-swift for visionOS (#606) * feat: add support for requiresLength trait and Transfer-Encoding: Chunked (#604) * chore: Update to aws-crt-swift 0.15.0 (#607) * fix: content-length middleware should not error on event streams (#608) * chore: Updates version to 0.33.0 * chore: Improved downstream task (#568) * chore: Convert idempotency token middleware from closure to reusable type (#610) * fix: Update aws-crt-swift dependency to 0.17.0 (#612) * chore: Updates version to 0.34.0 * fix: Endpoint url should be nil if host or scheme is missing (#614) * fix: Pool HTTP connections based on scheme, host, and port (#615) * add default log level to initialize method (#616) * feat: add utility method for converting SdkHttpRequest to URLRequest. (#613) * Add extension constructor to URLRequest to convert SDKHttpRequest * Add preprocessor conditional import functionality to SwiftWriter. --------- Co-authored-by: Sichan Yoo <chanyoo@amazon.com> * chore: Updates version to 0.35.0 * fix: Add a header to operation doc comments (#621) * remove unnecessary TODOs (#622) * fix: Codegen issues re: recursion, Swift keywords in unions (#623) * feat!: Replace the XML encoder with a custom Smithy implementation (#619) * feat!: Use closures for processing HTTP response (#624) * feat: add custom trait PaginationTruncationMember (#625) * allow isTruncated to be optional bool (#626) * chore: Updates version to 0.36.0 * chore: Run tvOS old & new in CI (#628) * fix: Fix Package.swift warning on Mac (#629) * chore: refactor HttpBody and ByteStream to be a single class ByteStream (#627) * chore: remove sync read in unused data extension (#630) * update smithy to 1.42.0 (#631) * chore: Updates version to 0.37.0 * chore: Update to aws-crt-swift 0.20.0 (#633) * fix: add back from method with fileHandle (#635) * fix!: Add no-op behavior for initialize methods of logging system. (#637) * Add no-op behavior for initialize methods if it isn't the first time being called. * Make LockingSystem threadsafe. * Make initialize methods async. --------- Co-authored-by: Sichan Yoo <chanyoo@amazon.com> * feat!: URLSession-based HTTP Client (#636) * Delete missed merge conflict marker. --------- Co-authored-by: Josh Elkins <jbelkins@users.noreply.github.com> Co-authored-by: David Yaffe <dayaffe@amazon.com> Co-authored-by: AWS SDK Swift Automation <github-aws-sdk-swift-automation@amazon.com> Co-authored-by: Cyprien Ricque <48893621+CyprienRicque@users.noreply.github.com> Co-authored-by: Sichan Yoo <chanyoo@amazon.com> * feat: tie up some loose ends (#645) * Rename runtime type and file in Kotlin side to match corresponding type in Swift side, now both called SignerMiddleware. * Change DefaultIdentityResolverConfiguration's identity resolvers member field to be Attributes so it can store multiple types of identity resolvers and return one with matching identity type. Necessary for supporting multiple types of identities down the line. * Detect newly added SigV4a trait and handle accordingly. --------- Co-authored-by: Sichan Yoo <chanyoo@amazon.com> * chore: Merge updates from main into project epic branch (#658) * chore: Require Swift 5.7, fix deprecation warnings (#600) * feat: support initial-response in RPC based event streams (#597) * chore: Updates version to 0.32.0 * chore: Add newline to README.md (#602) * feat: add limited support in smithy-swift for visionOS (#606) * feat: add support for requiresLength trait and Transfer-Encoding: Chunked (#604) * chore: Update to aws-crt-swift 0.15.0 (#607) * fix: content-length middleware should not error on event streams (#608) * chore: Updates version to 0.33.0 * chore: Improved downstream task (#568) * chore: Convert idempotency token middleware from closure to reusable type (#610) * fix: Update aws-crt-swift dependency to 0.17.0 (#612) * chore: Updates version to 0.34.0 * fix: Endpoint url should be nil if host or scheme is missing (#614) * fix: Pool HTTP connections based on scheme, host, and port (#615) * add default log level to initialize method (#616) * feat: add utility method for converting SdkHttpRequest to URLRequest. (#613) * Add extension constructor to URLRequest to convert SDKHttpRequest * Add preprocessor conditional import functionality to SwiftWriter. --------- Co-authored-by: Sichan Yoo <chanyoo@amazon.com> * chore: Updates version to 0.35.0 * fix: Add a header to operation doc comments (#621) * remove unnecessary TODOs (#622) * fix: Codegen issues re: recursion, Swift keywords in unions (#623) * feat!: Replace the XML encoder with a custom Smithy implementation (#619) * feat!: Use closures for processing HTTP response (#624) * feat: add custom trait PaginationTruncationMember (#625) * allow isTruncated to be optional bool (#626) * chore: Updates version to 0.36.0 * chore: Run tvOS old & new in CI (#628) * fix: Fix Package.swift warning on Mac (#629) * chore: refactor HttpBody and ByteStream to be a single class ByteStream (#627) * chore: remove sync read in unused data extension (#630) * update smithy to 1.42.0 (#631) * chore: Updates version to 0.37.0 * chore: Update to aws-crt-swift 0.20.0 (#633) * fix: add back from method with fileHandle (#635) * fix!: Add no-op behavior for initialize methods of logging system. (#637) * Add no-op behavior for initialize methods if it isn't the first time being called. * Make LockingSystem threadsafe. * Make initialize methods async. --------- Co-authored-by: Sichan Yoo <chanyoo@amazon.com> * feat!: URLSession-based HTTP Client (#636) * bump up CRT version to 0.22.0 (#639) * chore: Update version to 0.38.0 (#641) * chore: Empty commit (#643) * feat: add wrapper for checksums + unit tests (#642) * feat: Use the Foundation HTTP client by default on Mac (#646) * chore: Updates version to 0.39.0 * chore: Change MyURLQueryItem to SDKURLQueryItem (#652) * feat!: Provide HTTP request components by closure instead of protocol (#654) * fix: Don't retry modeled errors by default (#653) * Missed merge conflict marker - deleted. --------- Co-authored-by: Josh Elkins <jbelkins@users.noreply.github.com> Co-authored-by: David Yaffe <dayaffe@amazon.com> Co-authored-by: AWS SDK Swift Automation <github-aws-sdk-swift-automation@amazon.com> Co-authored-by: Cyprien Ricque <48893621+CyprienRicque@users.noreply.github.com> Co-authored-by: Sichan Yoo <chanyoo@amazon.com> * feat: test-suite (#651) * chore: Require Swift 5.7, fix deprecation warnings (#600) * feat: support initial-response in RPC based event streams (#597) * chore: Updates version to 0.32.0 * chore: Add newline to README.md (#602) * feat: add limited support in smithy-swift for visionOS (#606) * feat: add support for requiresLength trait and Transfer-Encoding: Chunked (#604) * chore: Update to aws-crt-swift 0.15.0 (#607) * fix: content-length middleware should not error on event streams (#608) * chore: Updates version to 0.33.0 * chore: Improved downstream task (#568) * chore: Convert idempotency token middleware from closure to reusable type (#610) * fix: Update aws-crt-swift dependency to 0.17.0 (#612) * chore: Updates version to 0.34.0 * fix: Endpoint url should be nil if host or scheme is missing (#614) * fix: Pool HTTP connections based on scheme, host, and port (#615) * add default log level to initialize method (#616) * feat: add utility method for converting SdkHttpRequest to URLRequest. (#613) * Add extension constructor to URLRequest to convert SDKHttpRequest * Add preprocessor conditional import functionality to SwiftWriter. --------- Co-authored-by: Sichan Yoo <chanyoo@amazon.com> * chore: Updates version to 0.35.0 * Add customizations to auth resolve process. Add internal modeled layer for services (S3, EventBridge) that use rules-based auth scheme resolver. Rules-based auth scheme resolver work wrap-up. Wrap-up presign / presign-url refactor. Wrap-up refactor for fitting in rules-based auth scheme resolver. Update test cases to include new middlewares. Move requestSignature getter / setter/ key from aws middleware context extension to here. Also, add saving requestSignature to SignerMiddleware for consumption by event stream signing. * Add signEvent API to Signer protocol, and rename sign to signRequest. * Add mock auth scheme resolver, mock auth schemes, mock identity, mock identity resolver, and mock signer to use for middleware unit tests. * Add unit tests for AuthSchemeMiddleware and SignerMiddleware. * Update MockSigner to conform to modified Signer API with signEvent. * Rename directory containing mocks for auth tests from AuthTest to AuthTestUtil. * fix: Add a header to operation doc comments (#621) * remove unnecessary TODOs (#622) * fix: Codegen issues re: recursion, Swift keywords in unions (#623) * feat!: Replace the XML encoder with a custom Smithy implementation (#619) * feat!: Use closures for processing HTTP response (#624) * feat: add custom trait PaginationTruncationMember (#625) * allow isTruncated to be optional bool (#626) * chore: Updates version to 0.36.0 * chore: Run tvOS old & new in CI (#628) * fix: Fix Package.swift warning on Mac (#629) * chore: refactor HttpBody and ByteStream to be a single class ByteStream (#627) * chore: remove sync read in unused data extension (#630) * update smithy to 1.42.0 (#631) * chore: Updates version to 0.37.0 * chore: Update to aws-crt-swift 0.20.0 (#633) * fix: add back from method with fileHandle (#635) * fix!: Add no-op behavior for initialize methods of logging system. (#637) * Add no-op behavior for initialize methods if it isn't the first time being called. * Make LockingSystem threadsafe. * Make initialize methods async. --------- Co-authored-by: Sichan Yoo <chanyoo@amazon.com> * feat!: URLSession-based HTTP Client (#636) * bump up CRT version to 0.22.0 (#639) * chore: Update version to 0.38.0 (#641) * chore: Empty commit (#643) * feat: add wrapper for checksums + unit tests (#642) * Update to reflect midleware generics change. * Delete unnessary line from test case. * Add CloudFront KeyValueStore as one of the services that use rules based auth scheme resolver customization. * feat: Use the Foundation HTTP client by default on Mac (#646) * chore: Updates version to 0.39.0 * Fix auth scheme middleware to save the selected auth scheme to middleware context by modifying the original context. Fixes transcribe streaming integration test where streaming signing flow was only accessing the original context and not the newly built one with selected auth scheme that was being passed to next middleware in line. * chore: Change MyURLQueryItem to SDKURLQueryItem (#652) * feat!: Provide HTTP request components by closure instead of protocol (#654) * fix: Don't retry modeled errors by default (#653) * Address Josh's PR comments. * Merge updated CRT version from main into feat/test-suite. --------- Co-authored-by: Josh Elkins <jbelkins@users.noreply.github.com> Co-authored-by: David Yaffe <dayaffe@amazon.com> Co-authored-by: AWS SDK Swift Automation <github-aws-sdk-swift-automation@amazon.com> Co-authored-by: Cyprien Ricque <48893621+CyprienRicque@users.noreply.github.com> Co-authored-by: Sichan Yoo <chanyoo@amazon.com> * chore: Merge latest changes from main into SRA I&A (#662) * chore: Require Swift 5.7, fix deprecation warnings (#600) * feat: support initial-response in RPC based event streams (#597) * chore: Updates version to 0.32.0 * chore: Add newline to README.md (#602) * feat: add limited support in smithy-swift for visionOS (#606) * feat: add support for requiresLength trait and Transfer-Encoding: Chunked (#604) * chore: Update to aws-crt-swift 0.15.0 (#607) * fix: content-length middleware should not error on event streams (#608) * chore: Updates version to 0.33.0 * chore: Improved downstream task (#568) * chore: Convert idempotency token middleware from closure to reusable type (#610) * fix: Update aws-crt-swift dependency to 0.17.0 (#612) * chore: Updates version to 0.34.0 * fix: Endpoint url should be nil if host or scheme is missing (#614) * fix: Pool HTTP connections based on scheme, host, and port (#615) * add default log level to initialize method (#616) * feat: add utility method for converting SdkHttpRequest to URLRequest. (#613) * Add extension constructor to URLRequest to convert SDKHttpRequest * Add preprocessor conditional import functionality to SwiftWriter. --------- Co-authored-by: Sichan Yoo <chanyoo@amazon.com> * chore: Updates version to 0.35.0 * fix: Add a header to operation doc comments (#621) * remove unnecessary TODOs (#622) * fix: Codegen issues re: recursion, Swift keywords in unions (#623) * feat!: Replace the XML encoder with a custom Smithy implementation (#619) * feat!: Use closures for processing HTTP response (#624) * feat: add custom trait PaginationTruncationMember (#625) * allow isTruncated to be optional bool (#626) * chore: Updates version to 0.36.0 * chore: Run tvOS old & new in CI (#628) * fix: Fix Package.swift warning on Mac (#629) * chore: refactor HttpBody and ByteStream to be a single class ByteStream (#627) * chore: remove sync read in unused data extension (#630) * update smithy to 1.42.0 (#631) * chore: Updates version to 0.37.0 * chore: Update to aws-crt-swift 0.20.0 (#633) * fix: add back from method with fileHandle (#635) * fix!: Add no-op behavior for initialize methods of logging system. (#637) * Add no-op behavior for initialize methods if it isn't the first time being called. * Make LockingSystem threadsafe. * Make initialize methods async. --------- Co-authored-by: Sichan Yoo <chanyoo@amazon.com> * feat!: URLSession-based HTTP Client (#636) * bump up CRT version to 0.22.0 (#639) * chore: Update version to 0.38.0 (#641) * chore: Empty commit (#643) * feat: add wrapper for checksums + unit tests (#642) * feat: Use the Foundation HTTP client by default on Mac (#646) * chore: Updates version to 0.39.0 * chore: Change MyURLQueryItem to SDKURLQueryItem (#652) * feat!: Provide HTTP request components by closure instead of protocol (#654) * fix: Don't retry modeled errors by default (#653) * feat!: Eliminate service client protocols (#655) * feat: add support for flexible checksums on Normal payloads (#647) * chore: Update aws-crt-swift to 0.26.0 (#661) --------- Co-authored-by: Josh Elkins <jbelkins@users.noreply.github.com> Co-authored-by: David Yaffe <dayaffe@amazon.com> Co-authored-by: AWS SDK Swift Automation <github-aws-sdk-swift-automation@amazon.com> Co-authored-by: Cyprien Ricque <48893621+CyprienRicque@users.noreply.github.com> Co-authored-by: Sichan Yoo <chanyoo@amazon.com> * Change package import statements for AuthSchemeResolverGeneratorTests.kt to match other codegen tests in the repo. * Just for readability & understandability, match order of statements that add middlewares to middleware execution generator with the acutal order in codegen output. * Add documentation for MiddlewareExecutionGenerator.ContextAttributeCodegenFlowType. * Change getSize() of Attributes struct to a computed property. * Factor out and delete IdentityKind enum & use scheme ID instead to determine which identity resolver to return for a given auth scheme in default identity resolver config. * Change auth scheme middleware tests to reflect removal of IdentityKind. * Revert back codegen middleware stack order and add clarifying comment. * Change getOrNull() to orElseNull(null) in auth scheme resolver generator. * Add authSchemes & authSchemeResolver config properties to swift & kotlins sides. Refactor client runtime types for SRA auth for better organization. * Remove unused generic from AuthSchemeMiddleware and SignerMiddleware, then update codegen and codgen tests accordingly. * Fix errors found by generating SDK code. * Fix ktlint. * Fix error caught from CI. Updating SignerMiddleware's name property to SignerMiddleware from SigningMiddleware caused it to not be removed properly for generating protocol tests. * Move default auth scheme resolve logic from runtime to codegen following Steven's feedback on aws-sdk-swift PR. * Address Josh's minor comments. --------- Co-authored-by: Sichan Yoo <chanyoo@amazon.com> Co-authored-by: Josh Elkins <jbelkins@users.noreply.github.com> Co-authored-by: David Yaffe <dayaffe@amazon.com> Co-authored-by: AWS SDK Swift Automation <github-aws-sdk-swift-automation@amazon.com> Co-authored-by: Cyprien Ricque <48893621+CyprienRicque@users.noreply.github.com> --- README.md | 1 + ...DefaultIdentityResolverConfiguration.swift | 18 ++ .../Auth/HTTPAuthAPI/AuthOption.swift | 22 ++ .../Auth/HTTPAuthAPI/AuthScheme.swift | 20 ++ .../Auth/HTTPAuthAPI/AuthSchemeResolver.swift | 11 + .../AuthSchemeResolverParameters.swift | 10 + .../IdentityResolverConfiguration.swift | 10 + .../Auth/HTTPAuthAPI/SelectedAuthScheme.swift | 20 ++ .../Auth/HTTPAuthAPI/Signer.swift | 30 ++ .../DefaultHttpClientConfiguration.swift | 10 +- .../Identity/IdentityAPI/Identity.swift | 11 + .../IdentityAPI/IdentityResolver.swift | 13 + .../ClientRuntime/Middleware/Attribute.swift | 1 + .../Networking/HashFunction.swift | 1 - .../Networking/Http/HttpContext.swift | 279 +++++++++++++---- .../Middlewares/AuthSchemeMiddleware.swift | 105 +++++++ .../Middlewares/ContentMD5Middleware.swift | 2 +- .../Http/Middlewares/SignerMiddleware.swift | 62 ++++ .../Networking/Http/SdkHttpRequest.swift | 10 + .../AuthTestUtil/MockAuthSchemeResolver.swift | 56 ++++ .../AuthTestUtil/MockAuthSchemes.swift | 52 ++++ .../AuthTestUtil/MockIdentity.swift | 21 ++ .../AuthTestUtil/MockSigner.swift | 30 ++ .../AuthSchemeMiddlewareTests.swift | 145 +++++++++ .../SignerMiddlewareTests.swift | 123 ++++++++ .../codegen/AuthSchemeResolverGenerator.kt | 292 ++++++++++++++++++ .../swift/codegen/ClientRuntimeTypes.kt | 13 + .../config/DefaultHttpClientConfiguration.kt | 2 + .../DefaultHttpProtocolCustomizations.kt | 5 + .../HttpBindingProtocolGenerator.kt | 19 +- .../integration/HttpProtocolTestGenerator.kt | 3 +- .../middlewares/AuthSchemeMiddleware.kt | 36 +++ .../middlewares/SignerMiddleware.kt | 36 +++ .../MiddlewareExecutionGenerator.kt | 43 ++- .../AuthSchemeResolverGeneratorTests.kt | 108 +++++++ .../test/kotlin/ContentMd5MiddlewareTests.kt | 5 + .../HttpProtocolClientGeneratorTests.kt | 26 +- .../test/kotlin/IdempotencyTokenTraitTests.kt | 6 +- ...auth-scheme-resolver-generator-test.smithy | 85 +++++ 39 files changed, 1663 insertions(+), 79 deletions(-) create mode 100644 Sources/ClientRuntime/Auth/HTTPAuth/DefaultIdentityResolverConfiguration.swift create mode 100644 Sources/ClientRuntime/Auth/HTTPAuthAPI/AuthOption.swift create mode 100644 Sources/ClientRuntime/Auth/HTTPAuthAPI/AuthScheme.swift create mode 100644 Sources/ClientRuntime/Auth/HTTPAuthAPI/AuthSchemeResolver.swift create mode 100644 Sources/ClientRuntime/Auth/HTTPAuthAPI/AuthSchemeResolverParameters.swift create mode 100644 Sources/ClientRuntime/Auth/HTTPAuthAPI/IdentityResolverConfiguration.swift create mode 100644 Sources/ClientRuntime/Auth/HTTPAuthAPI/SelectedAuthScheme.swift create mode 100644 Sources/ClientRuntime/Auth/HTTPAuthAPI/Signer.swift create mode 100644 Sources/ClientRuntime/Identity/IdentityAPI/Identity.swift create mode 100644 Sources/ClientRuntime/Identity/IdentityAPI/IdentityResolver.swift create mode 100644 Sources/ClientRuntime/Networking/Http/Middlewares/AuthSchemeMiddleware.swift create mode 100644 Sources/ClientRuntime/Networking/Http/Middlewares/SignerMiddleware.swift create mode 100644 Sources/SmithyTestUtil/RequestTestUtil/AuthTestUtil/MockAuthSchemeResolver.swift create mode 100644 Sources/SmithyTestUtil/RequestTestUtil/AuthTestUtil/MockAuthSchemes.swift create mode 100644 Sources/SmithyTestUtil/RequestTestUtil/AuthTestUtil/MockIdentity.swift create mode 100644 Sources/SmithyTestUtil/RequestTestUtil/AuthTestUtil/MockSigner.swift create mode 100644 Tests/ClientRuntimeTests/ClientRuntimeTests/NetworkingTests/Http/MiddlewareTests/AuthSchemeMiddlewareTests.swift create mode 100644 Tests/ClientRuntimeTests/ClientRuntimeTests/NetworkingTests/Http/MiddlewareTests/SignerMiddlewareTests.swift create mode 100644 smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/AuthSchemeResolverGenerator.kt create mode 100644 smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/AuthSchemeMiddleware.kt create mode 100644 smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/SignerMiddleware.kt create mode 100644 smithy-swift-codegen/src/test/kotlin/AuthSchemeResolverGeneratorTests.kt create mode 100644 smithy-swift-codegen/src/test/resources/auth-scheme-resolver-generator-test.smithy diff --git a/README.md b/README.md index b919cec7a..b589f7cbc 100644 --- a/README.md +++ b/README.md @@ -28,3 +28,4 @@ This project is licensed under the Apache-2.0 License. See [CONTRIBUTING](CONTRIBUTING.md) for more information. + diff --git a/Sources/ClientRuntime/Auth/HTTPAuth/DefaultIdentityResolverConfiguration.swift b/Sources/ClientRuntime/Auth/HTTPAuth/DefaultIdentityResolverConfiguration.swift new file mode 100644 index 000000000..7a455392c --- /dev/null +++ b/Sources/ClientRuntime/Auth/HTTPAuth/DefaultIdentityResolverConfiguration.swift @@ -0,0 +1,18 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public struct DefaultIdentityResolverConfiguration: IdentityResolverConfiguration { + let identityResolvers: Attributes + + public init(configuredIdResolvers: Attributes) { + self.identityResolvers = configuredIdResolvers + } + + func getIdentityResolver(schemeID: String) throws -> (any IdentityResolver)? { + return self.identityResolvers.get(key: AttributeKey<any IdentityResolver>(name: schemeID)) + } +} diff --git a/Sources/ClientRuntime/Auth/HTTPAuthAPI/AuthOption.swift b/Sources/ClientRuntime/Auth/HTTPAuthAPI/AuthOption.swift new file mode 100644 index 000000000..8349f3075 --- /dev/null +++ b/Sources/ClientRuntime/Auth/HTTPAuthAPI/AuthOption.swift @@ -0,0 +1,22 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public struct AuthOption { + let schemeID: String + public var identityProperties: Attributes + public var signingProperties: Attributes + + public init ( + schemeID: String, + identityProperties: Attributes = Attributes(), + signingProperties: Attributes = Attributes() + ) { + self.schemeID = schemeID + self.identityProperties = identityProperties + self.signingProperties = signingProperties + } +} diff --git a/Sources/ClientRuntime/Auth/HTTPAuthAPI/AuthScheme.swift b/Sources/ClientRuntime/Auth/HTTPAuthAPI/AuthScheme.swift new file mode 100644 index 000000000..4c1ad3264 --- /dev/null +++ b/Sources/ClientRuntime/Auth/HTTPAuthAPI/AuthScheme.swift @@ -0,0 +1,20 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public protocol AuthScheme { + var schemeID: String { get } + var signer: Signer { get } + + // Hook used by AuthSchemeMiddleware that allows signing properties customization, if needed by an auth scheme + func customizeSigningProperties(signingProperties: Attributes, context: HttpContext) throws -> Attributes +} + +extension AuthScheme { + func identityResolver(config: IdentityResolverConfiguration) throws -> (any IdentityResolver)? { + return try config.getIdentityResolver(schemeID: self.schemeID) + } +} diff --git a/Sources/ClientRuntime/Auth/HTTPAuthAPI/AuthSchemeResolver.swift b/Sources/ClientRuntime/Auth/HTTPAuthAPI/AuthSchemeResolver.swift new file mode 100644 index 000000000..980000198 --- /dev/null +++ b/Sources/ClientRuntime/Auth/HTTPAuthAPI/AuthSchemeResolver.swift @@ -0,0 +1,11 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public protocol AuthSchemeResolver { + func resolveAuthScheme(params: AuthSchemeResolverParameters) throws -> [AuthOption] + func constructParameters(context: HttpContext) throws -> AuthSchemeResolverParameters +} diff --git a/Sources/ClientRuntime/Auth/HTTPAuthAPI/AuthSchemeResolverParameters.swift b/Sources/ClientRuntime/Auth/HTTPAuthAPI/AuthSchemeResolverParameters.swift new file mode 100644 index 000000000..57b5e8528 --- /dev/null +++ b/Sources/ClientRuntime/Auth/HTTPAuthAPI/AuthSchemeResolverParameters.swift @@ -0,0 +1,10 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public protocol AuthSchemeResolverParameters { + var operation: String { get } +} diff --git a/Sources/ClientRuntime/Auth/HTTPAuthAPI/IdentityResolverConfiguration.swift b/Sources/ClientRuntime/Auth/HTTPAuthAPI/IdentityResolverConfiguration.swift new file mode 100644 index 000000000..9430cc668 --- /dev/null +++ b/Sources/ClientRuntime/Auth/HTTPAuthAPI/IdentityResolverConfiguration.swift @@ -0,0 +1,10 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +protocol IdentityResolverConfiguration { + func getIdentityResolver(schemeID: String) throws -> (any IdentityResolver)? +} diff --git a/Sources/ClientRuntime/Auth/HTTPAuthAPI/SelectedAuthScheme.swift b/Sources/ClientRuntime/Auth/HTTPAuthAPI/SelectedAuthScheme.swift new file mode 100644 index 000000000..04c2a929a --- /dev/null +++ b/Sources/ClientRuntime/Auth/HTTPAuthAPI/SelectedAuthScheme.swift @@ -0,0 +1,20 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public struct SelectedAuthScheme { + public let schemeID: String + public let identity: Identity? + public let signingProperties: Attributes? + public let signer: Signer? + + public init(schemeID: String, identity: Identity?, signingProperties: Attributes?, signer: Signer?) { + self.schemeID = schemeID + self.identity = identity + self.signingProperties = signingProperties + self.signer = signer + } +} diff --git a/Sources/ClientRuntime/Auth/HTTPAuthAPI/Signer.swift b/Sources/ClientRuntime/Auth/HTTPAuthAPI/Signer.swift new file mode 100644 index 000000000..8cc308de4 --- /dev/null +++ b/Sources/ClientRuntime/Auth/HTTPAuthAPI/Signer.swift @@ -0,0 +1,30 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public protocol Signer { + func signRequest<IdentityT: Identity>( + requestBuilder: SdkHttpRequestBuilder, + identity: IdentityT, + signingProperties: Attributes + ) async throws -> SdkHttpRequestBuilder + + func signEvent( + payload: Data, + previousSignature: String, + signingProperties: Attributes + ) async throws -> SigningResult<EventStream.Message> +} + +public struct SigningResult<T> { + public let output: T + public let signature: String + + public init(output: T, signature: String) { + self.output = output + self.signature = signature + } +} diff --git a/Sources/ClientRuntime/Config/DefaultHttpClientConfiguration.swift b/Sources/ClientRuntime/Config/DefaultHttpClientConfiguration.swift index 83a68397f..ea20355f8 100644 --- a/Sources/ClientRuntime/Config/DefaultHttpClientConfiguration.swift +++ b/Sources/ClientRuntime/Config/DefaultHttpClientConfiguration.swift @@ -16,5 +16,13 @@ public protocol DefaultHttpClientConfiguration: ClientConfiguration { /// Configuration for the HTTP client. var httpClientConfiguration: HttpClientConfiguration { get } - /// TODO: Add auth scheme + /// List of auth schemes to use for client calls. + /// + /// Defaults to auth schemes defined on the Smithy service model. + var authSchemes: [ClientRuntime.AuthScheme]? { get set } + + /// The auth scheme resolver to use for resolving auth scheme. + /// + /// Defaults to a auth scheme resolver generated based on Smithy service model. + var authSchemeResolver: ClientRuntime.AuthSchemeResolver { get set } } diff --git a/Sources/ClientRuntime/Identity/IdentityAPI/Identity.swift b/Sources/ClientRuntime/Identity/IdentityAPI/Identity.swift new file mode 100644 index 000000000..4812003ab --- /dev/null +++ b/Sources/ClientRuntime/Identity/IdentityAPI/Identity.swift @@ -0,0 +1,11 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +// Base protocol for all identity types +public protocol Identity { + var expiration: Date? { get } +} diff --git a/Sources/ClientRuntime/Identity/IdentityAPI/IdentityResolver.swift b/Sources/ClientRuntime/Identity/IdentityAPI/IdentityResolver.swift new file mode 100644 index 000000000..19900ffab --- /dev/null +++ b/Sources/ClientRuntime/Identity/IdentityAPI/IdentityResolver.swift @@ -0,0 +1,13 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +// Base protocol for all identity provider types +public protocol IdentityResolver { + associatedtype IdentityT: Identity + + func getIdentity(identityProperties: Attributes?) async throws -> IdentityT +} diff --git a/Sources/ClientRuntime/Middleware/Attribute.swift b/Sources/ClientRuntime/Middleware/Attribute.swift index b7c1261f5..566063495 100644 --- a/Sources/ClientRuntime/Middleware/Attribute.swift +++ b/Sources/ClientRuntime/Middleware/Attribute.swift @@ -21,6 +21,7 @@ public struct AttributeKey<ValueType> { /// Type safe property bag public struct Attributes { private var attributes = [String: Any]() + public var size: Int { attributes.count } public init() {} diff --git a/Sources/ClientRuntime/Networking/HashFunction.swift b/Sources/ClientRuntime/Networking/HashFunction.swift index 354a4907c..2e454b7af 100644 --- a/Sources/ClientRuntime/Networking/HashFunction.swift +++ b/Sources/ClientRuntime/Networking/HashFunction.swift @@ -31,7 +31,6 @@ public enum HashFunction { static func fromList(_ stringArray: [String]) -> [HashFunction] { var hashFunctions = [HashFunction]() - for string in stringArray { if let hashFunction = HashFunction.from(string: string) { hashFunctions.append(hashFunction) diff --git a/Sources/ClientRuntime/Networking/Http/HttpContext.swift b/Sources/ClientRuntime/Networking/Http/HttpContext.swift index 27c78c453..277ff9335 100644 --- a/Sources/ClientRuntime/Networking/Http/HttpContext.swift +++ b/Sources/ClientRuntime/Networking/Http/HttpContext.swift @@ -1,6 +1,8 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0. +import struct Foundation.TimeInterval + /// this struct implements middleware context and will serve as the context for all http middleware operations public class HttpContext: MiddlewareContext { public var attributes: Attributes @@ -9,41 +11,74 @@ public class HttpContext: MiddlewareContext { public init(attributes: Attributes) { self.attributes = attributes } - // FIXME: Move all defined keys to separate file as constants to be used elsewhere - public func getPath() -> String { - return attributes.get(key: AttributeKey<String>(name: "Path"))! + + public func toBuilder() -> HttpContextBuilder { + let builder = HttpContextBuilder() + builder.attributes = self.attributes + if let response = self.response { + builder.response = response + } + return builder } - public func getMethod() -> HttpMethodType { - return attributes.get(key: AttributeKey<HttpMethodType>(name: "Method"))! + public func getAuthSchemeResolver() -> AuthSchemeResolver? { + return attributes.get(key: AttributeKeys.authSchemeResolver) } - public func getEncoder() -> RequestEncoder { - return attributes.get(key: AttributeKey<RequestEncoder>(name: "Encoder"))! + public func getAuthSchemes() -> Attributes? { + return attributes.get(key: AttributeKeys.authSchemes) } public func getDecoder() -> ResponseDecoder { - return attributes.get(key: AttributeKey<ResponseDecoder>(name: "Decoder"))! + return attributes.get(key: AttributeKeys.decoder)! + } + + public func getEncoder() -> RequestEncoder { + return attributes.get(key: AttributeKeys.encoder)! + } + + public func getExpiration() -> TimeInterval { + return attributes.get(key: AttributeKeys.expiration) ?? 0 + } + + public func getFlowType() -> FlowType { + return attributes.get(key: AttributeKeys.flowType) ?? .NORMAL } public func getHost() -> String? { - return attributes.get(key: AttributeKey<String>(name: "Host")) + return attributes.get(key: AttributeKeys.host) } - public func getServiceName() -> String { - return attributes.get(key: AttributeKey<String>(name: "ServiceName"))! + public func getHostPrefix() -> String? { + return attributes.get(key: AttributeKeys.hostPrefix) } public func getIdempotencyTokenGenerator() -> IdempotencyTokenGenerator { - return attributes.get(key: AttributeKey<IdempotencyTokenGenerator>(name: "IdempotencyTokenGenerator"))! + return attributes.get(key: AttributeKeys.idempotencyTokenGenerator)! } - public func getHostPrefix() -> String? { - return attributes.get(key: AttributeKey<String>(name: "HostPrefix")) + public func getIdentityResolvers() -> Attributes? { + return attributes.get(key: AttributeKeys.identityResolvers) } public func getLogger() -> LogAgent? { - return attributes.get(key: AttributeKey<LogAgent>(name: "Logger")) + return attributes.get(key: AttributeKeys.logger) + } + + public func getMessageEncoder() -> MessageEncoder? { + return attributes.get(key: AttributeKeys.messageEncoder) + } + + public func getMessageSigner() -> MessageSigner? { + return attributes.get(key: AttributeKeys.messageSigner) + } + + public func getMethod() -> HttpMethodType { + return attributes.get(key: AttributeKeys.method)! + } + + public func getOperation() -> String? { + return attributes.get(key: AttributeKeys.operation) } /// The partition ID to be used for this context. @@ -51,19 +86,43 @@ public class HttpContext: MiddlewareContext { /// Requests made with the same partition ID will be grouped together for retry throttling purposes. /// If no partition ID is provided, requests will be partitioned based on the hostname. public func getPartitionID() -> String? { - return attributes.get(key: AttributeKey<String>(name: "PartitionID")) + return attributes.get(key: AttributeKeys.partitionId) } - public func getMessageEncoder() -> MessageEncoder? { - return attributes.get(key: HttpContext.messageEncoder) + public func getPath() -> String { + return attributes.get(key: AttributeKeys.path)! } - public func getMessageSigner() -> MessageSigner? { - return attributes.get(key: HttpContext.messageSigner) + public func getRegion() -> String? { + return attributes.get(key: AttributeKeys.region) + } + + public func getRequestSignature() -> String { + return attributes.get(key: AttributeKeys.requestSignature)! + } + + public func getSelectedAuthScheme() -> SelectedAuthScheme? { + return attributes.get(key: AttributeKeys.selectedAuthScheme) + } + + public func getServiceName() -> String { + return attributes.get(key: AttributeKeys.serviceName)! + } + + public func getSigningName() -> String? { + return attributes.get(key: AttributeKeys.signingName) + } + + public func getSigningRegion() -> String? { + return attributes.get(key: AttributeKeys.signingRegion) + } + + public func hasUnsignedPayloadTrait() -> Bool { + return attributes.get(key: AttributeKeys.hasUnsignedPayloadTrait) ?? false } public func isBidirectionalStreamingEnabled() -> Bool { - return attributes.get(key: HttpContext.bidirectionalStreaming) ?? false + return attributes.get(key: AttributeKeys.bidirectionalStreaming) ?? false } /// Returns `true` if the request should use `http2` and only `http2` without falling back to `http1` @@ -72,29 +131,11 @@ public class HttpContext: MiddlewareContext { } } -extension HttpContext { - public static let messageEncoder = AttributeKey<MessageEncoder>(name: "MessageEncoder") - public static let messageSigner = AttributeKey<MessageSigner>(name: "MessageSigner") - public static let bidirectionalStreaming = AttributeKey<Bool>(name: "BidirectionalStreaming") -} - public class HttpContextBuilder { - public init() {} public var attributes: Attributes = Attributes() - let encoder = AttributeKey<RequestEncoder>(name: "Encoder") - let method = AttributeKey<HttpMethodType>(name: "Method") - let path = AttributeKey<String>(name: "Path") - let operation = AttributeKey<String>(name: "Operation") - let host = AttributeKey<String>(name: "Host") - let serviceName = AttributeKey<String>(name: "ServiceName") var response: HttpResponse = HttpResponse() - let decoder = AttributeKey<ResponseDecoder>(name: "Decoder") - let idempotencyTokenGenerator = AttributeKey<IdempotencyTokenGenerator>(name: "IdempotencyTokenGenerator") - let hostPrefix = AttributeKey<String>(name: "HostPrefix") - let logger = AttributeKey<LogAgent>(name: "Logger") - let partitionID = AttributeKey<String>(name: "PartitionID") // We follow the convention of returning the builder object // itself from any configuration methods, and by adding the @@ -108,68 +149,92 @@ public class HttpContextBuilder { } @discardableResult - public func withEncoder(value: RequestEncoder) -> HttpContextBuilder { - self.attributes.set(key: encoder, value: value) + public func withAuthSchemeResolver(value: AuthSchemeResolver) -> HttpContextBuilder { + self.attributes.set(key: AttributeKeys.authSchemeResolver, value: value) return self } @discardableResult - public func withMethod(value: HttpMethodType) -> HttpContextBuilder { - self.attributes.set(key: method, value: value) + public func withAuthScheme(value: AuthScheme) -> HttpContextBuilder { + var authSchemes: Attributes = self.attributes.get(key: AttributeKeys.authSchemes) ?? Attributes() + authSchemes.set(key: AttributeKey<AuthScheme>(name: "\(value.schemeID)"), value: value) + self.attributes.set(key: AttributeKeys.authSchemes, value: authSchemes) return self } @discardableResult - public func withPath(value: String) -> HttpContextBuilder { - self.attributes.set(key: path, value: value) + public func withAuthSchemes(value: [AuthScheme]) -> HttpContextBuilder { + for scheme in value { + self.withAuthScheme(value: scheme) + } return self } @discardableResult - public func withHost(value: String) -> HttpContextBuilder { - self.attributes.set(key: host, value: value) + public func withDecoder(value: ResponseDecoder) -> HttpContextBuilder { + self.attributes.set(key: AttributeKeys.decoder, value: value) return self } @discardableResult - public func withHostPrefix(value: String) -> HttpContextBuilder { - self.attributes.set(key: hostPrefix, value: value) + public func withEncoder(value: RequestEncoder) -> HttpContextBuilder { + self.attributes.set(key: AttributeKeys.encoder, value: value) return self } @discardableResult - public func withOperation(value: String) -> HttpContextBuilder { - self.attributes.set(key: operation, value: value) + public func withExpiration(value: TimeInterval) -> HttpContextBuilder { + self.attributes.set(key: AttributeKeys.expiration, value: value) return self } @discardableResult - public func withServiceName(value: String) -> HttpContextBuilder { - self.attributes.set(key: serviceName, value: value) + public func withFlowType(value: FlowType) -> HttpContextBuilder { + self.attributes.set(key: AttributeKeys.flowType, value: value) return self } @discardableResult - public func withDecoder(value: ResponseDecoder) -> HttpContextBuilder { - self.attributes.set(key: decoder, value: value) + public func withHost(value: String) -> HttpContextBuilder { + self.attributes.set(key: AttributeKeys.host, value: value) return self } @discardableResult - public func withResponse(value: HttpResponse) -> HttpContextBuilder { - self.response = value + public func withHostPrefix(value: String) -> HttpContextBuilder { + self.attributes.set(key: AttributeKeys.hostPrefix, value: value) return self } @discardableResult public func withIdempotencyTokenGenerator(value: IdempotencyTokenGenerator) -> HttpContextBuilder { - self.attributes.set(key: idempotencyTokenGenerator, value: value) + self.attributes.set(key: AttributeKeys.idempotencyTokenGenerator, value: value) + return self + } + + @discardableResult + public func withIdentityResolver<T: IdentityResolver>(value: T, schemeID: String) -> HttpContextBuilder { + var identityResolvers: Attributes = self.attributes.get(key: AttributeKeys.identityResolvers) ?? Attributes() + identityResolvers.set(key: AttributeKey<any IdentityResolver>(name: schemeID), value: value) + self.attributes.set(key: AttributeKeys.identityResolvers, value: identityResolvers) return self } @discardableResult public func withLogger(value: LogAgent) -> HttpContextBuilder { - self.attributes.set(key: logger, value: value) + self.attributes.set(key: AttributeKeys.logger, value: value) + return self + } + + @discardableResult + public func withMethod(value: HttpMethodType) -> HttpContextBuilder { + self.attributes.set(key: AttributeKeys.method, value: value) + return self + } + + @discardableResult + public func withOperation(value: String) -> HttpContextBuilder { + self.attributes.set(key: AttributeKeys.operation, value: value) return self } @@ -181,7 +246,63 @@ public class HttpContextBuilder { /// - Returns: `self`, after the partition ID is set as specified. @discardableResult public func withPartitionID(value: String?) -> HttpContextBuilder { - self.attributes.set(key: partitionID, value: value) + self.attributes.set(key: AttributeKeys.partitionId, value: value) + return self + } + + @discardableResult + public func withPath(value: String) -> HttpContextBuilder { + self.attributes.set(key: AttributeKeys.path, value: value) + return self + } + + @discardableResult + public func withRegion(value: String?) -> HttpContextBuilder { + self.attributes.set(key: AttributeKeys.region, value: value) + return self + } + + @discardableResult + public func withResponse(value: HttpResponse) -> HttpContextBuilder { + self.response = value + return self + } + + /// Sets the request signature for the event stream operation + /// - Parameter value: `String` request signature + @discardableResult + public func withRequestSignature(value: String) -> HttpContextBuilder { + self.attributes.set(key: AttributeKeys.requestSignature, value: value) + return self + } + + @discardableResult + public func withSelectedAuthScheme(value: SelectedAuthScheme) -> HttpContextBuilder { + self.attributes.set(key: AttributeKeys.selectedAuthScheme, value: value) + return self + } + + @discardableResult + public func withServiceName(value: String) -> HttpContextBuilder { + self.attributes.set(key: AttributeKeys.serviceName, value: value) + return self + } + + @discardableResult + public func withSigningName(value: String) -> HttpContextBuilder { + self.attributes.set(key: AttributeKeys.signingName, value: value) + return self + } + + @discardableResult + public func withSigningRegion(value: String?) -> HttpContextBuilder { + self.attributes.set(key: AttributeKeys.signingRegion, value: value) + return self + } + + @discardableResult + public func withUnsignedPayloadTrait(value: Bool) -> HttpContextBuilder { + self.attributes.set(key: AttributeKeys.hasUnsignedPayloadTrait, value: value) return self } @@ -189,3 +310,41 @@ public class HttpContextBuilder { return HttpContext(attributes: attributes) } } + +public enum AttributeKeys { + public static let authSchemeResolver = AttributeKey<AuthSchemeResolver>(name: "AuthSchemeResolver") + public static let authSchemes = AttributeKey<Attributes>(name: "AuthSchemes") + public static let bidirectionalStreaming = AttributeKey<Bool>(name: "BidirectionalStreaming") + public static let decoder = AttributeKey<ResponseDecoder>(name: "Decoder") + public static let encoder = AttributeKey<RequestEncoder>(name: "Encoder") + public static let flowType = AttributeKey<FlowType>(name: "FlowType") + public static let host = AttributeKey<String>(name: "Host") + public static let hostPrefix = AttributeKey<String>(name: "HostPrefix") + public static let idempotencyTokenGenerator = AttributeKey<IdempotencyTokenGenerator>( + name: "IdempotencyTokenGenerator" + ) + public static let identityResolvers = AttributeKey<Attributes>(name: "IdentityResolvers") + public static let logger = AttributeKey<LogAgent>(name: "Logger") + public static let messageEncoder = AttributeKey<MessageEncoder>(name: "MessageEncoder") + public static let messageSigner = AttributeKey<MessageSigner>(name: "MessageSigner") + public static let method = AttributeKey<HttpMethodType>(name: "Method") + public static let operation = AttributeKey<String>(name: "Operation") + public static let partitionId = AttributeKey<String>(name: "PartitionID") + public static let path = AttributeKey<String>(name: "Path") + public static let region = AttributeKey<String>(name: "Region") + public static let requestSignature = AttributeKey<String>(name: "AWS_HTTP_SIGNATURE") + public static let selectedAuthScheme = AttributeKey<SelectedAuthScheme>(name: "SelectedAuthScheme") + public static let serviceName = AttributeKey<String>(name: "ServiceName") + public static let signingName = AttributeKey<String>(name: "SigningName") + public static let signingRegion = AttributeKey<String>(name: "SigningRegion") + + // Flags stored in signingProperties passed to signers for presigner customizations. + public static let hasUnsignedPayloadTrait = AttributeKey<Bool>(name: "HasUnsignedPayloadTrait") + public static let forceUnsignedBody = AttributeKey<Bool>(name: "ForceUnsignedBody") + public static let expiration = AttributeKey<TimeInterval>(name: "Expiration") +} + +// The type of flow the mdidleware context is being constructed for +public enum FlowType { + case NORMAL, PRESIGN_REQUEST, PRESIGN_URL +} diff --git a/Sources/ClientRuntime/Networking/Http/Middlewares/AuthSchemeMiddleware.swift b/Sources/ClientRuntime/Networking/Http/Middlewares/AuthSchemeMiddleware.swift new file mode 100644 index 000000000..12d3f52cd --- /dev/null +++ b/Sources/ClientRuntime/Networking/Http/Middlewares/AuthSchemeMiddleware.swift @@ -0,0 +1,105 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation + +public struct AuthSchemeMiddleware<OperationStackOutput>: Middleware { + public let id: String = "AuthSchemeMiddleware" + + public init () {} + + public typealias MInput = SdkHttpRequestBuilder + public typealias MOutput = OperationOutput<OperationStackOutput> + public typealias Context = HttpContext + + public func handle<H>(context: HttpContext, + input: SdkHttpRequestBuilder, + next: H) async throws -> OperationOutput<OperationStackOutput> + where H: Handler, + Self.Context == H.Context, + Self.MInput == H.Input, + Self.MOutput == H.Output { + // Get auth scheme resolver from middleware context + guard let resolver = context.getAuthSchemeResolver() else { + throw ClientError.authError("No auth scheme resolver has been configured on the service.") + } + + // Construct auth scheme resolver parameters + let resolverParams = try resolver.constructParameters(context: context) + // Retrieve valid auth options for the operation at hand + let validAuthOptions = try resolver.resolveAuthScheme(params: resolverParams) + + // Create IdentityResolverConfiguration + let identityResolvers = context.getIdentityResolvers() + guard let identityResolvers, identityResolvers.size > 0 else { + throw ClientError.authError("No identity resolver has been configured on the service.") + } + let identityResolverConfig = DefaultIdentityResolverConfiguration(configuredIdResolvers: identityResolvers) + + // Get auth schemes configured on the service + // If none is confugred, create empty Attributes object + let validAuthSchemes = context.getAuthSchemes() ?? Attributes() + + // Variable for selected auth scheme + var selectedAuthScheme: SelectedAuthScheme? + + // Error message to throw if no auth scheme can be resolved + var log: [String] = [] + + // For each auth option returned by auth scheme resolver: + for option in validAuthOptions { + // If current auth option is noAuth, set selectedAuthScheme with nil fields and break + if option.schemeID == "smithy.api#noAuth" { + selectedAuthScheme = SelectedAuthScheme( + schemeID: option.schemeID, + identity: nil, + signingProperties: nil, + signer: nil + ) + break + } + // Otherwise, + // 1) check if corresponding auth scheme for current auth option is configured on the service, then + // 2) check if corresponding identity resolver for the auth scheme is configured + // If both 1 & 2 are satisfied, resolve auth scheme and save to selectedAuthScheme + if let authScheme = validAuthSchemes.get(key: AttributeKey<AuthScheme>(name: "\(option.schemeID)")) { + if let identityResolver = try authScheme.identityResolver(config: identityResolverConfig) { + // Hook for auth scheme to customize signing properties + let signingProperties = try authScheme.customizeSigningProperties( + signingProperties: option.signingProperties, + context: context + ) + // Resolve identity using the resolver from auth scheme + let identity = try await identityResolver.getIdentity(identityProperties: option.identityProperties) + // Save selected auth scheme + selectedAuthScheme = SelectedAuthScheme( + schemeID: option.schemeID, + identity: identity, + signingProperties: signingProperties, + signer: authScheme.signer + ) + break + } else { + log.append("Auth scheme \(option.schemeID) did not have an identity resolver configured.") + } + } else { + log.append("Auth scheme \(option.schemeID) was not enabled for this request.") + } + } + + // If no auth scheme could be resolved, throw an error + guard let selectedAuthScheme else { + throw ClientError.authError( + "Could not resolve auth scheme for the operation call. Log: \(log.joined(separator: ","))" + ) + } + + // Set the selected auth scheme in context for subsequent middleware access, then pass to next middleware in chain + context.attributes.set(key: AttributeKeys.selectedAuthScheme, value: selectedAuthScheme) + return try await next.handle(context: context, input: input) + } +} diff --git a/Sources/ClientRuntime/Networking/Http/Middlewares/ContentMD5Middleware.swift b/Sources/ClientRuntime/Networking/Http/Middlewares/ContentMD5Middleware.swift index 0675ac062..dad7107e4 100644 --- a/Sources/ClientRuntime/Networking/Http/Middlewares/ContentMD5Middleware.swift +++ b/Sources/ClientRuntime/Networking/Http/Middlewares/ContentMD5Middleware.swift @@ -25,7 +25,7 @@ public struct ContentMD5Middleware<OperationStackOutput>: Middleware { switch input.body { case .data(let data): - guard let data = data else { + guard let data else { return try await next.handle(context: context, input: input) } let md5Hash = try data.computeMD5() diff --git a/Sources/ClientRuntime/Networking/Http/Middlewares/SignerMiddleware.swift b/Sources/ClientRuntime/Networking/Http/Middlewares/SignerMiddleware.swift new file mode 100644 index 000000000..a37976cbe --- /dev/null +++ b/Sources/ClientRuntime/Networking/Http/Middlewares/SignerMiddleware.swift @@ -0,0 +1,62 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation + +public struct SignerMiddleware<OperationStackOutput>: Middleware { + public let id: String = "SignerMiddleware" + + public init () {} + + public typealias MInput = SdkHttpRequestBuilder + public typealias MOutput = OperationOutput<OperationStackOutput> + public typealias Context = HttpContext + + public func handle<H>(context: HttpContext, + input: SdkHttpRequestBuilder, + next: H) async throws -> OperationOutput<OperationStackOutput> + where H: Handler, + Self.Context == H.Context, + Self.MInput == H.Input, + Self.MOutput == H.Output { + // Retrieve selected auth scheme from context + guard let selectedAuthScheme = context.getSelectedAuthScheme() else { + throw ClientError.authError("Auth scheme needed by signer middleware was not saved properly.") + } + + // Return without signing request if resolved auth scheme is of noAuth type + guard selectedAuthScheme.schemeID != "smithy.api#noAuth" else { + return try await next.handle(context: context, input: input) + } + + // Retrieve identity, signer, and signing properties from selected auth scheme to sign the request. + guard let identity = selectedAuthScheme.identity else { + throw ClientError.authError( + "Identity needed by signer middleware was not properly saved into loaded auth scheme." + ) + } + guard let signer = selectedAuthScheme.signer else { + throw ClientError.authError( + "Signer needed by signer middleware was not properly saved into loaded auth scheme." + ) + } + guard let signingProperties = selectedAuthScheme.signingProperties else { + throw ClientError.authError( + "Signing properties object needed by signer middleware was not properly saved into loaded auth scheme." + ) + } + + // Sign request and hand over to next middleware (handler) in line. + let signedInput = try await signer.signRequest( + requestBuilder: input, identity: identity, signingProperties: signingProperties + ) + // The saved signature is used to sign event stream messages if needed. + context.attributes.set(key: AttributeKeys.requestSignature, value: signedInput.signature) + + return try await next.handle(context: context, input: signedInput) + } +} diff --git a/Sources/ClientRuntime/Networking/Http/SdkHttpRequest.swift b/Sources/ClientRuntime/Networking/Http/SdkHttpRequest.swift index 6493883fe..2977d37b6 100644 --- a/Sources/ClientRuntime/Networking/Http/SdkHttpRequest.swift +++ b/Sources/ClientRuntime/Networking/Http/SdkHttpRequest.swift @@ -272,3 +272,13 @@ extension HTTPRequestBase { return String(signatureSequence) } } + +extension SdkHttpRequestBuilder { + public var signature: String? { + let authHeader = self.headers.value(for: "Authorization") + guard let signatureSequence = authHeader?.split(separator: "=").last else { + return nil + } + return String(signatureSequence) + } +} diff --git a/Sources/SmithyTestUtil/RequestTestUtil/AuthTestUtil/MockAuthSchemeResolver.swift b/Sources/SmithyTestUtil/RequestTestUtil/AuthTestUtil/MockAuthSchemeResolver.swift new file mode 100644 index 000000000..d43328a87 --- /dev/null +++ b/Sources/SmithyTestUtil/RequestTestUtil/AuthTestUtil/MockAuthSchemeResolver.swift @@ -0,0 +1,56 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation +import ClientRuntime + +public struct MockAuthSchemeResolverParameters: ClientRuntime.AuthSchemeResolverParameters { + public let operation: String +} + +public protocol MockAuthSchemeResolver: ClientRuntime.AuthSchemeResolver { + // Intentionally empty. + // This is the parent protocol that all auth scheme resolver implementations of + // the service Mock must conform to. +} + +public struct DefaultMockAuthSchemeResolver: MockAuthSchemeResolver { + public init () {} + + public func resolveAuthScheme(params: ClientRuntime.AuthSchemeResolverParameters) throws -> [AuthOption] { + var validAuthOptions = Array<AuthOption>() + guard let serviceParams = params as? MockAuthSchemeResolverParameters else { + throw ClientError.authError("Service specific auth scheme parameters type must be passed to auth scheme resolver.") + } + switch serviceParams.operation { + case "authA": + validAuthOptions.append(AuthOption(schemeID: "MockAuthSchemeA")) + case "authAB": + validAuthOptions.append(AuthOption(schemeID: "MockAuthSchemeA")) + validAuthOptions.append(AuthOption(schemeID: "MockAuthSchemeB")) + case "authABC": + validAuthOptions.append(AuthOption(schemeID: "MockAuthSchemeA")) + validAuthOptions.append(AuthOption(schemeID: "MockAuthSchemeB")) + validAuthOptions.append(AuthOption(schemeID: "MockAuthSchemeC")) + case "authABCNoAuth": + validAuthOptions.append(AuthOption(schemeID: "MockAuthSchemeA")) + validAuthOptions.append(AuthOption(schemeID: "MockAuthSchemeB")) + validAuthOptions.append(AuthOption(schemeID: "MockAuthSchemeC")) + validAuthOptions.append(AuthOption(schemeID: "smithy.api#noAuth")) + default: + validAuthOptions.append(AuthOption(schemeID: "fillerAuth")) + } + return validAuthOptions + } + + public func constructParameters(context: HttpContext) throws -> ClientRuntime.AuthSchemeResolverParameters { + guard let opName = context.getOperation() else { + throw ClientError.dataNotFound("Operation name not configured in middleware context for auth scheme resolver params construction.") + } + return MockAuthSchemeResolverParameters(operation: opName) + } +} diff --git a/Sources/SmithyTestUtil/RequestTestUtil/AuthTestUtil/MockAuthSchemes.swift b/Sources/SmithyTestUtil/RequestTestUtil/AuthTestUtil/MockAuthSchemes.swift new file mode 100644 index 000000000..426af3708 --- /dev/null +++ b/Sources/SmithyTestUtil/RequestTestUtil/AuthTestUtil/MockAuthSchemes.swift @@ -0,0 +1,52 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import ClientRuntime + +public struct MockAuthSchemeA: ClientRuntime.AuthScheme { + public let schemeID: String = "MockAuthSchemeA" + public let signer: ClientRuntime.Signer = MockSigner() + + public init() {} + + public func customizeSigningProperties(signingProperties: ClientRuntime.Attributes, context: ClientRuntime.HttpContext) -> ClientRuntime.Attributes { + return signingProperties + } +} + +public struct MockAuthSchemeB: ClientRuntime.AuthScheme { + public let schemeID: String = "MockAuthSchemeB" + public let signer: ClientRuntime.Signer = MockSigner() + + public init() {} + + public func customizeSigningProperties(signingProperties: ClientRuntime.Attributes, context: ClientRuntime.HttpContext) -> ClientRuntime.Attributes { + return signingProperties + } +} + +public struct MockAuthSchemeC: ClientRuntime.AuthScheme { + public let schemeID: String = "MockAuthSchemeC" + public let signer: ClientRuntime.Signer = MockSigner() + + public init() {} + + public func customizeSigningProperties(signingProperties: ClientRuntime.Attributes, context: ClientRuntime.HttpContext) -> ClientRuntime.Attributes { + return signingProperties + } +} + +public struct MockNoAuth: ClientRuntime.AuthScheme { + public let schemeID: String = "smithy.api#noAuth" + public let signer: ClientRuntime.Signer = MockSigner() + + public init() {} + + public func customizeSigningProperties(signingProperties: ClientRuntime.Attributes, context: ClientRuntime.HttpContext) -> ClientRuntime.Attributes { + return signingProperties + } +} diff --git a/Sources/SmithyTestUtil/RequestTestUtil/AuthTestUtil/MockIdentity.swift b/Sources/SmithyTestUtil/RequestTestUtil/AuthTestUtil/MockIdentity.swift new file mode 100644 index 000000000..594ac3929 --- /dev/null +++ b/Sources/SmithyTestUtil/RequestTestUtil/AuthTestUtil/MockIdentity.swift @@ -0,0 +1,21 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import ClientRuntime + +public struct MockIdentity: Identity { + public init() {} + public var expiration: ClientRuntime.Date? = nil +} + +public struct MockIdentityResolver: IdentityResolver { + public typealias IdentityT = MockIdentity + public init() {} + public func getIdentity(identityProperties: ClientRuntime.Attributes?) async throws -> MockIdentity { + return MockIdentity() + } +} diff --git a/Sources/SmithyTestUtil/RequestTestUtil/AuthTestUtil/MockSigner.swift b/Sources/SmithyTestUtil/RequestTestUtil/AuthTestUtil/MockSigner.swift new file mode 100644 index 000000000..ac442673b --- /dev/null +++ b/Sources/SmithyTestUtil/RequestTestUtil/AuthTestUtil/MockSigner.swift @@ -0,0 +1,30 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import ClientRuntime +import Foundation + +public struct MockSigner: ClientRuntime.Signer { + public init() {} + + public func signRequest<IdentityT: Identity>( + requestBuilder: ClientRuntime.SdkHttpRequestBuilder, + identity: IdentityT, + signingProperties: ClientRuntime.Attributes + ) async throws -> ClientRuntime.SdkHttpRequestBuilder { + requestBuilder.withHeader(name: "Mock-Authorization", value: "Mock-Signed") + return requestBuilder + } + + public func signEvent( + payload: Data, + previousSignature: String, + signingProperties: Attributes + ) async throws -> SigningResult<EventStream.Message> { + return SigningResult(output: EventStream.Message(), signature: "") + } +} diff --git a/Tests/ClientRuntimeTests/ClientRuntimeTests/NetworkingTests/Http/MiddlewareTests/AuthSchemeMiddlewareTests.swift b/Tests/ClientRuntimeTests/ClientRuntimeTests/NetworkingTests/Http/MiddlewareTests/AuthSchemeMiddlewareTests.swift new file mode 100644 index 000000000..10a481cd6 --- /dev/null +++ b/Tests/ClientRuntimeTests/ClientRuntimeTests/NetworkingTests/Http/MiddlewareTests/AuthSchemeMiddlewareTests.swift @@ -0,0 +1,145 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import XCTest +import SmithyTestUtil +@testable import ClientRuntime + +class AuthSchemeMiddlewareTests: XCTestCase { + private var contextBuilder: HttpContextBuilder! + private var operationStack: OperationStack<MockInput, MockOutput>! + + override func setUp() async throws { + try await super.setUp() + contextBuilder = HttpContextBuilder() + .withAuthSchemeResolver(value: DefaultMockAuthSchemeResolver()) + .withAuthScheme(value: MockNoAuth()) + .withIdentityResolver(value: MockIdentityResolver(), schemeID: "MockAuthSchemeA") + .withIdentityResolver(value: MockIdentityResolver(), schemeID: "MockAuthSchemeB") + .withIdentityResolver(value: MockIdentityResolver(), schemeID: "MockAuthSchemeC") + operationStack = OperationStack<MockInput, MockOutput>(id: "auth scheme middleware test stack") + } + + // Test exception cases + func testNoAuthSchemeResolverConfigured() async throws { + contextBuilder.attributes.remove(key: AttributeKeys.authSchemeResolver) + contextBuilder.withOperation(value: "fillerOp") + do { + try await AssertSelectedAuthSchemeMatches(builtContext: contextBuilder.build(), expectedAuthScheme: "") + } catch ClientError.authError(let message) { + let expected = "No auth scheme resolver has been configured on the service." + XCTAssertEqual(message, expected) + } catch { + XCTFail("Unexpected error thrown: \(error.localizedDescription)") + } + } + + func testNoIdentityResolverConfigured() async throws { + contextBuilder.attributes.remove(key: AttributeKeys.identityResolvers) + contextBuilder.withOperation(value: "fillerOp") + do { + try await AssertSelectedAuthSchemeMatches(builtContext: contextBuilder.build(), expectedAuthScheme: "") + } catch ClientError.authError(let message) { + let expected = "No identity resolver has been configured on the service." + XCTAssertEqual(message, expected) + } catch { + XCTFail("Unexpected error thrown: \(error.localizedDescription)") + } + } + + func testNoAuthSchemeCouldBeLoaded() async throws { + contextBuilder.withOperation(value: "fillerOp") + do { + try await AssertSelectedAuthSchemeMatches(builtContext: contextBuilder.build(), expectedAuthScheme: "") + } catch ClientError.authError(let message) { + let expected = "Could not resolve auth scheme for the operation call. Log: Auth scheme fillerAuth was not enabled for this request." + XCTAssertEqual(message, expected) + } catch { + XCTFail("Unexpected error thrown: \(error.localizedDescription)") + } + } + + // Test success cases + func testOnlyAuthSchemeA() async throws { + let context = contextBuilder + .withOperation(value: "authA") + .withAuthScheme(value: MockAuthSchemeA()) + .build() + try await AssertSelectedAuthSchemeMatches(builtContext: context, expectedAuthScheme: "MockAuthSchemeA") + } + + func testAuthOrderABSelectA() async throws { + let context = contextBuilder + .withOperation(value: "authAB") + .withAuthScheme(value: MockAuthSchemeA()) + .withAuthScheme(value: MockAuthSchemeB()) + .build() + try await AssertSelectedAuthSchemeMatches(builtContext: context, expectedAuthScheme: "MockAuthSchemeA") + } + + func testAuthOrderABSelectB() async throws { + let context = contextBuilder + .withOperation(value: "authAB") + .withAuthScheme(value: MockAuthSchemeB()) + .build() + try await AssertSelectedAuthSchemeMatches(builtContext: context, expectedAuthScheme: "MockAuthSchemeB") + } + + func testAuthOrderABCSelectA() async throws { + let context = contextBuilder + .withOperation(value: "authABC") + .withAuthScheme(value: MockAuthSchemeA()) + .withAuthScheme(value: MockAuthSchemeB()) + .withAuthScheme(value: MockAuthSchemeC()) + .build() + try await AssertSelectedAuthSchemeMatches(builtContext: context, expectedAuthScheme: "MockAuthSchemeA") + } + + func testAuthOrderABCSelectB() async throws { + let context = contextBuilder + .withOperation(value: "authABC") + .withAuthScheme(value: MockAuthSchemeB()) + .withAuthScheme(value: MockAuthSchemeC()) + .build() + try await AssertSelectedAuthSchemeMatches(builtContext: context, expectedAuthScheme: "MockAuthSchemeB") + } + + func testAuthOrderABCSelectC() async throws { + let context = contextBuilder + .withOperation(value: "authABC") + .withAuthScheme(value: MockAuthSchemeC()) + .build() + try await AssertSelectedAuthSchemeMatches(builtContext: context, expectedAuthScheme: "MockAuthSchemeC") + } + + func testAuthOrderABCNoAuthSelectNoAuth() async throws { + let context = contextBuilder + .withOperation(value: "authABCNoAuth") + .build() + try await AssertSelectedAuthSchemeMatches(builtContext: context, expectedAuthScheme: "smithy.api#noAuth") + } + + private func AssertSelectedAuthSchemeMatches( + builtContext: HttpContext, + expectedAuthScheme: String, + file: StaticString = #file, + line: UInt = #line + ) async throws { + operationStack.buildStep.intercept(position: .before, middleware: AuthSchemeMiddleware<MockOutput>()) + + let mockHandler = MockHandler<MockOutput>(handleCallback: { (context, input) in + let selectedAuthScheme = context.getSelectedAuthScheme() + XCTAssertEqual(expectedAuthScheme, selectedAuthScheme?.schemeID, file: file, line: line) + let httpResponse = HttpResponse(body: .noStream, statusCode: HttpStatusCode.ok) + let mockOutput = MockOutput() + let output = OperationOutput<MockOutput>(httpResponse: httpResponse, output: mockOutput) + return output + }) + + _ = try await operationStack.handleMiddleware(context: builtContext, input: MockInput(), next: mockHandler) + } +} diff --git a/Tests/ClientRuntimeTests/ClientRuntimeTests/NetworkingTests/Http/MiddlewareTests/SignerMiddlewareTests.swift b/Tests/ClientRuntimeTests/ClientRuntimeTests/NetworkingTests/Http/MiddlewareTests/SignerMiddlewareTests.swift new file mode 100644 index 000000000..e534be346 --- /dev/null +++ b/Tests/ClientRuntimeTests/ClientRuntimeTests/NetworkingTests/Http/MiddlewareTests/SignerMiddlewareTests.swift @@ -0,0 +1,123 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import XCTest +import SmithyTestUtil +@testable import ClientRuntime + +class SignerMiddlewareTests: XCTestCase { + private var contextBuilder: HttpContextBuilder! + private var operationStack: OperationStack<MockInput, MockOutput>! + + override func setUp() async throws { + try await super.setUp() + contextBuilder = HttpContextBuilder() + operationStack = OperationStack<MockInput, MockOutput>(id: "auth scheme middleware test stack") + } + + // Test exception cases + func testNoSelectedAuthScheme() async throws { + let context = contextBuilder.build() + do { + try await AssertRequestWasSigned(builtContext: context) + } catch ClientError.authError(let message) { + XCTAssertEqual(message, "Auth scheme needed by signer middleware was not saved properly.") + } catch { + XCTFail("Unexpected error thrown: \(error.localizedDescription)") + } + } + + func testNoIdentityInSelectedAuthScheme() async throws { + let context = contextBuilder + .withSelectedAuthScheme(value: SelectedAuthScheme( + schemeID: "mock", + identity: nil, + signingProperties: Attributes(), + signer: MockSigner()) + ) + .build() + do { + try await AssertRequestWasSigned(builtContext: context) + } catch ClientError.authError(let message) { + XCTAssertEqual(message, "Identity needed by signer middleware was not properly saved into loaded auth scheme.") + } catch { + XCTFail("Unexpected error thrown: \(error.localizedDescription)") + } + } + + func testNoSignerInSelectedAuthScheme() async throws { + let context = contextBuilder + .withSelectedAuthScheme(value: SelectedAuthScheme( + schemeID: "mock", + identity: MockIdentity(), + signingProperties: Attributes(), + signer: nil) + ) + .build() + do { + try await AssertRequestWasSigned(builtContext: context) + } catch ClientError.authError(let message) { + XCTAssertEqual(message, "Signer needed by signer middleware was not properly saved into loaded auth scheme.") + } catch { + XCTFail("Unexpected error thrown: \(error.localizedDescription)") + } + } + + func testNoSigningPropertiesInSelectedAuthScheme() async throws { + let context = contextBuilder + .withSelectedAuthScheme(value: SelectedAuthScheme( + schemeID: "mock", + identity: MockIdentity(), + signingProperties: nil, + signer: MockSigner()) + ) + .build() + do { + try await AssertRequestWasSigned(builtContext: context) + } catch ClientError.authError(let message) { + XCTAssertEqual(message, "Signing properties object needed by signer middleware was not properly saved into loaded auth scheme.") + } catch { + XCTFail("Unexpected error thrown: \(error.localizedDescription)") + } + } + + // Test success cases + func testSignedRequest() async throws { + let context = contextBuilder + .withSelectedAuthScheme(value: SelectedAuthScheme( + schemeID: "mock", + identity: MockIdentity(), + signingProperties: Attributes(), + signer: MockSigner()) + ) + .build() + try await AssertRequestWasSigned(builtContext: context) + } + + private func AssertRequestWasSigned( + builtContext: HttpContext, + file: StaticString = #file, + line: UInt = #line + ) async throws { + operationStack.finalizeStep.intercept(position: .before, middleware: SignerMiddleware<MockOutput>()) + + let mockHandler = MockHandler<MockOutput>(handleCallback: { (context, input) in + XCTAssertEqual( + input.headers.value(for: "Mock-Authorization"), + "Mock-Signed", + file: file, + line: line + ) + let httpResponse = HttpResponse(body: .noStream, statusCode: HttpStatusCode.ok) + let mockOutput = MockOutput() + let output = OperationOutput<MockOutput>(httpResponse: httpResponse, output: mockOutput) + return output + }) + + _ = try await operationStack.handleMiddleware(context: builtContext, input: MockInput(), next: mockHandler) + } +} diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/AuthSchemeResolverGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/AuthSchemeResolverGenerator.kt new file mode 100644 index 000000000..bb1b3581c --- /dev/null +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/AuthSchemeResolverGenerator.kt @@ -0,0 +1,292 @@ +package software.amazon.smithy.swift.codegen + +import software.amazon.smithy.aws.traits.ServiceTrait +import software.amazon.smithy.aws.traits.auth.SigV4Trait +import software.amazon.smithy.aws.traits.auth.UnsignedPayloadTrait +import software.amazon.smithy.codegen.core.CodegenException +import software.amazon.smithy.codegen.core.Symbol +import software.amazon.smithy.model.knowledge.ServiceIndex +import software.amazon.smithy.model.shapes.OperationShape +import software.amazon.smithy.model.shapes.ShapeId +import software.amazon.smithy.model.traits.AuthTrait +import software.amazon.smithy.model.traits.OptionalAuthTrait +import software.amazon.smithy.model.traits.Trait +import software.amazon.smithy.rulesengine.language.EndpointRuleSet +import software.amazon.smithy.rulesengine.language.syntax.parameters.Parameter +import software.amazon.smithy.rulesengine.language.syntax.parameters.ParameterType +import software.amazon.smithy.rulesengine.traits.EndpointRuleSetTrait +import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator +import software.amazon.smithy.swift.codegen.model.boxed +import software.amazon.smithy.swift.codegen.model.defaultValue +import software.amazon.smithy.swift.codegen.model.getTrait +import software.amazon.smithy.swift.codegen.utils.clientName +import software.amazon.smithy.swift.codegen.utils.toLowerCamelCase +import java.util.Locale + +class AuthSchemeResolverGenerator { + private val AUTH_SCHEME_RESOLVER = "AuthSchemeResolver" + fun render(ctx: ProtocolGenerator.GenerationContext) { + val rootNamespace = ctx.settings.moduleName + val serviceIndex = ServiceIndex(ctx.model) + + ctx.delegator.useFileWriter("./$rootNamespace/$AUTH_SCHEME_RESOLVER.swift") { + renderServiceSpecificAuthResolverParamsStruct(serviceIndex, ctx, it) + it.write("") + renderServiceSpecificAuthResolverProtocol(ctx, it) + it.write("") + renderServiceSpecificDefaultResolver(serviceIndex, ctx, it) + it.write("") + it.addImport(SwiftDependency.CLIENT_RUNTIME.target) + } + } + + private fun renderServiceSpecificAuthResolverParamsStruct( + serviceIndex: ServiceIndex, + ctx: ProtocolGenerator.GenerationContext, + writer: SwiftWriter + ) { + writer.apply { + openBlock( + "public struct ${getSdkId(ctx)}${ClientRuntimeTypes.Auth.AuthSchemeResolverParams.name}: \$L {", + "}", + ClientRuntimeTypes.Auth.AuthSchemeResolverParams + ) { + write("public let operation: String") + + if (usesRulesBasedAuthResolver(ctx)) { + // For rules based auth scheme resolvers, auth scheme resolver parameters must have + // 1-to-1 mapping of endpoint parameters, since rules based auth scheme resolvers rely on + // endpoint resolver's auth scheme resolution to resolve an auth scheme. + renderEndpointParamFields(ctx, this) + } else { + // If service supports SigV4/SigV4a auth scheme, it's a special-case for now - change once + // it becomes possible at model level to notate custom members for a given auth scheme. + // Region has to be in params in addition to operation string. + if (serviceIndex.getEffectiveAuthSchemes(ctx.service).contains(SigV4Trait.ID)) { + write("// Region is used for SigV4 auth scheme") + write("public let region: String?") + } + } + } + } + } + + private fun renderEndpointParamFields(ctx: ProtocolGenerator.GenerationContext, writer: SwiftWriter) { + writer.apply { + val ruleSetNode = ctx.service.getTrait<EndpointRuleSetTrait>()?.ruleSet + val ruleSet = if (ruleSetNode != null) EndpointRuleSet.fromNode(ruleSetNode) else null + ruleSet?.parameters?.toList()?.sortedBy { it.name.toString() }?.let { sortedParameters -> + sortedParameters.forEach { param -> + val memberName = param.name.toString().toLowerCamelCase() + val memberSymbol = param.toSymbol() + val optional = if (param.isRequired) "" else "?" + param.documentation.orElse(null)?.let { write("/// $it") } + write("public let \$L: \$L$optional", memberName, memberSymbol) + } + } + } + } + + private fun renderServiceSpecificAuthResolverProtocol(ctx: ProtocolGenerator.GenerationContext, writer: SwiftWriter) { + writer.apply { + openBlock( + "public protocol ${getSdkId(ctx)}$AUTH_SCHEME_RESOLVER: \$L {", + "}", + ClientRuntimeTypes.Auth.AuthSchemeResolver + ) { + // This is just a parent protocol that all auth scheme resolvers of a given service must conform to. + write("// Intentionally empty.") + write("// This is the parent protocol that all auth scheme resolver implementations of") + write("// the service ${getSdkId(ctx)} must conform to.") + } + } + } + + private fun renderServiceSpecificDefaultResolver( + serviceIndex: ServiceIndex, + ctx: ProtocolGenerator.GenerationContext, + writer: SwiftWriter + ) { + val sdkId = getSdkId(ctx) + + // Model-based auth scheme resolver is internal implementation detail for services that use rules based resolvers, + // and is used as fallback only if endpoint resolver returns no valid auth scheme(s). + val usesRulesBasedResolver = usesRulesBasedAuthResolver(ctx) + val defaultResolverName = + if (usesRulesBasedResolver) "InternalModeled$sdkId$AUTH_SCHEME_RESOLVER" + else "Default$sdkId$AUTH_SCHEME_RESOLVER" + + // Model-based auth scheme resolver should be private internal impl detail if service uses rules-based resolver. + val accessModifier = if (usesRulesBasedResolver) "private" else "public" + val serviceSpecificAuthResolverProtocol = sdkId + AUTH_SCHEME_RESOLVER + + writer.apply { + writer.openBlock( + "\$L struct \$L: \$L {", + "}", + accessModifier, + defaultResolverName, + serviceSpecificAuthResolverProtocol + ) { + renderResolveAuthSchemeMethod(serviceIndex, ctx, writer) + write("") + renderConstructParametersMethod( + serviceIndex.getEffectiveAuthSchemes(ctx.service).contains(SigV4Trait.ID), + ctx, + writer + ) + } + } + } + + private fun renderResolveAuthSchemeMethod( + serviceIndex: ServiceIndex, + ctx: ProtocolGenerator.GenerationContext, + writer: SwiftWriter + ) { + val sdkId = getSdkId(ctx) + val serviceParamsName = sdkId + ClientRuntimeTypes.Auth.AuthSchemeResolverParams.name + + writer.apply { + openBlock( + "public func resolveAuthScheme(params: \$L) throws -> [AuthOption] {", + "}", + ClientRuntimeTypes.Auth.AuthSchemeResolverParams + ) { + // Return value of array of auth options + write("var validAuthOptions = [AuthOption]()") + // Cast params to service specific params object + openBlock("guard let serviceParams = params as? \$L else {", "}", serviceParamsName) { + write("throw ClientError.authError(\"Service specific auth scheme parameters type must be passed to auth scheme resolver.\")") + } + // Render switch block + renderSwitchBlock(serviceIndex, ctx, writer) + // Return result + write("return validAuthOptions") + } + } + } + + private fun renderSwitchBlock( + serviceIndex: ServiceIndex, + ctx: ProtocolGenerator.GenerationContext, + writer: SwiftWriter + ) { + writer.apply { + // Switch block for iterating over operation name cases + openBlock("switch serviceParams.operation {", "}") { + // Handle each operation name case + val operations = ctx.service.operations + operations.forEach { + val opShape = ctx.model.getShape(it).get() as OperationShape + if ( + opShape.hasTrait(AuthTrait::class.java) || + opShape.hasTrait(OptionalAuthTrait::class.java) || + opShape.hasTrait(UnsignedPayloadTrait::class.java) + ) { + val opName = it.name.toLowerCamelCase() + val validSchemesForOp = serviceIndex.getEffectiveAuthSchemes( + ctx.service, + it, + ServiceIndex.AuthSchemeMode.NO_AUTH_AWARE + ) + write("case \"$opName\":") + renderSwitchCase(validSchemesForOp, writer) + } + } + // Handle default case, where operations default to auth schemes defined on service shape + val validSchemesForService = + serviceIndex.getEffectiveAuthSchemes(ctx.service, ServiceIndex.AuthSchemeMode.NO_AUTH_AWARE) + write("default:") + renderSwitchCase(validSchemesForService, writer) + } + } + } + + private fun renderSwitchCase(schemes: Map<ShapeId, Trait>, writer: SwiftWriter) { + writer.apply { + indent() + schemes.forEach { + if (it.key == SigV4Trait.ID) { + renderSigV4AuthOption(it, writer) + } else { + write("validAuthOptions.append(AuthOption(schemeID: \"${it.key}\"))") + } + } + dedent() + } + } + + private fun renderSigV4AuthOption(scheme: Map.Entry<ShapeId, Trait>, writer: SwiftWriter) { + writer.apply { + write("var sigV4Option = AuthOption(schemeID: \"${scheme.key}\")") + write("sigV4Option.signingProperties.set(key: AttributeKeys.signingName, value: \"${(scheme.value as SigV4Trait).name}\")") + openBlock("guard let region = serviceParams.region else {", "}") { + write("throw ClientError.authError(\"Missing region in auth scheme parameters for SigV4 auth scheme.\")") + } + write("sigV4Option.signingProperties.set(key: AttributeKeys.signingRegion, value: region)") + write("validAuthOptions.append(sigV4Option)") + } + } + + private fun renderConstructParametersMethod( + hasSigV4: Boolean, + ctx: ProtocolGenerator.GenerationContext, + writer: SwiftWriter + ) { + writer.apply { + openBlock( + "public func constructParameters(context: HttpContext) throws -> \$L {", + "}", + ClientRuntimeTypes.Auth.AuthSchemeResolverParams + ) { + if (usesRulesBasedAuthResolver(ctx)) { + write("return try Default${getSdkId(ctx) + AUTH_SCHEME_RESOLVER}().constructParameters(context: context)") + } else { + openBlock("guard let opName = context.getOperation() else {", "}") { + write("throw ClientError.dataNotFound(\"Operation name not configured in middleware context for auth scheme resolver params construction.\")") + } + val paramType = getSdkId(ctx) + ClientRuntimeTypes.Auth.AuthSchemeResolverParams.name + if (hasSigV4) { + write("let opRegion = context.getRegion()") + write("return $paramType(operation: opName, region: opRegion)") + } else { + write("return $paramType(operation: opName)") + } + } + } + } + } + + companion object { + // Utility function for checking if a service relies on endpoint resolver for auth scheme resolution + fun usesRulesBasedAuthResolver(ctx: ProtocolGenerator.GenerationContext): Boolean { + return listOf("s3", "eventbridge", "cloudfront keyvaluestore").contains(ctx.settings.sdkId.lowercase(Locale.US)) + } + + // Utility function for returning sdkId from generation context + fun getSdkId(ctx: ProtocolGenerator.GenerationContext): String { + return if (ctx.service.hasTrait(ServiceTrait::class.java)) + ctx.service.getTrait(ServiceTrait::class.java).get().sdkId.clientName() + else ctx.settings.sdkId.clientName() + } + } +} + +fun Parameter.toSymbol(): Symbol { + val swiftType = when (type) { + ParameterType.STRING -> SwiftTypes.String + ParameterType.BOOLEAN -> SwiftTypes.Bool + else -> throw CodegenException("Unsupported parameter type: $type") + } + var builder = Symbol.builder().name(swiftType.fullName) + if (!isRequired) { + builder = builder.boxed() + } + + default.ifPresent { defaultValue -> + builder.defaultValue(defaultValue.toString()) + } + + return builder.build() +} diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/ClientRuntimeTypes.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/ClientRuntimeTypes.kt index 1b010b03c..6f00e697f 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/ClientRuntimeTypes.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/ClientRuntimeTypes.kt @@ -76,6 +76,8 @@ object ClientRuntimeTypes { val RetryMiddleware = runtimeSymbol("RetryMiddleware") val IdempotencyTokenMiddleware = runtimeSymbol("IdempotencyTokenMiddleware") val NoopHandler = runtimeSymbol("NoopHandler") + val SignerMiddleware = runtimeSymbol("SignerMiddleware") + val AuthSchemeMiddleware = runtimeSymbol("AuthSchemeMiddleware") val BodyMiddleware = runtimeSymbol("BodyMiddleware") val PayloadBodyMiddleware = runtimeSymbol("PayloadBodyMiddleware") val EventStreamBodyMiddleware = runtimeSymbol("EventStreamBodyMiddleware") @@ -92,6 +94,12 @@ object ClientRuntimeTypes { } } + object Auth { + val AuthSchemes = runtimeSymbolWithoutNamespace("[ClientRuntime.AuthScheme]?") + val AuthSchemeResolver = runtimeSymbolWithoutNamespace("ClientRuntime.AuthSchemeResolver") + val AuthSchemeResolverParams = runtimeSymbol("AuthSchemeResolverParameters") + } + object Core { val AttributeKey = runtimeSymbol("AttributeKey") val Endpoint = runtimeSymbol("Endpoint") @@ -124,3 +132,8 @@ private fun runtimeSymbol(name: String): Symbol = buildSymbol { this.namespace = SwiftDependency.CLIENT_RUNTIME.target dependency(SwiftDependency.CLIENT_RUNTIME) } + +private fun runtimeSymbolWithoutNamespace(name: String): Symbol = buildSymbol { + this.name = name + dependency(SwiftDependency.CLIENT_RUNTIME) +} diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/config/DefaultHttpClientConfiguration.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/config/DefaultHttpClientConfiguration.kt index 1805c87a6..d13f7dce9 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/config/DefaultHttpClientConfiguration.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/config/DefaultHttpClientConfiguration.kt @@ -18,5 +18,7 @@ class DefaultHttpClientConfiguration : ClientConfiguration { get() = setOf( ConfigProperty("httpClientEngine", ClientRuntimeTypes.Http.HttpClient, "AWSClientConfigDefaultsProvider.httpClientEngine"), ConfigProperty("httpClientConfiguration", ClientRuntimeTypes.Http.HttpClientConfiguration, "AWSClientConfigDefaultsProvider.httpClientConfiguration"), + ConfigProperty("authSchemes", ClientRuntimeTypes.Auth.AuthSchemes, ""), + ConfigProperty("authSchemeResolver", ClientRuntimeTypes.Auth.AuthSchemeResolver, "") ) } diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/DefaultHttpProtocolCustomizations.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/DefaultHttpProtocolCustomizations.kt index 83cdbc3f5..b897882d9 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/DefaultHttpProtocolCustomizations.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/DefaultHttpProtocolCustomizations.kt @@ -6,6 +6,7 @@ package software.amazon.smithy.swift.codegen.integration import software.amazon.smithy.model.shapes.OperationShape +import software.amazon.smithy.swift.codegen.AuthSchemeResolverGenerator import software.amazon.smithy.swift.codegen.SwiftWriter abstract class DefaultHttpProtocolCustomizations : HttpProtocolCustomizable { @@ -25,4 +26,8 @@ abstract class DefaultHttpProtocolCustomizations : HttpProtocolCustomizable { ) { // Default implementation is no-op } + + override fun renderInternals(ctx: ProtocolGenerator.GenerationContext) { + AuthSchemeResolverGenerator().render(ctx) + } } diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpBindingProtocolGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpBindingProtocolGenerator.kt index c9bf2d37c..e3364a84b 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpBindingProtocolGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpBindingProtocolGenerator.kt @@ -40,6 +40,7 @@ import software.amazon.smithy.swift.codegen.SwiftTypes import software.amazon.smithy.swift.codegen.SwiftWriter import software.amazon.smithy.swift.codegen.integration.codingKeys.CodingKeysGenerator import software.amazon.smithy.swift.codegen.integration.httpResponse.HttpResponseGeneratable +import software.amazon.smithy.swift.codegen.integration.middlewares.AuthSchemeMiddleware import software.amazon.smithy.swift.codegen.integration.middlewares.ContentLengthMiddleware import software.amazon.smithy.swift.codegen.integration.middlewares.ContentMD5Middleware import software.amazon.smithy.swift.codegen.integration.middlewares.ContentTypeMiddleware @@ -52,6 +53,7 @@ import software.amazon.smithy.swift.codegen.integration.middlewares.OperationInp import software.amazon.smithy.swift.codegen.integration.middlewares.OperationInputUrlHostMiddleware import software.amazon.smithy.swift.codegen.integration.middlewares.OperationInputUrlPathMiddleware import software.amazon.smithy.swift.codegen.integration.middlewares.RetryMiddleware +import software.amazon.smithy.swift.codegen.integration.middlewares.SignerMiddleware import software.amazon.smithy.swift.codegen.integration.middlewares.providers.HttpHeaderProvider import software.amazon.smithy.swift.codegen.integration.middlewares.providers.HttpQueryItemProvider import software.amazon.smithy.swift.codegen.integration.middlewares.providers.HttpUrlPathProvider @@ -459,8 +461,11 @@ abstract class HttpBindingProtocolGenerator : ProtocolGenerator { override fun initializeMiddleware(ctx: ProtocolGenerator.GenerationContext) { val resolver = getProtocolHttpBindingResolver(ctx, defaultContentType) for (operation in getHttpBindingOperations(ctx)) { + /* + * Note: the order of middlewares here does not reflect the order of execution in the actual client call. + * The order here simply means the order in which middleware are added to CODEGEN middleware stack. + */ operationMiddleware.appendMiddleware(operation, IdempotencyTokenMiddleware(ctx.model, ctx.symbolProvider)) - operationMiddleware.appendMiddleware(operation, ContentMD5Middleware(ctx.model, ctx.symbolProvider)) operationMiddleware.appendMiddleware(operation, OperationInputUrlPathMiddleware(ctx.model, ctx.symbolProvider, "")) operationMiddleware.appendMiddleware(operation, OperationInputUrlHostMiddleware(ctx.model, ctx.symbolProvider, operation)) @@ -468,15 +473,19 @@ abstract class HttpBindingProtocolGenerator : ProtocolGenerator { operationMiddleware.appendMiddleware(operation, OperationInputQueryItemMiddleware(ctx.model, ctx.symbolProvider)) operationMiddleware.appendMiddleware(operation, ContentTypeMiddleware(ctx.model, ctx.symbolProvider, resolver.determineRequestContentType(operation))) operationMiddleware.appendMiddleware(operation, OperationInputBodyMiddleware(ctx.model, ctx.symbolProvider)) - operationMiddleware.appendMiddleware(operation, ContentLengthMiddleware(ctx.model, shouldRenderEncodableConformance, hasRequiresLengthTrait(ctx, operation), hasUnsignedPayloadTrait(operation))) - operationMiddleware.appendMiddleware(operation, DeserializeMiddleware(ctx.model, ctx.symbolProvider)) operationMiddleware.appendMiddleware(operation, LoggingMiddleware(ctx.model, ctx.symbolProvider)) operationMiddleware.appendMiddleware(operation, RetryMiddleware(ctx.model, ctx.symbolProvider, retryErrorInfoProviderSymbol)) - + operationMiddleware.appendMiddleware(operation, SignerMiddleware(ctx.model, ctx.symbolProvider)) addProtocolSpecificMiddleware(ctx, operation) - + /* + * Auth scheme middleware must be appended to codegen operation stack AFTER protocol specific middleware is appended. + * This is because endpoint middleware must be generated into client operation stack first with position: .before, + * AFTER which auth scheme must be generated into client operation stack with position: .before, which ensures + * auth scheme middleware is executed FIRST before endpoint middleware is executed, as SRA flow defines. + */ + operationMiddleware.appendMiddleware(operation, AuthSchemeMiddleware(ctx.model, ctx.symbolProvider)) for (integration in ctx.integrations) { integration.customizeMiddleware(ctx, operation, operationMiddleware) } diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpProtocolTestGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpProtocolTestGenerator.kt index 941198277..b50939508 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpProtocolTestGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpProtocolTestGenerator.kt @@ -71,8 +71,9 @@ class HttpProtocolTestGenerator( cloned.removeMiddleware(operation, MiddlewareStep.INITIALIZESTEP, "OperationInputUrlHostMiddleware") cloned.removeMiddleware(operation, MiddlewareStep.BUILDSTEP, "EndpointResolverMiddleware") cloned.removeMiddleware(operation, MiddlewareStep.BUILDSTEP, "UserAgentMiddleware") + cloned.removeMiddleware(operation, MiddlewareStep.BUILDSTEP, "AuthSchemeMiddleware") cloned.removeMiddleware(operation, MiddlewareStep.FINALIZESTEP, "RetryMiddleware") - cloned.removeMiddleware(operation, MiddlewareStep.FINALIZESTEP, "AWSSigningMiddleware") // causes tests to halt :( + cloned.removeMiddleware(operation, MiddlewareStep.FINALIZESTEP, "SignerMiddleware") cloned.removeMiddleware(operation, MiddlewareStep.DESERIALIZESTEP, "DeserializeMiddleware") cloned.removeMiddleware(operation, MiddlewareStep.DESERIALIZESTEP, "LoggingMiddleware") diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/AuthSchemeMiddleware.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/AuthSchemeMiddleware.kt new file mode 100644 index 000000000..c30c42ad0 --- /dev/null +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/AuthSchemeMiddleware.kt @@ -0,0 +1,36 @@ +package software.amazon.smithy.swift.codegen.integration.middlewares + +import software.amazon.smithy.codegen.core.SymbolProvider +import software.amazon.smithy.model.Model +import software.amazon.smithy.model.shapes.OperationShape +import software.amazon.smithy.swift.codegen.ClientRuntimeTypes +import software.amazon.smithy.swift.codegen.SwiftWriter +import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator +import software.amazon.smithy.swift.codegen.integration.middlewares.handlers.MiddlewareShapeUtils +import software.amazon.smithy.swift.codegen.middleware.MiddlewarePosition +import software.amazon.smithy.swift.codegen.middleware.MiddlewareRenderable +import software.amazon.smithy.swift.codegen.middleware.MiddlewareStep + +class AuthSchemeMiddleware( + val model: Model, + val symbolProvider: SymbolProvider +) : MiddlewareRenderable { + override val name = "AuthSchemeMiddleware" + + override val middlewareStep = MiddlewareStep.BUILDSTEP + + override val position = MiddlewarePosition.BEFORE + + override fun render( + ctx: ProtocolGenerator.GenerationContext, + writer: SwiftWriter, + op: OperationShape, + operationStackName: String + ) { + val output = MiddlewareShapeUtils.outputSymbol(symbolProvider, model, op) + writer.write( + "$operationStackName.${middlewareStep.stringValue()}.intercept(position: ${position.stringValue()}, middleware: \$N<\$N>())", + ClientRuntimeTypes.Middleware.AuthSchemeMiddleware, output + ) + } +} diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/SignerMiddleware.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/SignerMiddleware.kt new file mode 100644 index 000000000..cdca0ceab --- /dev/null +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/SignerMiddleware.kt @@ -0,0 +1,36 @@ +package software.amazon.smithy.swift.codegen.integration.middlewares + +import software.amazon.smithy.codegen.core.SymbolProvider +import software.amazon.smithy.model.Model +import software.amazon.smithy.model.shapes.OperationShape +import software.amazon.smithy.swift.codegen.ClientRuntimeTypes +import software.amazon.smithy.swift.codegen.SwiftWriter +import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator +import software.amazon.smithy.swift.codegen.integration.middlewares.handlers.MiddlewareShapeUtils +import software.amazon.smithy.swift.codegen.middleware.MiddlewarePosition +import software.amazon.smithy.swift.codegen.middleware.MiddlewareRenderable +import software.amazon.smithy.swift.codegen.middleware.MiddlewareStep + +class SignerMiddleware( + val model: Model, + val symbolProvider: SymbolProvider +) : MiddlewareRenderable { + override val name = "SignerMiddleware" + + override val middlewareStep = MiddlewareStep.FINALIZESTEP + + override val position = MiddlewarePosition.BEFORE + + override fun render( + ctx: ProtocolGenerator.GenerationContext, + writer: SwiftWriter, + op: OperationShape, + operationStackName: String + ) { + val output = MiddlewareShapeUtils.outputSymbol(symbolProvider, model, op) + writer.write( + "$operationStackName.${middlewareStep.stringValue()}.intercept(position: ${position.stringValue()}, middleware: \$N<\$N>())", + ClientRuntimeTypes.Middleware.SignerMiddleware, output + ) + } +} diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/middleware/MiddlewareExecutionGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/middleware/MiddlewareExecutionGenerator.kt index 768ea05f5..5cb492930 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/middleware/MiddlewareExecutionGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/middleware/MiddlewareExecutionGenerator.kt @@ -1,5 +1,6 @@ package software.amazon.smithy.swift.codegen.middleware +import software.amazon.smithy.aws.traits.auth.UnsignedPayloadTrait import software.amazon.smithy.model.Model import software.amazon.smithy.model.shapes.OperationShape import software.amazon.smithy.model.shapes.ServiceShape @@ -14,6 +15,7 @@ import software.amazon.smithy.swift.codegen.integration.serde.readwrite.WireProt import software.amazon.smithy.swift.codegen.integration.serde.readwrite.requestWireProtocol import software.amazon.smithy.swift.codegen.integration.serde.readwrite.responseWireProtocol import software.amazon.smithy.swift.codegen.model.toLowerCamelCase +import software.amazon.smithy.swift.codegen.model.toUpperCamelCase import software.amazon.smithy.swift.codegen.swiftFunctionParameterIndent typealias HttpMethodCallback = (OperationShape) -> String @@ -29,12 +31,18 @@ class MiddlewareExecutionGenerator( private val model: Model = ctx.model private val symbolProvider = ctx.symbolProvider - fun render(service: ServiceShape, op: OperationShape, onError: (SwiftWriter, String) -> Unit) { + fun render( + serviceShape: ServiceShape, + op: OperationShape, + flowType: ContextAttributeCodegenFlowType = ContextAttributeCodegenFlowType.NORMAL, + onError: (SwiftWriter, String) -> Unit, + ) { + val operationErrorName = "${op.toUpperCamelCase()}OutputError" val inputShapeName = MiddlewareShapeUtils.inputSymbol(symbolProvider, ctx.model, op).name val outputShapeName = MiddlewareShapeUtils.outputSymbol(symbolProvider, ctx.model, op).name writer.write("let context = \$N()", ClientRuntimeTypes.Http.HttpContextBuilder) writer.swiftFunctionParameterIndent { - renderContextAttributes(op) + renderContextAttributes(op, flowType) } httpProtocolCustomizable.renderEventStreamAttributes(ctx, writer, op) writer.write( @@ -48,7 +56,7 @@ class MiddlewareExecutionGenerator( renderMiddlewares(ctx, op, operationStackName) } - private fun renderContextAttributes(op: OperationShape) { + private fun renderContextAttributes(op: OperationShape, flowType: ContextAttributeCodegenFlowType) { val httpMethod = resolveHttpMethod(op) // FIXME it over indents if i add another indent, come up with better way to properly indent or format for swift @@ -65,6 +73,20 @@ class MiddlewareExecutionGenerator( writer.write(" .withIdempotencyTokenGenerator(value: config.idempotencyTokenGenerator)") writer.write(" .withLogger(value: config.logger)") writer.write(" .withPartitionID(value: config.partitionID)") + writer.write(" .withAuthSchemes(value: config.authSchemes ?? [])") + writer.write(" .withAuthSchemeResolver(value: config.authSchemeResolver)") + writer.write(" .withUnsignedPayloadTrait(value: ${op.hasTrait(UnsignedPayloadTrait::class.java)})") + + // Add flag for presign / presign-url flows + if (flowType == ContextAttributeCodegenFlowType.PRESIGN_REQUEST) { + writer.write(" .withFlowType(value: .PRESIGN_REQUEST)") + } else if (flowType == ContextAttributeCodegenFlowType.PRESIGN_URL) { + writer.write(" .withFlowType(value: .PRESIGN_URL)") + } + // Add expiration flag for presign / presign-url flows + if (flowType != ContextAttributeCodegenFlowType.NORMAL) { + writer.write(" .withExpiration(value: expiration)") + } val serviceShape = ctx.service httpProtocolCustomizable.renderContextAttributes(ctx, writer, serviceShape, op) @@ -87,4 +109,19 @@ class MiddlewareExecutionGenerator( operationMiddleware.renderMiddleware(ctx, writer, op, operationStackName, MiddlewareStep.FINALIZESTEP) operationMiddleware.renderMiddleware(ctx, writer, op, operationStackName, MiddlewareStep.DESERIALIZESTEP) } + + /* + * The enum in this companion object is used to determine under which codegen flow + * the middleware context is being code-generated. + * + * For PRESIGN_REQUEST & PRESIGN_URL flows: + * - The value of expiration is saved to middleware context during codegen. + * - The flow type information is saved to middleware context during codegen, for consumption by + * AWS auth schemes during runtime to determine where to put the request signature in the request. + */ + companion object { + enum class ContextAttributeCodegenFlowType { + NORMAL, PRESIGN_REQUEST, PRESIGN_URL + } + } } diff --git a/smithy-swift-codegen/src/test/kotlin/AuthSchemeResolverGeneratorTests.kt b/smithy-swift-codegen/src/test/kotlin/AuthSchemeResolverGeneratorTests.kt new file mode 100644 index 000000000..a0788b8ce --- /dev/null +++ b/smithy-swift-codegen/src/test/kotlin/AuthSchemeResolverGeneratorTests.kt @@ -0,0 +1,108 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +import io.kotest.matchers.string.shouldContainOnlyOnce +import org.junit.jupiter.api.Test + +class AuthSchemeResolverGeneratorTests { + @Test + fun `test auth scheme resolver generation`() { + val context = setupTests("auth-scheme-resolver-generator-test.smithy", "com.test#Example") + val contents = getFileContents(context.manifest, "/Example/AuthSchemeResolver.swift") + contents.shouldSyntacticSanityCheck() + contents.shouldContainOnlyOnce( + """ + public struct ExampleAuthSchemeResolverParameters: ClientRuntime.AuthSchemeResolverParameters { + public let operation: String + // Region is used for SigV4 auth scheme + public let region: String? + } + + public protocol ExampleAuthSchemeResolver: ClientRuntime.AuthSchemeResolver { + // Intentionally empty. + // This is the parent protocol that all auth scheme resolver implementations of + // the service Example must conform to. + } + + public struct DefaultExampleAuthSchemeResolver: ExampleAuthSchemeResolver { + public func resolveAuthScheme(params: ClientRuntime.AuthSchemeResolverParameters) throws -> [AuthOption] { + var validAuthOptions = [AuthOption]() + guard let serviceParams = params as? ExampleAuthSchemeResolverParameters else { + throw ClientError.authError("Service specific auth scheme parameters type must be passed to auth scheme resolver.") + } + switch serviceParams.operation { + case "onlyHttpApiKeyAuth": + validAuthOptions.append(AuthOption(schemeID: "smithy.api#httpApiKeyAuth")) + case "onlyHttpApiKeyAuthOptional": + validAuthOptions.append(AuthOption(schemeID: "smithy.api#httpApiKeyAuth")) + validAuthOptions.append(AuthOption(schemeID: "smithy.api#noAuth")) + case "onlyHttpBearerAuth": + validAuthOptions.append(AuthOption(schemeID: "smithy.api#httpBearerAuth")) + case "onlyHttpBearerAuthOptional": + validAuthOptions.append(AuthOption(schemeID: "smithy.api#httpBearerAuth")) + validAuthOptions.append(AuthOption(schemeID: "smithy.api#noAuth")) + case "onlyHttpApiKeyAndBearerAuth": + validAuthOptions.append(AuthOption(schemeID: "smithy.api#httpApiKeyAuth")) + validAuthOptions.append(AuthOption(schemeID: "smithy.api#httpBearerAuth")) + case "onlyHttpApiKeyAndBearerAuthReversed": + validAuthOptions.append(AuthOption(schemeID: "smithy.api#httpBearerAuth")) + validAuthOptions.append(AuthOption(schemeID: "smithy.api#httpApiKeyAuth")) + case "onlySigv4Auth": + var sigV4Option = AuthOption(schemeID: "aws.auth#sigv4") + sigV4Option.signingProperties.set(key: AttributeKeys.signingName, value: "weather") + guard let region = serviceParams.region else { + throw ClientError.authError("Missing region in auth scheme parameters for SigV4 auth scheme.") + } + sigV4Option.signingProperties.set(key: AttributeKeys.signingRegion, value: region) + validAuthOptions.append(sigV4Option) + case "onlySigv4AuthOptional": + var sigV4Option = AuthOption(schemeID: "aws.auth#sigv4") + sigV4Option.signingProperties.set(key: AttributeKeys.signingName, value: "weather") + guard let region = serviceParams.region else { + throw ClientError.authError("Missing region in auth scheme parameters for SigV4 auth scheme.") + } + sigV4Option.signingProperties.set(key: AttributeKeys.signingRegion, value: region) + validAuthOptions.append(sigV4Option) + validAuthOptions.append(AuthOption(schemeID: "smithy.api#noAuth")) + case "onlyCustomAuth": + validAuthOptions.append(AuthOption(schemeID: "com.test#customAuth")) + case "onlyCustomAuthOptional": + validAuthOptions.append(AuthOption(schemeID: "com.test#customAuth")) + validAuthOptions.append(AuthOption(schemeID: "smithy.api#noAuth")) + default: + var sigV4Option = AuthOption(schemeID: "aws.auth#sigv4") + sigV4Option.signingProperties.set(key: AttributeKeys.signingName, value: "weather") + guard let region = serviceParams.region else { + throw ClientError.authError("Missing region in auth scheme parameters for SigV4 auth scheme.") + } + sigV4Option.signingProperties.set(key: AttributeKeys.signingRegion, value: region) + validAuthOptions.append(sigV4Option) + } + return validAuthOptions + } + + public func constructParameters(context: HttpContext) throws -> ClientRuntime.AuthSchemeResolverParameters { + guard let opName = context.getOperation() else { + throw ClientError.dataNotFound("Operation name not configured in middleware context for auth scheme resolver params construction.") + } + let opRegion = context.getRegion() + return ExampleAuthSchemeResolverParameters(operation: opName, region: opRegion) + } + } + """.trimIndent() + ) + } + + private fun setupTests(smithyFile: String, serviceShapeId: String): TestContext { + val context = TestContext.initContextFrom(smithyFile, serviceShapeId, MockHttpRestJsonProtocolGenerator()) { model -> + model.defaultSettings(serviceShapeId, "Example", "2023-11-02", "Example") + } + context.generator.initializeMiddleware(context.generationCtx) + context.generator.generateProtocolClient(context.generationCtx) + context.generator.generateSerializers(context.generationCtx) + context.generationCtx.delegator.flushWriters() + return context + } +} diff --git a/smithy-swift-codegen/src/test/kotlin/ContentMd5MiddlewareTests.kt b/smithy-swift-codegen/src/test/kotlin/ContentMd5MiddlewareTests.kt index 94a8f20c3..1e44a3720 100644 --- a/smithy-swift-codegen/src/test/kotlin/ContentMd5MiddlewareTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/ContentMd5MiddlewareTests.kt @@ -15,16 +15,21 @@ class ContentMd5MiddlewareTests { .withIdempotencyTokenGenerator(value: config.idempotencyTokenGenerator) .withLogger(value: config.logger) .withPartitionID(value: config.partitionID) + .withAuthSchemes(value: config.authSchemes ?? []) + .withAuthSchemeResolver(value: config.authSchemeResolver) + .withUnsignedPayloadTrait(value: false) .build() var operation = ClientRuntime.OperationStack<IdempotencyTokenWithStructureInput, IdempotencyTokenWithStructureOutput>(id: "idempotencyTokenWithStructure") operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.IdempotencyTokenMiddleware<IdempotencyTokenWithStructureInput, IdempotencyTokenWithStructureOutput>(keyPath: \.token)) operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware<IdempotencyTokenWithStructureInput, IdempotencyTokenWithStructureOutput>(IdempotencyTokenWithStructureInput.urlPathProvider(_:))) operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware<IdempotencyTokenWithStructureInput, IdempotencyTokenWithStructureOutput>()) operation.buildStep.intercept(position: .before, middleware: ClientRuntime.ContentMD5Middleware<IdempotencyTokenWithStructureOutput>()) + operation.buildStep.intercept(position: .before, middleware: ClientRuntime.AuthSchemeMiddleware<IdempotencyTokenWithStructureOutput>()) operation.serializeStep.intercept(position: .after, middleware: ContentTypeMiddleware<IdempotencyTokenWithStructureInput, IdempotencyTokenWithStructureOutput>(contentType: "application/xml")) operation.serializeStep.intercept(position: .after, middleware: ClientRuntime.BodyMiddleware<IdempotencyTokenWithStructureInput, IdempotencyTokenWithStructureOutput, SmithyXML.Writer>(documentWritingClosure: SmithyXML.XMLReadWrite.documentWritingClosure(rootNodeInfo: "IdempotencyToken"), inputWritingClosure: IdempotencyTokenWithStructureInput.writingClosure(_:to:))) operation.finalizeStep.intercept(position: .before, middleware: ClientRuntime.ContentLengthMiddleware()) operation.finalizeStep.intercept(position: .after, middleware: ClientRuntime.RetryMiddleware<ClientRuntime.DefaultRetryStrategy, ClientRuntime.DefaultRetryErrorInfoProvider, IdempotencyTokenWithStructureOutput>(options: config.retryStrategyOptions)) + operation.finalizeStep.intercept(position: .before, middleware: ClientRuntime.SignerMiddleware<IdempotencyTokenWithStructureOutput>()) operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.DeserializeMiddleware<IdempotencyTokenWithStructureOutput>(responseClosure(IdempotencyTokenWithStructureOutput.httpBinding, responseDocumentBinding), responseErrorClosure(IdempotencyTokenWithStructureOutputError.httpBinding, responseDocumentBinding))) operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.LoggerMiddleware<IdempotencyTokenWithStructureOutput>(clientLogMode: config.clientLogMode)) let result = try await operation.handleMiddleware(context: context, input: input, next: client.getHandler()) diff --git a/smithy-swift-codegen/src/test/kotlin/HttpProtocolClientGeneratorTests.kt b/smithy-swift-codegen/src/test/kotlin/HttpProtocolClientGeneratorTests.kt index 7f7b4cb8b..b74d2b04b 100644 --- a/smithy-swift-codegen/src/test/kotlin/HttpProtocolClientGeneratorTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/HttpProtocolClientGeneratorTests.kt @@ -6,9 +6,7 @@ import io.kotest.matchers.string.shouldContain import io.kotest.matchers.string.shouldContainOnlyOnce import org.junit.jupiter.api.Test - class HttpProtocolClientGeneratorTests { - @Test fun `it renders client initialization block`() { val context = setupTests("service-generator-test-operations.smithy", "com.test#Example") @@ -69,7 +67,6 @@ class HttpProtocolClientGeneratorTests { """.trimIndent() ) } - @Test fun `it renders host prefix with label in context correctly`() { val context = setupTests("host-prefix-operation.smithy", "com.test#Example") @@ -87,7 +84,6 @@ class HttpProtocolClientGeneratorTests { """ contents.shouldContainOnlyOnce(expectedFragment) } - @Test fun `it renders operation implementations in extension`() { val context = setupTests("service-generator-test-operations.smithy", "com.test#Example") @@ -95,7 +91,6 @@ class HttpProtocolClientGeneratorTests { contents.shouldSyntacticSanityCheck() contents.shouldContain("extension RestJsonProtocolClient {") } - @Test fun `it renders an operation body`() { val context = setupTests("service-generator-test-operations.smithy", "com.test#Example") @@ -112,15 +107,20 @@ class HttpProtocolClientGeneratorTests { .withIdempotencyTokenGenerator(value: config.idempotencyTokenGenerator) .withLogger(value: config.logger) .withPartitionID(value: config.partitionID) + .withAuthSchemes(value: config.authSchemes ?? []) + .withAuthSchemeResolver(value: config.authSchemeResolver) + .withUnsignedPayloadTrait(value: false) .build() var operation = ClientRuntime.OperationStack<AllocateWidgetInput, AllocateWidgetOutput>(id: "allocateWidget") operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.IdempotencyTokenMiddleware<AllocateWidgetInput, AllocateWidgetOutput>(keyPath: \.clientToken)) operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware<AllocateWidgetInput, AllocateWidgetOutput>(AllocateWidgetInput.urlPathProvider(_:))) operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware<AllocateWidgetInput, AllocateWidgetOutput>()) + operation.buildStep.intercept(position: .before, middleware: ClientRuntime.AuthSchemeMiddleware<AllocateWidgetOutput>()) operation.serializeStep.intercept(position: .after, middleware: ContentTypeMiddleware<AllocateWidgetInput, AllocateWidgetOutput>(contentType: "application/json")) operation.serializeStep.intercept(position: .after, middleware: ClientRuntime.BodyMiddleware<AllocateWidgetInput, AllocateWidgetOutput, ClientRuntime.JSONWriter>(documentWritingClosure: ClientRuntime.JSONReadWrite.documentWritingClosure(encoder: encoder), inputWritingClosure: JSONReadWrite.writingClosure())) operation.finalizeStep.intercept(position: .before, middleware: ClientRuntime.ContentLengthMiddleware()) operation.finalizeStep.intercept(position: .after, middleware: ClientRuntime.RetryMiddleware<ClientRuntime.DefaultRetryStrategy, ClientRuntime.DefaultRetryErrorInfoProvider, AllocateWidgetOutput>(options: config.retryStrategyOptions)) + operation.finalizeStep.intercept(position: .before, middleware: ClientRuntime.SignerMiddleware<AllocateWidgetOutput>()) operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.DeserializeMiddleware<AllocateWidgetOutput>(responseClosure(decoder: decoder), responseErrorClosure(AllocateWidgetOutputError.self, decoder: decoder))) operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.LoggerMiddleware<AllocateWidgetOutput>(clientLogMode: config.clientLogMode)) let result = try await operation.handleMiddleware(context: context, input: input, next: client.getHandler()) @@ -146,14 +146,19 @@ class HttpProtocolClientGeneratorTests { .withIdempotencyTokenGenerator(value: config.idempotencyTokenGenerator) .withLogger(value: config.logger) .withPartitionID(value: config.partitionID) + .withAuthSchemes(value: config.authSchemes ?? []) + .withAuthSchemeResolver(value: config.authSchemeResolver) + .withUnsignedPayloadTrait(value: true) .build() var operation = ClientRuntime.OperationStack<UnsignedFooBlobStreamInput, UnsignedFooBlobStreamOutput>(id: "unsignedFooBlobStream") operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware<UnsignedFooBlobStreamInput, UnsignedFooBlobStreamOutput>(UnsignedFooBlobStreamInput.urlPathProvider(_:))) operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware<UnsignedFooBlobStreamInput, UnsignedFooBlobStreamOutput>()) + operation.buildStep.intercept(position: .before, middleware: ClientRuntime.AuthSchemeMiddleware<UnsignedFooBlobStreamOutput>()) operation.serializeStep.intercept(position: .after, middleware: ContentTypeMiddleware<UnsignedFooBlobStreamInput, UnsignedFooBlobStreamOutput>(contentType: "application/json")) operation.serializeStep.intercept(position: .after, middleware: ClientRuntime.BodyMiddleware<UnsignedFooBlobStreamInput, UnsignedFooBlobStreamOutput, ClientRuntime.JSONWriter>(documentWritingClosure: ClientRuntime.JSONReadWrite.documentWritingClosure(encoder: encoder), inputWritingClosure: JSONReadWrite.writingClosure())) operation.finalizeStep.intercept(position: .before, middleware: ClientRuntime.ContentLengthMiddleware(requiresLength: false, unsignedPayload: true)) operation.finalizeStep.intercept(position: .after, middleware: ClientRuntime.RetryMiddleware<ClientRuntime.DefaultRetryStrategy, ClientRuntime.DefaultRetryErrorInfoProvider, UnsignedFooBlobStreamOutput>(options: config.retryStrategyOptions)) + operation.finalizeStep.intercept(position: .before, middleware: ClientRuntime.SignerMiddleware<UnsignedFooBlobStreamOutput>()) operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.DeserializeMiddleware<UnsignedFooBlobStreamOutput>(responseClosure(decoder: decoder), responseErrorClosure(UnsignedFooBlobStreamOutputError.self, decoder: decoder))) operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.LoggerMiddleware<UnsignedFooBlobStreamOutput>(clientLogMode: config.clientLogMode)) let result = try await operation.handleMiddleware(context: context, input: input, next: client.getHandler()) @@ -179,14 +184,19 @@ class HttpProtocolClientGeneratorTests { .withIdempotencyTokenGenerator(value: config.idempotencyTokenGenerator) .withLogger(value: config.logger) .withPartitionID(value: config.partitionID) + .withAuthSchemes(value: config.authSchemes ?? []) + .withAuthSchemeResolver(value: config.authSchemeResolver) + .withUnsignedPayloadTrait(value: true) .build() var operation = ClientRuntime.OperationStack<UnsignedFooBlobStreamWithLengthInput, UnsignedFooBlobStreamWithLengthOutput>(id: "unsignedFooBlobStreamWithLength") operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware<UnsignedFooBlobStreamWithLengthInput, UnsignedFooBlobStreamWithLengthOutput>(UnsignedFooBlobStreamWithLengthInput.urlPathProvider(_:))) operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware<UnsignedFooBlobStreamWithLengthInput, UnsignedFooBlobStreamWithLengthOutput>()) + operation.buildStep.intercept(position: .before, middleware: ClientRuntime.AuthSchemeMiddleware<UnsignedFooBlobStreamWithLengthOutput>()) operation.serializeStep.intercept(position: .after, middleware: ContentTypeMiddleware<UnsignedFooBlobStreamWithLengthInput, UnsignedFooBlobStreamWithLengthOutput>(contentType: "application/octet-stream")) operation.serializeStep.intercept(position: .after, middleware: ClientRuntime.BlobStreamBodyMiddleware<UnsignedFooBlobStreamWithLengthInput, UnsignedFooBlobStreamWithLengthOutput>(keyPath: \.payload1)) operation.finalizeStep.intercept(position: .before, middleware: ClientRuntime.ContentLengthMiddleware(requiresLength: true, unsignedPayload: true)) operation.finalizeStep.intercept(position: .after, middleware: ClientRuntime.RetryMiddleware<ClientRuntime.DefaultRetryStrategy, ClientRuntime.DefaultRetryErrorInfoProvider, UnsignedFooBlobStreamWithLengthOutput>(options: config.retryStrategyOptions)) + operation.finalizeStep.intercept(position: .before, middleware: ClientRuntime.SignerMiddleware<UnsignedFooBlobStreamWithLengthOutput>()) operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.DeserializeMiddleware<UnsignedFooBlobStreamWithLengthOutput>(responseClosure(decoder: decoder), responseErrorClosure(UnsignedFooBlobStreamWithLengthOutputError.self, decoder: decoder))) operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.LoggerMiddleware<UnsignedFooBlobStreamWithLengthOutput>(clientLogMode: config.clientLogMode)) let result = try await operation.handleMiddleware(context: context, input: input, next: client.getHandler()) @@ -212,14 +222,19 @@ class HttpProtocolClientGeneratorTests { .withIdempotencyTokenGenerator(value: config.idempotencyTokenGenerator) .withLogger(value: config.logger) .withPartitionID(value: config.partitionID) + .withAuthSchemes(value: config.authSchemes ?? []) + .withAuthSchemeResolver(value: config.authSchemeResolver) + .withUnsignedPayloadTrait(value: true) .build() var operation = ClientRuntime.OperationStack<UnsignedFooBlobStreamWithLengthInput, UnsignedFooBlobStreamWithLengthOutput>(id: "unsignedFooBlobStreamWithLength") operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware<UnsignedFooBlobStreamWithLengthInput, UnsignedFooBlobStreamWithLengthOutput>(UnsignedFooBlobStreamWithLengthInput.urlPathProvider(_:))) operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware<UnsignedFooBlobStreamWithLengthInput, UnsignedFooBlobStreamWithLengthOutput>()) + operation.buildStep.intercept(position: .before, middleware: ClientRuntime.AuthSchemeMiddleware<UnsignedFooBlobStreamWithLengthOutput>()) operation.serializeStep.intercept(position: .after, middleware: ContentTypeMiddleware<UnsignedFooBlobStreamWithLengthInput, UnsignedFooBlobStreamWithLengthOutput>(contentType: "application/octet-stream")) operation.serializeStep.intercept(position: .after, middleware: ClientRuntime.BlobStreamBodyMiddleware<UnsignedFooBlobStreamWithLengthInput, UnsignedFooBlobStreamWithLengthOutput>(keyPath: \.payload1)) operation.finalizeStep.intercept(position: .before, middleware: ClientRuntime.ContentLengthMiddleware(requiresLength: true, unsignedPayload: true)) operation.finalizeStep.intercept(position: .after, middleware: ClientRuntime.RetryMiddleware<ClientRuntime.DefaultRetryStrategy, ClientRuntime.DefaultRetryErrorInfoProvider, UnsignedFooBlobStreamWithLengthOutput>(options: config.retryStrategyOptions)) + operation.finalizeStep.intercept(position: .before, middleware: ClientRuntime.SignerMiddleware<UnsignedFooBlobStreamWithLengthOutput>()) operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.DeserializeMiddleware<UnsignedFooBlobStreamWithLengthOutput>(responseClosure(decoder: decoder), responseErrorClosure(UnsignedFooBlobStreamWithLengthOutputError.self, decoder: decoder))) operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.LoggerMiddleware<UnsignedFooBlobStreamWithLengthOutput>(clientLogMode: config.clientLogMode)) let result = try await operation.handleMiddleware(context: context, input: input, next: client.getHandler()) @@ -228,7 +243,6 @@ class HttpProtocolClientGeneratorTests { """ contents.shouldContainOnlyOnce(expected) } - private fun setupTests(smithyFile: String, serviceShapeId: String): TestContext { val context = TestContext.initContextFrom(smithyFile, serviceShapeId, MockHttpRestJsonProtocolGenerator()) { model -> model.defaultSettings(serviceShapeId, "RestJson", "2019-12-16", "Rest Json Protocol") diff --git a/smithy-swift-codegen/src/test/kotlin/IdempotencyTokenTraitTests.kt b/smithy-swift-codegen/src/test/kotlin/IdempotencyTokenTraitTests.kt index d4bd21a57..b6a3387e0 100644 --- a/smithy-swift-codegen/src/test/kotlin/IdempotencyTokenTraitTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/IdempotencyTokenTraitTests.kt @@ -1,6 +1,5 @@ import io.kotest.matchers.string.shouldContainOnlyOnce import org.junit.jupiter.api.Test - class IdempotencyTokenTraitTests { @Test fun `generates idempotent middleware`() { @@ -15,15 +14,20 @@ class IdempotencyTokenTraitTests { .withIdempotencyTokenGenerator(value: config.idempotencyTokenGenerator) .withLogger(value: config.logger) .withPartitionID(value: config.partitionID) + .withAuthSchemes(value: config.authSchemes ?? []) + .withAuthSchemeResolver(value: config.authSchemeResolver) + .withUnsignedPayloadTrait(value: false) .build() var operation = ClientRuntime.OperationStack<IdempotencyTokenWithStructureInput, IdempotencyTokenWithStructureOutput>(id: "idempotencyTokenWithStructure") operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.IdempotencyTokenMiddleware<IdempotencyTokenWithStructureInput, IdempotencyTokenWithStructureOutput>(keyPath: \.token)) operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware<IdempotencyTokenWithStructureInput, IdempotencyTokenWithStructureOutput>(IdempotencyTokenWithStructureInput.urlPathProvider(_:))) operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware<IdempotencyTokenWithStructureInput, IdempotencyTokenWithStructureOutput>()) + operation.buildStep.intercept(position: .before, middleware: ClientRuntime.AuthSchemeMiddleware<IdempotencyTokenWithStructureOutput>()) operation.serializeStep.intercept(position: .after, middleware: ContentTypeMiddleware<IdempotencyTokenWithStructureInput, IdempotencyTokenWithStructureOutput>(contentType: "application/xml")) operation.serializeStep.intercept(position: .after, middleware: ClientRuntime.BodyMiddleware<IdempotencyTokenWithStructureInput, IdempotencyTokenWithStructureOutput, SmithyXML.Writer>(documentWritingClosure: SmithyXML.XMLReadWrite.documentWritingClosure(rootNodeInfo: "IdempotencyToken"), inputWritingClosure: IdempotencyTokenWithStructureInput.writingClosure(_:to:))) operation.finalizeStep.intercept(position: .before, middleware: ClientRuntime.ContentLengthMiddleware()) operation.finalizeStep.intercept(position: .after, middleware: ClientRuntime.RetryMiddleware<ClientRuntime.DefaultRetryStrategy, ClientRuntime.DefaultRetryErrorInfoProvider, IdempotencyTokenWithStructureOutput>(options: config.retryStrategyOptions)) + operation.finalizeStep.intercept(position: .before, middleware: ClientRuntime.SignerMiddleware<IdempotencyTokenWithStructureOutput>()) operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.DeserializeMiddleware<IdempotencyTokenWithStructureOutput>(responseClosure(IdempotencyTokenWithStructureOutput.httpBinding, responseDocumentBinding), responseErrorClosure(IdempotencyTokenWithStructureOutputError.httpBinding, responseDocumentBinding))) operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.LoggerMiddleware<IdempotencyTokenWithStructureOutput>(clientLogMode: config.clientLogMode)) let result = try await operation.handleMiddleware(context: context, input: input, next: client.getHandler()) diff --git a/smithy-swift-codegen/src/test/resources/auth-scheme-resolver-generator-test.smithy b/smithy-swift-codegen/src/test/resources/auth-scheme-resolver-generator-test.smithy new file mode 100644 index 000000000..9bca46df4 --- /dev/null +++ b/smithy-swift-codegen/src/test/resources/auth-scheme-resolver-generator-test.smithy @@ -0,0 +1,85 @@ +$version: "2.0" + +namespace com.test + +use aws.auth#sigv4 +use aws.protocols#restJson1 + +@authDefinition +@trait +structure customAuth {} + +@restJson1 +@httpApiKeyAuth(name: "X-Api-Key", in: "header") +@httpBearerAuth +@sigv4(name: "weather") +@customAuth +@auth([sigv4]) +service Example { + version: "2023-11-02" + operations: [ + // experimentalIdentityAndAuth + OnlyHttpApiKeyAuth + OnlyHttpApiKeyAuthOptional + OnlyHttpBearerAuth + OnlyHttpBearerAuthOptional + OnlyHttpApiKeyAndBearerAuth + OnlyHttpApiKeyAndBearerAuthReversed + OnlySigv4Auth + OnlySigv4AuthOptional + OnlyCustomAuth + OnlyCustomAuthOptional + SameAsService + ] +} + +@http(method: "GET", uri: "/OnlyHttpApiKeyAuth") +@auth([httpApiKeyAuth]) +operation OnlyHttpApiKeyAuth {} + +@http(method: "GET", uri: "/OnlyHttpBearerAuth") +@auth([httpBearerAuth]) +operation OnlyHttpBearerAuth {} + +@http(method: "GET", uri: "/OnlySigv4Auth") +@auth([sigv4]) +operation OnlySigv4Auth {} + +@http(method: "GET", uri: "/OnlyHttpApiKeyAndBearerAuth") +@auth([httpApiKeyAuth, httpBearerAuth]) +operation OnlyHttpApiKeyAndBearerAuth {} + +@http(method: "GET", uri: "/OnlyHttpApiKeyAndBearerAuthReversed") +@auth([httpBearerAuth, httpApiKeyAuth]) +operation OnlyHttpApiKeyAndBearerAuthReversed {} + +@http(method: "GET", uri: "/OnlyHttpApiKeyAuthOptional") +@auth([httpApiKeyAuth]) +@optionalAuth +operation OnlyHttpApiKeyAuthOptional {} + +@http(method: "GET", uri: "/OnlyHttpBearerAuthOptional") +@auth([httpBearerAuth]) +@optionalAuth +operation OnlyHttpBearerAuthOptional {} + +@http(method: "GET", uri: "/OnlySigv4AuthOptional") +@auth([sigv4]) +@optionalAuth +operation OnlySigv4AuthOptional {} + +@http(method: "GET", uri: "/OnlyCustomAuth") +@auth([customAuth]) +operation OnlyCustomAuth {} + +@http(method: "GET", uri: "/OnlyCustomAuthOptional") +@auth([customAuth]) +@optionalAuth +operation OnlyCustomAuthOptional {} + +@http(method: "GET", uri: "/SameAsService") +operation SameAsService { + output := { + service: String + } +} \ No newline at end of file