diff --git a/api/src/main/resources/custom_templates/ApiClient.mustache b/api/src/main/resources/custom_templates/ApiClient.mustache index 9d25f21af89..fe3e6b5279e 100644 --- a/api/src/main/resources/custom_templates/ApiClient.mustache +++ b/api/src/main/resources/custom_templates/ApiClient.mustache @@ -238,9 +238,19 @@ protected List servers = new ArrayList objectMapper.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY); // OKTA-1227472: disable broken polymorphic type resolution on models whose spec-declared - // discriminator subtypes don't actually extend them. + // discriminator subtypes don't actually extend them. Found via an audit of every + // oneOf/anyOf + discriminator pair in the spec for this same defect class - these six + // have the identical problem (ListJwk200ResponseInner/SamlAttributeStatement above were + // the first two found; OrgContactTypeObj in particular breaks the real, commonly-used + // listOrgContactTypes endpoint). objectMapper.addMixIn(com.okta.sdk.resource.model.ListJwk200ResponseInner.class, NoPolymorphicTypeInfoMixin.class); objectMapper.addMixIn(com.okta.sdk.resource.model.SamlAttributeStatement.class, NoPolymorphicTypeInfoMixin.class); + objectMapper.addMixIn(com.okta.sdk.resource.model.AgentJsonSigningKeyRequest.class, NoPolymorphicTypeInfoMixin.class); + objectMapper.addMixIn(com.okta.sdk.resource.model.AgentJsonSigningKeyResponse.class, NoPolymorphicTypeInfoMixin.class); + objectMapper.addMixIn(com.okta.sdk.resource.model.ManagedConnection.class, NoPolymorphicTypeInfoMixin.class); + objectMapper.addMixIn(com.okta.sdk.resource.model.ManagedConnectionCreatable.class, NoPolymorphicTypeInfoMixin.class); + objectMapper.addMixIn(com.okta.sdk.resource.model.OrgContactTypeObj.class, NoPolymorphicTypeInfoMixin.class); + objectMapper.addMixIn(com.okta.sdk.resource.model.PotentialConnection.class, NoPolymorphicTypeInfoMixin.class); // OKTA-1218351: restore NON_NULL serialization on Application and its subtypes, overriding the // per-property JsonInclude(ALWAYS) generated for their required properties. diff --git a/api/src/test/java/com/okta/sdk/resource/client/ApiClientJacksonMixinTest.java b/api/src/test/java/com/okta/sdk/resource/client/ApiClientJacksonMixinTest.java index a3cfcc8ef70..9fb7ba34a46 100644 --- a/api/src/test/java/com/okta/sdk/resource/client/ApiClientJacksonMixinTest.java +++ b/api/src/test/java/com/okta/sdk/resource/client/ApiClientJacksonMixinTest.java @@ -19,10 +19,19 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.okta.sdk.cache.Cache; import com.okta.sdk.cache.CacheManager; +import com.okta.sdk.resource.model.AgentJsonSigningKeyRequest; +import com.okta.sdk.resource.model.AgentJsonSigningKeyResponse; +import com.okta.sdk.resource.model.Application; import com.okta.sdk.resource.model.ApplicationVisibility; import com.okta.sdk.resource.model.ApplicationVisibilityHide; import com.okta.sdk.resource.model.ListJwk200ResponseInner; +import com.okta.sdk.resource.model.ManagedConnection; +import com.okta.sdk.resource.model.ManagedConnectionCreatable; import com.okta.sdk.resource.model.OpenIdConnectApplication; +import com.okta.sdk.resource.model.OrgContactType; +import com.okta.sdk.resource.model.OrgContactTypeObj; +import com.okta.sdk.resource.model.PotentialConnection; +import com.okta.sdk.resource.model.SamlApplication; import com.okta.sdk.resource.model.SamlAttributeStatement; import org.apache.hc.client5.http.impl.classic.HttpClients; import org.testng.annotations.Test; @@ -135,4 +144,156 @@ public void serializeFullOpenIdConnectApplication_stillIncludesRequiredFields() assertNotNull(json); assertTrue(json.contains("\"name\""), "name should be present when set, got: " + json); } + + /** + * GH-1654: reported that listApplications only returned OIDC apps, no SAML apps. A SAML app whose + * settings.signOn.attributeStatements uses the (now-fixed) broken SamlAttributeStatement type would + * throw InvalidTypeIdException while parsing the response array - depending on how a caller's + * pagination/error handling reacted to that, it could plausibly look like SAML apps were being + * silently dropped. Confirms a mixed OIDC/SAML list - with attribute statements populated - now + * deserializes cleanly end-to-end via Application's own (structurally correct) polymorphism. + */ + @Test + public void deserializeApplicationList_withMixedOidcAndSamlAttributeStatements_doesNotThrow() throws Exception { + String json = "[" + + "{\"signOnMode\":\"OPENID_CONNECT\",\"label\":\"oidc-app\",\"id\":\"0oa1\"}," + + "{\"signOnMode\":\"SAML_2_0\",\"label\":\"saml-app\",\"id\":\"0oa2\",\"settings\":{\"signOn\":{" + + "\"attributeStatements\":[" + + "{\"type\":\"EXPRESSION\",\"name\":\"email\",\"values\":[\"user.email\"]}," + + "{\"type\":\"GROUP\",\"filterType\":\"STARTS_WITH\",\"filterValue\":\"Team\"}" + + "]}}}" + + "]"; + + List apps = objectMapper.readValue(json, new TypeReference>() { }); + + assertEquals(apps.size(), 2); + assertTrue(apps.get(0) instanceof OpenIdConnectApplication, "expected OpenIdConnectApplication, got " + apps.get(0).getClass()); + assertTrue(apps.get(1) instanceof SamlApplication, "expected SamlApplication, got " + apps.get(1).getClass()); + + SamlApplication samlApp = (SamlApplication) apps.get(1); + List statements = samlApp.getSettings().getSignOn().getAttributeStatements(); + assertEquals(statements.size(), 2); + assertEquals(statements.get(0).getType(), SamlAttributeStatement.TypeEnum.EXPRESSION); + assertEquals(statements.get(1).getType(), SamlAttributeStatement.TypeEnum.GROUP); + } + + /** + * Found via an audit of every oneOf/anyOf + discriminator pair in the spec for the same defect class + * as OKTA-1227472: OrgContactTypeObj declares BILLING/TECHNICAL subtypes that don't extend it, breaking + * the real listOrgContactTypes endpoint. + */ + @Test + public void deserializeOrgContactTypeObj_withBillingAndTechnicalEntries_doesNotThrow() throws Exception { + String json = "[" + + "{\"contactType\":\"BILLING\"}," + + "{\"contactType\":\"TECHNICAL\"}" + + "]"; + + List contacts = objectMapper.readValue(json, new TypeReference>() { }); + + assertEquals(contacts.size(), 2); + assertEquals(contacts.get(0).getContactType(), OrgContactType.BILLING); + assertEquals(contacts.get(1).getContactType(), OrgContactType.TECHNICAL); + } + + /** + * Same audit finding as OrgContactTypeObj: AgentJsonSigningKeyRequest declares RSA/EC subtypes that + * don't extend it. Currently unreferenced by any operation in the spec, fixed for consistency. + */ + @Test + public void deserializeAgentJsonSigningKeyRequest_withRsaAndEcEntries_doesNotThrow() throws Exception { + String json = "[" + + "{\"kty\":\"RSA\",\"e\":\"AQAB\",\"n\":\"mkC6\",\"use\":\"sig\",\"alg\":\"RS256\"}," + + "{\"kty\":\"EC\",\"crv\":\"P-256\",\"x\":\"abc\",\"y\":\"def\",\"use\":\"sig\",\"alg\":\"ES256\"}" + + "]"; + + List keys = objectMapper.readValue(json, + new TypeReference>() { }); + + assertEquals(keys.size(), 2); + assertEquals(keys.get(0).getE(), "AQAB"); + assertEquals(keys.get(1).getCrv().getValue(), "P-256"); + } + + /** + * Same audit finding, response-side counterpart of AgentJsonSigningKeyRequest. + */ + @Test + public void deserializeAgentJsonSigningKeyResponse_withRsaAndEcEntries_doesNotThrow() throws Exception { + String json = "[" + + "{\"kty\":\"RSA\",\"e\":\"AQAB\",\"n\":\"mkC6\",\"use\":\"sig\",\"alg\":\"RS256\",\"id\":\"key1\"}," + + "{\"kty\":\"EC\",\"crv\":\"P-256\",\"x\":\"abc\",\"y\":\"def\",\"use\":\"sig\",\"alg\":\"ES256\",\"id\":\"key2\"}" + + "]"; + + List keys = objectMapper.readValue(json, + new TypeReference>() { }); + + assertEquals(keys.size(), 2); + assertEquals(keys.get(0).getId(), "key1"); + assertEquals(keys.get(1).getCrv().getValue(), "P-256"); + } + + /** + * Same audit finding: ManagedConnection declares 4 connectionType subtypes that don't extend it. + * Reachable from the managed-connection-list endpoint. + * + * Separate, pre-existing quirk unrelated to this fix: each oneOf branch declares its own + * single-value connectionType enum (e.g. just "IDENTITY_ASSERTION_APP_INSTANCE"), and the flat + * merged class ends up keeping only the last-merged branch's enum ("STS_SERVICE_ACCOUNT") - every + * other value falls back to UNKNOWN_DEFAULT_OPEN_API (harmless, since + * READ_UNKNOWN_ENUM_VALUES_AS_NULL-style fallback is already relied on elsewhere; it doesn't throw). + * This test only asserts the polymorphism fix - that deserialization doesn't throw - not full type + * fidelity, which is a separate, wider issue with how the generator merges oneOf enum properties. + */ + @Test + public void deserializeManagedConnection_withDifferentConnectionTypes_doesNotThrow() throws Exception { + String json = "[" + + "{\"connectionType\":\"IDENTITY_ASSERTION_APP_INSTANCE\",\"id\":\"conn1\"}," + + "{\"connectionType\":\"STS_SERVICE_ACCOUNT\",\"id\":\"conn2\"}" + + "]"; + + List connections = objectMapper.readValue(json, + new TypeReference>() { }); + + assertEquals(connections.size(), 2); + assertEquals(connections.get(0).getId(), "conn1"); + assertEquals(connections.get(1).getConnectionType(), ManagedConnection.ConnectionTypeEnum.STS_SERVICE_ACCOUNT); + } + + /** + * Same audit finding, "creatable" (request-body) counterpart of ManagedConnection. See the enum + * fidelity caveat on {@link #deserializeManagedConnection_withDifferentConnectionTypes_doesNotThrow}. + */ + @Test + public void deserializeManagedConnectionCreatable_withDifferentConnectionTypes_doesNotThrow() throws Exception { + String json = "[" + + "{\"connectionType\":\"IDENTITY_ASSERTION_CUSTOM_AS\"}," + + "{\"connectionType\":\"STS_SERVICE_ACCOUNT\"}" + + "]"; + + List connections = objectMapper.readValue(json, + new TypeReference>() { }); + + assertEquals(connections.size(), 2); + assertEquals(connections.get(1).getConnectionType(), ManagedConnectionCreatable.ConnectionTypeEnum.STS_SERVICE_ACCOUNT); + } + + /** + * Same audit finding: PotentialConnection is a near-duplicate of ManagedConnection with the identical + * defect (same 4 subtypes, same discriminator). See the enum fidelity caveat on + * {@link #deserializeManagedConnection_withDifferentConnectionTypes_doesNotThrow}. + */ + @Test + public void deserializePotentialConnection_withDifferentConnectionTypes_doesNotThrow() throws Exception { + String json = "[" + + "{\"connectionType\":\"IDENTITY_ASSERTION_APP_INSTANCE\"}," + + "{\"connectionType\":\"STS_SERVICE_ACCOUNT\"}" + + "]"; + + List connections = objectMapper.readValue(json, + new TypeReference>() { }); + + assertEquals(connections.size(), 2); + assertEquals(connections.get(1).getConnectionType(), PotentialConnection.ConnectionTypeEnum.STS_SERVICE_ACCOUNT); + } }