diff --git a/backend/open_webui/utils/audit.py b/backend/open_webui/utils/audit.py index 313f4cf235..d2802613ea 100644 --- a/backend/open_webui/utils/audit.py +++ b/backend/open_webui/utils/audit.py @@ -140,6 +140,15 @@ class AuditLoggingMiddleware: self.audited_methods.add('GET') self.audit_level = audit_level + # Paths are fixed for the process lifetime; compile once instead of + # per request. None means the corresponding mode has nothing to match. + self._included_pattern = ( + re.compile(r'^/api(?:/v1)?/(' + '|'.join(self.included_paths) + r')\b') if self.included_paths else None + ) + self._excluded_pattern = ( + re.compile(r'^/api(?:/v1)?/(' + '|'.join(self.excluded_paths) + r')\b') if self.excluded_paths else None + ) + if self.included_paths and self.excluded_paths: logger.warning( 'Both AUDIT_INCLUDED_PATHS and AUDIT_EXCLUDED_PATHS are set. ' @@ -196,6 +205,13 @@ class AuditLoggingMiddleware: await self._log_audit_entry(request, context) async def _get_authenticated_user(self, request: Request) -> Optional[UserModel]: + # get_current_user stashes the resolved user on the scope-backed state; + # reuse it instead of running the full auth pipeline (JWT decode, Redis + # revocation checks, DB fetch, last-active write) a second time. + user = getattr(request.state, 'user', None) + if isinstance(user, UserModel): + return user + auth_header = request.headers.get('Authorization') try: @@ -206,6 +222,12 @@ class AuditLoggingMiddleware: return None + ALWAYS_LOG_ENDPOINTS = ( + '/api/v1/auths/signin', + '/api/v1/auths/signout', + '/api/v1/auths/signup', + ) + def _should_skip_auditing(self, request: Request) -> bool: if AUDIT_LOG_LEVEL == 'NONE': return True @@ -213,13 +235,8 @@ class AuditLoggingMiddleware: if request.method not in self.audited_methods: return True - ALWAYS_LOG_ENDPOINTS = { - '/api/v1/auths/signin', - '/api/v1/auths/signout', - '/api/v1/auths/signup', - } path = request.url.path.lower() - for endpoint in ALWAYS_LOG_ENDPOINTS: + for endpoint in self.ALWAYS_LOG_ENDPOINTS: if path.startswith(endpoint): return False # Do NOT skip logging for auth endpoints @@ -229,15 +246,11 @@ class AuditLoggingMiddleware: return True # 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 + if self._included_pattern: + return not self._included_pattern.match(request.url.path) # Blacklist mode: skip paths that match excluded_paths - pattern = re.compile(r'^/api(?:/v1)?/(' + '|'.join(self.excluded_paths) + r')\b') - if pattern.match(request.url.path): + if self._excluded_pattern and self._excluded_pattern.match(request.url.path): return True return False diff --git a/backend/open_webui/utils/auth.py b/backend/open_webui/utils/auth.py index 42b1aea9bd..f9d596e615 100644 --- a/backend/open_webui/utils/auth.py +++ b/backend/open_webui/utils/auth.py @@ -356,6 +356,8 @@ async def get_current_user( current_span.set_attribute('client.user.role', user.role) current_span.set_attribute('client.auth.type', 'api_key') + # Scope-backed, so outer middleware (audit) can reuse the resolved user + request.state.user = user return user # auth by jwt token @@ -404,6 +406,9 @@ async def get_current_user( # Refresh the user's last active timestamp # Fire-and-forget via asyncio.create_task to avoid blocking asyncio.create_task(Users.update_last_active_by_id(user.id)) + + # Scope-backed, so outer middleware (audit) can reuse the resolved user + request.state.user = user return user else: raise HTTPException(