From 78d5b208fec32fbf500283ad24829be2a941cd21 Mon Sep 17 00:00:00 2001 From: BinoyOza-okta Date: Sun, 7 Jun 2026 19:08:35 +0530 Subject: [PATCH 1/2] fix: Align AccessPolicyConstraint methods/types enums with actual API casing This commit fixes Pydantic deserialization failures on `PolicyApi.list_policy_rules()` (GET `/api/v1/policies/{policyId}/rules`) when an access policy rule's `constraints[]` contains lowercase `methods` or `types` values (e.g. `"password"`, `"push"`, `"webauthn"`). Problem: - The Okta API returns `methods` and `types` constraint values in lowercase (e.g. `"types": ["password"]`, `"methods": ["push"]`), but the OpenAPI spec defined the corresponding enums as UPPERCASE-only (`PASSWORD`, `PUSH`, `WEBAUTHN`, ...). - Generated Pydantic validators rejected the real API payload with: each list item must be one of ('PASSWORD', 'SECURITY_QUESTION', 'SMS', 'VOICE', 'EMAIL', 'PUSH', 'SIGNED_NONCE', 'OTP', 'TOTP', 'WEBAUTHN', 'DUO', 'IDP', 'CERT') - This broke `list_policy_rules()` for any access policy whose rules carry `KnowledgeConstraint` / `PossessionConstraint` / base `AccessPolicyConstraint` blocks (i.e. most modern authentication policies). Root Cause: - Enum case mismatch between the OAS3 spec and the live API response for `AccessPolicyConstraint.methods[]` and `AccessPolicyConstraint.types[]` (inherited by `KnowledgeConstraint` and `PossessionConstraint`). Solution: 1. Updated `openapi/api.yaml` to redefine both enums in lowercase to match what the API actually returns: - `methods[]`: password, security_question, sms, voice, email, push, signed_nonce, otp, totp, webauthn, duo, idp, cert - `types[]`: security_key, phone, email, password, security_question, app, federated 2. Regenerated the affected models so their `field_validator` allow-lists and error messages reflect the lowercase values: - `okta/models/access_policy_constraint.py` - `okta/models/knowledge_constraint.py` - `okta/models/possession_constraint.py` Testing: - Verified against a live Okta org with an access policy whose knowledge constraint contains `"types": ["password"]`. `client.list_policy_rules(policy_id)` now returns the full rule list without raising `ValidationError`, and `constraints[].knowledge.types` deserializes cleanly. - All three regenerated models accept the lowercase values; no other factor / policy code paths were touched. Backward Compatibility: - This is a wire-format alignment fix: previously the SDK could not parse real API responses at all, so no existing successful call is affected. Callers constructing constraints client-side must now use lowercase values (matching API behavior). Fixes: OKTA-1172239 --- okta/models/access_policy_constraint.py | 48 ++++++++++++------------- okta/models/knowledge_constraint.py | 48 ++++++++++++------------- okta/models/possession_constraint.py | 48 ++++++++++++------------- openapi/api.yaml | 40 ++++++++++----------- 4 files changed, 92 insertions(+), 92 deletions(-) diff --git a/okta/models/access_policy_constraint.py b/okta/models/access_policy_constraint.py index ccbd38c6..62b4c537 100644 --- a/okta/models/access_policy_constraint.py +++ b/okta/models/access_policy_constraint.py @@ -99,24 +99,24 @@ def methods_validate_enum(cls, value): for i in value: if i not in set( [ - "PASSWORD", - "SECURITY_QUESTION", - "SMS", - "VOICE", - "EMAIL", - "PUSH", - "SIGNED_NONCE", - "OTP", - "TOTP", - "WEBAUTHN", - "DUO", - "IDP", - "CERT", + "password", + "security_question", + "sms", + "voice", + "email", + "push", + "signed_nonce", + "otp", + "totp", + "webauthn", + "duo", + "idp", + "cert", ] ): raise ValueError( - "each list item must be one of ('PASSWORD', 'SECURITY_QUESTION', 'SMS', 'VOICE', 'EMAIL', 'PUSH', " - "'SIGNED_NONCE', 'OTP', 'TOTP', 'WEBAUTHN', 'DUO', 'IDP', 'CERT')" + "each list item must be one of ('password', 'security_question', 'sms', 'voice', 'email', 'push', " + "'signed_nonce', 'otp', 'totp', 'webauthn', 'duo', 'idp', 'cert')" ) return value @@ -129,18 +129,18 @@ def types_validate_enum(cls, value): for i in value: if i not in set( [ - "SECURITY_KEY", - "PHONE", - "EMAIL", - "PASSWORD", - "SECURITY_QUESTION", - "APP", - "FEDERATED", + "security_key", + "phone", + "email", + "password", + "security_question", + "app", + "federated", ] ): raise ValueError( - "each list item must be one of ('SECURITY_KEY', 'PHONE', 'EMAIL', 'PASSWORD', 'SECURITY_QUESTION', " - "'APP', 'FEDERATED')" + "each list item must be one of ('security_key', 'phone', 'email', 'password', 'security_question', " + "'app', 'federated')" ) return value diff --git a/okta/models/knowledge_constraint.py b/okta/models/knowledge_constraint.py index c14c8e76..d3cfbb6e 100644 --- a/okta/models/knowledge_constraint.py +++ b/okta/models/knowledge_constraint.py @@ -96,24 +96,24 @@ def methods_validate_enum(cls, value): for i in value: if i not in set( [ - "PASSWORD", - "SECURITY_QUESTION", - "SMS", - "VOICE", - "EMAIL", - "PUSH", - "SIGNED_NONCE", - "OTP", - "TOTP", - "WEBAUTHN", - "DUO", - "IDP", - "CERT", + "password", + "security_question", + "sms", + "voice", + "email", + "push", + "signed_nonce", + "otp", + "totp", + "webauthn", + "duo", + "idp", + "cert", ] ): raise ValueError( - "each list item must be one of ('PASSWORD', 'SECURITY_QUESTION', 'SMS', 'VOICE', 'EMAIL', 'PUSH', " - "'SIGNED_NONCE', 'OTP', 'TOTP', 'WEBAUTHN', 'DUO', 'IDP', 'CERT')" + "each list item must be one of ('password', 'security_question', 'sms', 'voice', 'email', 'push', " + "'signed_nonce', 'otp', 'totp', 'webauthn', 'duo', 'idp', 'cert')" ) return value @@ -126,18 +126,18 @@ def types_validate_enum(cls, value): for i in value: if i not in set( [ - "SECURITY_KEY", - "PHONE", - "EMAIL", - "PASSWORD", - "SECURITY_QUESTION", - "APP", - "FEDERATED", + "security_key", + "phone", + "email", + "password", + "security_question", + "app", + "federated", ] ): raise ValueError( - "each list item must be one of ('SECURITY_KEY', 'PHONE', 'EMAIL', 'PASSWORD', 'SECURITY_QUESTION', " - "'APP', 'FEDERATED')" + "each list item must be one of ('security_key', 'phone', 'email', 'password', 'security_question', " + "'app', 'federated')" ) return value diff --git a/okta/models/possession_constraint.py b/okta/models/possession_constraint.py index b8ca7eec..3c611bab 100644 --- a/okta/models/possession_constraint.py +++ b/okta/models/possession_constraint.py @@ -137,24 +137,24 @@ def methods_validate_enum(cls, value): for i in value: if i not in set( [ - "PASSWORD", - "SECURITY_QUESTION", - "SMS", - "VOICE", - "EMAIL", - "PUSH", - "SIGNED_NONCE", - "OTP", - "TOTP", - "WEBAUTHN", - "DUO", - "IDP", - "CERT", + "password", + "security_question", + "sms", + "voice", + "email", + "push", + "signed_nonce", + "otp", + "totp", + "webauthn", + "duo", + "idp", + "cert", ] ): raise ValueError( - "each list item must be one of ('PASSWORD', 'SECURITY_QUESTION', 'SMS', 'VOICE', 'EMAIL', 'PUSH', " - "'SIGNED_NONCE', 'OTP', 'TOTP', 'WEBAUTHN', 'DUO', 'IDP', 'CERT')" + "each list item must be one of ('password', 'security_question', 'sms', 'voice', 'email', 'push', " + "'signed_nonce', 'otp', 'totp', 'webauthn', 'duo', 'idp', 'cert')" ) return value @@ -167,18 +167,18 @@ def types_validate_enum(cls, value): for i in value: if i not in set( [ - "SECURITY_KEY", - "PHONE", - "EMAIL", - "PASSWORD", - "SECURITY_QUESTION", - "APP", - "FEDERATED", + "security_key", + "phone", + "email", + "password", + "security_question", + "app", + "federated", ] ): raise ValueError( - "each list item must be one of ('SECURITY_KEY', 'PHONE', 'EMAIL', 'PASSWORD', 'SECURITY_QUESTION', " - "'APP', 'FEDERATED')" + "each list item must be one of ('security_key', 'phone', 'email', 'password', 'security_question', " + "'app', 'federated')" ) return value diff --git a/openapi/api.yaml b/openapi/api.yaml index d07ef21e..050b0fbb 100644 --- a/openapi/api.yaml +++ b/openapi/api.yaml @@ -57789,19 +57789,19 @@ components: items: type: string enum: - - PASSWORD - - SECURITY_QUESTION - - SMS - - VOICE - - EMAIL - - PUSH - - SIGNED_NONCE - - OTP - - TOTP - - WEBAUTHN - - DUO - - IDP - - CERT + - password + - security_question + - sms + - voice + - email + - push + - signed_nonce + - otp + - totp + - webauthn + - duo + - idp + - cert type: array reauthenticateIn: description: The duration after which the user must re-authenticate regardless of user activity. This re-authentication interval overrides the Verification Method object's `reauthenticateIn` interval. The supported values use ISO 8601 period format for recurring time intervals (for example, `PT1H`). @@ -57814,13 +57814,13 @@ components: items: type: string enum: - - SECURITY_KEY - - PHONE - - EMAIL - - PASSWORD - - SECURITY_QUESTION - - APP - - FEDERATED + - security_key + - phone + - email + - password + - security_question + - app + - federated type: array AccessPolicyConstraints: description: Specifies constraints for the authenticator. Constraints are logically evaluated such that only one constraint object needs to be satisfied. But, within a constraint object, each constraint property must be satisfied. From 967f6523ec969128d43e7f64acb1e098c5154cc2 Mon Sep 17 00:00:00 2001 From: BinoyOza-okta Date: Mon, 29 Jun 2026 19:07:17 +0530 Subject: [PATCH 2/2] test: add unit tests for AccessPolicyConstraint enum casing Add unit tests verifying the lowercase enum alignment for `methods` and `types` fields on `AccessPolicyConstraint`, `KnowledgeConstraint`, and `PossessionConstraint`. Covers: - Lowercase values (e.g. "password", "push", "webauthn", "security_key") are accepted on all three models. - UPPERCASE values (e.g. "PASSWORD", "PUSH", "SECURITY_KEY") are rejected with a Pydantic `ValidationError`. - Unknown values are rejected. - Full lowercase enum allow-lists are accepted. - `AccessPolicyConstraint.from_dict()` correctly deserializes a realistic lowercase API payload including `methods`, `types`, `required`, and `reauthenticateIn`. --- tests/test_access_policy_constraint_enums.py | 126 +++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 tests/test_access_policy_constraint_enums.py diff --git a/tests/test_access_policy_constraint_enums.py b/tests/test_access_policy_constraint_enums.py new file mode 100644 index 00000000..ae5ae91b --- /dev/null +++ b/tests/test_access_policy_constraint_enums.py @@ -0,0 +1,126 @@ +# The Okta software accompanied by this notice is provided pursuant to the following terms: +# Copyright © 2025-Present, Okta, Inc. +# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the +# License. +# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and limitations under the License. +# coding: utf-8 + +""" +Unit tests verifying that `methods` and `types` enum validators on +`AccessPolicyConstraint`, `KnowledgeConstraint`, and `PossessionConstraint` +accept lowercase values (matching the live Okta API response casing) and +reject UPPERCASE / invalid values. + +Related: Alignment of OAS3 enum casing with the API. +""" + +import pytest +from pydantic import ValidationError + +from okta.models.access_policy_constraint import AccessPolicyConstraint +from okta.models.knowledge_constraint import KnowledgeConstraint +from okta.models.possession_constraint import PossessionConstraint + + +VALID_METHODS = [ + "password", "security_question", "sms", "voice", "email", + "push", "signed_nonce", "otp", "totp", "webauthn", "duo", "idp", "cert", +] +VALID_TYPES = [ + "security_key", "phone", "email", "password", + "security_question", "app", "federated", +] + + +class TestAccessPolicyConstraintEnums: + """Validator tests for the base `AccessPolicyConstraint` model.""" + + def test_lowercase_methods_accepted(self): + obj = AccessPolicyConstraint(methods=["password", "push", "webauthn"]) + assert obj.methods == ["password", "push", "webauthn"] + + def test_lowercase_types_accepted(self): + obj = AccessPolicyConstraint(types=["password", "security_key", "phone"]) + assert obj.types == ["password", "security_key", "phone"] + + def test_all_valid_methods_accepted(self): + obj = AccessPolicyConstraint(methods=list(VALID_METHODS)) + assert obj.methods == VALID_METHODS + + def test_all_valid_types_accepted(self): + obj = AccessPolicyConstraint(types=list(VALID_TYPES)) + assert obj.types == VALID_TYPES + + def test_uppercase_methods_rejected(self): + with pytest.raises(ValidationError): + AccessPolicyConstraint(methods=["PASSWORD"]) + + def test_uppercase_types_rejected(self): + with pytest.raises(ValidationError): + AccessPolicyConstraint(types=["PASSWORD"]) + + def test_unknown_method_rejected(self): + with pytest.raises(ValidationError): + AccessPolicyConstraint(methods=["not_a_real_method"]) + + def test_unknown_type_rejected(self): + with pytest.raises(ValidationError): + AccessPolicyConstraint(types=["not_a_real_type"]) + + def test_from_dict_lowercase_payload(self): + """Simulates a real API response payload.""" + payload = { + "methods": ["password", "push"], + "types": ["password"], + "required": True, + "reauthenticateIn": "PT2H", + } + obj = AccessPolicyConstraint.from_dict(payload) + assert obj.methods == ["password", "push"] + assert obj.types == ["password"] + assert obj.required is True + assert obj.reauthenticate_in == "PT2H" + + +class TestKnowledgeConstraintEnums: + """Validator tests for `KnowledgeConstraint` (inherits the same enums).""" + + def test_lowercase_methods_accepted(self): + obj = KnowledgeConstraint(methods=["password"]) + assert obj.methods == ["password"] + + def test_lowercase_types_accepted(self): + obj = KnowledgeConstraint(types=["password", "security_question"]) + assert obj.types == ["password", "security_question"] + + def test_uppercase_methods_rejected(self): + with pytest.raises(ValidationError): + KnowledgeConstraint(methods=["PASSWORD"]) + + def test_uppercase_types_rejected(self): + with pytest.raises(ValidationError): + KnowledgeConstraint(types=["PASSWORD"]) + + +class TestPossessionConstraintEnums: + """Validator tests for `PossessionConstraint` (inherits the same enums).""" + + def test_lowercase_methods_accepted(self): + obj = PossessionConstraint(methods=["push", "signed_nonce", "webauthn"]) + assert obj.methods == ["push", "signed_nonce", "webauthn"] + + def test_lowercase_types_accepted(self): + obj = PossessionConstraint(types=["security_key", "phone"]) + assert obj.types == ["security_key", "phone"] + + def test_uppercase_methods_rejected(self): + with pytest.raises(ValidationError): + PossessionConstraint(methods=["PUSH"]) + + def test_uppercase_types_rejected(self): + with pytest.raises(ValidationError): + PossessionConstraint(types=["SECURITY_KEY"]) +