| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| package org.apache.directory.scim.server.utility; |
| |
| import com.fasterxml.jackson.annotation.JsonInclude.Include; |
| import com.fasterxml.jackson.databind.DeserializationFeature; |
| import com.fasterxml.jackson.databind.ObjectMapper; |
| import com.fasterxml.jackson.databind.module.SimpleModule; |
| import jakarta.enterprise.context.ApplicationScoped; |
| import lombok.extern.slf4j.Slf4j; |
| import org.apache.commons.lang3.StringUtils; |
| import org.apache.directory.scim.server.exception.AttributeDoesNotExistException; |
| import org.apache.directory.scim.server.rest.ScimResourceDeserializer; |
| import org.apache.directory.scim.server.schema.Registry; |
| import org.apache.directory.scim.spec.json.ObjectMapperFactory; |
| import org.apache.directory.scim.spec.protocol.attribute.AttributeReference; |
| import org.apache.directory.scim.spec.resources.ScimExtension; |
| import org.apache.directory.scim.spec.resources.ScimGroup; |
| import org.apache.directory.scim.spec.resources.ScimResource; |
| import org.apache.directory.scim.spec.resources.ScimUser; |
| import org.apache.directory.scim.spec.schema.AttributeContainer; |
| import org.apache.directory.scim.spec.schema.Schema; |
| import org.apache.directory.scim.spec.schema.Schema.Attribute; |
| import org.apache.directory.scim.spec.schema.Schema.Attribute.Returned; |
| import org.apache.directory.scim.spec.schema.Schema.Attribute.Type; |
| |
| import jakarta.inject.Inject; |
| import java.io.ByteArrayInputStream; |
| import java.io.ByteArrayOutputStream; |
| import java.io.IOException; |
| import java.io.ObjectInputStream; |
| import java.io.ObjectOutputStream; |
| import java.lang.reflect.Field; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map.Entry; |
| import java.util.Set; |
| import java.util.function.Function; |
| |
| @Slf4j |
| @ApplicationScoped |
| public class AttributeUtil { |
| |
| Registry registry; |
| |
| ObjectMapper objectMapper; |
| |
| @Inject |
| public AttributeUtil(Registry registry) { |
| this.registry = registry; |
| |
| // TODO move this to a CDI producer |
| objectMapper = ObjectMapperFactory.getObjectMapper(); |
| objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); |
| objectMapper.setSerializationInclusion(Include.NON_NULL); |
| |
| SimpleModule module = new SimpleModule(); |
| module.addDeserializer(ScimResource.class, new ScimResourceDeserializer(this.registry, this.objectMapper)); |
| objectMapper.registerModule(module); |
| } |
| |
| public <T extends ScimResource> T keepAlwaysAttributesForDisplay(T resource) throws IllegalArgumentException, IllegalAccessException, AttributeDoesNotExistException, IOException { |
| return setAttributesForDisplayInternal(resource, Returned.DEFAULT, Returned.REQUEST, Returned.NEVER); |
| } |
| |
| public <T extends ScimResource> T setAttributesForDisplay(T resource) throws IllegalArgumentException, IllegalAccessException, AttributeDoesNotExistException, IOException { |
| return setAttributesForDisplayInternal(resource, Returned.REQUEST, Returned.NEVER); |
| } |
| |
| private <T extends ScimResource> T setAttributesForDisplayInternal(T resource, Returned ... removeAttributesOfTypes) throws IllegalArgumentException, IllegalAccessException, AttributeDoesNotExistException, IOException { |
| T copy = cloneScimResource(resource); |
| String resourceType = copy.getResourceType(); |
| Schema schema = registry.getBaseSchemaOfResourceType(resourceType); |
| |
| // return always and default, exclude never and requested |
| for (Returned removeAttributesOfType : removeAttributesOfTypes) { |
| removeAttributesOfType(copy, schema, removeAttributesOfType); |
| } |
| |
| for (Entry<String, ScimExtension> extensionEntry : copy.getExtensions().entrySet()) { |
| String extensionUrn = extensionEntry.getKey(); |
| ScimExtension scimExtension = extensionEntry.getValue(); |
| |
| Schema extensionSchema = registry.getSchema(extensionUrn); |
| |
| for (Returned removeAttributesOfType : removeAttributesOfTypes) { |
| removeAttributesOfType(scimExtension, extensionSchema, removeAttributesOfType); |
| } |
| } |
| return copy; |
| } |
| |
| public <T extends ScimResource> T setAttributesForDisplay(T resource, Set<AttributeReference> attributes) throws IllegalArgumentException, IllegalAccessException, AttributeDoesNotExistException, IOException { |
| if (attributes.isEmpty()) { |
| return setAttributesForDisplay(resource); |
| } else { |
| T copy = cloneScimResource(resource); |
| |
| String resourceType = copy.getResourceType(); |
| Schema schema = registry.getBaseSchemaOfResourceType(resourceType); |
| |
| // return always and specified attributes, exclude never |
| Set<Attribute> attributesToKeep = resolveAttributeReferences(attributes, true); |
| Set<String> extensionsToRemove = new HashSet<>(); |
| removeAttributesOfType(copy, schema, Returned.DEFAULT, attributesToKeep); |
| removeAttributesOfType(copy, schema, Returned.REQUEST, attributesToKeep); |
| removeAttributesOfType(copy, schema, Returned.NEVER); |
| |
| for (Entry<String, ScimExtension> extensionEntry : copy.getExtensions().entrySet()) { |
| String extensionUrn = extensionEntry.getKey(); |
| ScimExtension scimExtension = extensionEntry.getValue(); |
| boolean removeExtension = true; |
| |
| for (Attribute attributeToKeep : attributesToKeep) { |
| if (extensionUrn.equalsIgnoreCase(attributeToKeep.getUrn())) { |
| removeExtension = false; |
| |
| break; |
| } |
| } |
| if (removeExtension) { |
| extensionsToRemove.add(extensionUrn); |
| |
| continue; |
| } |
| Schema extensionSchema = registry.getSchema(extensionUrn); |
| |
| removeAttributesOfType(scimExtension, extensionSchema, Returned.DEFAULT, attributesToKeep); |
| removeAttributesOfType(scimExtension, extensionSchema, Returned.REQUEST, attributesToKeep); |
| removeAttributesOfType(scimExtension, extensionSchema, Returned.NEVER); |
| } |
| for (String extensionUrn : extensionsToRemove) { |
| copy.removeExtension(extensionUrn); |
| } |
| return copy; |
| } |
| } |
| |
| public <T extends ScimResource> T setExcludedAttributesForDisplay(T resource, Set<AttributeReference> excludedAttributes) throws IllegalArgumentException, IllegalAccessException, AttributeDoesNotExistException, IOException { |
| |
| if (excludedAttributes.isEmpty()) { |
| return setAttributesForDisplay(resource); |
| } else { |
| T copy = cloneScimResource(resource); |
| |
| String resourceType = copy.getResourceType(); |
| Schema schema = registry.getBaseSchemaOfResourceType(resourceType); |
| |
| // return always and default, exclude never and specified attributes |
| Set<Attribute> attributesToRemove = resolveAttributeReferences(excludedAttributes, false); |
| removeAttributesOfType(copy, schema, Returned.REQUEST); |
| removeAttributesOfType(copy, schema, Returned.NEVER); |
| removeAttributes(copy, schema, attributesToRemove); |
| |
| for (Entry<String, ScimExtension> extensionEntry : copy.getExtensions().entrySet()) { |
| String extensionUrn = extensionEntry.getKey(); |
| ScimExtension scimExtension = extensionEntry.getValue(); |
| |
| Schema extensionSchema = registry.getSchema(extensionUrn); |
| |
| removeAttributesOfType(scimExtension, extensionSchema, Returned.REQUEST); |
| removeAttributesOfType(scimExtension, extensionSchema, Returned.NEVER); |
| removeAttributes(scimExtension, extensionSchema, attributesToRemove); |
| } |
| return copy; |
| } |
| } |
| |
| @SuppressWarnings("unchecked") |
| private <T extends ScimResource> T cloneScimResource(T original) throws IOException { |
| ByteArrayOutputStream boas = new ByteArrayOutputStream(); |
| ObjectOutputStream oos = new ObjectOutputStream(boas); |
| oos.writeObject(original); |
| |
| ByteArrayInputStream bais = new ByteArrayInputStream(boas.toByteArray()); |
| ObjectInputStream ois = new ObjectInputStream(bais); |
| T copy = null; |
| try { |
| copy = (T) ois.readObject(); |
| } catch (ClassNotFoundException e) { |
| // Should never happen |
| log.error("", e); |
| } |
| return copy; |
| } |
| |
| private void removeAttributesOfType(Object object, AttributeContainer attributeContainer, Returned returned) throws IllegalArgumentException, IllegalAccessException { |
| Function<Attribute, Boolean> function = (attribute) -> returned == attribute.getReturned(); |
| processAttributes(object, attributeContainer, function); |
| } |
| |
| private void removeAttributesOfType(Object object, AttributeContainer attributeContainer, Returned returned, Set<Attribute> attributesToKeep) throws IllegalArgumentException, IllegalAccessException { |
| Function<Attribute, Boolean> function = (attribute) -> !attributesToKeep.contains(attribute) && returned == attribute.getReturned(); |
| processAttributes(object, attributeContainer, function); |
| } |
| |
| private void removeAttributes(Object object, AttributeContainer attributeContainer, Set<Attribute> attributesToRemove) throws IllegalArgumentException, IllegalAccessException { |
| Function<Attribute, Boolean> function = (attribute) -> attributesToRemove.contains(attribute); |
| processAttributes(object, attributeContainer, function); |
| } |
| |
| private void processAttributes(Object object, AttributeContainer attributeContainer, Function<Attribute, Boolean> function) throws IllegalArgumentException, IllegalAccessException { |
| if (attributeContainer != null && object != null) { |
| for (Attribute attribute : attributeContainer.getAttributes()) { |
| Field field = attribute.getField(); |
| if (function.apply(attribute)) { |
| field.setAccessible(true); |
| if (!field.getType().isPrimitive()) { |
| Object obj = field.get(object); |
| if (obj == null) { |
| continue; |
| } |
| |
| log.info("field to be set to null = " + field.getType().getName()); |
| field.set(object, null); |
| } |
| } else if (!attribute.isMultiValued() && attribute.getType() == Type.COMPLEX) { |
| String name = field.getName(); |
| log.debug("### Processing single value complex field " + name); |
| field.setAccessible(true); |
| Object subObject = field.get(object); |
| |
| if (subObject == null) { |
| continue; |
| } |
| |
| Attribute subAttribute = attributeContainer.getAttribute(name); |
| log.debug("### container type = " + attributeContainer.getClass().getName()); |
| if (subAttribute == null) { |
| log.debug("#### subattribute == null"); |
| } |
| processAttributes(subObject, subAttribute, function); |
| } else if (attribute.isMultiValued() && attribute.getType() == Type.COMPLEX) { |
| String name = field.getName(); |
| log.debug("### Processing multi-valued complex field " + name); |
| field.setAccessible(true); |
| Object subObject = field.get(object); |
| |
| if (subObject == null) { |
| continue; |
| } |
| |
| if (Collection.class.isAssignableFrom(subObject.getClass())) { |
| Collection<?> collection = (Collection<?>) subObject; |
| for (Object o : collection) { |
| Attribute subAttribute = attributeContainer.getAttribute(name); |
| processAttributes(o, subAttribute, function); |
| } |
| } else if (field.getType().isArray()) { |
| Object[] array = (Object[]) subObject; |
| |
| for (Object o : array) { |
| Attribute subAttribute = attributeContainer.getAttribute(name); |
| processAttributes(o, subAttribute, function); |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| public Set<AttributeReference> getAttributeReferences(String s) { |
| Set<AttributeReference> attributeReferences = new HashSet<>(); |
| |
| String[] split = StringUtils.split(s, ","); |
| |
| for (String af : split) { |
| AttributeReference attributeReference = new AttributeReference(af); |
| attributeReferences.add(attributeReference); |
| } |
| |
| return attributeReferences; |
| } |
| |
| private Set<Attribute> resolveAttributeReferences(Set<AttributeReference> attributeReferences, boolean includeAttributeChain) throws AttributeDoesNotExistException { |
| Set<Attribute> attributes = new HashSet<>(); |
| |
| for (AttributeReference attributeReference : attributeReferences) { |
| Set<Attribute> findAttributes = findAttribute(attributeReference, includeAttributeChain); |
| if (!findAttributes.isEmpty()) { |
| attributes.addAll(findAttributes); |
| } |
| } |
| |
| return attributes; |
| } |
| |
| private Set<Attribute> findAttribute(AttributeReference attributeReference, boolean includeAttributeChain) throws AttributeDoesNotExistException { |
| String schemaUrn = attributeReference.getUrn(); |
| Schema schema = null; |
| Set<Attribute> attributes; |
| |
| if (!StringUtils.isEmpty(schemaUrn)) { |
| schema = registry.getSchema(schemaUrn); |
| |
| attributes = findAttributeInSchema(schema, attributeReference, includeAttributeChain); |
| if (attributes.isEmpty()) { |
| log.error("Attribute " + attributeReference.getFullyQualifiedAttributeName() + "not found in schema " + schemaUrn); |
| throw new AttributeDoesNotExistException(attributeReference.getFullyQualifiedAttributeName()); |
| } |
| return attributes; |
| } |
| |
| // Handle unqualified attributes, look in the core schemas |
| schema = registry.getSchema(ScimUser.SCHEMA_URI); |
| attributes = findAttributeInSchema(schema, attributeReference, includeAttributeChain); |
| if (!attributes.isEmpty()) { |
| return attributes; |
| } |
| |
| schema = registry.getSchema(ScimGroup.SCHEMA_URI); |
| attributes = findAttributeInSchema(schema, attributeReference, includeAttributeChain); |
| if (!attributes.isEmpty()) { |
| return attributes; |
| } |
| |
| log.error("Attribute " + attributeReference.getFullyQualifiedAttributeName() + "not found in any schema."); |
| throw new AttributeDoesNotExistException(attributeReference.getFullyQualifiedAttributeName()); |
| } |
| |
| private Set<Attribute> findAttributeInSchema(Schema schema, AttributeReference attributeReference, boolean includeAttributeChain) { |
| AttributeContainer attributeContainer = schema; |
| if (attributeContainer == null) { |
| return Collections.emptySet(); |
| } |
| Set<Attribute> attributes = new HashSet<>(); |
| String attributeName = attributeReference.getAttributeName(); |
| String subAttributeName = attributeReference.getSubAttributeName(); |
| Attribute attribute = attributeContainer.getAttribute(attributeName); |
| |
| if (attribute == null) { |
| return Collections.emptySet(); |
| } |
| if (includeAttributeChain || subAttributeName == null) { |
| attributes.add(attribute); |
| } |
| if (subAttributeName != null) { |
| attribute = attribute.getAttribute(subAttributeName); |
| |
| if (attribute == null) { |
| return Collections.emptySet(); |
| } |
| attributes.add(attribute); |
| } |
| if (attribute.getType() == Type.COMPLEX && includeAttributeChain) { |
| List<Attribute> remaininAttributes = attribute.getAttributes(); |
| attributes.addAll(remaininAttributes); |
| } |
| return attributes; |
| } |
| |
| } |