diff --git a/Sources/AWSLambdaEvents/APIGateway+V2.swift b/Sources/AWSLambdaEvents/APIGateway+V2.swift index 9069ac6..706a994 100644 --- a/Sources/AWSLambdaEvents/APIGateway+V2.swift +++ b/Sources/AWSLambdaEvents/APIGateway+V2.swift @@ -52,6 +52,8 @@ public struct APIGatewayV2Request: Codable { } public let iam: IAM? + + public let lambda: LambdaAuthorizerContext? } public let accountId: String diff --git a/Sources/AWSLambdaEvents/APIGatewayLambdaAuthorizers.swift b/Sources/AWSLambdaEvents/APIGatewayLambdaAuthorizers.swift new file mode 100644 index 0000000..bfae6f8 --- /dev/null +++ b/Sources/AWSLambdaEvents/APIGatewayLambdaAuthorizers.swift @@ -0,0 +1,133 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2017-2022 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 +// +//===----------------------------------------------------------------------===// + +/// `LambdaAuthorizerContext` contains authorizer informations passed to a Lambda function authorizer +public typealias LambdaAuthorizerContext = [String: String] + +/// `APIGatewayLambdaAuthorizerRequest` contains the payload sent to a Lambda Authorizer function +public struct APIGatewayLambdaAuthorizerRequest: Codable { + public let version: String + public let type: String + public let routeArn: String? + public let identitySource: [String] + public let routeKey: String + public let rawPath: String + public let rawQueryString: String + public let headers: [String: String] + + /// `Context` contains information to identify the AWS account and resources invoking the Lambda function. + public struct Context: Codable { + public struct HTTP: Codable { + public let method: HTTPMethod + public let path: String + public let `protocol`: String + public let sourceIp: String + public let userAgent: String + } + + public let accountId: String + public let apiId: String + public let domainName: String + public let domainPrefix: String + public let stage: String + public let requestId: String + + public let http: HTTP + + /// The request time in format: 23/Apr/2020:11:08:18 +0000 + public let time: String + public let timeEpoch: UInt64 + } + + let requestContext: Context? +} + +/// `APIGatewayLambdaAuthorizerSimpleResponse` contains a simple response (yes/no) returned by a Lambda authorizer function +public struct APIGatewayLambdaAuthorizerSimpleResponse: Codable { + public let isAuthorized: Bool + public let context: LambdaAuthorizerContext? + + public init(isAuthorized: Bool, + context: LambdaAuthorizerContext?) { + self.isAuthorized = isAuthorized + self.context = context + } +} + +/// `APIGatewayLambdaAuthorizerPolicyResponse` contains a Policy response (inc. an IAM policy document) returned by a Lambda authorizer function +public struct APIGatewayLambdaAuthorizerPolicyResponse: Codable { + public let principalId: String + + /// `PolicyDocument` contains an IAM policy document + public struct PolicyDocument: Codable { + public let version: String + + public struct Statement: Codable { + public enum Effect: String, Codable { + case allow = "Allow" + case deny = "Deny" + } + + public let action: String + public let effect: Effect + public let resource: String + + public init(action: String, effect: Effect, resource: String) { + self.action = action + self.effect = effect + self.resource = resource + } + + public enum CodingKeys: String, CodingKey { + case action = "Action" + case effect = "Effect" + case resource = "Resource" + } + } + + public let statement: [Statement] + + public init(version: String = "2012-10-17", statement: [Statement]) { + self.version = version + self.statement = statement + } + + public enum CodingKeys: String, CodingKey { + case version = "Version" + case statement = "Statement" + } + } + + public let policyDocument: PolicyDocument + + public let context: LambdaAuthorizerContext? + + public init(principalId: String, policyDocument: PolicyDocument, context: LambdaAuthorizerContext?) { + self.principalId = principalId + self.policyDocument = policyDocument + self.context = context + } +} + +#if swift(>=5.6) +extension LambdaAuthorizerContext: Sendable {} +extension APIGatewayLambdaAuthorizerRequest: Sendable {} +extension APIGatewayLambdaAuthorizerRequest.Context: Sendable {} +extension APIGatewayLambdaAuthorizerRequest.Context.HTTP: Sendable {} +extension APIGatewayLambdaAuthorizerSimpleResponse: Sendable {} +extension APIGatewayLambdaAuthorizerPolicyResponse: Sendable {} +extension APIGatewayLambdaAuthorizerPolicyResponse.PolicyDocument: Sendable {} +extension APIGatewayLambdaAuthorizerPolicyResponse.PolicyDocument.Statement: Sendable {} +extension APIGatewayLambdaAuthorizerPolicyResponse.PolicyDocument.Statement.Effect: Sendable {} +#endif diff --git a/Tests/AWSLambdaEventsTests/APIGatewayLambdaAuthorizerTest.swift b/Tests/AWSLambdaEventsTests/APIGatewayLambdaAuthorizerTest.swift new file mode 100644 index 0000000..ea4dcc6 --- /dev/null +++ b/Tests/AWSLambdaEventsTests/APIGatewayLambdaAuthorizerTest.swift @@ -0,0 +1,208 @@ +//===----------------------------------------------------------------------===// +// +// 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 APIGatewayLambdaAuthorizerTests: XCTestCase { + static let getEventWithLambdaAuthorizer = """ + { + "version": "2.0", + "routeKey": "$default", + "rawPath": "/hello", + "rawQueryString": "", + "headers": { + "accept": "*/*", + "authorization": "AWS4-HMAC-SHA256 Credential=ASIA-redacted/us-east-1/execute-api/aws4_request, SignedHeaders=host;x-amz-date;x-amz-security-token, Signature=289b5fcef3d1156f019cc1140cb5565cc052880a5a0d5586c753e3e3c75556f9", + "content-length": "0", + "host": "74bxj8iqjc.execute-api.us-east-1.amazonaws.com", + "user-agent": "curl/8.4.0", + "x-amz-date": "20231214T203121Z", + "x-amz-security-token": "IQoJb3JpZ2luX2VjEO3//////////-redacted", + "x-amzn-trace-id": "Root=1-657b6619-3222de40051925dd66e1fd72", + "x-forwarded-for": "191.95.150.52", + "x-forwarded-port": "443", + "x-forwarded-proto": "https" + }, + "requestContext": { + "accountId": "012345678912", + "apiId": "74bxj8iqjc", + "authorizer": { + "lambda": { + "abc1": "xyz1", + "abc2": "xyz2", + } + }, + "domainName": "74bxj8iqjc.execute-api.us-east-1.amazonaws.com", + "domainPrefix": "74bxj8iqjc", + "http": { + "method": "GET", + "path": "/liveness", + "protocol": "HTTP/1.1", + "sourceIp": "191.95.150.52", + "userAgent": "curl/8.4.0" + }, + "requestId": "P8zkDiQ8oAMEJsQ=", + "routeKey": "$default", + "stage": "$default", + "time": "14/Dec/2023:20:31:21 +0000", + "timeEpoch": 1702585881671 + }, + "isBase64Encoded": false + } + """ + + static let lambdaAuthorizerRequest = """ + { + "version": "2.0", + "type": "REQUEST", + "routeArn": "arn:aws:execute-api:eu-north-1:000000000000:0000000000/dev/GET/applications", + "identitySource": [ + "abc.xyz.123" + ], + "routeKey": "GET /applications", + "rawPath": "/dev/applications", + "rawQueryString": "", + "headers": { + "accept": "*/*", + "authorization": "abc.xyz.123", + "content-length": "0", + "host": "0000000000.execute-api.eu-north-1.amazonaws.com", + "user-agent": "curl/8.1.2", + "x-amzn-trace-id": "Root=1-00000000-000000000000000000000000", + "x-forwarded-for": "0.0.0.0", + "x-forwarded-port": "443", + "x-forwarded-proto": "https" + }, + "requestContext": { + "accountId": "000000000000", + "apiId": "0000000000", + "domainName": "0000000000.execute-api.eu-north-1.amazonaws.com", + "domainPrefix": "0000000000", + "http": { + "method": "GET", + "path": "/dev/applications", + "protocol": "HTTP/1.1", + "sourceIp": "0.0.0.0", + "userAgent": "curl/8.1.2" + }, + "requestId": "QHACgr8sig0MELg=", + "routeKey": "GET /applications", + "stage": "dev", + "time": "15/Dec/2023:20:35:03 +0000", + "timeEpoch": 1702672503230 + } + } + """ + + static let lambdaAuthorizerSimpleResponse = """ + { + "isAuthorized": true, + "context": { + "exampleKey": "exampleValue" + } + } + """ + + static let lambdaAuthorizerPolicyResponse = """ + { + "principalId": "abcdef", + "policyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": "execute-api:Invoke", + "Effect": "Allow|Deny", + "Resource": "arn:aws:execute-api:{regionId}:{accountId}:{apiId}/{stage}/{httpVerb}/[{resource}/[{child-resources}]]" + } + ] + }, + "context": { + "exampleKey": "exampleValue" + } + } + """ + + // MARK: - Request - + + // MARK: Decoding + + func testRequestDecodingGetRequestWithLambdaAuthorizer() { + let data = APIGatewayLambdaAuthorizerTests.getEventWithLambdaAuthorizer.data(using: .utf8)! + var req: APIGatewayV2Request? + XCTAssertNoThrow(req = try JSONDecoder().decode(APIGatewayV2Request.self, from: data)) + + XCTAssertEqual(req?.rawPath, "/hello") + XCTAssertEqual(req?.context.authorizer?.lambda?.count, 2) + XCTAssertEqual(req?.context.authorizer?.lambda?["abc1"], "xyz1") + XCTAssertEqual(req?.context.authorizer?.lambda?["abc2"], "xyz2") + XCTAssertNil(req?.body) + } + + func testLambdaAuthorizerRequestRequestDecoding() { + let data = APIGatewayLambdaAuthorizerTests.lambdaAuthorizerRequest.data(using: .utf8)! + var req: APIGatewayLambdaAuthorizerRequest? + XCTAssertNoThrow(req = try JSONDecoder().decode(APIGatewayLambdaAuthorizerRequest.self, from: data)) + + XCTAssertEqual(req?.rawPath, "/dev/applications") + XCTAssertEqual(req?.version, "2.0") + } + + // MARK: Encoding + + func testDecodingLambdaAuthorizerSimpleResponse() { + var resp = APIGatewayLambdaAuthorizerSimpleResponse( + isAuthorized: true, + context: ["abc1": "xyz1", "abc2": "xyz2"] + ) + + var data: Data? + XCTAssertNoThrow(data = try JSONEncoder().encode(resp)) + + var stringData: String? + XCTAssertNoThrow(stringData = String(data: try XCTUnwrap(data), encoding: .utf8)) + + data = stringData?.data(using: .utf8) + XCTAssertNoThrow(resp = try JSONDecoder().decode(APIGatewayLambdaAuthorizerSimpleResponse.self, from: XCTUnwrap(data))) + + XCTAssertEqual(resp.isAuthorized, true) + XCTAssertEqual(resp.context?.count, 2) + XCTAssertEqual(resp.context?["abc1"], "xyz1") + } + + func testDecodingLambdaAuthorizerPolicyResponse() { + let statement = APIGatewayLambdaAuthorizerPolicyResponse.PolicyDocument.Statement(action: "s3:getObject", + effect: .allow, + resource: "*") + let policy = APIGatewayLambdaAuthorizerPolicyResponse.PolicyDocument(statement: [statement]) + var resp = APIGatewayLambdaAuthorizerPolicyResponse(principalId: "John Appleseed", + policyDocument: policy, + context: ["abc1": "xyz1", "abc2": "xyz2"]) + + var data: Data? + XCTAssertNoThrow(data = try JSONEncoder().encode(resp)) + + var stringData: String? + XCTAssertNoThrow(stringData = String(data: try XCTUnwrap(data), encoding: .utf8)) + + data = stringData?.data(using: .utf8) + XCTAssertNoThrow(resp = try JSONDecoder().decode(APIGatewayLambdaAuthorizerPolicyResponse.self, from: XCTUnwrap(data))) + + XCTAssertEqual(resp.principalId, "John Appleseed") + XCTAssertEqual(resp.policyDocument.statement.count, 1) + XCTAssertEqual(resp.policyDocument.statement[0].action, "s3:getObject") + XCTAssertEqual(resp.context?.count, 2) + XCTAssertEqual(resp.context?["abc1"], "xyz1") + } +}