add auth middleware
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# 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
|
||||
|
||||
ELASTIC_HOSTS=http://host.docker.internal:9200
|
||||
ELASTIC_HOSTS=http://localhost:9200
|
||||
DYNAMODB_PARTITION_KEY=id
|
||||
DYNAMODB_SORT_KEY=sk
|
||||
|
||||
@@ -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.utilities.typing import LambdaContext
|
||||
|
||||
from middlewares import CorrelationIdMiddleware
|
||||
from routes import courses, enrollments, orders, users, webhooks
|
||||
from middlewares import AuthorizerMiddleware
|
||||
from routes import courses, enrollments, lookup, orders, settings, users, webhooks
|
||||
|
||||
tracer = Tracer()
|
||||
logger = Logger(__name__)
|
||||
app = APIGatewayHttpResolver(enable_validation=True)
|
||||
app.use(middlewares=[CorrelationIdMiddleware('workspace')])
|
||||
app.include_router(users.router, prefix='/users')
|
||||
app.use(middlewares=[AuthorizerMiddleware()])
|
||||
app.include_router(courses.router, prefix='/courses')
|
||||
app.include_router(enrollments.router, prefix='/enrollments')
|
||||
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(settings.router, prefix='/settings')
|
||||
app.include_router(lookup.router, prefix='/lookup')
|
||||
|
||||
|
||||
@app.exception_handler(ServiceError)
|
||||
|
||||
@@ -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
|
||||
|
||||
import boto3
|
||||
|
||||
@@ -6,24 +6,29 @@ from aws_lambda_powertools.event_handler.middlewares import (
|
||||
BaseMiddlewareHandler,
|
||||
NextMiddleware,
|
||||
)
|
||||
from pydantic import UUID4, BaseModel, Field
|
||||
|
||||
|
||||
class CorrelationIdMiddleware(BaseMiddlewareHandler):
|
||||
def __init__(self, header: str):
|
||||
super().__init__()
|
||||
self.header = header
|
||||
|
||||
class AuthorizerMiddleware(BaseMiddlewareHandler):
|
||||
def handler(
|
||||
self, app: APIGatewayHttpResolver, next_middleware: NextMiddleware
|
||||
self,
|
||||
app: APIGatewayHttpResolver,
|
||||
next_middleware: NextMiddleware,
|
||||
) -> Response:
|
||||
# BEFORE logic
|
||||
request_id = app.current_event.request_context.request_id
|
||||
correlation_id = app.current_event.headers.get(self.header, request_id)
|
||||
# Gets the Lambda authorizer associated with the current API Gateway event.
|
||||
# You can check the file `auth.py` for more details.
|
||||
authorizer = app.current_event.request_context.authorizer.get_lambda
|
||||
|
||||
# Call next middleware or route handler ('/todos')
|
||||
response = next_middleware(app)
|
||||
if 'user' in authorizer:
|
||||
user = authorizer['user']
|
||||
app.append_context(user=AuthenticatedUser(**user))
|
||||
|
||||
# AFTER logic
|
||||
response.headers[self.header] = correlation_id
|
||||
return next_middleware(app)
|
||||
|
||||
return response
|
||||
|
||||
class AuthenticatedUser(BaseModel):
|
||||
id: str = Field(alias='custom:user_id')
|
||||
name: str
|
||||
email: str
|
||||
email_verified: bool
|
||||
sub: UUID4
|
||||
|
||||
32
http-api/routes/lookup/__init__.py
Normal file
32
http-api/routes/lookup/__init__.py
Normal 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,
|
||||
)
|
||||
11
http-api/routes/settings/__init__.py
Normal file
11
http-api/routes/settings/__init__.py
Normal 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 {}
|
||||
@@ -5,12 +5,20 @@ ORDER_TABLE: str = os.getenv('ORDER_TABLE') # type: ignore
|
||||
ENROLLMENT_TABLE: str = os.getenv('ENROLLMENT_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):
|
||||
ELASTIC_CONN = {'hosts': 'http://host.docker.internal:9200'}
|
||||
else:
|
||||
match (os.getenv('AWS_SAM_LOCAL'), os.getenv('ELASTIC_HOSTS')):
|
||||
case (str() as AWS_SAM_LOCAL, _) if AWS_SAM_LOCAL:
|
||||
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 = {
|
||||
'cloud_id': ELASTIC_CLOUD_ID,
|
||||
'basic_auth': ('elastic', ELASTIC_AUTH_PASS),
|
||||
|
||||
@@ -60,8 +60,7 @@ Resources:
|
||||
EnableFunctionDefaultPermissions: true
|
||||
EnableSimpleResponses: true
|
||||
Identity:
|
||||
Headers:
|
||||
- Authorization
|
||||
Headers: [Authorization]
|
||||
|
||||
HttpApiFunction:
|
||||
Type: AWS::Serverless::Function
|
||||
@@ -81,10 +80,10 @@ Resources:
|
||||
Path: /{proxy+}
|
||||
Method: ANY
|
||||
ApiId: !Ref HttpApi
|
||||
Swagger:
|
||||
Lookup:
|
||||
Type: HttpApi
|
||||
Properties:
|
||||
Path: /_swagger
|
||||
Path: /lookup/{username}
|
||||
Method: GET
|
||||
ApiId: !Ref HttpApi
|
||||
Auth:
|
||||
|
||||
@@ -49,8 +49,13 @@ class HttpApiProxy:
|
||||
'apiId': 'api-id',
|
||||
'authorizer': {
|
||||
'lambda': {
|
||||
'user': {},
|
||||
'tenant': '*',
|
||||
'user': {
|
||||
'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': {
|
||||
'claims': {'claim1': 'value1', 'claim2': 'value2'},
|
||||
|
||||
26
http-api/tests/routes/test_lookup.py
Normal file
26
http-api/tests/routes/test_lookup.py
Normal 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
|
||||
24
http-api/tests/routes/test_settings.py
Normal file
24
http-api/tests/routes/test_settings.py
Normal 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
|
||||
@@ -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": "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": "2019-03-25T00:00:00-03:00"}, "action": {"S": "CLICK_EMAIL"}}
|
||||
|
||||
Reference in New Issue
Block a user