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}