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}