From b43ca4ad91d0f9b5e1e62caab3fd92bcbff61dfa Mon Sep 17 00:00:00 2001 From: aloftus23 Date: Fri, 17 Jan 2025 14:49:44 -0500 Subject: [PATCH 1/4] Fix matomo proxy --- .../xfd_django/xfd_api/api_methods/proxy.py | 29 ++++++++++- backend/src/xfd_django/xfd_api/views.py | 48 ++++++++++++------- frontend/src/pages/Settings/Settings.tsx | 9 ++++ 3 files changed, 66 insertions(+), 20 deletions(-) diff --git a/backend/src/xfd_django/xfd_api/api_methods/proxy.py b/backend/src/xfd_django/xfd_api/api_methods/proxy.py index 467924f2..01df54cb 100644 --- a/backend/src/xfd_django/xfd_api/api_methods/proxy.py +++ b/backend/src/xfd_django/xfd_api/api_methods/proxy.py @@ -24,11 +24,36 @@ async def proxy_request( target_url: str, path: Optional[str] = None, cookie_name: Optional[str] = None, + public_paths: Optional[list] = None, ): - """Proxy the request to the target URL.""" + """ + Proxy the request to the target URL. + + - Handles public paths without authentication. + - Manipulates cookies for session-based authentication. + """ headers = dict(request.headers) - # Cookie manipulation for specific cookie names + # Handle public paths + if public_paths and path in public_paths: + async with httpx.AsyncClient() as client: + proxy_response = await client.request( + method=request.method, + url=f"{target_url}/{path}", + headers=headers, + params=request.query_params, + content=await request.body(), + ) + proxy_response_headers = dict(proxy_response.headers) + proxy_response_headers.pop("transfer-encoding", None) + + return Response( + content=proxy_response.content, + status_code=proxy_response.status_code, + headers=proxy_response_headers, + ) + + # Handle cookies for private paths if cookie_name: cookies = manipulate_cookie(request, cookie_name) if cookies: diff --git a/backend/src/xfd_django/xfd_api/views.py b/backend/src/xfd_django/xfd_api/views.py index be38760d..dbdf203e 100644 --- a/backend/src/xfd_django/xfd_api/views.py +++ b/backend/src/xfd_django/xfd_api/views.py @@ -108,37 +108,45 @@ async def get_redis_client(request: Request): # Matomo Proxy @api_router.api_route( "/matomo/{path:path}", - dependencies=[Depends(get_current_active_user)], + methods=["GET", "POST", "PUT", "DELETE"], tags=["Analytics"], ) async def matomo_proxy( - path: str, request: Request, current_user: User = Depends(get_current_active_user) + path: str, + request: Request, + current_user: Optional[UserSchema] = None, # Optional for public paths ): """Proxy requests to the Matomo analytics instance.""" - # Public paths -- directly allowed - allowed_paths = ["/matomo.php", "/matomo.js"] - if any( - [request.url.path.startswith(allowed_path) for allowed_path in allowed_paths] - ): - return await proxy.proxy_request(path, request, os.getenv("MATOMO_URL")) - - # Redirects for specific font files - if request.url.path in [ + MATOMO_URL = os.getenv("MATOMO_URL", "") + + # Redirect font requests to CDN + font_paths = [ "/plugins/Morpheus/fonts/matomo.woff2", "/plugins/Morpheus/fonts/matomo.woff", "/plugins/Morpheus/fonts/matomo.ttf", - ]: + ] + if request.url.path in font_paths: return RedirectResponse( url=f"https://cdn.jsdelivr.net/gh/matomo-org/matomo@5.2.1{request.url.path}" ) - # Ensure only global admin can access other paths - if current_user.userType != "globalAdmin": + # Public paths allowed without authentication + public_paths = ["matomo.php", "matomo.js", "index.php"] + if path in public_paths: + return await proxy.proxy_request(request, MATOMO_URL, path) + + # Authentication for private paths + if not current_user: + current_user = await get_current_active_user(request) + if current_user is None or current_user.userType != "globalAdmin": raise HTTPException(status_code=403, detail="Unauthorized") - # Handle the proxy request to Matomo + # Proxy private paths return await proxy.proxy_request( - request, os.getenv("MATOMO_URL", ""), path, cookie_name="MATOMO_SESSID" + request=request, + target_url=MATOMO_URL, + path=path, + cookie_name="MATOMO_SESSID", ) @@ -157,8 +165,12 @@ async def pe_proxy( if current_user.userType not in ["globalView", "globalAdmin"]: raise HTTPException(status_code=403, detail="Unauthorized") - # Handle the proxy request to the P&E Django application - return await proxy.proxy_request(request, os.getenv("PE_API_URL", ""), path) + # Proxy the request to the P&E Django application + return await proxy.proxy_request( + request=request, + target_url=os.getenv("PE_API_URL", ""), + path=path, + ) # ======================================== diff --git a/frontend/src/pages/Settings/Settings.tsx b/frontend/src/pages/Settings/Settings.tsx index 9eb61a4c..d4855dbe 100644 --- a/frontend/src/pages/Settings/Settings.tsx +++ b/frontend/src/pages/Settings/Settings.tsx @@ -20,6 +20,15 @@ const Settings: React.FC = () => { .join(', ')}

