diff --git a/lambda/shared/src/main/scala/feral/lambda/events/ApiGatewayCustomAuthorizerEvent.scala b/lambda/shared/src/main/scala/feral/lambda/events/ApiGatewayCustomAuthorizerEvent.scala new file mode 100644 index 00000000..5faa9592 --- /dev/null +++ b/lambda/shared/src/main/scala/feral/lambda/events/ApiGatewayCustomAuthorizerEvent.scala @@ -0,0 +1,221 @@ +/* + * Copyright 2021 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package feral.lambda +package events + +import com.comcast.ip4s.Hostname +import feral.lambda.KernelSource +import io.circe.Decoder +import natchez.Kernel +import org.typelevel.ci.CIString + +import codecs.decodeHostname +import codecs.decodeKeyCIString + +sealed abstract class AuthorizerRequestContext { + def resourceId: String + def resourcePath: String + def httpMethod: String + def extendedRequestId: String + def requestTime: String + def path: String + def accountId: String + def protocol: String + def stage: String + def domainPrefix: String + def requestTimeEpoch: Long + def requestId: String + def identity: Map[String, Option[String]] + def domainName: Hostname + def deploymentId: String + def apiId: String +} + +object AuthorizerRequestContext { + + def apply( + resourceId: String, + resourcePath: String, + httpMethod: String, + extendedRequestId: String, + requestTime: String, + path: String, + accountId: String, + protocol: String, + stage: String, + domainPrefix: String, + requestTimeEpoch: Long, + requestId: String, + identity: Map[String, Option[String]], + domainName: Hostname, + deploymentId: String, + apiId: String + ): AuthorizerRequestContext = + new Impl( + resourceId, + resourcePath, + httpMethod, + extendedRequestId, + requestTime, + path, + accountId, + protocol, + stage, + domainPrefix, + requestTimeEpoch, + requestId, + identity, + domainName, + deploymentId, + apiId + ) + + implicit def decoder: Decoder[AuthorizerRequestContext] = Decoder.forProduct16( + "resourceId", + "resourcePath", + "httpMethod", + "extendedRequestId", + "requestTime", + "path", + "accountId", + "protocol", + "stage", + "domainPrefix", + "requestTimeEpoch", + "requestId", + "identity", + "domainName", + "deploymentId", + "apiId" + )(AuthorizerRequestContext.apply) + + private case class Impl( + resourceId: String, + resourcePath: String, + httpMethod: String, + extendedRequestId: String, + requestTime: String, + path: String, + accountId: String, + protocol: String, + stage: String, + domainPrefix: String, + requestTimeEpoch: Long, + requestId: String, + identity: Map[String, Option[String]], + domainName: Hostname, + deploymentId: String, + apiId: String + ) extends AuthorizerRequestContext { + override def productPrefix = "AuthorizerRequestContext" + } +} + +sealed abstract class ApiGatewayCustomAuthorizerEvent { + def eventType: CustomAuthorizerEventType + def methodArn: String + def resource: String + def path: String + def httpMethod: String + def headers: Option[Map[CIString, String]] + def multiValueHeaders: Map[CIString, List[String]] + def queryStringParameters: Map[CIString, Option[String]] + def multiValueQueryStringParameters: Map[CIString, Option[List[String]]] + def pathParameters: Map[CIString, String] + def stageVariables: Map[CIString, String] + def requestContext: AuthorizerRequestContext +} + +object ApiGatewayCustomAuthorizerEvent { + + def apply( + eventType: CustomAuthorizerEventType, + methodArn: String, + resource: String, + path: String, + httpMethod: String, + headers: Option[Map[CIString, String]], + multiValueHeaders: Map[CIString, List[String]], + queryStringParameters: Map[CIString, Option[String]], + multiValueQueryStringParameters: Map[CIString, Option[List[String]]], + pathParameters: Map[CIString, String], + stageVariables: Map[CIString, String], + requestContext: AuthorizerRequestContext + ): ApiGatewayCustomAuthorizerEvent = + new Impl( + eventType, + methodArn, + resource, + path, + httpMethod, + headers, + multiValueHeaders, + queryStringParameters, + multiValueQueryStringParameters, + pathParameters, + stageVariables, + requestContext + ) + + implicit def kernelSource: KernelSource[ApiGatewayCustomAuthorizerEvent] = + e => Kernel(e.headers.getOrElse(Map.empty)) + + implicit def decoder: Decoder[ApiGatewayCustomAuthorizerEvent] = Decoder.forProduct12( + "type", + "methodArn", + "resource", + "path", + "httpMethod", + "headers", + "multiValueHeaders", + "queryStringParameters", + "multiValueQueryStringParameters", + "pathParameters", + "stageVariables", + "requestContext" + )(ApiGatewayCustomAuthorizerEvent.apply) + + private case class Impl( + eventType: CustomAuthorizerEventType, + methodArn: String, + resource: String, + path: String, + httpMethod: String, + headers: Option[Map[CIString, String]], + multiValueHeaders: Map[CIString, List[String]], + queryStringParameters: Map[CIString, Option[String]], + multiValueQueryStringParameters: Map[CIString, Option[List[String]]], + pathParameters: Map[CIString, String], + stageVariables: Map[CIString, String], + requestContext: AuthorizerRequestContext + ) extends ApiGatewayCustomAuthorizerEvent { + override def productPrefix = "ApiGatewayCustomAuthorizerEvent" + } +} + +sealed abstract class CustomAuthorizerEventType + +object CustomAuthorizerEventType { + case object Token extends CustomAuthorizerEventType + case object Request extends CustomAuthorizerEventType + + private[events] implicit val decoder: Decoder[CustomAuthorizerEventType] = + Decoder.decodeString.map { + case "TOKEN" => Token + case "REQUEST" => Request + } +} diff --git a/lambda/shared/src/main/scala/feral/lambda/events/ApiGatewayCustomAuthorizerEventResult.scala b/lambda/shared/src/main/scala/feral/lambda/events/ApiGatewayCustomAuthorizerEventResult.scala new file mode 100644 index 00000000..7b684b61 --- /dev/null +++ b/lambda/shared/src/main/scala/feral/lambda/events/ApiGatewayCustomAuthorizerEventResult.scala @@ -0,0 +1,96 @@ +/* + * Copyright 2021 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package feral.lambda +package events + +import io.circe.Encoder + +import java.time.LocalDate + +import codecs.encodeDate + +sealed abstract class ApiGatewayCustomAuthorizerEventResult { + def principalId: String + def policyDocument: PolicyDocument +} + +object ApiGatewayCustomAuthorizerEventResult { + + def apply( + principalId: String, + policyDocument: PolicyDocument): ApiGatewayCustomAuthorizerEventResult = + Impl(principalId, policyDocument) + + implicit def encoder: Encoder[ApiGatewayCustomAuthorizerEventResult] = Encoder.forProduct2( + "principalId", + "policyDocument" + )(r => (r.principalId, r.policyDocument)) + + private case class Impl( + principalId: String, + policyDocument: PolicyDocument + ) extends ApiGatewayCustomAuthorizerEventResult { + override def productPrefix = "ApiGatewayCustomAuthorizerEventResult" + } +} + +sealed abstract class PolicyDocument { + def version: LocalDate + def statement: List[Statement] +} + +object PolicyDocument { + def apply(version: LocalDate, statement: List[Statement]): PolicyDocument = + Impl(version, statement) + + implicit def encoder: Encoder[PolicyDocument] = Encoder.forProduct2( + "Version", + "Statement" + )(r => (r.version, r.statement)) + + private case class Impl( + version: LocalDate, + statement: List[Statement] + ) extends PolicyDocument { + override def productPrefix = "PolicyDocument" + } +} + +sealed abstract class Statement { + def action: String + def effect: String + def resource: String +} + +object Statement { + def apply(action: String, effect: String, resource: String): Statement = + Impl(action, effect, resource) + + implicit def encoder: Encoder[Statement] = Encoder.forProduct3( + "Action", + "Effect", + "Resource" + )(r => (r.action, r.effect, r.resource)) + + private case class Impl( + action: String, + effect: String, + resource: String + ) extends Statement { + override def productPrefix = "Statement" + } +} diff --git a/lambda/shared/src/main/scala/feral/lambda/events/codecs.scala b/lambda/shared/src/main/scala/feral/lambda/events/codecs.scala index 44713caf..6b64dad2 100644 --- a/lambda/shared/src/main/scala/feral/lambda/events/codecs.scala +++ b/lambda/shared/src/main/scala/feral/lambda/events/codecs.scala @@ -19,11 +19,13 @@ package feral.lambda.events import com.comcast.ip4s.Hostname import com.comcast.ip4s.IpAddress import io.circe.Decoder +import io.circe.Encoder import io.circe.KeyDecoder import io.circe.KeyEncoder import org.typelevel.ci.CIString import java.time.Instant +import java.time.format.DateTimeFormatter import scala.util.Try private object codecs { @@ -38,6 +40,12 @@ private object codecs { } } + implicit def encodeDate: Encoder[java.time.LocalDate] = + Encoder.encodeString.contramap { str => + val dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") + str.format(dateFormatter) + } + implicit def decodeIpAddress: Decoder[IpAddress] = Decoder.decodeString.emap(IpAddress.fromString(_).toRight("Cannot parse IP address")) diff --git a/lambda/shared/src/test/scala/feral/lambda/events/ApiGatewayCustomAuthorizerEventResultSuite.scala b/lambda/shared/src/test/scala/feral/lambda/events/ApiGatewayCustomAuthorizerEventResultSuite.scala new file mode 100644 index 00000000..6ce2861f --- /dev/null +++ b/lambda/shared/src/test/scala/feral/lambda/events/ApiGatewayCustomAuthorizerEventResultSuite.scala @@ -0,0 +1,63 @@ +/* + * Copyright 2021 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package feral.lambda.events + +import io.circe.literal._ +import io.circe.syntax._ +import munit.FunSuite + +class ApiGatewayCustomAuthorizerEventResultSuite extends FunSuite { + + import ApiGatewayCustomAuthorizerEventResultSuite._ + val policyDocument = PolicyDocument( + version = java.time.LocalDate.parse("2012-10-17"), + statement = List( + Statement( + action = "execute-Api:Invoke", + effect = "Allow", + resource = "arn:aws:execute-Api:us-east-1:677276103099:or71kuogm2/test/GET/" + ) + ) + ) + + val result = ApiGatewayCustomAuthorizerEventResult( + principalId = "me", + policyDocument = policyDocument + ) + + test("encoder") { + assertEquals(event, result.asJson) + } +} + +object ApiGatewayCustomAuthorizerEventResultSuite { + def event = json""" + { + "principalId": "me", + "policyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": "execute-Api:Invoke", + "Effect": "Allow", + "Resource": "arn:aws:execute-Api:us-east-1:677276103099:or71kuogm2/test/GET/" + } + ] + } + } + """ +} diff --git a/lambda/shared/src/test/scala/feral/lambda/events/ApiGatewayCustomAuthorizerEventSuite.scala b/lambda/shared/src/test/scala/feral/lambda/events/ApiGatewayCustomAuthorizerEventSuite.scala new file mode 100644 index 00000000..4103aa7c --- /dev/null +++ b/lambda/shared/src/test/scala/feral/lambda/events/ApiGatewayCustomAuthorizerEventSuite.scala @@ -0,0 +1,122 @@ +/* + * Copyright 2021 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package feral.lambda.events + +import feral.lambda.events.ApiGatewayCustomAuthorizerEvent +import io.circe.literal._ +import munit.FunSuite + +class ApiGatewayCustomAuthorizerEventSuite extends FunSuite { + + import ApiGatewayCustomAuthorizerEventSuite._ + + test("decoder") { + event.as[ApiGatewayCustomAuthorizerEvent].toTry.get + } +} + +object ApiGatewayCustomAuthorizerEventSuite { + def event = json""" + { + "type": "REQUEST", + "methodArn": "arn:aws:execute-api:us-east-1:677276103099:or71kuogm2/test/GET/", + "resource": "/", + "path": "/", + "httpMethod": "ANY", + "headers": { + "accept": "*/*", + "headerauth1": "headerValue1", + "Host": "or71kuogm2.execute-api.us-east-1.amazonaws.com", + "user-agent": "curl/7.81.0", + "X-Amzn-Trace-Id": "Root=1-69b199a4-2c09a07109124da13b8ff7b0", + "X-Forwarded-For": "102.176.65.68", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "multiValueHeaders": { + "accept": [ + "*/*" + ], + "headerauth1": [ + "headerValue1" + ], + "Host": [ + "or71kuogm2.execute-api.us-east-1.amazonaws.com" + ], + "user-agent": [ + "curl/7.81.0" + ], + "X-Amzn-Trace-Id": [ + "Root=1-69b199a4-2c09a07109124da13b8ff7b0" + ], + "X-Forwarded-For": [ + "102.176.65.68" + ], + "X-Forwarded-Port": [ + "443" + ], + "X-Forwarded-Proto": [ + "https" + ] + }, + "queryStringParameters": { + "QueryString1": "queryValue1" + }, + "multiValueQueryStringParameters": { + "QueryString1": [ + "queryValue1" + ] + }, + "pathParameters": {}, + "stageVariables": { + "StageVar1": "stageValue1" + }, + "requestContext": { + "resourceId": "8qkclnaedk", + "resourcePath": "/", + "httpMethod": "GET", + "extendedRequestId": "aETx1HJcIAMEGdw=", + "requestTime": "11/Mar/2026:16:34:44 +0000", + "path": "/test", + "accountId": "677276103099", + "protocol": "HTTP/1.1", + "stage": "test", + "domainPrefix": "or71kuogm2", + "requestTimeEpoch": 1773246884924, + "requestId": "b20e5103-7f01-4bef-9fa2-682889187b5b", + "identity": { + "cognitoIdentityPoolId": null, + "accountId": null, + "cognitoIdentityId": null, + "caller": null, + "sourceIp": "102.176.65.68", + "principalOrgId": null, + "accessKey": null, + "cognitoAuthenticationType": null, + "cognitoAuthenticationProvider": null, + "userArn": null, + "userAgent": "curl/7.81.0", + "user": null + }, + "domainName": "or71kuogm2.execute-api.us-east-1.amazonaws.com", + "deploymentId": "ltoy3m", + "apiId": "or71kuogm2" + } + } + + """ +}