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 (
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

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 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,
)

View File

@@ -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)}

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_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

View File

@@ -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

View File

@@ -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)

View File

@@ -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',

View File

@@ -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(

View File

@@ -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',

View File

@@ -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',