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