Skip to content

Commit

Permalink
feat(api): add authorizationMode to GraphQLRequest (#3630)
Browse files Browse the repository at this point in the history
* feat(api): add authorizationMode to GraphQLRequest

* add unit tests

* finalize API

* remove appSync api

* Delete AmplifyPlugins/Storage/Tests/StorageHostApp/amplify_outputs.json

* Delete AmplifyPlugins/API/Tests/APIHostApp/APIHostApp.xcodeproj/xcshareddata/xcschemes/AWSAPIPluginGen2LazyLoadTests.xcscheme
  • Loading branch information
lawmicha authored Apr 26, 2024
1 parent a86dc2e commit 3ffde2b
Show file tree
Hide file tree
Showing 11 changed files with 219 additions and 58 deletions.
5 changes: 5 additions & 0 deletions Amplify/Categories/API/Request/GraphQLOperationRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ public struct GraphQLOperationRequest<R: Decodable>: AmplifyOperationRequest {
/// The path to traverse before decoding to `responseType`.
public let decodePath: String?

/// The authorization mode
public let authMode: AuthorizationMode?

/// Options to adjust the behavior of this request, including plugin-options
public let options: Options

Expand All @@ -35,13 +38,15 @@ public struct GraphQLOperationRequest<R: Decodable>: AmplifyOperationRequest {
variables: [String: Any]? = nil,
responseType: R.Type,
decodePath: String? = nil,
authMode: AuthorizationMode? = nil,
options: Options) {
self.apiName = apiName
self.operationType = operationType
self.document = document
self.variables = variables
self.responseType = responseType
self.decodePath = decodePath
self.authMode = authMode
self.options = options
}
}
Expand Down
8 changes: 8 additions & 0 deletions Amplify/Categories/API/Request/GraphQLRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
// SPDX-License-Identifier: Apache-2.0
//

/// Empty protocol for plugins to define specific `AuthorizationMode` types for the request.
public protocol AuthorizationMode { }

/// GraphQL Request
public struct GraphQLRequest<R: Decodable> {

Expand All @@ -21,6 +24,9 @@ public struct GraphQLRequest<R: Decodable> {
/// Type to decode the graphql response data object to
public let responseType: R.Type

/// The authorization mode
public let authMode: AuthorizationMode?

/// The path to decode to the graphQL response data to `responseType`. Delimited by `.` The decode path
/// "listTodos.items" will traverse to the object at `listTodos`, and decode the object at `items` to `responseType`
/// The data at that decode path is a list of Todo objects so `responseType` should be `[Todo].self`
Expand All @@ -34,11 +40,13 @@ public struct GraphQLRequest<R: Decodable> {
variables: [String: Any]? = nil,
responseType: R.Type,
decodePath: String? = nil,
authMode: AuthorizationMode? = nil,
options: GraphQLRequest<R>.Options? = nil) {
self.apiName = apiName
self.document = document
self.variables = variables
self.responseType = responseType
self.authMode = authMode
self.decodePath = decodePath
self.options = options
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ final public class AWSGraphQLOperation<R: Decodable>: GraphQLOperation<R> {
}

let urlRequest = validateRequest(request).flatMap(buildURLRequest(from:))
let finalRequest = await getEndpointInterceptors(from: request).flatMapAsync { requestInterceptors in
let finalRequest = await getEndpointInterceptors().flatMapAsync { requestInterceptors in
let preludeInterceptors = requestInterceptors?.preludeInterceptors ?? []
let customerInterceptors = requestInterceptors?.interceptors ?? []
let postludeInterceptors = requestInterceptors?.postludeInterceptors ?? []
Expand Down Expand Up @@ -150,7 +150,7 @@ final public class AWSGraphQLOperation<R: Decodable>: GraphQLOperation<R> {
}
}

private func getEndpointInterceptors(from request: GraphQLOperationRequest<R>) -> Result<AWSAPIEndpointInterceptors?, APIError> {
func getEndpointInterceptors() -> Result<AWSAPIEndpointInterceptors?, APIError> {
getEndpointConfig(from: request).flatMap { endpointConfig in
do {
if let pluginOptions = request.options.pluginOptions as? AWSAPIPluginDataStoreOptions,
Expand All @@ -159,6 +159,11 @@ final public class AWSGraphQLOperation<R: Decodable>: GraphQLOperation<R> {
withConfig: endpointConfig,
authType: authType
))
} else if let authType = request.authMode as? AWSAuthorizationType {
return .success(try pluginConfig.interceptorsForEndpoint(
withConfig: endpointConfig,
authType: authType
))
} else {
return .success(pluginConfig.interceptorsForEndpoint(withConfig: endpointConfig))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,14 +91,21 @@ public class AWSGraphQLSubscriptionTaskRunner<R: Decodable>: InternalTaskRunner,
return
}

let pluginOptions = request.options.pluginOptions as? AWSAPIPluginDataStoreOptions
let authType: AWSAuthorizationType?
if let pluginOptions = request.options.pluginOptions as? AWSAPIPluginDataStoreOptions {
authType = pluginOptions.authType
} else if let authorizationMode = request.authMode as? AWSAuthorizationType {
authType = authorizationMode
} else {
authType = nil
}
// Retrieve the subscription connection
do {
self.appSyncClient = try await appSyncClientFactory.getAppSyncRealTimeClient(
for: endpointConfig,
endpoint: endpointConfig.baseURL,
authService: authService,
authType: pluginOptions?.authType,
authType: authType,
apiAuthProviderFactory: apiAuthProviderFactory
)

Expand Down Expand Up @@ -262,14 +269,21 @@ final public class AWSGraphQLSubscriptionOperation<R: Decodable>: GraphQLSubscri
return
}

let pluginOptions = request.options.pluginOptions as? AWSAPIPluginDataStoreOptions
let authType: AWSAuthorizationType?
if let pluginOptions = request.options.pluginOptions as? AWSAPIPluginDataStoreOptions {
authType = pluginOptions.authType
} else if let authorizationMode = request.authMode as? AWSAuthorizationType {
authType = authorizationMode
} else {
authType = nil
}
Task {
do {
appSyncRealTimeClient = try await appSyncRealTimeClientFactory.getAppSyncRealTimeClient(
for: endpointConfig,
endpoint: endpointConfig.baseURL,
authService: authService,
authType: pluginOptions?.authType,
authType: authType,
apiAuthProviderFactory: apiAuthProviderFactory
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ extension GraphQLRequest {
variables: variables,
responseType: responseType,
decodePath: decodePath,
authMode: authMode,
options: requestOptions)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,19 @@
import XCTest
import Amplify
@testable import AWSAPIPlugin
import AWSPluginsCore

class AWSAPICategoryPluginGraphQLBehaviorTests: AWSAPICategoryPluginTestBase {

// MARK: Query API Tests

func testQuery() {
let operationFinished = expectation(description: "Operation should finish")
let request = GraphQLRequest(apiName: apiName,
document: testDocument,
variables: nil,
responseType: JSONValue.self)
let request = GraphQLRequest<JSONValue>(apiName: apiName,
document: testDocument,
variables: nil,
responseType: JSONValue.self,
authMode: AWSAuthorizationType.apiKey)
let operation = apiPlugin.query(request: request) { _ in
operationFinished.fulfill()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import XCTest
@testable import Amplify
@testable import AmplifyTestCommon
@testable import AWSAPIPlugin
@testable import AWSPluginsTestCommon
import AWSPluginsCore

class AWSGraphQLOperationTests: AWSAPICategoryPluginTestBase {

Expand Down Expand Up @@ -37,4 +39,44 @@ class AWSGraphQLOperationTests: AWSAPICategoryPluginTestBase {
XCTAssertNil(task)
}


/// Request for `.amazonCognitoUserPool` at runtime with `request` while passing in what
/// is configured as `.apiKey`. Expect that the interceptor is the token interceptor
func testGetEndpointInterceptors() throws {
let request = GraphQLRequest<JSONValue>(apiName: apiName,
document: testDocument,
variables: nil,
responseType: JSONValue.self,
authMode: AWSAuthorizationType.amazonCognitoUserPools)
let task = try OperationTestBase.makeSingleValueErrorMockTask()
let mockSession = MockURLSession(onTaskForRequest: { _ in task })
let pluginConfig = AWSAPICategoryPluginConfiguration(
endpoints: [
apiName: try .init(
name: apiName,
baseURL: URL(string: "url")!,
region: "us-test-1",
authorizationType: .apiKey,
endpointType: .graphQL,
apiKey: "apiKey",
apiAuthProviderFactory: .init())],
apiAuthProviderFactory: .init(),
authService: MockAWSAuthService())
let operation = AWSGraphQLOperation(request: request.toOperationRequest(operationType: .query),
session: mockSession,
mapper: OperationTaskMapper(),
pluginConfig: pluginConfig,
resultListener: { _ in })

// Act
let results = operation.getEndpointInterceptors()

// Assert
guard case let .success(interceptors) = results,
let interceptor = interceptors?.preludeInterceptors.first,
(interceptor as? AuthTokenURLRequestInterceptor) != nil else {
XCTFail("Should be token interceptor for Cognito User Pool")
return
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ class OperationTestBase: XCTestCase {
}

func setUpPluginForSingleError(for endpointType: AWSAPICategoryPluginEndpointType) throws {
let task = try makeSingleValueErrorMockTask()
let task = try Self.makeSingleValueErrorMockTask()
let mockSession = MockURLSession(onTaskForRequest: { _ in task })
let sessionFactory = MockSessionFactory(returning: mockSession)
try setUpPlugin(sessionFactory: sessionFactory, endpointType: endpointType)
Expand Down Expand Up @@ -102,7 +102,7 @@ class OperationTestBase: XCTestCase {
return task
}

func makeSingleValueErrorMockTask() throws -> MockURLSessionTask {
static func makeSingleValueErrorMockTask() throws -> MockURLSessionTask {
var mockTask: MockURLSessionTask!
mockTask = MockURLSessionTask(onResume: {
guard let mockSession = mockTask.mockSession,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@
//

import Foundation
import Amplify

// swiftlint:disable line_length

/// The types of authorization one can use while talking to an Amazon AppSync
/// GraphQL backend, or an Amazon API Gateway endpoint.
///
/// - SeeAlso: [https://docs.aws.amazon.com/appsync/latest/devguide/security.html](AppSync Security)
public enum AWSAuthorizationType: String {
public enum AWSAuthorizationType: String, AuthorizationMode {

/// For public APIs
case none = "NONE"
Expand Down
Loading

0 comments on commit 3ffde2b

Please sign in to comment.