Auto-commit local changes before build (2026-01-15 12:31:08)

This commit is contained in:
Ivo Oskamp 2026-01-15 12:31:08 +01:00
parent 83d487a206
commit 1a2ca59d16
3 changed files with 76 additions and 24 deletions

View File

@ -1 +1 @@
v20260115-05-autotask-queues-picklist-fix v20260115-06-autotask-auth-fallback

View File

@ -33,32 +33,51 @@ class AutotaskClient:
self.timeout_seconds = timeout_seconds self.timeout_seconds = timeout_seconds
self._zone_info: Optional[AutotaskZoneInfo] = None self._zone_info: Optional[AutotaskZoneInfo] = None
self._zoneinfo_base_used: Optional[str] = None
def _zoneinfo_base(self) -> str: def _zoneinfo_bases(self) -> List[str]:
# Production zone lookup endpoint: webservices.autotask.net """Return a list of zoneInformation base URLs to try.
# Sandbox is typically pre-release: webservices2.autotask.net
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": if self.environment == "sandbox":
return "https://webservices2.autotask.net/atservicesrest" return [sb, prod]
return "https://webservices.autotask.net/atservicesrest" return [prod, sb]
def get_zone_info(self) -> AutotaskZoneInfo: def get_zone_info(self) -> AutotaskZoneInfo:
if self._zone_info is not None: if self._zone_info is not None:
return self._zone_info return self._zone_info
url = f"{self._zoneinfo_base().rstrip('/')}/v1.0/zoneInformation" 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} params = {"user": self.username}
try: try:
resp = requests.get(url, params=params, timeout=self.timeout_seconds) resp = requests.get(url, params=params, timeout=self.timeout_seconds)
except Exception as exc: except Exception as exc:
raise AutotaskError(f"ZoneInformation request failed: {exc}") from exc last_error = f"ZoneInformation request failed for {base}: {exc}"
continue
if resp.status_code >= 400: if resp.status_code >= 400:
raise AutotaskError(f"ZoneInformation request failed (HTTP {resp.status_code}).") last_error = f"ZoneInformation request failed for {base} (HTTP {resp.status_code})."
continue
try: try:
data = resp.json() data = resp.json()
except Exception as exc: except Exception:
raise AutotaskError("ZoneInformation response is not valid JSON.") from exc 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 = AutotaskZoneInfo(
zone_name=str(data.get("zoneName") or ""), zone_name=str(data.get("zoneName") or ""),
@ -74,8 +93,11 @@ class AutotaskClient:
return zone return zone
def _headers(self) -> Dict[str, str]: 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 { return {
"ApiIntegrationCode": self.api_integration_code,
"APIIntegrationcode": self.api_integration_code, "APIIntegrationcode": self.api_integration_code,
"Content-Type": "application/json", "Content-Type": "application/json",
"Accept": "application/json", "Accept": "application/json",
@ -85,20 +107,41 @@ class AutotaskClient:
zone = self.get_zone_info() zone = self.get_zone_info()
base = zone.api_url.rstrip("/") base = zone.api_url.rstrip("/")
url = f"{base}/v1.0/{path.lstrip('/')}" url = f"{base}/v1.0/{path.lstrip('/')}"
try: headers = self._headers()
resp = requests.request(
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(), method=method.upper(),
url=url, url=url,
headers=self._headers(), headers=h,
params=params or None, params=params or None,
auth=(self.username, self.password), auth=(self.username, self.password) if use_basic_auth else None,
timeout=self.timeout_seconds, 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: except Exception as exc:
raise AutotaskError(f"Request failed: {exc}") from exc raise AutotaskError(f"Request failed: {exc}") from exc
if resp.status_code == 401: 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: if resp.status_code == 403:
raise AutotaskError("Access forbidden (HTTP 403). API user permissions may be insufficient.") raise AutotaskError("Access forbidden (HTTP 403). API user permissions may be insufficient.")
if resp.status_code == 404: if resp.status_code == 404:

View File

@ -46,6 +46,15 @@ Changes:
- Normalized picklist values so IDs and display labels are handled consistently in settings dropdowns. - 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. - 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 ## v0.1.21