All Downloads are FREE. Search and download functionalities are using the official Maven repository.

de.captaingoldfish.scim.sdk.server.patch.PatchTargetHandler Maven / Gradle / Ivy

There is a newer version: 1.26.0
Show newest version
// Generated by delombok at Thu Nov 02 20:38:53 CET 2023
package de.captaingoldfish.scim.sdk.server.patch;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.commons.lang3.StringUtils;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import de.captaingoldfish.scim.sdk.common.constants.AttributeNames;
import de.captaingoldfish.scim.sdk.common.constants.ScimType;
import de.captaingoldfish.scim.sdk.common.constants.enums.Mutability;
import de.captaingoldfish.scim.sdk.common.constants.enums.PatchOp;
import de.captaingoldfish.scim.sdk.common.constants.enums.Type;
import de.captaingoldfish.scim.sdk.common.exceptions.BadRequestException;
import de.captaingoldfish.scim.sdk.common.exceptions.IOException;
import de.captaingoldfish.scim.sdk.common.exceptions.ScimException;
import de.captaingoldfish.scim.sdk.common.resources.ResourceNode;
import de.captaingoldfish.scim.sdk.common.resources.base.ScimArrayNode;
import de.captaingoldfish.scim.sdk.common.resources.base.ScimBooleanNode;
import de.captaingoldfish.scim.sdk.common.resources.base.ScimDoubleNode;
import de.captaingoldfish.scim.sdk.common.resources.base.ScimIntNode;
import de.captaingoldfish.scim.sdk.common.resources.base.ScimLongNode;
import de.captaingoldfish.scim.sdk.common.resources.base.ScimObjectNode;
import de.captaingoldfish.scim.sdk.common.resources.base.ScimTextNode;
import de.captaingoldfish.scim.sdk.common.resources.complex.PatchConfig;
import de.captaingoldfish.scim.sdk.common.schemas.SchemaAttribute;
import de.captaingoldfish.scim.sdk.common.utils.JsonHelper;
import de.captaingoldfish.scim.sdk.server.filter.AttributePathRoot;
import de.captaingoldfish.scim.sdk.server.filter.resources.PatchFilterResolver;
import de.captaingoldfish.scim.sdk.server.patch.msazure.MsAzurePatchComplexValueRebuilder;
import de.captaingoldfish.scim.sdk.server.patch.msazure.MsAzurePatchFilterWorkaround;
import de.captaingoldfish.scim.sdk.server.schemas.ResourceType;
import de.captaingoldfish.scim.sdk.server.utils.RequestUtils;


