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._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:

View File

@ -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