[ISSUE] add_timeout_rule_v2 documents an action keyword but hard-codes the action and silently discards what the caller passed
Environment
|
|
| SDK version |
zscaler-sdk-python 1.9.44 (editable install of master @ e7f5f7ef) |
| Python |
3.11.15 |
| OS |
macOS (arm64) |
| Affected product / resource |
ZPA / policies (add_timeout_rule_v2) |
Found by reading the SDK source and confirmed with the offline reproduction below (a stub
request executor, so no credentials and no network are involved). All identifiers used are
synthetic.
Description
PolicySetControllerAPI.add_timeout_rule_v2 documents action as a supported keyword
argument (zscaler/zpa/policies.py:1858-1860):
Keyword Args:
...
action (str):
The action for the policy. Accepted values are:
| ``RE_AUTH``
The signature is add_timeout_rule_v2(self, name: str, **kwargs)
(zscaler/zpa/policies.py:1832), so action= is accepted without complaint. But the payload
hard-codes it (zscaler/zpa/policies.py:1928):
payload = {
"name": name,
"description": kwargs.get("description"),
"custom_msg": kwargs.get("custom_msg"),
"action": "RE_AUTH", # <- caller's action is never read
"conditions": self._create_conditions_v2(kwargs.pop("conditions", [])),
...
}
kwargs["action"] is never read anywhere in the method. The value is accepted, dropped, and
the call returns err = None, so there is no signal to the caller that the argument had no
effect.
I appreciate RE_AUTH may well be the only action the timeout policy accepts, in which case
the resulting rule is correct — the problem is narrower than that: the SDK advertises a
parameter, takes it, and silently ignores it. A caller who passes anything else (a typo, a
value carried over from a shared helper that also builds access rules, a value read from a
config file) gets no indication that it was thrown away.
Same pattern in update_timeout_rule_v2 — documents action at
zscaler/zpa/policies.py:1979-1980, hard-codes "action": "RE_AUTH" at :2056.
Reproduction
Fully offline — no credentials, no network.
import json
from zscaler.zpa.policies import PolicySetControllerAPI
CONFIG = {"client": {"customerId": "72058304855015000"}}
class StubResponse:
def __init__(self, body):
self._body = body
def get_body(self):
return self._body
def get_status(self):
return 200
class StubExecutor:
"""Stands in for RequestExecutor: records requests, replies with canned bodies."""
def __init__(self):
self.requests = []
def create_request(self, method, endpoint, body=None, headers=None, params=None,
use_raw_data_for_body=False):
req = {"method": method, "endpoint": endpoint.strip(),
"body": {} if body is None else body, "params": params or {}}
self.requests.append(req)
return req, None
def execute(self, request, response_type=None, return_raw_response=False):
if "policySet/policyType/" in request["endpoint"]:
return StubResponse({"id": "72058304855015100", "policyType": "2"}), None
return StubResponse({"id": "72058304855099900", "name": "rule-a",
"action": "RE_AUTH"}), None
ex = StubExecutor()
rule, resp, err = PolicySetControllerAPI(ex, CONFIG).add_timeout_rule_v2(
name="rule-a",
action="isolate", # accepted by **kwargs, never used
conditions=[("client_type", ["zpn_client_type_exporter"])],
)
post = [r for r in ex.requests if r["method"] == "POST"][0]
print("err =", err)
print("caller passed action='isolate'; POST body action ->",
json.dumps(post["body"].get("action")))
print("full POST body:", json.dumps(post["body"], indent=2, sort_keys=True))
Expected behaviour
One of the two — the argument is honoured, or it is rejected. Concretely, either the value the
caller passed reaches the request body, or the call fails with a clear error naming the
accepted values, or action stops being documented and accepted at all.
What should not happen is a successful-looking call (err is None) whose documented argument
was silently discarded.
Actual behaviour
err = None
caller passed action='isolate'; POST body action -> "RE_AUTH"
full POST body: {
"action": "RE_AUTH",
"conditions": [
{
"operands": [
{
"objectType": "CLIENT_TYPE",
"values": [
"zpn_client_type_exporter"
]
}
],
"operator": "OR"
}
],
"custom_msg": null,
"description": null,
"name": "rule-a",
"reauthIdleTimeout": 600,
"reauthTimeout": 172800
}
Root cause
| Location |
Code |
zscaler/zpa/policies.py:1832 |
def add_timeout_rule_v2(self, name: str, **kwargs) — action absorbed by **kwargs |
zscaler/zpa/policies.py:1858-1860 |
docstring advertises action (str) |
zscaler/zpa/policies.py:1928 |
"action": "RE_AUTH" — hard-coded, kwargs["action"] never read |
Same shape in update_timeout_rule_v2: docstring at zscaler/zpa/policies.py:1979-1980, hard-coded at zscaler/zpa/policies.py:2056.
Suggested direction
I don't know whether the timeout policy will ever accept an action other than RE_AUTH, so
I'd rather not pick for you. The options as I see them:
- Honour it:
"action": (kwargs.get("action") or "RE_AUTH").upper(), which matches how
add_access_rule_v2 and the other _v2 builders treat action.
- Reject it: raise or return an error when
action is present and not RE_AUTH, so a wrong
value is loud instead of silent.
- Document it away: drop
action from the docstring's Keyword Args and state that the action
is fixed, so callers stop passing it.
Happy to open a PR for whichever you'd prefer — all three are small.
[ISSUE] add_timeout_rule_v2 documents an
actionkeyword but hard-codes the action and silently discards what the caller passedEnvironment
zscaler-sdk-python1.9.44 (editable install ofmaster@e7f5f7ef)policies(add_timeout_rule_v2)Found by reading the SDK source and confirmed with the offline reproduction below (a stub
request executor, so no credentials and no network are involved). All identifiers used are
synthetic.
Description
PolicySetControllerAPI.add_timeout_rule_v2documentsactionas a supported keywordargument (
zscaler/zpa/policies.py:1858-1860):The signature is
add_timeout_rule_v2(self, name: str, **kwargs)(
zscaler/zpa/policies.py:1832), soaction=is accepted without complaint. But the payloadhard-codes it (
zscaler/zpa/policies.py:1928):kwargs["action"]is never read anywhere in the method. The value is accepted, dropped, andthe call returns
err = None, so there is no signal to the caller that the argument had noeffect.
I appreciate
RE_AUTHmay well be the only action the timeout policy accepts, in which casethe resulting rule is correct — the problem is narrower than that: the SDK advertises a
parameter, takes it, and silently ignores it. A caller who passes anything else (a typo, a
value carried over from a shared helper that also builds access rules, a value read from a
config file) gets no indication that it was thrown away.
Same pattern in
update_timeout_rule_v2— documentsactionatzscaler/zpa/policies.py:1979-1980, hard-codes"action": "RE_AUTH"at:2056.Reproduction
Fully offline — no credentials, no network.
Expected behaviour
One of the two — the argument is honoured, or it is rejected. Concretely, either the value the
caller passed reaches the request body, or the call fails with a clear error naming the
accepted values, or
actionstops being documented and accepted at all.What should not happen is a successful-looking call (
err is None) whose documented argumentwas silently discarded.
Actual behaviour
Root cause
zscaler/zpa/policies.py:1832def add_timeout_rule_v2(self, name: str, **kwargs)—actionabsorbed by**kwargszscaler/zpa/policies.py:1858-1860action (str)zscaler/zpa/policies.py:1928"action": "RE_AUTH"— hard-coded,kwargs["action"]never readSame shape in
update_timeout_rule_v2: docstring atzscaler/zpa/policies.py:1979-1980, hard-coded atzscaler/zpa/policies.py:2056.Suggested direction
I don't know whether the timeout policy will ever accept an action other than
RE_AUTH, soI'd rather not pick for you. The options as I see them:
"action": (kwargs.get("action") or "RE_AUTH").upper(), which matches howadd_access_rule_v2and the other_v2builders treataction.actionis present and notRE_AUTH, so a wrongvalue is loud instead of silent.
actionfrom the docstring's Keyword Args and state that the actionis fixed, so callers stop passing it.
Happy to open a PR for whichever you'd prefer — all three are small.