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:
-
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.
-
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.
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_POLICYrules. 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 aPolicySetControllerV1.update_access_rule()takes named parameters. There's no method that accepts the object we just read:Since the payload is assembled from arguments and then filtered
anything we don't re-supply isn't in the PUT body. And
**kwargsdoesn't cover it, becausetransform_common_id_fieldsonly walks the three tuples inreformat_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:
enabledPolicySetControllerV1parsesdisabledbut doesn't declareenabled, anrequest_format()doesn't emit it, so rule enablement doesn'tsurvive the round trip.
operator(AND/OR) no parameter, and kwargs won't carry it.policySetIdparsed in__init__, absent fromrequest_format().bypassDefaultRule,auditMessage,zpnCbiProfileId,browserPostureProfileIdand a few others appear in the Go SDK's wire struct but aren't parsed here."appConnectorGroups": [...] or []produces[]rather thanNone, so it survives the filter and is always sent which clears the groups unless every ID is re-supplied.idp_id, while a GET returnsidpId.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:
An update that accepts the full rule e.g.
update_access_rule_raw(rule_id, body: dict),or letting
update_access_ruletake the objectget_rule()returned.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_casepasses camelCase keys through untouched:It just isn't in the README or
examples/, so we've been reluctant to buildon 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 passa 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-python1.9.43, Python 3.13, ZPA OneAPI.Happy to provide more detail or test a change if it's helpful. Thanks again.