add audiolog

This commit is contained in:
2025-03-27 20:50:51 -03:00
parent 5af61465f3
commit 5756451738
10 changed files with 54 additions and 60 deletions

View File

@@ -1,3 +1,5 @@
import json
from aws_lambda_powertools.event_handler.api_gateway import ( from aws_lambda_powertools.event_handler.api_gateway import (
APIGatewayHttpResolver, APIGatewayHttpResolver,
Response, Response,
@@ -6,10 +8,13 @@ from aws_lambda_powertools.event_handler.middlewares import (
BaseMiddlewareHandler, BaseMiddlewareHandler,
NextMiddleware, NextMiddleware,
) )
from aws_lambda_powertools.shared.json_encoder import Encoder
from layercake.dateutils import now, ttl from layercake.dateutils import now, ttl
from layercake.dynamodb import ComposeKey, DynamoDBCollection, KeyPair from layercake.dynamodb import ComposeKey, DynamoDBCollection, KeyPair
from pydantic import UUID4, BaseModel, Field from pydantic import UUID4, BaseModel, Field
LOG_RETENTION_DAYS = 365 * 2 # 2 years
class AuthorizerMiddleware(BaseMiddlewareHandler): class AuthorizerMiddleware(BaseMiddlewareHandler):
def handler( def handler(
@@ -37,32 +42,52 @@ class AuthenticatedUser(BaseModel):
class AuditLogMiddleware(BaseMiddlewareHandler): 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.action = action
self.collect = collect self.collect = collect
self.retention_days = retention_days
def handler( def handler(
self, self,
app: APIGatewayHttpResolver, app: APIGatewayHttpResolver,
next_middleware: NextMiddleware, next_middleware: NextMiddleware,
) -> Response: ) -> Response:
collect = self.collect
response = next_middleware(app) response = next_middleware(app)
auth_user = app.context.get('authenticated_user') user = app.context.get('authenticated_user')
request_ctx = app.current_event.request_context request_ctx = app.current_event.request_context
ip_addr = request_ctx.http.source_ip ip_addr = request_ctx.http.source_ip
# Successful request # Successful request
if 200 <= response.status_code < 300 and auth_user: if 200 <= response.status_code < 300 and user:
now_ = now() 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( key=KeyPair(
pk=ComposeKey(auth_user.id, prefix='logs'), pk=ComposeKey(user.id, prefix='logs'),
sk=now_.isoformat(), sk=now_.isoformat(),
), ),
action=self.action, action=self.action,
data=data,
ip=ip_addr, ip=ip_addr,
ttl=ttl(start_dt=now_, days=365 * 2), ttl=retention_days,
) )
return response return response

View File

@@ -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 aws_lambda_powertools.event_handler.api_gateway import Router
from elasticsearch import Elasticsearch from elasticsearch import Elasticsearch
from layercake.dynamodb import DynamoDBCollection, DynamoDBPersistenceLayer from layercake.dynamodb import DynamoDBCollection, DynamoDBPersistenceLayer
from pydantic import BaseModel
import elastic import elastic
from boto3clients import dynamodb_client from boto3clients import dynamodb_client
@@ -40,25 +39,21 @@ def get_courses():
) )
class CoursePayload(BaseModel):
course: Course
@router.post( @router.post(
'/', '/',
compress=True, compress=True,
tags=['Course'], tags=['Course'],
middlewares=[AuditLogMiddleware('COURSE_ADD', collect)], middlewares=[AuditLogMiddleware('COURSE_ADD', collect)],
) )
def post_course(payload: CoursePayload): def post_course(payload: Course):
create_course( create_course(
course=payload.course, course=payload,
org=Org(id='*', name='default'), org=Org(id='*', name='default'),
persistence_layer=course_layer, persistence_layer=course_layer,
) )
return Response( return Response(
body=payload.course, body={'id': str(payload.id), },
content_type=content_types.APPLICATION_JSON, content_type=content_types.APPLICATION_JSON,
status_code=HTTPStatus.CREATED, status_code=HTTPStatus.CREATED,
) )

View File

