From 6073c5837f7d1065108cddd69cc32433aa2bb423 Mon Sep 17 00:00:00 2001 From: Dwayne Coussement Date: Fri, 19 Feb 2021 16:57:09 +0100 Subject: [PATCH 1/5] Add support for AppSync events. --- Sources/AWSLambdaEvents/AppSync.swift | 110 ++++++++++++++++ Tests/AWSLambdaEventsTests/AppSyncTests.swift | 121 ++++++++++++++++++ 2 files changed, 231 insertions(+) create mode 100644 Sources/AWSLambdaEvents/AppSync.swift create mode 100644 Tests/AWSLambdaEventsTests/AppSyncTests.swift diff --git a/Sources/AWSLambdaEvents/AppSync.swift b/Sources/AWSLambdaEvents/AppSync.swift new file mode 100644 index 00000000..fd53da82 --- /dev/null +++ b/Sources/AWSLambdaEvents/AppSync.swift @@ -0,0 +1,110 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2017-2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +public struct AppSync { + public struct Request: Codable { + public let arguments: [String: ArgumentValue] + + public enum ArgumentValue: Codable { + case string(String) + case dictionary([String: String]) + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + if let strValue = try? container.decode(String.self) { + self = .string(strValue) + } else if let dictionaryValue = try? container.decode([String: String].self) { + self = .dictionary(dictionaryValue) + } else { + throw DecodingError.dataCorruptedError(in: container, debugDescription: """ + Unexpected AppSync argument. + Expected a String or a Dictionary. + """) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case .dictionary(let array): + try container.encode(array) + case .string(let str): + try container.encode(str) + } + } + } + + public let info: Info + public struct Info: Codable { + public var selectionSetList: [String] + public var selectionSetGraphQL: String + public var parentTypeName: String + public var fieldName: String + public var variables: [String: String] + } + public let identity: Identity + public struct Identity: Codable { + public struct Claims { + let sub: String + let emailVerified: Bool + let iss: String + let phoneNumberVerified: Bool + let cognitoUsername: String + let aud: String + let eventId: String + let tokenUse: String + let authTime: Int + let phoneNumber: String? + let exp: Int + let iat: Int + let email: String? + + enum CodingKeys: String, CodingKey { + case sub, emailVerified = "email_verified", iss, phoneNumberVerified = "phone_number_verified", cognitoUsername = "cognito:username", aud, eventId = "event_id", tokenUse = "token_use", authTime = "auth_time", phoneNumber = "phone_number", exp, iat, email + } + } + + public let defaultAuthStrategy: String + public let issuer: String + public let sourceIp: [String] + public let sub: String + public let userName: String? + } + } +} + +extension AppSync { + public enum Response: Encodable { + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case .array(let array): + try container.encode(array) + case .object(let object): + try container.encode(object) + case .dictionary(let dictionary): + try container.encode(dictionary) + } + } + + case object(ResultType) + case array([ResultType]) + case dictionary([String: ResultType]) + } + + public typealias JSONStringResponse = Response +} + diff --git a/Tests/AWSLambdaEventsTests/AppSyncTests.swift b/Tests/AWSLambdaEventsTests/AppSyncTests.swift new file mode 100644 index 00000000..5b9a7076 --- /dev/null +++ b/Tests/AWSLambdaEventsTests/AppSyncTests.swift @@ -0,0 +1,121 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2017-2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@testable import AWSLambdaEvents +import XCTest + +class AppSyncTests: XCTestCase { + static let exampleEventBody = """ + { + "arguments": { + "id": "my identifier" + }, + "identity": { + "claims": { + "sub": "192879fc-a240-4bf1-ab5a-d6a00f3063f9", + "email_verified": true, + "iss": "https://cognito-idp.us-west-2.amazonaws.com/us-west-xxxxxxxxxxx", + "phone_number_verified": false, + "cognito:username": "jdoe", + "aud": "7471s60os7h0uu77i1tk27sp9n", + "event_id": "bc334ed8-a938-4474-b644-9547e304e606", + "token_use": "id", + "auth_time": 1599154213, + "phone_number": "+19999999999", + "exp": 1599157813, + "iat": 1599154213, + "email": "jdoe@email.com" + }, + "defaultAuthStrategy": "ALLOW", + "groups": null, + "issuer": "https://cognito-idp.us-west-2.amazonaws.com/us-west-xxxxxxxxxxx", + "sourceIp": [ + "1.1.1.1" + ], + "sub": "192879fc-a240-4bf1-ab5a-d6a00f3063f9", + "username": "jdoe" + }, + "source": null, + "request": { + "headers": { + "x-forwarded-for": "1.1.1.1, 2.2.2.2", + "cloudfront-viewer-country": "US", + "cloudfront-is-tablet-viewer": "false", + "via": "2.0 xxxxxxxxxxxxxxxx.cloudfront.net (CloudFront)", + "cloudfront-forwarded-proto": "https", + "origin": "https://us-west-1.console.aws.amazon.com", + "content-length": "217", + "accept-language": "en-US,en;q=0.9", + "host": "xxxxxxxxxxxxxxxx.appsync-api.us-west-1.amazonaws.com", + "x-forwarded-proto": "https", + "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36", + "accept": "*/*", + "cloudfront-is-mobile-viewer": "false", + "cloudfront-is-smarttv-viewer": "false", + "accept-encoding": "gzip, deflate, br", + "referer": "https://us-west-1.console.aws.amazon.com/appsync/home?region=us-west-1", + "content-type": "application/json", + "sec-fetch-mode": "cors", + "x-amz-cf-id": "3aykhqlUwQeANU-HGY7E_guV5EkNeMMtwyOgiA==", + "x-amzn-trace-id": "Root=1-5f512f51-fac632066c5e848ae714", + "authorization": "eyJraWQiOiJScWFCSlJqYVJlM0hrSnBTUFpIcVRXazNOW...", + "sec-fetch-dest": "empty", + "x-amz-user-agent": "AWS-Console-AppSync/", + "cloudfront-is-desktop-viewer": "true", + "sec-fetch-site": "cross-site", + "x-forwarded-port": "443" + } + }, + "prev": null, + "info": { + "selectionSetList": [ + "id", + "field1", + "field2" + ], + "selectionSetGraphQL": "{ id }", + "parentTypeName": "Mutation", + "fieldName": "createSomething", + "variables": {} + }, + "stash": {} + } + """ + + // MARK: Decoding + func testRequestDecodingExampleEvent() { + let data = AppSyncTests.exampleEventBody.data(using: .utf8)! + var req: AppSync.Request? + XCTAssertNoThrow(req = try JSONDecoder().decode(AppSync.Request.self, from: data)) + + XCTAssertNotNil(req?.arguments) + XCTAssertEqual(req?.arguments["id"], .string("my identifier")) + XCTAssertEqual(req?.info.fieldName, "createSomething") + XCTAssertEqual(req?.info.parentTypeName, "Mutation") + XCTAssertEqual(req?.info.selectionSetList, ["id", "field1", "field2"]) + } +} + +extension AppSync.Request.ArgumentValue: Equatable { + public static func == (lhs: Self, rhs: Self) -> Bool { + switch (lhs, rhs) { + case (.string(let lhsString), .string(let rhsString)): + return lhsString == rhsString + case (.dictionary(let lhsDictionary), .dictionary(let rhsDictionary)): + return lhsDictionary == rhsDictionary + default: + return false + } + } +} From 2e6ca2b1268bd20f1dee38aadcfeb78e76e50ebc Mon Sep 17 00:00:00 2001 From: DwayneCoussement Date: Sun, 21 Feb 2021 16:36:41 +0100 Subject: [PATCH 2/5] AppSync.Request => AppSync.Event Co-authored-by: Laurent Gaches --- Sources/AWSLambdaEvents/AppSync.swift | 15 ++-- Tests/AWSLambdaEventsTests/AppSyncTests.swift | 74 +++++++++++++++++-- 2 files changed, 76 insertions(+), 13 deletions(-) diff --git a/Sources/AWSLambdaEvents/AppSync.swift b/Sources/AWSLambdaEvents/AppSync.swift index fd53da82..df463049 100644 --- a/Sources/AWSLambdaEvents/AppSync.swift +++ b/Sources/AWSLambdaEvents/AppSync.swift @@ -15,7 +15,7 @@ import Foundation public struct AppSync { - public struct Request: Codable { + public struct Event: Decodable { public let arguments: [String: ArgumentValue] public enum ArgumentValue: Codable { @@ -46,8 +46,14 @@ public struct AppSync { } } } - - public let info: Info + public let request: Request + public struct Request: Decodable { + let headers: HTTPHeaders + } + public let source: [String: String]? + public let stash: [String: String]? + + public let info: Info public struct Info: Codable { public var selectionSetList: [String] public var selectionSetGraphQL: String @@ -55,7 +61,7 @@ public struct AppSync { public var fieldName: String public var variables: [String: String] } - public let identity: Identity + public let identity: Identity? public struct Identity: Codable { public struct Claims { let sub: String @@ -107,4 +113,3 @@ extension AppSync { public typealias JSONStringResponse = Response } - diff --git a/Tests/AWSLambdaEventsTests/AppSyncTests.swift b/Tests/AWSLambdaEventsTests/AppSyncTests.swift index 5b9a7076..4d3f4c63 100644 --- a/Tests/AWSLambdaEventsTests/AppSyncTests.swift +++ b/Tests/AWSLambdaEventsTests/AppSyncTests.swift @@ -96,18 +96,76 @@ class AppSyncTests: XCTestCase { // MARK: Decoding func testRequestDecodingExampleEvent() { let data = AppSyncTests.exampleEventBody.data(using: .utf8)! - var req: AppSync.Request? - XCTAssertNoThrow(req = try JSONDecoder().decode(AppSync.Request.self, from: data)) + var event: AppSync.Event? + XCTAssertNoThrow(event = try JSONDecoder().decode(AppSync.Event.self, from: data)) - XCTAssertNotNil(req?.arguments) - XCTAssertEqual(req?.arguments["id"], .string("my identifier")) - XCTAssertEqual(req?.info.fieldName, "createSomething") - XCTAssertEqual(req?.info.parentTypeName, "Mutation") - XCTAssertEqual(req?.info.selectionSetList, ["id", "field1", "field2"]) + XCTAssertNotNil(event?.arguments) + XCTAssertEqual(event?.arguments["id"], .string("my identifier")) + XCTAssertEqual(event?.info.fieldName, "createSomething") + XCTAssertEqual(event?.info.parentTypeName, "Mutation") + XCTAssertEqual(event?.info.selectionSetList, ["id", "field1", "field2"]) + XCTAssertEqual(event?.request.headers["accept-language"], "en-US,en;q=0.9") } + func testRequestDecodingEventWithSource() { + let eventBody = """ + { + "arguments": {}, + "identity": null, + "source": { + "name": "Hello", + "id": "1" + }, + "request": { + "headers": { + "x-forwarded-for": "1.1.1.1, 2.2.2.2", + "accept-encoding": "gzip, deflate, br", + "cloudfront-viewer-country": "CA", + "cloudfront-is-tablet-viewer": "false", + "referer": "https://us-west-2.console.aws.amazon.com/", + "via": "2.0 xxxxxx.cloudfront.net (CloudFront)", + "cloudfront-forwarded-proto": "https", + "origin": "https://us-west-2.console.aws.amazon.com", + "x-api-key": "xxxxxxxxxxxxxxxxxxxxx", + "content-type": "application/json", + "x-amzn-trace-id": "Root=1-5fcd9a24-364c62405b418bd53c7984ce", + "x-amz-cf-id": "3aykhqlUwQeANU-HGY7E_guV5EkNeMMtwyOgiA==", + "content-length": "173", + "x-amz-user-agent": "AWS-Console-AppSync/", + "x-forwarded-proto": "https", + "host": "xxxxxxxxxxxxxxxx.appsync-api.us-west-2.amazonaws.com", + "accept-language": "en-ca", + "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.1 Safari/605.1.15", + "cloudfront-is-desktop-viewer": "true", + "cloudfront-is-mobile-viewer": "false", + "accept": "*/*", + "x-forwarded-port": "443", + "cloudfront-is-smarttv-viewer": "false" + } + }, + "prev": null, + "info": { + "selectionSetList": [ + "address", + "id" + ], + "selectionSetGraphQL": "{ address id}", + "parentTypeName": "Customer", + "fieldName": "address", + "variables": {} + }, + "stash": {} + } + """ + + let data = eventBody.data(using: .utf8)! + var event: AppSync.Event? + XCTAssertNoThrow(event = try JSONDecoder().decode(AppSync.Event.self, from: data)) + XCTAssertEqual(event?.source?["name"], "Hello") + XCTAssertTrue(event?.stash?.isEmpty ?? false, "stash dictionary must be empty") + } } -extension AppSync.Request.ArgumentValue: Equatable { +extension AppSync.Event.ArgumentValue: Equatable { public static func == (lhs: Self, rhs: Self) -> Bool { switch (lhs, rhs) { case (.string(let lhsString), .string(let rhsString)): From 19234945ea487c3ba7aae01b1df01a5acb1a877b Mon Sep 17 00:00:00 2001 From: DwayneCoussement Date: Fri, 26 Feb 2021 09:20:32 +0100 Subject: [PATCH 3/5] Update Sources/AWSLambdaEvents/AppSync.swift Co-authored-by: Laurent Gaches --- Sources/AWSLambdaEvents/AppSync.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/AWSLambdaEvents/AppSync.swift b/Sources/AWSLambdaEvents/AppSync.swift index df463049..b4a5f205 100644 --- a/Sources/AWSLambdaEvents/AppSync.swift +++ b/Sources/AWSLambdaEvents/AppSync.swift @@ -13,7 +13,7 @@ //===----------------------------------------------------------------------===// import Foundation - +// https://docs.aws.amazon.com/appsync/latest/devguide/resolver-context-reference.html public struct AppSync { public struct Event: Decodable { public let arguments: [String: ArgumentValue] From 0d5d61e1cc70a071413ba9bb747d0ea46e7231bc Mon Sep 17 00:00:00 2001 From: Dwayne Coussement Date: Fri, 26 Feb 2021 09:35:00 +0100 Subject: [PATCH 4/5] Swiftformat + review comments --- Sources/AWSLambdaEvents/AppSync.swift | 196 +++++++++-------- Tests/AWSLambdaEventsTests/AppSyncTests.swift | 198 +++++++++--------- 2 files changed, 205 insertions(+), 189 deletions(-) diff --git a/Sources/AWSLambdaEvents/AppSync.swift b/Sources/AWSLambdaEvents/AppSync.swift index b4a5f205..321d82c5 100644 --- a/Sources/AWSLambdaEvents/AppSync.swift +++ b/Sources/AWSLambdaEvents/AppSync.swift @@ -12,104 +12,118 @@ // //===----------------------------------------------------------------------===// -import Foundation // https://docs.aws.amazon.com/appsync/latest/devguide/resolver-context-reference.html -public struct AppSync { - public struct Event: Decodable { - public let arguments: [String: ArgumentValue] - - public enum ArgumentValue: Codable { - case string(String) - case dictionary([String: String]) - - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - if let strValue = try? container.decode(String.self) { - self = .string(strValue) - } else if let dictionaryValue = try? container.decode([String: String].self) { - self = .dictionary(dictionaryValue) - } else { - throw DecodingError.dataCorruptedError(in: container, debugDescription: """ - Unexpected AppSync argument. - Expected a String or a Dictionary. - """) - } - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - switch self { - case .dictionary(let array): - try container.encode(array) - case .string(let str): - try container.encode(str) - } - } - } +public enum AppSync { + public struct Event: Decodable { + public let arguments: [String: ArgumentValue] + + public enum ArgumentValue: Codable { + case string(String) + case dictionary([String: String]) + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + if let strValue = try? container.decode(String.self) { + self = .string(strValue) + } else if let dictionaryValue = try? container.decode([String: String].self) { + self = .dictionary(dictionaryValue) + } else { + throw DecodingError.dataCorruptedError(in: container, debugDescription: """ + Unexpected AppSync argument. + Expected a String or a Dictionary. + """) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case .dictionary(let array): + try container.encode(array) + case .string(let str): + try container.encode(str) + } + } + } + public let request: Request public struct Request: Decodable { let headers: HTTPHeaders } - public let source: [String: String]? - public let stash: [String: String]? - public let info: Info - public struct Info: Codable { - public var selectionSetList: [String] - public var selectionSetGraphQL: String - public var parentTypeName: String - public var fieldName: String - public var variables: [String: String] - } - public let identity: Identity? - public struct Identity: Codable { - public struct Claims { - let sub: String - let emailVerified: Bool - let iss: String - let phoneNumberVerified: Bool - let cognitoUsername: String - let aud: String - let eventId: String - let tokenUse: String - let authTime: Int - let phoneNumber: String? - let exp: Int - let iat: Int - let email: String? - - enum CodingKeys: String, CodingKey { - case sub, emailVerified = "email_verified", iss, phoneNumberVerified = "phone_number_verified", cognitoUsername = "cognito:username", aud, eventId = "event_id", tokenUse = "token_use", authTime = "auth_time", phoneNumber = "phone_number", exp, iat, email - } - } - - public let defaultAuthStrategy: String - public let issuer: String - public let sourceIp: [String] - public let sub: String - public let userName: String? - } - } + public let source: [String: String]? + public let stash: [String: String]? + + public let info: Info + public struct Info: Codable { + public var selectionSetList: [String] + public var selectionSetGraphQL: String + public var parentTypeName: String + public var fieldName: String + public var variables: [String: String] + } + + public let identity: Identity? + public struct Identity: Codable { + public struct Claims { + let sub: String + let emailVerified: Bool + let iss: String + let phoneNumberVerified: Bool + let cognitoUsername: String + let aud: String + let eventId: String + let tokenUse: String + let authTime: Int + let phoneNumber: String? + let exp: Int + let iat: Int + let email: String? + + enum CodingKeys: String, CodingKey { + case sub + case emailVerified = "email_verified" + case iss + case phoneNumberVerified = "phone_number_verified" + case cognitoUsername = "cognito:username" + case aud + case eventId = "event_id" + case tokenUse = "token_use" + case authTime = "auth_time" + case phoneNumber = "phone_number" + case exp + case iat + case email + } + } + + public let defaultAuthStrategy: String + public let issuer: String + public let sourceIp: [String] + public let sub: String + public let userName: String? + } + } } -extension AppSync { - public enum Response: Encodable { - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - switch self { - case .array(let array): - try container.encode(array) - case .object(let object): - try container.encode(object) - case .dictionary(let dictionary): - try container.encode(dictionary) - } - } +public extension AppSync { + enum Response: Encodable { + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case .array(let array): + try container.encode(array) + case .object(let object): + try container.encode(object) + case .dictionary(let dictionary): + try container.encode(dictionary) + } + } + + case object(ResultType) + case array([ResultType]) + case dictionary([String: ResultType]) + } - case object(ResultType) - case array([ResultType]) - case dictionary([String: ResultType]) - } - - public typealias JSONStringResponse = Response + typealias JSONStringResponse = Response } diff --git a/Tests/AWSLambdaEventsTests/AppSyncTests.swift b/Tests/AWSLambdaEventsTests/AppSyncTests.swift index 4d3f4c63..72995b52 100644 --- a/Tests/AWSLambdaEventsTests/AppSyncTests.swift +++ b/Tests/AWSLambdaEventsTests/AppSyncTests.swift @@ -16,96 +16,98 @@ import XCTest class AppSyncTests: XCTestCase { - static let exampleEventBody = """ - { - "arguments": { - "id": "my identifier" - }, - "identity": { - "claims": { - "sub": "192879fc-a240-4bf1-ab5a-d6a00f3063f9", - "email_verified": true, - "iss": "https://cognito-idp.us-west-2.amazonaws.com/us-west-xxxxxxxxxxx", - "phone_number_verified": false, - "cognito:username": "jdoe", - "aud": "7471s60os7h0uu77i1tk27sp9n", - "event_id": "bc334ed8-a938-4474-b644-9547e304e606", - "token_use": "id", - "auth_time": 1599154213, - "phone_number": "+19999999999", - "exp": 1599157813, - "iat": 1599154213, - "email": "jdoe@email.com" - }, - "defaultAuthStrategy": "ALLOW", - "groups": null, - "issuer": "https://cognito-idp.us-west-2.amazonaws.com/us-west-xxxxxxxxxxx", - "sourceIp": [ - "1.1.1.1" - ], - "sub": "192879fc-a240-4bf1-ab5a-d6a00f3063f9", - "username": "jdoe" - }, - "source": null, - "request": { - "headers": { - "x-forwarded-for": "1.1.1.1, 2.2.2.2", - "cloudfront-viewer-country": "US", - "cloudfront-is-tablet-viewer": "false", - "via": "2.0 xxxxxxxxxxxxxxxx.cloudfront.net (CloudFront)", - "cloudfront-forwarded-proto": "https", - "origin": "https://us-west-1.console.aws.amazon.com", - "content-length": "217", - "accept-language": "en-US,en;q=0.9", - "host": "xxxxxxxxxxxxxxxx.appsync-api.us-west-1.amazonaws.com", - "x-forwarded-proto": "https", - "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36", - "accept": "*/*", - "cloudfront-is-mobile-viewer": "false", - "cloudfront-is-smarttv-viewer": "false", - "accept-encoding": "gzip, deflate, br", - "referer": "https://us-west-1.console.aws.amazon.com/appsync/home?region=us-west-1", - "content-type": "application/json", - "sec-fetch-mode": "cors", - "x-amz-cf-id": "3aykhqlUwQeANU-HGY7E_guV5EkNeMMtwyOgiA==", - "x-amzn-trace-id": "Root=1-5f512f51-fac632066c5e848ae714", - "authorization": "eyJraWQiOiJScWFCSlJqYVJlM0hrSnBTUFpIcVRXazNOW...", - "sec-fetch-dest": "empty", - "x-amz-user-agent": "AWS-Console-AppSync/", - "cloudfront-is-desktop-viewer": "true", - "sec-fetch-site": "cross-site", - "x-forwarded-port": "443" - } - }, - "prev": null, - "info": { - "selectionSetList": [ - "id", - "field1", - "field2" - ], - "selectionSetGraphQL": "{ id }", - "parentTypeName": "Mutation", - "fieldName": "createSomething", - "variables": {} - }, - "stash": {} - } - """ + static let exampleEventBody = """ + { + "arguments": { + "id": "my identifier" + }, + "identity": { + "claims": { + "sub": "192879fc-a240-4bf1-ab5a-d6a00f3063f9", + "email_verified": true, + "iss": "https://cognito-idp.us-west-2.amazonaws.com/us-west-xxxxxxxxxxx", + "phone_number_verified": false, + "cognito:username": "jdoe", + "aud": "7471s60os7h0uu77i1tk27sp9n", + "event_id": "bc334ed8-a938-4474-b644-9547e304e606", + "token_use": "id", + "auth_time": 1599154213, + "phone_number": "+19999999999", + "exp": 1599157813, + "iat": 1599154213, + "email": "jdoe@email.com" + }, + "defaultAuthStrategy": "ALLOW", + "groups": null, + "issuer": "https://cognito-idp.us-west-2.amazonaws.com/us-west-xxxxxxxxxxx", + "sourceIp": [ + "1.1.1.1" + ], + "sub": "192879fc-a240-4bf1-ab5a-d6a00f3063f9", + "username": "jdoe" + }, + "source": null, + "request": { + "headers": { + "x-forwarded-for": "1.1.1.1, 2.2.2.2", + "cloudfront-viewer-country": "US", + "cloudfront-is-tablet-viewer": "false", + "via": "2.0 xxxxxxxxxxxxxxxx.cloudfront.net (CloudFront)", + "cloudfront-forwarded-proto": "https", + "origin": "https://us-west-1.console.aws.amazon.com", + "content-length": "217", + "accept-language": "en-US,en;q=0.9", + "host": "xxxxxxxxxxxxxxxx.appsync-api.us-west-1.amazonaws.com", + "x-forwarded-proto": "https", + "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36", + "accept": "*/*", + "cloudfront-is-mobile-viewer": "false", + "cloudfront-is-smarttv-viewer": "false", + "accept-encoding": "gzip, deflate, br", + "referer": "https://us-west-1.console.aws.amazon.com/appsync/home?region=us-west-1", + "content-type": "application/json", + "sec-fetch-mode": "cors", + "x-amz-cf-id": "3aykhqlUwQeANU-HGY7E_guV5EkNeMMtwyOgiA==", + "x-amzn-trace-id": "Root=1-5f512f51-fac632066c5e848ae714", + "authorization": "eyJraWQiOiJScWFCSlJqYVJlM0hrSnBTUFpIcVRXazNOW...", + "sec-fetch-dest": "empty", + "x-amz-user-agent": "AWS-Console-AppSync/", + "cloudfront-is-desktop-viewer": "true", + "sec-fetch-site": "cross-site", + "x-forwarded-port": "443" + } + }, + "prev": null, + "info": { + "selectionSetList": [ + "id", + "field1", + "field2" + ], + "selectionSetGraphQL": "{ id }", + "parentTypeName": "Mutation", + "fieldName": "createSomething", + "variables": {} + }, + "stash": {} + } + """ + + // MARK: Decoding + + func testRequestDecodingExampleEvent() { + let data = AppSyncTests.exampleEventBody.data(using: .utf8)! + var event: AppSync.Event? + XCTAssertNoThrow(event = try JSONDecoder().decode(AppSync.Event.self, from: data)) - // MARK: Decoding - func testRequestDecodingExampleEvent() { - let data = AppSyncTests.exampleEventBody.data(using: .utf8)! - var event: AppSync.Event? - XCTAssertNoThrow(event = try JSONDecoder().decode(AppSync.Event.self, from: data)) + XCTAssertNotNil(event?.arguments) + XCTAssertEqual(event?.arguments["id"], .string("my identifier")) + XCTAssertEqual(event?.info.fieldName, "createSomething") + XCTAssertEqual(event?.info.parentTypeName, "Mutation") + XCTAssertEqual(event?.info.selectionSetList, ["id", "field1", "field2"]) + XCTAssertEqual(event?.request.headers["accept-language"], "en-US,en;q=0.9") + } - XCTAssertNotNil(event?.arguments) - XCTAssertEqual(event?.arguments["id"], .string("my identifier")) - XCTAssertEqual(event?.info.fieldName, "createSomething") - XCTAssertEqual(event?.info.parentTypeName, "Mutation") - XCTAssertEqual(event?.info.selectionSetList, ["id", "field1", "field2"]) - XCTAssertEqual(event?.request.headers["accept-language"], "en-US,en;q=0.9") - } func testRequestDecodingEventWithSource() { let eventBody = """ { @@ -166,14 +168,14 @@ class AppSyncTests: XCTestCase { } extension AppSync.Event.ArgumentValue: Equatable { - public static func == (lhs: Self, rhs: Self) -> Bool { - switch (lhs, rhs) { - case (.string(let lhsString), .string(let rhsString)): - return lhsString == rhsString - case (.dictionary(let lhsDictionary), .dictionary(let rhsDictionary)): - return lhsDictionary == rhsDictionary - default: - return false - } - } + public static func == (lhs: Self, rhs: Self) -> Bool { + switch (lhs, rhs) { + case (.string(let lhsString), .string(let rhsString)): + return lhsString == rhsString + case (.dictionary(let lhsDictionary), .dictionary(let rhsDictionary)): + return lhsDictionary == rhsDictionary + default: + return false + } + } } From c28333cafbe722839cd4d8e36b62765fb69b1bbe Mon Sep 17 00:00:00 2001 From: The Almighty Incredible Dwayne Date: Sat, 27 Feb 2021 10:49:33 +0100 Subject: [PATCH 5/5] Improve parsing of identity --- Sources/AWSLambdaEvents/AppSync.swift | 110 ++++++++++++------ Tests/AWSLambdaEventsTests/AppSyncTests.swift | 90 ++++++++++++++ 2 files changed, 165 insertions(+), 35 deletions(-) diff --git a/Sources/AWSLambdaEvents/AppSync.swift b/Sources/AWSLambdaEvents/AppSync.swift index 321d82c5..002ab68f 100644 --- a/Sources/AWSLambdaEvents/AppSync.swift +++ b/Sources/AWSLambdaEvents/AppSync.swift @@ -64,44 +64,84 @@ public enum AppSync { } public let identity: Identity? - public struct Identity: Codable { - public struct Claims { - let sub: String - let emailVerified: Bool - let iss: String - let phoneNumberVerified: Bool - let cognitoUsername: String - let aud: String - let eventId: String - let tokenUse: String - let authTime: Int - let phoneNumber: String? - let exp: Int - let iat: Int - let email: String? - - enum CodingKeys: String, CodingKey { - case sub - case emailVerified = "email_verified" - case iss - case phoneNumberVerified = "phone_number_verified" - case cognitoUsername = "cognito:username" - case aud - case eventId = "event_id" - case tokenUse = "token_use" - case authTime = "auth_time" - case phoneNumber = "phone_number" - case exp - case iat - case email + public enum Identity: Codable { + case iam(IAMIdentity) + case cognitoUserPools(CognitoUserPoolIdentity) + + public struct IAMIdentity: Codable { + public let accountId: String + public let cognitoIdentityPoolId: String + public let cognitoIdentityId: String + public let sourceIp: [String] + public let username: String? + public let userArn: String + public let cognitoIdentityAuthType: String + public let cognitoIdentityAuthProvider: String + } + + public struct CognitoUserPoolIdentity: Codable { + public let defaultAuthStrategy: String + public let issuer: String + public let sourceIp: [String] + public let sub: String + public let username: String? + + public struct Claims { + let sub: String + let emailVerified: Bool + let iss: String + let phoneNumberVerified: Bool + let cognitoUsername: String + let aud: String + let eventId: String + let tokenUse: String + let authTime: Int + let phoneNumber: String? + let exp: Int + let iat: Int + let email: String? + + enum CodingKeys: String, CodingKey { + case sub + case emailVerified = "email_verified" + case iss + case phoneNumberVerified = "phone_number_verified" + case cognitoUsername = "cognito:username" + case aud + case eventId = "event_id" + case tokenUse = "token_use" + case authTime = "auth_time" + case phoneNumber = "phone_number" + case exp + case iat + case email + } + } + } + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + if let iamIdentity = try? container.decode(IAMIdentity.self) { + self = .iam(iamIdentity) + } else if let cognitoIdentity = try? container.decode(CognitoUserPoolIdentity.self) { + self = .cognitoUserPools(cognitoIdentity) + } else { + throw DecodingError.dataCorruptedError(in: container, debugDescription: """ + Unexpected Identity argument. + Expected a IAM Identity or a Cognito User Pool Identity. + """) } } - public let defaultAuthStrategy: String - public let issuer: String - public let sourceIp: [String] - public let sub: String - public let userName: String? + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case .iam(let iamIdentity): + try container.encode(iamIdentity) + case .cognitoUserPools(let cognitoUserPool): + try container.encode(cognitoUserPool) + } + } } } } diff --git a/Tests/AWSLambdaEventsTests/AppSyncTests.swift b/Tests/AWSLambdaEventsTests/AppSyncTests.swift index 72995b52..5aeda17d 100644 --- a/Tests/AWSLambdaEventsTests/AppSyncTests.swift +++ b/Tests/AWSLambdaEventsTests/AppSyncTests.swift @@ -106,6 +106,17 @@ class AppSyncTests: XCTestCase { XCTAssertEqual(event?.info.parentTypeName, "Mutation") XCTAssertEqual(event?.info.selectionSetList, ["id", "field1", "field2"]) XCTAssertEqual(event?.request.headers["accept-language"], "en-US,en;q=0.9") + + switch event?.identity { + case .cognitoUserPools(let cognitoIdentity): + XCTAssertEqual(cognitoIdentity.defaultAuthStrategy, "ALLOW") + XCTAssertEqual(cognitoIdentity.issuer, "https://cognito-idp.us-west-2.amazonaws.com/us-west-xxxxxxxxxxx") + XCTAssertEqual(cognitoIdentity.sourceIp, ["1.1.1.1"]) + XCTAssertEqual(cognitoIdentity.username, "jdoe") + XCTAssertEqual(cognitoIdentity.sub, "192879fc-a240-4bf1-ab5a-d6a00f3063f9") + default: + XCTAssertTrue(false, "a cognito identity was expected, but didn't find one.") + } } func testRequestDecodingEventWithSource() { @@ -164,6 +175,85 @@ class AppSyncTests: XCTestCase { XCTAssertNoThrow(event = try JSONDecoder().decode(AppSync.Event.self, from: data)) XCTAssertEqual(event?.source?["name"], "Hello") XCTAssertTrue(event?.stash?.isEmpty ?? false, "stash dictionary must be empty") + XCTAssertNil(event?.identity) + } + + func testRequestDecodingIamIdentity() { + let eventBody = """ + { + "arguments": {}, + "identity": { + "accountId" : "accountId1", + "cognitoIdentityPoolId" : "cognitoIdentityPool2", + "cognitoIdentityId" : "cognitoIdentity3", + "sourceIp" : ["1.1.1.1"], + "username" : null, + "userArn" : "arn123", + "cognitoIdentityAuthType" : "authenticated", + "cognitoIdentityAuthProvider" : "authprovider" + }, + "source": { + "name": "Hello", + "id": "1" + }, + "request": { + "headers": { + "x-forwarded-for": "1.1.1.1, 2.2.2.2", + "accept-encoding": "gzip, deflate, br", + "cloudfront-viewer-country": "CA", + "cloudfront-is-tablet-viewer": "false", + "referer": "https://us-west-2.console.aws.amazon.com/", + "via": "2.0 xxxxxx.cloudfront.net (CloudFront)", + "cloudfront-forwarded-proto": "https", + "origin": "https://us-west-2.console.aws.amazon.com", + "x-api-key": "xxxxxxxxxxxxxxxxxxxxx", + "content-type": "application/json", + "x-amzn-trace-id": "Root=1-5fcd9a24-364c62405b418bd53c7984ce", + "x-amz-cf-id": "3aykhqlUwQeANU-HGY7E_guV5EkNeMMtwyOgiA==", + "content-length": "173", + "x-amz-user-agent": "AWS-Console-AppSync/", + "x-forwarded-proto": "https", + "host": "xxxxxxxxxxxxxxxx.appsync-api.us-west-2.amazonaws.com", + "accept-language": "en-ca", + "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.1 Safari/605.1.15", + "cloudfront-is-desktop-viewer": "true", + "cloudfront-is-mobile-viewer": "false", + "accept": "*/*", + "x-forwarded-port": "443", + "cloudfront-is-smarttv-viewer": "false" + } + }, + "prev": null, + "info": { + "selectionSetList": [ + "address", + "id" + ], + "selectionSetGraphQL": "{ address id}", + "parentTypeName": "Customer", + "fieldName": "address", + "variables": {} + }, + "stash": {} + } + """ + + let data = eventBody.data(using: .utf8)! + var event: AppSync.Event? + XCTAssertNoThrow(event = try JSONDecoder().decode(AppSync.Event.self, from: data)) + switch event?.identity { + case .iam(let iamIdentity): + XCTAssertEqual(iamIdentity.accountId, "accountId1") + XCTAssertEqual(iamIdentity.cognitoIdentityPoolId, "cognitoIdentityPool2") + XCTAssertEqual(iamIdentity.cognitoIdentityId, "cognitoIdentity3") + XCTAssertEqual(iamIdentity.sourceIp, ["1.1.1.1"]) + XCTAssertNil(iamIdentity.username) + XCTAssertEqual(iamIdentity.userArn, "arn123") + XCTAssertEqual(iamIdentity.cognitoIdentityAuthType, "authenticated") + XCTAssertEqual(iamIdentity.cognitoIdentityAuthProvider, "authprovider") + default: + XCTAssertTrue(false, "an iam identity was expected, but didn't find one.") + } } }