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

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

package de.captaingoldfish.scim.sdk.server.patch;

import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;

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.resources.ResourceNode;
import de.captaingoldfish.scim.sdk.common.resources.base.ScimArrayNode;
import de.captaingoldfish.scim.sdk.common.resources.base.ScimObjectNode;
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.patch.msazure.MsAzurePatchExtensionResourceRebuilder;
import de.captaingoldfish.scim.sdk.server.schemas.ResourceType;
import de.captaingoldfish.scim.sdk.server.utils.RequestUtils;


/**
 * author Pascal Knueppel 
* created at: 30.10.2019 - 08:49
*
* this class will handle the in which the patch-add operation does not define a target and the value is * represented by the resource itself:
*
* *
 *    The result of the add operation depends upon what the target location
 *    indicated by "path" references:
 *
 *    o  If omitted, the target location is assumed to be the resource
 *       itself.  The "value" parameter contains a set of attributes to be
 *       added to the resource.
 * 
*/ public class PatchResourceHandler extends AbstractPatch { /** * the patch configuration of the current request. Used to check if workarounds are activated */ private final PatchConfig patchConfig; /** * tells us if the current operation is an add or a replace operation. */ private PatchOp patchOp; public PatchResourceHandler(PatchConfig patchConfig, ResourceType resourceType, PatchOp op) { super(resourceType); this.patchConfig = patchConfig; this.patchOp = op; } /** * adds the values of the patch operation into the given resource node * * @param resource the resource node into which the values should be added * @param patchJsonDocument the patch operation resource from which the values should be added into the * resource node * @param extensionUri this extensionUri is used for resolving extensions in the resource if name conflicts do * exist we need the fully qualified name for verifying the attributes. */ public boolean addResourceValues(ObjectNode resource, JsonNode patchJsonDocument, String extensionUri) { if (patchJsonDocument == null || patchJsonDocument.size() == 0) { throw new BadRequestException("no attributes present in value-resource in patch operation", null, ScimType.RFC7644.INVALID_VALUE); } AtomicBoolean changeWasMade = new AtomicBoolean(false); JsonHelper.removeAttribute(patchJsonDocument, AttributeNames.RFC7643.SCHEMAS); patchJsonDocument.fields().forEachRemaining(stringJsonNodeEntry -> { String key = stringJsonNodeEntry.getKey(); JsonNode value = stringJsonNodeEntry.getValue(); ResourceType.SchemaExtension extensionRef = resourceType.getSchemaExtensions() .stream() .filter(ext -> key.equals(ext.getSchema()) // ms azure workaround // https://github.com/Captain-P-Goldfish/SCIM-SDK/issues/193 || key.startsWith(ext.getSchema())) .findAny() .orElse(null); if (extensionRef != null) { JsonNode extensionResource = resource.get(extensionRef.getSchema()); if (extensionResource == null) { extensionResource = new ScimObjectNode(); resource.set(extensionRef.getSchema(), extensionResource); ((ResourceNode)resource).addSchema(key); } // ms azure workaround // https://github.com/Captain-P-Goldfish/SCIM-SDK/issues/193 JsonNode effectiveValue = value; // if the attribute is not identical to the extension-schema-reference but starts with the // extension-schema-reference boolean executeMsAzureWorkaround = !key.equals(extensionRef.getSchema()) && key.startsWith(extensionRef.getSchema()); if (executeMsAzureWorkaround) { MsAzurePatchExtensionResourceRebuilder workaroundHandler = new MsAzurePatchExtensionResourceRebuilder(resourceType); effectiveValue = workaroundHandler.rebuildResource(extensionRef, key, value); } if (effectiveValue.isEmpty()) { resource.remove(extensionRef.getSchema()); changeWasMade.compareAndSet(false, extensionResource.size() > 0); } else { final boolean changeMade = addResourceValues((ObjectNode)extensionResource, effectiveValue, extensionRef.getSchema()); changeWasMade.compareAndSet(false, changeMade); } } else { SchemaAttribute schemaAttribute; try { schemaAttribute = getSchemaAttribute((extensionUri == null ? resourceType.getSchema() : extensionUri) + ":" + key); } catch (BadRequestException ex) { if (patchConfig.isIgnoreUnknownAttribute() && ScimType.RFC7644.INVALID_PATH.equals(ex.getScimType())) { changeWasMade.set(false); return; } else { throw ex; } } boolean resourceChanged; boolean isReadOnlyAndSet = isReadOnlyAndSet(resource, schemaAttribute); if (Type.COMPLEX.equals(schemaAttribute.getType())) { resourceChanged = addComplexAttribute(resource, schemaAttribute, value, extensionUri); } else if (schemaAttribute.isMultiValued()) { resourceChanged = addMultivaluedAttribute(resource, schemaAttribute, value); } else { resourceChanged = addSimpleAttribute(resource, schemaAttribute, value); } verifyReadOnlyNotModified(schemaAttribute, resourceChanged, isReadOnlyAndSet); changeWasMade.weakCompareAndSet(false, resourceChanged); } }); return changeWasMade.get(); } /** * determines if the given json node is read-only or immutable and previously set, if so returns true * * @param jsonNode the node that should be verified * @param schemaAttribute the schema definition of the node */ private boolean isReadOnlyAndSet(JsonNode jsonNode, SchemaAttribute schemaAttribute) { return Mutability.READ_ONLY.equals(schemaAttribute.getMutability()) || Mutability.IMMUTABLE.equals(schemaAttribute.getMutability()) && jsonNode.get(schemaAttribute.getName()) != null; } /** * verifies that the given json node is neither readOnly nor immutable and already set * * @param schemaAttribute the schema definition of the node * @param resourceChanged true if the resource was modified * @param isReadOnlyAndSet true if the field is read-only OR immutable and previously set */ private void verifyReadOnlyNotModified(SchemaAttribute schemaAttribute, boolean resourceChanged, boolean isReadOnlyAndSet) { if (resourceChanged && isReadOnlyAndSet) { throw new BadRequestException("attribute with name '" + schemaAttribute.getFullResourceName() + "' cannot be written it has a mutability of '" + schemaAttribute.getMutability() + "'", null, ScimType.RFC7644.MUTABILITY); } } /** * verifies that the given json node is neither readOnly nor immutable and already set * * @param jsonNode the node that should be verified * @param schemaAttribute the schema definition of the node */ private void verifyImmutableAndReadOnly(JsonNode jsonNode, SchemaAttribute schemaAttribute) { if (Mutability.READ_ONLY.equals(schemaAttribute.getMutability()) || jsonNode.get(schemaAttribute.getName()) != null && Mutability.IMMUTABLE.equals(schemaAttribute.getMutability())) { throw new BadRequestException("attribute with name '" + schemaAttribute.getFullResourceName() + "' cannot be written it has a mutability of '" + schemaAttribute.getMutability() + "'", null, ScimType.RFC7644.MUTABILITY); } } /** * adds a simple attribute to the given resource. If the attribute does already exist it is replaced if the * value is different from before * * @param resource the resource in which the attribute should be added or replaced * @param schemaAttribute the schema attribute definition * @param value the value that should be added to the resource * @return true if a change was made, false else */ private boolean addSimpleAttribute(ObjectNode resource, SchemaAttribute schemaAttribute, JsonNode value) { if (!value.equals(resource.get(schemaAttribute.getName()))) { resource.set(schemaAttribute.getName(), value); return true; } else { return false; } } /** * adds a complex attribute to the given resource * * @param resource the resource to which the attribute should be added * @param schemaAttribute the schema attribute definition * @param value the value that should be added to the resource * @param extensionUri * @return true if a change was made, false else */ private boolean addComplexAttribute(ObjectNode resource, SchemaAttribute schemaAttribute, JsonNode value, String extensionUri) { if (schemaAttribute.isMultiValued()) { return addMultiValuedComplexNode(resource, schemaAttribute, value, extensionUri); } else { if (value.equals(resource.get(schemaAttribute.getName()))) { // 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. return false; } PatchOp effectivePatchOp = patchOp; if (patchConfig.isActivateSailsPointWorkaround() && PatchOp.REPLACE.equals(patchOp)) { effectivePatchOp = PatchOp.ADD; } if (PatchOp.ADD.equals(effectivePatchOp)) { ObjectNode complexNode; if (resource.get(schemaAttribute.getName()) == null) { complexNode = new ScimObjectNode(schemaAttribute); } else { complexNode = (ObjectNode)resource.get(schemaAttribute.getName()); } AtomicBoolean changeWasMade = new AtomicBoolean(false); value.fields().forEachRemaining(stringJsonNodeEntry -> { final String key = stringJsonNodeEntry.getKey(); final JsonNode newValue = stringJsonNodeEntry.getValue(); final String uri = extensionUri == null ? resourceType.getSchema() : extensionUri; final String fullName = uri + ":" + schemaAttribute.getName() + "." + key; SchemaAttribute subAttribute = RequestUtils.getSchemaAttributeByAttributeName(resourceType, fullName); verifyImmutableAndReadOnly(complexNode, subAttribute); if (!newValue.equals(complexNode.get(key))) { complexNode.set(key, newValue); changeWasMade.set(true); } }); if (changeWasMade.get()) { resource.set(schemaAttribute.getName(), complexNode); } return changeWasMade.get(); } else { resource.set(schemaAttribute.getName(), value); return true; } } } /** * adds a new value to the specified multi valued complex type or creates the node if the node does not exist * yet * * @param resource the resource that should be modified * @param schemaAttribute the schema attribute definition * @param value the array node whose values should be added to the resource * @param extensionUri * @return always true */ private boolean addMultiValuedComplexNode(ObjectNode resource, SchemaAttribute schemaAttribute, JsonNode value, String extensionUri) { JsonNode multiValuedComplexNode = resource.get(schemaAttribute.getName()); ArrayNode arrayNode; boolean changeWasMade = true; if (multiValuedComplexNode == null) { arrayNode = new ScimArrayNode(schemaAttribute); } else { arrayNode = (ArrayNode)multiValuedComplexNode; if (PatchOp.REPLACE.equals(patchOp)) { if (arrayNode.equals(value)) { changeWasMade = false; } arrayNode.removeAll(); } } Optional primaryNode = getPrimaryFromMultiComplex(arrayNode); AtomicBoolean newPrimaryNodeDetected = new AtomicBoolean(false); value.forEach(complex -> { complex.fields().forEachRemaining(stringJsonNodeEntry -> { String fullName = (extensionUri == null ? resourceType.getSchema() : extensionUri) + ":" + schemaAttribute.getName() + "." + stringJsonNodeEntry.getKey(); SchemaAttribute subAttribute = RequestUtils.getSchemaAttributeByAttributeName(resourceType, fullName); verifyImmutableAndReadOnly(complex, subAttribute); }); JsonNode prime = complex.get(AttributeNames.RFC7643.PRIMARY); boolean primaryFound = prime != null && prime.booleanValue(); if (newPrimaryNodeDetected.get()) { throw new BadRequestException("Found 2 primary values in the new dataset of node: " + schemaAttribute.getFullResourceName(), null, ScimType.RFC7644.INVALID_VALUE); } newPrimaryNodeDetected.weakCompareAndSet(false, primaryFound); arrayNode.add(complex); }); if (newPrimaryNodeDetected.get()) { primaryNode.ifPresent(complex -> JsonHelper.removeAttribute(complex, AttributeNames.RFC7643.PRIMARY)); } resource.set(schemaAttribute.getName(), arrayNode); return changeWasMade; } /** * searches for the complex type that holds the primary=true value and returns it * * @param arrayNode the multi valued complex node * @return the primary node or an empty */ private Optional getPrimaryFromMultiComplex(ArrayNode arrayNode) { for ( JsonNode complex : arrayNode ) { JsonNode primaryNode = complex.get(AttributeNames.RFC7643.PRIMARY); if (primaryNode != null && primaryNode.booleanValue()) { return Optional.of(complex); } } return Optional.empty(); } /** * adds a simple multi valued attribute to the resource * * @param resource the resource to which the simple multi valued attribute should be added * @param schemaAttribute the schema attribute definition * @param value the value(s) that should be added * @return always true */ private boolean addMultivaluedAttribute(ObjectNode resource, SchemaAttribute schemaAttribute, JsonNode value) { ArrayNode arrayNode; if (resource.get(schemaAttribute.getName()) == null) { arrayNode = new ScimArrayNode(schemaAttribute); } else { arrayNode = (ArrayNode)resource.get(schemaAttribute.getName()); } for ( JsonNode jsonNode : value ) { arrayNode.add(jsonNode); } resource.set(schemaAttribute.getName(), arrayNode); return true; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy