/*
* (C) Copyright 2007-2016 Nuxeo SA (http://nuxeo.com/) and others.
*
* Licensed 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.
*
* Contributors:
* Nuxeo - initial API and implementation
*
*/
package org.nuxeo.ecm.directory.ldap;
import java.io.IOException;
import java.text.ParseException;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import org.apache.directory.shared.ldap.filter.BranchNode;
import org.apache.directory.shared.ldap.filter.ExprNode;
import org.apache.directory.shared.ldap.filter.FilterParser;
import org.apache.directory.shared.ldap.filter.FilterParserImpl;
import org.apache.directory.shared.ldap.filter.PresenceNode;
import org.apache.directory.shared.ldap.filter.SimpleNode;
import org.apache.directory.shared.ldap.filter.SubstringNode;
import org.apache.directory.shared.ldap.name.DefaultStringNormalizer;
import org.apache.directory.shared.ldap.schema.Normalizer;
import org.nuxeo.ecm.directory.DirectoryException;
/**
* Helper class to parse and evaluate if a LDAP filter expression matches a fetched LDAP entry.
* <p>
* This is done by recursively evaluating the abstract syntax tree of the expression as parsed by an apache directory
* shared method.
*
* @author Olivier Grisel <ogrisel@nuxeo.com>
*/
public class LDAPFilterMatcher {
private final FilterParser parser;
// lazily initialized normalizer for the substring match
private Normalizer normalizer;
LDAPFilterMatcher() {
parser = new FilterParserImpl();
}
/**
* Check whether a raw string filter expression matches on the given LDAP entry.
*
* @param attributes the ldap entry to match
* @param filter a raw string filter expression (eg. <tt>(!(&(attr1=*)(attr2=value2)(attr3=val*)))</tt> )
* @return true if the ldap entry matches the filter
* @throws DirectoryException if the filter is not a valid LDAP filter
*/
public boolean match(Attributes attributes, String filter) throws DirectoryException {
if (filter == null || "".equals(filter)) {
return true;
}
try {
ExprNode parsedFilter = parser.parse(filter);
return recursiveMatch(attributes, parsedFilter);
} catch (DirectoryException | IOException | ParseException e) {
throw new DirectoryException("could not parse LDAP filter: " + filter, e);
}
}
private boolean recursiveMatch(Attributes attributes, ExprNode filterElement) throws DirectoryException {
if (filterElement instanceof PresenceNode) {
return presenceMatch(attributes, (PresenceNode) filterElement);
} else if (filterElement instanceof SimpleNode) {
return simpleMatch(attributes, (SimpleNode) filterElement);
} else if (filterElement instanceof SubstringNode) {
return substringMatch(attributes, (SubstringNode) filterElement);
} else if (filterElement instanceof BranchNode) {
return branchMatch(attributes, (BranchNode) filterElement);
} else {
throw new DirectoryException("unsupported filter element type: " + filterElement);
}
}
/**
* Handle attribute presence check (eg: <tt>(attr1=*)</tt>)
*/
private boolean presenceMatch(Attributes attributes, PresenceNode presenceElement) {
return attributes.get(presenceElement.getAttribute()) != null;
}
/**
* Handle simple equality test on any non-null value (eg: <tt>(attr2=value2)</tt>).
*
* @return true if the equality holds
*/
protected static boolean simpleMatch(Attributes attributes, SimpleNode simpleElement) throws DirectoryException {
Attribute attribute = attributes.get(simpleElement.getAttribute());
if (attribute == null) {
// null attribute cannot match any equality statement
return false;
}
boolean isCaseSensitive = isCaseSensitiveMatch(attribute);
try {
NamingEnumeration<?> rawValues = attribute.getAll();
try {
while (rawValues.hasMore()) {
String rawValue = rawValues.next().toString();
if (isCaseSensitive || !(simpleElement.getValue() instanceof String)) {
if (simpleElement.getValue().equals(rawValue)) {
return true;
}
} else {
String stringElementValue = (String) simpleElement.getValue();
if (stringElementValue.equalsIgnoreCase(rawValue)) {
return true;
}
}
}
} finally {
rawValues.close();
}
} catch (NamingException e) {
throw new DirectoryException("could not retrieve value for attribute: " + simpleElement.getAttribute());
}
return false;
}
protected static boolean isCaseSensitiveMatch(Attribute attribute) {
// TODO: introspect the content of
// attribute.getAttributeSyntaxDefinition() to know whether the
// attribute is case sensitive for exact match and cache the results.
// fallback to case in-sensitive if syntax definition is missing
return false;
}
protected static boolean isCaseSensitiveSubstringMatch(Attribute attribute) {
// TODO: introspect the content of
// attribute.getAttributeSyntaxDefinition() to know whether the
// attribute is case sensitive for substring match and cache the
// results.
// fallback to case in-sensitive if syntax definition is missing
return false;
}
/**
* Implement the substring match on any non-null value of a string attribute (eg: <tt>(attr3=val*)</tt>).
*
* @return the result of the regex evaluation
*/
protected boolean substringMatch(Attributes attributes, SubstringNode substringElement) throws DirectoryException {
try {
Attribute attribute = attributes.get(substringElement.getAttribute());
if (attribute == null) {
// null attribute cannot match any regex
return false;
}
NamingEnumeration<?> rawValues = attribute.getAll();
try {
while (rawValues.hasMore()) {
String rawValue = rawValues.next().toString();
getNormalizer();
StringBuffer sb = new StringBuffer();
String initial = substringElement.getInitial();
String finalSegment = substringElement.getFinal();
if (initial != null && !initial.isEmpty()) {
sb.append(Pattern.quote((String) normalizer.normalize(initial)));
}
sb.append(".*");
for (Object segment : substringElement.getAny()) {
if (segment instanceof String) {
sb.append(Pattern.quote((String) normalizer.normalize(segment)));
sb.append(".*");
}
}
if (finalSegment != null && !finalSegment.isEmpty()) {
sb.append(Pattern.quote((String) normalizer.normalize(finalSegment)));
}
Pattern pattern;
try {
if (isCaseSensitiveSubstringMatch(attribute)) {
pattern = Pattern.compile(sb.toString());
} else {
pattern = Pattern.compile(sb.toString(), Pattern.CASE_INSENSITIVE);
}
} catch (PatternSyntaxException e) {
throw new DirectoryException("could not build regexp for substring: "
+ substringElement.toString());
}
if (pattern.matcher(rawValue).matches()) {
return true;
}
}
} finally {
rawValues.close();
}
return false;
} catch (NamingException e1) {
throw new DirectoryException("could not retrieve value for attribute: " + substringElement.getAttribute());
}
}
private Normalizer getNormalizer() {
if (normalizer == null) {
normalizer = new DefaultStringNormalizer();
}
return normalizer;
}
/**
* Handle conjunction, disjunction and negation nodes and recursively call the generic matcher on children.
*
* @return the boolean value of the evaluation of the sub expression
*/
private boolean branchMatch(Attributes attributes, BranchNode branchElement) throws DirectoryException {
if (branchElement.isConjunction()) {
for (ExprNode child : branchElement.getChildren()) {
if (!recursiveMatch(attributes, child)) {
return false;
}
}
return true;
} else if (branchElement.isDisjunction()) {
for (ExprNode child : branchElement.getChildren()) {
if (recursiveMatch(attributes, child)) {
return true;
}
}
return false;
} else if (branchElement.isNegation()) {
return !recursiveMatch(attributes, branchElement.getChild());
} else {
throw new DirectoryException("unsupported branching filter element type: " + branchElement.toString());
}
}
}