@@ -21,7 +21,7 @@ LIMIT = 25
@router.get('/', include_in_schema=False) @router.get('/', include_in_schema=False)
def me(): def me():
user: AuthenticatedUser = router.context['user'] user: AuthenticatedUser = router.context['authenticated_user']
acls = collect.get_items( acls = collect.get_items(
KeyPair(user.id, PrefixKey('acls')), KeyPair(user.id, PrefixKey('acls')),
limit=LIMIT, limit=LIMIT,
@@ -39,7 +39,7 @@ def me():
@router.get('/konviva', include_in_schema=False) @router.get('/konviva', include_in_schema=False)
def konviva_(): def konviva_():
user: AuthenticatedUser = router.context['user'] user: AuthenticatedUser = router.context['authenticated_user']
token = konviva.token(user.email) token = konviva.token(user.email)
return {'redirect_uri': konviva.redirect_uri(token)} return {'redirect_uri': konviva.redirect_uri(token)}

View File

@@ -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_API_URL: str = os.getenv('KONVIVA_API_URL') # type: ignore
KONVIVA_SECRET_KEY: str = os.getenv('KONVIVA_SECRET_KEY') # type: ignore KONVIVA_SECRET_KEY: str = os.getenv('KONVIVA_SECRET_KEY') # type: ignore
match os.getenv('AWS_SAM_LOCAL'), os.getenv('ELASTIC_HOSTS'): match os.getenv('AWS_SAM_LOCAL'), os.getenv('ELASTIC_HOSTS'):
case str() as AWS_SAM_LOCAL, _ if ( case str() as AWS_SAM_LOCAL, _ if (
AWS_SAM_LOCAL AWS_SAM_LOCAL

View File

@@ -136,7 +136,10 @@ def dynamodb_seeds(dynamodb_client):
@pytest.fixture @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 import app
return app return app

View File

@@ -1,36 +1,28 @@
import json import json
from http import HTTPMethod, HTTPStatus from http import HTTPMethod, HTTPStatus
from layercake.dynamodb import DynamoDBPersistenceLayer from layercake.dynamodb import ComposeKey, DynamoDBCollection, PartitionKey
import app
from ..conftest import HttpApiProxy, LambdaContext from ..conftest import HttpApiProxy, LambdaContext
def test_post_course( def test_post_course(
mock_app, mock_app,
monkeypatch, dynamodb_persistence_layer,
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
http_api_proxy: HttpApiProxy, http_api_proxy: HttpApiProxy,
lambda_context: LambdaContext, lambda_context: LambdaContext,
): ):
app.courses.course_layer = dynamodb_persistence_layer r = mock_app.lambda_handler(
app.courses.user_layer = dynamodb_persistence_layer
r = app.lambda_handler(
http_api_proxy( http_api_proxy(
raw_path='/courses', raw_path='/courses',
method=HTTPMethod.POST, method=HTTPMethod.POST,
headers={'X-Tenant': '*'}, headers={'X-Tenant': '*'},
body={ body={
'course': {
'name': 'pytest', 'name': 'pytest',
'access_period': 365, 'access_period': 365,
'cert': { 'cert': {
'exp_interval': 730, # 2 years 'exp_interval': 730, # 2 years
}, },
}
}, },
), ),
lambda_context, lambda_context,
@@ -40,3 +32,9 @@ def test_post_course(
assert 'id' in json.loads(r['body']) assert 'id' in json.loads(r['body'])
assert r['statusCode'] == HTTPStatus.CREATED assert r['statusCode'] == HTTPStatus.CREATED
collect = DynamoDBCollection(dynamodb_persistence_layer)
logs = collect.get_items(
PartitionKey(ComposeKey('5OxmMjL-ujoR5IMGegQz', prefix='logs'))
)
print(logs)

View File

@@ -1,19 +1,14 @@
import json import json
from http import HTTPMethod, HTTPStatus from http import HTTPMethod, HTTPStatus
from layercake.dynamodb import DynamoDBPersistenceLayer
from ..conftest import HttpApiProxy, LambdaContext from ..conftest import HttpApiProxy, LambdaContext
def test_lookup( def test_lookup(
mock_app, mock_app,
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
http_api_proxy: HttpApiProxy, http_api_proxy: HttpApiProxy,
lambda_context: LambdaContext, lambda_context: LambdaContext,
): ):
mock_app.courses.course_layer = dynamodb_persistence_layer
r = mock_app.lambda_handler( r = mock_app.lambda_handler(
http_api_proxy( http_api_proxy(
raw_path='/lookup/sergio@somosbeta.com.br', raw_path='/lookup/sergio@somosbeta.com.br',

View File

@@ -1,20 +1,15 @@
import json import json
from http import HTTPMethod, HTTPStatus from http import HTTPMethod, HTTPStatus
from layercake.dynamodb import DynamoDBCollection, DynamoDBPersistenceLayer
from ..conftest import HttpApiProxy, LambdaContext from ..conftest import HttpApiProxy, LambdaContext
def test_me( def test_me(
mock_app, mock_app,
dynamodb_seeds, dynamodb_seeds,
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
http_api_proxy: HttpApiProxy, http_api_proxy: HttpApiProxy,
lambda_context: LambdaContext, lambda_context: LambdaContext,
): ):
mock_app.me.collect = DynamoDBCollection(dynamodb_persistence_layer)
# This data was added from seeds # This data was added from seeds
r = mock_app.lambda_handler( r = mock_app.lambda_handler(
http_api_proxy( http_api_proxy(

View File

@@ -1,20 +1,15 @@
import json import json
from http import HTTPMethod, HTTPStatus from http import HTTPMethod, HTTPStatus
from layercake.dynamodb import DynamoDBCollection, DynamoDBPersistenceLayer
from ..conftest import HttpApiProxy, LambdaContext from ..conftest import HttpApiProxy, LambdaContext
def test_get_emails( def test_get_emails(
mock_app, mock_app,
dynamodb_seeds, dynamodb_seeds,
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
http_api_proxy: HttpApiProxy, http_api_proxy: HttpApiProxy,
lambda_context: LambdaContext, lambda_context: LambdaContext,
): ):
mock_app.users.collect = DynamoDBCollection(dynamodb_persistence_layer)
r = mock_app.lambda_handler( r = mock_app.lambda_handler(
http_api_proxy( http_api_proxy(
raw_path='/users/5OxmMjL-ujoR5IMGegQz/emails', raw_path='/users/5OxmMjL-ujoR5IMGegQz/emails',
@@ -43,12 +38,9 @@ def test_get_emails(
def test_get_orgs( def test_get_orgs(
mock_app, mock_app,
dynamodb_seeds, dynamodb_seeds,
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
http_api_proxy: HttpApiProxy, http_api_proxy: HttpApiProxy,
lambda_context: LambdaContext, lambda_context: LambdaContext,
): ):
mock_app.users.collect = DynamoDBCollection(dynamodb_persistence_layer)
r = mock_app.lambda_handler( r = mock_app.lambda_handler(
http_api_proxy( http_api_proxy(
raw_path='/users/5OxmMjL-ujoR5IMGegQz/orgs', raw_path='/users/5OxmMjL-ujoR5IMGegQz/orgs',
@@ -63,12 +55,9 @@ def test_get_orgs(
def test_get_logs( def test_get_logs(
mock_app, mock_app,
dynamodb_seeds, dynamodb_seeds,
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
http_api_proxy: HttpApiProxy, http_api_proxy: HttpApiProxy,
lambda_context: LambdaContext, lambda_context: LambdaContext,
): ):
mock_app.users.collect = DynamoDBCollection(dynamodb_persistence_layer)
r = mock_app.lambda_handler( r = mock_app.lambda_handler(
http_api_proxy( http_api_proxy(
raw_path='/users/5OxmMjL-ujoR5IMGegQz/logs', raw_path='/users/5OxmMjL-ujoR5IMGegQz/logs',

View File

@@ -1,5 +1,3 @@
from layercake.dynamodb import DynamoDBCollection, DynamoDBPersistenceLayer
import auth as app import auth as app
from auth import _parse_bearer_token from auth import _parse_bearer_token
@@ -33,11 +31,8 @@ def test_bearer_jwt(lambda_context: LambdaContext):
def test_bearer_apikey( def test_bearer_apikey(
dynamodb_seeds, dynamodb_seeds,
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
lambda_context: LambdaContext, lambda_context: LambdaContext,
): ):
app.collect = DynamoDBCollection(dynamodb_persistence_layer)
event = { event = {
'headers': { 'headers': {
'authorization': 'Bearer sk-MzI1MDQ0NTctZjEzMy00YzAwLTkzNmItNmFhNzEyY2E5ZjQw', 'authorization': 'Bearer sk-MzI1MDQ0NTctZjEzMy00YzAwLTkzNmItNmFhNzEyY2E5ZjQw',