fix(ovhcloud): treat the billing-account list as advice, never as a verdict (1.2.6)
The listing now answers 200 with an empty array rather than erroring, and the probe failed on "not one of the accounts these credentials can see" — the same mistake as 1.2.4, in its other shape: a diagnostic step deciding the outcome of the test it exists to explain. OVH's IAM filters what you may SEE rather than refusing you, so an empty list is how it says no — and a policy can grant a resource without granting the route that enumerates it, which makes "not in the list" weak evidence either way. Only the account-scoped routes decide now. When nothing is reachable anywhere, the message names the IAM policy as the cause and says what to grant, because an empty array is the least self-explanatory refusal this API produces. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TN1R4EnNFUV2Xwtd9q98tj
This commit is contained in:
@@ -238,7 +238,7 @@ def _queue_id(cfg):
|
||||
# It also answers its own VERSION. When a test fails identically twice, the first
|
||||
# question is whether the new code is the code that ran, and nothing on the screen
|
||||
# could answer it.
|
||||
VERSION = "1.2.5"
|
||||
VERSION = "1.2.6"
|
||||
|
||||
|
||||
def _try(step, fn):
|
||||
@@ -273,15 +273,9 @@ def _walk(cfg, inputs):
|
||||
# failure, which is exactly what it did here.
|
||||
accounts, err = _try("listing telephony billing accounts", lambda: request("GET", "/telephony", cfg))
|
||||
if err:
|
||||
out["billing_accounts"] = "could not be listed (" + err + ") — not fatal, carrying on"
|
||||
out["billing_accounts"] = "could not be listed (" + err + ")"
|
||||
else:
|
||||
accounts = [str(a) for a in (accounts or [])]
|
||||
out["billing_accounts"] = accounts
|
||||
if ba not in accounts:
|
||||
out.update({"ok": False, "step": "billing account",
|
||||
"detail": "%s is not one of the accounts these credentials can see: [%s]"
|
||||
% (ba, ", ".join(accounts) or "none")})
|
||||
return out
|
||||
out["billing_accounts"] = [str(a) for a in (accounts or [])]
|
||||
|
||||
# 2. Which product holds the queue, and under what name. Both are listed even on
|
||||
# success: seeing the OTHER product empty is what tells somebody their number
|
||||
@@ -300,7 +294,21 @@ def _walk(cfg, inputs):
|
||||
if product is None:
|
||||
listed = [n for p in _PRODUCTS for n in (services[p] if isinstance(services[p], list) else [])]
|
||||
failures = [services[p] for p in _PRODUCTS if not isinstance(services[p], list)]
|
||||
if not listed and failures:
|
||||
seen = out.get("billing_accounts")
|
||||
if isinstance(seen, list) and ba not in seen and not listed:
|
||||
# The IAM policy is the likeliest answer, and an empty LIST is how OVH's
|
||||
# IAM says no: it filters what you may see rather than refusing you. Named
|
||||
# here rather than at the listing step, because an empty list on its own
|
||||
# is not proof — a policy can grant a resource without granting the route
|
||||
# that enumerates it, which is why that check is advisory and this one is
|
||||
# not.
|
||||
detail = ("these credentials can see no telephony billing account at all, and %s is not "
|
||||
"reachable. Grant this service account the telephony resources in the control "
|
||||
"panel's Identity and Access Management: a policy on the account %s (or on the "
|
||||
"telephony resource group) with the telephony actions. OVH's IAM answers an empty "
|
||||
"list rather than a refusal, which is why this reads as \"none\" rather than as a 403. "
|
||||
"Details: %s") % (ba, ba, " | ".join(failures) or "both product listings came back empty")
|
||||
elif not listed and failures:
|
||||
# Neither product could even be LISTED. That is not a wrong service name,
|
||||
# it is the account or the credentials — and the three shapes it comes in
|
||||
# want three different answers, so they are named rather than merged.
|
||||
|
||||
Reference in New Issue
Block a user