Region: {user && user.regionId ? user.regionId : 'None'}

+ {user?.userType === 'globalAdmin' && ( + <> + + + +
+
+ + )} From 35dfacf44cfdab55cea584c0be83bb62b960a7db Mon Sep 17 00:00:00 2001 From: aloftus23 Date: Tue, 21 Jan 2025 08:49:57 -0500 Subject: [PATCH 2/4] FIx matomo proxy --- .../xfd_django/xfd_api/api_methods/proxy.py | 99 ++++++++++--------- backend/src/xfd_django/xfd_api/views.py | 44 ++------- 2 files changed, 61 insertions(+), 82 deletions(-) diff --git a/backend/src/xfd_django/xfd_api/api_methods/proxy.py b/backend/src/xfd_django/xfd_api/api_methods/proxy.py index 01df54cb..2f1e15e0 100644 --- a/backend/src/xfd_django/xfd_api/api_methods/proxy.py +++ b/backend/src/xfd_django/xfd_api/api_methods/proxy.py @@ -4,18 +4,11 @@ from typing import Optional # Third-Party Libraries -from fastapi import Request -from fastapi.responses import Response +from fastapi import HTTPException, Request +from fastapi.responses import RedirectResponse, Response import httpx - - -# Helper function to handle cookie manipulation -def manipulate_cookie(request: Request, cookie_name: str): - """Manipulate cookie.""" - cookies = request.cookies.get(cookie_name) - if cookies: - return {cookie_name: cookies} - return {} +from xfd_api.auth import get_current_active_user +from xfd_api.schema_models.user import User as UserSchema # Helper function to proxy requests @@ -24,57 +17,73 @@ async def proxy_request( target_url: str, path: Optional[str] = None, cookie_name: Optional[str] = None, - public_paths: Optional[list] = None, ): - """ - Proxy the request to the target URL. - - - Handles public paths without authentication. - - Manipulates cookies for session-based authentication. - """ + """Proxy requests to the specified target URL with optional cookie handling.""" headers = dict(request.headers) - # Handle public paths - if public_paths and path in public_paths: - async with httpx.AsyncClient() as client: - proxy_response = await client.request( - method=request.method, - url=f"{target_url}/{path}", - headers=headers, - params=request.query_params, - content=await request.body(), - ) - proxy_response_headers = dict(proxy_response.headers) - proxy_response_headers.pop("transfer-encoding", None) - - return Response( - content=proxy_response.content, - status_code=proxy_response.status_code, - headers=proxy_response_headers, - ) - - # Handle cookies for private paths + # Include specified cookie in the headers if present if cookie_name: - cookies = manipulate_cookie(request, cookie_name) + cookies = request.cookies.get(cookie_name) if cookies: - headers["Cookie"] = f"{cookie_name}={cookies[cookie_name]}" + headers["Cookie"] = f"{cookie_name}={cookies}" - # Make the request to the target URL - async with httpx.AsyncClient() as client: + # Send the request to the target + async with httpx.AsyncClient(timeout=httpx.Timeout(30.0)) as client: proxy_response = await client.request( method=request.method, - url=f"{target_url}/{path}", + url=f"{target_url}/{path}" if path else target_url, headers=headers, params=request.query_params, content=await request.body(), ) - # Remove chunked encoding for API Gateway compatibility + # Adjust response headers proxy_response_headers = dict(proxy_response.headers) - proxy_response_headers.pop("transfer-encoding", None) + for header in ["content-encoding", "transfer-encoding", "content-length"]: + proxy_response_headers.pop(header, None) return Response( content=proxy_response.content, status_code=proxy_response.status_code, headers=proxy_response_headers, ) + + +async def matomo_proxy_handler( + request: Request, + path: str, + current_user: Optional[UserSchema], + MATOMO_URL: str, +): + """ + Handles Matomo-specific proxy logic, including public paths, font redirects, + and authentication for private paths. + """ + # Redirect font requests to CDN + font_paths = { + "/plugins/Morpheus/fonts/matomo.woff2": "https://cdn.jsdelivr.net/gh/matomo-org/matomo@5.2.1/plugins/Morpheus/fonts/matomo.woff2", + "/plugins/Morpheus/fonts/matomo.woff": "https://cdn.jsdelivr.net/gh/matomo-org/matomo@5.2.1/plugins/Morpheus/fonts/matomo.woff", + "/plugins/Morpheus/fonts/matomo.ttf": "https://cdn.jsdelivr.net/gh/matomo-org/matomo@5.2.1/plugins/Morpheus/fonts/matomo.ttf", + } + if path in font_paths: + return RedirectResponse(url=font_paths[path]) + + # Public paths allowed without authentication + public_paths = ["matomo.php", "matomo.js", "index.php"] + if path in public_paths: + print("THIS IS A PUBLIC PATH") + return await proxy_request(request, MATOMO_URL, path) + + # Authenticate private paths + if not current_user: + current_user = await get_current_active_user(request) + if current_user is None or current_user.userType != "globalAdmin": + raise HTTPException(status_code=403, detail="Unauthorized") + + # Proxy private paths + return await proxy_request( + request=request, + target_url=MATOMO_URL, + path=path, + cookie_name="MATOMO_SESSID", + ) diff --git a/backend/src/xfd_django/xfd_api/views.py b/backend/src/xfd_django/xfd_api/views.py index dbdf203e..edae45b4 100644 --- a/backend/src/xfd_django/xfd_api/views.py +++ b/backend/src/xfd_django/xfd_api/views.py @@ -6,7 +6,6 @@ # Third-Party Libraries from fastapi import APIRouter, Body, Depends, HTTPException, Query, Request, status -from fastapi.responses import RedirectResponse from redis import asyncio as aioredis # from .schemas import Cpe @@ -118,36 +117,7 @@ async def matomo_proxy( ): """Proxy requests to the Matomo analytics instance.""" MATOMO_URL = os.getenv("MATOMO_URL", "") - - # Redirect font requests to CDN - font_paths = [ - "/plugins/Morpheus/fonts/matomo.woff2", - "/plugins/Morpheus/fonts/matomo.woff", - "/plugins/Morpheus/fonts/matomo.ttf", - ] - if request.url.path in font_paths: - return RedirectResponse( - url=f"https://cdn.jsdelivr.net/gh/matomo-org/matomo@5.2.1{request.url.path}" - ) - - # Public paths allowed without authentication - public_paths = ["matomo.php", "matomo.js", "index.php"] - if path in public_paths: - return await proxy.proxy_request(request, MATOMO_URL, path) - - # Authentication for private paths - if not current_user: - current_user = await get_current_active_user(request) - if current_user is None or current_user.userType != "globalAdmin": - raise HTTPException(status_code=403, detail="Unauthorized") - - # Proxy private paths - return await proxy.proxy_request( - request=request, - target_url=MATOMO_URL, - path=path, - cookie_name="MATOMO_SESSID", - ) + return await proxy.matomo_proxy_handler(request, path, current_user, MATOMO_URL) # P&E Proxy @@ -158,19 +128,19 @@ async def matomo_proxy( tags=["Analytics"], ) async def pe_proxy( - path: str, request: Request, current_user: User = Depends(get_current_active_user) + path: str, + request: Request, + current_user: UserSchema = Depends(get_current_active_user), ): """Proxy requests to the P&E Django application.""" + PE_API_URL = os.getenv("PE_API_URL", "") + # Ensure only Global Admin and Global View users can access if current_user.userType not in ["globalView", "globalAdmin"]: raise HTTPException(status_code=403, detail="Unauthorized") # Proxy the request to the P&E Django application - return await proxy.proxy_request( - request=request, - target_url=os.getenv("PE_API_URL", ""), - path=path, - ) + return await proxy.proxy_request(request, PE_API_URL, path) # ======================================== From 6c679cef4134cfd855af902fbf970bddd4bce76b Mon Sep 17 00:00:00 2001 From: Chrtorres Date: Thu, 23 Jan 2025 14:22:31 -0500 Subject: [PATCH 3/4] Added logging to matomodb --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 2be9bd07..16d89270 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -121,7 +121,7 @@ services: environment: - MYSQL_ROOT_PASSWORD=password logging: - driver: none + driver: json-file ports: - 3306:3306 From c30c8f923a9e25587c63b2c2cbf0df51aee58c31 Mon Sep 17 00:00:00 2001 From: Chrtorres Date: Tue, 28 Jan 2025 11:56:42 -0500 Subject: [PATCH 4/4] Added logging to matomo container --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 16d89270..f0926c84 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -142,7 +142,7 @@ services: - MATOMO_GENERAL_PROXY_URI_HEADER=1 - MATOMO_GENERAL_ASSUME_SECURE_PROTOCOL=1 logging: - driver: none + driver: json-file ports: - "8080:80"