Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.
* <p>
* Per RFC 7643, complex sub-attributes are not supported; this option
* relaxes that constraint for non-conformant resources.
*/
ALLOW_OBJECT_VALUED_STRING_SUB_ATTRIBUTES

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should have a comma at the end.

}

@NotNull
Expand Down Expand Up @@ -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)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The node type is checked twice, which we can avoid (along with the copied code) by rewriting this with the following. I also think we can rename the option:

case STRING:
  if (node.isObject() && path.size() > 1
    && enabledOptions.contains(Option.ALLOW_OBJECT_AS_STRING_SUB_ATTR))
  {
    return;
  }

  // Fall through for string evaluation.
case DATETIME:
...

{
return;
}
results.syntaxIssues.add(prefix + "Value for attribute " + path +
" must be a JSON string");
return;
}
break;
case DATETIME:
case REFERENCE:
if (!node.isString())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The old tests don't do this much, but we should leverage the builder pattern on this:

AttributeDefinition.Builder builder = new AttributeDefinition.Builder()
    .setName(...)
    .setType(...)

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"));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New unit tests should use AssertJ's assertion statements instead of TestNG's. For example, this should be:

assertThat(results.getSyntaxIssues()).hasSize(1);
assertThat(results.getSyntaxIssues().get(0))
    .contains("must be a JSON string");

For assertion statements that currently have a reason string, you can use .as() if you really want, but I think it's also okay to drop them from this test.


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<PatchOperation> patchOps = Collections.singletonList(
PatchOperation.replace(
Path.root().attribute("iamServiceUser"), iamServiceUserNode));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you want, this can use the string attribute directly since the method throws an exception.


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<String> syntaxIssues,
List<String> mutabilityIssues, List<String> pathIssues) throws Exception
{
Expand Down
Loading