Skip to content

v1.20.0

Compare
Choose a tag to compare
@release-drafter release-drafter released this 21 Aug 14:17
· 2788 commits to develop since this release
27e3930

Summary

This release highlights 1/ support for Python 3.9, 2/ support for API Gateway and AppSync Lambda Authorizers, 3/ support for API Gateway Custom Domain Mappings, 4/ support to make any Python synchronous function idempotent, and a number of documentation improvements & bugfixes.

Lambda Authorizer support

AppSync

This release adds Data Class support for AppSyncAuthorizerEvent, AppSyncAuthorizerResponse, and correlation ID in Logger.

You can use AppSyncAuthorizerEvent to easily access all self-documented properties, and AppSyncAuthorizerResponse to serialize the response in the expected format.

You can read more in the announcement blog post for more details

from typing import Dict

from aws_lambda_powertools.logging import correlation_paths
from aws_lambda_powertools.logging.logger import Logger
from aws_lambda_powertools.utilities.data_classes.appsync_authorizer_event import (
    AppSyncAuthorizerEvent,
    AppSyncAuthorizerResponse,
)
from aws_lambda_powertools.utilities.data_classes.event_source import event_source

logger = Logger()


def get_user_by_token(token: str):
    """Look a user by token"""


@logger.inject_lambda_context(correlation_id_path=correlation_paths.APPSYNC_AUTHORIZER)
@event_source(data_class=AppSyncAuthorizerEvent)
def lambda_handler(event: AppSyncAuthorizerEvent, context) -> Dict:
    user = get_user_by_token(event.authorization_token)

    if not user:
        # No user found, return not authorized
        return AppSyncAuthorizerResponse().to_dict()

    return AppSyncAuthorizerResponse(
        authorize=True,
        resolver_context={"id": user.id},
        # Only allow admins to delete events
        deny_fields=None if user.is_admin else ["Mutation.deleteEvent"],
    ).asdict()

API Gateway

This release adds support for both Lambda Authorizer for payload v1 - APIGatewayAuthorizerRequestEvent, APIGatewayAuthorizerResponse - and v2 formats APIGatewayAuthorizerEventV2, APIGatewayAuthorizerResponseV2.

Similar to AppSync, you can use APIGatewayAuthorizerRequestEvent and APIGatewayAuthorizerEventV2 to easily access all self-documented properties available, and its corresponding APIGatewayAuthorizerResponse and APIGatewayAuthorizerResponseV2 to serialize the response in the expected format.

You can read more in the announcement blog post for more details

v2 format

from aws_lambda_powertools.utilities.data_classes import event_source
from aws_lambda_powertools.utilities.data_classes.api_gateway_authorizer_event import (
    APIGatewayAuthorizerEventV2,
    APIGatewayAuthorizerResponseV2,
)
from secrets import compare_digest


def get_user_by_token(token):
    if compare_digest(token, "Foo"):
        return {"name": "Foo"}
    return None


@event_source(data_class=APIGatewayAuthorizerEventV2)
def handler(event: APIGatewayAuthorizerEventV2, context):
    user = get_user_by_token(event.get_header_value("x-token"))

    if user is None:
        # No user was found, so we return not authorized
        return APIGatewayAuthorizerResponseV2().asdict()

    # Found the user and setting the details in the context
    return APIGatewayAuthorizerResponseV2(authorize=True, context=user).asdict()

v1 format

from aws_lambda_powertools.utilities.data_classes import event_source
from aws_lambda_powertools.utilities.data_classes.api_gateway_authorizer_event import (
    APIGatewayAuthorizerRequestEvent,
    APIGatewayAuthorizerResponse,
    HttpVerb,
)
from secrets import compare_digest


def get_user_by_token(token):
    if compare_digest(token, "admin-foo"):
        return {"isAdmin": True, "name": "Admin"}
    elif compare_digest(token, "regular-foo"):
        return {"name": "Joe"}
    else:
        return None


@event_source(data_class=APIGatewayAuthorizerRequestEvent)
def handler(event: APIGatewayAuthorizerRequestEvent, context):
    user = get_user_by_token(event.get_header_value("Authorization"))

    # parse the `methodArn` as an `APIGatewayRouteArn`
    arn = event.parsed_arn
    # Create the response builder from parts of the `methodArn`
    policy = APIGatewayAuthorizerResponse(
        principal_id="user",
        region=arn.region,
        aws_account_id=arn.aws_account_id,
        api_id=arn.api_id,
        stage=arn.stage
    )

    if user is None:
        # No user was found, so we return not authorized
        policy.deny_all_routes()
        return policy.asdict()

    # Found the user and setting the details in the context
    policy.context = user

    # Conditional IAM Policy
    if user.get("isAdmin", False):
        policy.allow_all_routes()
    else:
        policy.allow_route(HttpVerb.GET, "/user-profile")

    return policy.asdict()

Custom Domain API Mappings

When using Custom Domain API Mappings feature, you must use the new strip_prefixes param in the ApiGatewayResolver constructor.

Scenario: You have a custom domain api.mydomain.dev and set an API Mapping payment to forward requests to your Payments API, the path argument will be /payment/<your_actual_path>.

This will lead to a HTTP 404 despite having your Lambda configured correctly. See the example below on how to account for this change.

from aws_lambda_powertools import Logger, Tracer
from aws_lambda_powertools.logging import correlation_paths
from aws_lambda_powertools.event_handler.api_gateway import ApiGatewayResolver

tracer = Tracer()
logger = Logger()
app = ApiGatewayResolver(strip_prefixes=["/payment"])

@app.get("/subscriptions/<subscription>")
@tracer.capture_method
def get_subscription(subscription):
    return {"subscription_id": subscription}

@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST)
@tracer.capture_lambda_handler
def lambda_handler(event, context):
    return app.resolve(event, context)

Make any Python function idempotent

Previously, you could only make the entire Lambda function handler idempotent. You can now make any Python function idempotent with the new idempotent_function.

This also enables easy integration with any other utility in Powertools. Take example the Batch utility, where you wouldn't want to make the entire Lambda handler idempotent as the batch will vary, instead you'd want to make sure you can process a given message only once.

As a trade-off to allow any Python function with an arbitrary number of parameters, you must call your function with a keyword argument, and you tell us upfront which one that might be using data_keyword_argument, so we can apply all operations like hashing the idempotency token, payload extraction, parameter validation, etc.

import uuid

from aws_lambda_powertools.utilities.batch import sqs_batch_processor
from aws_lambda_powertools.utilities.idempotency import idempotent_function, DynamoDBPersistenceLayer, IdempotencyConfig


dynamodb = DynamoDBPersistenceLayer(table_name="idem")
config =  IdempotencyConfig(
    event_key_jmespath="messageId",  # see "Choosing a payload subset for idempotency" docs section
    use_local_cache=True,
)

@idempotent_function(data_keyword_argument="data", config=config, persistence_store=dynamodb)
def dummy(arg_one, arg_two, data: dict, **kwargs):
    return {"data": data}


@idempotent_function(data_keyword_argument="record", config=config, persistence_store=dynamodb)
def record_handler(record):
    return {"message": record["body"]}


@sqs_batch_processor(record_handler=record_handler)
def lambda_handler(event, context):
    # `data` parameter must be called as a keyword argument to work
    dummy("hello", "universe", data="test")
    return {"statusCode": 200}

Changes

🌟New features and non-breaking changes

📜 Documentation updates

🐛 Bug and hot fixes

🔧 Maintenance

This release was made possible by the following contributors:

@dependabot, @dependabot[bot], @heitorlessa, @hjurong and @michaelbrewer