diff --git a/http-api/cognito.py b/http-api/cognito.py index 485bf6d..839dfba 100644 --- a/http-api/cognito.py +++ b/http-api/cognito.py @@ -1,4 +1,4 @@ -class UserNotFound(Exception): ... +class UnauthorizedError(Exception): ... def get_user(access_token: str, *, idp_client) -> dict | None: @@ -6,6 +6,6 @@ def get_user(access_token: str, *, idp_client) -> dict | None: try: user = idp_client.get_user(AccessToken=access_token) except idp_client.exceptions.ClientError: - raise UserNotFound() + raise UnauthorizedError() else: return {attr['Name']: attr['Value'] for attr in user['UserAttributes']} diff --git a/http-api/middlewares.py b/http-api/middlewares.py index dd03d1a..fcdcb7a 100644 --- a/http-api/middlewares.py +++ b/http-api/middlewares.py @@ -1,5 +1,3 @@ -import json - from aws_lambda_powertools.event_handler.api_gateway import ( APIGatewayHttpResolver, Response, @@ -8,7 +6,9 @@ from aws_lambda_powertools.event_handler.middlewares import ( BaseMiddlewareHandler, NextMiddleware, ) -from aws_lambda_powertools.shared.json_encoder import Encoder +from aws_lambda_powertools.shared.functions import ( + extract_event_from_common_models, +) from layercake.dateutils import now, ttl from layercake.dynamodb import ComposeKey, DynamoDBCollection, KeyPair from pydantic import UUID4, BaseModel, Field @@ -16,6 +16,14 @@ from pydantic import UUID4, BaseModel, Field LOG_RETENTION_DAYS = 365 * 2 # 2 years +class AuthenticatedUser(BaseModel): + id: str = Field(alias='custom:user_id') + name: str + email: str + email_verified: bool + sub: UUID4 + + class AuthorizerMiddleware(BaseMiddlewareHandler): def handler( self, @@ -33,14 +41,6 @@ class AuthorizerMiddleware(BaseMiddlewareHandler): return next_middleware(app) -class AuthenticatedUser(BaseModel): - id: str = Field(alias='custom:user_id') - name: str - email: str - email_verified: bool - sub: UUID4 - - class AuditLogMiddleware(BaseMiddlewareHandler): """This middleware logs audit details for successful requests, storing user, action, and IP info with a specified retention period..""" @@ -71,7 +71,9 @@ class AuditLogMiddleware(BaseMiddlewareHandler): if 200 <= response.status_code < 300 and user: now_ = now() data = ( - json.dumps(response.body, cls=Encoder) if response.is_json() else None + extract_event_from_common_models(response.body) + if response.is_json() + else None ) retention_days = ( ttl(start_dt=now_, days=self.retention_days) diff --git a/http-api/routes/courses/__init__.py b/http-api/routes/courses/__init__.py index 6c0f3b6..894e01a 100644 --- a/http-api/routes/courses/__init__.py +++ b/http-api/routes/courses/__init__.py @@ -53,7 +53,7 @@ def post_course(payload: Course): ) return Response( - body={'id': str(payload.id), }, + body=payload, content_type=content_types.APPLICATION_JSON, status_code=HTTPStatus.CREATED, ) diff --git a/http-api/routes/users/__init__.py b/http-api/routes/users/__init__.py index 1147b8e..7a994ee 100644 --- a/http-api/routes/users/__init__.py +++ b/http-api/routes/users/__init__.py @@ -23,6 +23,7 @@ from pydantic import UUID4, BaseModel, StringConstraints import elastic from boto3clients import dynamodb_client +from middlewares import AuditLogMiddleware from models import User from settings import ELASTIC_CONN, USER_TABLE @@ -32,7 +33,7 @@ class BadRequestError(MissingError, PowertoolsBadRequestError): ... router = Router() user_layer = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client) -collect = DynamoDBCollection(user_layer, exc_cls=BadRequestError) +collect = DynamoDBCollection(user_layer, exception_cls=BadRequestError) elastic_client = Elasticsearch(**ELASTIC_CONN) @@ -60,6 +61,7 @@ def get_users(): compress=True, tags=['User'], summary='Create user', + middlewares=[AuditLogMiddleware('USER_ADD', collect)], ) def post_user(payload: User): return Response(status_code=HTTPStatus.CREATED) diff --git a/http-api/template.yaml b/http-api/template.yaml index 87ca981..df019e1 100644 --- a/http-api/template.yaml +++ b/http-api/template.yaml @@ -23,7 +23,7 @@ Globals: Architectures: - x86_64 Layers: - - !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:25 + - !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:26 Environment: Variables: TZ: America/Sao_Paulo diff --git a/http-api/tests/conftest.py b/http-api/tests/conftest.py index ba69cbe..09ece6f 100644 --- a/http-api/tests/conftest.py +++ b/http-api/tests/conftest.py @@ -13,6 +13,11 @@ PK = os.getenv('DYNAMODB_PARTITION_KEY', 'pk') SK = os.getenv('DYNAMODB_SORT_KEY', 'sk') +patch = pytest.MonkeyPatch() +patch.setenv('USER_TABLE', PYTEST_TABLE_NAME) +patch.setenv('COURSE_TABLE', PYTEST_TABLE_NAME) + + @dataclass class LambdaContext: function_name: str = 'test' @@ -137,9 +142,6 @@ def dynamodb_seeds(dynamodb_client): @pytest.fixture def mock_app(monkeypatch): - for table_name in ['USER_TABLE', 'COURSE_TABLE']: - monkeypatch.setenv(table_name, PYTEST_TABLE_NAME) - import app return app diff --git a/http-api/tests/routes/test_users.py b/http-api/tests/routes/test_users.py index b93e1f8..dfdb4d6 100644 --- a/http-api/tests/routes/test_users.py +++ b/http-api/tests/routes/test_users.py @@ -87,6 +87,7 @@ def test_get_logs( def test_post_user( mock_app, + dynamodb_client, http_api_proxy: HttpApiProxy, lambda_context: LambdaContext, ): diff --git a/http-api/tests/test_auth.py b/http-api/tests/test_auth.py index 7b9b545..8cea1b3 100644 --- a/http-api/tests/test_auth.py +++ b/http-api/tests/test_auth.py @@ -5,6 +5,8 @@ from .conftest import LambdaContext def test_bearer_jwt(lambda_context: LambdaContext): + import auth as app + # You should mock the Cognito user to pass the test app.get_user = lambda *args, **kwargs: { 'sub': '58efed8d-d276-41a8-8502-4ab8b5a6415e', @@ -30,6 +32,7 @@ def test_bearer_jwt(lambda_context: LambdaContext): def test_bearer_apikey( + monkeypatch, dynamodb_seeds, lambda_context: LambdaContext, ): @@ -38,6 +41,7 @@ def test_bearer_apikey( 'authorization': 'Bearer sk-MzI1MDQ0NTctZjEzMy00YzAwLTkzNmItNmFhNzEyY2E5ZjQw', } } + # This data was added from seeds assert app.lambda_handler(event, lambda_context) == { 'isAuthorized': True, @@ -49,7 +53,7 @@ def test_bearer_apikey( }, } - # This data was added from seeds + # # This data was added from seeds assert app.lambda_handler( { 'headers': { diff --git a/http-api/uv.lock b/http-api/uv.lock index 9464757..852df5b 100644 --- a/http-api/uv.lock +++ b/http-api/uv.lock @@ -444,7 +444,7 @@ wheels = [ [[package]] name = "layercake" -version = "0.1.8" +version = "0.1.9" source = { directory = "../layercake" } dependencies = [ { name = "aws-lambda-powertools", extra = ["all"] }, diff --git a/layercake/layercake/dynamodb.py b/layercake/layercake/dynamodb.py index 190ad00..3adb3ff 100644 --- a/layercake/layercake/dynamodb.py +++ b/layercake/layercake/dynamodb.py @@ -27,30 +27,31 @@ serializer = TypeSerializer() deserializer = TypeDeserializer() -def _serialize_python_type(value: Any) -> str | dict | list: - match value: +def _serialize_to_primitive_types(data: Any) -> str | dict | list: + match data: case datetime(): - return value.isoformat() + return data.isoformat() case UUID(): - return str(value) + return str(data) case IPv4Address(): - return str(value) + return str(data) case list() | tuple(): - return [_serialize_python_type(v) for v in value] + return [_serialize_to_primitive_types(v) for v in data] case dict(): - return {k: _serialize_python_type(v) for k, v in value.items()} + return {k: _serialize_to_primitive_types(v) for k, v in data.items()} case _: - return value + return data -def serialize(value: dict) -> dict: +def serialize(data: dict) -> dict: return { - k: serializer.serialize(_serialize_python_type(v)) for k, v in value.items() + k: serializer.serialize(_serialize_to_primitive_types(v)) + for k, v in data.items() } -def deserialize(value: dict) -> dict: - return {k: deserializer.deserialize(v) for k, v in value.items()} +def deserialize(data: dict) -> dict: + return {k: deserializer.deserialize(v) for k, v in data.items()} if TYPE_CHECKING: @@ -555,16 +556,16 @@ class DynamoDBCollection: self, persistence_layer: DynamoDBPersistenceLayer, /, - exc_cls: Type[ValueError] = MissingError, + exception_cls: Type[ValueError] = MissingError, tz: str = TZ, ) -> None: - if not issubclass(exc_cls, ValueError): + if not issubclass(exception_cls, ValueError): raise TypeError( - f'exception_cls must be a subclass of ValueError, got {exc_cls}' + f'exception_cls must be a subclass of ValueError, got {exception_cls}' ) self.persistence_layer = persistence_layer - self.exc_cls = exc_cls + self.exception_cls = exception_cls self.tz = tz def get_item( @@ -575,7 +576,7 @@ class DynamoDBCollection: default: Any = None, delimiter: str = '#', ) -> Any: - exc_cls = self.exc_cls + exc_cls = self.exception_cls data = self.persistence_layer.get_item(key) if raise_on_error and not data: diff --git a/layercake/pyproject.toml b/layercake/pyproject.toml index 5aa5698..231e963 100644 --- a/layercake/pyproject.toml +++ b/layercake/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "layercake" -version = "0.1.8" +version = "0.1.9" description = "Add your description here" readme = "README.md" authors = [ diff --git a/layercake/uv.lock b/layercake/uv.lock index 83d1ac1..cbcf934 100644 --- a/layercake/uv.lock +++ b/layercake/uv.lock @@ -465,7 +465,7 @@ wheels = [ [[package]] name = "layercake" -version = "0.1.6" +version = "0.1.9" source = { editable = "." } dependencies = [ { name = "aws-lambda-powertools", extra = ["all"] },