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

de.captaingoldfish.scim.sdk.server.endpoints.BulkEndpoint Maven / Gradle / Ivy

// Generated by delombok at Thu Nov 02 20:38:53 CET 2023
package de.captaingoldfish.scim.sdk.server.endpoints;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import com.fasterxml.jackson.databind.JsonNode;
import de.captaingoldfish.scim.sdk.common.constants.AttributeNames;
import de.captaingoldfish.scim.sdk.common.constants.EndpointPaths;
import de.captaingoldfish.scim.sdk.common.constants.HttpHeader;
import de.captaingoldfish.scim.sdk.common.constants.HttpStatus;
import de.captaingoldfish.scim.sdk.common.constants.SchemaUris;
import de.captaingoldfish.scim.sdk.common.constants.ScimType;
import de.captaingoldfish.scim.sdk.common.constants.enums.HttpMethod;
import de.captaingoldfish.scim.sdk.common.etag.ETag;
import de.captaingoldfish.scim.sdk.common.exceptions.BadRequestException;
import de.captaingoldfish.scim.sdk.common.exceptions.ConflictException;
import de.captaingoldfish.scim.sdk.common.exceptions.NotImplementedException;
import de.captaingoldfish.scim.sdk.common.exceptions.PreconditionFailedException;
import de.captaingoldfish.scim.sdk.common.exceptions.ScimException;
import de.captaingoldfish.scim.sdk.common.request.BulkRequest;
import de.captaingoldfish.scim.sdk.common.request.BulkRequestOperation;
import de.captaingoldfish.scim.sdk.common.resources.ServiceProvider;
import de.captaingoldfish.scim.sdk.common.resources.base.ScimObjectNode;
import de.captaingoldfish.scim.sdk.common.resources.complex.BulkConfig;
import de.captaingoldfish.scim.sdk.common.resources.complex.Meta;
import de.captaingoldfish.scim.sdk.common.response.BulkResponse;
import de.captaingoldfish.scim.sdk.common.response.BulkResponseGetOperation;
import de.captaingoldfish.scim.sdk.common.response.BulkResponseOperation;
import de.captaingoldfish.scim.sdk.common.response.CreateResponse;
import de.captaingoldfish.scim.sdk.common.response.ErrorResponse;
import de.captaingoldfish.scim.sdk.common.response.GetResponse;
import de.captaingoldfish.scim.sdk.common.response.ScimResponse;
import de.captaingoldfish.scim.sdk.common.response.UpdateResponse;
import de.captaingoldfish.scim.sdk.common.schemas.Schema;
import de.captaingoldfish.scim.sdk.common.utils.JsonHelper;
import de.captaingoldfish.scim.sdk.server.endpoints.bulkget.BulkGetResolver;
import de.captaingoldfish.scim.sdk.server.endpoints.bulkid.BulkIdResolver;
import de.captaingoldfish.scim.sdk.server.endpoints.bulkid.BulkIdResolverAbstract;
import de.captaingoldfish.scim.sdk.server.schemas.ResourceType;
import de.captaingoldfish.scim.sdk.server.schemas.ResourceTypeFactory;
import de.captaingoldfish.scim.sdk.server.schemas.SchemaFactory;
import de.captaingoldfish.scim.sdk.server.schemas.validation.RequestSchemaValidator;
import de.captaingoldfish.scim.sdk.server.utils.RequestUtils;
import de.captaingoldfish.scim.sdk.server.utils.UriInfos;


