add auth middleware

This commit is contained in:
2025-03-25 15:07:49 -03:00
parent 317c79cee2
commit cd6fdd58ad
12 changed files with 176 additions and 36 deletions

View File

@@ -1,6 +1,6 @@
# If UV does not load this file, try running `export UV_ENV_FILE=.env` # If UV does not load this file, try running `export UV_ENV_FILE=.env`
# See more details at https://docs.astral.sh/uv/configuration/files/#env # See more details at https://docs.astral.sh/uv/configuration/files/#env
ELASTIC_HOSTS=http://host.docker.internal:9200 ELASTIC_HOSTS=http://localhost:9200
DYNAMODB_PARTITION_KEY=id DYNAMODB_PARTITION_KEY=id
DYNAMODB_SORT_KEY=sk DYNAMODB_SORT_KEY=sk

View File

@@ -8,18 +8,20 @@ from aws_lambda_powertools.event_handler.exceptions import ServiceError
from aws_lambda_powertools.logging import correlation_paths from aws_lambda_powertools.logging import correlation_paths
from aws_lambda_powertools.utilities.typing import LambdaContext from aws_lambda_powertools.utilities.typing import LambdaContext
from middlewares import CorrelationIdMiddleware from middlewares import AuthorizerMiddleware
from routes import courses, enrollments, orders, users, webhooks from routes import courses, enrollments, lookup, orders, settings, users, webhooks
tracer = Tracer() tracer = Tracer()
logger = Logger(__name__) logger = Logger(__name__)
app = APIGatewayHttpResolver(enable_validation=True) app = APIGatewayHttpResolver(enable_validation=True)
app.use(middlewares=[CorrelationIdMiddleware('workspace')]) app.use(middlewares=[AuthorizerMiddleware()])
app.include_router(users.router, prefix='/users') app.include_router(courses.router, prefix='/courses')
app.include_router(enrollments.router, prefix='/enrollments') app.include_router(enrollments.router, prefix='/enrollments')
app.include_router(orders.router, prefix='/orders') app.include_router(orders.router, prefix='/orders')
app.include_router(courses.router, prefix='/courses') app.include_router(users.router, prefix='/users')
app.include_router(webhooks.router, prefix='/webhooks') app.include_router(webhooks.router, prefix='/webhooks')
app.include_router(settings.router, prefix='/settings')
app.include_router(lookup.router, prefix='/lookup')
@app.exception_handler(ServiceError) @app.exception_handler(ServiceError)

View File

@@ -1,3 +1,28 @@
"""
Example
-------
Resources:
HttpApi:
Type: AWS::Serverless::HttpApi
Properties:
Auth:
DefaultAuthorizer: LambdaRequestAuthorizer
Authorizers:
LambdaRequestAuthorizer:
FunctionArn: !GetAtt Authorizer.Arn
AuthorizerPayloadFormatVersion: "2.0"
EnableFunctionDefaultPermissions: true
EnableSimpleResponses: true
Identity:
Headers: [Authorization]
Authorizer:
Type: AWS::Serverless::Function
Properties:
Handler: auth.lambda_handler
"""
from dataclasses import dataclass from dataclasses import dataclass
import boto3 import boto3

View File

@@ -6,24 +6,29 @@ from aws_lambda_powertools.event_handler.middlewares import (
BaseMiddlewareHandler, BaseMiddlewareHandler,
NextMiddleware, NextMiddleware,
) )
from pydantic import UUID4, BaseModel, Field
class CorrelationIdMiddleware(BaseMiddlewareHandler): class AuthorizerMiddleware(BaseMiddlewareHandler):
def __init__(self, header: str):
super().__init__()
self.header = header
def handler( def handler(
self, app: APIGatewayHttpResolver, next_middleware: NextMiddleware self,
app: APIGatewayHttpResolver,
next_middleware: NextMiddleware,
) -> Response: ) -> Response:
# BEFORE logic # Gets the Lambda authorizer associated with the current API Gateway event.
request_id = app.current_event.request_context.request_id # You can check the file `auth.py` for more details.
correlation_id = app.current_event.headers.get(self.header, request_id) authorizer = app.current_event.request_context.authorizer.get_lambda
# Call next middleware or route handler ('/todos') if 'user' in authorizer:
response = next_middleware(app) user = authorizer['user']
app.append_context(user=AuthenticatedUser(**user))
# AFTER logic return next_middleware(app)
response.headers[self.header] = correlation_id
return response
class AuthenticatedUser(BaseModel):
id: str = Field(alias='custom:user_id')
name: str
email: str
email_verified: bool
sub: UUID4

View File

