Skip to content

fix: tolerate unparseable individual records in Okta list responses - #108

Open
jackvaughanjr wants to merge 2 commits into
okta:mainfrom
jackvaughanjr:fix/tolerant-list-deserialization
Open

fix: tolerate unparseable individual records in Okta list responses#108
jackvaughanjr wants to merge 2 commits into
okta:mainfrom
jackvaughanjr:fix/tolerant-list-deserialization

Conversation

@jackvaughanjr

Copy link
Copy Markdown

Stacked on #107. The first commit here is #107's; review it there. The reviewable change in this PR is the second commit. If #107 merges first, this rebases to a single commit.

Addresses the general form of #48.

The problem

ApiClient.__deserialize builds a list response with a bare comprehension and no per-element guard:

return [self.__deserialize(sub_data, sub_kls) for sub_data in data]

One record that fails Pydantic validation raises out through the whole call. The caller gets an error string instead of the page, with no indication that other valid records exist. #107 fixes the four models known to trigger this, but the models live in a pinned dependency generated from a spec that keeps drifting, so the next addition breaks things again.

For audit and inventory work, where the question is "show me everything you can see", partial results with a clear warning are more useful than a hard failure.

Approach

Per-item parsing installed at that seam. Successful records go into items; unparseable ones are collected into a warnings array carrying the model name, the validation error, and the raw payload, then surfaced through json_response. Warnings live in a ContextVar, so collection is per-request and async-safe.

OKTA_MCP_STRICT_DESERIALIZATION=1 restores the current fail-the-whole-page behavior for callers who want it.

Why the SDK seam rather than the tool functions. #64 and #85 both approach this by bypassing the typed client and calling the request executor directly, which forces them to re-implement the SDK's snake_case to camelCase query-param mapping in a local _camel_case_param() helper. That is a permanent maintenance cost and it only covers applications. Patching __deserialize covers every list endpoint and leaves query-param handling to the SDK.

Why not blanket-relax the models. #86 makes every field on Application optional and sets extra="allow". That does stop the crash, but it also removes the ability to detect a genuinely malformed response. The intent here is targeted patches for known defects (#107) plus a general net for unknown ones (this PR), so validation still means something.

Scope is list responses only. Single-object endpoints are covered by the model patches in #107.

Testing

26 tests. The tolerant-path test uses a record broken in a way #107 does not fix, so it cannot pass accidentally. Covers: good records returned alongside a bad one, the warning carrying the raw payload, strict mode restoring the raise, and json_response attaching warnings without clobbering an existing key.

Full suite: 581 passed, up from 529 on main (26 here, 26 from #107).

Note on total_fetched

It now means "records successfully returned", which stays consistent with items. Dropped records are counted in warnings. has_more and next_cursor come from response headers and are unaffected. Documented in create_paginated_response.

Consolidates every SDK model compatibility patch into one module,
okta_mcp_server.utils.okta_compat, applied at import time.

Covers four defects where a generated Pydantic model is stricter than
the API it describes, each of which aborts an entire response:

  A  SamlApplicationSettingsSignOn: five required StrictBool fields the
     API omits on apps created from OIN catalog templates.
  B  Policy.embedded: typed Dict[str, Dict[str, Any]], but Okta returns
     scalars inside _embedded (e.g. {"resourceType": "APP"}).
  C  AuthenticatorEnrollmentPolicyAuthenticatorSettings.key: closed enum
     missing smart_card_idp, and brittle to any future authenticator.
  D  UserTypeCondition.exclude/include: required lists where Okta sends
     null. Also RiskDetectionTypesPolicyRuleCondition and
     UserIdentifierPolicyRuleCondition.

Also migrates the existing LogSecurityContext.user_behaviors patch out
of system_logs.py so all SDK compatibility lives in one place.

Supersedes the inline patches in PR okta#102 (issues okta#100, okta#101).
Upstream: okta/okta-sdk-python#546 (A), #572 (D); okta#100
(B), okta#101 (C), okta#48 (A).

Regression fixtures are synthetic: example.invalid hosts and fake IDs.
The SDK deserializes a list response with a bare per-item comprehension
in ApiClient.__deserialize and no per-element guard, so one record that
fails Pydantic validation aborts the entire page. For audit and
inventory work, partial results with a clear warning beat a hard
failure.

Installs a per-item tolerant parse at that seam. Successful records are
returned in items; unparseable ones are collected in a warnings array
carrying the model name, the validation error, and the raw payload, and
surfaced through json_response. Warnings are held in a ContextVar so
collection is per-request and async-safe.

Set OKTA_MCP_STRICT_DESERIALIZATION=1 to restore the current
fail-the-whole-page behavior.

Patching the SDK seam rather than the individual tool functions means
this covers every list endpoint and does not require re-implementing the
SDK's snake_case to camelCase query-param mapping, which a per-tool
approach would.

Relates to okta#48.

Stacked on the SDK model compat branch; that commit is included here so
the suite runs, and should be reviewed separately.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant