Skip to content

Add support for AppSync events. #187

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Mar 2, 2021
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 129 additions & 0 deletions Sources/AWSLambdaEvents/AppSync.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//

// https://docs.aws.amazon.com/appsync/latest/devguide/resolver-context-reference.html
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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation in https://docs.aws.amazon.com/appsync/latest/devguide/resolver-context-reference.html#aws-appsync-resolver-context-reference-identity says:

Identity

The identity section contains information about the caller. The shape of this section depends on the authorization type of your AWS AppSync API.

For more information about this section and how it can be used, see Security.

API_KEY authorization
The identity field isn’t populated.

AWS_IAM authorization
The identity has the following shape:

{
    "accountId" : "string",
    "cognitoIdentityPoolId" : "string",
    "cognitoIdentityId" : "string",
    "sourceIp" : ["string"],
    "username" : "string", // IAM user principal
    "userArn" : "string",
    "cognitoIdentityAuthType" : "string", // authenticated/unauthenticated based on the identity type
    "cognitoIdentityAuthProvider" : "string" // the auth provider that was used to obtain the credentials
}

AMAZON_COGNITO_USER_POOLS authorization
The identity has the following shape:

{
    "sub" : "uuid",
    "issuer" : "string",
    "username" : "string"
    "claims" : { ... },
    "sourceIp" : ["x.x.x.x"],
    "defaultAuthStrategy" : "string"
}

Should we model this differently?

Copy link
Contributor Author

@DwayneCoussement DwayneCoussement Feb 26, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good point! I've updated the model slightly here to take this all into account. Some updates were needed for AWS_IAM authorization. Please see the latest commit

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?
}
}
}

public extension AppSync {
enum Response<ResultType: Encodable>: 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])
}

typealias JSONStringResponse = Response<String>
}
181 changes: 181 additions & 0 deletions Tests/AWSLambdaEventsTests/AppSyncTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
//===----------------------------------------------------------------------===//
//
// 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 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")
}

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.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
}
}
}