Skip to content

Commit 673a075

Browse files
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]>
1 parent bdfa37a commit 673a075

File tree

12 files changed

+576
-6
lines changed

12 files changed

+576
-6
lines changed

Amplify/Core/Configuration/AmplifyOutputsData.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,8 @@ public struct AmplifyOutputsData: Codable {
251251
public struct AmplifyOutputs {
252252

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

256257
/// Resolves configuration with `amplify_outputs.json` in the main bundle.
257258
public static let amplifyOutputs: AmplifyOutputs = {
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
//
2+
// Copyright Amazon.com Inc. or its affiliates.
3+
// All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
import Foundation
9+
import Amplify // Amplify.Auth
10+
import AWSPluginsCore // AuthAWSCredentialsProvider
11+
import AWSClientRuntime // AWSClientRuntime.CredentialsProviding
12+
import ClientRuntime // SdkHttpRequestBuilder
13+
import AwsCommonRuntimeKit // CommonRuntimeKit.initialize()
14+
15+
extension AWSCognitoAuthPlugin {
16+
17+
18+
/// Creates a AWS IAM SigV4 signer capable of signing AWS AppSync requests.
19+
///
20+
/// **Note**. Although this method is static, **Amplify.Auth** is required to be configured with **AWSCognitoAuthPlugin** as
21+
/// it depends on the credentials provider from Cognito through `Amplify.Auth.fetchAuthSession()`. The static type allows
22+
/// developers to simplify their callsite without having to access the method on the plugin instance.
23+
///
24+
/// - Parameter region: The region of the AWS AppSync API
25+
/// - Returns: A closure that takes in a requestand returns a signed request.
26+
public static func createAppSyncSigner(region: String) -> ((URLRequest) async throws -> URLRequest) {
27+
return { request in
28+
try await signAppSyncRequest(request,
29+
region: region)
30+
}
31+
}
32+
33+
static func signAppSyncRequest(_ urlRequest: URLRequest,
34+
region: Swift.String,
35+
signingName: Swift.String = "appsync",
36+
date: ClientRuntime.Date = Date()) async throws -> URLRequest {
37+
CommonRuntimeKit.initialize()
38+
39+
// Convert URLRequest to SDK's HTTPRequest
40+
guard let requestBuilder = try createAppSyncSdkHttpRequestBuilder(
41+
urlRequest: urlRequest) else {
42+
return urlRequest
43+
}
44+
45+
// Retrieve the credentials from credentials provider
46+
let credentials: AWSClientRuntime.AWSCredentials
47+
let authSession = try await Amplify.Auth.fetchAuthSession()
48+
if let awsCredentialsProvider = authSession as? AuthAWSCredentialsProvider {
49+
let awsCredentials = try awsCredentialsProvider.getAWSCredentials().get()
50+
credentials = awsCredentials.toAWSSDKCredentials()
51+
} else {
52+
let error = AuthError.unknown("Auth session does not include AWS credentials information")
53+
throw error
54+
}
55+
56+
// Prepare signing
57+
let flags = SigningFlags(useDoubleURIEncode: true,
58+
shouldNormalizeURIPath: true,
59+
omitSessionToken: false)
60+
let signedBodyHeader: AWSSignedBodyHeader = .none
61+
let signedBodyValue: AWSSignedBodyValue = .empty
62+
let signingConfig = AWSSigningConfig(credentials: credentials,
63+
signedBodyHeader: signedBodyHeader,
64+
signedBodyValue: signedBodyValue,
65+
flags: flags,
66+
date: date,
67+
service: signingName,
68+
region: region,
69+
signatureType: .requestHeaders,
70+
signingAlgorithm: .sigv4)
71+
72+
// Sign request
73+
guard let httpRequest = await AWSSigV4Signer.sigV4SignedRequest(
74+
requestBuilder: requestBuilder,
75+
76+
signingConfig: signingConfig
77+
) else {
78+
return urlRequest
79+
}
80+
81+
// Update original request with new headers
82+
return setHeaders(from: httpRequest, to: urlRequest)
83+
}
84+
85+
static func setHeaders(from sdkRequest: SdkHttpRequest, to urlRequest: URLRequest) -> URLRequest {
86+
var urlRequest = urlRequest
87+
for header in sdkRequest.headers.headers {
88+
urlRequest.setValue(header.value.joined(separator: ","), forHTTPHeaderField: header.name)
89+
}
90+
return urlRequest
91+
}
92+
93+
static func createAppSyncSdkHttpRequestBuilder(urlRequest: URLRequest) throws -> SdkHttpRequestBuilder? {
94+
95+
guard let url = urlRequest.url,
96+
let host = url.host else {
97+
return nil
98+
}
99+
100+
var headers = urlRequest.allHTTPHeaderFields ?? [:]
101+
headers.updateValue(host, forKey: "host")
102+
103+
let httpMethod = (urlRequest.httpMethod?.uppercased())
104+
.flatMap(HttpMethodType.init(rawValue:)) ?? .get
105+
106+
let queryItems = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems?
107+
.map { ClientRuntime.SDKURLQueryItem(name: $0.name, value: $0.value)} ?? []
108+
109+
let requestBuilder = SdkHttpRequestBuilder()
110+
.withHost(host)
111+
.withPath(url.path)
112+
.withQueryItems(queryItems)
113+
.withMethod(httpMethod)
114+
.withPort(443)
115+
.withProtocol(.https)
116+
.withHeaders(.init(headers))
117+
.withBody(.data(urlRequest.httpBody))
118+
119+
return requestBuilder
120+
}
121+
}
122+
123+
extension AWSPluginsCore.AWSCredentials {
124+
125+
func toAWSSDKCredentials() -> AWSClientRuntime.AWSCredentials {
126+
if let tempCredentials = self as? AWSTemporaryCredentials {
127+
return AWSClientRuntime.AWSCredentials(
128+
accessKey: tempCredentials.accessKeyId,
129+
secret: tempCredentials.secretAccessKey,
130+
expirationTimeout: tempCredentials.expiration,
131+
sessionToken: tempCredentials.sessionToken)
132+
} else {
133+
return AWSClientRuntime.AWSCredentials(
134+
accessKey: accessKeyId,
135+
secret: secretAccessKey,
136+
expirationTimeout: Date())
137+
}
138+
139+
}
140+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
//
2+
// Copyright Amazon.com Inc. or its affiliates.
3+
// All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
import XCTest
9+
@testable import Amplify
10+
@testable import AWSCognitoAuthPlugin
11+
12+
class AWSCognitoAuthPluginAppSyncSignerTests: XCTestCase {
13+
14+
/// Tests translating the URLRequest to the SDKRequest
15+
/// The translation should account for expected fields, as asserted in the test.
16+
func testCreateAppSyncSdkHttpRequestBuilder() throws {
17+
var urlRequest = URLRequest(url: URL(string: "http://graphql.com")!)
18+
urlRequest.httpMethod = "post"
19+
let dataObject = Data()
20+
urlRequest.httpBody = dataObject
21+
guard let sdkRequestBuilder = try AWSCognitoAuthPlugin.createAppSyncSdkHttpRequestBuilder(urlRequest: urlRequest) else {
22+
XCTFail("Could not create SDK request")
23+
return
24+
}
25+
26+
let request = sdkRequestBuilder.build()
27+
XCTAssertEqual(request.host, "graphql.com")
28+
XCTAssertEqual(request.path, "")
29+
XCTAssertEqual(request.queryItems, [])
30+
XCTAssertEqual(request.method, .post)
31+
XCTAssertEqual(request.endpoint.port, 443)
32+
XCTAssertEqual(request.endpoint.protocolType, .https)
33+
XCTAssertEqual(request.endpoint.headers?.headers, [.init(name: "host", value: "graphql.com")])
34+
guard case let .data(data) = request.body else {
35+
XCTFail("Unexpected body")
36+
return
37+
}
38+
XCTAssertEqual(data, dataObject)
39+
}
40+
}

AmplifyPlugins/Auth/Tests/AuthHostApp/AuthHostApp.xcodeproj/project.pbxproj

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
objects = {
88

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

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

270272
/* Begin PBXGroup section */
273+
21CFD7C42C75243B0071C70F /* AppSyncSignerTests */ = {
274+
isa = PBXGroup;
275+
children = (
276+
21CFD7C52C7524570071C70F /* AppSyncSignerTests.swift */,
277+
);
278+
path = AppSyncSignerTests;
279+
sourceTree = "<group>";
280+
};
271281
4821B2F0286B5F74000EC1D7 /* AuthDeleteUserTests */ = {
272282
isa = PBXGroup;
273283
children = (
@@ -355,6 +365,7 @@
355365
485CB5A027B61E04006CCEC7 /* AuthIntegrationTests */ = {
356366
isa = PBXGroup;
357367
children = (
368+
21CFD7C42C75243B0071C70F /* AppSyncSignerTests */,
358369
21F762CC2BD6B1CD0048845A /* AuthGen2IntegrationTests.xctestplan */,
359370
48916F362A412AF800E3E1B1 /* MFATests */,
360371
97B370C32878DA3500F1C088 /* DeviceTests */,
@@ -851,6 +862,7 @@
851862
681DFEAC28E747B80000C36A /* AsyncExpectation.swift in Sources */,
852863
48E3AB3128E52590004EE395 /* GetCurrentUserTests.swift in Sources */,
853864
48916F3A2A412CEE00E3E1B1 /* TOTPHelper.swift in Sources */,
865+
21CFD7C62C7524570071C70F /* AppSyncSignerTests.swift in Sources */,
854866
485CB5B127B61EAC006CCEC7 /* AWSAuthBaseTest.swift in Sources */,
855867
485CB5C027B61F1E006CCEC7 /* SignedOutAuthSessionTests.swift in Sources */,
856868
485CB5BA27B61F10006CCEC7 /* AuthSignInHelper.swift in Sources */,
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
//
2+
// Copyright Amazon.com Inc. or its affiliates.
3+
// All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
import XCTest
9+
@testable import Amplify
10+
import AWSCognitoAuthPlugin
11+
12+
class AppSyncSignerTests: AWSAuthBaseTest {
13+
14+
/// Test signing an AppSync request with a live credentials provider
15+
///
16+
/// - Given: Base test configures Amplify and adds AWSCognitoAuthPlugin
17+
/// - When:
18+
/// - I invoke AWSCognitoAuthPlugin's AppSync signer
19+
/// - Then:
20+
/// - I should get a signed request.
21+
///
22+
func testSignAppSyncRequest() async throws {
23+
let request = URLRequest(url: URL(string: "http://graphql.com")!)
24+
let signer = AWSCognitoAuthPlugin.createAppSyncSigner(region: "us-east-1")
25+
let signedRequest = try await signer(request)
26+
guard let headers = signedRequest.allHTTPHeaderFields else {
27+
XCTFail("Missing headers")
28+
return
29+
}
30+
XCTAssertEqual(headers.count, 4)
31+
let containsExpectedHeaders = headers.keys.contains(where: { key in
32+
key == "Authorization" || key == "Host" || key == "X-Amz-Security-Token" || key == "X-Amz-Date"
33+
})
34+
XCTAssertTrue(containsExpectedHeaders)
35+
}
36+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
//
2+
// Copyright Amazon.com Inc. or its affiliates.
3+
// All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
import Foundation
9+
@_spi(InternalAmplifyConfiguration) import Amplify
10+
11+
12+
/// Hold necessary AWS AppSync configuration values to interact with the AppSync API
13+
public struct AWSAppSyncConfiguration {
14+
15+
/// The region of the AWS AppSync API
16+
public let region: String
17+
18+
/// The endpoint of the AWS AppSync API
19+
public let endpoint: URL
20+
21+
/// API key for API Key authentication.
22+
public let apiKey: String?
23+
24+
25+
/// Initializes an `AWSAppSyncConfiguration` instance using the provided AmplifyOutputs file.
26+
/// AmplifyOutputs support multiple ways to read the `amplify_outputs.json` configuration file
27+
///
28+
/// For example, `try AWSAppSyncConfiguraton(with: .amplifyOutputs)` will read the
29+
/// `amplify_outputs.json` file from the main bundle.
30+
public init(with amplifyOutputs: AmplifyOutputs) throws {
31+
let resolvedConfiguration = try amplifyOutputs.resolveConfiguration()
32+
33+
guard let dataCategory = resolvedConfiguration.data else {
34+
throw ConfigurationError.invalidAmplifyOutputsFile(
35+
"Missing data category", "", nil)
36+
}
37+
38+
self.region = dataCategory.awsRegion
39+
guard let endpoint = URL(string: dataCategory.url) else {
40+
throw ConfigurationError.invalidAmplifyOutputsFile(
41+
"Missing region from data category", "", nil)
42+
}
43+
self.endpoint = endpoint
44+
self.apiKey = dataCategory.apiKey
45+
}
46+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
//
2+
// Copyright Amazon.com Inc. or its affiliates.
3+
// All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
import XCTest
9+
import AWSPluginsCore
10+
@_spi(InternalAmplifyConfiguration) @testable import Amplify
11+
12+
final class AWSAppSyncConfigurationTests: XCTestCase {
13+
14+
func testSuccess() throws {
15+
let config = AmplifyOutputsData(data: .init(
16+
awsRegion: "us-east-1",
17+
url: "http://www.example.com",
18+
modelIntrospection: nil,
19+
apiKey: "apiKey123",
20+
defaultAuthorizationType: .amazonCognitoUserPools,
21+
authorizationTypes: [.apiKey, .awsIAM]))
22+
let encoder = JSONEncoder()
23+
let data = try! encoder.encode(config)
24+
25+
let configuration = try AWSAppSyncConfiguration(with: .data(data))
26+
27+
XCTAssertEqual(configuration.region, "us-east-1")
28+
XCTAssertEqual(configuration.endpoint, URL(string: "http://www.example.com")!)
29+
XCTAssertEqual(configuration.apiKey, "apiKey123")
30+
}
31+
}

AmplifyPlugins/Core/AmplifyCredentials/AmplifyAWSCredentialsProvider.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import AWSPluginsCore
1212
import Foundation
1313

1414
public class AmplifyAWSCredentialsProvider: AWSClientRuntime.CredentialsProviding {
15-
15+
1616
public func getCredentials() async throws -> AWSClientRuntime.AWSCredentials {
1717
let authSession = try await Amplify.Auth.fetchAuthSession()
1818
if let awsCredentialsProvider = authSession as? AuthAWSCredentialsProvider {

api-dump/AWSDataStorePlugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8205,7 +8205,7 @@
82058205
"-module",
82068206
"AWSDataStorePlugin",
82078207
"-o",
8208-
"\/var\/folders\/hw\/1f0gcr8d6kn9ms0_wn0_57qc0000gn\/T\/tmp.rjSPtedPzR\/AWSDataStorePlugin.json",
8208+
"\/var\/folders\/4d\/0gnh84wj53j7wyk695q0tc_80000gn\/T\/tmp.BZQxGLOyZz\/AWSDataStorePlugin.json",
82098209
"-I",
82108210
".build\/debug",
82118211
"-sdk-version",

0 commit comments

Comments
 (0)