From 1a2ca59d16b69f5de59a0dbd01f9bed201a8f2df Mon Sep 17 00:00:00 2001 From: Ivo Oskamp Date: Thu, 15 Jan 2026 12:31:08 +0100 Subject: [PATCH] Auto-commit local changes before build (2026-01-15 12:31:08) --- .last-branch | 2 +- .../app/integrations/autotask/client.py | 89 ++++++++++++++----- docs/changelog.md | 9 ++ 3 files changed, 76 insertions(+), 24 deletions(-) diff --git a/.last-branch b/.last-branch index f8987a1..83d7747 100644 --- a/.last-branch +++ b/.last-branch @@ -1 +1 @@ -v20260115-05-autotask-queues-picklist-fix +v20260115-06-autotask-auth-fallback diff --git a/containers/backupchecks/src/backend/app/integrations/autotask/client.py b/containers/backupchecks/src/backend/app/integrations/autotask/client.py index b82983c..e45f4f5 100644 --- a/containers/backupchecks/src/backend/app/integrations/autotask/client.py +++ b/containers/backupchecks/src/backend/app/integrations/autotask/client.py @@ -33,32 +33,51 @@ class AutotaskClient: self.timeout_seconds = timeout_seconds self._zone_info: Optional[AutotaskZoneInfo] = None + self._zoneinfo_base_used: Optional[str] = None - def _zoneinfo_base(self) -> str: - # Production zone lookup endpoint: webservices.autotask.net - # Sandbox is typically pre-release: webservices2.autotask.net + def _zoneinfo_bases(self) -> List[str]: + """Return a list of zoneInformation base URLs to try. + + Autotask tenants can behave differently for Sandbox vs Production. + To keep connection testing reliable, we try the expected base first + and fall back to the alternative if needed. + """ + prod = "https://webservices.autotask.net/atservicesrest" + sb = "https://webservices2.autotask.net/atservicesrest" if self.environment == "sandbox": - return "https://webservices2.autotask.net/atservicesrest" - return "https://webservices.autotask.net/atservicesrest" + return [sb, prod] + return [prod, sb] def get_zone_info(self) -> AutotaskZoneInfo: if self._zone_info is not None: return self._zone_info - url = f"{self._zoneinfo_base().rstrip('/')}/v1.0/zoneInformation" - params = {"user": self.username} - try: - resp = requests.get(url, params=params, timeout=self.timeout_seconds) - except Exception as exc: - raise AutotaskError(f"ZoneInformation request failed: {exc}") from exc + last_error: Optional[str] = None + data: Optional[Dict[str, Any]] = None + for base in self._zoneinfo_bases(): + url = f"{base.rstrip('/')}/v1.0/zoneInformation" + params = {"user": self.username} + try: + resp = requests.get(url, params=params, timeout=self.timeout_seconds) + except Exception as exc: + last_error = f"ZoneInformation request failed for {base}: {exc}" + continue - if resp.status_code >= 400: - raise AutotaskError(f"ZoneInformation request failed (HTTP {resp.status_code}).") + if resp.status_code >= 400: + last_error = f"ZoneInformation request failed for {base} (HTTP {resp.status_code})." + continue - try: - data = resp.json() - except Exception as exc: - raise AutotaskError("ZoneInformation response is not valid JSON.") from exc + try: + data = resp.json() + except Exception: + last_error = f"ZoneInformation response from {base} is not valid JSON." + continue + + self._zoneinfo_base_used = base + break + + if data is None: + raise AutotaskError(last_error or "ZoneInformation request failed.") zone = AutotaskZoneInfo( zone_name=str(data.get("zoneName") or ""), @@ -74,8 +93,11 @@ class AutotaskClient: return zone def _headers(self) -> Dict[str, str]: - # Autotask REST API requires the APIIntegrationcode header for API-only users. + # Autotask REST API requires the ApiIntegrationCode header. + # Some tenants/proxies appear picky despite headers being case-insensitive, + # so we include both common casings for maximum compatibility. return { + "ApiIntegrationCode": self.api_integration_code, "APIIntegrationcode": self.api_integration_code, "Content-Type": "application/json", "Accept": "application/json", @@ -85,20 +107,41 @@ class AutotaskClient: zone = self.get_zone_info() base = zone.api_url.rstrip("/") url = f"{base}/v1.0/{path.lstrip('/')}" - try: - resp = requests.request( + headers = self._headers() + + def do_request(use_basic_auth: bool, extra_headers: Optional[Dict[str, str]] = None): + h = dict(headers) + if extra_headers: + h.update(extra_headers) + return requests.request( method=method.upper(), url=url, - headers=self._headers(), + headers=h, params=params or None, - auth=(self.username, self.password), + auth=(self.username, self.password) if use_basic_auth else None, timeout=self.timeout_seconds, ) + + try: + # Primary auth method: HTTP Basic (username + API secret) + resp = do_request(use_basic_auth=True) + + # Compatibility fallback: some environments accept credentials only via headers. + if resp.status_code == 401: + resp = do_request( + use_basic_auth=False, + extra_headers={"UserName": self.username, "Secret": self.password}, + ) except Exception as exc: raise AutotaskError(f"Request failed: {exc}") from exc if resp.status_code == 401: - raise AutotaskError("Authentication failed (HTTP 401). Check username/password and APIIntegrationcode.") + zi_base = self._zoneinfo_base_used or "unknown" + raise AutotaskError( + "Authentication failed (HTTP 401). " + "Verify API Username, API Secret, and ApiIntegrationCode. " + f"Environment={self.environment}, ZoneInfoBase={zi_base}, ZoneApiUrl={zone.api_url}." + ) if resp.status_code == 403: raise AutotaskError("Access forbidden (HTTP 403). API user permissions may be insufficient.") if resp.status_code == 404: diff --git a/docs/changelog.md b/docs/changelog.md index 161d863..cd4b606 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -46,6 +46,15 @@ Changes: - Normalized picklist values so IDs and display labels are handled consistently in settings dropdowns. - Fixed Autotask connection test to rely on picklist availability, preventing false 404 errors. +## v20260115-06-autotask-auth-fallback + +### Changes: +- Improved Autotask authentication handling to support sandbox-specific behavior. +- Implemented automatic fallback authentication flow when initial Basic Auth returns HTTP 401. +- Added support for header-based authentication using UserName and Secret headers alongside the Integration Code. +- Extended authentication error diagnostics to include selected environment and resolved Autotask zone information. +- Increased reliability of Autotask connection testing across different tenants and sandbox configurations. + *** ## v0.1.21