/**
 * author Pascal Knueppel 
* created at: 30.10.2019 - 09:07
*
* this class will handle the patch-add operation if a target is specified
* *
 *    The result of the add operation depends upon what the target location
 *    indicated by "path" references:
 *
 *    o  If the target location does not exist, the attribute and value are
 *       added.
 *
 *    o  If the target location specifies a complex attribute, a set of
 *       sub-attributes SHALL be specified in the "value" parameter.
 *
 *    o  If the target location specifies a multi-valued attribute, a new
 *       value is added to the attribute.
 *
 *    o  If the target location specifies a single-valued attribute, the
 *       existing value is replaced.
 *
 *    o  If the target location specifies an attribute that does not exist
 *       (has no value), the attribute is added with the new value.
 *
 *    o  If the target location exists, the value is replaced.
 *
 *    o  If the target location already contains the value specified, no
 *       changes SHOULD be made to the resource, and a success response
 *       SHOULD be returned.  Unless other operations change the resource,
 *       this operation SHALL NOT change the modify timestamp of the
 *       resource.
 * 
*/ public class PatchTargetHandler extends AbstractPatch { @java.lang.SuppressWarnings("all") private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(PatchTargetHandler.class); /** * used to check if the ms-azure workaround for patch operations with filters is active. */ private final PatchConfig patchConfig; /** * the specified path where the value should be added */ private final AttributePathRoot path; /** * the patch operation to handle */ private final PatchOp patchOp; /** * the attribute definition of the target */ private SchemaAttribute schemaAttribute; public PatchTargetHandler(PatchConfig patchConfig, ResourceType resourceType, PatchOp patchOp, String path) { super(resourceType); this.patchConfig = patchConfig; try { if (resourceType.getAllSchemaExtensions().stream().anyMatch(schema -> schema.getNonNullId().equals(path))) { this.path = new PatchExtensionAttributePath(path); this.schemaAttribute = null; } else { this.path = RequestUtils.parsePatchPath(resourceType, path); this.schemaAttribute = getSchemaAttribute(); } } catch (ScimException ex) { ex.setScimType(ScimType.RFC7644.INVALID_PATH); throw ex; } this.patchOp = patchOp; } /** * this method will check that the referenced attribute is not a readOnly or immutable node. If the client * tries to process a patch operation on such an object the current value of the resource must meet the * requirements of RFC7644
*
* *
   * If the attribute path is "readOnly" an exception should be thrown
   * If the attributes mutability is "immutable" an add operation is only allowed if the value is unassigned
   * 
*/ private void evaluatePatchPathOperation(SchemaAttribute schemaAttribute, JsonNode attribute) { if (Mutability.READ_ONLY.equals(schemaAttribute.getMutability())) { throw new BadRequestException("the attribute \'" + schemaAttribute.getScimNodeName() + "\' is a \'" + Mutability.READ_ONLY + "\' attribute and cannot be changed", null, ScimType.RFC7644.INVALID_PATH); } if (!PatchOp.REMOVE.equals(patchOp) && Mutability.IMMUTABLE.equals(schemaAttribute.getMutability()) && attribute != null) { throw new BadRequestException("the attribute \'" + schemaAttribute.getScimNodeName() + "\' is \'" + Mutability.IMMUTABLE + "\' and is not unassigned. Current value is: " + attribute.asText(), null, ScimType.RFC7644.INVALID_PATH); } } /** * will add, replace or remove the specified values based on the given path-attribute * * @param resource the resource to which the values should be added * @param values the values that should be added into the resource * @return true if an effective change was made, false else */ public boolean handleOperationValues(ResourceNode resource, List values) { if (path instanceof PatchExtensionAttributePath) { return handleExtensionOperation(resource, values); } return handlePathAttributeOperation(resource, values); } /** * handles path references that will directly address an extension of a resource * * @param resource the resource to which the values should be handled * @param values the values that should contain a single element that contains the referenced extension, or * should be empty. * @return true if an effective change was made, false else */ private boolean handleExtensionOperation(ResourceNode resource, List values) { final boolean areTooManyValuesPresent = values.size() > 1; if (areTooManyValuesPresent) { throw new BadRequestException(String.format("Patch request contains too many values. Expected a single value " + "representing an extension but got several. \'%s\'", values)); } boolean changeMade; final boolean addOrReplaceValue = !PatchOp.REMOVE.equals(patchOp); ObjectNode currentNode = (ObjectNode)resource.get(path.getFullName()); if (addOrReplaceValue) { ObjectNode extensionNode = (ObjectNode)JsonHelper.readJsonDocument(values.get(0)); if (extensionNode == null) { throw new BadRequestException(String.format("Received invalid data on patch values. Expected an extension " + "resource but got: \'%s\'", values.get(0))); } changeMade = !extensionNode.equals(currentNode); if (extensionNode.isEmpty()) { resource.remove(path.getFullName()); } else { resource.set(path.getFullName(), extensionNode); } } else { changeMade = currentNode != null && !currentNode.isEmpty(); resource.remove(path.getFullName()); } return changeMade; } /** * handles patch requests whose path will directly address an attribute of either the main resource or an * extension * * @param resource the resource to which the values should be handled * @param values the values that should be handled * @return true if an effective change was made, false else */ private boolean handlePathAttributeOperation(ResourceNode resource, List values) { validateRequest(values); String[] fullAttributeNames = getAttributeNames(); String firstAttributeName = fullAttributeNames[0]; SchemaAttribute schemaAttribute = getSchemaAttribute(firstAttributeName); boolean isExtension = resourceType.getSchemaExtensions() .stream() .anyMatch(ext -> ext.getSchema().equals(schemaAttribute.getResourceUri())); ObjectNode currentParent = resource; if (isExtension) { addExtensionToSchemas(resource, patchOp, schemaAttribute); currentParent = (ObjectNode)currentParent.get(schemaAttribute.getResourceUri()); if (currentParent == null) { currentParent = new ScimObjectNode(); resource.set(schemaAttribute.getResourceUri(), currentParent); } } JsonNode firstAttribute = getAttributeFromObject(currentParent, firstAttributeName); // if the attribute is null we know that this is a simple attribute if (firstAttribute == null && !Type.COMPLEX.equals(schemaAttribute.getType()) || (firstAttribute != null && !firstAttribute.isArray() && !firstAttribute.isObject())) { boolean changeWasMade = handleSimpleNode(schemaAttribute, currentParent, values); removeExtensionIfEmpty(resource, schemaAttribute, isExtension, currentParent); return changeWasMade; } else if (firstAttribute != null && firstAttribute.isArray()) { return handlePatchOperationOnMultiValued(resource, values, fullAttributeNames, schemaAttribute, isExtension, currentParent, firstAttribute); } else if (Type.COMPLEX.equals(schemaAttribute.getType())) { return handlePatchOperationOnComplex(resource, values, fullAttributeNames, schemaAttribute, isExtension, currentParent, firstAttribute); } return false; } /** * handles a single patch operation on a complex type * * @param resource the resource that is currently processed * @param values the values that should be added or replaced * @param fullAttributeNames the full attribute names from the top level to leaf level e.g. ["emails", * "emails.value"] * @param schemaAttribute the schema attribute definition of the value that should be replaced * @param isExtension if this operation is executed on an extension * @param currentParent if {@code isExtension} is true this is the extension node otherwise this node is * equals to {@code resource} * @param firstAttribute the attribute extracted from the {@code currentParent} * @return true if an effective change was made, false else */ private boolean handlePatchOperationOnComplex(ResourceNode resource, List values, String[] fullAttributeNames, SchemaAttribute schemaAttribute, boolean isExtension, ObjectNode currentParent, JsonNode firstAttribute) { if (PatchOp.REMOVE.equals(patchOp) && fullAttributeNames.length == 1 && path.getSubAttributeName() == null) { evaluatePatchPathOperation(schemaAttribute, firstAttribute); if (firstAttribute == null) { throw new BadRequestException(String.format("No target found for path-filter \'%s\'", path), ScimType.RFC7644.NO_TARGET); } else { currentParent.remove(schemaAttribute.getName()); removeExtensionIfEmpty(resource, schemaAttribute, isExtension, currentParent); return true; } } if (patchConfig.isMsAzureComplexSimpleValueWorkaroundActive()) { MsAzurePatchComplexValueRebuilder workaroundHandler = new MsAzurePatchComplexValueRebuilder(schemaAttribute, values); values = workaroundHandler.fixValues(); } boolean changeMade = handleComplexAttribute(schemaAttribute, currentParent, fullAttributeNames, values); removeExtensionIfEmpty(resource, schemaAttribute, isExtension, currentParent); return changeMade; } /** * handles a single patch operation on a multi valued type * * @param resource the resource that is currently processed * @param values the values that should be added or replaced * @param fullAttributeNames the full attribute names from the top level to leaf level e.g. ["emails", * "emails.value"] * @param schemaAttribute the schema attribute definition of the value that should be replaced * @param isExtension if this operation is executed on an extension * @param currentParent if {@code isExtension} is true this is the extension node otherwise this node is * equals to {@code resource} * @param firstAttribute the attribute extracted from the {@code currentParent} * @return true if an effective change was made, false else */ private boolean handlePatchOperationOnMultiValued(ResourceNode resource, List values, String[] fullAttributeNames, SchemaAttribute schemaAttribute, boolean isExtension, ObjectNode currentParent, JsonNode firstAttribute) { if (PatchOp.REMOVE.equals(patchOp) && fullAttributeNames.length == 1 && path.getSubAttributeName() == null && path.getChild() == null) { evaluatePatchPathOperation(schemaAttribute, firstAttribute.size() == 0 ? null : firstAttribute); int sizeBefore = currentParent.size(); JsonNode removedNode = currentParent.remove(schemaAttribute.getName()); boolean effectiveChangeMade = false; if (sizeBefore > currentParent.size() && removedNode.size() != 0) { effectiveChangeMade = true; } else { throw new BadRequestException(String.format("No target found for path-filter \'%s\'", path), ScimType.RFC7644.NO_TARGET); } removeExtensionIfEmpty(resource, schemaAttribute, isExtension, currentParent); return effectiveChangeMade; } boolean changeWasMade = handleMultiValuedAttribute(schemaAttribute, (ArrayNode)firstAttribute, fullAttributeNames, values); if (firstAttribute.size() == 0) { resource.remove(schemaAttribute.getName()); removeExtensionIfEmpty(resource, schemaAttribute, isExtension, currentParent); } return changeWasMade; } /** * in case that an extension object is empty after a remove operation the extension will be removed from the * resource * * @param resource the resource that owns the extension attribute * @param schemaAttribute the attribute of the extension that was removed * @param isExtension if the current operation is executed on an extension or not * @param currentParent if the value {@code isExtension} is true this is the extension object otherwise it is * equals to {@code resource} */ private void removeExtensionIfEmpty(ResourceNode resource, SchemaAttribute schemaAttribute, boolean isExtension, ObjectNode currentParent) { if (isExtension && currentParent.size() == 0) { resource.remove(schemaAttribute.getResourceUri()); resource.removeSchema(schemaAttribute.getResourceUri()); } } /** * adds an extension uri to the schemas attribute under the condition that the operation is not a remove * operation and that the extension uri is not already present within the schemas * * @param resource the resource node to which the extension uri should be added * @param patchOp the current patch operation * @param schemaAttribute the schema attribute that is the target of operation */ private void addExtensionToSchemas(ResourceNode resource, PatchOp patchOp, SchemaAttribute schemaAttribute) { if (patchOp.equals(PatchOp.REMOVE)) { return; } resource.addSchema(schemaAttribute.getResourceUri()); } /** * adds or replaces a simple node in the given object node * * @param schemaAttribute the attribute schema definition * @param objectNode the object node into which the new node should be added or replaced * @param values the values that should be added to the node. This list must not contain more than a single * entry * @return true if an effective change was made, false else */ protected boolean handleSimpleNode(SchemaAttribute schemaAttribute, ObjectNode objectNode, List values) { if (!PatchOp.REMOVE.equals(patchOp) && values.size() > 1 && !schemaAttribute.isMultiValued()) { throw new BadRequestException("found multiple values for simple attribute \'" + schemaAttribute.getFullResourceName() + "\': " + String.join(",", values), null, ScimType.RFC7644.INVALID_VALUE); } JsonNode oldNode = objectNode.get(schemaAttribute.getName()); evaluatePatchPathOperation(schemaAttribute, oldNode); if (PatchOp.REMOVE.equals(patchOp)) { boolean isSimpleNode = schemaAttribute.getParent() == null; boolean isSimpleNodeInMultivaluedComplex = schemaAttribute.getParent() != null && schemaAttribute.getParent().isMultiValued(); if ((oldNode == null && isSimpleNode) || (oldNode == null && !isSimpleNodeInMultivaluedComplex)) { throw new BadRequestException(String.format("No target found for path-filter \'%s\'", path), ScimType.RFC7644.NO_TARGET); } else { objectNode.remove(schemaAttribute.getName()); return true; } } JsonNode newNode = createNewNode(schemaAttribute, values.get(0)); if (!newNode.equals(oldNode)) { objectNode.set(schemaAttribute.getName(), newNode); return true; } else { return false; } } /** * adds or replaces complex attributes * * @param schemaAttribute the attribute schema definition * @param resource the resource into which the complex type should be added or replaced * @param fullAttributeNames contains all attribute names. It starts with the name of the complex attributes * and might follow with the sub-attribute of the complex type if specified. the full name contains * the full resource uri * @param values the values that should be added to the complex type * @return true if an effective change has been made, false else */ private boolean handleComplexAttribute(SchemaAttribute schemaAttribute, ObjectNode resource, String[] fullAttributeNames, List values) { if (fullAttributeNames.length > 1) { ObjectNode complexNode = (ObjectNode)resource.get(schemaAttribute.getName()); evaluatePatchPathOperation(schemaAttribute, complexNode); return handleComplexSubAttributePathReference(schemaAttribute, resource, fullAttributeNames[1], values); } else { return handleDirectComplexPathReference(schemaAttribute, resource, values); } } /** * will handle a direct complex path reference e.g. "name" * * @param schemaAttribute the schema attribute definition of the complex attribute * @param resource the resource that is currently processed * @param values the values that will be added or replaced * @return true if an effective change has been made, false else */ private boolean handleDirectComplexPathReference(SchemaAttribute schemaAttribute, ObjectNode resource, List values) { ObjectNode complexNode = (ObjectNode)resource.get(schemaAttribute.getName()); evaluatePatchPathOperation(schemaAttribute, complexNode); if (values.size() != 1 || StringUtils.isBlank(values.get(0))) { throw new BadRequestException("found multiple or no values for non multi valued complex type \'" + schemaAttribute.getFullResourceName() + "\': \n\t" + String.join(",", values), null, ScimType.RFC7644.INVALID_VALUE); } JsonNode newNode = JsonHelper.readJsonDocument(values.get(0)); if (newNode == null || !newNode.isObject()) { throw new BadRequestException("given value is not a complex json representation for attribute \'" + schemaAttribute.getFullResourceName() + "\':\n\t" + String.join(",", values), null, ScimType.RFC7644.INVALID_VALUE); } PatchFilterResolver filterResolver = new PatchFilterResolver(); boolean hasFilterExpression = path.getChild() != null; if (complexNode != null && hasFilterExpression && !filterResolver.isNodeMatchingFilter(complexNode, path).isPresent()) { throw new BadRequestException(String.format("No target found for path-filter \'%s\'", path), ScimType.RFC7644.NO_TARGET); } boolean changeWasMade = false; if (PatchOp.ADD.equals(patchOp)) { JsonNode oldNode = resource.get(schemaAttribute.getName()); newNode = mergeObjectNodes((ObjectNode)newNode, oldNode); resource.set(schemaAttribute.getName(), newNode); changeWasMade = !newNode.equals(oldNode); } else if (PatchOp.REPLACE.equals(patchOp)) { resource.set(schemaAttribute.getName(), newNode); changeWasMade = true; } return changeWasMade; } /** * handles a complex sub attribute path reference e.g. "name.givenName" * * @param schemaAttribute the schema attribute definition of the sub attribute * @param resource the resource that is currently processed * @param fullAttributeName the attribute name e.g. name.givenName * @param values the values that should be added or replaced * @return true if an effective change was made on this resource false else */ private boolean handleComplexSubAttributePathReference(SchemaAttribute schemaAttribute, ObjectNode resource, String fullAttributeName, List values) { SchemaAttribute subAttribute = getSchemaAttribute(fullAttributeName); ObjectNode complexNode = (ObjectNode)resource.get(schemaAttribute.getName()); if (complexNode == null) { complexNode = new ScimObjectNode(schemaAttribute); resource.set(schemaAttribute.getName(), complexNode); } Optional matchingNode = new PatchFilterResolver().isNodeMatchingFilter(complexNode, path); if (!matchingNode.isPresent()) { throw new BadRequestException(String.format("No target found for path-filter \'%s\'", path), ScimType.RFC7644.NO_TARGET); } if (handleInnerComplexAttribute(subAttribute, complexNode, values)) { if (complexNode.size() == 0) { resource.remove(schemaAttribute.getName()); } return true; } else if (complexNode.size() == 0) { resource.remove(schemaAttribute.getName()); return false; } return false; } /** * extracts the relevant node from the given complex node and adds a value, replaces it or removes it * * @param subAttribute the attribute that should be added, replaced or removed * @param complexNode the parent node of the node that should be added, replaced or removed * @param values the value(s) that should be added, replaced or removed * @return true if an effective change was made, false else */ private boolean handleInnerComplexAttribute(SchemaAttribute subAttribute, ObjectNode complexNode, List values) { if (subAttribute.isMultiValued()) { ArrayNode arrayNode = (ArrayNode)complexNode.get(subAttribute.getName()); if (arrayNode == null) { arrayNode = new ScimArrayNode(subAttribute); complexNode.set(subAttribute.getName(), arrayNode); } if (PatchOp.REPLACE.equals(patchOp)) { arrayNode.removeAll(); } if (PatchOp.REMOVE.equals(patchOp)) { boolean effectiveChange = complexNode.get(subAttribute.getName()).size() != 0; complexNode.remove(subAttribute.getName()); boolean isParentMultivalued = subAttribute.getParent().isMultiValued(); if (!effectiveChange && !isParentMultivalued) { throw new BadRequestException(String.format("No target found for path-filter \'%s\'", path), ScimType.RFC7644.NO_TARGET); } else if (!effectiveChange && isParentMultivalued) { return false; } return true; } else { values.forEach(arrayNode::add); } return true; } else { return handleSimpleNode(subAttribute, complexNode, values); } } /** * merges two object nodes into a single node */ private JsonNode mergeObjectNodes(ObjectNode newNode, JsonNode oldNode) { if (oldNode == null) { return newNode; } oldNode.fields().forEachRemaining(stringJsonNodeEntry -> { final String key = stringJsonNodeEntry.getKey(); final JsonNode value = stringJsonNodeEntry.getValue(); JsonNode newSubNode = newNode.get(key); if (newSubNode == null) { newNode.set(key, value); } else if (newSubNode.isArray()) { // did it in this way to preserve the original array order and to append the new values newSubNode.forEach(((ArrayNode)value)::add); newNode.set(key, value); } }); return newNode; } /** * handles multi valued complex nodes * * @param schemaAttribute the schema attribute definition of the top level node * @param multiValued the array node that is represented by the {@code schemaAttribute} * @param fullAttributeNames the array of full attribute names with their resourceUris e.g.
* *
   *              urn:gold:params:scim:schemas:custom:2.0:AllTypes:name
* urn:gold:params:scim:schemas:custom:2.0:AllTypes:name.givenName *
* * @param values the values that should be added to the multi valued complex type * @return true if an effective change has been made, false else */ private boolean handleMultiValuedAttribute(SchemaAttribute schemaAttribute, ArrayNode multiValued, String[] fullAttributeNames, List values) { if (Type.COMPLEX.equals(schemaAttribute.getType())) { if (fullAttributeNames.length > 1) { return handleMultiComplexSubAttributePath(multiValued, fullAttributeNames[1], values); } else { evaluatePatchPathOperation(schemaAttribute, multiValued.size() == 0 ? null : multiValued); return handleDirectMultiValuedComplexPathReference(multiValued, values); } } else { if (PatchOp.REPLACE.equals(patchOp)) { multiValued.removeAll(); } for ( String value : values ) { multiValued.add(createNewNode(schemaAttribute, value)); } return true; } } /** * handles a direct multivalued complex path reference e.g. "emails" or "emails[type eq "work"]" * * @param multiValued the multi values array node that holds the complex nodes * @param values the values that should be added or replaced on all matching nodes * @return true if an effective change has been made, false else */ private boolean handleDirectMultiValuedComplexPathReference(ArrayNode multiValued, List values) { List matchingComplexNodes = resolveFilter(multiValued, path); if (PatchOp.REMOVE.equals(patchOp)) { boolean changeWasMade = false; for ( int i = matchingComplexNodes.size() - 1 ; i >= 0 ; i-- ) { multiValued.remove(matchingComplexNodes.get(i).getIndex()); changeWasMade = true; } return changeWasMade; } if (matchingComplexNodes.isEmpty() && path.getChild() != null) { throw new BadRequestException(String.format("Cannot \'%s\' value on path \'%s\' for no matching object was found", patchOp, path), null, ScimType.RFC7644.NO_TARGET); } if (PatchOp.REPLACE.equals(patchOp)) { for ( int i = matchingComplexNodes.size() - 1 ; i >= 0 ; i-- ) { IndexNode indexNode = matchingComplexNodes.get(i); multiValued.remove(indexNode.getIndex()); } } for ( String value : values ) { try { JsonNode jsonNode = JsonHelper.readJsonDocument(value); JsonNode primary = jsonNode.get(AttributeNames.RFC7643.PRIMARY); checkForPrimary(multiValued, primary != null && primary.booleanValue()); multiValued.add(jsonNode); } catch (IOException ex) { throw new BadRequestException("the value must be a whole complex type json structure but was: \'" + value + "\'", ex, ScimType.RFC7644.INVALID_VALUE); } } return true; } /** * this method will check if the current operation adds a new primary value and will set the original primary * to false if such a value exists * * @param multiValued the multivalued complex array that might hold any primary values * @param primary if the new value is primary or not */ private void checkForPrimary(ArrayNode multiValued, boolean primary) { if (!primary) { return; } multiValued.forEach(jsonNode -> { ((ObjectNode)jsonNode).remove(AttributeNames.RFC7643.PRIMARY); }); } /** * handles a multivalued complex type with a path reference that points to a sub attribute of the multi-valued * complex type e.g. emails.value * * @param multiValued the array of the multivalued complex node * @param fullAttributeName the full name of the sub attribute * @param values the values that should be added or replaced * @return true if an effective change has been made, false else */ private boolean handleMultiComplexSubAttributePath(ArrayNode multiValued, String fullAttributeName, List values) { SchemaAttribute subAttribute = RequestUtils.getSchemaAttributeByAttributeName(resourceType, fullAttributeName); List matchingComplexNodes = resolveFilter(multiValued, path); if (patchConfig.isMsAzureFilterWorkaroundActive()) { if (matchingComplexNodes.isEmpty()) { MsAzurePatchFilterWorkaround filterWorkaround = new MsAzurePatchFilterWorkaround(); ScimObjectNode newNode = filterWorkaround.createAttributeFromPatchFilter(path); if (!newNode.isEmpty()) { multiValued.add(newNode); matchingComplexNodes.add(new IndexNode(multiValued.size() - 1, newNode)); } } } AtomicBoolean changeWasMade = new AtomicBoolean(false); if (AttributeNames.RFC7643.PRIMARY.equals(subAttribute.getName())) { checkForPrimary(multiValued, Boolean.parseBoolean(values.get(0))); } if (path.getChild() != null && matchingComplexNodes.isEmpty()) { // a filter expression was present and no matches were found e.g. (emails[type eq "work"].type) throw new BadRequestException(String.format("No target found for path-filter \'%s\'", path), ScimType.RFC7644.NO_TARGET); } if (path.getChild() == null && matchingComplexNodes.isEmpty() && PatchOp.REMOVE.equals(patchOp)) { // no filter expression was present e.g. (emails.type) return false; } for ( int i = 0 ; i < matchingComplexNodes.size() ; i++ ) { ObjectNode complexNode = matchingComplexNodes.get(i).getObjectNode(); changeWasMade.weakCompareAndSet(false, handleInnerComplexAttribute(subAttribute, complexNode, values)); if (complexNode.size() == 0) { multiValued.remove(matchingComplexNodes.get(i).getIndex()); } } return changeWasMade.get(); } /** * this method will extract all complex types from the given array node that do match the filter * * @param multiValuedComplex the multi valued complex node * @param path the filter expression that must be resolved to get the matching nodes * @return the list of nodes that should be modified */ private List resolveFilter(ArrayNode multiValuedComplex, AttributePathRoot path) { PatchFilterResolver patchFilterResolver = new PatchFilterResolver(); List matchingComplexNodes = new ArrayList<>(); for ( int i = 0 ; i < multiValuedComplex.size() ; i++ ) { JsonNode complex = multiValuedComplex.get(i); Optional filteredNode = patchFilterResolver.isNodeMatchingFilter((ObjectNode)complex, path); if (filteredNode.isPresent()) { matchingComplexNodes.add(new IndexNode(i, filteredNode.get())); } } if (path.getChild() != null && matchingComplexNodes.size() == 0) { return new ArrayList<>(); } return matchingComplexNodes; } /** * creates a new json node with the given value * * @param schemaAttribute the attribute schema definition * @param value the value that should be added into the node * @return the simple json node */ private JsonNode createNewNode(SchemaAttribute schemaAttribute, String value) { switch (schemaAttribute.getType()) { case STRING: case DATE_TIME: case REFERENCE: case BINARY: return new ScimTextNode(schemaAttribute, value); case BOOLEAN: return new ScimBooleanNode(schemaAttribute, Boolean.parseBoolean(value)); case INTEGER: Long longVal = Long.parseLong(value); if (longVal == longVal.intValue()) { return new ScimIntNode(schemaAttribute, longVal.intValue()); } else { return new ScimLongNode(schemaAttribute, longVal); } default: return new ScimDoubleNode(schemaAttribute, Double.parseDouble(value)); } } /** * will check that the expressions are correctly written for the defined patch operation * * @param values the values of the request */ protected void validateRequest(List values) { validateAttributeType(values); validatePath(path, patchOp, values); } /** * this method will check the the expression send by the client does follow its syntax rules based on the used * operation * * @param path the target expression * @param patchOp the operation * @param values the values (should be empty on delete) */ private void validatePath(AttributePathRoot path, PatchOp patchOp, List values) { switch (patchOp) { case ADD: checkIsValidComplexJson(path, values); break; case REPLACE: validateReplaceOperation(path, values); break; case REMOVE: validateRemoveOperation(path, values); break; } } /** * will validate that no values are present in the values list all other path representations should be valid * except for an empty representation * * @param path the attribute path expression * @param values in remove operation no values should be present */ private void validateRemoveOperation(AttributePathRoot path, List values) { if (values != null && !values.isEmpty()) { throw new BadRequestException("values must not be set for remove operation but was: " + String.join(",", values), null, ScimType.RFC7644.INVALID_VALUE); } if (path == null) { throw new BadRequestException("no target present within the request", null, ScimType.RFC7644.INVALID_PATH); } } /** * will validate that the given attribute path expression is valid for a replace operation * * @param path the attribute path expression * @param values the values that should replace other values */ private void validateReplaceOperation(AttributePathRoot path, List values) { if (values == null || values.size() == 0) { throw new BadRequestException("values parameter must be set for replace operation but was empty", null, ScimType.RFC7644.INVALID_VALUE); } if (StringUtils.isBlank(path.getSubAttributeName()) && path.getChild() != null && !values.stream().allMatch(JsonHelper::isValidJson)) { throw new BadRequestException("the values are expected to be valid json representations for an expression as " + "\'" + path + "\' but was: " + String.join(",\n", values), null, ScimType.RFC7644.INVALID_PATH); } checkIsValidComplexJson(path, values); } /** * verifies that the values are valid json representations if we have an injection into a complex type without * a sub-attribute * * @param path the target of the expression * @param values the values should be added or replaced */ private void checkIsValidComplexJson(AttributePathRoot path, List values) { String[] namePath = path.getShortName().split("\\."); // emails or name if (path.getChild() == null && Type.COMPLEX.equals(path.getSchemaAttribute().getType()) && namePath.length == 1 && !values.stream().allMatch(JsonHelper::isValidJson)) { throw new BadRequestException("the value parameters must be valid json representations but was \'" + String.join(",", values) + "\'", null, ScimType.RFC7644.INVALID_VALUE); } } /** * checks that if the attribute is a simple type and not multivalued that only a single attribute is allowed * in the values parameter of the patch request * * @param values the values parameter that is under test */ private void validateAttributeType(List values) { switch (schemaAttribute.getType()) { case STRING: case DATE_TIME: case REFERENCE: case BOOLEAN: case INTEGER: case DECIMAL: if (!schemaAttribute.isMultiValued() && values.size() > 1) { throw new BadRequestException("several values found for non multivalued node of type \'" + schemaAttribute.getType() + "\'", null, ScimType.RFC7644.INVALID_VALUE); } break; } } /** * will get the fully qualified attribute names */ private String[] getAttributeNames() { String attributeName = path.getShortName() + (StringUtils.isBlank(path.getSubAttributeName()) ? "" : "." + path.getSubAttributeName()); String[] attributeNames = attributeName.split("\\."); String resourceUri = path.getResourceUri() == null ? "" : path.getResourceUri() + ":"; attributeNames[0] = resourceUri + attributeNames[0]; for ( int i = 1 ; i < attributeNames.length ; i++ ) { attributeNames[i] = attributeNames[i - 1] + "." + attributeNames[i]; } return attributeNames; } /** * retrieves the schema attribute definition of the top loevel node of the patch attribute. The top level node * would be e.g. 'name' in the following representation "name.givenName" */ private SchemaAttribute getSchemaAttribute() { if (this.schemaAttribute == null) { this.schemaAttribute = getSchemaAttribute(path.getFullName()); } return this.schemaAttribute; } /** * a helper class that is used in case of filtering. We will also hold the index of the filtered nodes */ private static class IndexNode { /** * the index of a filtered node */ private int index; /** * a filtered node */ private ObjectNode objectNode; @java.lang.SuppressWarnings("all") public int getIndex() { return this.index; } @java.lang.SuppressWarnings("all") public ObjectNode getObjectNode() { return this.objectNode; } @java.lang.SuppressWarnings("all") public IndexNode(final int index, final ObjectNode objectNode) { this.index = index; this.objectNode = objectNode; } } /** * the specified path where the value should be added */ @java.lang.SuppressWarnings("all") protected AttributePathRoot getPath() { return this.path; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy