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