-
Notifications
You must be signed in to change notification settings - Fork 199
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Add AppSync components (#3825)
* add Amplify components for AppSync * Add AWSAppSyncConfigurationTests * Add signing tests * remove unnecessary public apis * add doc comments * Update API dumps for new version --------- Co-authored-by: aws-amplify-ops <[email protected]>
- Loading branch information
1 parent
bdfa37a
commit 673a075
Showing
12 changed files
with
576 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
140 changes: 140 additions & 0 deletions
140
AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/AWSCognitoAuthPlugin+AppSyncSigner.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
// | ||
// Copyright Amazon.com Inc. or its affiliates. | ||
// All Rights Reserved. | ||
// | ||
// SPDX-License-Identifier: Apache-2.0 | ||
// | ||
|
||
import Foundation | ||
import Amplify // Amplify.Auth | ||
import AWSPluginsCore // AuthAWSCredentialsProvider | ||
import AWSClientRuntime // AWSClientRuntime.CredentialsProviding | ||
import ClientRuntime // SdkHttpRequestBuilder | ||
import AwsCommonRuntimeKit // CommonRuntimeKit.initialize() | ||
|
||
extension AWSCognitoAuthPlugin { | ||
|
||
|
||
/// Creates a AWS IAM SigV4 signer capable of signing AWS AppSync requests. | ||
/// | ||
/// **Note**. Although this method is static, **Amplify.Auth** is required to be configured with **AWSCognitoAuthPlugin** as | ||
/// it depends on the credentials provider from Cognito through `Amplify.Auth.fetchAuthSession()`. The static type allows | ||
/// developers to simplify their callsite without having to access the method on the plugin instance. | ||
/// | ||
/// - Parameter region: The region of the AWS AppSync API | ||
/// - Returns: A closure that takes in a requestand returns a signed request. | ||
public static func createAppSyncSigner(region: String) -> ((URLRequest) async throws -> URLRequest) { | ||
return { request in | ||
try await signAppSyncRequest(request, | ||
region: region) | ||
} | ||
} | ||
|
||
static func signAppSyncRequest(_ urlRequest: URLRequest, | ||
region: Swift.String, | ||
signingName: Swift.String = "appsync", | ||
date: ClientRuntime.Date = Date()) async throws -> URLRequest { | ||
CommonRuntimeKit.initialize() | ||
|
||
// Convert URLRequest to SDK's HTTPRequest | ||
guard let requestBuilder = try createAppSyncSdkHttpRequestBuilder( | ||
urlRequest: urlRequest) else { | ||
return urlRequest | ||
} | ||
|
||
// Retrieve the credentials from credentials provider | ||
let credentials: AWSClientRuntime.AWSCredentials | ||
let authSession = try await Amplify.Auth.fetchAuthSession() | ||
if let awsCredentialsProvider = authSession as? AuthAWSCredentialsProvider { | ||
let awsCredentials = try awsCredentialsProvider.getAWSCredentials().get() | ||
credentials = awsCredentials.toAWSSDKCredentials() | ||
} else { | ||
let error = AuthError.unknown("Auth session does not include AWS credentials information") | ||
throw error | ||
} | ||
|
||
// Prepare signing | ||
let flags = SigningFlags(useDoubleURIEncode: true, | ||
shouldNormalizeURIPath: true, | ||
omitSessionToken: false) | ||
let signedBodyHeader: AWSSignedBodyHeader = .none | ||
let signedBodyValue: AWSSignedBodyValue = .empty | ||
let signingConfig = AWSSigningConfig(credentials: credentials, | ||
signedBodyHeader: signedBodyHeader, | ||
signedBodyValue: signedBodyValue, | ||
flags: flags, | ||
date: date, | ||
service: signingName, | ||
region: region, | ||
signatureType: .requestHeaders, | ||
signingAlgorithm: .sigv4) | ||
|
||
// Sign request | ||
guard let httpRequest = await AWSSigV4Signer.sigV4SignedRequest( | ||
requestBuilder: requestBuilder, | ||
|
||
signingConfig: signingConfig | ||
) else { | ||
return urlRequest | ||
} | ||
|
||
// Update original request with new headers | ||
return setHeaders(from: httpRequest, to: urlRequest) | ||
} | ||
|
||
static func setHeaders(from sdkRequest: SdkHttpRequest, to urlRequest: URLRequest) -> URLRequest { | ||
var urlRequest = urlRequest | ||
for header in sdkRequest.headers.headers { | ||
urlRequest.setValue(header.value.joined(separator: ","), forHTTPHeaderField: header.name) | ||
} | ||
return urlRequest | ||
} | ||
|
||
static func createAppSyncSdkHttpRequestBuilder(urlRequest: URLRequest) throws -> SdkHttpRequestBuilder? { | ||
|
||
guard let url = urlRequest.url, | ||
let host = url.host else { | ||
return nil | ||
} | ||
|
||
var headers = urlRequest.allHTTPHeaderFields ?? [:] | ||
headers.updateValue(host, forKey: "host") | ||
|
||
let httpMethod = (urlRequest.httpMethod?.uppercased()) | ||
.flatMap(HttpMethodType.init(rawValue:)) ?? .get | ||
|
||
let queryItems = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems? | ||
.map { ClientRuntime.SDKURLQueryItem(name: $0.name, value: $0.value)} ?? [] | ||
|
||
let requestBuilder = SdkHttpRequestBuilder() | ||
.withHost(host) | ||
.withPath(url.path) | ||
.withQueryItems(queryItems) | ||
.withMethod(httpMethod) | ||
.withPort(443) | ||
.withProtocol(.https) | ||
.withHeaders(.init(headers)) | ||
.withBody(.data(urlRequest.httpBody)) | ||
|
||
return requestBuilder | ||
} | ||
} | ||
|
||
extension AWSPluginsCore.AWSCredentials { | ||
|
||
func toAWSSDKCredentials() -> AWSClientRuntime.AWSCredentials { | ||
if let tempCredentials = self as? AWSTemporaryCredentials { | ||
return AWSClientRuntime.AWSCredentials( | ||
accessKey: tempCredentials.accessKeyId, | ||
secret: tempCredentials.secretAccessKey, | ||
expirationTimeout: tempCredentials.expiration, | ||
sessionToken: tempCredentials.sessionToken) | ||
} else { | ||
return AWSClientRuntime.AWSCredentials( | ||
accessKey: accessKeyId, | ||
secret: secretAccessKey, | ||
expirationTimeout: Date()) | ||
} | ||
|
||
} | ||
} |
40 changes: 40 additions & 0 deletions
40
...ins/Auth/Tests/AWSCognitoAuthPluginUnitTests/AWSCognitoAuthPluginAppSyncSignerTests.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
// | ||
// Copyright Amazon.com Inc. or its affiliates. | ||
// All Rights Reserved. | ||
// | ||
// SPDX-License-Identifier: Apache-2.0 | ||
// | ||
|
||
import XCTest | ||
@testable import Amplify | ||
@testable import AWSCognitoAuthPlugin | ||
|
||
class AWSCognitoAuthPluginAppSyncSignerTests: XCTestCase { | ||
|
||
/// Tests translating the URLRequest to the SDKRequest | ||
/// The translation should account for expected fields, as asserted in the test. | ||
func testCreateAppSyncSdkHttpRequestBuilder() throws { | ||
var urlRequest = URLRequest(url: URL(string: "http://graphql.com")!) | ||
urlRequest.httpMethod = "post" | ||
let dataObject = Data() | ||
urlRequest.httpBody = dataObject | ||
guard let sdkRequestBuilder = try AWSCognitoAuthPlugin.createAppSyncSdkHttpRequestBuilder(urlRequest: urlRequest) else { | ||
XCTFail("Could not create SDK request") | ||
return | ||
} | ||
|
||
let request = sdkRequestBuilder.build() | ||
XCTAssertEqual(request.host, "graphql.com") | ||
XCTAssertEqual(request.path, "") | ||
XCTAssertEqual(request.queryItems, []) | ||
XCTAssertEqual(request.method, .post) | ||
XCTAssertEqual(request.endpoint.port, 443) | ||
XCTAssertEqual(request.endpoint.protocolType, .https) | ||
XCTAssertEqual(request.endpoint.headers?.headers, [.init(name: "host", value: "graphql.com")]) | ||
guard case let .data(data) = request.body else { | ||
XCTFail("Unexpected body") | ||
return | ||
} | ||
XCTAssertEqual(data, dataObject) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
36 changes: 36 additions & 0 deletions
36
...s/Auth/Tests/AuthHostApp/AuthIntegrationTests/AppSyncSignerTests/AppSyncSignerTests.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
// | ||
// Copyright Amazon.com Inc. or its affiliates. | ||
// All Rights Reserved. | ||
// | ||
// SPDX-License-Identifier: Apache-2.0 | ||
// | ||
|
||
import XCTest | ||
@testable import Amplify | ||
import AWSCognitoAuthPlugin | ||
|
||
class AppSyncSignerTests: AWSAuthBaseTest { | ||
|
||
/// Test signing an AppSync request with a live credentials provider | ||
/// | ||
/// - Given: Base test configures Amplify and adds AWSCognitoAuthPlugin | ||
/// - When: | ||
/// - I invoke AWSCognitoAuthPlugin's AppSync signer | ||
/// - Then: | ||
/// - I should get a signed request. | ||
/// | ||
func testSignAppSyncRequest() async throws { | ||
let request = URLRequest(url: URL(string: "http://graphql.com")!) | ||
let signer = AWSCognitoAuthPlugin.createAppSyncSigner(region: "us-east-1") | ||
let signedRequest = try await signer(request) | ||
guard let headers = signedRequest.allHTTPHeaderFields else { | ||
XCTFail("Missing headers") | ||
return | ||
} | ||
XCTAssertEqual(headers.count, 4) | ||
let containsExpectedHeaders = headers.keys.contains(where: { key in | ||
key == "Authorization" || key == "Host" || key == "X-Amz-Security-Token" || key == "X-Amz-Date" | ||
}) | ||
XCTAssertTrue(containsExpectedHeaders) | ||
} | ||
} |
46 changes: 46 additions & 0 deletions
46
AmplifyPlugins/Core/AWSPluginsCore/API/AWSAppSyncConfiguration.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
// | ||
// Copyright Amazon.com Inc. or its affiliates. | ||
// All Rights Reserved. | ||
// | ||
// SPDX-License-Identifier: Apache-2.0 | ||
// | ||
|
||
import Foundation | ||
@_spi(InternalAmplifyConfiguration) import Amplify | ||
|
||
|
||
/// Hold necessary AWS AppSync configuration values to interact with the AppSync API | ||
public struct AWSAppSyncConfiguration { | ||
|
||
/// The region of the AWS AppSync API | ||
public let region: String | ||
|
||
/// The endpoint of the AWS AppSync API | ||
public let endpoint: URL | ||
|
||
/// API key for API Key authentication. | ||
public let apiKey: String? | ||
|
||
|
||
/// Initializes an `AWSAppSyncConfiguration` instance using the provided AmplifyOutputs file. | ||
/// AmplifyOutputs support multiple ways to read the `amplify_outputs.json` configuration file | ||
/// | ||
/// For example, `try AWSAppSyncConfiguraton(with: .amplifyOutputs)` will read the | ||
/// `amplify_outputs.json` file from the main bundle. | ||
public init(with amplifyOutputs: AmplifyOutputs) throws { | ||
let resolvedConfiguration = try amplifyOutputs.resolveConfiguration() | ||
|
||
guard let dataCategory = resolvedConfiguration.data else { | ||
throw ConfigurationError.invalidAmplifyOutputsFile( | ||
"Missing data category", "", nil) | ||
} | ||
|
||
self.region = dataCategory.awsRegion | ||
guard let endpoint = URL(string: dataCategory.url) else { | ||
throw ConfigurationError.invalidAmplifyOutputsFile( | ||
"Missing region from data category", "", nil) | ||
} | ||
self.endpoint = endpoint | ||
self.apiKey = dataCategory.apiKey | ||
} | ||
} |
31 changes: 31 additions & 0 deletions
31
AmplifyPlugins/Core/AWSPluginsCoreTests/API/AWSAppSyncConfigurationTests.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
// | ||
// Copyright Amazon.com Inc. or its affiliates. | ||
// All Rights Reserved. | ||
// | ||
// SPDX-License-Identifier: Apache-2.0 | ||
// | ||
|
||
import XCTest | ||
import AWSPluginsCore | ||
@_spi(InternalAmplifyConfiguration) @testable import Amplify | ||
|
||
final class AWSAppSyncConfigurationTests: XCTestCase { | ||
|
||
func testSuccess() throws { | ||
let config = AmplifyOutputsData(data: .init( | ||
awsRegion: "us-east-1", | ||
url: "http://www.example.com", | ||
modelIntrospection: nil, | ||
apiKey: "apiKey123", | ||
defaultAuthorizationType: .amazonCognitoUserPools, | ||
authorizationTypes: [.apiKey, .awsIAM])) | ||
let encoder = JSONEncoder() | ||
let data = try! encoder.encode(config) | ||
|
||
let configuration = try AWSAppSyncConfiguration(with: .data(data)) | ||
|
||
XCTAssertEqual(configuration.region, "us-east-1") | ||
XCTAssertEqual(configuration.endpoint, URL(string: "http://www.example.com")!) | ||
XCTAssertEqual(configuration.apiKey, "apiKey123") | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.