diff --git a/AWSAppSyncClient.xcodeproj/project.pbxproj b/AWSAppSyncClient.xcodeproj/project.pbxproj index 54655cb9..cba6da17 100644 --- a/AWSAppSyncClient.xcodeproj/project.pbxproj +++ b/AWSAppSyncClient.xcodeproj/project.pbxproj @@ -121,6 +121,10 @@ 21D5286A2416A2B3005186BA /* IAMAuthInterceptorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21D528692416A2B3005186BA /* IAMAuthInterceptorTests.swift */; }; 3D9BF115227836800079F52F /* NetworkReachability.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D9BF114227836800079F52F /* NetworkReachability.swift */; }; 70C68E4D132FE62623DB8C07 /* Pods_AWSAppSyncTestHostApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8C707001F57B091A8A001CAB /* Pods_AWSAppSyncTestHostApp.framework */; }; + 7638897026A9E4D70061AF0B /* LambdaBasedConnectionPool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7638896F26A9E4D70061AF0B /* LambdaBasedConnectionPool.swift */; }; + 763C857726B08D74005164B2 /* AWSAppSyncLambdaAuthTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 763C857626B08D74005164B2 /* AWSAppSyncLambdaAuthTests.swift */; }; + 763C857926B1C262005164B2 /* LambdaAuthInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 763C857826B1C262005164B2 /* LambdaAuthInterceptor.swift */; }; + 763C857B26B1CB18005164B2 /* LambdaBasedConnectionPoolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 763C857A26B1CB18005164B2 /* LambdaBasedConnectionPoolTests.swift */; }; 8032C5415EF414C038394D69 /* Pods_AWSAppSyncTestCommon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 74071C397A83DEA980BB2F4C /* Pods_AWSAppSyncTestCommon.framework */; }; 90DE0C49240A304D000E875B /* AWSAppSyncAuthTypeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90DE0C48240A304D000E875B /* AWSAppSyncAuthTypeTests.swift */; }; A70604C0C722923A70C937A1 /* Pods_AWSAppSyncTestApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 57F5A94352E1ABE35159489D /* Pods_AWSAppSyncTestApp.framework */; }; @@ -510,6 +514,10 @@ 6878E4A0F7372438C7043A88 /* Pods_AWSAppSync_AWSAppSyncTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_AWSAppSync_AWSAppSyncTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 74071C397A83DEA980BB2F4C /* Pods_AWSAppSyncTestCommon.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_AWSAppSyncTestCommon.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 741880AF213878B400523CA8 /* AuthProviderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthProviderTests.swift; sourceTree = ""; }; + 7638896F26A9E4D70061AF0B /* LambdaBasedConnectionPool.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LambdaBasedConnectionPool.swift; sourceTree = ""; }; + 763C857626B08D74005164B2 /* AWSAppSyncLambdaAuthTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AWSAppSyncLambdaAuthTests.swift; sourceTree = ""; }; + 763C857826B1C262005164B2 /* LambdaAuthInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LambdaAuthInterceptor.swift; sourceTree = ""; }; + 763C857A26B1CB18005164B2 /* LambdaBasedConnectionPoolTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LambdaBasedConnectionPoolTests.swift; sourceTree = ""; }; 82E714A3E9BFB80BD9E5EF90 /* Pods-ApolloTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ApolloTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-ApolloTests/Pods-ApolloTests.debug.xcconfig"; sourceTree = ""; }; 8623D545D4837963CF2FFF02 /* Pods-AWSAppSyncUnitTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AWSAppSyncUnitTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-AWSAppSyncUnitTests/Pods-AWSAppSyncUnitTests.debug.xcconfig"; sourceTree = ""; }; 8C707001F57B091A8A001CAB /* Pods_AWSAppSyncTestHostApp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_AWSAppSyncTestHostApp.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -958,6 +966,7 @@ 2171807B23FDB22B00E520C9 /* SubscriptionConnectionFactory.swift */, 2171808123FDB22B00E520C9 /* UserPoolsBasedConnectionPool.swift */, 21D38B91240C099900EC2A8D /* AppSyncRealTimeClientOIDCAuthProvider.swift */, + 7638896F26A9E4D70061AF0B /* LambdaBasedConnectionPool.swift */, ); path = SubscriptionFactory; sourceTree = ""; @@ -970,6 +979,7 @@ 2171808C23FDB28100E520C9 /* UserPoolsBasedConnectionPoolTests.swift */, 2171808D23FDB28100E520C9 /* OIDCBasedConnectionPoolTests.swift */, 2171808E23FDB28100E520C9 /* IAMBasedConnectionPoolTests.swift */, + 763C857A26B1CB18005164B2 /* LambdaBasedConnectionPoolTests.swift */, ); path = ConnectionPool; sourceTree = ""; @@ -978,6 +988,7 @@ isa = PBXGroup; children = ( 21D5286224169CEE005186BA /* IAMAuthInterceptor.swift */, + 763C857826B1C262005164B2 /* LambdaAuthInterceptor.swift */, ); path = AuthInterceptor; sourceTree = ""; @@ -1106,6 +1117,7 @@ FAFD409121D702EA0063D894 /* Helpers */, FADC6B8822679B00008588FC /* Resources */, 741880AF213878B400523CA8 /* AuthProviderTests.swift */, + 763C857626B08D74005164B2 /* AWSAppSyncLambdaAuthTests.swift */, 17664128214F6732003AE269 /* AWSAppSyncAPIKeyAuthTests.swift */, 174F80AE2109229C00775D0D /* AWSAppSyncCognitoAuthTests.swift */, E48168AD226E8325005A1A41 /* AWSAppSyncMultiAuthClientsTests.swift */, @@ -1967,6 +1979,7 @@ 17E009BB1FCAB234005031DB /* GraphQLDependencyTracker.swift in Sources */, 17E009C71FCAB234005031DB /* JSONSerializationFormat.swift in Sources */, 17E009CD1FCAB234005031DB /* Collections.swift in Sources */, + 763C857926B1C262005164B2 /* LambdaAuthInterceptor.swift in Sources */, 17E009C51FCAB234005031DB /* GraphQLResultAccumulator.swift in Sources */, 178B31071FCDB34100EA4619 /* AWSAppSyncClientS3ObjectsExtensions.swift in Sources */, 17E009CF1FCAB234005031DB /* ResultOrPromise.swift in Sources */, @@ -2003,6 +2016,7 @@ CCEF79DD21DE7EED004AD64D /* AWSAppSyncClientError.swift in Sources */, FA0C12BB21CD308A00B438CB /* AWSAppSyncClientConfiguration.swift in Sources */, 178B31081FCDB34100EA4619 /* AWSAppSyncClientConflictResolutionExtensions.swift in Sources */, + 7638897026A9E4D70061AF0B /* LambdaBasedConnectionPool.swift in Sources */, 1729A0D01FA1365900F88594 /* AWSAppSyncSubscriptionWatcher.swift in Sources */, FA0D82582230D0AF00E0EA82 /* AWSAppSyncSubscriptionError.swift in Sources */, FAAACC2421DD7AC600D24B37 /* InternalS3ObjectDetails.swift in Sources */, @@ -2064,6 +2078,7 @@ 21933B3E24DA629B00F4D741 /* JSONValueSerializationTests.swift in Sources */, E47789592284B3DC008E7D6E /* MockAWSAppSyncServiceConfig.swift in Sources */, FA4F0D9521D6ED9E0099D165 /* AppSyncClientComplexObjectMutationUnitTests.swift in Sources */, + 763C857B26B1CB18005164B2 /* LambdaBasedConnectionPoolTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2073,6 +2088,7 @@ files = ( FADC6B8922679B38008588FC /* MutationQueuePerformanceTests.swift in Sources */, FA902D0E21D97E9600C4052F /* AuthProviderTests.swift in Sources */, + 763C857726B08D74005164B2 /* AWSAppSyncLambdaAuthTests.swift in Sources */, FA902D1021D97EB100C4052F /* AWSAppSyncCognitoAuthTests.swift in Sources */, E414DDB52289BD8A004C37CE /* AWSAppSyncMultiAuthClientsTests.swift in Sources */, FA902D1321D97EC500C4052F /* SubscriptionStressTestHelper.swift in Sources */, diff --git a/AWSAppSyncClient/AWSAppSyncAuthProvider.swift b/AWSAppSyncClient/AWSAppSyncAuthProvider.swift index d7d986e6..d387ac95 100644 --- a/AWSAppSyncClient/AWSAppSyncAuthProvider.swift +++ b/AWSAppSyncClient/AWSAppSyncAuthProvider.swift @@ -3,6 +3,7 @@ // AWSAppSync // +// MARK: AWSOIDCAuthProvider // For using OIDC based authorization, this protocol needs to be implemented and passed to configuration object. // Use this for cases where the OIDC token needs to be fetched asynchronously and requires a callback public protocol AWSOIDCAuthProviderAsync: AWSOIDCAuthProvider { @@ -14,6 +15,13 @@ extension AWSOIDCAuthProviderAsync { public func getLatestAuthToken() -> String { fatalError("Callback method required") } } +// For using OIDC based authorization, this protocol needs to be implemented and passed to configuration object. +public protocol AWSOIDCAuthProvider { + /// The method should fetch the token and return it to the client for using in header request. + func getLatestAuthToken() -> String +} + +// MARK: - AWSCognitoUserPoolsProvider // For using User Pool based authorization, this protocol needs to be implemented and passed to configuration object. // Use this for cases where the UserPool auth token needs to be fetched asynchronously and requires a callback public protocol AWSCognitoUserPoolsAuthProviderAsync: AWSCognitoUserPoolsAuthProvider { @@ -25,17 +33,30 @@ extension AWSCognitoUserPoolsAuthProviderAsync { public func getLatestAuthToken() -> String { fatalError("Callback method required") } } -// For using OIDC based authorization, this protocol needs to be implemented and passed to configuration object. -public protocol AWSOIDCAuthProvider { - /// The method should fetch the token and return it to the client for using in header request. - func getLatestAuthToken() -> String -} - // For using Cognito User Pools based authorization, this protocol needs to be implemented and passed to configuration object. public protocol AWSCognitoUserPoolsAuthProvider: AWSOIDCAuthProvider { } +// MARK: - AWSLambdaAuthProvider +// For using Lambda based authorization, this protocol needs to be implemented and passed to configuration object. +// Use this for cases where the authorization token needs to be fetched asynchronously and requires a callback +public protocol AWSLambdaAuthProviderAsync: AWSLambdaAuthProvider { + func getLatestAuthToken(_ callback: @escaping (String?, Error?) -> Void) +} + +// For AWSLambdaAuthProvider that use a callback, the getLatestAuthToken is defaulted to return an empty string +extension AWSLambdaAuthProviderAsync { + public func getLatestAuthToken() -> String { fatalError("Callback method required") } +} + +// For using AWS Lambda based authorization, this protocol needs to be implemented and passed to configuration object. +public protocol AWSLambdaAuthProvider { + /// The method should fetch the token and return it to the client for using in header request. + func getLatestAuthToken() -> String +} + +// MARK: - AWSAPIKeyAuthProvider // For using API Key based authorization, this protocol needs to be implemented and passed to configuration object. public protocol AWSAPIKeyAuthProvider { func getAPIKey() -> String diff --git a/AWSAppSyncClient/AWSAppSyncAuthType.swift b/AWSAppSyncClient/AWSAppSyncAuthType.swift index a75174d2..5e9c19ec 100644 --- a/AWSAppSyncClient/AWSAppSyncAuthType.swift +++ b/AWSAppSyncClient/AWSAppSyncAuthType.swift @@ -17,6 +17,8 @@ public enum AWSAppSyncAuthType: String, Codable, Hashable { /// User directory based authentication case amazonCognitoUserPools = "AMAZON_COGNITO_USER_POOLS" + + case awsLambda = "AWS_LAMBDA" /// Convenience method to use instead of `AuthType(rawValue:)` public static func getAuthType(rawValue: String) throws -> AWSAppSyncAuthType { diff --git a/AWSAppSyncClient/AWSAppSyncClientConfiguration.swift b/AWSAppSyncClient/AWSAppSyncClientConfiguration.swift index b8e2d8ee..e476fad3 100644 --- a/AWSAppSyncClient/AWSAppSyncClientConfiguration.swift +++ b/AWSAppSyncClient/AWSAppSyncClientConfiguration.swift @@ -109,6 +109,7 @@ public class AWSAppSyncClientConfiguration { /// `credentialsProvider`. /// - `oidcAuthProvider` is specified, and `appSyncClientInfo.authType` is "OPENID_CONNECT" /// - `userPoolsAuthProvider` is specified, and `appSyncClientInfo.authType` is "AMAZON_COGNITO_USER_POOLS" + /// - `awsLambdaAuthProvider` is specified, and `appSyncClientInfo.authType` is "AWS_LAMBDA" /// /// If none of those conditions are met, or if more than provider is specified, the initializer will throw an error. /// @@ -117,6 +118,7 @@ public class AWSAppSyncClientConfiguration { /// - apiKeyAuthProvider: A `AWSAPIKeyAuthProvider` protocol object for API Key based authorization. /// - credentialsProvider: A `AWSCredentialsProvider` object for AWS_IAM based authorization. /// - userPoolsAuthProvider: A `AWSCognitoUserPoolsAuthProvider` protocol object for User Pool based authorization. + /// - awsLambdaAuthProvider: A `AWSLambdaAuthProvider` protocol object for AWS Lambda based authorization. /// - oidcAuthProvider: A `AWSOIDCAuthProvider` protocol object for OIDC based authorization. /// - urlSessionConfiguration: A `URLSessionConfiguration` configuration object for custom HTTP configuration. /// - cacheConfiguration: Configuration for local queries, mutations, and subscriptions caches. @@ -131,6 +133,7 @@ public class AWSAppSyncClientConfiguration { credentialsProvider: AWSCredentialsProvider? = nil, oidcAuthProvider: AWSOIDCAuthProvider? = nil, userPoolsAuthProvider: AWSCognitoUserPoolsAuthProvider? = nil, + awsLambdaAuthProvider: AWSLambdaAuthProvider? = nil, urlSessionConfiguration: URLSessionConfiguration = URLSessionConfiguration.default, cacheConfiguration: AWSAppSyncCacheConfiguration? = nil, connectionStateChangeHandler: ConnectionStateChangeHandler? = nil, @@ -150,6 +153,7 @@ public class AWSAppSyncClientConfiguration { apiKeyAuthProvider: apiKeyAuthProvider, credentialsProvider: credentialsProvider, userPoolsAuthProvider: userPoolsAuthProvider, + awsLambdaAuthProvider: awsLambdaAuthProvider, oidcAuthProvider: oidcAuthProvider, urlSessionConfiguration: urlSessionConfiguration, cacheConfiguration: cacheConfiguration, @@ -177,6 +181,7 @@ public class AWSAppSyncClientConfiguration { /// - credentialsProvider: A `AWSCredentialsProvider` object for AWS_IAM based authorization /// - oidcAuthProvider: A `AWSOIDCAuthProvider` protocol object for OIDC based authorization /// - userPoolsAuthProvider: A `AWSCognitoUserPoolsAuthProvider` protocol object for User Pool based authorization + /// - awsLambdaAuthProvider: A `AWSLambdaAuthProvider` protocol object for AWS Lambda based authorization. /// - urlSessionConfiguration: A `URLSessionConfiguration` configuration object for custom HTTP configuration /// - cacheConfiguration: Configuration for local queries, mutations, and subscriptions caches. /// - connectionStateChangeHandler: The delegate object to be notified when client network state changes @@ -191,6 +196,7 @@ public class AWSAppSyncClientConfiguration { credentialsProvider: AWSCredentialsProvider? = nil, oidcAuthProvider: AWSOIDCAuthProvider? = nil, userPoolsAuthProvider: AWSCognitoUserPoolsAuthProvider? = nil, + awsLambdaAuthProvider: AWSLambdaAuthProvider? = nil, urlSessionConfiguration: URLSessionConfiguration = URLSessionConfiguration.default, cacheConfiguration: AWSAppSyncCacheConfiguration? = nil, connectionStateChangeHandler: ConnectionStateChangeHandler? = nil, @@ -207,6 +213,8 @@ public class AWSAppSyncClientConfiguration { authType = .oidcToken } else if userPoolsAuthProvider != nil { authType = .amazonCognitoUserPools + } else if awsLambdaAuthProvider != nil { + authType = .awsLambda } else { throw AWSAppSyncClientConfigurationError.invalidAuthConfiguration("Invalid auth provider configuration. Exactly one of the supported auth providers must be passed") } @@ -218,6 +226,7 @@ public class AWSAppSyncClientConfiguration { apiKeyAuthProvider: apiKeyAuthProvider, credentialsProvider: credentialsProvider, userPoolsAuthProvider: userPoolsAuthProvider, + awsLambdaAuthProvider: awsLambdaAuthProvider, oidcAuthProvider: oidcAuthProvider, urlSessionConfiguration: urlSessionConfiguration, cacheConfiguration: cacheConfiguration, @@ -240,6 +249,7 @@ public class AWSAppSyncClientConfiguration { /// - credentialsProvider: A `AWSCredentialsProvider` object for AWS_IAM based authorization. /// - userPoolsAuthProvider: A `AWSCognitoUserPoolsAuthProvider` protocol object for Cognito User Pools based authorization. /// - oidcAuthProvider: A `AWSOIDCAuthProvider` protocol object for any OpenId Connect based authorization. + /// - awsLambdaAuthProvider: A `AWSLambdaAuthProvider` protocol object for AWS Lambda based authorization. /// - urlSessionConfiguration: A `URLSessionConfiguration` configuration object for custom HTTP configuration. /// - cacheConfiguration: Configuration for local queries, mutations, and subscriptions caches. /// - connectionStateChangeHandler: The delegate object to be notified when client network state changes. @@ -254,6 +264,7 @@ public class AWSAppSyncClientConfiguration { apiKeyAuthProvider: AWSAPIKeyAuthProvider?, credentialsProvider: AWSCredentialsProvider?, userPoolsAuthProvider: AWSCognitoUserPoolsAuthProvider?, + awsLambdaAuthProvider: AWSLambdaAuthProvider? = nil, oidcAuthProvider: AWSOIDCAuthProvider?, urlSessionConfiguration: URLSessionConfiguration = URLSessionConfiguration.default, cacheConfiguration: AWSAppSyncCacheConfiguration?, @@ -287,6 +298,7 @@ public class AWSAppSyncClientConfiguration { region: serviceRegion, apiKeyProvider: apikeyProvider, cognitoUserPoolProvider: userPoolsAuthProvider, + awsLambdaAuthProvider: awsLambdaAuthProvider, oidcAuthProvider: oidcAuthProvider, iamAuthProvider: credentialsProvider) self.networkTransport = try AWSAppSyncClientConfiguration.getNetworkTransport( @@ -298,6 +310,7 @@ public class AWSAppSyncClientConfiguration { apiKeyAuthProvider: apiKeyAuthProvider, credentialsProvider: credentialsProvider, userPoolsAuthProvider: userPoolsAuthProvider, + awsLambdaAuthProvider: awsLambdaAuthProvider, oidcAuthProvider: oidcAuthProvider, retryStrategy: retryStrategy ) @@ -315,24 +328,28 @@ public class AWSAppSyncClientConfiguration { /// - credentialsProvider: Should be `nil` unless `authType` is `.awsIAM` /// - oidcAuthProvider: Should be `nil` unless `authType` is `.oidcToken` /// - userPoolsAuthProvider: Should be `nil` unless `authType` is `.amazonCognitoUserPools` + /// - awsLambdaAuthProvider: Should be `nil` unless `authType` is `.awsLambda` /// - Returns: `true` if the auth providers not required for `authType` are all `nil`, `false` otherwise. private static func unusedAuthProvidersAreNil(for authType: AWSAppSyncAuthType, apiKeyAuthProvider: AWSAPIKeyAuthProvider?, credentialsProvider: AWSCredentialsProvider?, oidcAuthProvider: AWSOIDCAuthProvider?, - userPoolsAuthProvider: AWSCognitoUserPoolsAuthProvider?) -> Bool { + userPoolsAuthProvider: AWSCognitoUserPoolsAuthProvider?, + awsLambdaAuthProvider: AWSLambdaAuthProvider?) -> Bool { let unneededProviders: [Any?] switch authType { case .apiKey: - unneededProviders = [credentialsProvider, userPoolsAuthProvider, oidcAuthProvider] + unneededProviders = [credentialsProvider, userPoolsAuthProvider, oidcAuthProvider, awsLambdaAuthProvider] case .amazonCognitoUserPools: - unneededProviders = [apiKeyAuthProvider, credentialsProvider, oidcAuthProvider] + unneededProviders = [apiKeyAuthProvider, credentialsProvider, oidcAuthProvider, awsLambdaAuthProvider] case .awsIAM: - unneededProviders = [apiKeyAuthProvider, oidcAuthProvider, userPoolsAuthProvider] + unneededProviders = [apiKeyAuthProvider, oidcAuthProvider, userPoolsAuthProvider, awsLambdaAuthProvider] case .oidcToken: - unneededProviders = [apiKeyAuthProvider, credentialsProvider, userPoolsAuthProvider] + unneededProviders = [apiKeyAuthProvider, credentialsProvider, userPoolsAuthProvider, awsLambdaAuthProvider] + case .awsLambda: + unneededProviders = [apiKeyAuthProvider, credentialsProvider, userPoolsAuthProvider, oidcAuthProvider] } return unneededProviders.allSatisfy { $0 == nil } @@ -361,6 +378,7 @@ public class AWSAppSyncClientConfiguration { apiKeyAuthProvider: AWSAPIKeyAuthProvider?, credentialsProvider: AWSCredentialsProvider?, userPoolsAuthProvider: AWSCognitoUserPoolsAuthProvider?, + awsLambdaAuthProvider: AWSLambdaAuthProvider?, oidcAuthProvider: AWSOIDCAuthProvider?, retryStrategy: AWSAppSyncRetryStrategy) throws -> AWSAppSyncHTTPNetworkTransport { @@ -370,7 +388,8 @@ public class AWSAppSyncClientConfiguration { apiKeyAuthProvider: apiKeyAuthProvider, credentialsProvider: credentialsProvider, oidcAuthProvider: oidcAuthProvider, - userPoolsAuthProvider: userPoolsAuthProvider + userPoolsAuthProvider: userPoolsAuthProvider, + awsLambdaAuthProvider: awsLambdaAuthProvider ) guard unusedProvidersAreNil else { @@ -405,6 +424,11 @@ public class AWSAppSyncClientConfiguration { urlSessionConfiguration: urlSessionConfiguration, authProvider: oidcAuthProvider, retryStrategy: retryStrategy) + case .awsLambda: + networkTransport = try makeNetworkTransportForAWSLambda(url: url, + urlSessionConfiguration: urlSessionConfiguration, + authProvider: awsLambdaAuthProvider, + retryStrategy: retryStrategy) } return networkTransport @@ -506,6 +530,30 @@ public class AWSAppSyncClientConfiguration { retryStrategy: retryStrategy) return networkTransport } + + /// Returns an AWSAppSyncHTTPNetworkTransport configured to use the provided auth provider + /// + /// - Parameters: + /// - url: The endpoint URL + /// - urlSessionConfiguration: The URLSessionConfiguration to use for network connections managed by the transport + /// - authProvider: The auth provider to use for authenticating network requests + /// - retryStrategy: The retry strategy specified in client configuration + /// - Returns: The AWSAppSyncHTTPNetworkTransport + /// - Throws: An AWSAppSyncClientConfigurationError if the auth provider is nil + private static func makeNetworkTransportForAWSLambda(url: URL, + urlSessionConfiguration: URLSessionConfiguration, + authProvider: AWSLambdaAuthProvider?, + retryStrategy: AWSAppSyncRetryStrategy) throws -> AWSAppSyncHTTPNetworkTransport { + // No default AWS Lambda provider available + guard let authProvider = authProvider else { + throw AWSAppSyncClientConfigurationError.invalidAuthConfiguration("AWSLambdaAuthProvider cannot be nil.") + } + let networkTransport = AWSAppSyncHTTPNetworkTransport(url: url, + awsLambdaAuthProvider: authProvider, + configuration: urlSessionConfiguration, + retryStrategy: retryStrategy) + return networkTransport + } /// Given at least one non-nil parameter, resolves and returns an AWSAPIKeyAuthProvider to use for creating an /// AWSHTTPNetworkTransport. If `apiKeyAuthProvider` is not nil, returns that object. If it is nil, but `apiKey` is not nil, diff --git a/AWSAppSyncClient/AWSAppSyncHTTPNetworkTransport.swift b/AWSAppSyncClient/AWSAppSyncHTTPNetworkTransport.swift index 21f9e7c0..a80484b5 100644 --- a/AWSAppSyncClient/AWSAppSyncHTTPNetworkTransport.swift +++ b/AWSAppSyncClient/AWSAppSyncHTTPNetworkTransport.swift @@ -13,6 +13,7 @@ public class AWSAppSyncHTTPNetworkTransport: AWSNetworkTransport { case apiKey(AWSAPIKeyAuthProvider) case oidcToken(AWSOIDCAuthProvider) case amazonCognitoUserPools(AWSCognitoUserPoolsAuthProvider) + case awsLambda(AWSLambdaAuthProvider) public var appSyncAuthType: AWSAppSyncAuthType { switch self { @@ -24,6 +25,8 @@ public class AWSAppSyncHTTPNetworkTransport: AWSNetworkTransport { return .amazonCognitoUserPools case .oidcToken: return .oidcToken + case .awsLambda: + return .awsLambda } } } @@ -132,6 +135,28 @@ public class AWSAppSyncHTTPNetworkTransport: AWSNetworkTransport { retryStrategy: retryStrategy ) } + + /// Creates a network transport with the specified server URL and session configuration. + /// + /// - Parameters: + /// - url: The URL of a GraphQL server to connect to. + /// - awsLambdaAuthProvider: An implementation of `AWSLambdaAuthProvider` protocol. + /// - configuration: A session configuration used to configure the session. Defaults to `URLSessionConfiguration.default`. + /// - sendOperationIdentifiers: Whether to send operation identifiers rather than full operation text, for use with servers that support query persistence. Defaults to false. + /// - retryStrategy: The retry strategy to be followed by HTTP client + public convenience init(url: URL, + awsLambdaAuthProvider: AWSLambdaAuthProvider, + configuration: URLSessionConfiguration = URLSessionConfiguration.default, + sendOperationIdentifiers: Bool = false, + retryStrategy: AWSAppSyncRetryStrategy = .exponential) { + self.init( + url: url, + urlSession: URLSession(configuration: configuration), + authProvider: .awsLambda(awsLambdaAuthProvider), + sendOperationIdentifiers: sendOperationIdentifiers, + retryStrategy: retryStrategy + ) + } /// Creates a network transport with the specified server URL and session configuration. /// @@ -345,6 +370,23 @@ public class AWSAppSyncHTTPNetworkTransport: AWSNetworkTransport { mutableRequest.setValue(provider.getLatestAuthToken(), forHTTPHeaderField: "authorization") completionHandler(.success(())) } + + case .awsLambda(let provider): + guard let asyncProvider = provider as? AWSLambdaAuthProviderAsync else { + mutableRequest.setValue(provider.getLatestAuthToken(), forHTTPHeaderField: "authorization") + completionHandler(.success(())) + break + } + asyncProvider.getLatestAuthToken { (token, error) in + if let error = error { + completionHandler(.failure(error)) + } else if let token = token { + mutableRequest.setValue(token, forHTTPHeaderField: "authorization") + completionHandler(.success(())) + } else { + fatalError("Invalid data returned in token callback") + } + } } } diff --git a/AWSAppSyncClient/Internal/AuthInterceptor/LambdaAuthInterceptor.swift b/AWSAppSyncClient/Internal/AuthInterceptor/LambdaAuthInterceptor.swift new file mode 100644 index 00000000..1c03f8a3 --- /dev/null +++ b/AWSAppSyncClient/Internal/AuthInterceptor/LambdaAuthInterceptor.swift @@ -0,0 +1,85 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation +import AppSyncRealTimeClient + +/// AWS Lambda Authorizer interceptor +class LambdaAuthInterceptor: AuthInterceptor { + + let authTokenProvider: AWSLambdaAuthProvider + + init(authTokenProvider: AWSLambdaAuthProvider) { + self.authTokenProvider = authTokenProvider + } + + func interceptMessage(_ message: AppSyncMessage, for endpoint: URL) -> AppSyncMessage { + let host = endpoint.host! + let authToken = authTokenProvider.getLatestAuthToken() + guard case .subscribe = message.messageType else { + return message + } + + let authHeader = TokenAuthHeader(token: authToken, host: host) + var payload = message.payload ?? AppSyncMessage.Payload() + payload.authHeader = authHeader + + let signedMessage = AppSyncMessage( + id: message.id, + payload: payload, + type: message.messageType + ) + return signedMessage + } + + func interceptConnection( + _ request: AppSyncConnectionRequest, + for endpoint: URL + ) -> AppSyncConnectionRequest { + let host = endpoint.host! + let authToken = authTokenProvider.getLatestAuthToken() + + let authHeader = TokenAuthHeader(token: authToken, host: host) + let base64Auth = AppSyncJSONHelper.base64AuthenticationBlob(authHeader) + + let payloadData = SubscriptionConstants.emptyPayload.data(using: .utf8) + let payloadBase64 = payloadData?.base64EncodedString() + + guard var urlComponents = URLComponents(url: request.url, resolvingAgainstBaseURL: false) else { + return request + } + let headerQuery = URLQueryItem(name: RealtimeProviderConstants.header, value: base64Auth) + let payloadQuery = URLQueryItem(name: RealtimeProviderConstants.payload, value: payloadBase64) + urlComponents.queryItems = [headerQuery, payloadQuery] + guard let url = urlComponents.url else { + return request + } + let signedRequest = AppSyncConnectionRequest(url: url) + return signedRequest + } +} + +// MARK: - TokenAuthenticationHeader +/// Authentication header for user pool based auth +private class TokenAuthHeader: AuthenticationHeader { + let authorization: String + + init(token: String, host: String) { + self.authorization = token + super.init(host: host) + } + + private enum CodingKeys: String, CodingKey { + case authorization = "Authorization" + } + + override func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(authorization, forKey: .authorization) + try super.encode(to: encoder) + } +} diff --git a/AWSAppSyncClient/Internal/SubscriptionFactory/ConnectionPool/BasicSubscriptionConnectionFactory.swift b/AWSAppSyncClient/Internal/SubscriptionFactory/ConnectionPool/BasicSubscriptionConnectionFactory.swift index 9a437164..e5b8dbcb 100644 --- a/AWSAppSyncClient/Internal/SubscriptionFactory/ConnectionPool/BasicSubscriptionConnectionFactory.swift +++ b/AWSAppSyncClient/Internal/SubscriptionFactory/ConnectionPool/BasicSubscriptionConnectionFactory.swift @@ -14,6 +14,7 @@ class BasicSubscriptionConnectionFactory: SubscriptionConnectionFactory { var userpoolsBasedPool: UserPoolsBasedConnectionPool? var iamBasedPool: IAMBasedConnectionPool? var oidcBasedPool: OIDCBasedConnectionPool? + var lambdaBasedPool: LambdaBasedConnectionPool? let url: URL let retryStrategy: AWSAppSyncRetryStrategy @@ -25,6 +26,7 @@ class BasicSubscriptionConnectionFactory: SubscriptionConnectionFactory { region: AWSRegionType?, apiKeyProvider: AWSAPIKeyAuthProvider?, cognitoUserPoolProvider: AWSCognitoUserPoolsAuthProvider?, + awsLambdaAuthProvider: AWSLambdaAuthProvider?, oidcAuthProvider: AWSOIDCAuthProvider?, iamAuthProvider: AWSCredentialsProvider?) { @@ -44,6 +46,10 @@ class BasicSubscriptionConnectionFactory: SubscriptionConnectionFactory { if let oidcAuthProvider = oidcAuthProvider { self.oidcBasedPool = OIDCBasedConnectionPool(oidcAuthProvider) } + if let awsLambdaAuthProvider = awsLambdaAuthProvider { + self.lambdaBasedPool = LambdaBasedConnectionPool(awsLambdaAuthProvider) + } + } func connection(connectionType: SubscriptionConnectionType) -> SubscriptionConnection? { @@ -70,6 +76,8 @@ class BasicSubscriptionConnectionFactory: SubscriptionConnectionFactory { return userpoolsBasedPool case .oidcToken: return oidcBasedPool + case .awsLambda: + return lambdaBasedPool } } } diff --git a/AWSAppSyncClient/Internal/SubscriptionFactory/LambdaBasedConnectionPool.swift b/AWSAppSyncClient/Internal/SubscriptionFactory/LambdaBasedConnectionPool.swift new file mode 100644 index 00000000..8239a681 --- /dev/null +++ b/AWSAppSyncClient/Internal/SubscriptionFactory/LambdaBasedConnectionPool.swift @@ -0,0 +1,33 @@ +// +// Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// Licensed under the Amazon Software License +// http://aws.amazon.com/asl/ +// + +import Foundation +import AppSyncRealTimeClient + +class LambdaBasedConnectionPool: SubscriptionConnectionPool { + + private let tokenProvider: AWSLambdaAuthProvider + var endPointToProvider: [String: ConnectionProvider] + + init(_ tokenProvider: AWSLambdaAuthProvider) { + self.tokenProvider = tokenProvider + self.endPointToProvider = [:] + } + + func connection(for url: URL, connectionType: SubscriptionConnectionType) -> SubscriptionConnection { + if let connectionProvider = endPointToProvider[url.absoluteString] { + return AppSyncSubscriptionConnection(provider: connectionProvider) + } + + let authInterceptor = LambdaAuthInterceptor(authTokenProvider: tokenProvider) + let connectionProvider = ConnectionProviderFactory.createConnectionProvider(for: url, + authInterceptor: authInterceptor, + connectionType: connectionType) + endPointToProvider[url.absoluteString] = connectionProvider + + return AppSyncSubscriptionConnection(provider: connectionProvider) + } +} diff --git a/AWSAppSyncIntegrationTests/AWSAppSyncLambdaAuthTests.swift b/AWSAppSyncIntegrationTests/AWSAppSyncLambdaAuthTests.swift new file mode 100644 index 00000000..fbff3e20 --- /dev/null +++ b/AWSAppSyncIntegrationTests/AWSAppSyncLambdaAuthTests.swift @@ -0,0 +1,97 @@ +// +// Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// Licensed under the Amazon Software License +// http://aws.amazon.com/asl/ +// + +import XCTest +@testable import AWSAppSync +@testable import AWSCore +@testable import AWSAppSyncTestCommon + +class AWSAppSyncLambdaAuthTests: XCTestCase { + + var appSyncClient: AWSAppSyncClient? + + /// Use this as our timeout value for any operation that hits the network. Note that this may need to be higher + /// than you think, to account for CI systems running in shared environments + private static let networkOperationTimeout = 180.0 + + private static let mutationQueue = DispatchQueue(label: "com.amazonaws.appsync.AWSAppSyncLambdaAuthTests.mutationQueue") + private static let subscriptionAndFetchQueue = DispatchQueue(label: "com.amazonaws.appsync.AWSAppSyncLambdaAuthTests.subscriptionAndFetchQueue") + + + override func setUp() { + super.setUp() + let authType = AppSyncClientTestHelper.AuthenticationType.lambda + do { + appSyncClient = try AWSAppSyncLambdaAuthTests.makeAppSyncClient(authType: authType) + } catch { + XCTFail(error.localizedDescription) + } + } + + func testMutation() { + let postCreated = expectation(description: "Post created successfully.") + let addPost = DefaultTestPostData.defaultCreatePostWithoutFileUsingParametersMutation + + appSyncClient?.perform(mutation: addPost, queue: AWSAppSyncLambdaAuthTests.mutationQueue, resultHandler: { result, error in + XCTAssertNil(error) + XCTAssertNotNil(result?.data?.createPostWithoutFileUsingParameters?.id) + XCTAssertEqual( + result!.data!.createPostWithoutFileUsingParameters?.author, + DefaultTestPostData.author + ) + print("Created post \(result?.data?.createPostWithoutFileUsingParameters?.id ?? "(ID unexpectedly nil)")") + postCreated.fulfill() + }) + + wait(for: [postCreated], timeout: AWSAppSyncLambdaAuthTests.networkOperationTimeout) + } + + func testQuery() { + let postCreated = expectation(description: "Post created successfully.") + let addPost = DefaultTestPostData.defaultCreatePostWithoutFileUsingParametersMutation + + appSyncClient?.perform(mutation: addPost, queue: AWSAppSyncLambdaAuthTests.mutationQueue, resultHandler: { result, error in + XCTAssertNil(error) + XCTAssertNotNil(result?.data?.createPostWithoutFileUsingParameters?.id) + XCTAssertEqual( + result!.data!.createPostWithoutFileUsingParameters?.author, + DefaultTestPostData.author + ) + postCreated.fulfill() + }) + + wait(for: [postCreated], timeout: AWSAppSyncLambdaAuthTests.networkOperationTimeout) + + let query = ListPostsQuery() + + let listPostsCompleted = expectation(description: "Query done successfully.") + + appSyncClient?.fetch(query: query, cachePolicy: .fetchIgnoringCacheData, queue: AWSAppSyncLambdaAuthTests.subscriptionAndFetchQueue) { result, error in + XCTAssertNil(error) + XCTAssertNotNil(result?.data?.listPosts) + XCTAssertGreaterThan(result!.data!.listPosts!.count, 0, "Expected service to return at least 1 post.") + listPostsCompleted.fulfill() + } + + wait(for: [listPostsCompleted], timeout: AWSAppSyncLambdaAuthTests.networkOperationTimeout) + } + + + // MARK: - Utilities + + static func makeAppSyncClient(authType: AppSyncClientTestHelper.AuthenticationType, + cacheConfiguration: AWSAppSyncCacheConfiguration? = nil) throws -> DeinitNotifiableAppSyncClient { + + let testBundle = Bundle(for: AWSAppSyncLambdaAuthTests.self) + let helper = try AppSyncClientTestHelper( + with: authType, + cacheConfiguration: cacheConfiguration, + testBundle: testBundle + ) + return helper.appSyncClient + } +} + diff --git a/AWSAppSyncIntegrationTests/ConsoleResources/appsync-lambda-authorizer.js b/AWSAppSyncIntegrationTests/ConsoleResources/appsync-lambda-authorizer.js new file mode 100644 index 00000000..ce2f2353 --- /dev/null +++ b/AWSAppSyncIntegrationTests/ConsoleResources/appsync-lambda-authorizer.js @@ -0,0 +1,13 @@ +exports.handler = async (event) => { + console.log(`auth event >`, JSON.stringify(event, null, 2)) + const { + authorizationToken, + requestContext: { apiId, accountId }, + } = event + const response = { + isAuthorized: authorizationToken === 'custom-lambda-token', + ttlOverride: 10, + } + console.log(`response >`, JSON.stringify(response, null, 2)) + return response +}; diff --git a/AWSAppSyncTestCommon/AppSyncClientTestConfiguration.swift b/AWSAppSyncTestCommon/AppSyncClientTestConfiguration.swift index 5f59cd22..9947a096 100644 --- a/AWSAppSyncTestCommon/AppSyncClientTestConfiguration.swift +++ b/AWSAppSyncTestCommon/AppSyncClientTestConfiguration.swift @@ -13,6 +13,9 @@ struct AppSyncClientTestConfiguration { static let apiKey = "AppSyncAPIKey" static let apiKeyEndpointURL = "AppSyncEndpointAPIKey" static let apiKeyEndpointRegion = "AppSyncEndpointAPIKeyRegion" + + static let lambdaEndpointURL = "AppSyncEndpointAPIKeyLambda" + static let lambdaEndpointRegion = "AppSyncEndpointAPIKeyLambdaRegion" static let cognitoPoolId = "CognitoIdentityPoolId" static let cognitoPoolRegion = "CognitoIdentityPoolRegion" @@ -37,12 +40,17 @@ struct AppSyncClientTestConfiguration { bucketName: "FOR_UNIT_TESTING", bucketRegion: .USEast1, clientDatabasePrefix: "", - apiKeyForCognitoPoolEndpoint: "FOR_UNIT_TESTING") + apiKeyForCognitoPoolEndpoint: "FOR_UNIT_TESTING", + lambdaEndpointURL: URL(string: "http://www.amazon.com/for_unit_testing")!, + lambdaEndpointRegion: .USEast1) }() let apiKey: String let apiKeyEndpointURL: URL let apiKeyEndpointRegion: AWSRegionType + + let lambdaEndpointURL: URL + let lambdaEndpointRegion: AWSRegionType let cognitoPoolId: String let cognitoPoolRegion: AWSRegionType @@ -74,7 +82,9 @@ struct AppSyncClientTestConfiguration { bucketName: AppSyncClientTestConfigurationDefaults.bucketName, bucketRegion: AppSyncClientTestConfigurationDefaults.bucketRegion, clientDatabasePrefix: "", - apiKeyForCognitoPoolEndpoint: AppSyncClientTestConfigurationDefaults.apiKeyForCognitoPoolEndpoint) + apiKeyForCognitoPoolEndpoint: AppSyncClientTestConfigurationDefaults.apiKeyForCognitoPoolEndpoint, + lambdaEndpointURL: AppSyncClientTestConfigurationDefaults.lambdaEndpointURL, + lambdaEndpointRegion: AppSyncClientTestConfigurationDefaults.lambdaEndpointRegion) } init?(with bundle: Bundle) { @@ -146,6 +156,18 @@ struct AppSyncClientTestConfiguration { } self.bucketRegion = bucketRegionString.aws_regionTypeValue() self.clientDatabasePrefix = "" + + guard let lambdaEndpointRegionString = jsonObject[JSONKeys.lambdaEndpointRegion] as? String else { + return nil + } + self.lambdaEndpointRegion = lambdaEndpointRegionString.aws_regionTypeValue() + + guard let lambdaEndpointString = jsonObject[JSONKeys.lambdaEndpointURL] as? String, + let lambdaEndpoint = URL(string: lambdaEndpointString) else { + return nil + } + self.lambdaEndpointURL = lambdaEndpoint + } private init(apiKey: String, @@ -158,7 +180,9 @@ struct AppSyncClientTestConfiguration { bucketName: String, bucketRegion: AWSRegionType, clientDatabasePrefix: String?, - apiKeyForCognitoPoolEndpoint: String) { + apiKeyForCognitoPoolEndpoint: String, + lambdaEndpointURL: URL, + lambdaEndpointRegion: AWSRegionType) { self.apiKey = apiKey self.apiKeyEndpointURL = apiKeyEndpointURL self.apiKeyEndpointRegion = apiKeyEndpointRegion @@ -170,6 +194,8 @@ struct AppSyncClientTestConfiguration { self.bucketRegion = bucketRegion self.clientDatabasePrefix = clientDatabasePrefix ?? "" self.apiKeyForCognitoPoolEndpoint = apiKeyForCognitoPoolEndpoint + self.lambdaEndpointRegion = lambdaEndpointRegion + self.lambdaEndpointURL = lambdaEndpointURL } } diff --git a/AWSAppSyncTestCommon/AppSyncClientTestConfigurationDefaults.swift b/AWSAppSyncTestCommon/AppSyncClientTestConfigurationDefaults.swift index dce2db35..95495923 100644 --- a/AWSAppSyncTestCommon/AppSyncClientTestConfigurationDefaults.swift +++ b/AWSAppSyncTestCommon/AppSyncClientTestConfigurationDefaults.swift @@ -49,4 +49,10 @@ struct AppSyncClientTestConfigurationDefaults { // Equivalent to the JSON key "BucketRegion" static let bucketRegion = AWSRegionType.USEast1 + + // Equivalent to the JSON key "AppSyncEndpoint" + static let lambdaEndpointURL = URL(string: "https://localhost")! + + // Equivalent to the JSON key "AppSyncRegion" + static let lambdaEndpointRegion = AWSRegionType.USEast1 } diff --git a/AWSAppSyncTestCommon/AppSyncClientTestHelper.swift b/AWSAppSyncTestCommon/AppSyncClientTestHelper.swift index 1e92f751..0aacc090 100644 --- a/AWSAppSyncTestCommon/AppSyncClientTestHelper.swift +++ b/AWSAppSyncTestCommon/AppSyncClientTestHelper.swift @@ -48,6 +48,7 @@ public class AppSyncClientTestHelper: NSObject { case invalidAPIKey case invalidOIDC case invalidStaticCredentials + case lambda /// Delay set at 120 seconds case delayedInvalidOIDC } @@ -232,6 +233,14 @@ public class AppSyncClientTestHelper: NSObject { s3ObjectManager: s3ObjectManager ) + case .lambda: + appSyncConfig = try AWSAppSyncClientConfiguration( + url: testConfiguration.lambdaEndpointURL, + serviceRegion: testConfiguration.lambdaEndpointRegion, + awsLambdaAuthProvider: MockLambdaAuthProvider(), + cacheConfiguration: cacheConfiguration, + s3ObjectManager: s3ObjectManager + ) } return appSyncConfig diff --git a/AWSAppSyncTestCommon/MockAuthProviders.swift b/AWSAppSyncTestCommon/MockAuthProviders.swift index 10067841..dbdfc801 100644 --- a/AWSAppSyncTestCommon/MockAuthProviders.swift +++ b/AWSAppSyncTestCommon/MockAuthProviders.swift @@ -82,3 +82,16 @@ struct MockAWSCognitoUserPoolsAuthProvider: AWSCognitoUserPoolsAuthProvider { return token } } + + +struct MockLambdaAuthProvider: AWSLambdaAuthProvider { + var token: String + + init(with token: String = "custom-lambda-token") { + self.token = token + } + + func getLatestAuthToken() -> String { + return token + } +} diff --git a/AWSAppSyncUnitTests/AWSAppSyncAuthTypeTests.swift b/AWSAppSyncUnitTests/AWSAppSyncAuthTypeTests.swift index b66837e9..78bee83a 100644 --- a/AWSAppSyncUnitTests/AWSAppSyncAuthTypeTests.swift +++ b/AWSAppSyncUnitTests/AWSAppSyncAuthTypeTests.swift @@ -60,6 +60,10 @@ class AWSAppSyncAuthTypeTests: XCTestCase { try performSuccessDecodableTest(inputString: "AMAZON_COGNITO_USER_POOLS", expectedOutput: .amazonCognitoUserPools) } + func test_SuccessfulDecodable_AWSLambda() throws { + try performSuccessDecodableTest(inputString: "AWS_LAMBDA", expectedOutput: .awsLambda) + } + func test_FailureDecodable_BadData() throws { let inputData = try JSONSerialization.data(withJSONObject: "INVALID_DATA", options: .fragmentsAllowed) XCTAssertThrowsError(try JSONDecoder().decode(AWSAppSyncAuthType.self, from: inputData)) @@ -83,6 +87,10 @@ class AWSAppSyncAuthTypeTests: XCTestCase { try performSuccessEncodableTest(inputType: .amazonCognitoUserPools, expectedString: "AMAZON_COGNITO_USER_POOLS") } + func test_SuccessfulEncodable_AWSLambda() throws { + try performSuccessEncodableTest(inputType: .awsLambda, expectedString: "AWS_LAMBDA") + } + // MARK: - Tests: Hashable func test_Hashable_AwsIAM() { @@ -109,6 +117,14 @@ class AWSAppSyncAuthTypeTests: XCTestCase { XCTAssertNotEqual(oidcToken.hashValue, AWSAppSyncAuthType.amazonCognitoUserPools.hashValue) } + func test_Hashable_AWSLambdaToken() { + let lambdaToken = AWSAppSyncAuthType.awsLambda + XCTAssertEqual(lambdaToken.hashValue, AWSAppSyncAuthType.awsLambda.hashValue) + XCTAssertNotEqual(lambdaToken.hashValue, AWSAppSyncAuthType.awsIAM.hashValue) + XCTAssertNotEqual(lambdaToken.hashValue, AWSAppSyncAuthType.apiKey.hashValue) + XCTAssertNotEqual(lambdaToken.hashValue, AWSAppSyncAuthType.amazonCognitoUserPools.hashValue) + } + func test_Hashable_AmazonCognitoUserPools() { let amazonCognitoUserPools = AWSAppSyncAuthType.amazonCognitoUserPools XCTAssertEqual(amazonCognitoUserPools.hashValue, AWSAppSyncAuthType.amazonCognitoUserPools.hashValue) diff --git a/AWSAppSyncUnitTests/AWSAppSyncClientConfigurationTests.swift b/AWSAppSyncUnitTests/AWSAppSyncClientConfigurationTests.swift index fa24f740..1336388a 100644 --- a/AWSAppSyncUnitTests/AWSAppSyncClientConfigurationTests.swift +++ b/AWSAppSyncUnitTests/AWSAppSyncClientConfigurationTests.swift @@ -355,6 +355,27 @@ class AWSAppSyncClientConfigurationTests: XCTestCase { XCTAssert(clientInfoError.localizedDescription.starts(with: "Invalid Auth Configuration"), "Expected error to begin with 'Invalid Auth Configuration', but got '\(clientInfoError.localizedDescription)'") } } + + func testInitializer_MultipleProviders_AWSLambda() { + let serviceConfig = MockAWSAppSyncServiceConfig( + endpoint: URL(string: "http://www.amazon.com/for_unit_testing")!, + region: .USEast1, + authType: .awsLambda + ) + + do { + let _ = try AWSAppSyncClientConfiguration(appSyncServiceConfig: serviceConfig, + apiKeyAuthProvider: MockAWSAPIKeyAuthProvider(), + awsLambdaAuthProvider: MockLambdaAuthProvider()) + XCTFail("Expected validation to fail with multiple auth providers") + } catch { + guard let clientInfoError = error as? AWSAppSyncClientConfigurationError else { + XCTFail("Expected validation to throw AWSAppSyncClientInfoError if specifying multiple providers, but got \(type(of: error))") + return + } + XCTAssert(clientInfoError.localizedDescription.starts(with: "Invalid Auth Configuration"), "Expected error to begin with 'Invalid Auth Configuration', but got '\(clientInfoError.localizedDescription)'") + } + } // MARK: - Test other derived properties diff --git a/AWSAppSyncUnitTests/Subscription/Connection/ConnectionPool/LambdaBasedConnectionPoolTests.swift b/AWSAppSyncUnitTests/Subscription/Connection/ConnectionPool/LambdaBasedConnectionPoolTests.swift new file mode 100644 index 00000000..083898f3 --- /dev/null +++ b/AWSAppSyncUnitTests/Subscription/Connection/ConnectionPool/LambdaBasedConnectionPoolTests.swift @@ -0,0 +1,76 @@ +// +// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// Licensed under the Amazon Software License +// http://aws.amazon.com/asl/ +// + +import XCTest +@testable import AWSAppSync +@testable import AWSAppSyncTestCommon + +class LambdaBasedConnectionPoolTests: XCTestCase { + + var connectionPool: LambdaBasedConnectionPool! + + let url = URL(string: "http://appsyncendpoint.com/graphql")! + let url2 = URL(string: "http://appsyncendpoint-2.com/graphql")! + + override func setUp() { + connectionPool = LambdaBasedConnectionPool(MockLambdaAuthProvider()) + } + + /// Test retrieve connection + /// + /// - Given: A connection pool + /// - When: + /// - I call connection(for:connectionType:) + /// - Then: + /// - I should get a non-nil connection + /// + func testRetrieveConnection() { + let connection = connectionPool.connection(for: url, connectionType: .appSyncRealtime) + XCTAssertNotNil(connection) + } + + /// Test retrieving multiple connection using the same url + /// + /// - Given: A connection pool + /// - When: + /// - I try to retrieve multiple connection with same url + /// - Then: + /// - I should get non-nil connections for each request. And the internal count of provider should be 1. + /// + func testRetreiveMultipleConnectionSameUrl() { + let connection1 = connectionPool.connection(for: url, connectionType: .appSyncRealtime) + XCTAssertNotNil(connection1) + XCTAssertEqual(connectionPool.endPointToProvider.count, 1, "Only one connection provider should be created") + let provider1 = connectionPool.endPointToProvider[url.absoluteString] + + let connection2 = connectionPool.connection(for: url, connectionType: .appSyncRealtime) + XCTAssertNotNil(connection2) + XCTAssertEqual(connectionPool.endPointToProvider.count, 1, "Only one connection provider should be created") + let provider2 = connectionPool.endPointToProvider[url.absoluteString] + XCTAssertTrue(provider1 === provider2, "Internal connection provider should be same") + } + + /// Test retrieving multiple connection using the different url + /// + /// - Given: A connection pool + /// - When: + /// - I try to retrieve multiple connection with 2 different urls + /// - Then: + /// - I should get non-nil connections for each request. And the internal count of provider should be 2. + /// + func testRetreiveMultipleConnectionDifferentUrl() { + let connection1 = connectionPool.connection(for: url, connectionType: .appSyncRealtime) + XCTAssertNotNil(connection1) + XCTAssertEqual(connectionPool.endPointToProvider.count, 1, "Only one connection provider should be created") + let provider1 = connectionPool.endPointToProvider[url.absoluteString] + + let connection2 = connectionPool.connection(for: url2, connectionType: .appSyncRealtime) + XCTAssertNotNil(connection2) + XCTAssertEqual(connectionPool.endPointToProvider.count, 2, "Second connection provider should be created") + let provider2 = connectionPool.endPointToProvider[url2.absoluteString] + XCTAssertFalse(provider1 === provider2, "Internal connection provider should not be same") + } +} diff --git a/AWSAppSyncUnitTests/Subscription/Connection/ConnectionPool/SubscriptionConnectionFactoryTests.swift b/AWSAppSyncUnitTests/Subscription/Connection/ConnectionPool/SubscriptionConnectionFactoryTests.swift index 82ca09fb..9371f1f7 100644 --- a/AWSAppSyncUnitTests/Subscription/Connection/ConnectionPool/SubscriptionConnectionFactoryTests.swift +++ b/AWSAppSyncUnitTests/Subscription/Connection/ConnectionPool/SubscriptionConnectionFactoryTests.swift @@ -20,6 +20,7 @@ class SubscriptionConnectionFactoryTests: XCTestCase { region: .USWest2, apiKeyProvider: MockAPIKeyAuthProvider(), cognitoUserPoolProvider: MockUserPoolsAuthProvider(), + awsLambdaAuthProvider: MockLambdaAuthProvider(), oidcAuthProvider: MockUserPoolsAuthProvider(), iamAuthProvider: MockIAMAuthProvider()) } diff --git a/README.md b/README.md index e8285b38..5e951673 100644 --- a/README.md +++ b/README.md @@ -193,6 +193,16 @@ You can get the backend setup by following the steps below: - `BucketName` - `BucketRegion` - `AppSyncMultiAuthAPIKey` +1. Create another CloudFormation Stack following step 1-6 above with `API Key` as the Auth type (we'll change that later) + 1. Create a Lambda function using the template provided in this project at `AWSAppSyncIntegrationTests/ConsoleResources/appsync-lambda-authorize +r.js` + 1. Once the stack is complete click on the __Outputs__ tab + 1. Copy the appropriate values to the test configuration file `AppSyncIntegrationTests/appsync_test_credentials.json`: + - `AppSyncEndpointAPIKeyLambda` + - `AppSyncEndpointAPIKeyLambdaRegion` + + 1. Go to the [AWS AppSync console](https://console.aws.amazon.com/appsync/home), select the newly created AppSync instance + 1. In the `Settings` section, change the default authentication type to `AWS Lambda` and select the Lambda function created at the previous step > Note: You must either provide all values in the `AppSyncIntegrationTests/appsync_test_credentials.json` or in code. There is no mechanism to handle partial overrides of one source with the other. All values must be specified before running the integration tests.