@@ -0,0 +1,32 @@
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 elasticsearch_dsl import Search
from layercake.funcs import pick
from settings import ELASTIC_CONN, USER_TABLE
router = Router()
elastic_client = Elasticsearch(**ELASTIC_CONN)
@router.get('/<username>', include_in_schema=False)
def lookup(username: str):
s = Search(using=elastic_client, index=USER_TABLE).query(
'bool',
should=[
{'term': {'email.keyword': username}},
{'term': {'cpf.keyword': username}},
],
minimum_should_match=1,
)
for hit in s.execute():
return pick(('id', 'name', 'email', 'cognito:sub'), hit.to_dict())
return Response(
content_type=content_types.APPLICATION_JSON,
status_code=HTTPStatus.NOT_FOUND,
)

View File

@@ -0,0 +1,11 @@
from aws_lambda_powertools.event_handler.api_gateway import Router
router = Router()
@router.get('/')
def settings():
user = router.context['user']
print(user.email_verified)
return {}

View File

@@ -5,12 +5,20 @@ ORDER_TABLE: str = os.getenv('ORDER_TABLE') # type: ignore
ENROLLMENT_TABLE: str = os.getenv('ENROLLMENT_TABLE') # type: ignore ENROLLMENT_TABLE: str = os.getenv('ENROLLMENT_TABLE') # type: ignore
COURSE_TABLE: str = os.getenv('COURSE_TABLE') # type: ignore COURSE_TABLE: str = os.getenv('COURSE_TABLE') # type: ignore
ELASTIC_CLOUD_ID = os.getenv('ELASTIC_CLOUD_ID')
ELASTIC_AUTH_PASS = os.getenv('ELASTIC_AUTH_PASS')
if {'AWS_SAM_LOCAL', 'ELASTIC_HOSTS'}.intersection(os.environ): match (os.getenv('AWS_SAM_LOCAL'), os.getenv('ELASTIC_HOSTS')):
ELASTIC_CONN = {'hosts': 'http://host.docker.internal:9200'} case (str() as AWS_SAM_LOCAL, _) if AWS_SAM_LOCAL:
else: ELASTIC_CONN = {
'hosts': 'http://host.docker.internal:9200',
}
case (_, str() as ELASTIC_HOSTS) if ELASTIC_HOSTS:
ELASTIC_CONN = {
'hosts': ELASTIC_HOSTS,
}
case _:
ELASTIC_CLOUD_ID = os.getenv('ELASTIC_CLOUD_ID')
ELASTIC_AUTH_PASS = os.getenv('ELASTIC_AUTH_PASS')
ELASTIC_CONN = { ELASTIC_CONN = {
'cloud_id': ELASTIC_CLOUD_ID, 'cloud_id': ELASTIC_CLOUD_ID,
'basic_auth': ('elastic', ELASTIC_AUTH_PASS), 'basic_auth': ('elastic', ELASTIC_AUTH_PASS),

View File

@@ -60,8 +60,7 @@ Resources:
EnableFunctionDefaultPermissions: true EnableFunctionDefaultPermissions: true
EnableSimpleResponses: true EnableSimpleResponses: true
Identity: Identity:
Headers: Headers: [Authorization]
- Authorization
HttpApiFunction: HttpApiFunction:
Type: AWS::Serverless::Function Type: AWS::Serverless::Function
@@ -81,10 +80,10 @@ Resources:
Path: /{proxy+} Path: /{proxy+}
Method: ANY Method: ANY
ApiId: !Ref HttpApi ApiId: !Ref HttpApi
Swagger: Lookup:
Type: HttpApi Type: HttpApi
Properties: Properties:
Path: /_swagger Path: /lookup/{username}
Method: GET Method: GET
ApiId: !Ref HttpApi ApiId: !Ref HttpApi
Auth: Auth:

View File

@@ -49,8 +49,13 @@ class HttpApiProxy:
'apiId': 'api-id', 'apiId': 'api-id',
'authorizer': { 'authorizer': {
'lambda': { 'lambda': {
'user': {}, 'user': {
'tenant': '*', 'name': 'Sérgio R Siqueira',
'email': 'sergio@somosbeta.com.br',
'email_verified': 'true',
'custom:user_id': '5OxmMjL-ujoR5IMGegQz',
'sub': 'c4f30dbd-083e-4b84-aa50-c31afe9b9c01',
},
}, },
'jwt': { 'jwt': {
'claims': {'claim1': 'value1', 'claim2': 'value2'}, 'claims': {'claim1': 'value1', 'claim2': 'value2'},

View File

@@ -0,0 +1,26 @@
import json
from http import HTTPMethod, HTTPStatus
from layercake.dynamodb import DynamoDBPersistenceLayer
from ..conftest import HttpApiProxy, LambdaContext
def test_lookup(
mock_app,
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
http_api_proxy: HttpApiProxy,
lambda_context: LambdaContext,
):
mock_app.courses.course_layer = dynamodb_persistence_layer
r = mock_app.lambda_handler(
http_api_proxy(
raw_path='/lookup/sergio@somosbeta.com.br',
method=HTTPMethod.GET,
),
lambda_context,
)
assert 'id' in json.loads(r['body'])
assert r['statusCode'] == HTTPStatus.OK

View File

@@ -0,0 +1,24 @@
from http import HTTPMethod, HTTPStatus
from layercake.dynamodb import DynamoDBPersistenceLayer
from ..conftest import HttpApiProxy, LambdaContext
def test_settings(
mock_app,
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
http_api_proxy: HttpApiProxy,
lambda_context: LambdaContext,
):
r = mock_app.lambda_handler(
http_api_proxy(
raw_path='/settings',
method=HTTPMethod.GET,
),
lambda_context,
)
print(r)
# assert 'id' in json.loads(r['body'])
assert r['statusCode'] == HTTPStatus.OK

View File

@@ -1,5 +1,8 @@
{"id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "sk": {"S": "0"}, "update_date": {"S": "2024-02-08T16:42:33.776409-03:00"}, "create_date": {"S": "2019-03-25T00:00:00-03:00"}, "email_verified": {"BOOL": true}, "cognito:sub": {"S": "58efed8d-d276-41a8-8502-4ab8b5a6415e"}, "cpf": {"S": "07879819908"}, "email": {"S": "sergio@somosbeta.com.br"}, "name": {"S": "S\u00e9rgio Rafael de Siqueira"}, "last_login": {"S": "2024-02-08T20:53:45.818126-03:00"}, "tenant:org_id": {"L": [{"S": "cJtK9SsnJhKPyxESe7g3DG"}, {"S": "edp8njvgQuzNkLx2ySNfAD"}, {"S": "8TVSi5oACLxTiT8ycKPmaQ"}]}} {"id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "sk": {"S": "0"}, "update_date": {"S": "2024-02-08T16:42:33.776409-03:00"}, "create_date": {"S": "2019-03-25T00:00:00-03:00"}, "email_verified": {"BOOL": true}, "cognito:sub": {"S": "58efed8d-d276-41a8-8502-4ab8b5a6415e"}, "cpf": {"S": "07879819908"}, "email": {"S": "sergio@somosbeta.com.br"}, "name": {"S": "S\u00e9rgio Rafael de Siqueira"}, "last_login": {"S": "2024-02-08T20:53:45.818126-03:00"}, "tenant:org_id": {"L": [{"S": "cJtK9SsnJhKPyxESe7g3DG"}]}}
{"id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "sk": {"S": "cognito"}, "create_date": {"S": "2025-03-03T17:12:26.443507-03:00"}, "sub": {"S": "58efed8d-d276-41a8-8502-4ab8b5a6415e"}}
{"id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "sk": {"S": "emails#sergio@somosbeta.com.br"}, "email_verified": {"BOOL": true}, "update_date": {"S": "2024-02-08T16:42:33.776409-03:00"}, "create_date": {"S": "2019-03-25T00:00:00-03:00"}, "email_primary": {"BOOL": true}, "mx_record_exists": {"BOOL": true}, "update_date": {"S": "2023-11-09T12:13:04.308986-03:00"}} {"id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "sk": {"S": "emails#sergio@somosbeta.com.br"}, "email_verified": {"BOOL": true}, "update_date": {"S": "2024-02-08T16:42:33.776409-03:00"}, "create_date": {"S": "2019-03-25T00:00:00-03:00"}, "email_primary": {"BOOL": true}, "mx_record_exists": {"BOOL": true}, "update_date": {"S": "2023-11-09T12:13:04.308986-03:00"}}
{"id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "sk": {"S": "acls#*"}, "create_date": {"S": "2022-06-13T15:00:24.309410-03:00"}, "roles": {"L": [{"S": "ADMIN"}]}} {"id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "sk": {"S": "acls#*"}, "create_date": {"S": "2022-06-13T15:00:24.309410-03:00"}, "roles": {"L": [{"S": "ADMIN"}]}}
{"id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "sk": {"S": "acls#cJtK9SsnJhKPyxESe7g3DG"}, "create_date": {"S": "2025-03-14T10:06:34.628078-03:00"}, "roles": {"L": [{"S": "ADMIN"}]}}
{"id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "sk": {"S": "orgs#cJtK9SsnJhKPyxESe7g3DG"}, "cnpj": {"S": "15608435000190"}, "create_date": {"S": "2025-03-13T16:36:50.073156-03:00"}, "name": {"S": "Beta Educação"}}
{"id": {"S": "logs#5OxmMjL-ujoR5IMGegQz"}, "sk": {"S": "2024-02-08T16:42:33.776409-03:00"}, "action": {"S": "OPEN_EMAIL"}} {"id": {"S": "logs#5OxmMjL-ujoR5IMGegQz"}, "sk": {"S": "2024-02-08T16:42:33.776409-03:00"}, "action": {"S": "OPEN_EMAIL"}}
{"id": {"S": "logs#5OxmMjL-ujoR5IMGegQz"}, "sk": {"S": "2019-03-25T00:00:00-03:00"}, "action": {"S": "CLICK_EMAIL"}} {"id": {"S": "logs#5OxmMjL-ujoR5IMGegQz"}, "sk": {"S": "2019-03-25T00:00:00-03:00"}, "action": {"S": "CLICK_EMAIL"}}