add audiolog
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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)}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
Reference in New Issue
Block a user