diff --git a/backend/app/routers/admin_config.py b/backend/app/routers/admin_config.py index d009ec5..7166ac7 100644 --- a/backend/app/routers/admin_config.py +++ b/backend/app/routers/admin_config.py @@ -32,9 +32,17 @@ from app.models.webhook_config import WebhookConfig router = APIRouter(prefix="/admin", tags=["admin"]) -# Keys whose values contain secrets and must be redacted in the export +# Keys whose values contain secrets and must be redacted in the export. +# Must be kept in sync with the actual SystemConfig keys written by +# routers/system.py's _JIRA_KEYS map and the SMTP settings section — +# a stale/mismatched name here silently exports a live credential in +# plaintext instead of redacting it. _REDACTED_KEYS = { "smtp.password", + "jira.admin_api_token", + "tempo.admin_token", + # Older/alternate key names kept defensively in case a prior schema + # version wrote under these instead. "jira.api_token", "jira.password", "tempo.api_token", diff --git a/backend/tests/test_admin_config_export_import.py b/backend/tests/test_admin_config_export_import.py index fae6fb1..9a800a4 100644 --- a/backend/tests/test_admin_config_export_import.py +++ b/backend/tests/test_admin_config_export_import.py @@ -46,6 +46,23 @@ def test_export_config_redacts_smtp_password(client, db, auth_headers): assert values["smtp.host"] == "smtp.example.com" +def test_export_config_redacts_live_jira_and_tempo_admin_tokens(client, db, auth_headers): + """Regression test: the redaction key list previously named + jira.api_token/jira.password/tempo.api_token, which don't match the + actual keys routers/system.py writes (jira.admin_api_token, + tempo.admin_token) — so the live admin Jira/Tempo credentials were + exported in plaintext instead of being redacted.""" + db.add(SystemConfig(key="jira.admin_api_token", value="ATATT-real-jira-token")) + db.add(SystemConfig(key="tempo.admin_token", value="real-tempo-token")) + db.commit() + + body = client.get("/api/v1/admin/export-config", headers=auth_headers).json() + values = {r["key"]: r["value"] for r in body["system_configs"]} + + assert values["jira.admin_api_token"] == "[REDACTED]" + assert values["tempo.admin_token"] == "[REDACTED]" + + def test_export_config_never_exports_sso_private_key(client, db, auth_headers): db.add(SsoConfig(is_enabled=True, provider_name="Okta", sp_private_key="super-secret-key")) db.commit()