Skip to content

Read-modify-write on an existing policy rule requires rebuilding it from named parameters #571

Description

@Ahmet-Djedovic

Hi first, thanks for the SDK.

I've hit a use case that I can't find a clean path for, and I suspect it's just outside the pattern the policy methods were designed around. Wanted to describe it in case it's useful input.

What we're trying to do

We have existing ACCESS_POLICY rules. We need to add one device-posture operand to the POSTURE condition block on each of them a new client certificate joining the set the rule already accepts and change nothing else about any rule.

Where it gets difficult

get_rule() gives us the rule as a PolicySetControllerV1. update_access_rule() takes named parameters. There's no method that accepts the object we just read:

rule = policies.get_rule("access", rule_id)     # PolicySetControllerV1
# ...no update(rule) so every field has to be taken apart and passed back
policies.update_access_rule(rule_id, name=..., action=..., conditions=..., ...)

Since the payload is assembled from arguments and then filtered

payload = {k: v for k, v in payload.items() if v is not None}

anything we don't re-supply isn't in the PUT body. And **kwargs doesn't cover it, because transform_common_id_fields only walks the three tuples in reformat_params; other keys are dropped silently.

In practice that leaves us hand-mapping ~17 fields per rule, with a few we couldn't map at all:

  • enabled PolicySetControllerV1 parses disabled but doesn't declare enabled, an request_format() doesn't emit it, so rule enablement doesn't
    survive the round trip.
  • rule-level operator (AND/OR) no parameter, and kwargs won't carry it.
  • policySetId parsed in __init__, absent from request_format().
  • bypassDefaultRule, auditMessage, zpnCbiProfileId, browserPostureProfileId and a few others appear in the Go SDK's wire struct but aren't parsed here.
  • "appConnectorGroups": [...] or [] produces [] rather than None, so it survives the filter and is always sent which clears the groups unless every ID is re-supplied.
  • Feeding conditions back as dicts goes through a key whitelist expecting snake-case idp_id, while a GET returns idpId.

We may well be holding it wrong if there's an intended pattern for this we've
missed, we'd genuinely like to know.

What would help

Either of these would solve it for us:

  1. An update that accepts the full rule e.g. update_access_rule_raw(rule_id, body: dict),
    or letting update_access_rule take the object get_rule() returned.

  2. Documenting the request-executor path as supported. For what it's worth,
    this already works and is lossless get_body() returns the raw response,
    and convert_keys_to_camel_case passes camelCase keys through untouched:

    executor = client.get_request_executor()
    request, err = executor.create_request("PUT", url, body=rule_dict)
    response, err = executor.execute(request, return_raw_response=True)

    It just isn't in the README or examples/, so we've been reluctant to build
    on it.

Context

This may simply be a different shape of problem than the SDK targets. The
Terraform provider does the same operation via zscaler-sdk-go, but it can pass
a fully populated struct because Terraform already holds the complete desired
state. Tools that edit pre-existing objects they didn't create don't have that,
which is where the gap shows up.

Environment: zscaler-sdk-python 1.9.43, Python 3.13, ZPA OneAPI.

Happy to provide more detail or test a change if it's helpful. Thanks again.

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