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}