diff --git a/http-api/middlewares.py b/http-api/middlewares.py index cfd7ac0..dd03d1a 100644 --- a/http-api/middlewares.py +++ b/http-api/middlewares.py @@ -1,3 +1,5 @@ +import json + from aws_lambda_powertools.event_handler.api_gateway import ( APIGatewayHttpResolver, Response, @@ -6,10 +8,13 @@ from aws_lambda_powertools.event_handler.middlewares import ( BaseMiddlewareHandler, NextMiddleware, ) +from aws_lambda_powertools.shared.json_encoder import Encoder from layercake.dateutils import now, ttl from layercake.dynamodb import ComposeKey, DynamoDBCollection, KeyPair from pydantic import UUID4, BaseModel, Field +LOG_RETENTION_DAYS = 365 * 2 # 2 years + class AuthorizerMiddleware(BaseMiddlewareHandler): def handler( @@ -37,32 +42,52 @@ class AuthenticatedUser(BaseModel): class AuditLogMiddleware(BaseMiddlewareHandler): - def __init__(self, action: str, /, collect: DynamoDBCollection) -> None: + """This middleware logs audit details for successful requests, storing user, + action, and IP info with a specified retention period..""" + + def __init__( + self, + action: str, + /, + collect: DynamoDBCollection, + retention_days: int | None = LOG_RETENTION_DAYS, + ) -> None: self.action = action self.collect = collect + self.retention_days = retention_days def handler( self, app: APIGatewayHttpResolver, next_middleware: NextMiddleware, ) -> Response: + collect = self.collect response = next_middleware(app) - auth_user = app.context.get('authenticated_user') + 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: + if 200 <= response.status_code < 300 and user: now_ = now() + data = ( + json.dumps(response.body, cls=Encoder) if response.is_json() else None + ) + retention_days = ( + ttl(start_dt=now_, days=self.retention_days) + if self.retention_days + else None + ) - self.collect.put_item( + collect.put_item( key=KeyPair( - pk=ComposeKey(auth_user.id, prefix='logs'), + pk=ComposeKey(user.id, prefix='logs'), sk=now_.isoformat(), ), action=self.action, + data=data, ip=ip_addr, - ttl=ttl(start_dt=now_, days=365 * 2), + ttl=retention_days, ) return response diff --git a/http-api/routes/courses/__init__.py b/http-api/routes/courses/__init__.py index d0e8d07..6c0f3b6 100644 --- a/http-api/routes/courses/__init__.py +++ b/http-api/routes/courses/__init__.py @@ -5,7 +5,6 @@ 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 DynamoDBCollection, DynamoDBPersistenceLayer -from pydantic import BaseModel import elastic from boto3clients import dynamodb_client @@ -40,25 +39,21 @@ def get_courses(): ) -class CoursePayload(BaseModel): - course: Course - - @router.post( '/', compress=True, tags=['Course'], middlewares=[AuditLogMiddleware('COURSE_ADD', collect)], ) -def post_course(payload: CoursePayload): +def post_course(payload: Course): create_course( - course=payload.course, + course=payload, org=Org(id='*', name='default'), persistence_layer=course_layer, ) return Response( - body=payload.course, + body={'id': str(payload.id), }, content_type=content_types.APPLICATION_JSON, status_code=HTTPStatus.CREATED, ) diff --git a/http-api/routes/me/__init__.py b/http-api/routes/me/__init__.py index c5461df..05ee9cf 100644 --- a/http-api/routes/me/__init__.py +++ b/http-api/routes/me/__init__.py @@ -21,7 +21,7 @@ LIMIT = 25 @router.get('/', include_in_schema=False) def me(): - user: AuthenticatedUser = router.context['user'] + user: AuthenticatedUser = router.context['authenticated_user'] acls = collect.get_items( KeyPair(user.id, PrefixKey('acls')), limit=LIMIT, @@ -39,7 +39,7 @@ def me(): @router.get('/konviva', include_in_schema=False) def konviva_(): - user: AuthenticatedUser = router.context['user'] + user: AuthenticatedUser = router.context['authenticated_user'] token = konviva.token(user.email) return {'redirect_uri': konviva.redirect_uri(token)} diff --git a/http-api/settings.py b/http-api/settings.py index 8ae1502..43ce276 100644 --- a/http-api/settings.py +++ b/http-api/settings.py @@ -8,7 +8,6 @@ COURSE_TABLE: str = os.getenv('COURSE_TABLE') # type: ignore KONVIVA_API_URL: str = os.getenv('KONVIVA_API_URL') # type: ignore 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 diff --git a/http-api/tests/conftest.py b/http-api/tests/conftest.py index cb78f36..ba69cbe 100644 --- a/http-api/tests/conftest.py +++ b/http-api/tests/conftest.py @@ -136,7 +136,10 @@ def dynamodb_seeds(dynamodb_client): @pytest.fixture -def mock_app(): +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_courses.py b/http-api/tests/routes/test_courses.py index bf0c3d3..4c8af93 100644 --- a/http-api/tests/routes/test_courses.py +++ b/http-api/tests/routes/test_courses.py @@ -1,36 +1,28 @@ import json from http import HTTPMethod, HTTPStatus -from layercake.dynamodb import DynamoDBPersistenceLayer - -import app +from layercake.dynamodb import ComposeKey, DynamoDBCollection, PartitionKey from ..conftest import HttpApiProxy, LambdaContext def test_post_course( mock_app, - monkeypatch, - dynamodb_persistence_layer: DynamoDBPersistenceLayer, + dynamodb_persistence_layer, http_api_proxy: HttpApiProxy, lambda_context: LambdaContext, ): - app.courses.course_layer = dynamodb_persistence_layer - app.courses.user_layer = dynamodb_persistence_layer - - r = app.lambda_handler( + r = mock_app.lambda_handler( http_api_proxy( raw_path='/courses', method=HTTPMethod.POST, headers={'X-Tenant': '*'}, body={ - 'course': { - 'name': 'pytest', - 'access_period': 365, - 'cert': { - 'exp_interval': 730, # 2 years - }, - } + 'name': 'pytest', + 'access_period': 365, + 'cert': { + 'exp_interval': 730, # 2 years + }, }, ), lambda_context, @@ -40,3 +32,9 @@ def test_post_course( assert 'id' in json.loads(r['body']) assert r['statusCode'] == HTTPStatus.CREATED + + collect = DynamoDBCollection(dynamodb_persistence_layer) + logs = collect.get_items( + PartitionKey(ComposeKey('5OxmMjL-ujoR5IMGegQz', prefix='logs')) + ) + print(logs) diff --git a/http-api/tests/routes/test_lookup.py b/http-api/tests/routes/test_lookup.py index 6c8140f..252d783 100644 --- a/http-api/tests/routes/test_lookup.py +++ b/http-api/tests/routes/test_lookup.py @@ -1,19 +1,14 @@ 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', diff --git a/http-api/tests/routes/test_me.py b/http-api/tests/routes/test_me.py index f8a496b..873e1ed 100644 --- a/http-api/tests/routes/test_me.py +++ b/http-api/tests/routes/test_me.py @@ -1,20 +1,15 @@ import json from http import HTTPMethod, HTTPStatus -from layercake.dynamodb import DynamoDBCollection, DynamoDBPersistenceLayer - from ..conftest import HttpApiProxy, LambdaContext def test_me( mock_app, dynamodb_seeds, - dynamodb_persistence_layer: DynamoDBPersistenceLayer, http_api_proxy: HttpApiProxy, lambda_context: LambdaContext, ): - mock_app.me.collect = DynamoDBCollection(dynamodb_persistence_layer) - # This data was added from seeds r = mock_app.lambda_handler( http_api_proxy( diff --git a/http-api/tests/routes/test_users.py b/http-api/tests/routes/test_users.py index 87e804d..b93e1f8 100644 --- a/http-api/tests/routes/test_users.py +++ b/http-api/tests/routes/test_users.py @@ -1,20 +1,15 @@ import json from http import HTTPMethod, HTTPStatus -from layercake.dynamodb import DynamoDBCollection, DynamoDBPersistenceLayer - from ..conftest import HttpApiProxy, LambdaContext def test_get_emails( mock_app, dynamodb_seeds, - dynamodb_persistence_layer: DynamoDBPersistenceLayer, http_api_proxy: HttpApiProxy, lambda_context: LambdaContext, ): - mock_app.users.collect = DynamoDBCollection(dynamodb_persistence_layer) - r = mock_app.lambda_handler( http_api_proxy( raw_path='/users/5OxmMjL-ujoR5IMGegQz/emails', @@ -43,12 +38,9 @@ def test_get_emails( def test_get_orgs( mock_app, dynamodb_seeds, - dynamodb_persistence_layer: DynamoDBPersistenceLayer, http_api_proxy: HttpApiProxy, lambda_context: LambdaContext, ): - mock_app.users.collect = DynamoDBCollection(dynamodb_persistence_layer) - r = mock_app.lambda_handler( http_api_proxy( raw_path='/users/5OxmMjL-ujoR5IMGegQz/orgs', @@ -63,12 +55,9 @@ def test_get_orgs( def test_get_logs( mock_app, dynamodb_seeds, - dynamodb_persistence_layer: DynamoDBPersistenceLayer, http_api_proxy: HttpApiProxy, lambda_context: LambdaContext, ): - mock_app.users.collect = DynamoDBCollection(dynamodb_persistence_layer) - r = mock_app.lambda_handler( http_api_proxy( raw_path='/users/5OxmMjL-ujoR5IMGegQz/logs', diff --git a/http-api/tests/test_auth.py b/http-api/tests/test_auth.py index c60370b..7b9b545 100644 --- a/http-api/tests/test_auth.py +++ b/http-api/tests/test_auth.py @@ -1,5 +1,3 @@ -from layercake.dynamodb import DynamoDBCollection, DynamoDBPersistenceLayer - import auth as app from auth import _parse_bearer_token @@ -33,11 +31,8 @@ def test_bearer_jwt(lambda_context: LambdaContext): def test_bearer_apikey( dynamodb_seeds, - dynamodb_persistence_layer: DynamoDBPersistenceLayer, lambda_context: LambdaContext, ): - app.collect = DynamoDBCollection(dynamodb_persistence_layer) - event = { 'headers': { 'authorization': 'Bearer sk-MzI1MDQ0NTctZjEzMy00YzAwLTkzNmItNmFhNzEyY2E5ZjQw',