blob: 5faec26e1736204dcc2d236b7f9339efcedf7f5d [file] [log] [blame]
/*
* 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.spec.protocol.filter;
import org.apache.directory.scim.spec.exception.ScimResourceInvalidException;
import org.apache.directory.scim.spec.protocol.attribute.AttributeReference;
import org.apache.directory.scim.spec.schema.AttributeContainer;
import org.apache.directory.scim.spec.schema.Schema;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collection;
import java.util.Comparator;
import java.util.function.Predicate;
final class InMemoryScimFilterMatcher {
private static final Logger log = LoggerFactory.getLogger(InMemoryScimFilterMatcher.class);
private InMemoryScimFilterMatcher() {}
/**
* Converts a FilterExpression to a Predicate that can be used to later test a ScimResource (or child attribute).
* <p>
* The {@code attributeContainer} should be a top level Schema for the initial call, but recursive calls to this method
* will pass the AttributeContainer (a child-schema). For example, if given a
* {@link org.apache.directory.scim.spec.resources.ScimUser ScimUser} expression of {@code emails.value co "example.org"},
* The initial call to this method pass the ScimUser Schema object. When this method is called recursively, the
* {@link org.apache.directory.scim.spec.resources.Email Email}'s AttributeContainer will be used.
*
* @param expression the FilterExpression to build a predicate from.
* @param attributeContainer the Schema (or other sub attribute container) to build the predicate from.
* @return A Predicate that can be used to later test a ScimResource (or child attribute).
*/
static <R> Predicate<R> toPredicate(FilterExpression expression, AttributeContainer attributeContainer) {
// attribute EQ "something"
if (expression instanceof AttributeComparisonExpression) {
return new AttributeComparisonPredicate<>((AttributeComparisonExpression) expression, attributeContainer);
}
// (attribute EQ "something") AND (otherAttribute EQ "something else")
else if (expression instanceof LogicalExpression) {
LogicalExpression logicalExpression = (LogicalExpression) expression;
Predicate<R> left = toPredicate(logicalExpression.getLeft(), attributeContainer);
Predicate<R> right = toPredicate(logicalExpression.getRight(), attributeContainer);
LogicalOperator op = logicalExpression.getOperator();
if (op == LogicalOperator.AND) {
return left.and(right);
} else {
return left.or(right);
}
}
// NOT (attribute EQ "something")
else if (expression instanceof GroupExpression) {
GroupExpression groupExpression = (GroupExpression) expression;
Predicate<R> predicate = toPredicate(groupExpression.getFilterExpression(), attributeContainer);
return groupExpression.isNot()
? predicate.negate()
: predicate;
}
// attribute PR
else if (expression instanceof AttributePresentExpression) {
return new AttributePresentPredicate<>((AttributePresentExpression) expression, attributeContainer);
}
// addresses[type EQ "work"]
else if (expression instanceof ValuePathExpression) {
ValuePathExpression valuePathExpression = (ValuePathExpression) expression;
Predicate<Object> nestedPredicate = toPredicate(valuePathExpression.getAttributeExpression(), attribute(attributeContainer, valuePathExpression.getAttributePath()));
return new ValuePathPredicate<>(valuePathExpression, attributeContainer, nestedPredicate);
}
log.debug("Unsupported Filter expression of type: " + expression.getClass());
return scimResource -> false;
}
private static boolean isStringExpression(Schema.Attribute attribute, Object compareValue) {
if (attribute.getType() != Schema.Attribute.Type.STRING) {
log.debug("Invalid query, non String value for expression : " + attribute.getType());
return false;
}
if (compareValue == null) {
log.debug("Invalid query, empty value for expression : " + attribute.getType());
return false;
}
return true;
}
private static Schema.Attribute attribute(AttributeContainer attributeContainer, AttributeReference attributeReference) {
String baseAttributeName = attributeReference.getAttributeName();
Schema.Attribute schemaAttribute = attributeContainer.getAttribute(baseAttributeName);
if (schemaAttribute == null) {
log.warn("Invalid filter: attribute '" + baseAttributeName + "' is NOT a valid SCIM attribute.");
return null;
}
String subAttribute = attributeReference.getSubAttributeName();
if (subAttribute != null) {
schemaAttribute = schemaAttribute.getAttribute(subAttribute);
if (schemaAttribute == null) {
log.warn("Invalid filter: attribute '" + attributeReference.getFullyQualifiedAttributeName() + "' is NOT a valid SCIM attribute.");
return null;
}
}
// filter out fields like passwords that should not be queried against
if (schemaAttribute.getReturned() == Schema.Attribute.Returned.NEVER) {
log.warn("Invalid filter: attribute '" + attributeReference.getAttributeName() + "' is not filterable.");
return null;
}
return schemaAttribute;
}
private static abstract class AbstractAttributePredicate<T extends FilterExpression, R> implements Predicate<R> {
private static final Logger log = LoggerFactory.getLogger(InMemoryScimFilterMatcher.class);
final T expression;
final AttributeContainer attributeContainer;
final AttributeReference attributeReference;
private AbstractAttributePredicate(T expression, AttributeContainer attributeContainer, AttributeReference attributeReference) {
this.expression = expression;
this.attributeContainer = attributeContainer;
this.attributeReference = attributeReference;
}
protected abstract boolean test(Schema.Attribute attribute, Object actualValue);
@Override
public boolean test(R actual) {
try {
// get and validate attribute
Schema.Attribute resolvedAttribute = attribute(attributeContainer, attributeReference);
if (resolvedAttribute != null) {
// now walk the attribute path again to get the accessor and value
Schema.Attribute schemaAttribute = attributeContainer.getAttribute(attributeReference.getAttributeName());
actual = schemaAttribute.getAccessor().get(actual);
// if the attribute has a sub-level, continue on
String subAttribute = attributeReference.getSubAttributeName();
if (subAttribute != null) {
schemaAttribute = schemaAttribute.getAttribute(subAttribute);
actual = schemaAttribute.getAccessor().get(actual);
}
return test(schemaAttribute, actual);
}
} catch (Exception e) {
// The SCIM spec states to ignore the query instead of rejecting it - rfc7644 - 3.4.2
log.debug("Invalid SCIM filter received", e);
}
return false;
}
}
private static class ValuePathPredicate<R> extends AbstractAttributePredicate<ValuePathExpression, R> {
final private Predicate<Object> nestedPredicate;
ValuePathPredicate(ValuePathExpression expression, AttributeContainer attributeContainer, Predicate<Object> nestedPredicate) {
super(expression, attributeContainer, expression.getAttributePath());
this.nestedPredicate = nestedPredicate;
}
@Override
protected boolean test(Schema.Attribute attribute, Object actualValue) {
// actualValue must be a Collection
if (attribute.isMultiValued()) {
return ((Collection<?>) actualValue).stream().anyMatch(nestedPredicate);
}
return false;
}
}
private static class AttributeComparisonPredicate<R> extends AbstractAttributePredicate<AttributeComparisonExpression, R> {
private AttributeComparisonPredicate(AttributeComparisonExpression expression, AttributeContainer attributeContainer) {
super(expression, attributeContainer, expression.getAttributePath());
}
@Override
protected boolean test(Schema.Attribute attribute, Object actualValue) {
if (actualValue == null) {
return false;
}
if (attribute.isMultiValued()) {
log.warn("Invalid expression, target is collection");
return false;
}
CompareOperator op = expression.getOperation();
Object compareValue = expression.getCompareValue();
if (op == CompareOperator.EQ) {
return compareValue.equals(actualValue);
}
if (op == CompareOperator.NE) {
return !compareValue.equals(actualValue);
}
if (op == CompareOperator.SW) {
return isStringExpression(attribute, compareValue)
&& actualValue.toString().startsWith(compareValue.toString());
}
if (op == CompareOperator.EW) {
return isStringExpression(attribute, compareValue)
&& actualValue.toString().endsWith(compareValue.toString());
}
if (op == CompareOperator.CO) {
return isStringExpression(attribute, compareValue)
&& actualValue.toString().contains(compareValue.toString());
}
if (actualValue instanceof Comparable) {
Comparable actual = (Comparable) actualValue;
Comparable compare = (Comparable) compareValue;
return CompareOperatorPredicate.naturalOrder(op, compare).test(actual);
}
throw new ScimResourceInvalidException("Unsupported operation in filter: " + op.name());
}
}
private static class CompareOperatorPredicate<T> implements Predicate<T> {
private final CompareOperator op;
private final Comparator<T> comparator;
private final T comparedValue;
private CompareOperatorPredicate(CompareOperator op, T comparedValue, Comparator<T> comparator) {
this.op = op;
this.comparator = comparator;
this.comparedValue = comparedValue;
}
@Override
public boolean test(T actualValue) {
int compareResult = comparator.compare(actualValue, comparedValue);
if (op == CompareOperator.LT) {
return compareResult < 0;
} else if (op == CompareOperator.GT) {
return compareResult > 0;
} else if (op == CompareOperator.LE) {
return compareResult <= 0;
} else if (op == CompareOperator.GE) {
return compareResult >= 0;
}
return false;
}
static <T extends Comparable<T>> CompareOperatorPredicate<T> naturalOrder(CompareOperator op, T comparedValue) {
return new CompareOperatorPredicate<>(op, comparedValue, Comparator.naturalOrder());
}
}
private static class AttributePresentPredicate<R> extends AbstractAttributePredicate<AttributePresentExpression, R> {
private AttributePresentPredicate(AttributePresentExpression expression, AttributeContainer attributeContainer) {
super(expression, attributeContainer, expression.getAttributePath());
}
@Override
protected boolean test(Schema.Attribute attribute, Object actualValue) {
if (attribute.isMultiValued()) {
log.debug("Invalid expression, target is collection");
return false;
}
return actualValue != null;
}
}
}