001/*
002 * (C) Copyright 2007 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Nuxeo - initial API and implementation
018 *
019 * $Id$
020 */
021
022package org.nuxeo.ecm.directory.ldap;
023
024import java.io.IOException;
025import java.text.ParseException;
026import java.util.regex.Pattern;
027import java.util.regex.PatternSyntaxException;
028
029import javax.naming.NamingEnumeration;
030import javax.naming.NamingException;
031import javax.naming.directory.Attribute;
032import javax.naming.directory.Attributes;
033
034import org.apache.directory.shared.ldap.filter.BranchNode;
035import org.apache.directory.shared.ldap.filter.ExprNode;
036import org.apache.directory.shared.ldap.filter.FilterParser;
037import org.apache.directory.shared.ldap.filter.FilterParserImpl;
038import org.apache.directory.shared.ldap.filter.PresenceNode;
039import org.apache.directory.shared.ldap.filter.SimpleNode;
040import org.apache.directory.shared.ldap.filter.SubstringNode;
041import org.apache.directory.shared.ldap.name.DefaultStringNormalizer;
042import org.apache.directory.shared.ldap.schema.Normalizer;
043import org.nuxeo.ecm.directory.DirectoryException;
044
045/**
046 * Helper class to parse and evaluate if a LDAP filter expression matches a fetched LDAP entry.
047 * <p>
048 * This is done by recursively evaluating the abstract syntax tree of the expression as parsed by an apache directory
049 * shared method.
050 *
051 * @author Olivier Grisel <ogrisel@nuxeo.com>
052 */
053public class LDAPFilterMatcher {
054
055    private final FilterParser parser;
056
057    // lazily initialized normalizer for the substring match
058    private Normalizer normalizer;
059
060    LDAPFilterMatcher() {
061        parser = new FilterParserImpl();
062    }
063
064    /**
065     * Check whether a raw string filter expression matches on the given LDAP entry.
066     *
067     * @param attributes the ldap entry to match
068     * @param filter a raw string filter expression (eg. <tt>(!(&(attr1=*)(attr2=value2)(attr3=val*)))</tt> )
069     * @return true if the ldap entry matches the filter
070     * @throws DirectoryException if the filter is not a valid LDAP filter
071     */
072    public boolean match(Attributes attributes, String filter) throws DirectoryException {
073        if (filter == null || "".equals(filter)) {
074            return true;
075        }
076        try {
077            ExprNode parsedFilter = parser.parse(filter);
078            return recursiveMatch(attributes, parsedFilter);
079        } catch (DirectoryException | IOException | ParseException e) {
080            throw new DirectoryException("could not parse LDAP filter: " + filter, e);
081        }
082    }
083
084    private boolean recursiveMatch(Attributes attributes, ExprNode filterElement) throws DirectoryException {
085        if (filterElement instanceof PresenceNode) {
086            return presenceMatch(attributes, (PresenceNode) filterElement);
087        } else if (filterElement instanceof SimpleNode) {
088            return simpleMatch(attributes, (SimpleNode) filterElement);
089        } else if (filterElement instanceof SubstringNode) {
090            return substringMatch(attributes, (SubstringNode) filterElement);
091        } else if (filterElement instanceof BranchNode) {
092            return branchMatch(attributes, (BranchNode) filterElement);
093        } else {
094            throw new DirectoryException("unsupported filter element type: " + filterElement);
095        }
096    }
097
098    /**
099     * Handle attribute presence check (eg: <tt>(attr1=*)</tt>)
100     */
101    private boolean presenceMatch(Attributes attributes, PresenceNode presenceElement) {
102        return attributes.get(presenceElement.getAttribute()) != null;
103    }
104
105    /**
106     * Handle simple equality test on any non-null value (eg: <tt>(attr2=value2)</tt>).
107     *
108     * @return true if the equality holds
109     */
110    protected static boolean simpleMatch(Attributes attributes, SimpleNode simpleElement) throws DirectoryException {
111        Attribute attribute = attributes.get(simpleElement.getAttribute());
112        if (attribute == null) {
113            // null attribute cannot match any equality statement
114            return false;
115        }
116        boolean isCaseSensitive = isCaseSensitiveMatch(attribute);
117        try {
118            NamingEnumeration<?> rawValues = attribute.getAll();
119            try {
120                while (rawValues.hasMore()) {
121                    String rawValue = rawValues.next().toString();
122                    if (isCaseSensitive || !(rawValue instanceof String)
123                            || !(simpleElement.getValue() instanceof String)) {
124                        if (simpleElement.getValue().equals(rawValue)) {
125                            return true;
126                        }
127                    } else {
128                        String stringValue = (String) rawValue;
129                        String stringElementValue = (String) simpleElement.getValue();
130                        if (stringElementValue.equalsIgnoreCase(stringValue)) {
131                            return true;
132                        }
133                    }
134                }
135            } finally {
136                rawValues.close();
137            }
138        } catch (NamingException e) {
139            throw new DirectoryException("could not retrieve value for attribute: " + simpleElement.getAttribute());
140        }
141        return false;
142    }
143
144    protected static boolean isCaseSensitiveMatch(Attribute attribute) {
145        // TODO: introspect the content of
146        // attribute.getAttributeSyntaxDefinition() to know whether the
147        // attribute is case sensitive for exact match and cache the results.
148        // fallback to case in-sensitive if syntax definition is missing
149        return false;
150    }
151
152    protected static boolean isCaseSensitiveSubstringMatch(Attribute attribute) {
153        // TODO: introspect the content of
154        // attribute.getAttributeSyntaxDefinition() to know whether the
155        // attribute is case sensitive for substring match and cache the
156        // results.
157        // fallback to case in-sensitive if syntax definition is missing
158        return false;
159    }
160
161    /**
162     * Implement the substring match on any non-null value of a string attribute (eg: <tt>(attr3=val*)</tt>).
163     *
164     * @return the result of the regex evaluation
165     */
166    protected boolean substringMatch(Attributes attributes, SubstringNode substringElement) throws DirectoryException {
167        try {
168
169            Attribute attribute = attributes.get(substringElement.getAttribute());
170            if (attribute == null) {
171                // null attribute cannot match any regex
172                return false;
173            }
174            NamingEnumeration<?> rawValues = attribute.getAll();
175            try {
176                while (rawValues.hasMore()) {
177                    String rawValue = rawValues.next().toString();
178                    Normalizer normalizer = getNormalizer();
179                    StringBuffer sb = new StringBuffer();
180                    String initial = substringElement.getInitial();
181                    String finalSegment = substringElement.getFinal();
182                    if (initial != null && !initial.isEmpty()) {
183                        sb.append(Pattern.quote((String) normalizer.normalize(initial)));
184                    }
185                    sb.append(".*");
186                    for (Object segment : substringElement.getAny()) {
187                        if (segment instanceof String) {
188                            sb.append(Pattern.quote((String) normalizer.normalize(segment)));
189                            sb.append(".*");
190                        }
191                    }
192                    if (finalSegment != null && !finalSegment.isEmpty()) {
193                        sb.append(Pattern.quote((String) normalizer.normalize(finalSegment)));
194                    }
195                    Pattern pattern;
196                    try {
197                        if (isCaseSensitiveSubstringMatch(attribute)) {
198                            pattern = Pattern.compile(sb.toString());
199                        } else {
200                            pattern = Pattern.compile(sb.toString(), Pattern.CASE_INSENSITIVE);
201                        }
202                    } catch (PatternSyntaxException e) {
203                        throw new DirectoryException("could not build regexp for substring: "
204                                + substringElement.toString());
205                    }
206                    if (pattern.matcher(rawValue).matches()) {
207                        return true;
208                    }
209                }
210            } finally {
211                rawValues.close();
212            }
213            return false;
214        } catch (NamingException e1) {
215            throw new DirectoryException("could not retrieve value for attribute: " + substringElement.getAttribute());
216        }
217    }
218
219    private Normalizer getNormalizer() {
220        if (normalizer == null) {
221            normalizer = new DefaultStringNormalizer();
222        }
223        return normalizer;
224    }
225
226    /**
227     * Handle conjunction, disjunction and negation nodes and recursively call the generic matcher on children.
228     *
229     * @return the boolean value of the evaluation of the sub expression
230     */
231    private boolean branchMatch(Attributes attributes, BranchNode branchElement) throws DirectoryException {
232        if (branchElement.isConjunction()) {
233            for (ExprNode child : branchElement.getChildren()) {
234                if (!recursiveMatch(attributes, child)) {
235                    return false;
236                }
237            }
238            return true;
239        } else if (branchElement.isDisjunction()) {
240            for (ExprNode child : branchElement.getChildren()) {
241                if (recursiveMatch(attributes, child)) {
242                    return true;
243                }
244            }
245            return false;
246        } else if (branchElement.isNegation()) {
247            return !recursiveMatch(attributes, branchElement.getChild());
248        } else {
249            throw new DirectoryException("unsupported branching filter element type: " + branchElement.toString());
250        }
251    }
252
253}