keylime/0008-refactor-Remove-dead-code-AuthSession.authenticate_a.patch
Sergio Arroutbi d3a4e38571
Add patches to fix DB connection leaks
Resolves: #RHEL-153811

Backport upstream fixes for database connection pool exhaustion
that occurred during multi-host push attestation with multiple
agents, causing QueuePool timeout and HTTP 500 errors.

Upstream commits:
- 5b622eae Close DB sessions to prevent connection exhaustion
- bc28d5d2 Include thread-safe session management
- 4f5f09a6 Address some improvements from code review
- 309a0ef0 Fix race condition in SessionManager
- e75921f0 Fix linter errors in PersistableModel.get() and .all()
- 2d809d8b refactor: Remove dead code AuthSession.authenticate_agent()
- e935df8f db: Clean up scoped session after each request
- 08c0c67c fix: Check active flag in _extract_identity and guard receive_pop
- d74e7499 fix: Add fork-safety to DBManager via dispose()

Signed-off-by: Sergio Arroutbi <sarroutb@redhat.com>
2026-03-23 11:28:49 +01:00

458 lines
22 KiB
Diff

From 2d809d8b537c0d9faab05ee5fe7efb85f48918f3 Mon Sep 17 00:00:00 2001
From: Anderson Toshiyuki Sasaki <ansasaki@redhat.com>
Date: Fri, 13 Mar 2026 10:53:54 +0100
Subject: [PATCH] refactor: Remove dead code AuthSession.authenticate_agent()
authenticate_agent() was superseded by _extract_identity() in
action_handler.py, which performs token-based agent authentication
directly via AuthSession.get_by_token(). The method, its helper
get_session(), the module-level _engine global, and the associated
unused imports (Session, SessionManager, make_engine) are all removed.
The corresponding tests (test_authenticate_agent_success,
test_authenticate_agent_inactive_session,
test_authenticate_agent_no_session) are also removed.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Anderson Toshiyuki Sasaki <ansasaki@redhat.com>
---
keylime/models/verifier/auth_session.py | 67 +-----------
keylime/web/verifier/session_controller.py | 9 +-
test/test_auth_session.py | 113 +--------------------
test/test_session_controller.py | 52 +++++-----
4 files changed, 32 insertions(+), 209 deletions(-)
diff --git a/keylime/models/verifier/auth_session.py b/keylime/models/verifier/auth_session.py
index 918dfb4..b0b40b0 100644
--- a/keylime/models/verifier/auth_session.py
+++ b/keylime/models/verifier/auth_session.py
@@ -1,12 +1,8 @@
import base64
import hmac
-import threading
import uuid
-from contextlib import contextmanager
from datetime import timedelta
-from typing import Any, Dict, Iterator, Optional, Sequence
-
-from sqlalchemy.orm import Session
+from typing import Any, Dict, Optional, Sequence
from keylime import config, keylime_logging
from keylime.crypto import (
@@ -16,7 +12,6 @@ from keylime.crypto import (
parse_session_token,
verify_token_hash,
)
-from keylime.db.keylime_db import SessionManager, make_engine
from keylime.db.verifier_db import VerfierMain
from keylime.models.base import *
from keylime.shared_data import get_shared_memory
@@ -31,21 +26,6 @@ from keylime.tpm.tpm_main import Tpm
logger = keylime_logging.init_logging("verifier")
-_engine = None
-_engine_lock = threading.Lock()
-_session_manager = SessionManager()
-
-
-@contextmanager
-def get_session_context() -> Iterator[Session]:
- global _engine
- if _engine is None:
- with _engine_lock:
- if _engine is None:
- _engine = make_engine("cloud_verifier")
- with _session_manager.session_context(_engine) as session:
- yield session
-
class AuthSession(PersistableModel):
# Explicit attribute declarations for type checkers
@@ -244,51 +224,6 @@ class AuthSession(PersistableModel):
# Slow path: query database by primary key
return cls.get(session_id) # type: ignore[return-value]
- @classmethod
- def authenticate_agent(cls, token: str): # type: ignore[no-untyped-def]
- """Authenticate an agent using their session token.
-
- Uses indexed database lookup by token hash for performance (O(1) instead of O(n)).
- Tokens are hashed before lookup since only hashes are stored in the database.
-
- Args:
- token: The session token to verify
-
- Returns:
- VerfierMain object if authenticated, False otherwise
- """
- # Use indexed lookup by token hash (much faster than scanning all sessions)
- auth_session = cls.get_by_token(token)
-
- if not auth_session:
- return False
-
- # Validate session is active
- if not getattr(auth_session, "active", False):
- return False
-
- # Validate session hasn't expired
- token_expires_at = getattr(auth_session, "token_expires_at", None)
- if token_expires_at and token_expires_at < Timestamp.now():
- logger.debug(
- "Authentication attempted with expired token for agent '%s' (expired at %s)",
- getattr(auth_session, "agent_id", "unknown"),
- token_expires_at,
- )
- return False
-
- # Use old engine to query VerfierMain (legacy model)
- with get_session_context() as session:
- agent = (
- session.query(VerfierMain)
- .filter(VerfierMain.agent_id == auth_session.agent_id) # type: ignore[attr-defined]
- .one_or_none()
- )
- if agent:
- session.expunge(agent) # type: ignore[no-untyped-call]
-
- return agent
-
@classmethod
def create(
cls, agent: Optional[VerfierMain], data: Dict[str, Any], agent_id: Optional[str] = None
diff --git a/keylime/web/verifier/session_controller.py b/keylime/web/verifier/session_controller.py
index c8664e2..9a314f2 100644
--- a/keylime/web/verifier/session_controller.py
+++ b/keylime/web/verifier/session_controller.py
@@ -2,9 +2,8 @@ import base64
from keylime import config, keylime_logging
from keylime.db.verifier_db import VerfierMain
-from keylime.models.base import Timestamp
+from keylime.models.base import Timestamp, db_manager
from keylime.models.verifier import AuthSession
-from keylime.models.verifier.auth_session import get_session_context
from keylime.web.base import Controller
logger = keylime_logging.init_logging("verifier")
@@ -186,7 +185,7 @@ class SessionController(Controller):
# Check if agent exists - this is where we validate enrollment
agent = None
- with get_session_context() as session:
+ with db_manager.session_context() as session:
agent = session.query(VerfierMain).filter(VerfierMain.agent_id == agent_id).one_or_none()
if agent:
session.expunge(agent) # type: ignore[no-untyped-call]
@@ -384,7 +383,7 @@ class SessionController(Controller):
# POST /v3[.:minor]/agents/:agent_id/session
def create(self, agent_id, **params):
agent = None
- with get_session_context() as session:
+ with db_manager.session_context() as session:
agent = session.query(VerfierMain).filter(VerfierMain.agent_id == agent_id).one_or_none()
if agent:
session.expunge(agent) # type: ignore[no-untyped-call]
@@ -410,7 +409,7 @@ class SessionController(Controller):
def update(self, agent_id, token, **params):
agent = None
- with get_session_context() as session:
+ with db_manager.session_context() as session:
agent = session.query(VerfierMain).filter(VerfierMain.agent_id == agent_id).one_or_none()
if agent:
session.expunge(agent) # type: ignore[no-untyped-call]
diff --git a/test/test_auth_session.py b/test/test_auth_session.py
index 2c78547..dd554b6 100644
--- a/test/test_auth_session.py
+++ b/test/test_auth_session.py
@@ -2,74 +2,15 @@
import base64
import unittest
-from contextlib import contextmanager
from datetime import timedelta
from unittest.mock import MagicMock, PropertyMock, patch
from keylime.crypto import generate_session_token, generate_token_salt, hash_token_for_storage
from keylime.models.base.types import Timestamp
-from keylime.models.verifier.auth_session import AuthSession, get_session_context
+from keylime.models.verifier.auth_session import AuthSession
from keylime.shared_data import cleanup_global_shared_memory, get_shared_memory
-class TestGetSessionContext(unittest.TestCase):
- """Test cases for get_session_context context manager."""
-
- def _make_mock_session_manager(self, mock_session):
- """Create a mock SessionManager whose session_context() mirrors real lifecycle."""
- mock_scoped = MagicMock()
- mock_session_manager = MagicMock()
- mock_session_manager.make_session.return_value = mock_session
- mock_session_manager._scoped_session = mock_scoped # pylint: disable=protected-access
-
- @contextmanager
- def fake_session_context(engine): # pylint: disable=unused-argument
- session = mock_session_manager.make_session(engine)
- try:
- yield session
- session.commit()
- except Exception:
- session.rollback()
- raise
- finally:
- scoped = mock_session_manager._scoped_session # pylint: disable=protected-access
- if scoped is not None:
- scoped.remove()
-
- mock_session_manager.session_context = fake_session_context
- return mock_session_manager, mock_scoped
-
- @patch("keylime.models.verifier.auth_session.make_engine")
- def test_session_cleanup_on_normal_exit(self, _mock_make_engine):
- """Test that session is committed and cleaned up when context manager exits normally."""
- mock_session = MagicMock()
- mock_session_manager, mock_scoped = self._make_mock_session_manager(mock_session)
-
- with patch("keylime.models.verifier.auth_session._engine", None):
- with patch("keylime.models.verifier.auth_session._session_manager", mock_session_manager):
- with get_session_context() as session:
- self.assertIs(session, mock_session)
-
- mock_session.commit.assert_called_once()
- mock_scoped.remove.assert_called_once()
-
- @patch("keylime.models.verifier.auth_session.make_engine")
- def test_session_rollback_on_exception(self, _mock_make_engine):
- """Test that session is rolled back and cleaned up when an exception occurs."""
- mock_session = MagicMock()
- mock_session_manager, mock_scoped = self._make_mock_session_manager(mock_session)
-
- with patch("keylime.models.verifier.auth_session._engine", None):
- with patch("keylime.models.verifier.auth_session._session_manager", mock_session_manager):
- with self.assertRaises(RuntimeError):
- with get_session_context():
- raise RuntimeError("simulated error")
-
- mock_session.rollback.assert_called_once()
- mock_session.commit.assert_not_called()
- mock_scoped.remove.assert_called_once()
-
-
class TestAuthSessionHelpers(unittest.TestCase):
"""Test cases for AuthSession helper methods."""
@@ -457,58 +398,6 @@ class TestAuthSessionCore(unittest.TestCase):
self.assertIn("errors", result)
self.assertIn("authentication_supported", result["errors"])
- @patch("keylime.models.verifier.auth_session.get_session_context")
- @patch.object(AuthSession, "get_by_token")
- def test_authenticate_agent_success(self, mock_get_by_token, mock_get_session):
- """Test successful agent authentication with valid token."""
- # Create a mock agent
- mock_agent = MagicMock()
- mock_agent.agent_id = self.test_agent_id
-
- # Mock session query
- mock_db_session = MagicMock()
- mock_db_session.query.return_value.filter.return_value.one_or_none.return_value = mock_agent
- mock_get_session.return_value.__enter__ = MagicMock(return_value=mock_db_session)
- mock_get_session.return_value.__exit__ = MagicMock(return_value=False)
-
- # Mock AuthSession.get_by_token to return an active session
- mock_auth_session = MagicMock()
- mock_auth_session.session_id = "550e8400-e29b-41d4-a716-446655440000"
- mock_auth_session.active = True
- mock_auth_session.agent_id = self.test_agent_id
- mock_auth_session.token_expires_at = Timestamp.now() + timedelta(hours=1)
- mock_get_by_token.return_value = mock_auth_session
-
- result = AuthSession.authenticate_agent("test-token")
-
- # Should return the agent
- self.assertIsNotNone(result)
- self.assertEqual(result.agent_id, self.test_agent_id) # type: ignore[union-attr]
-
- @patch.object(AuthSession, "get_by_token")
- def test_authenticate_agent_inactive_session(self, mock_get_by_token):
- """Test that inactive sessions cannot authenticate."""
- # Mock AuthSession.get_by_token to return an inactive session
- mock_auth_session = MagicMock()
- mock_auth_session.active = False
- mock_get_by_token.return_value = mock_auth_session
-
- result = AuthSession.authenticate_agent("test-token")
-
- # Should return False
- self.assertFalse(result)
-
- @patch.object(AuthSession, "get_by_token")
- def test_authenticate_agent_no_session(self, mock_get_by_token):
- """Test that authentication fails when session doesn't exist."""
- # Mock AuthSession.get_by_token to return None (no session found)
- mock_get_by_token.return_value = None
-
- result = AuthSession.authenticate_agent("test-token")
-
- # Should return False
- self.assertFalse(result)
-
@patch.object(AuthSession, "empty")
def test_create_with_agent(self, mock_empty):
"""Test AuthSession.create() with an enrolled agent."""
diff --git a/test/test_session_controller.py b/test/test_session_controller.py
index eec7fef..f8db8db 100644
--- a/test/test_session_controller.py
+++ b/test/test_session_controller.py
@@ -272,8 +272,8 @@ class TestSessionControllerUpdateSession(unittest.TestCase):
# Verify session was deleted from cache
self.assertNotIn(self.test_session_id, self.sessions_cache)
- @patch("keylime.web.verifier.session_controller.get_session_context")
- def test_update_session_agent_not_enrolled(self, mock_get_session):
+ @patch("keylime.web.verifier.session_controller.db_manager")
+ def test_update_session_agent_not_enrolled(self, mock_db_manager):
"""Test update_session with unenrolled agent."""
# Create session in cache
now = Timestamp.now()
@@ -290,8 +290,8 @@ class TestSessionControllerUpdateSession(unittest.TestCase):
# Mock database query to return no agent
mock_session = MagicMock()
mock_session.query.return_value.filter.return_value.one_or_none.return_value = None
- mock_get_session.return_value.__enter__ = MagicMock(return_value=mock_session)
- mock_get_session.return_value.__exit__ = MagicMock(return_value=False)
+ mock_db_manager.session_context.return_value.__enter__ = MagicMock(return_value=mock_session)
+ mock_db_manager.session_context.return_value.__exit__ = MagicMock(return_value=False)
# Call update_session
params = {
@@ -319,9 +319,9 @@ class TestSessionControllerUpdateSession(unittest.TestCase):
body = call_args[1]["body"]
self.assertEqual(body["data"]["attributes"]["evaluation"], "fail")
- @patch("keylime.web.verifier.session_controller.get_session_context")
+ @patch("keylime.web.verifier.session_controller.db_manager")
@patch("keylime.models.verifier.auth_session.AuthSession.create_from_memory")
- def test_update_session_authentication_failed(self, mock_create_from_memory, mock_get_session):
+ def test_update_session_authentication_failed(self, mock_create_from_memory, mock_db_manager):
"""Test update_session with failed authentication."""
# Create session in cache
now = Timestamp.now()
@@ -340,8 +340,8 @@ class TestSessionControllerUpdateSession(unittest.TestCase):
mock_agent.agent_id = self.test_agent_id
mock_session = MagicMock()
mock_session.query.return_value.filter.return_value.one_or_none.return_value = mock_agent
- mock_get_session.return_value.__enter__ = MagicMock(return_value=mock_session)
- mock_get_session.return_value.__exit__ = MagicMock(return_value=False)
+ mock_db_manager.session_context.return_value.__enter__ = MagicMock(return_value=mock_session)
+ mock_db_manager.session_context.return_value.__exit__ = MagicMock(return_value=False)
# Mock AuthSession.create_from_memory to return errors
mock_auth_session = MagicMock()
@@ -379,11 +379,11 @@ class TestSessionControllerUpdateSession(unittest.TestCase):
call_args = self.controller.send_response.call_args # type: ignore[attr-defined]
self.assertEqual(call_args[1]["code"], 401)
- @patch("keylime.web.verifier.session_controller.get_session_context")
+ @patch("keylime.web.verifier.session_controller.db_manager")
@patch("keylime.models.verifier.auth_session.AuthSession.create_from_memory")
@patch("keylime.models.verifier.auth_session.AuthSession.delete_active_session_for_agent")
@patch("keylime.web.verifier.session_controller.config")
- def test_update_session_success(self, mock_config, _mock_delete_active, mock_create_from_memory, mock_get_session):
+ def test_update_session_success(self, mock_config, _mock_delete_active, mock_create_from_memory, mock_db_manager):
"""Test successful session update."""
# Create session in cache
now = Timestamp.now()
@@ -405,8 +405,8 @@ class TestSessionControllerUpdateSession(unittest.TestCase):
mock_agent.agent_id = self.test_agent_id
mock_session = MagicMock()
mock_session.query.return_value.filter.return_value.one_or_none.return_value = mock_agent
- mock_get_session.return_value.__enter__ = MagicMock(return_value=mock_session)
- mock_get_session.return_value.__exit__ = MagicMock(return_value=False)
+ mock_db_manager.session_context.return_value.__enter__ = MagicMock(return_value=mock_session)
+ mock_db_manager.session_context.return_value.__exit__ = MagicMock(return_value=False)
# Mock config
mock_config.getboolean.return_value = False # Don't keep in memory
@@ -525,17 +525,17 @@ class TestSessionControllerLegacyEndpoints(unittest.TestCase):
self.assertEqual(call_args[0][0], 404)
@patch("keylime.models.verifier.auth_session.AuthSession.delete_stale")
- @patch("keylime.web.verifier.session_controller.get_session_context")
+ @patch("keylime.web.verifier.session_controller.db_manager")
@patch("keylime.models.verifier.auth_session.AuthSession.create")
- def test_create_success(self, mock_create, mock_get_session, _mock_delete_stale):
+ def test_create_success(self, mock_create, mock_db_manager, _mock_delete_stale):
"""Test successful create endpoint."""
# Mock database query
mock_agent = MagicMock()
mock_agent.agent_id = self.test_agent_id
mock_session = MagicMock()
mock_session.query.return_value.filter.return_value.one_or_none.return_value = mock_agent
- mock_get_session.return_value.__enter__ = MagicMock(return_value=mock_session)
- mock_get_session.return_value.__exit__ = MagicMock(return_value=False)
+ mock_db_manager.session_context.return_value.__enter__ = MagicMock(return_value=mock_session)
+ mock_db_manager.session_context.return_value.__exit__ = MagicMock(return_value=False)
# Mock AuthSession.create
mock_auth_session = MagicMock()
@@ -553,14 +553,14 @@ class TestSessionControllerLegacyEndpoints(unittest.TestCase):
call_args = self.controller.respond.call_args # type: ignore[attr-defined]
self.assertEqual(call_args[0][0], 200)
- @patch("keylime.web.verifier.session_controller.get_session_context")
- def test_create_agent_not_found(self, mock_get_session):
+ @patch("keylime.web.verifier.session_controller.db_manager")
+ def test_create_agent_not_found(self, mock_db_manager):
"""Test create endpoint with non-existent agent."""
# Mock database query to return None
mock_session = MagicMock()
mock_session.query.return_value.filter.return_value.one_or_none.return_value = None
- mock_get_session.return_value.__enter__ = MagicMock(return_value=mock_session)
- mock_get_session.return_value.__exit__ = MagicMock(return_value=False)
+ mock_db_manager.session_context.return_value.__enter__ = MagicMock(return_value=mock_session)
+ mock_db_manager.session_context.return_value.__exit__ = MagicMock(return_value=False)
# Call create
params = {"data": {}}
@@ -571,17 +571,17 @@ class TestSessionControllerLegacyEndpoints(unittest.TestCase):
call_args = self.controller.respond.call_args # type: ignore[attr-defined]
self.assertEqual(call_args[0][0], 404)
- @patch("keylime.web.verifier.session_controller.get_session_context")
+ @patch("keylime.web.verifier.session_controller.db_manager")
@patch("keylime.models.verifier.auth_session.AuthSession.get_by_token")
- def test_update_success(self, mock_get, mock_get_session):
+ def test_update_success(self, mock_get, mock_db_manager):
"""Test successful update endpoint."""
# Mock database query
mock_agent = MagicMock()
mock_agent.agent_id = self.test_agent_id
mock_session = MagicMock()
mock_session.query.return_value.filter.return_value.one_or_none.return_value = mock_agent
- mock_get_session.return_value.__enter__ = MagicMock(return_value=mock_session)
- mock_get_session.return_value.__exit__ = MagicMock(return_value=False)
+ mock_db_manager.session_context.return_value.__enter__ = MagicMock(return_value=mock_session)
+ mock_db_manager.session_context.return_value.__exit__ = MagicMock(return_value=False)
# Mock AuthSession.get_by_token
mock_auth_session = MagicMock()
@@ -601,9 +601,9 @@ class TestSessionControllerLegacyEndpoints(unittest.TestCase):
call_args = self.controller.respond.call_args # type: ignore[attr-defined]
self.assertEqual(call_args[0][0], 200)
- @patch("keylime.web.verifier.session_controller.get_session_context")
+ @patch("keylime.web.verifier.session_controller.db_manager")
@patch("keylime.models.verifier.auth_session.AuthSession.get_by_token")
- def test_update_not_found(self, mock_get, _mock_get_session):
+ def test_update_not_found(self, mock_get, _mock_db_manager):
"""Test update endpoint with non-existent session."""
# Mock AuthSession.get_by_token to return None
mock_get.return_value = None
--
2.53.0