From 3c32a75e9452415c4717ab27227c22a446451ea7 Mon Sep 17 00:00:00 2001 From: Kole Hey Date: Tue, 16 Jun 2026 13:03:44 -0500 Subject: [PATCH] Add a new schema checker option Added a new ALLOW_OBJECT_VALUED_STRING_SUB_ATTRIBUTES schema checker option to allow string-type sub-attributes whose value is a json object. Reviewer: kqarryzada Reviewer: dougbulkley JiraIssue: DS-51562 --- .../scim2/server/utils/SchemaChecker.java | 22 +++ .../server/utils/SchemaCheckerTestCase.java | 127 ++++++++++++++++++ 2 files changed, 149 insertions(+) diff --git a/scim2-sdk-server/src/main/java/com/unboundid/scim2/server/utils/SchemaChecker.java b/scim2-sdk-server/src/main/java/com/unboundid/scim2/server/utils/SchemaChecker.java index daf75bef..0ce8364b 100644 --- a/scim2-sdk-server/src/main/java/com/unboundid/scim2/server/utils/SchemaChecker.java +++ b/scim2-sdk-server/src/main/java/com/unboundid/scim2/server/utils/SchemaChecker.java @@ -201,6 +201,15 @@ public enum Option * that are not defined by the definition of the parent attribute. */ ALLOW_UNDEFINED_SUB_ATTRIBUTES, + + /** + * Relax SCIM 2 standard schema requirements by allowing STRING-typed + * sub-attributes to contain JSON object values. + *

+ * Per RFC 7643, complex sub-attributes are not supported; this option + * relaxes that constraint for non-conformant resources. + */ + ALLOW_OBJECT_VALUED_STRING_SUB_ATTRIBUTES } @NotNull @@ -1040,6 +1049,19 @@ private void checkAttributeValue( switch (attribute.getType()) { case STRING: + if (!node.isString()) + { + if (enabledOptions.contains( + Option.ALLOW_OBJECT_VALUED_STRING_SUB_ATTRIBUTES) + && node.isObject() && path.size() > 1) + { + return; + } + results.syntaxIssues.add(prefix + "Value for attribute " + path + + " must be a JSON string"); + return; + } + break; case DATETIME: case REFERENCE: if (!node.isString()) diff --git a/scim2-sdk-server/src/test/java/com/unboundid/scim2/server/utils/SchemaCheckerTestCase.java b/scim2-sdk-server/src/test/java/com/unboundid/scim2/server/utils/SchemaCheckerTestCase.java index c943413b..48f23e7a 100644 --- a/scim2-sdk-server/src/test/java/com/unboundid/scim2/server/utils/SchemaCheckerTestCase.java +++ b/scim2-sdk-server/src/test/java/com/unboundid/scim2/server/utils/SchemaCheckerTestCase.java @@ -2268,6 +2268,133 @@ private Object[][] getResultData() }; } + /** + * Test case for the allow object valued string sub-attributes option. + * + * @throws Exception if an error occurs. + */ + @Test + public void testAllowObjectValuedStringSubAttributesOption() + throws Exception + { + // Build a schema with a complex parent attribute whose sub-attribute is + // STRING-typed but stores a JSON object value. + AttributeDefinition.Builder builder = new AttributeDefinition.Builder(); + builder.setName("iamSvcUsrData"); + builder.setType(AttributeDefinition.Type.STRING); + builder.setMultiValued(true); + AttributeDefinition iamSvcUsrData = builder.build(); + + builder = new AttributeDefinition.Builder(); + builder.setName("iamServiceUser"); + builder.setType(AttributeDefinition.Type.COMPLEX); + builder.addSubAttributes(iamSvcUsrData); + AttributeDefinition iamServiceUser = builder.build(); + + builder = new AttributeDefinition.Builder(); + builder.setName("iamAdminUser"); + builder.setType(AttributeDefinition.Type.STRING); + AttributeDefinition iamAdminUser = builder.build(); + + SchemaResource testSchema = new SchemaResource("urn:id:test", "test", "", + List.of(iamServiceUser, iamAdminUser)); + + ResourceTypeDefinition testResourceTypeDefinition = + new ResourceTypeDefinition.Builder("test", "/test"). + setCoreSchema(testSchema).build(); + + // Define two schema checkers, one with the option to allow object valued + // string sub-attributes. + SchemaChecker refuseObjValStrSubAttributesChecker = + new SchemaChecker(testResourceTypeDefinition); + + SchemaChecker allowObjValStrSubAttributesChecker = + new SchemaChecker(testResourceTypeDefinition); + allowObjValStrSubAttributesChecker.enable( + SchemaChecker.Option.ALLOW_OBJECT_VALUED_STRING_SUB_ATTRIBUTES); + + // Construct JSON object value for the sub-attribute. + ObjectNode iamSvcUsrDataJsonObjectValue = JsonUtils.getJsonNodeFactory().objectNode(); + iamSvcUsrDataJsonObjectValue.put("key", "initialValue"); + iamSvcUsrDataJsonObjectValue.put("active", "true"); + + ArrayNode iamSvcUsrDataArray = JsonUtils.getJsonNodeFactory(). + arrayNode().add(iamSvcUsrDataJsonObjectValue); + + ObjectNode iamServiceUserNode = JsonUtils.getJsonNodeFactory().objectNode(); + iamServiceUserNode.set("iamSvcUsrData", iamSvcUsrDataArray); + + // checkCreate: JSON object value for a STRING sub-attribute should produce + // a syntax error by default but be accepted when + // ALLOW_OBJECT_VALUED_STRING_SUB_ATTRIBUTES is enabled. + ObjectNode iamServiceUserJsonObjectNode = JsonUtils.getJsonNodeFactory().objectNode(); + iamServiceUserJsonObjectNode.putArray("schemas").add("urn:id:test"); + iamServiceUserJsonObjectNode.set("iamServiceUser", iamServiceUserNode); + + SchemaChecker.Results results = refuseObjValStrSubAttributesChecker.checkCreate( + iamServiceUserJsonObjectNode); + assertEquals(results.getSyntaxIssues().size(), 1, + results.getSyntaxIssues().toString()); + assertTrue(containsIssueWith(results.getSyntaxIssues(), + "must be a JSON string")); + + results = allowObjValStrSubAttributesChecker.checkCreate( + iamServiceUserJsonObjectNode); + assertTrue(results.getSyntaxIssues().isEmpty(), + results.getSyntaxIssues().toString()); + + // checkModify (PATCH replace on iamServiceUser). + ObjectNode currentResource = JsonUtils.getJsonNodeFactory().objectNode(); + currentResource.putArray("schemas").add("urn:id:test"); + currentResource.set("iamServiceUser", iamServiceUserNode); + + List patchOps = Collections.singletonList( + PatchOperation.replace( + Path.root().attribute("iamServiceUser"), iamServiceUserNode)); + + results = refuseObjValStrSubAttributesChecker.checkModify( + patchOps, currentResource); + assertTrue(containsIssueWith(results.getSyntaxIssues(), + "must be a JSON string"), + results.getSyntaxIssues().toString()); + + results = allowObjValStrSubAttributesChecker.checkModify( + patchOps, currentResource); + assertTrue(results.getSyntaxIssues().isEmpty(), + results.getSyntaxIssues().toString()); + + // checkReplace (PUT): same expectation as checkCreate. + results = refuseObjValStrSubAttributesChecker.checkReplace( + iamServiceUserJsonObjectNode, null); + assertEquals(results.getSyntaxIssues().size(), 1, + results.getSyntaxIssues().toString()); + assertTrue(containsIssueWith(results.getSyntaxIssues(), + "must be a JSON string")); + + results = allowObjValStrSubAttributesChecker.checkReplace( + iamServiceUserJsonObjectNode, null); + assertTrue(results.getSyntaxIssues().isEmpty(), + results.getSyntaxIssues().toString()); + + // A top-level STRING attribute with a JSON object value must still be + // rejected even when ALLOW_OBJECT_VALUED_STRING_SUB_ATTRIBUTES is enabled, + // since the option only applies to sub-attributes (path.size() > 1). + ObjectNode iamAdminUserJsonObjectValue = + JsonUtils.getJsonNodeFactory().objectNode(); + iamAdminUserJsonObjectValue.put("key", "initialValue"); + iamAdminUserJsonObjectValue.put("active", "true"); + + ObjectNode iamAdminUserNode = JsonUtils.getJsonNodeFactory().objectNode(); + iamAdminUserNode.putArray("schemas").add("urn:id:test"); + iamAdminUserNode.set("iamAdminUser", iamAdminUserJsonObjectValue); + + results = allowObjValStrSubAttributesChecker.checkCreate(iamAdminUserNode); + assertEquals(results.getSyntaxIssues().size(), 1, + results.getSyntaxIssues().toString()); + assertTrue(containsIssueWith(results.getSyntaxIssues(), + "must be a JSON string")); + } + private SchemaChecker.Results getResults(List syntaxIssues, List mutabilityIssues, List pathIssues) throws Exception {