wip middleware

This commit is contained in:
2025-03-27 19:25:32 -03:00
parent 76c2656dd1
commit 5af61465f3
16 changed files with 422 additions and 52 deletions

View File

@@ -1,3 +1,4 @@
import os
from typing import Any
from aws_lambda_powertools import Logger, Tracer
@@ -13,9 +14,11 @@ from aws_lambda_powertools.utilities.typing import LambdaContext
from middlewares import AuthorizerMiddleware
from routes import courses, enrollments, lookup, me, orders, users, webhooks
DEBUG = os.getenv('LOG_LEVEL') == 'DEBUG'
tracer = Tracer()
logger = Logger(__name__)
app = APIGatewayHttpResolver(enable_validation=True)
app = APIGatewayHttpResolver(enable_validation=True, debug=DEBUG)
app.use(middlewares=[AuthorizerMiddleware()])
app.include_router(courses.router, prefix='/courses')
app.include_router(enrollments.router, prefix='/enrollments')

View File

@@ -80,6 +80,19 @@ class Authorizer:
def _authorizer(bearer: BearerToken) -> Authorizer:
"""
Build an Authorizer object based on the bearer token's auth type.
Parameters
----------
bearer : BearerToken
The bearer token containing authentication information.
Returns
-------
Authorizer
An Authorizer object with the appropriate authorization status and context.
"""
try:
match bearer.auth_type:
case TokenType.USER_TOKEN:

View File

@@ -4,6 +4,7 @@ import boto3
DYNAMODB_ENDPOINT_URL: str | None = None
# Only when running `sam local start-api`
if 'AWS_SAM_LOCAL' in os.environ:
DYNAMODB_ENDPOINT_URL = 'http://host.docker.internal:8000'

View File

@@ -6,6 +6,8 @@ from aws_lambda_powertools.event_handler.middlewares import (
BaseMiddlewareHandler,
NextMiddleware,
)
from layercake.dateutils import now, ttl
from layercake.dynamodb import ComposeKey, DynamoDBCollection, KeyPair
from pydantic import UUID4, BaseModel, Field
@@ -21,7 +23,7 @@ class AuthorizerMiddleware(BaseMiddlewareHandler):
if 'user' in authorizer:
user = authorizer['user']
app.append_context(user=AuthenticatedUser(**user))
app.append_context(authenticated_user=AuthenticatedUser(**user))
return next_middleware(app)
@@ -32,3 +34,35 @@ class AuthenticatedUser(BaseModel):
email: str
email_verified: bool
sub: UUID4
class AuditLogMiddleware(BaseMiddlewareHandler):
def __init__(self, action: str, /, collect: DynamoDBCollection) -> None:
self.action = action
self.collect = collect
def handler(
self,
app: APIGatewayHttpResolver,
next_middleware: NextMiddleware,
) -> Response:
response = next_middleware(app)
auth_user = app.context.get('authenticated_user')
request_ctx = app.current_event.request_context
ip_addr = request_ctx.http.source_ip
# Successful request
if 200 <= response.status_code < 300 and auth_user:
now_ = now()
self.collect.put_item(
key=KeyPair(
pk=ComposeKey(auth_user.id, prefix='logs'),
sk=now_.isoformat(),
),
action=self.action,
ip=ip_addr,
ttl=ttl(start_dt=now_, days=365 * 2),
)
return response

View File

@@ -4,18 +4,21 @@ from http import HTTPStatus
from aws_lambda_powertools.event_handler import Response, content_types
from aws_lambda_powertools.event_handler.api_gateway import Router
from elasticsearch import Elasticsearch
from layercake.dynamodb import DynamoDBPersistenceLayer
from layercake.dynamodb import DynamoDBCollection, DynamoDBPersistenceLayer
from pydantic import BaseModel
import elastic
from boto3clients import dynamodb_client
from course import create_course
from middlewares import AuditLogMiddleware
from models import Course, Org
from settings import COURSE_TABLE, ELASTIC_CONN
from settings import COURSE_TABLE, ELASTIC_CONN, USER_TABLE
router = Router()
elastic_client = Elasticsearch(**ELASTIC_CONN)
course_layer = DynamoDBPersistenceLayer(COURSE_TABLE, dynamodb_client)
user_layer = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client)
collect = DynamoDBCollection(user_layer)
@router.get(
@@ -41,7 +44,12 @@ class CoursePayload(BaseModel):
course: Course
@router.post('/', compress=True, tags=['Course'])
@router.post(
'/',
compress=True,
tags=['Course'],
middlewares=[AuditLogMiddleware('COURSE_ADD', collect)],
)
def post_course(payload: CoursePayload):
create_course(
course=payload.course,

View File

@@ -10,7 +10,9 @@ KONVIVA_SECRET_KEY: str = os.getenv('KONVIVA_SECRET_KEY') # type: ignore
match os.getenv('AWS_SAM_LOCAL'), os.getenv('ELASTIC_HOSTS'):
case str() as AWS_SAM_LOCAL, _ if AWS_SAM_LOCAL:
case str() as AWS_SAM_LOCAL, _ if (
AWS_SAM_LOCAL
): # Only when running `sam local start-api`
ELASTIC_CONN = {
'hosts': 'http://host.docker.internal:9200',
}

View File

@@ -23,7 +23,7 @@ Globals:
Architectures:
- x86_64
Layers:
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:22
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:25
Environment:
Variables:
TZ: America/Sao_Paulo

View File

@@ -3,22 +3,26 @@ from http import HTTPMethod, HTTPStatus
from layercake.dynamodb import DynamoDBPersistenceLayer
import app
from ..conftest import HttpApiProxy, LambdaContext
def test_post_course(
mock_app,
monkeypatch,
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
http_api_proxy: HttpApiProxy,
lambda_context: LambdaContext,
):
mock_app.courses.course_layer = dynamodb_persistence_layer
app.courses.course_layer = dynamodb_persistence_layer
app.courses.user_layer = dynamodb_persistence_layer
r = mock_app.lambda_handler(
r = app.lambda_handler(
http_api_proxy(
raw_path='/courses',
method=HTTPMethod.POST,
headers={'Tenant': '*'},
headers={'X-Tenant': '*'},
body={
'course': {
'name': 'pytest',
@@ -32,7 +36,7 @@ def test_post_course(
lambda_context,
)
print(r)
# print(r)
assert 'id' in json.loads(r['body'])
assert r['statusCode'] == HTTPStatus.CREATED

3
http-api/uv.lock generated
View File

@@ -444,7 +444,7 @@ wheels = [
[[package]]
name = "layercake"
version = "0.1.5"
version = "0.1.8"
source = { directory = "../layercake" }
dependencies = [
{ name = "aws-lambda-powertools", extra = ["all"] },
@@ -481,6 +481,7 @@ requires-dist = [
[package.metadata.requires-dev]
dev = [
{ name = "mkdocstrings", extras = ["python"], specifier = ">=0.29.0" },
{ name = "pytest", specifier = ">=8.3.5" },
{ name = "pytest-cov", specifier = ">=6.0.0" },
{ name = "pytest-env", specifier = ">=1.1.5" },