fix(tempo): fix EU base URL, trailing space in account ID, and tempo_synced tracking
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled

Root causes found for Tempo worklogs never reaching Tempo:
1. Wrong API region: workspace is on api.eu.tempo.io/4 but code used api.tempo.io/4
   → Tempo returned "User is invalid" (400) for all POST /worklogs
2. Trailing space in jira_account_id stored in DB (now stripped with .strip())
3. tempo_synced field was never updated even on success (now set from Tempo response)

Fix: add tempo.base_url system_config key (admin-configurable without redeploy),
fall back to TEMPO_BASE_URL env-var, then global default. DB already updated with
https://api.eu.tempo.io/4 for this workspace.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
kitos
2026-05-28 12:48:22 +02:00
parent 0830b36cd6
commit 2ee74bf6c9
3 changed files with 48 additions and 5 deletions

View File

@@ -63,6 +63,9 @@ class Settings(BaseSettings):
TEMPO_API_TOKEN: str = "" TEMPO_API_TOKEN: str = ""
TEMPO_API_VERSION: int = 4 TEMPO_API_VERSION: int = 4
TEMPO_DEFAULT_WORK_TYPE: str = "Red Team" TEMPO_DEFAULT_WORK_TYPE: str = "Red Team"
# Tempo API base URL — use https://api.eu.tempo.io/4 for EU workspaces.
# Can also be set via system_configs key "tempo.base_url" at runtime.
TEMPO_BASE_URL: str = "" # empty → falls back to https://api.tempo.io/4
# ── OSINT / Intelligence ──────────────────────────────────────── # ── OSINT / Intelligence ────────────────────────────────────────
NVD_API_KEY: str = "" # optional; increases NVD rate limit from 5/30s to 50/30s NVD_API_KEY: str = "" # optional; increases NVD rate limit from 5/30s to 50/30s

View File

@@ -40,9 +40,41 @@ def has_tempo_configured(user) -> bool:
return bool(getattr(user, "tempo_api_token", None)) return bool(getattr(user, "tempo_api_token", None))
def get_user_tempo_client(user): _TEMPO_DEFAULT_BASE_URL = "https://api.tempo.io/4"
_TEMPO_EU_BASE_URL = "https://api.eu.tempo.io/4"
# system_configs key for admin-configurable Tempo base URL
_TEMPO_BASE_URL_CONFIG_KEY = "tempo.base_url"
def _get_tempo_base_url(db=None) -> str:
"""Return the Tempo API base URL.
Reads ``tempo.base_url`` from ``system_configs`` first (allows the admin
to set the EU endpoint ``https://api.eu.tempo.io/4`` without redeploying).
Falls back to ``settings.TEMPO_BASE_URL`` env-var, then the global default.
"""
if db is not None:
try:
from app.models.system_config import SystemConfig
row = db.query(SystemConfig).filter(
SystemConfig.key == _TEMPO_BASE_URL_CONFIG_KEY
).first()
if row and row.value:
return row.value.rstrip("/")
except Exception:
pass # DB unavailable — fall through to defaults
env_url = getattr(settings, "TEMPO_BASE_URL", None)
return (env_url or _TEMPO_DEFAULT_BASE_URL).rstrip("/")
def get_user_tempo_client(user, db=None):
"""Return a Tempo API v4 client authenticated as *user*. """Return a Tempo API v4 client authenticated as *user*.
Pass ``db`` to allow reading the admin-configured base URL from
``system_configs`` (needed for EU Tempo workspaces).
Raises ``InvalidOperationError`` when the user has no token or the Raises ``InvalidOperationError`` when the user has no token or the
client library is not installed. client library is not installed.
""" """
@@ -54,7 +86,9 @@ def get_user_tempo_client(user):
) )
try: try:
from tempoapiclient import client_v4 as tempo_client from tempoapiclient import client_v4 as tempo_client
return tempo_client.Tempo(auth_token=token) base_url = _get_tempo_base_url(db)
logger.debug("Using Tempo base URL: %s", base_url)
return tempo_client.Tempo(auth_token=token, base_url=base_url)
except ImportError: except ImportError:
raise InvalidOperationError( raise InvalidOperationError(
"tempo-api-python-client is not installed. " "tempo-api-python-client is not installed. "
@@ -69,13 +103,14 @@ def log_worklog(
date: str, date: str,
time_spent_seconds: int, time_spent_seconds: int,
description: str, description: str,
db=None,
) -> dict: ) -> dict:
"""Create a worklog entry in Tempo using *user*'s personal token. """Create a worklog entry in Tempo using *user*'s personal token.
Note: tempoapiclient raises SystemExit (not Exception) on API errors, so Note: tempoapiclient raises SystemExit (not Exception) on API errors, so
we intercept BaseException and re-raise as RuntimeError to keep it non-fatal. we intercept BaseException and re-raise as RuntimeError to keep it non-fatal.
""" """
tempo = get_user_tempo_client(user) tempo = get_user_tempo_client(user, db=db)
try: try:
return tempo.create_worklog( return tempo.create_worklog(
accountId=author_account_id, accountId=author_account_id,
@@ -151,7 +186,7 @@ def auto_log_test_worklog(
logger.debug("No Jira link for test %s, skipping Tempo worklog", test.id) logger.debug("No Jira link for test %s, skipping Tempo worklog", test.id)
return None return None
jira_account_id = getattr(user, "jira_account_id", "") or "" jira_account_id = (getattr(user, "jira_account_id", "") or "").strip()
if not jira_account_id: if not jira_account_id:
logger.debug( logger.debug(
"User %s has no jira_account_id; skipping Tempo worklog", "User %s has no jira_account_id; skipping Tempo worklog",
@@ -181,6 +216,7 @@ def auto_log_test_worklog(
date=work_date, date=work_date,
time_spent_seconds=duration_seconds, time_spent_seconds=duration_seconds,
description=description, description=description,
db=db,
) )
logger.info( logger.info(
"Tempo worklog created for test %s by user %s: %ds on %s", "Tempo worklog created for test %s by user %s: %ds on %s",

View File

@@ -368,7 +368,11 @@ def _create_phase_worklog(
# duration so the Tempo entry is identical to the Aegis worklog. # duration so the Tempo entry is identical to the Aegis worklog.
try: try:
from app.services.tempo_service import auto_log_test_worklog from app.services.tempo_service import auto_log_test_worklog
auto_log_test_worklog(db, test, user, activity_type, duration_seconds) tempo_result = auto_log_test_worklog(db, test, user, activity_type, duration_seconds)
if tempo_result and isinstance(tempo_result, dict):
wl.tempo_synced = datetime.utcnow()
wl.tempo_worklog_id = str(tempo_result.get("tempoWorklogId", ""))
db.flush()
except Exception as e: except Exception as e:
logger.warning("Tempo sync failed for worklog: %s", e, exc_info=True) logger.warning("Tempo sync failed for worklog: %s", e, exc_info=True)