diff --git a/Package.swift b/Package.swift index 2aeb077b2..4c71074f2 100644 --- a/Package.swift +++ b/Package.swift @@ -34,7 +34,7 @@ let package = Package( .library(name: "SmithyTestUtil", targets: ["SmithyTestUtil"]), ], dependencies: [ - .package(url: "https://github.com/awslabs/aws-crt-swift.git", exact: "0.22.0"), + .package(url: "https://github.com/awslabs/aws-crt-swift.git", exact: "0.26.0"), .package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"), .package(url: "https://github.com/MaxDesiatov/XMLCoder.git", exact: "0.17.0") ], diff --git a/Sources/ClientRuntime/Networking/Http/Middlewares/AuthSchemeMiddleware.swift b/Sources/ClientRuntime/Networking/Http/Middlewares/AuthSchemeMiddleware.swift index cd6863378..76c481ef0 100644 --- a/Sources/ClientRuntime/Networking/Http/Middlewares/AuthSchemeMiddleware.swift +++ b/Sources/ClientRuntime/Networking/Http/Middlewares/AuthSchemeMiddleware.swift @@ -100,8 +100,7 @@ public struct AuthSchemeMiddleware: 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/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() + 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..615a5059c --- /dev/null +++ b/Sources/SmithyTestUtil/RequestTestUtil/AuthTestUtil/MockAuthSchemes.swift @@ -0,0 +1,56 @@ +// +// 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 let idKind: ClientRuntime.IdentityKind = .aws + + 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 let idKind: ClientRuntime.IdentityKind = .aws + + 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 let idKind: ClientRuntime.IdentityKind = .aws + + 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 let idKind: ClientRuntime.IdentityKind = .aws + + 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( + 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 { + 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..74dbb1578 --- /dev/null +++ b/Tests/ClientRuntimeTests/ClientRuntimeTests/NetworkingTests/Http/MiddlewareTests/AuthSchemeMiddlewareTests.swift @@ -0,0 +1,138 @@ +// +// 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! + + override func setUp() async throws { + try await super.setUp() + contextBuilder = HttpContextBuilder() + .withAuthSchemeResolver(value: DefaultMockAuthSchemeResolver()) + .withAuthScheme(value: MockNoAuth()) + .withIdentityResolver(value: MockIdentityResolver(), type: .aws) + operationStack = OperationStack(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 testAuthOderABCNoAuthSelectNoAuth() 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) async throws { + operationStack.buildStep.intercept(position: .before, middleware: AuthSchemeMiddleware()) + + let mockHandler = MockHandler(handleCallback: { (context, input) in + let selectedAuthScheme = context.getSelectedAuthScheme() + XCTAssertEqual(expectedAuthScheme, selectedAuthScheme?.schemeID) + let httpResponse = HttpResponse(body: .noStream, statusCode: HttpStatusCode.ok) + let mockOutput = try! MockOutput(httpResponse: httpResponse, decoder: nil) + let output = OperationOutput(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..c36c0b4ee --- /dev/null +++ b/Tests/ClientRuntimeTests/ClientRuntimeTests/NetworkingTests/Http/MiddlewareTests/SignerMiddlewareTests.swift @@ -0,0 +1,114 @@ +// +// 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! + + override func setUp() async throws { + try await super.setUp() + contextBuilder = HttpContextBuilder() + operationStack = OperationStack(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) async throws { + operationStack.finalizeStep.intercept(position: .before, middleware: SignerMiddleware()) + + let mockHandler = MockHandler(handleCallback: { (context, input) in + XCTAssertEqual(input.headers.value(for: "Mock-Authorization"), "Mock-Signed") + let httpResponse = HttpResponse(body: .noStream, statusCode: HttpStatusCode.ok) + let mockOutput = try! MockOutput(httpResponse: httpResponse, decoder: nil) + let output = OperationOutput(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 index 6b0f15059..2b86ed118 100644 --- 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 @@ -260,7 +260,7 @@ class AuthSchemeResolverGenerator { 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").contains(ctx.settings.sdkId.lowercase(Locale.US)) + return listOf("s3", "eventbridge", "cloudfront keyvaluestore").contains(ctx.settings.sdkId.lowercase(Locale.US)) } // Utility function for returning sdkId from generation context diff --git a/smithy-swift-codegen/src/test/kotlin/HttpProtocolUnitTestRequestGeneratorTests.kt b/smithy-swift-codegen/src/test/kotlin/HttpProtocolUnitTestRequestGeneratorTests.kt index 8a4457262..e5b4309d5 100644 --- a/smithy-swift-codegen/src/test/kotlin/HttpProtocolUnitTestRequestGeneratorTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/HttpProtocolUnitTestRequestGeneratorTests.kt @@ -776,7 +776,6 @@ class HttpProtocolUnitTestRequestGeneratorTests { throw serviceError }) } - } """ contents.shouldContainOnlyOnce(expectedContents) }