Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PR Feature/ new /verify-request for nginx #140

Merged
merged 3 commits into from
Jun 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions hdx_hapi/endpoints/get_request_verification.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from fastapi import APIRouter

router = APIRouter(
tags=['Nginx'],
)


@router.get(
'/api/util/verify-request',
response_model=None,
include_in_schema=False,
)
@router.get(
'/api/v1/util/verify-request',
response_model=None,
include_in_schema=False,
)
async def get_request_verification():
return None
82 changes: 65 additions & 17 deletions hdx_hapi/endpoints/middleware/app_identifier_middleware.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import Optional, Tuple
from urllib.parse import parse_qs, urlparse
from fastapi import Request, status
from fastapi.responses import JSONResponse
from pydantic import BaseModel, EmailStr
Expand Down Expand Up @@ -33,27 +35,73 @@ async def app_identifier_middleware(request: Request, call_next):
"""
Middleware to check for the app_identifier in the request and add it to the request state
"""
if (
CONFIG.HAPI_IDENTIFIER_FILTERING
and request.url.path.startswith('/api')
and request.url.path not in ALLOWED_API_ENDPOINTS
):
app_identifier = request.query_params.get('app_identifier')
authorization = request.headers.get('X-HDX-HAPI-APP-IDENTIFIER')
encoded_value = app_identifier or authorization

if not encoded_value:
return JSONResponse(content={'error': 'Missing app identifier'}, status_code=status.HTTP_400_BAD_REQUEST)
if CONFIG.HAPI_IDENTIFIER_FILTERING:
header_identifier = request.headers.get('X-HDX-HAPI-APP-IDENTIFIER')

is_nginx_verify_request = request.url.path.startswith(
'/api/v1/util/verify-request'
) or request.url.path.startswith('/api/util/verify-request')
original_uri_from_nginx = request.headers.get('X-Original-URI')

if is_nginx_verify_request:
if not original_uri_from_nginx:
return JSONResponse(content={'error': 'Missing X-Original-URI'}, status_code=status.HTTP_403_FORBIDDEN)
path, app_identifier = _extract_path_and_identifier_from_original_url(original_uri_from_nginx)
else:
path = request.url.path
app_identifier = request.query_params.get('app_identifier')

status_code, error_message, identifier_params = _check_allow_request(path, app_identifier or header_identifier)

if status_code == status.HTTP_200_OK:
request.state.app_name = identifier_params.application if identifier_params else None
else:
return JSONResponse(content={'error': error_message}, status_code=status_code)

response = await call_next(request)
return response


def _extract_path_and_identifier_from_original_url(original_url: str) -> Tuple[str, Optional[str]]:
"""
Extract the path and app_identifier from the Nginx header.
Args:
original_url: The original URL from the Nginx header
Returns:
Tuple of path and app_identifier
"""

parsed_url = urlparse(original_url)
path = parsed_url.path
query_params = parse_qs(parsed_url.query)
app_identifier = query_params.get('app_identifier', [None])[0]
return path, app_identifier


def _check_allow_request(
request_path: str, encoded_app_identifier: Optional[str]
) -> Tuple[int, Optional[str], Optional[IdentifierParams]]:
"""
Check if the request is allowed.
Args:
request_path: The path of the request
encoded_app_identifier: The app_identifier
Returns:
Tuple of status code, error message and IdentifierParams
"""
if request_path and request_path.startswith('/api') and request_path not in ALLOWED_API_ENDPOINTS:
if not encoded_app_identifier:
return status.HTTP_403_FORBIDDEN, 'Missing app identifier', None

try:
decoded_value = base64.b64decode(encoded_value).decode('utf-8')
decoded_value = base64.b64decode(encoded_app_identifier).decode('utf-8')
application, email = decoded_value.split(':')
identifier_params = IdentifierParams(application=application, email=email)
logger.warning(f'Application: {application}, Email: {email}')
# Adding the app_name to the request state so it can be accessed in the endpoint
request.state.app_name = identifier_params.application

return status.HTTP_200_OK, None, identifier_params

except Exception:
return JSONResponse(content={'error': 'Invalid app identifier'}, status_code=status.HTTP_400_BAD_REQUEST)
return status.HTTP_403_FORBIDDEN, 'Invalid app identifier', None

response = await call_next(request)
return response
return status.HTTP_200_OK, None, None
2 changes: 2 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from hdx_hapi.endpoints.middleware.mixpanel_tracking_middleware import mixpanel_tracking_middleware # noqa

from hdx_hapi.endpoints.get_encoded_identifier import router as encoded_identifier_router # noqa
from hdx_hapi.endpoints.get_request_verification import router as request_verification_router # noqa

from hdx_hapi.endpoints.favicon import router as favicon_router # noqa

Expand Down Expand Up @@ -74,6 +75,7 @@
)

app.include_router(encoded_identifier_router)
app.include_router(request_verification_router)
app.include_router(favicon_router)
app.include_router(operational_presence_router)
app.include_router(funding_router)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ async def test_endpoints_vs_encode_identifier(event_loop, refresh_db, enable_hap
for endpoint_router in ENDPOINT_ROUTER_LIST:
async with AsyncClient(app=app, base_url='http://test') as ac:
response = await ac.get(endpoint_router)
assert response.status_code == 400
assert response.status_code == 403

async with AsyncClient(app=app, base_url='http://test', params=query_parameters) as ac:
response = await ac.get(endpoint_router)
Expand Down
38 changes: 38 additions & 0 deletions tests/test_endpoints/test_request_verification_endpoint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import pytest

from httpx import AsyncClient
from main import app


ENDPOINT_ROUTER = '/api/v1/util/verify-request'

APP_IDENTIFIER = 'cHl0ZXN0czpweXRlc3RzQGh1bWRhdGEub3Jn'
URL_WITHOUT_APP_IDENTIFIER = 'http://localhost/api/v1/population-social/population?output_format=json&limit=10&offset=0'
URL_WITH_APP_IDENTIFIER = (
'http://localhost/api/v1/population-social/population?output_format=json&'
f'app_identifier={APP_IDENTIFIER}&limit=10&offset=0'
)


@pytest.mark.asyncio
async def test_verify_request(event_loop, refresh_db, enable_hapi_identifier_filtering):
status_code = await _perform_request({})
assert status_code == 403

headers = {'X-Original-URI': URL_WITHOUT_APP_IDENTIFIER}
status_code = await _perform_request(headers)
assert status_code == 403

headers = {'X-Original-URI': URL_WITHOUT_APP_IDENTIFIER, 'X-HDX-HAPI-APP-IDENTIFIER': APP_IDENTIFIER}
status_code = await _perform_request(headers)
assert status_code == 200

headers = {'X-Original-URI': URL_WITH_APP_IDENTIFIER}
status_code = await _perform_request(headers)
assert status_code == 200


async def _perform_request(headers) -> int:
async with AsyncClient(app=app, base_url='http://test', headers=headers) as ac:
response = await ac.get(ENDPOINT_ROUTER)
return response.status_code
Loading