/**
 * author Pascal Knueppel 
* created at: 07.11.2019 - 23:48
*
* the bulk endpoint implementation */ class BulkEndpoint { @java.lang.SuppressWarnings("all") private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(BulkEndpoint.class); /** * the resource endpoint that is triggered for each bulk-operation */ private final ResourceEndpoint resourceEndpoint; /** * the service provider configuration to check if the bulk-request meets the requirements of the configuration */ private final ServiceProvider serviceProvider; /** * necessary to get access to the resource-types */ private final ResourceTypeFactory resourceTypeFactory; /** * arbitary code that is executed before the endpoint is called. This might be used to execute authentication * on dedicated resource types */ private final Consumer doBeforeExecution; private final Map originalHttpHeaders; private final Map originalQueryParams; /** * this implementation is used to modify the bulk-operations by replacing the bulkId references for the ids of * the actual references resource after the resource was created */ private final BulkIdResolver bulkIdResolver = new BulkIdResolver(); public BulkEndpoint(ResourceEndpoint resourceEndpoint, ServiceProvider serviceProvider, ResourceTypeFactory resourceTypeFactory, Map originalHttpHeaders, Map originalQueryParams, Consumer doBeforeExecution) { this.resourceEndpoint = resourceEndpoint; this.serviceProvider = serviceProvider; this.resourceTypeFactory = resourceTypeFactory; this.originalHttpHeaders = originalHttpHeaders; this.originalQueryParams = originalQueryParams; this.doBeforeExecution = doBeforeExecution; } /** * resolves a bulk request * * @param requestBody the bulk request body * @param context the current context of the request that might hold authorization details and other context * based information * @return the response of the bulk request */ public BulkResponse bulk(String baseUri, String requestBody, Context context) { BulkRequest bulkRequest = parseAndValidateBulkRequest(requestBody); List operations = bulkRequest.getBulkRequestOperations(); List responseOperations = new ArrayList<>(); final int failOnErrors = RequestUtils.getEffectiveFailOnErrors(bulkRequest); int httpStatus = handleBulkOperationList(baseUri, operations, responseOperations, failOnErrors, context); return BulkResponse.builder().httpStatus(httpStatus).bulkResponseOperation(responseOperations).build(); } /** * handles a list of bulk request operations and will verify that the failOnErrors value is not exceeded * * @param baseUri the base uri of all SCIM endpoints * @param operations the list of request operations * @param responseOperations a predefined list of response operations that will get its elements from this * method * @param failOnErrors the failOnErrors value that must not be exceeded * @param context the current context of the request that might hold authorization details and other context * based information * @return the http status code of the response */ private int handleBulkOperationList(String baseUri, List operations, List responseOperations, int failOnErrors, Context context) { int errorCounter = 0; // this is a security switch in case a bad crafted bulk request will end in an infinite loop this switch is // used to break the infinite loop long maxIterations = serviceProvider.getBulkConfig().getMaxOperations() * 2L; int iterations = 0; while (!operations.isEmpty()) { if (iterations >= maxIterations) { break; } iterations++; BulkRequestOperation requestOperation = operations.get(0); if (errorCounter >= failOnErrors) { operations.remove(0); BulkResponseOperation.BulkResponseOperationBuilder responseBuilder = BulkResponseOperation.builder(); final String errorMessage = String.format("Operation with bulkId \'%s\' at iteration \'%s\' was not handled due to " + "previous failed precondition", requestOperation.getBulkId().orElse(null), iterations); PreconditionFailedException ex = new PreconditionFailedException(errorMessage); responseOperations.add(responseBuilder.status(HttpStatus.PRECONDITION_FAILED) .bulkId(requestOperation.getBulkId().orElse(null)) .response(new ErrorResponse(ex)) .method(requestOperation.getMethod()) .build()); // The service provider stops processing the bulk operation and immediately returns a response to the client continue; } try { validateOperation(requestOperation); } catch (BadRequestException ex) { errorCounter++; BulkResponseOperation.BulkResponseOperationBuilder responseBuilder = BulkResponseOperation.builder(); responseOperations.add(responseBuilder.status(ex.getStatus()).response(new ErrorResponse(ex)).build()); operations.remove(0); continue; } BulkResponseOperation bulkResponseOperation = handleSingleBulkOperation(baseUri, requestOperation, context); if (bulkResponseOperation == null) { // mark this operation as already handled once String operationIdentifier = UUID.randomUUID().toString(); requestOperation.setUniqueIdentifier(operationIdentifier); // the bulk operation references another operation that was not resolved yet, so we move the operation for // another run to the end of the line operations.remove(0); operations.add(requestOperation); continue; } else { operations.remove(0); } boolean isSuccessfulResponseCode = isSuccessResponseCode(requestOperation, bulkResponseOperation); if (!isSuccessfulResponseCode) { errorCounter++; } responseOperations.add(bulkResponseOperation); } int httpStatus = HttpStatus.OK; if (errorCounter >= failOnErrors) { // The service returns an appropriate response status code if too many errors occurred httpStatus = HttpStatus.PRECONDITION_FAILED; } else if (iterations >= maxIterations) { httpStatus = HttpStatus.INTERNAL_SERVER_ERROR; } return httpStatus; } /** * verifies that the correct response code is returned based on the given http method * * @param requestOperation the operation that contains the method to execute * @param responseOperation the response operation that contains the result of the processed operation * @return true if the success-response code was returned, false else */ private boolean isSuccessResponseCode(BulkRequestOperation requestOperation, BulkResponseOperation responseOperation) { switch (requestOperation.getMethod()) { case POST: return HttpStatus.CREATED == responseOperation.getStatus(); case DELETE: return HttpStatus.NO_CONTENT == responseOperation.getStatus(); default: return HttpStatus.OK == responseOperation.getStatus(); } } /** * this method handles a single bulk request operation and will also resolve bulkIds if such references do * exist in the request * * @param baseUri the base uri of all SCIM endpoints * @param operation the operation that should be handled * @param context the current context of the request that might hold authorization details and other context * based information * @return the response for the single bulk request */ private BulkResponseOperation handleSingleBulkOperation(String baseUri, BulkRequestOperation operation, Context context) { HttpMethod httpMethod = operation.getMethod(); Map httpHeaders = getHttpHeadersForBulk(operation); UriInfos operationUriInfo = UriInfos.getRequestUrlInfos(getResourceTypeFactory(), baseUri + operation.getPath(), httpMethod, httpHeaders); operationUriInfo.getQueryParameters().putAll(originalQueryParams); String id = Optional.ofNullable(operationUriInfo.getResourceId()).map(resourceId -> "/" + resourceId).orElse(""); String location = baseUri + operationUriInfo.getResourceEndpoint() + id; String bulkId = operation.getBulkId().orElseGet(() -> { String bId = UUID.randomUUID().toString(); operation.setBulkId(bId); return bId; }); BulkResponseOperation.BulkResponseOperationBuilder responseBuilder = BulkResponseOperation.builder() .bulkId(bulkId) .method(httpMethod) .location(location); try { // this method call will modify the data within the operation-object if it contains bulkId-references Optional bulkIdResolver = resolveBulkIds(operation, httpMethod, operationUriInfo, bulkId); // override the operation uri infos in case that a bulkId within the uri was resolved operationUriInfo = bulkIdResolver.map(BulkIdResolverAbstract::getUriInfos).orElse(operationUriInfo); if (bulkIdResolver.map(BulkIdResolverAbstract::hasAnyBulkIdReferences).orElse(false)) { // will cause this operation to be moved to the end of the request operations list. This happens if the // operation contains a bulkId-reference that has not been resolved yet return null; } } catch (ScimException ex) { return responseBuilder.status(ex.getStatus()).response(new ErrorResponse(ex)).build(); } responseBuilder.bulkId(operation.getBulkId().orElse(null)).method(operation.getMethod()).location(location); ScimResponse scimResponse = resourceEndpoint.resolveRequest(httpMethod, operation.getData().orElse(null), operationUriInfo, doBeforeExecution, context); boolean isResourceResponse = scimResponse instanceof CreateResponse || scimResponse instanceof UpdateResponse; if (isResourceResponse) { Optional.ofNullable(scimResponse.get(AttributeNames.RFC7643.ID)) .map(JsonNode::textValue) .ifPresent(resourceId -> bulkIdResolver.addResolvedBulkId(bulkId, resourceId)); } final boolean isErrorResponse = ErrorResponse.class.isAssignableFrom(scimResponse.getClass()); responseBuilder.status(scimResponse.getHttpStatus()); if (isErrorResponse) { responseBuilder.response(scimResponse); } else { // if we are currently processing a bulk-get response we need to resolve the transitive resources if (scimResponse instanceof GetResponse && serviceProvider.getBulkConfig().isSupportBulkGet()) { // get all transitive children of the retrieved resource final String resourceTypeName = operationUriInfo.getResourceType().getName(); BiFunction bulkGetOpCaller = getTransitiveBulkGetResolver(baseUri, httpHeaders, context); BulkGetResolver bulkGetResolver = BulkGetResolver.builder() .maxResourceLevel(operation.getMaxResourceLevel()) .parentResourceResponse(scimResponse) .resourceTypeFactory(resourceTypeFactory) .resourceType(operationUriInfo.getResourceType()) .callResourceEndpoint(bulkGetOpCaller) .build(); List children = bulkGetResolver.getTransitiveResources(); BulkResponseGetOperation bulkResponseGetOperation = BulkResponseGetOperation.builder() .status(HttpStatus.OK) .resourceId(operationUriInfo.getResourceId()) .resourceType(resourceTypeName) .resource(scimResponse) .children(children) .build(); responseBuilder.response(bulkResponseGetOperation); } else { addResponse(operation, scimResponse, operationUriInfo.getResourceType(), responseBuilder); } } if (isErrorResponse) { if (HttpMethod.POST.equals(operation.getMethod())) { // A "location" attribute that includes the resource's endpoint MUST be returned for all operations // except for failed POST operations (which have no location) responseBuilder.location(null); } } else { id = Optional.ofNullable(scimResponse.get(AttributeNames.RFC7643.ID)) .map(jsonNode -> "/" + jsonNode.textValue()) .orElse(""); location = baseUri + operationUriInfo.getResourceEndpoint() + id; responseBuilder.location(location); final boolean isResourceDeleted = HttpMethod.DELETE.equals(operationUriInfo.getHttpMethod()); if (isResourceDeleted) { responseBuilder.resourceId(operationUriInfo.getResourceId()); } else { final ETag resourceVersion = Optional.ofNullable(scimResponse.get(AttributeNames.RFC7643.META)) .map(JsonNode::toString) .map(metaResource -> { Meta meta = JsonHelper.readJsonDocument(metaResource, Meta.class); return meta.getVersion().orElse(null); }) .orElse(null); responseBuilder.version(resourceVersion); final String resourceId = Optional.ofNullable(scimResponse.get(AttributeNames.RFC7643.ID)) .map(JsonNode::textValue) .orElse(null); responseBuilder.resourceId(resourceId); } } return responseBuilder.build(); } /** * calls the resource endpoint with a get call for the bulk-get-feature. This call will be executed for each * child resource that is being extracted * * @param baseUri the base uri of this server * @param httpHeaders the http headers from the current request * @param context the current request context * @return a function to call the {@link ResourceEndpoint} from {@link BulkGetResolver} */ private BiFunction getTransitiveBulkGetResolver(String baseUri, Map httpHeaders, Context context) { return (resourceId, resourceType) -> { UriInfos uriInfos = UriInfos.getRequestUrlInfos(getResourceTypeFactory(), String.format("%s%s/%s", baseUri, resourceType.getEndpoint(), resourceId), HttpMethod.GET, httpHeaders); return resourceEndpoint.resolveRequest(uriInfos.getHttpMethod(), null, uriInfos, doBeforeExecution, context); }; } /** * this method will try to resolve all currently resolved bulkIds within the given bulk-request-operation * * @param operation the bulk request operation that might contain some bulkId references * @param httpMethod the http method that will let us know which type of bulk-request we need to handle * @param operationUriInfo the url information of the current request operation. This object might also * contain a bulkId-reference within the url * @param bulkId the bulkId-reference of the bulk-request-operation * @return true if all bulkId-references of the operation could be resolved, false else */ private Optional resolveBulkIds(BulkRequestOperation operation, HttpMethod httpMethod, UriInfos operationUriInfo, String bulkId) { if (bulkIdResolver.isDuplicateBulkId(bulkId)) { throw new BadRequestException(String.format("Found duplicate %s \'%s\' in bulk request operations", AttributeNames.RFC7643.BULK_ID, bulkId)); } if (HttpMethod.DELETE.equals(httpMethod)) { return Optional.empty(); } BulkIdResolverAbstract resolverForBulkIds = bulkIdResolver.getBulkIdResolver(bulkId).orElseGet(() -> { return bulkIdResolver.createNewBulkIdResolver(bulkId, operationUriInfo, operation.getData().orElse("{}")); }); boolean allBulkIdReferencesResolved = !resolverForBulkIds.hasAnyBulkIdReferences(); // the BulkIdResolverAbstract contains the modified data with the resolved bulkIds if (allBulkIdReferencesResolved) { operation.setData(resolverForBulkIds.getResource().toString()); } boolean hadSuccessInLastRun = resolverForBulkIds.isHadSuccessInLastRun(); boolean isSecondTryToResolveIds = operation.getUniqueIdentifier() != null; if (isSecondTryToResolveIds && !allBulkIdReferencesResolved && !hadSuccessInLastRun) { String unresolvedBulkIds = (String)resolverForBulkIds.getUnresolvedBulkIds() .stream() .map(id -> String.format("%s:%s", AttributeNames.RFC7643.BULK_ID, id)) .collect(Collectors.joining(", ")); throw new ConflictException(String.format("the operation failed because the following " + "bulkId-references could not be resolved [%s]", unresolvedBulkIds)); } return Optional.of(resolverForBulkIds); } /** * adds the response to the bulk response builder if allowed based on the service-provider and resource type * configuration * * @param operation the request operation that might have an attribute that tells us that the user wants the * resource to be returned * @param scimResponse the response object from the {@link ResourceEndpointHandler} * @param resourceType the specific configuration for the current resource type * @param responseBuilder the response object that is being extended by the response if the configuration * allows it */ private void addResponse(BulkRequestOperation operation, ScimResponse scimResponse, ResourceType resourceType, BulkResponseOperation.BulkResponseOperationBuilder responseBuilder) { if (!serviceProvider.getBulkConfig().isReturnResourcesEnabled()) { return; } if (resourceType.getFeatures().isDenyReturnResourcesOnBulk()) { return; } final boolean returnResourceByDefault = serviceProvider.getBulkConfig().isReturnResourcesByDefault(); final boolean doesClientWantResourceBack = operation.isReturnResource().orElse(returnResourceByDefault); if (doesClientWantResourceBack) { responseBuilder.response(scimResponse); } } /** * gets the http headers for a single bulk request operation. This method is explicitly used to add the * etags-value into the http request headers if present within the request * * @param operation the current operation for which the request headers should be built * @return a map with the necessary http request headers */ private Map getHttpHeadersForBulk(BulkRequestOperation operation) { Map httpHeaders = new HashMap<>(originalHttpHeaders); httpHeaders.put(EndpointPaths.BULK, "true"); operation.getVersion().ifPresent(eTag -> httpHeaders.put(HttpHeader.IF_MATCH_HEADER, eTag.getEntityTag())); return httpHeaders; } /** * tries to parse the bulk request and validates it eventually * * @param requestBody the request body that shall represent the bulk request * @return the parsed bulk request */ private BulkRequest parseAndValidateBulkRequest(String requestBody) { BulkConfig bulkConfig = getServiceProvider().getBulkConfig(); if (!bulkConfig.isSupported()) { throw new NotImplementedException("bulk is not supported by this service provider"); } try { JsonNode jsonNode = JsonHelper.readJsonDocument(requestBody); SchemaFactory schemaFactory = getResourceTypeFactory().getSchemaFactory(); Schema bulkRequestSchema = schemaFactory.getMetaSchema(SchemaUris.BULK_REQUEST_URI); JsonNode validatedRequest = new RequestSchemaValidator(serviceProvider, ScimObjectNode.class, HttpMethod.POST).validateDocument(bulkRequestSchema, jsonNode); BulkRequest bulkRequest = JsonHelper.copyResourceToObject(validatedRequest, BulkRequest.class); if (bulkConfig.getMaxOperations() < bulkRequest.getBulkRequestOperations().size()) { throw new BadRequestException("too many operations maximum number of operations is \'" + bulkConfig.getMaxOperations() + "\' but got \'" + bulkRequest.getBulkRequestOperations().size() + "\'", null, ScimType.RFC7644.TOO_MANY); } if (bulkConfig.getMaxPayloadSize() < requestBody.getBytes().length) { throw new BadRequestException("request body too large with \'" + requestBody.getBytes().length + "\'-bytes maximum payload size is \'" + bulkConfig.getMaxPayloadSize() + "\'", null, ScimType.Custom.TOO_LARGE); } return bulkRequest; } catch (ScimException ex) { throw new BadRequestException(ex.getMessage(), ex, ScimType.Custom.UNPARSEABLE_REQUEST); } } /** * verifies that the bulk operation is valid
*
* e.g. not all http methods are allowed on the bulk endpoint * *
   *    The body of a bulk operation contains a set of HTTP resource operations
   *    using one of the HTTP methods supported by the API, i.e., POST, PUT,
   *    PATCH, or DELETE.
   * 
* * @param operation the operation to validate */ private void validateOperation(BulkRequestOperation operation) { List validMethods = new ArrayList<>(Arrays.asList(HttpMethod.POST, HttpMethod.PUT, HttpMethod.PATCH, HttpMethod.DELETE)); if (serviceProvider.getBulkConfig().isSupportBulkGet()) { validMethods.add(HttpMethod.GET); } if (!validMethods.contains(operation.getMethod())) { throw new BadRequestException("bulk request used invalid http method. Only the following methods are allowed " + "for bulk: " + validMethods, null, ScimType.Custom.UNPARSEABLE_REQUEST); } if (HttpMethod.POST.equals(operation.getMethod()) && (operation.getBulkId().isPresent() && StringUtils.isBlank(operation.getBulkId().get()) || !operation.getBulkId().isPresent())) { throw new BadRequestException("missing \'bulkId\' on BULK-POST request", null, ScimType.Custom.UNPARSEABLE_REQUEST); } } /** * the service provider configuration to check if the bulk-request meets the requirements of the configuration */ @java.lang.SuppressWarnings("all") public ServiceProvider getServiceProvider() { return this.serviceProvider; } /** * necessary to get access to the resource-types */ @java.lang.SuppressWarnings("all") public ResourceTypeFactory getResourceTypeFactory() { return this.resourceTypeFactory; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy