fed8b2eed7
Backend release / release (push) Waiting to run
Bandit Security Scan / bandit_scan (push) Waiting to run
Build and push multi-arch DocsGPT Docker image / build (linux/amd64, ubuntu-latest, amd64) (push) Waiting to run
Build and push multi-arch DocsGPT Docker image / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Waiting to run
Build and push multi-arch DocsGPT Docker image / manifest (push) Blocked by required conditions
Build and push DocsGPT FE Docker image for development / build (linux/amd64, ubuntu-latest, amd64) (push) Waiting to run
Build and push DocsGPT FE Docker image for development / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Waiting to run
Build and push DocsGPT FE Docker image for development / manifest (push) Blocked by required conditions
Python linting / ruff (push) Waiting to run
Run python tests with pytest / Run tests and count coverage (3.12) (push) Waiting to run
React Widget Build / build (push) Waiting to run
145 lines
4.9 KiB
Python
145 lines
4.9 KiB
Python
"""Regression tests: connector auth must not leak session tokens.
|
|
|
|
Three connector auth modules previously interpolated the raw session
|
|
token into ``ValueError`` messages. With ``exc_info=True`` on upstream
|
|
loggers those messages land in ``stack_logs`` (Postgres) and Sentry.
|
|
These tests pin the behaviour so the raw token never reappears in the
|
|
raised exception's ``str()`` representation, while a stable, short
|
|
SHA-256 fingerprint is present for correlation.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any, Dict, Optional
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from application.parser.connectors._auth_utils import session_token_fingerprint
|
|
|
|
|
|
SECRET_TOKEN = "super-secret-session-token-ABCDEF1234567890"
|
|
|
|
|
|
class _FakeRepo:
|
|
"""Fake ``ConnectorSessionsRepository`` returning a preset session."""
|
|
|
|
_session: Optional[Dict[str, Any]] = None
|
|
|
|
def __init__(self, conn: Any) -> None:
|
|
self.conn = conn
|
|
|
|
def get_by_session_token(self, session_token: str) -> Optional[Dict[str, Any]]:
|
|
return self._session
|
|
|
|
|
|
class _FakeReadonlyCtx:
|
|
"""Fake ``db_readonly`` context manager yielding a dummy connection."""
|
|
|
|
def __enter__(self) -> MagicMock:
|
|
return MagicMock()
|
|
|
|
def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> bool:
|
|
return False
|
|
|
|
|
|
def _patches(session_return: Optional[Dict[str, Any]]):
|
|
fake_repo_cls = type(
|
|
"FakeRepo",
|
|
(_FakeRepo,),
|
|
{"_session": session_return},
|
|
)
|
|
return (
|
|
patch(
|
|
"application.storage.db.repositories.connector_sessions."
|
|
"ConnectorSessionsRepository",
|
|
fake_repo_cls,
|
|
),
|
|
patch(
|
|
"application.storage.db.session.db_readonly",
|
|
lambda: _FakeReadonlyCtx(),
|
|
),
|
|
)
|
|
|
|
|
|
class TestSessionTokenFingerprint:
|
|
"""Unit tests for the shared fingerprint helper."""
|
|
|
|
@pytest.mark.unit
|
|
def test_fingerprint_is_stable(self) -> None:
|
|
assert session_token_fingerprint("abc") == session_token_fingerprint("abc")
|
|
|
|
@pytest.mark.unit
|
|
def test_fingerprint_does_not_contain_token(self) -> None:
|
|
fp = session_token_fingerprint(SECRET_TOKEN)
|
|
assert SECRET_TOKEN not in fp
|
|
assert fp.startswith("sha256:")
|
|
# 6 hex chars after the prefix.
|
|
assert len(fp) == len("sha256:") + 6
|
|
|
|
@pytest.mark.unit
|
|
def test_empty_token_has_sentinel(self) -> None:
|
|
assert session_token_fingerprint("") == "sha256:<empty>"
|
|
# type-check intentionally ignored: defensive for None.
|
|
assert session_token_fingerprint(None) == "sha256:<empty>" # type: ignore[arg-type]
|
|
|
|
@pytest.mark.unit
|
|
def test_different_tokens_produce_different_fingerprints(self) -> None:
|
|
assert session_token_fingerprint("a") != session_token_fingerprint("b")
|
|
|
|
|
|
class TestConfluenceAuthDoesNotLeakToken:
|
|
|
|
@pytest.mark.unit
|
|
def test_invalid_session_does_not_interpolate_token(self) -> None:
|
|
from application.parser.connectors.confluence.auth import ConfluenceAuth
|
|
|
|
auth = ConfluenceAuth.__new__(ConfluenceAuth)
|
|
repo_patch, ctx_patch = _patches(None)
|
|
with repo_patch, ctx_patch:
|
|
with pytest.raises(ValueError) as excinfo:
|
|
auth.get_token_info_from_session(SECRET_TOKEN)
|
|
|
|
message = str(excinfo.value)
|
|
assert SECRET_TOKEN not in message
|
|
assert session_token_fingerprint(SECRET_TOKEN) in message
|
|
|
|
|
|
class TestGoogleDriveAuthDoesNotLeakToken:
|
|
|
|
@pytest.mark.unit
|
|
def test_invalid_session_does_not_interpolate_token(self) -> None:
|
|
from application.parser.connectors.google_drive.auth import GoogleDriveAuth
|
|
|
|
auth = GoogleDriveAuth.__new__(GoogleDriveAuth)
|
|
repo_patch, ctx_patch = _patches(None)
|
|
with repo_patch, ctx_patch:
|
|
with pytest.raises(ValueError) as excinfo:
|
|
auth.get_token_info_from_session(SECRET_TOKEN)
|
|
|
|
# The Google Drive module wraps the inner ValueError in a broad
|
|
# ``except Exception as e: raise ValueError(... {str(e)})`` block,
|
|
# so the outer message still carries the fingerprint from the
|
|
# inner raise but must never carry the raw token.
|
|
message = str(excinfo.value)
|
|
assert SECRET_TOKEN not in message
|
|
assert session_token_fingerprint(SECRET_TOKEN) in message
|
|
|
|
|
|
class TestSharePointAuthDoesNotLeakToken:
|
|
|
|
@pytest.mark.unit
|
|
def test_invalid_session_does_not_interpolate_token(self) -> None:
|
|
from application.parser.connectors.share_point.auth import SharePointAuth
|
|
|
|
auth = SharePointAuth.__new__(SharePointAuth)
|
|
repo_patch, ctx_patch = _patches(None)
|
|
with repo_patch, ctx_patch:
|
|
with pytest.raises(ValueError) as excinfo:
|
|
auth.get_token_info_from_session(SECRET_TOKEN)
|
|
|
|
# SharePoint also wraps the inner ValueError. Same invariants.
|
|
message = str(excinfo.value)
|
|
assert SECRET_TOKEN not in message
|
|
assert session_token_fingerprint(SECRET_TOKEN) in message
|