Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add AppSync components #3825

Merged
merged 6 commits into from
Aug 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Amplify/Core/Configuration/AmplifyOutputsData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,8 @@ public struct AmplifyOutputsData: Codable {
public struct AmplifyOutputs {

/// A closure that resolves the `AmplifyOutputsData` configuration
let resolveConfiguration: () throws -> AmplifyOutputsData
@_spi(InternalAmplifyConfiguration)
public let resolveConfiguration: () throws -> AmplifyOutputsData

/// Resolves configuration with `amplify_outputs.json` in the main bundle.
public static let amplifyOutputs: AmplifyOutputs = {
Expand Down
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())
}

}
}
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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
objects = {

/* Begin PBXBuildFile section */
21CFD7C62C7524570071C70F /* AppSyncSignerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21CFD7C52C7524570071C70F /* AppSyncSignerTests.swift */; };
21F762A52BD6B1AA0048845A /* AuthSessionHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 485CB5B727B61F0F006CCEC7 /* AuthSessionHelper.swift */; };
21F762A62BD6B1AA0048845A /* AsyncTesting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 681DFEA828E747B80000C36A /* AsyncTesting.swift */; };
21F762A72BD6B1AA0048845A /* AuthSRPSignInTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 485CB5BE27B61F1D006CCEC7 /* AuthSRPSignInTests.swift */; };
Expand Down Expand Up @@ -169,6 +170,7 @@
/* End PBXContainerItemProxy section */

/* Begin PBXFileReference section */
21CFD7C52C7524570071C70F /* AppSyncSignerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSyncSignerTests.swift; sourceTree = "<group>"; };
21F762CB2BD6B1AA0048845A /* AuthGen2IntegrationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AuthGen2IntegrationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
21F762CC2BD6B1CD0048845A /* AuthGen2IntegrationTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = AuthGen2IntegrationTests.xctestplan; sourceTree = "<group>"; };
4821B2F1286B5F74000EC1D7 /* AuthDeleteUserTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthDeleteUserTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -268,6 +270,14 @@
/* End PBXFrameworksBuildPhase section */

/* Begin PBXGroup section */
21CFD7C42C75243B0071C70F /* AppSyncSignerTests */ = {
isa = PBXGroup;
children = (
21CFD7C52C7524570071C70F /* AppSyncSignerTests.swift */,
);
path = AppSyncSignerTests;
sourceTree = "<group>";
};
4821B2F0286B5F74000EC1D7 /* AuthDeleteUserTests */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -355,6 +365,7 @@
485CB5A027B61E04006CCEC7 /* AuthIntegrationTests */ = {
isa = PBXGroup;
children = (
21CFD7C42C75243B0071C70F /* AppSyncSignerTests */,
21F762CC2BD6B1CD0048845A /* AuthGen2IntegrationTests.xctestplan */,
48916F362A412AF800E3E1B1 /* MFATests */,
97B370C32878DA3500F1C088 /* DeviceTests */,
Expand Down Expand Up @@ -851,6 +862,7 @@
681DFEAC28E747B80000C36A /* AsyncExpectation.swift in Sources */,
48E3AB3128E52590004EE395 /* GetCurrentUserTests.swift in Sources */,
48916F3A2A412CEE00E3E1B1 /* TOTPHelper.swift in Sources */,
21CFD7C62C7524570071C70F /* AppSyncSignerTests.swift in Sources */,
485CB5B127B61EAC006CCEC7 /* AWSAuthBaseTest.swift in Sources */,
485CB5C027B61F1E006CCEC7 /* SignedOutAuthSessionTests.swift in Sources */,
485CB5BA27B61F10006CCEC7 /* AuthSignInHelper.swift in Sources */,
Expand Down
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)
}
}
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
}
}
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")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import AWSPluginsCore
import Foundation

public class AmplifyAWSCredentialsProvider: AWSClientRuntime.CredentialsProviding {

public func getCredentials() async throws -> AWSClientRuntime.AWSCredentials {
let authSession = try await Amplify.Auth.fetchAuthSession()
if let awsCredentialsProvider = authSession as? AuthAWSCredentialsProvider {
Expand Down
2 changes: 1 addition & 1 deletion api-dump/AWSDataStorePlugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -8205,7 +8205,7 @@
"-module",
"AWSDataStorePlugin",
"-o",
"\/var\/folders\/hw\/1f0gcr8d6kn9ms0_wn0_57qc0000gn\/T\/tmp.rjSPtedPzR\/AWSDataStorePlugin.json",
"\/var\/folders\/4d\/0gnh84wj53j7wyk695q0tc_80000gn\/T\/tmp.BZQxGLOyZz\/AWSDataStorePlugin.json",
"-I",
".build\/debug",
"-sdk-version",
Expand Down
Loading