Skip to content

Commit

Permalink
feat(api): log requests parameters (#108)
Browse files Browse the repository at this point in the history
  • Loading branch information
vmttn committed Jul 3, 2023
1 parent b8c1327 commit f0f0b76
Show file tree
Hide file tree
Showing 6 changed files with 106 additions and 38 deletions.
39 changes: 39 additions & 0 deletions api/src/alembic/versions/2daaedf28c12_extended_request.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""extended_request
Revision ID: 2daaedf28c12
Revises: ca282a8389ef
Create Date: 2023-07-01 16:11:01.969832
"""
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql

from alembic import op

# revision identifiers, used by Alembic.
revision = "2daaedf28c12"
down_revision = "ca282a8389ef"
branch_labels = None
depends_on = None


def upgrade() -> None:
op.add_column("api_request", sa.Column("base_url", sa.Text(), nullable=True))
op.add_column(
"api_request",
sa.Column(
"path_params", postgresql.JSONB(astext_type=sa.Text()), nullable=True
),
)
op.add_column(
"api_request",
sa.Column(
"query_params", postgresql.JSONB(astext_type=sa.Text()), nullable=True
),
)
op.add_column("api_request", sa.Column("client_host", sa.Text(), nullable=True))
op.add_column("api_request", sa.Column("client_port", sa.Integer(), nullable=True))


def downgrade() -> None:
pass
38 changes: 38 additions & 0 deletions api/src/data_inclusion/api/core/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from starlette import authentication, responses
from starlette.middleware.authentication import ( # noqa: F401
AuthenticationMiddleware as AuthenticationMiddleware,
)
from starlette.requests import HTTPConnection

import fastapi
from fastapi.security import HTTPBearer

from data_inclusion.api.core import jwt


def on_error(conn: HTTPConnection, exc: Exception) -> responses.Response:
return responses.JSONResponse(content=str(exc), status_code=403)


class AuthenticationBackend(authentication.AuthenticationBackend):
async def authenticate(self, conn):
http_bearer_instance = HTTPBearer()

try:
token = await http_bearer_instance(request=conn)
except fastapi.HTTPException as exc:
raise authentication.AuthenticationError(exc.detail)

payload = jwt.verify_token(token.credentials)

if payload is None:
raise authentication.AuthenticationError("Not authenticated")

scopes = ["authenticated"]

if payload.get("admin", False):
scopes += ["admin"]

return authentication.AuthCredentials(scopes=scopes), authentication.SimpleUser(
username=payload["sub"]
)
7 changes: 6 additions & 1 deletion api/src/data_inclusion/api/core/request/models.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import uuid

import sqlalchemy as sqla
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.dialects.postgresql import JSONB, UUID

from data_inclusion.api.core.db import Base

Expand All @@ -17,4 +17,9 @@ class Request(Base):
status_code = sqla.Column(sqla.SmallInteger)
method = sqla.Column(sqla.Text)
path = sqla.Column(sqla.Text)
base_url = sqla.Column(sqla.Text)
user = sqla.Column(sqla.Text, nullable=True)
path_params = sqla.Column(JSONB)
query_params = sqla.Column(JSONB)
client_host = sqla.Column(sqla.Text)
client_port = sqla.Column(sqla.Integer)
7 changes: 6 additions & 1 deletion api/src/data_inclusion/api/core/request/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ def save_request(request: requests.Request, response: responses.Response) -> Non
status_code=response.status_code,
method=request.method,
path=request.url.path,
user=request.scope.get("user"),
base_url=str(request.base_url),
user=request.user.username,
path_params=request.path_params,
query_params=dict(request.query_params),
client_host=request.client.host,
client_port=request.client.port,
)
session.add(request_instance)
session.commit()
35 changes: 10 additions & 25 deletions api/src/data_inclusion/api/entrypoints/fastapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@

import fastapi
import fastapi_pagination
from fastapi import middleware, requests
from fastapi import middleware
from fastapi.middleware import cors
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from fastapi.security import HTTPBearer
from fastapi_pagination.ext.sqlalchemy import paginate

from data_inclusion.api import models, schema, settings
from data_inclusion.api.core import db, jwt
from data_inclusion.api.core import auth, db, jwt
from data_inclusion.api.core.request.middleware import RequestMiddleware
from data_inclusion.api.utils import pagination

Expand Down Expand Up @@ -64,6 +64,11 @@ def create_app() -> fastapi.FastAPI:
allow_methods=["*"],
allow_headers=["*"],
),
middleware.Middleware(
auth.AuthenticationMiddleware,
backend=auth.AuthenticationBackend(),
on_error=auth.on_error,
),
middleware.Middleware(RequestMiddleware),
],
)
Expand All @@ -76,29 +81,9 @@ def create_app() -> fastapi.FastAPI:
return app


def authenticated(
request: requests.Request,
token: HTTPAuthorizationCredentials = fastapi.Depends(HTTPBearer()),
) -> Optional[dict]:
payload = jwt.verify_token(token.credentials)
if payload is None:
raise fastapi.HTTPException(
status_code=fastapi.status.HTTP_403_FORBIDDEN,
detail="Not authenticated",
)

# attach username to the request
# TODO: https://www.starlette.io/authentication/ instead
if "sub" in payload:
request.scope["user"] = payload["sub"]
request.scope["admin"] = payload.get("admin", False)

return payload


v0_api_router = fastapi.APIRouter(
prefix="/api/v0",
dependencies=[fastapi.Depends(authenticated)] if settings.TOKEN_ENABLED else [],
dependencies=[fastapi.Depends(HTTPBearer())] if settings.TOKEN_ENABLED else [],
tags=["Données"],
)

Expand Down Expand Up @@ -653,7 +638,7 @@ def create_token_endpoint(
token_creation_data: schema.TokenCreationData,
request: fastapi.Request,
):
if request.scope.get("admin"):
if "admin" in request.auth.scopes:
return create_token(email=token_creation_data.email)
else:
raise fastapi.HTTPException(
Expand Down
18 changes: 7 additions & 11 deletions api/tests/core/test_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,20 @@

@pytest.mark.with_token
def test_save_api_request_with_token(api_client, db_session):
url = "/api/v0/structures"
url = "/api/v0/structures/foo/bar?baz=1"

response = api_client.get(url)

assert response.status_code == 200
assert response.status_code == 404
assert db_session.query(models.Request).count() == 1

request_instance = db_session.query(models.Request).first()
assert request_instance.status_code == 200
assert request_instance.status_code == 404
assert request_instance.user == "some_user"
assert request_instance.path == "/api/v0/structures"
assert request_instance.path == "/api/v0/structures/foo/bar"
assert request_instance.method == "GET"
assert request_instance.path_params == {"source": "foo", "id": "bar"}
assert request_instance.query_params == {"baz": "1"}


def test_save_api_request_without_token(api_client, db_session):
Expand All @@ -25,10 +27,4 @@ def test_save_api_request_without_token(api_client, db_session):
response = api_client.get(url)

assert response.status_code == 403
assert db_session.query(models.Request).count() == 1

request_instance = db_session.query(models.Request).first()
assert request_instance.status_code == 403
assert request_instance.user is None
assert request_instance.path == "/api/v0/structures"
assert request_instance.method == "GET"
assert db_session.query(models.Request).count() == 0

0 comments on commit f0f0b76

Please sign in to comment.