fix: redact secret diagnostics

This commit is contained in:
Christian Krakau-Louis
2026-05-22 22:00:28 +02:00
parent 4d1ed329ec
commit eeff24fa5c
10 changed files with 125 additions and 15 deletions
+73 -4
View File
@@ -4,6 +4,7 @@ Tests for MailSource model and mail-sources API endpoints.
import asyncio
import json
import logging
from datetime import datetime
from unittest.mock import MagicMock, patch
from urllib.parse import parse_qs, urlparse
@@ -138,6 +139,34 @@ class TestMailSourceImportModel:
assert len(details[0]["filename"]) == 300
assert "ignored" not in details[0]
def test_record_import_attempt_redacts_sensitive_values(self, db_session: Session):
source = MailSource(name="Secret Sanitizer", method="GMAIL_API")
db_session.add(source)
db_session.commit()
db_session.refresh(source)
attempt = record_import_attempt(
db_session,
source,
{
"success": False,
"errors": ["provider returned access_token=ya29.raw-token"],
"details": [
{
"status": "failed",
"error": 'oauth failed: {"client_secret":"GOCSPX-raw-secret"}',
},
],
},
started_at=datetime.utcnow(),
trigger="manual",
)
assert "ya29.raw-token" not in attempt.errors
assert "GOCSPX-raw-secret" not in attempt.details
assert "**redacted**" in attempt.errors
assert "**redacted**" in attempt.details
class TestImportHistoryDecoding:
"""Unit tests for import-history JSON decoding helpers."""
@@ -1058,6 +1087,25 @@ class TestSanitizeForLog:
assert _sanitize_for_log("example.com") == "example.com"
def test_redacts_sensitive_key_values(self):
from app.api.api_v1.endpoints.mail_sources import _redact_sensitive_text
text = _redact_sensitive_text(
'Token exchange failed: access_token=ya29.secret client_secret="GOCSPX-secret"'
)
assert "ya29.secret" not in text
assert "GOCSPX-secret" not in text
assert text.count("**redacted**") == 2
def test_redacts_bearer_tokens(self):
from app.api.api_v1.endpoints.mail_sources import _redact_sensitive_text
text = _redact_sensitive_text("Authorization failed for Bearer ya29.long-secret-token")
assert "ya29.long-secret-token" not in text
assert "Bearer **redacted**" in text
# ---------------------------------------------------------------------------
# Source-to-response helper (password masking)
@@ -1164,7 +1212,9 @@ class TestGmailCallbackGet:
resp = authed_client.get(f"/api/v1/mail-sources/{source_id}/gmail/callback?code=xyz")
assert resp.status_code == 404
def test_callback_token_exchange_error_returns_html_400(self, authed_client: TestClient):
def test_callback_token_exchange_error_returns_html_400(
self, authed_client: TestClient, caplog
):
"""Token exchange raises ValueError return a user-facing HTML error page."""
create_resp = authed_client.post(
"/api/v1/mail-sources",
@@ -1172,14 +1222,20 @@ class TestGmailCallbackGet:
)
source_id = create_resp.json()["id"]
caplog.set_level(logging.ERROR, logger="app.api.api_v1.endpoints.mail_sources")
with patch(
"app.api.api_v1.endpoints.mail_sources.GmailClient.exchange_code_for_tokens",
side_effect=ValueError("bad token"),
side_effect=ValueError(
"bad token access_token=ya29.raw-token client_secret=GOCSPX-raw-secret"
),
):
resp = authed_client.get(f"/api/v1/mail-sources/{source_id}/gmail/callback?code=abc")
assert resp.status_code == 400
assert "token exchange failed" in resp.text.lower() or "failed" in resp.text.lower()
assert "ya29.raw-token" not in caplog.text
assert "GOCSPX-raw-secret" not in caplog.text
assert "**redacted**" in caplog.text
def test_callback_no_access_token_returns_html_400(self, authed_client: TestClient):
"""Exchange succeeds but Google returns no access token HTML 400."""
@@ -1270,16 +1326,21 @@ class TestGmailCallbackPost:
)
assert resp.status_code == 400
def test_post_callback_token_exchange_error_returns_400(self, authed_client: TestClient):
def test_post_callback_token_exchange_error_returns_400(
self, authed_client: TestClient, caplog
):
create_resp = authed_client.post(
"/api/v1/mail-sources",
json={"name": "Post TokenErr", "method": "GMAIL_API"},
)
source_id = create_resp.json()["id"]
caplog.set_level(logging.ERROR, logger="app.api.api_v1.endpoints.mail_sources")
with patch(
"app.api.api_v1.endpoints.mail_sources.GmailClient.exchange_code_for_tokens",
side_effect=ValueError("bad exchange"),
side_effect=ValueError(
"bad exchange refresh_token=1//raw-refresh client_secret=GOCSPX-raw-secret"
),
):
resp = authed_client.post(
f"/api/v1/mail-sources/{source_id}/gmail/callback",
@@ -1287,6 +1348,14 @@ class TestGmailCallbackPost:
)
assert resp.status_code == 400
assert resp.json()["detail"] == (
"Token exchange failed. Please check the Gmail connection settings and try again."
)
assert "1//raw-refresh" not in resp.text
assert "GOCSPX-raw-secret" not in resp.text
assert "1//raw-refresh" not in caplog.text
assert "GOCSPX-raw-secret" not in caplog.text
assert "**redacted**" in caplog.text
def test_post_callback_no_access_token_returns_400(self, authed_client: TestClient):
create_resp = authed_client.post(