Auto-commit local changes before build (2026-01-15 12:31:08)
This commit is contained in:
parent
83d487a206
commit
1a2ca59d16
@ -1 +1 @@
|
|||||||
v20260115-05-autotask-queues-picklist-fix
|
v20260115-06-autotask-auth-fallback
|
||||||
|
|||||||
@ -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
|
||||||
params = {"user": self.username}
|
data: Optional[Dict[str, Any]] = None
|
||||||
try:
|
for base in self._zoneinfo_bases():
|
||||||
resp = requests.get(url, params=params, timeout=self.timeout_seconds)
|
url = f"{base.rstrip('/')}/v1.0/zoneInformation"
|
||||||
except Exception as exc:
|
params = {"user": self.username}
|
||||||
raise AutotaskError(f"ZoneInformation request failed: {exc}") from exc
|
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:
|
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:
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user