Skip to content

Commit 997094f

Browse files
authored
Add support for AWS Lambda Authorizers (#42)
Add support for lambda authorizers
1 parent 2cf63ff commit 997094f

File tree

3 files changed

+343
-0
lines changed

3 files changed

+343
-0
lines changed

Sources/AWSLambdaEvents/APIGateway+V2.swift

+2
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ public struct APIGatewayV2Request: Codable {
5252
}
5353

5454
public let iam: IAM?
55+
56+
public let lambda: LambdaAuthorizerContext?
5557
}
5658

5759
public let accountId: String
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftAWSLambdaRuntime open source project
4+
//
5+
// Copyright (c) 2017-2022 Apple Inc. and the SwiftAWSLambdaRuntime project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
/// `LambdaAuthorizerContext` contains authorizer informations passed to a Lambda function authorizer
16+
public typealias LambdaAuthorizerContext = [String: String]
17+
18+
/// `APIGatewayLambdaAuthorizerRequest` contains the payload sent to a Lambda Authorizer function
19+
public struct APIGatewayLambdaAuthorizerRequest: Codable {
20+
public let version: String
21+
public let type: String
22+
public let routeArn: String?
23+
public let identitySource: [String]
24+
public let routeKey: String
25+
public let rawPath: String
26+
public let rawQueryString: String
27+
public let headers: [String: String]
28+
29+
/// `Context` contains information to identify the AWS account and resources invoking the Lambda function.
30+
public struct Context: Codable {
31+
public struct HTTP: Codable {
32+
public let method: HTTPMethod
33+
public let path: String
34+
public let `protocol`: String
35+
public let sourceIp: String
36+
public let userAgent: String
37+
}
38+
39+
public let accountId: String
40+
public let apiId: String
41+
public let domainName: String
42+
public let domainPrefix: String
43+
public let stage: String
44+
public let requestId: String
45+
46+
public let http: HTTP
47+
48+
/// The request time in format: 23/Apr/2020:11:08:18 +0000
49+
public let time: String
50+
public let timeEpoch: UInt64
51+
}
52+
53+
let requestContext: Context?
54+
}
55+
56+
/// `APIGatewayLambdaAuthorizerSimpleResponse` contains a simple response (yes/no) returned by a Lambda authorizer function
57+
public struct APIGatewayLambdaAuthorizerSimpleResponse: Codable {
58+
public let isAuthorized: Bool
59+
public let context: LambdaAuthorizerContext?
60+
61+
public init(isAuthorized: Bool,
62+
context: LambdaAuthorizerContext?) {
63+
self.isAuthorized = isAuthorized
64+
self.context = context
65+
}
66+
}
67+
68+
/// `APIGatewayLambdaAuthorizerPolicyResponse` contains a Policy response (inc. an IAM policy document) returned by a Lambda authorizer function
69+
public struct APIGatewayLambdaAuthorizerPolicyResponse: Codable {
70+
public let principalId: String
71+
72+
/// `PolicyDocument` contains an IAM policy document
73+
public struct PolicyDocument: Codable {
74+
public let version: String
75+
76+
public struct Statement: Codable {
77+
public enum Effect: String, Codable {
78+
case allow = "Allow"
79+
case deny = "Deny"
80+
}
81+
82+
public let action: String
83+
public let effect: Effect
84+
public let resource: String
85+
86+
public init(action: String, effect: Effect, resource: String) {
87+
self.action = action
88+
self.effect = effect
89+
self.resource = resource
90+
}
91+
92+
public enum CodingKeys: String, CodingKey {
93+
case action = "Action"
94+
case effect = "Effect"
95+
case resource = "Resource"
96+
}
97+
}
98+
99+
public let statement: [Statement]
100+
101+
public init(version: String = "2012-10-17", statement: [Statement]) {
102+
self.version = version
103+
self.statement = statement
104+
}
105+
106+
public enum CodingKeys: String, CodingKey {
107+
case version = "Version"
108+
case statement = "Statement"
109+
}
110+
}
111+
112+
public let policyDocument: PolicyDocument
113+
114+
public let context: LambdaAuthorizerContext?
115+
116+
public init(principalId: String, policyDocument: PolicyDocument, context: LambdaAuthorizerContext?) {
117+
self.principalId = principalId
118+
self.policyDocument = policyDocument
119+
self.context = context
120+
}
121+
}
122+
123+
#if swift(>=5.6)
124+
extension LambdaAuthorizerContext: Sendable {}
125+
extension APIGatewayLambdaAuthorizerRequest: Sendable {}
126+
extension APIGatewayLambdaAuthorizerRequest.Context: Sendable {}
127+
extension APIGatewayLambdaAuthorizerRequest.Context.HTTP: Sendable {}
128+
extension APIGatewayLambdaAuthorizerSimpleResponse: Sendable {}
129+
extension APIGatewayLambdaAuthorizerPolicyResponse: Sendable {}
130+
extension APIGatewayLambdaAuthorizerPolicyResponse.PolicyDocument: Sendable {}
131+
extension APIGatewayLambdaAuthorizerPolicyResponse.PolicyDocument.Statement: Sendable {}
132+
extension APIGatewayLambdaAuthorizerPolicyResponse.PolicyDocument.Statement.Effect: Sendable {}
133+
#endif
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftAWSLambdaRuntime open source project
4+
//
5+
// Copyright (c) 2017-2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
@testable import AWSLambdaEvents
16+
import XCTest
17+
18+
class APIGatewayLambdaAuthorizerTests: XCTestCase {
19+
static let getEventWithLambdaAuthorizer = """
20+
{
21+
"version": "2.0",
22+
"routeKey": "$default",
23+
"rawPath": "/hello",
24+
"rawQueryString": "",
25+
"headers": {
26+
"accept": "*/*",
27+
"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",
28+
"content-length": "0",
29+
"host": "74bxj8iqjc.execute-api.us-east-1.amazonaws.com",
30+
"user-agent": "curl/8.4.0",
31+
"x-amz-date": "20231214T203121Z",
32+
"x-amz-security-token": "IQoJb3JpZ2luX2VjEO3//////////-redacted",
33+
"x-amzn-trace-id": "Root=1-657b6619-3222de40051925dd66e1fd72",
34+
"x-forwarded-for": "191.95.150.52",
35+
"x-forwarded-port": "443",
36+
"x-forwarded-proto": "https"
37+
},
38+
"requestContext": {
39+
"accountId": "012345678912",
40+
"apiId": "74bxj8iqjc",
41+
"authorizer": {
42+
"lambda": {
43+
"abc1": "xyz1",
44+
"abc2": "xyz2",
45+
}
46+
},
47+
"domainName": "74bxj8iqjc.execute-api.us-east-1.amazonaws.com",
48+
"domainPrefix": "74bxj8iqjc",
49+
"http": {
50+
"method": "GET",
51+
"path": "/liveness",
52+
"protocol": "HTTP/1.1",
53+
"sourceIp": "191.95.150.52",
54+
"userAgent": "curl/8.4.0"
55+
},
56+
"requestId": "P8zkDiQ8oAMEJsQ=",
57+
"routeKey": "$default",
58+
"stage": "$default",
59+
"time": "14/Dec/2023:20:31:21 +0000",
60+
"timeEpoch": 1702585881671
61+
},
62+
"isBase64Encoded": false
63+
}
64+
"""
65+
66+
static let lambdaAuthorizerRequest = """
67+
{
68+
"version": "2.0",
69+
"type": "REQUEST",
70+
"routeArn": "arn:aws:execute-api:eu-north-1:000000000000:0000000000/dev/GET/applications",
71+
"identitySource": [
72+
"abc.xyz.123"
73+
],
74+
"routeKey": "GET /applications",
75+
"rawPath": "/dev/applications",
76+
"rawQueryString": "",
77+
"headers": {
78+
"accept": "*/*",
79+
"authorization": "abc.xyz.123",
80+
"content-length": "0",
81+
"host": "0000000000.execute-api.eu-north-1.amazonaws.com",
82+
"user-agent": "curl/8.1.2",
83+
"x-amzn-trace-id": "Root=1-00000000-000000000000000000000000",
84+
"x-forwarded-for": "0.0.0.0",
85+
"x-forwarded-port": "443",
86+
"x-forwarded-proto": "https"
87+
},
88+
"requestContext": {
89+
"accountId": "000000000000",
90+
"apiId": "0000000000",
91+
"domainName": "0000000000.execute-api.eu-north-1.amazonaws.com",
92+
"domainPrefix": "0000000000",
93+
"http": {
94+
"method": "GET",
95+
"path": "/dev/applications",
96+
"protocol": "HTTP/1.1",
97+
"sourceIp": "0.0.0.0",
98+
"userAgent": "curl/8.1.2"
99+
},
100+
"requestId": "QHACgr8sig0MELg=",
101+
"routeKey": "GET /applications",
102+
"stage": "dev",
103+
"time": "15/Dec/2023:20:35:03 +0000",
104+
"timeEpoch": 1702672503230
105+
}
106+
}
107+
"""
108+
109+
static let lambdaAuthorizerSimpleResponse = """
110+
{
111+
"isAuthorized": true,
112+
"context": {
113+
"exampleKey": "exampleValue"
114+
}
115+
}
116+
"""
117+
118+
static let lambdaAuthorizerPolicyResponse = """
119+
{
120+
"principalId": "abcdef",
121+
"policyDocument": {
122+
"Version": "2012-10-17",
123+
"Statement": [
124+
{
125+
"Action": "execute-api:Invoke",
126+
"Effect": "Allow|Deny",
127+
"Resource": "arn:aws:execute-api:{regionId}:{accountId}:{apiId}/{stage}/{httpVerb}/[{resource}/[{child-resources}]]"
128+
}
129+
]
130+
},
131+
"context": {
132+
"exampleKey": "exampleValue"
133+
}
134+
}
135+
"""
136+
137+
// MARK: - Request -
138+
139+
// MARK: Decoding
140+
141+
func testRequestDecodingGetRequestWithLambdaAuthorizer() {
142+
let data = APIGatewayLambdaAuthorizerTests.getEventWithLambdaAuthorizer.data(using: .utf8)!
143+
var req: APIGatewayV2Request?
144+
XCTAssertNoThrow(req = try JSONDecoder().decode(APIGatewayV2Request.self, from: data))
145+
146+
XCTAssertEqual(req?.rawPath, "/hello")
147+
XCTAssertEqual(req?.context.authorizer?.lambda?.count, 2)
148+
XCTAssertEqual(req?.context.authorizer?.lambda?["abc1"], "xyz1")
149+
XCTAssertEqual(req?.context.authorizer?.lambda?["abc2"], "xyz2")
150+
XCTAssertNil(req?.body)
151+
}
152+
153+
func testLambdaAuthorizerRequestRequestDecoding() {
154+
let data = APIGatewayLambdaAuthorizerTests.lambdaAuthorizerRequest.data(using: .utf8)!
155+
var req: APIGatewayLambdaAuthorizerRequest?
156+
XCTAssertNoThrow(req = try JSONDecoder().decode(APIGatewayLambdaAuthorizerRequest.self, from: data))
157+
158+
XCTAssertEqual(req?.rawPath, "/dev/applications")
159+
XCTAssertEqual(req?.version, "2.0")
160+
}
161+
162+
// MARK: Encoding
163+
164+
func testDecodingLambdaAuthorizerSimpleResponse() {
165+
var resp = APIGatewayLambdaAuthorizerSimpleResponse(
166+
isAuthorized: true,
167+
context: ["abc1": "xyz1", "abc2": "xyz2"]
168+
)
169+
170+
var data: Data?
171+
XCTAssertNoThrow(data = try JSONEncoder().encode(resp))
172+
173+
var stringData: String?
174+
XCTAssertNoThrow(stringData = String(data: try XCTUnwrap(data), encoding: .utf8))
175+
176+
data = stringData?.data(using: .utf8)
177+
XCTAssertNoThrow(resp = try JSONDecoder().decode(APIGatewayLambdaAuthorizerSimpleResponse.self, from: XCTUnwrap(data)))
178+
179+
XCTAssertEqual(resp.isAuthorized, true)
180+
XCTAssertEqual(resp.context?.count, 2)
181+
XCTAssertEqual(resp.context?["abc1"], "xyz1")
182+
}
183+
184+
func testDecodingLambdaAuthorizerPolicyResponse() {
185+
let statement = APIGatewayLambdaAuthorizerPolicyResponse.PolicyDocument.Statement(action: "s3:getObject",
186+
effect: .allow,
187+
resource: "*")
188+
let policy = APIGatewayLambdaAuthorizerPolicyResponse.PolicyDocument(statement: [statement])
189+
var resp = APIGatewayLambdaAuthorizerPolicyResponse(principalId: "John Appleseed",
190+
policyDocument: policy,
191+
context: ["abc1": "xyz1", "abc2": "xyz2"])
192+
193+
var data: Data?
194+
XCTAssertNoThrow(data = try JSONEncoder().encode(resp))
195+
196+
var stringData: String?
197+
XCTAssertNoThrow(stringData = String(data: try XCTUnwrap(data), encoding: .utf8))
198+
199+
data = stringData?.data(using: .utf8)
200+
XCTAssertNoThrow(resp = try JSONDecoder().decode(APIGatewayLambdaAuthorizerPolicyResponse.self, from: XCTUnwrap(data)))
201+
202+
XCTAssertEqual(resp.principalId, "John Appleseed")
203+
XCTAssertEqual(resp.policyDocument.statement.count, 1)
204+
XCTAssertEqual(resp.policyDocument.statement[0].action, "s3:getObject")
205+
XCTAssertEqual(resp.context?.count, 2)
206+
XCTAssertEqual(resp.context?["abc1"], "xyz1")
207+
}
208+
}

0 commit comments

Comments
 (0)