ZPA data model quirks
If you are building anything against the ZPA API, save yourself the rediscovery tax and read this page first. Every item below cost real time to figure out.
Operand value lives on RHS
Across every operand ObjectType (SCIM_GROUP, SCIM, APP,
APP_GROUP, CLIENT_TYPE, PLATFORM, TRUSTED_NETWORK, …), the
single source of truth for the value is the RHS string field.
The struct also has Values []string. It is always nil. Do not read
it. Do not write it. Trust RHS.
Why: the SDK type is generic across an old and new operand schema. Only the new shape is in use.
Rule ordering uses RuleOrder, not Priority
RuleOrder is a string. Parse it with strconv.Atoi. Sort ascending.
Priority exists on the struct but does not drive evaluation order in any
context we have observed. Ignore it.
Disabled is a string, not a bool
"0" means enabled. "1" means disabled. Anything else is a bug somewhere.
SCIM operand ID types
SCIM group IDs are stored as int64 in the index (Index.ScimGroups map[int64]*ScimGroup). On the wire and in policy operands they appear as
strings. Convert at the boundary - convert once, fail loudly if it does
not parse.
SCIM attribute headers and values are strings throughout. Don’t try to be clever and unify them.
Segment / segment-group is one-to-one
A given application segment belongs to exactly one segment group. ZPA
enforces this. The backlink index relies on it: SegmentToGroup is a
map[string]string, not map[string][]string.
If you ever observe a segment in two groups simultaneously, something is broken upstream. File it.
Domain matching has wildcards
Segments declare an array of domain entries. Each entry is either:
- An exact hostname (
db.prod.example.com). - A wildcard (
*.prod.example.com) which matches any single-or-multi-label prefix.
The reachability query walks parents bottom-up: exact -> *.parent ->
*.grandparent -> *.tld. First match wins.
Multiple segments can declare the same domain or wildcard. That is the basis of the domain overlap report.
Empty condition list = match everyone
A policy rule with zero conditions matches every user. ZPA actually behaves this way - it is not an SDK quirk. The simulator surfaces it as a warning because it is rarely intentional.
Unknown ObjectType
The SDK ships strings for object types but the union is not closed - new
ones appear. The simulator’s policy is “skip with a warning” rather than
“guess”. When you see a SkipReason mentioning an unknown type, that is
the simulator refusing to invent semantics.
Authentication
ZPA uses ZIdentity (ZID) OAuth2. The same credentials also unlock the
OneAPI / BI endpoint at https://api.zsapi.net/bi/api/v1/report/all,
provided you grant the right scopes on the ZIdentity client. PainScaler
does not yet use that endpoint - it is on the roadmap.
The things ZPA does not give you
- SCIM group membership: there is no management-API call that returns “user X is in groups Y, Z”. The SCIM API requires a per-IdP static bearer token, which is operationally hostile. The realistic answer is going direct to the IdP (Azure AD Graph, Okta API). Not implemented yet.
- Per-rule hit counts: ZPA does not expose how often each access policy rule actually fires. You can guess from the BI endpoint logs if you have them; you cannot ask the management API.
- Backlinks of any kind: the API gives you forward edges only.
Everything in PainScaler’s index that ends in
*ToPoliciesis computed, not fetched.