Skip to content

[ISSUE] add_timeout_rule_v2 documents an action keyword but hard-codes the action and silently discards what the caller passed #579

Description

@hackerboey

[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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions