diff --git a/backend/open_webui/env.py b/backend/open_webui/env.py index 7cd3898eaf..f05f16591e 100644 --- a/backend/open_webui/env.py +++ b/backend/open_webui/env.py @@ -964,6 +964,12 @@ AUDIT_EXCLUDED_PATHS = os.getenv("AUDIT_EXCLUDED_PATHS", "/chats,/chat,/folders" AUDIT_EXCLUDED_PATHS = [path.strip() for path in AUDIT_EXCLUDED_PATHS] AUDIT_EXCLUDED_PATHS = [path.lstrip("/") for path in AUDIT_EXCLUDED_PATHS] +# Comma separated list of urls to include in audit (whitelist mode) +# When set, only these paths are audited and AUDIT_EXCLUDED_PATHS is ignored +AUDIT_INCLUDED_PATHS = os.getenv("AUDIT_INCLUDED_PATHS", "").split(",") +AUDIT_INCLUDED_PATHS = [path.strip() for path in AUDIT_INCLUDED_PATHS] +AUDIT_INCLUDED_PATHS = [path.lstrip("/") for path in AUDIT_INCLUDED_PATHS if path] + #################################### # OPENTELEMETRY diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index f13d317108..e1d237b431 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -467,6 +467,7 @@ from open_webui.env import ( ENABLE_CUSTOM_MODEL_FALLBACK, LICENSE_KEY, AUDIT_EXCLUDED_PATHS, + AUDIT_INCLUDED_PATHS, AUDIT_LOG_LEVEL, CHANGELOG, REDIS_URL, @@ -1581,6 +1582,7 @@ if audit_level != AuditLevel.NONE: AuditLoggingMiddleware, audit_level=audit_level, excluded_paths=AUDIT_EXCLUDED_PATHS, + included_paths=AUDIT_INCLUDED_PATHS, max_body_size=MAX_BODY_LOG_SIZE, ) ################################## diff --git a/backend/open_webui/utils/audit.py b/backend/open_webui/utils/audit.py index c4abb445b9..bf3c33774c 100644 --- a/backend/open_webui/utils/audit.py +++ b/backend/open_webui/utils/audit.py @@ -24,7 +24,7 @@ from asgiref.typing import ( from loguru import logger from starlette.requests import Request -from open_webui.env import AUDIT_LOG_LEVEL, MAX_BODY_LOG_SIZE +from open_webui.env import AUDIT_LOG_LEVEL, AUDIT_INCLUDED_PATHS, MAX_BODY_LOG_SIZE from open_webui.utils.auth import get_current_user, get_http_authorization_cred from open_webui.models.users import UserModel @@ -129,15 +129,23 @@ class AuditLoggingMiddleware: app: ASGI3Application, *, excluded_paths: Optional[list[str]] = None, + included_paths: Optional[list[str]] = None, max_body_size: int = MAX_BODY_LOG_SIZE, audit_level: AuditLevel = AuditLevel.NONE, ) -> None: self.app = app self.audit_logger = AuditLogger(logger) self.excluded_paths = excluded_paths or [] + self.included_paths = included_paths or [] self.max_body_size = max_body_size self.audit_level = audit_level + if self.included_paths and self.excluded_paths: + logger.warning( + "Both AUDIT_INCLUDED_PATHS and AUDIT_EXCLUDED_PATHS are set. " + "AUDIT_INCLUDED_PATHS (whitelist) takes precedence." + ) + async def __call__( self, scope: ASGIScope, @@ -226,7 +234,16 @@ class AuditLoggingMiddleware: ): return True - # match either /api//...(for the endpoint /api/chat case) or /api/v1//... + # Whitelist mode: only log paths that match included_paths + if self.included_paths: + pattern = re.compile( + r"^/api(?:/v1)?/(" + "|".join(self.included_paths) + r")\b" + ) + if not pattern.match(request.url.path): + return True # Skip: path not in whitelist + return False # Do NOT skip: path is in whitelist + + # Blacklist mode: skip paths that match excluded_paths pattern = re.compile( r"^/api(?:/v1)?/(" + "|".join(self.excluded_paths) + r")\b" )