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

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

There is a newer version: 1.26.0
Show newest version
// Generated by delombok at Sat Aug 24 10:10:59 CEST 2024
package de.captaingoldfish.scim.sdk.server.endpoints;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiFunction;
import java.util.function.Supplier;
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.SchemaUris;
import de.captaingoldfish.scim.sdk.common.constants.ScimType;
import de.captaingoldfish.scim.sdk.common.constants.enums.HttpMethod;
import de.captaingoldfish.scim.sdk.common.constants.enums.SortOrder;
import de.captaingoldfish.scim.sdk.common.exceptions.BadRequestException;
import de.captaingoldfish.scim.sdk.common.exceptions.IOException;
import de.captaingoldfish.scim.sdk.common.exceptions.InternalServerException;
import de.captaingoldfish.scim.sdk.common.exceptions.NotImplementedException;
import de.captaingoldfish.scim.sdk.common.exceptions.ResourceNotFoundException;
import de.captaingoldfish.scim.sdk.common.exceptions.ScimException;
import de.captaingoldfish.scim.sdk.common.request.PatchOpRequest;
import de.captaingoldfish.scim.sdk.common.request.SearchRequest;
import de.captaingoldfish.scim.sdk.common.resources.ResourceNode;
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.Meta;
import de.captaingoldfish.scim.sdk.common.response.CreateResponse;
import de.captaingoldfish.scim.sdk.common.response.DeleteResponse;
import de.captaingoldfish.scim.sdk.common.response.EmptyPatchResponse;
import de.captaingoldfish.scim.sdk.common.response.ErrorResponse;
import de.captaingoldfish.scim.sdk.common.response.GetResponse;
import de.captaingoldfish.scim.sdk.common.response.ListResponse;
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.schemas.SchemaAttribute;
import de.captaingoldfish.scim.sdk.common.utils.EncodingUtils;
import de.captaingoldfish.scim.sdk.common.utils.JsonHelper;
import de.captaingoldfish.scim.sdk.server.endpoints.base.ResourceTypeEndpointDefinition;
import de.captaingoldfish.scim.sdk.server.endpoints.base.SchemaEndpointDefinition;
import de.captaingoldfish.scim.sdk.server.endpoints.base.ServiceProviderEndpointDefinition;
import de.captaingoldfish.scim.sdk.server.endpoints.features.EndpointType;
import de.captaingoldfish.scim.sdk.server.endpoints.validation.RequestContextException;
import de.captaingoldfish.scim.sdk.server.endpoints.validation.RequestValidatorHandler;
import de.captaingoldfish.scim.sdk.server.etag.ETagHandler;
import de.captaingoldfish.scim.sdk.server.filter.FilterNode;
import de.captaingoldfish.scim.sdk.server.filter.resources.FilterResourceResolver;
import de.captaingoldfish.scim.sdk.server.interceptor.Interceptor;
import de.captaingoldfish.scim.sdk.server.patch.PatchRequestHandler;
import de.captaingoldfish.scim.sdk.server.patch.workarounds.PatchWorkaround;
import de.captaingoldfish.scim.sdk.server.patch.workarounds.msazure.MsAzurePatchComplexValueRebuilder;
import de.captaingoldfish.scim.sdk.server.patch.workarounds.msazure.MsAzurePatchRemoveRebuilder;
import de.captaingoldfish.scim.sdk.server.patch.workarounds.msazure.MsAzurePatchValueSubAttributeRebuilder;
import de.captaingoldfish.scim.sdk.server.response.PartialListResponse;
import de.captaingoldfish.scim.sdk.server.schemas.ResourceType;
import de.captaingoldfish.scim.sdk.server.schemas.ResourceTypeFactory;
import de.captaingoldfish.scim.sdk.server.schemas.custom.ResourceTypeFeatures;
import de.captaingoldfish.scim.sdk.server.schemas.validation.AbstractResourceValidator;
import de.captaingoldfish.scim.sdk.server.schemas.validation.RequestResourceValidator;
import de.captaingoldfish.scim.sdk.server.schemas.validation.RequestSchemaValidator;
import de.captaingoldfish.scim.sdk.server.sort.ResourceNodeComparator;
import de.captaingoldfish.scim.sdk.server.utils.RequestUtils;


/**
 * author Pascal Knueppel 
* created at: 03.10.2019 - 19:28
*
* This class is used to execute the SCIM operations on the specific endpoints */ class ResourceEndpointHandler { @java.lang.SuppressWarnings("all") @lombok.Generated private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ResourceEndpointHandler.class); /** * each created {@link ResourceEndpointHandler} must get hold of a single {@link ServiceProvider} instance * which holds the configuration of this service provider implementation */ private final ServiceProvider serviceProvider; /** * contains a number of patch-workarounds that can be executed on patch-requests to fix illegal * operation-setups */ private final List> patchWorkarounds = new ArrayList<>(); /** * this is used to prevent application context pollution in unit tests */ private ResourceTypeFactory resourceTypeFactory; /** * this constructor was introduced for unit tests to add a specific resourceTypeFactory instance which will * prevent application context pollution within unit tests */ protected ResourceEndpointHandler(ServiceProvider serviceProvider, EndpointDefinition... endpointDefinitions) { this.resourceTypeFactory = new ResourceTypeFactory(); this.serviceProvider = serviceProvider; List endpointDefinitionList = new ArrayList<>(Arrays.asList(endpointDefinitions)); registerEndpoint(new ServiceProviderEndpointDefinition(serviceProvider)); registerEndpoint(new ResourceTypeEndpointDefinition(resourceTypeFactory)); registerEndpoint(new SchemaEndpointDefinition(resourceTypeFactory)); endpointDefinitionList.forEach(this::registerEndpoint); addDefaultPatchWorkarounds(); } /** * adds the default patch-workaround-handlers to the given serviceprovider configuration */ public void addDefaultPatchWorkarounds() { patchWorkarounds.add(MsAzurePatchRemoveRebuilder::new); patchWorkarounds.add(MsAzurePatchValueSubAttributeRebuilder::new); patchWorkarounds.add(MsAzurePatchComplexValueRebuilder::new); } /** * registers a new endpoint * * @param endpointDefinition the endpoint to register that will override an existing one if one is already * present */ public ResourceType registerEndpoint(EndpointDefinition endpointDefinition) { ResourceType resourceType = resourceTypeFactory.registerResourceType(endpointDefinition.getResourceHandler(), endpointDefinition.getResourceType(), endpointDefinition.getResourceSchema(), endpointDefinition.getResourceSchemaExtensions() .toArray(new JsonNode[0])); ResourceHandler resourceHandler = resourceType.getResourceHandlerImpl(); resourceHandler.setServiceProvider(serviceProvider); resourceHandler.setResourceType(resourceType); Schema mainSchema = resourceType.getMainSchema(); resourceHandler.setSchema(mainSchema); resourceHandler.setSchemaExtensions(resourceType.getAllSchemas() .stream() .filter(schema -> !schema.getId() .get() .equals(mainSchema.getId().get())) .collect(Collectors.toList())); resourceHandler.setChangePasswordSupported(() -> serviceProvider.getChangePasswordConfig().isSupported()); resourceHandler.setMaxResults(() -> serviceProvider.getFilterConfig().getMaxResults()); resourceHandler.setGetResourceTypeByRef(resourceTypeRefValue -> { return getResourceTypeByName((String)resourceTypeRefValue).orElseGet(() -> { final String[] urlParts = ((String)resourceTypeRefValue).split("/"); final String resourceTypeEndpointOrV2 = "/" + urlParts[urlParts.length - 2]; final String idOrResourceTypeEndpoint = "/" + urlParts[urlParts.length - 1]; return Optional.ofNullable(resourceTypeFactory.getResourceType(resourceTypeEndpointOrV2)) .orElseGet(() -> resourceTypeFactory.getResourceType(idOrResourceTypeEndpoint)); }); }); resourceHandler.postConstruct(resourceType); return resourceType; } /** * will get a resource type definition by its name * * @param name the name of the resource type e.g. User, Group, ServiceProviderConfig, ResourceType, Schema * @return the resource type if one is registered under the given id */ public Optional getResourceTypeByName(String name) { return resourceTypeFactory.getResourceTypeByName(name); } /** * @return the names of all resource types that have been registered */ public Set getRegisteredResourceTypeNames() { return resourceTypeFactory.getAllResourceTypes().stream().map(ResourceType::getName).collect(Collectors.toSet()); } /** * @return all registered resource types */ public Set getRegisteredResourceTypes() { return new HashSet<>(resourceTypeFactory.getAllResourceTypes()); } /** * @return all resource schemata that have been registered on this SCIM provider */ public Set getRegisteredSchemas() { return resourceTypeFactory.getSchemaFactory().getAllResourceSchemas(); } /** * checks if a resource type exists under the given endpoint and validates the request if it does by the * corresponding meta schema. If the validation succeeds the single json nodes expanded with its meta * information will be given to the developer custom implementation. The returned object for the response will * be validated again and then returned as a SCIM response * * @param endpoint the resource endpoint that was called * @param resourceDocument the resource document * @param baseUrlSupplier this supplier is an optional attribute that should be used to supply the information * of the base URL of this application e.g.: https://example.com/scim/v2. This return value will be * used to create the location URL of the resources like 'https://example.com/scim/v2/Users/123456'. * If this parameter is not present the application will try to read a hardcoded URL from the service * provider configuration that is also an optional attribute. If both ways fail an exception will be * thrown * @param context the current request context that holds additional useful information. This object is never * null * @return the scim response for the client */ protected ScimResponse createResource(String endpoint, String resourceDocument, Supplier baseUrlSupplier, Context context) { try { if (StringUtils.isBlank(resourceDocument)) { throw new BadRequestException("the request body is empty", null, ScimType.Custom.INVALID_PARAMETERS); } ResourceType resourceType = getResourceType(endpoint); JsonNode resource; try { resource = JsonHelper.readJsonDocument(resourceDocument); } catch (IOException ex) { throw new BadRequestException(ex.getMessage(), ex, ScimType.Custom.UNPARSEABLE_REQUEST); } ResourceHandler resourceHandler = resourceType.getResourceHandlerImpl(); RequestResourceValidator resourceValidator = new RequestResourceValidator(serviceProvider, resourceType, HttpMethod.POST); ResourceNode resourceNode = (ResourceNode)resourceValidator.validateDocument(resource); Meta meta = resourceNode.getMeta().orElse(Meta.builder().build()); meta.setResourceType(resourceType.getName()); resourceNode.remove(AttributeNames.RFC7643.META); resourceNode.setMeta(meta); new RequestValidatorHandler(resourceHandler, resourceValidator, context).validateCreate(resourceNode); Interceptor interceptor = resourceHandler.getInterceptor(EndpointType.CREATE); ResourceNode resourceNodeCreated = interceptor.doAround(() -> { return resourceHandler.createResource(resourceNode, context); }); if (resourceNodeCreated == null) { throw new NotImplementedException("create was not implemented for resourceType \'" + resourceType.getName() + "\'"); } String resourceId = resourceNodeCreated.getId().orElseThrow(() -> { String errorMessage = "ID attribute not set on created resource"; return new InternalServerException(errorMessage, null, null); }); final String location = getLocation(resourceType, resourceId, baseUrlSupplier); Meta createdMeta = resourceNodeCreated.getMeta().orElseThrow(() -> { String metaErrorMessage = "Meta attribute not set on created resource"; return new InternalServerException(metaErrorMessage, null, null); }); if (!createdMeta.getLastModified().isPresent()) { createdMeta.setLastModified(createdMeta.getCreated().orElse(null)); } if (meta.getLocation().isPresent()) { createdMeta.setLocation(meta.getLocation().get()); } else { createdMeta.setLocation(location); } createdMeta.setResourceType(resourceType.getName()); ETagHandler.getResourceVersion(serviceProvider, resourceType, resourceNodeCreated) .ifPresent(createdMeta::setVersion); Optional responseValidator = // resourceHandler.getResponseValidator(null, null, resource, getReferenceUrlSupplier(baseUrlSupplier)); JsonNode responseResource = resourceNodeCreated; if (responseValidator.isPresent()) { responseResource = responseValidator.get().validateDocument(resourceNodeCreated); } return new CreateResponse(responseResource, location, createdMeta); } catch (RequestContextException ex) { ErrorResponse errorResponse = new ErrorResponse(ex); ex.getValidationContext().writeToErrorResponse(errorResponse); return errorResponse; } catch (ScimException ex) { return new ErrorResponse(ex); } catch (Exception ex) { return new ErrorResponse(new InternalServerException(ex.getMessage(), ex, null)); } } /** * checks if a resource type exists under the given endpoint and will then give the id to the developers * custom implementation stored under the found resource type. The returned {@link ResourceNode} will then be * validated and eventually returned to the client * * @param endpoint the resource endpoint that was called * @param id the id of the resource that was requested * @param attributes When specified, the default list of attributes SHALL be overridden, and each resource * returned MUST contain the minimum set of resource attributes and any attributes or sub-attributes * explicitly requested by the "attributes" parameter. The query parameter attributes value is a * comma-separated list of resource attribute names in standard attribute notation (Section 3.10) * form (e.g., userName, name, emails). * @param excludedAttributes When specified, each resource returned MUST contain the minimum set of resource * attributes. Additionally, the default set of attributes minus those attributes listed in * "excludedAttributes" is returned. The query parameter attributes value is a comma-separated list * of resource attribute names in standard attribute notation (Section 3.10) form (e.g., userName, * name, emails). * @param baseUrlSupplier this supplier is an optional attribute that should be used to supply the information * of the base URL of this application e.g.: https://example.com/scim/v2. This return value will be * used to create the location URL of the resources like 'https://example.com/scim/v2/Users/123456'. * If this parameter is not present the application will try to read a hardcoded URL from the service * provider configuration that is also an optional attribute. If both ways fail an exception will be * thrown * @param context the current request context that holds additional useful information. This object is never * null * @return the scim response for the client */ protected ScimResponse getResource(String endpoint, String id, String attributes, String excludedAttributes, Supplier baseUrlSupplier, Context context) { try { ResourceType resourceType = getResourceType(endpoint); ResourceHandler resourceHandler = resourceType.getResourceHandlerImpl(); final List attributesList = RequestUtils.getAttributes(resourceType, attributes); final List excludedAttributesList = RequestUtils.getAttributes(resourceType, excludedAttributes); Interceptor interceptor = resourceHandler.getInterceptor(EndpointType.GET); ResourceNode resourceNode = interceptor.doAround(() -> { return resourceHandler.getResource(id, attributesList, excludedAttributesList, context); }); if (resourceNode == null) { throw new ResourceNotFoundException("the \'" + resourceType.getName() + "\' resource with id \'" + id + "\' does " + "not exist", null, null); } ETagHandler.validateVersion(serviceProvider, resourceType, () -> resourceNode, context.getUriInfos().getHttpHeaders()); String resourceId = resourceNode.getId().orElse(null); if (resourceId != null && !resourceId.equals(id)) { ResourceTypeFeatures resourceTypeFeatures = resourceType.getFeatures(); if (resourceTypeFeatures != null && !resourceTypeFeatures.isSingletonEndpoint()) { throw new InternalServerException("the id of the returned resource does not match the " + "requested id: requestedId: \'" + id + "\', returnedId: \'" + resourceId + "\'", null, null); } } final String location = getLocation(resourceType, resourceId, baseUrlSupplier); resourceNode.getMeta().ifPresent(meta -> { if (!meta.getLastModified().isPresent()) { meta.setLastModified(meta.getCreated().orElse(null)); } if (!meta.getLocation().isPresent()) { meta.setLocation(location); } meta.setResourceType(resourceType.getName()); ETagHandler.getResourceVersion(serviceProvider, resourceType, resourceNode).ifPresent(meta::setVersion); }); Optional responseValidator = // resourceHandler.getResponseValidator(attributesList, excludedAttributesList, null, getReferenceUrlSupplier(baseUrlSupplier)); JsonNode responseResource = resourceNode; if (responseValidator.isPresent()) { responseResource = responseValidator.get().validateDocument(resourceNode); } return new GetResponse(responseResource, location, resourceNode.getMeta().orElse(null)); } catch (ScimException ex) { return new ErrorResponse(ex); } catch (Exception ex) { return new ErrorResponse(new InternalServerException(ex.getMessage(), ex, null)); } } /** * Clients MAY execute queries without passing parameters on the URL by using the HTTP POST verb combined with * the "/.search" path extension. The inclusion of "/.search" on the end of a valid SCIM endpoint SHALL be * used to indicate that the HTTP POST verb is intended to be a query operation. * * @param endpoint the resource endpoint that was called. This string should only contain the * resources-endpoint not the "/.search" extension e.g. "/Users" or "Users". * @param searchRequest the JSON request body of the search request if the request was sent over POST * @param baseUrlSupplier this supplier is an optional attribute that should be used to supply the information * of the base URL of this application e.g.: https://example.com/scim/v2. This return value will be * used to create the location URL of the resources like 'https://example.com/scim/v2/Users/123456'. * If this parameter is not present the application will try to read a hardcoded URL from the service * provider configuration that is also an optional attribute. If both ways fail an exception will be * thrown * @param context the current request context that holds additional useful information. This object is never * null * @return a {@link ListResponse} with all returned resources or an {@link ErrorResponse} */ protected ScimResponse listResources(String endpoint, String searchRequest, Supplier baseUrlSupplier, Context context) { return listResources(endpoint, StringUtils.isBlank(searchRequest) ? SearchRequest.builder().build() : JsonHelper.readJsonDocument(searchRequest, SearchRequest.class), baseUrlSupplier, context); } /** * Clients MAY execute queries without passing parameters on the URL by using the HTTP POST verb combined with * the "/.search" path extension. The inclusion of "/.search" on the end of a valid SCIM endpoint SHALL be * used to indicate that the HTTP POST verb is intended to be a query operation. * * @param endpoint the resource endpoint that was called. This string should only contain the * resources-endpoint * not the "/.search" extension e.g. "/Users" or "Users". * @param searchRequest the JSON request body of the search request if the request was sent over POST * @param baseUrlSupplier this supplier is an optional attribute that should be used to supply the information * of the base URL of this application e.g.: https://example.com/scim/v2. This return value will be * used to create the location URL of the resources like 'https://example.com/scim/v2/Users/123456'. * If this parameter is not present the application will try to read a hardcoded URL from the service * provider configuration that is also an optional attribute. If both ways fail an exception will be * thrown * @param context the current request context that holds additional useful information. This object is never * null * @return a {@link ListResponse} with all returned resources or an {@link ErrorResponse} */ protected ScimResponse listResources(String endpoint, SearchRequest searchRequest, Supplier baseUrlSupplier, Context context) { return listResources(endpoint, searchRequest.getStartIndex().orElse(null), searchRequest.getCount().orElse(null), searchRequest.getFilter().orElse(null), searchRequest.getSortBy().orElse(null), searchRequest.getSortOrder().orElse(null), searchRequest.getAttributes(), searchRequest.getExcludedAttributes(), baseUrlSupplier, context); } /** * Clients MAY execute queries without passing parameters on the URL by using the HTTP POST verb combined with * the "/.search" path extension. The inclusion of "/.search" on the end of a valid SCIM endpoint SHALL be * used to indicate that the HTTP POST verb is intended to be a query operation. * * @param endpoint the resource endpoint that was called e.g. "/Users" or "Users". * @param startIndex The 1-based index of the first query result. A value less than 1 SHALL be interpreted as * 1.
* DEFAULT: 1 * @param count Non-negative integer. Specifies the desired maximum number of query results per page, e.g., * 10. A negative value SHALL be interpreted as "0". A value of "0" indicates that no resource * results are to be returned except for "totalResults".
* DEFAULT: None
* When specified, the service provider MUST NOT return more results than specified, although it MAY * return fewer results. If unspecified, the maximum number of results is set by the service * provider. * @param filter Filtering is an OPTIONAL parameter for SCIM service providers. Clients MAY discover service * provider filter capabilities by looking at the "filter" attribute of the "ServiceProviderConfig" * endpoint. Clients MAY request a subset of resources by specifying the "filter" query parameter * containing a filter expression. When specified, only those resources matching the filter * expression SHALL be returned. The expression language that is used with the filter parameter * supports references to attributes and literals. * @param sortBy The "sortBy" parameter specifies the attribute whose value SHALL be used to order the * returned responses. If the "sortBy" attribute corresponds to a singular attribute, resources are * sorted according to that attribute's value; if it's a multi-valued attribute, resources are sorted * by the value of the primary attribute (see Section 2.4 of [RFC7643]), if any, or else the first * value in the list, if any. If the attribute is complex, the attribute name must be a path to a * sub-attribute in standard attribute notation (Section 3.10), e.g., "sortBy=name.givenName". For * all attribute types, if there is no data for the specified "sortBy" value, they are sorted via the * "sortOrder" parameter, i.e., they are ordered last if ascending and first if descending. * @param sortOrder The order in which the "sortBy" parameter is applied. Allowed values are "ascending" and * "descending". If a value for "sortBy" is provided and no "sortOrder" is specified, "sortOrder" * SHALL default to ascending. String type attributes are case insensitive by default, unless the * attribute type is defined as a case-exact string. "sortOrder" MUST sort according to the attribute * type; i.e., for case-insensitive attributes, sort the result using case-insensitive Unicode * alphabetic sort order with no specific locale implied, and for case-exact attribute types, sort * the result using case-sensitive Unicode alphabetic sort order. * @param attributes When specified, the default list of attributes SHALL be overridden, and each resource * returned MUST contain the minimum set of resource attributes and any attributes or sub-attributes * explicitly requested by the "attributes" parameter. The query parameter attributes value is a * comma-separated list of resource attribute names in standard attribute notation (Section 3.10) * form (e.g., userName, name, emails). * @param excludedAttributes When specified, each resource returned MUST contain the minimum set of resource * attributes. Additionally, the default set of attributes minus those attributes listed in * "excludedAttributes" is returned. The query parameter attributes value is a comma-separated list * of resource attribute names in standard attribute notation (Section 3.10) form (e.g., userName, * name, emails). * @param baseUrlSupplier this supplier is an optional attribute that should be used to supply the information * of the base URL of this application e.g.: https://example.com/scim/v2. This return value will be * used to create the location URL of the resources like 'https://example.com/scim/v2/Users/123456'. * If this parameter is not present the application will try to read a hardcoded URL from the service * provider configuration that is also an optional attribute. If both ways fail an exception will be * thrown * @param context the current request context that holds additional useful information. This object is never * null * @return a {@link ListResponse} with all returned resources or an {@link ErrorResponse} */ protected ScimResponse listResources(String endpoint, Long startIndex, Integer count, String filter, String sortBy, String sortOrder, List attributes, List excludedAttributes, Supplier baseUrlSupplier, Context context) { try { final ResourceType resourceType = getResourceType(endpoint); final long effectiveStartIndex = RequestUtils.getEffectiveStartIndex(startIndex); final int effectiveCount = RequestUtils.getEffectiveCount(serviceProvider, count); final FilterNode filterNode = getFilterNode(resourceType, filter); final boolean autoFiltering = resourceType.getFeatures().isAutoFiltering(); final SchemaAttribute sortByAttribute = getSortByAttribute(resourceType, sortBy); final SortOrder sortOrdering = getSortOrdering(sortOrder, sortByAttribute); final boolean autoSorting = resourceType.getFeatures().isAutoSorting(); final List attributesList = RequestUtils.getAttributes(resourceType, attributes); final List excludedAttributesList = RequestUtils.getAttributes(resourceType, excludedAttributes); ResourceHandler resourceHandler = resourceType.getResourceHandlerImpl(); Interceptor interceptor = resourceHandler.getInterceptor(EndpointType.LIST); PartialListResponse resources = interceptor.doAround(() -> { return resourceHandler.listResources(effectiveStartIndex, effectiveCount, autoFiltering ? null : filterNode, autoSorting ? null : sortByAttribute, autoSorting ? null : sortOrdering, attributesList, excludedAttributesList, context); }); if (resources == null) { throw new NotImplementedException("listResources was not implemented for resourceType \'" + resourceType.getName() + "\'"); } List resourceList = resources.getResources(); List filteredResources = filterResources(filterNode, resourceList, resourceType); filteredResources = sortResources(filteredResources, sortByAttribute, sortOrdering, resourceType); long totalResults = resourceList.size() != filteredResources.size() ? filteredResources.size() : (resources.getTotalResults() == 0 ? filteredResources.size() : resources.getTotalResults()); // override filteredResources only in case of auto-filtering since we expect the implementation to handle // everything if auto-filtering is deactivated if (autoFiltering) { // this if-block will assert that no more results will be returned than the countValue allows. if (effectiveStartIndex <= filteredResources.size()) { filteredResources = filteredResources.subList((int)Math.min(effectiveStartIndex - 1, filteredResources.size() - 1), (int)Math.min(effectiveStartIndex - 1 + effectiveCount, filteredResources.size())); } else { log.debug("startIndex \'{}\' is > than number of entries available \'{}\'. Returning empty list", effectiveStartIndex, filteredResources.size()); filteredResources = Collections.emptyList(); } } if (filteredResources.size() > effectiveCount) { log.warn("The service provider tried to return more results than allowed. Tried to return \'{}\' results. " + "The list will be reduced to \'{}\' results", filteredResources.size(), effectiveCount); filteredResources = filteredResources.subList(0, effectiveCount); } List validatedResourceList = new ArrayList<>(); for ( ResourceNode resourceNode : filteredResources ) { final String location = getLocation(resourceType, resourceNode.getId().orElse(null), baseUrlSupplier); log.trace("Determined resource location at \'{}\'", location); resourceNode.getMeta().ifPresent(meta -> { if (!meta.getLastModified().isPresent()) { meta.setLastModified(meta.getCreated().orElse(null)); } if (!meta.getLocation().isPresent()) { meta.setLocation(location); } meta.setResourceType(resourceType.getName()); ETagHandler.getResourceVersion(serviceProvider, resourceType, resourceNode).ifPresent(meta::setVersion); }); Optional responseValidator = // resourceHandler.getResponseValidator(attributesList, excludedAttributesList, null, getReferenceUrlSupplier(baseUrlSupplier)); JsonNode responseResource = resourceNode; if (responseValidator.isPresent()) { responseResource = responseValidator.get().validateDocument(resourceNode); } validatedResourceList.add(responseResource); } return new ListResponse(validatedResourceList, totalResults, validatedResourceList.size(), effectiveStartIndex); } catch (ScimException ex) { return new ErrorResponse(ex); } catch (Exception ex) { return new ErrorResponse(new InternalServerException(ex.getMessage(), ex, null)); } } /** * this method will sort the resources based on the given attribute and the ordering * * @param filteredResources the resources that might have already been filtered * @param sortByAttribute the sortby attribute that tells us which attribute should be used for sorting * @param sortOrdering the sort order to use * @return the ordered resources */ private List sortResources(List filteredResources, SchemaAttribute sortByAttribute, SortOrder sortOrdering, ResourceType resourceType) { if (!serviceProvider.getSortConfig().isSupported() || sortByAttribute == null || !resourceType.getFeatures().isAutoSorting()) { log.trace("Auto-sorting skipped for auto-sorting is not supported or missing sortBy attribute"); return filteredResources; } log.trace("Starting auto sorting resources by attribute \'{}\' in order \'{}\'", sortByAttribute.getFullResourceName(), sortOrdering); try { return serviceProvider.getThreadPool() .submit(() -> filteredResources.parallelStream() .sorted(new ResourceNodeComparator(sortByAttribute, sortOrdering)) .collect(Collectors.toList())) .get(); } catch (InterruptedException | ExecutionException e) { throw new InternalServerException(e); } } /** * this method executes filtering on the given resource list * * @param filterNode the filter expression from the client. Might be null if filtering is disabled * @param resourceList the list that should be filtered * @param resourceType the resource type must have filtering enabled. If filtering is not explicitly enabled * the developer must do the filtering manually * @return the filtered list or the {@code resourceList} */ protected List filterResources(FilterNode filterNode, List resourceList, ResourceType resourceType) { boolean isApplicationFilteringEnabled = resourceType.getFeatures().isAutoFiltering(); List filteredResourceType; if (isApplicationFilteringEnabled && filterNode != null) { log.trace("Starting with auto filtering resources"); filteredResourceType = FilterResourceResolver.filterResources(serviceProvider, resourceList, filterNode); } else { log.trace("No filtering performed"); filteredResourceType = resourceList; } return filteredResourceType; } /** * checks if the given filter expression must be evaluated or not. If the filtering is disabled in the * {@link ServiceProvider} instance the filter expression is ignored and a debug message is printed * * @param resourceType the resource type on which the filter expression should be evaluated * @param filter the filter expression that should be evaluated * @return a filter tree structure that makes resolving the filter very easy */ private FilterNode getFilterNode(ResourceType resourceType, String filter) { if (StringUtils.isBlank(filter)) { log.trace("No filter expression found in request"); return null; } if (serviceProvider.getFilterConfig().isSupported()) { log.trace("Evaluating filter expression \'{}\' for resourceType \'{}\'", filter, resourceType.getName()); return RequestUtils.parseFilter(resourceType, filter); } log.debug("Filter expression \'{}\' is not evaluated because filter support is disabled", filter); return null; } /** * will parse the sortBy value to a {@link SchemaAttribute} from the {@link ResourceType} if sorting is * enabled and the sortBy attribute is provided * * @param resourceType the resourceType to which the sortBy value must apply * @param sortBy the sortBy value that will be evaluated * @return either the {@link SchemaAttribute} that applies to the sortBy value or null */ private SchemaAttribute getSortByAttribute(ResourceType resourceType, String sortBy) { if (StringUtils.isBlank(sortBy)) { log.trace("No sortBy attribute found in request"); return null; } if (serviceProvider.getSortConfig().isSupported()) { log.trace("Evaluating sortBy attribute \'{}\' for resourceType \'{}\'", sortBy, resourceType.getName()); return RequestUtils.getSchemaAttributeByAttributeName(resourceType, sortBy); } log.debug("sortBy value \'{}\' is ignored because sorting support is disabled", sortBy); return null; } /** * will parse the sortOrdering value to a {@link SortOrder} instance if sorting is enabled and the sortOrder * value is provided. If the sortBy value is not null and was evaluated before but the sortOrder value is not * present it will default to value {@link SortOrder#ASCENDING} * * @param sortOrder the sortOrder value specified by the client * @param sortBy the sortBy value specified by the client * @return the sortOrder value if the value was specified and sorting is enabled or null */ private SortOrder getSortOrdering(String sortOrder, SchemaAttribute sortBy) { if (StringUtils.isBlank(sortOrder)) { if (sortBy != null) { log.trace("No sortBy attribute found in request. Using default \'{}\'", SortOrder.ASCENDING); // If a value for "sortBy" is provided and no "sortOrder" is specified, "sortOrder" SHALL default to ascending return SortOrder.ASCENDING; } } if (serviceProvider.getSortConfig().isSupported()) { log.trace("Evaluating sortOrder attribute \'{}\'", sortOrder); return SortOrder.getByValue(sortOrder); } log.debug("SortOrder value \'{}\' is ignored because sorting support is disabled", sortOrder); return null; } /** * checks if a resource type exists under the given endpoint and validates the request if it does by the * corresponding meta schema. If the validation succeeds the single json nodes expanded with its meta * information will be given to the developer custom implementation. The returned object for the response will * be validated again and then returned as a SCIM response * * @param endpoint the resource endpoint that was called * @param id the id of the resource that was requested * @param resourceDocument the resource document * @param baseUrlSupplier this supplier is an optional attribute that should be used to supply the information * of the base URL of this application e.g.: https://example.com/scim/v2. This return value will be * used to create the location URL of the resources like 'https://example.com/scim/v2/Users/123456'. * If this parameter is not present the application will try to read a hardcoded URL from the service * provider configuration that is also an optional attribute. If both ways fail an exception will be * thrown * @param context the current request context that holds additional useful information. This object is never * null * @return the scim response for the client */ protected ScimResponse updateResource(String endpoint, String id, String resourceDocument, Supplier baseUrlSupplier, Context context) { try { if (StringUtils.isBlank(resourceDocument)) { throw new BadRequestException("the request body is empty", null, ScimType.Custom.INVALID_PARAMETERS); } ResourceType resourceType = getResourceType(endpoint); JsonNode resource; try { resource = JsonHelper.readJsonDocument(resourceDocument); } catch (IOException ex) { throw new BadRequestException(ex.getMessage(), ex, ScimType.Custom.UNPARSEABLE_REQUEST); } ResourceHandler resourceHandler = resourceType.getResourceHandlerImpl(); RequestResourceValidator requestResourceValidator = new RequestResourceValidator(serviceProvider, resourceType, HttpMethod.PUT); ResourceNode resourceNodeForUpdate = (ResourceNode)requestResourceValidator.validateDocument(resource); if (resource == null) { throw new BadRequestException("the request body does not contain any writable parameters", null, ScimType.Custom.UNPARSEABLE_REQUEST); } if (resourceNodeForUpdate == null) { throw new ResourceNotFoundException("the \'" + resourceType.getName() + "\' resource with id \'" + id + "\' does " + "not exist", null, null); } resourceNodeForUpdate.setId(id); final String location = getLocation(resourceType, id, baseUrlSupplier); Meta meta = resourceNodeForUpdate.getMeta().orElse(Meta.builder().build()); resourceNodeForUpdate.remove(AttributeNames.RFC7643.META); meta.setLocation(location); meta.setResourceType(resourceType.getName()); resourceNodeForUpdate.setMeta(meta); AtomicReference oldResourceNode = new AtomicReference<>(); Supplier oldResourceSupplier = () -> { ResourceNode oldResource = resourceHandler.getResourceForUpdate(id, null, null, context, EndpointType.UPDATE); oldResourceNode.compareAndSet(null, oldResource); return oldResourceNode.get(); }; Interceptor interceptor = resourceHandler.getInterceptor(EndpointType.UPDATE); ResourceNode resourceNode = interceptor.doAround(() -> { validateResourceVersion(id, resourceType, oldResourceSupplier, context.getUriInfos().getHttpHeaders()); new RequestValidatorHandler(resourceHandler, requestResourceValidator, context).validateUpdate(oldResourceSupplier, resourceNodeForUpdate); return resourceHandler.updateResource(resourceNodeForUpdate, context); }); if (resourceNode == null) { throw new ResourceNotFoundException("the \'" + resourceType.getName() + "\' resource with id \'" + id + "\' does " + "not exist", null, null); } Supplier metaErrorMessage = () -> "Meta attribute not set on created resource"; Meta createdMeta = resourceNode.getMeta() .orElseThrow(() -> new InternalServerException(metaErrorMessage.get(), null, null)); if (!createdMeta.getLastModified().isPresent()) { createdMeta.setLastModified(createdMeta.getCreated().orElse(null)); } if (!createdMeta.getLocation().isPresent()) { createdMeta.setLocation(location); } createdMeta.setResourceType(resourceType.getName()); ETagHandler.getResourceVersion(serviceProvider, resourceType, resourceNode).ifPresent(createdMeta::setVersion); Supplier errorMessage = () -> "ID attribute not set on updated resource"; String resourceId = resourceNode.getId() .orElseThrow(() -> new InternalServerException(errorMessage.get(), null, null)); if (!resourceId.equals(id) && !resourceType.getFeatures().isSingletonEndpoint()) { throw new InternalServerException("the id of the returned resource does not match the " + "requested id: requestedId: \'" + id + "\', returnedId: \'" + resourceId + "\'", null, null); } Optional responseValidator = // resourceHandler.getResponseValidator(null, null, resourceNode, getReferenceUrlSupplier(baseUrlSupplier)); JsonNode responseResource = resourceNode; if (responseValidator.isPresent()) { responseResource = responseValidator.get().validateDocument(resourceNode); } return new UpdateResponse(responseResource, location, meta); } catch (RequestContextException ex) { ErrorResponse errorResponse = new ErrorResponse(ex); ex.getValidationContext().writeToErrorResponse(errorResponse); return errorResponse; } catch (ScimException ex) { return new ErrorResponse(ex); } catch (Exception ex) { return new ErrorResponse(new InternalServerException(ex.getMessage(), ex, null)); } } /** * checks if a resource type exists under the given endpoint and will then give the id to the developers * custom implementation stored under the found resource type. If no exception occurred the client will be * informed of a successful request * * @param endpoint the resource endpoint that was called * @param id the id of the resource that was requested * @param context the current request context that holds additional useful information. This object is never * null * @return an empty response that does not create a response body */ protected ScimResponse deleteResource(String endpoint, String id, Map httpHeaders, Context context) { try { ResourceType resourceType = getResourceType(endpoint); ResourceHandler resourceHandler = resourceType.getResourceHandlerImpl(); Interceptor interceptor = resourceHandler.getInterceptor(EndpointType.DELETE); return interceptor.doAround(() -> { Supplier oldResourceSupplier = () -> { return resourceHandler.getResourceForUpdate(id, null, null, context, EndpointType.DELETE); }; validateResourceVersion(id, resourceType, oldResourceSupplier, httpHeaders); resourceHandler.deleteResource(id, context); return new DeleteResponse(); }); } catch (ScimException ex) { return new ErrorResponse(ex); } catch (Exception ex) { return new ErrorResponse(new InternalServerException(ex.getMessage(), ex, null)); } } /** * gets the resource that should be patched and will inject the patch operations into the returned resource. * After the patch operation has been processed the patched object will be given to the * {@link ResourceHandler#updateResource(ResourceNode, Context)} method * * @param endpoint the resource endpoint that was called * @param id the id of the resource that should be patched * @param requestBody the patch request body * @param attributes When specified, the default list of attributes SHALL be overridden, and each resource * returned MUST contain the minimum set of resource attributes and any attributes or sub-attributes * explicitly requested by the "attributes" parameter. The query parameter attributes value is a * comma-separated list of resource attribute names in standard attribute notation (Section 3.10) * form (e.g., userName, name, emails). * @param excludedAttributes When specified, each resource returned MUST contain the minimum set of resource * attributes. Additionally, the default set of attributes minus those attributes listed in * "excludedAttributes" is returned. The query parameter attributes value is a comma-separated list * of resource attribute names in standard attribute notation (Section 3.10) form (e.g., userName, * name, emails). * @param baseUrlSupplier this supplier is an optional attribute that should be used to supply the information * of the base URL of this application e.g.: https://example.com/scim/v2. This return value will be * used to create the location URL of the resources like 'https://example.com/scim/v2/Users/123456'. * If this parameter is not present the application will try to read a hardcoded URL from the service * provider configuration that is also an optional attribute. If both ways fail an exception will be * thrown * @param context the current request context that holds additional useful information. This object is never * null * @return the updated resource or an error response */ protected ScimResponse patchResource(String endpoint, String id, String requestBody, String attributes, String excludedAttributes, Supplier baseUrlSupplier, Context context) { try { if (!serviceProvider.getPatchConfig().isSupported()) { throw new NotImplementedException("patch is not supported by this service provider"); } ResourceType resourceType = getResourceType(endpoint); ResourceHandler resourceHandler = resourceType.getResourceHandlerImpl(); Schema patchSchema = resourceTypeFactory.getSchemaFactory().getMetaSchema(SchemaUris.PATCH_OP); JsonNode patchDocument = JsonHelper.readJsonDocument(requestBody); if (patchDocument == null) { throw new BadRequestException("Missing patch request body"); } patchDocument = new RequestSchemaValidator(serviceProvider, ScimObjectNode.class, HttpMethod.PATCH).validateDocument(patchSchema, patchDocument); PatchOpRequest patchOpRequest = JsonHelper.copyResourceToObject(patchDocument, PatchOpRequest.class); PatchRequestHandler patchRequestHandler = new PatchRequestHandler(id, resourceHandler, getPatchWorkarounds(), context); final List attributesList = RequestUtils.getAttributes(resourceType, attributes); final List excludedAttributesList = RequestUtils.getAttributes(resourceType, excludedAttributes); Supplier oldResourceSupplier = patchRequestHandler.getOldResourceSupplier(id, attributesList, excludedAttributesList); Interceptor patchInterceptor = resourceHandler.getInterceptor(EndpointType.PATCH); ResourceNode updatedResource = patchInterceptor.doAround(() -> { ResourceNode resourceNode = null; if (serviceProvider.getETagConfig().isSupported()) { resourceNode = oldResourceSupplier.get(); Meta meta = resourceNode.getMeta().orElseGet(Meta::new); resourceNode.remove(AttributeNames.RFC7643.META); final String location = context.getResourceReferenceUrl(id); meta.setLocation(location); meta.setResourceType(resourceType.getName()); resourceNode.setMeta(meta); ETagHandler.getResourceVersion(serviceProvider, resourceType, resourceNode).ifPresent(meta::setVersion); } ResourceNode patchedResourceNode = patchRequestHandler.handlePatchRequest(patchOpRequest); patchedResourceNode.setId(id); Meta meta = patchedResourceNode.getMeta().orElseGet(Meta::new); Optional.ofNullable(resourceNode).flatMap(ResourceNode::getMeta).ifPresent(previousMeta -> { meta.setResourceType(resourceType.getName()); meta.setLocation(previousMeta.getLocation().orElse(getLocation(resourceType, id, baseUrlSupplier))); meta.setVersion(previousMeta.getVersion().orElse(null)); }); return patchRequestHandler.getUpdatedResource(patchedResourceNode, attributesList, excludedAttributesList); }); if (updatedResource == null) { // can only happen with custom implementations return new EmptyPatchResponse(getLocation(resourceType, id, baseUrlSupplier)); } ETagHandler.validateVersion(serviceProvider, resourceType, () -> updatedResource, context.getUriInfos().getHttpHeaders()); Optional responseValidator = // resourceHandler.getResponseValidator(attributesList, excludedAttributesList, patchRequestHandler.getRequestedAttributes(), getReferenceUrlSupplier(baseUrlSupplier)); JsonNode responseResource = updatedResource; if (responseValidator.isPresent()) { responseResource = responseValidator.get().validateDocument(responseResource); } return new UpdateResponse(responseResource, getLocation(resourceType, id, baseUrlSupplier), updatedResource.getMeta().orElseThrow(() -> { return new InternalServerException("Missing meta-attribute on patched resource"); })); } catch (RequestContextException ex) { ErrorResponse errorResponse = new ErrorResponse(ex); ex.getValidationContext().writeToErrorResponse(errorResponse); return errorResponse; } catch (ScimException ex) { return new ErrorResponse(ex); } catch (Exception ex) { return new ErrorResponse(new InternalServerException(ex.getMessage(), ex, null)); } } /** * tries to extract the resource type by its endpoint path suffix e.g. "/Users" or "/Groups" * * @param endpoint the endpoint path suffix * @return the resource type registered for the given path */ private ResourceType getResourceType(String endpoint) { Supplier errorMessage = () -> "no resource found for endpoint \'" + endpoint + "\'"; ResourceType resourceType = Optional.ofNullable(resourceTypeFactory.getResourceType(endpoint)) .orElseThrow(() -> new BadRequestException(errorMessage.get(), null, ScimType.Custom.UNKNOWN_RESOURCE)); log.trace("Determined resource type \'{}\'", resourceType.getName()); return resourceType; } /** * builds the location attribute for the meta-object * * @param resourceType holds the endpoint definition * @param resourceId the id of the resource to which the location should be determined * @param getBaseUrlSupplier this supplier is an optional attribute that should be used to supply the * information of the base URL of this application e.g.: https://example.com/scim/v2. This return * value will be used to create the location URL of the resources like * 'https://example.com/scim/v2/Users/123456'. If this parameter is not present the application will * try to read a hardcoded URL from the service provider configuration that is also an optional * attribute. If both ways fail an exception will be thrown * @return the current location */ private String getLocation(ResourceType resourceType, String resourceId, Supplier getBaseUrlSupplier) { String baseUrl = getBaseUrlSupplier == null ? null : getBaseUrlSupplier.get(); String escapedResourceId = EncodingUtils.urlEncode(resourceId); if (StringUtils.isBlank(baseUrl)) { return StringUtils.stripToEmpty(System.getProperty("SCIM_BASE_URL")) + resourceType.getEndpoint() + (StringUtils.isBlank(escapedResourceId) ? "" : "/" + escapedResourceId); } if (baseUrl.endsWith("/")) { baseUrl = baseUrl.substring(0, baseUrl.length() - 1); } return baseUrl + resourceType.getEndpoint() + (StringUtils.isBlank(escapedResourceId) ? "" : "/" + escapedResourceId); } /** * resolves the given baseUrl to a reference url based on the given resourceName and resourceId. This is used * to set absolute urls in resource attribute nodes that define the "$ref" attribute but did not set it * previously * * @param baseUrl the base url of this resource endpoint * @return the fully qualified url to the given resource or null */ BiFunction getReferenceUrlSupplier(Supplier baseUrl) { return (resourceId, resourceName) -> { Optional resourceType = resourceTypeFactory.getResourceTypeByName(resourceName); String id = StringUtils.isBlank(resourceId) ? "" : "/" + resourceId; return resourceType.map(jsonNodes -> baseUrl.get() + jsonNodes.getEndpoint() + id).orElse(null); }; } private void validateResourceVersion(String id, ResourceType resourceType, Supplier oldResourceSupplier, Map httpHeaders) { try { ETagHandler.validateVersion(serviceProvider, resourceType, oldResourceSupplier, httpHeaders); } catch (ResourceNotFoundException ex) { throw new ResourceNotFoundException("the \'" + resourceType.getName() + "\' resource with id \'" + id + "\' does " + "not exist", ex, null); } } /** * each created {@link ResourceEndpointHandler} must get hold of a single {@link ServiceProvider} instance * which holds the configuration of this service provider implementation */ @java.lang.SuppressWarnings("all") @lombok.Generated public ServiceProvider getServiceProvider() { return this.serviceProvider; } /** * contains a number of patch-workarounds that can be executed on patch-requests to fix illegal * operation-setups */ @java.lang.SuppressWarnings("all") @lombok.Generated public List> getPatchWorkarounds() { return this.patchWorkarounds; } /** * this is used to prevent application context pollution in unit tests */ @java.lang.SuppressWarnings("all") @lombok.Generated protected ResourceTypeFactory getResourceTypeFactory() { return this.resourceTypeFactory; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy