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`
|
# 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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
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,13 +5,21 @@ 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 = {
|
||||||
ELASTIC_CONN = {
|
'hosts': 'http://host.docker.internal:9200',
|
||||||
'cloud_id': ELASTIC_CLOUD_ID,
|
}
|
||||||
'basic_auth': ('elastic', ELASTIC_AUTH_PASS),
|
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
|
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:
|
||||||
|
|||||||
@@ -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'},
|
||||||
|
|||||||
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": "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"}}
|
||||||
|
|||||||
Reference in New Issue
Block a user