Chris@909
|
1 # $Id: filter.rb 151 2006-08-15 08:34:53Z blackhedd $
|
Chris@909
|
2 #
|
Chris@909
|
3 #
|
Chris@909
|
4 #----------------------------------------------------------------------------
|
Chris@909
|
5 #
|
Chris@909
|
6 # Copyright (C) 2006 by Francis Cianfrocca. All Rights Reserved.
|
Chris@909
|
7 #
|
Chris@909
|
8 # Gmail: garbagecat10
|
Chris@909
|
9 #
|
Chris@909
|
10 # This program is free software; you can redistribute it and/or modify
|
Chris@909
|
11 # it under the terms of the GNU General Public License as published by
|
Chris@909
|
12 # the Free Software Foundation; either version 2 of the License, or
|
Chris@909
|
13 # (at your option) any later version.
|
Chris@909
|
14 #
|
Chris@909
|
15 # This program is distributed in the hope that it will be useful,
|
Chris@909
|
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
|
Chris@909
|
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
Chris@909
|
18 # GNU General Public License for more details.
|
Chris@909
|
19 #
|
Chris@909
|
20 # You should have received a copy of the GNU General Public License
|
Chris@909
|
21 # along with this program; if not, write to the Free Software
|
Chris@909
|
22 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
Chris@909
|
23 #
|
Chris@909
|
24 #---------------------------------------------------------------------------
|
Chris@909
|
25 #
|
Chris@909
|
26 #
|
Chris@909
|
27
|
Chris@909
|
28
|
Chris@909
|
29 module Net
|
Chris@909
|
30 class LDAP
|
Chris@909
|
31
|
Chris@909
|
32
|
Chris@909
|
33 # Class Net::LDAP::Filter is used to constrain
|
Chris@909
|
34 # LDAP searches. An object of this class is
|
Chris@909
|
35 # passed to Net::LDAP#search in the parameter :filter.
|
Chris@909
|
36 #
|
Chris@909
|
37 # Net::LDAP::Filter supports the complete set of search filters
|
Chris@909
|
38 # available in LDAP, including conjunction, disjunction and negation
|
Chris@909
|
39 # (AND, OR, and NOT). This class supplants the (infamous) RFC-2254
|
Chris@909
|
40 # standard notation for specifying LDAP search filters.
|
Chris@909
|
41 #
|
Chris@909
|
42 # Here's how to code the familiar "objectclass is present" filter:
|
Chris@909
|
43 # f = Net::LDAP::Filter.pres( "objectclass" )
|
Chris@909
|
44 # The object returned by this code can be passed directly to
|
Chris@909
|
45 # the <tt>:filter</tt> parameter of Net::LDAP#search.
|
Chris@909
|
46 #
|
Chris@909
|
47 # See the individual class and instance methods below for more examples.
|
Chris@909
|
48 #
|
Chris@909
|
49 class Filter
|
Chris@909
|
50
|
Chris@909
|
51 def initialize op, a, b
|
Chris@909
|
52 @op = op
|
Chris@909
|
53 @left = a
|
Chris@909
|
54 @right = b
|
Chris@909
|
55 end
|
Chris@909
|
56
|
Chris@909
|
57 # #eq creates a filter object indicating that the value of
|
Chris@909
|
58 # a paticular attribute must be either <i>present</i> or must
|
Chris@909
|
59 # match a particular string.
|
Chris@909
|
60 #
|
Chris@909
|
61 # To specify that an attribute is "present" means that only
|
Chris@909
|
62 # directory entries which contain a value for the particular
|
Chris@909
|
63 # attribute will be selected by the filter. This is useful
|
Chris@909
|
64 # in case of optional attributes such as <tt>mail.</tt>
|
Chris@909
|
65 # Presence is indicated by giving the value "*" in the second
|
Chris@909
|
66 # parameter to #eq. This example selects only entries that have
|
Chris@909
|
67 # one or more values for <tt>sAMAccountName:</tt>
|
Chris@909
|
68 # f = Net::LDAP::Filter.eq( "sAMAccountName", "*" )
|
Chris@909
|
69 #
|
Chris@909
|
70 # To match a particular range of values, pass a string as the
|
Chris@909
|
71 # second parameter to #eq. The string may contain one or more
|
Chris@909
|
72 # "*" characters as wildcards: these match zero or more occurrences
|
Chris@909
|
73 # of any character. Full regular-expressions are <i>not</i> supported
|
Chris@909
|
74 # due to limitations in the underlying LDAP protocol.
|
Chris@909
|
75 # This example selects any entry with a <tt>mail</tt> value containing
|
Chris@909
|
76 # the substring "anderson":
|
Chris@909
|
77 # f = Net::LDAP::Filter.eq( "mail", "*anderson*" )
|
Chris@909
|
78 #--
|
Chris@909
|
79 # Removed gt and lt. They ain't in the standard!
|
Chris@909
|
80 #
|
Chris@909
|
81 def Filter::eq attribute, value; Filter.new :eq, attribute, value; end
|
Chris@909
|
82 def Filter::ne attribute, value; Filter.new :ne, attribute, value; end
|
Chris@909
|
83 #def Filter::gt attribute, value; Filter.new :gt, attribute, value; end
|
Chris@909
|
84 #def Filter::lt attribute, value; Filter.new :lt, attribute, value; end
|
Chris@909
|
85 def Filter::ge attribute, value; Filter.new :ge, attribute, value; end
|
Chris@909
|
86 def Filter::le attribute, value; Filter.new :le, attribute, value; end
|
Chris@909
|
87
|
Chris@909
|
88 # #pres( attribute ) is a synonym for #eq( attribute, "*" )
|
Chris@909
|
89 #
|
Chris@909
|
90 def Filter::pres attribute; Filter.eq attribute, "*"; end
|
Chris@909
|
91
|
Chris@909
|
92 # operator & ("AND") is used to conjoin two or more filters.
|
Chris@909
|
93 # This expression will select only entries that have an <tt>objectclass</tt>
|
Chris@909
|
94 # attribute AND have a <tt>mail</tt> attribute that begins with "George":
|
Chris@909
|
95 # f = Net::LDAP::Filter.pres( "objectclass" ) & Net::LDAP::Filter.eq( "mail", "George*" )
|
Chris@909
|
96 #
|
Chris@909
|
97 def & filter; Filter.new :and, self, filter; end
|
Chris@909
|
98
|
Chris@909
|
99 # operator | ("OR") is used to disjoin two or more filters.
|
Chris@909
|
100 # This expression will select entries that have either an <tt>objectclass</tt>
|
Chris@909
|
101 # attribute OR a <tt>mail</tt> attribute that begins with "George":
|
Chris@909
|
102 # f = Net::LDAP::Filter.pres( "objectclass" ) | Net::LDAP::Filter.eq( "mail", "George*" )
|
Chris@909
|
103 #
|
Chris@909
|
104 def | filter; Filter.new :or, self, filter; end
|
Chris@909
|
105
|
Chris@909
|
106
|
Chris@909
|
107 #
|
Chris@909
|
108 # operator ~ ("NOT") is used to negate a filter.
|
Chris@909
|
109 # This expression will select only entries that <i>do not</i> have an <tt>objectclass</tt>
|
Chris@909
|
110 # attribute:
|
Chris@909
|
111 # f = ~ Net::LDAP::Filter.pres( "objectclass" )
|
Chris@909
|
112 #
|
Chris@909
|
113 #--
|
Chris@909
|
114 # This operator can't be !, evidently. Try it.
|
Chris@909
|
115 # Removed GT and LT. They're not in the RFC.
|
Chris@909
|
116 def ~@; Filter.new :not, self, nil; end
|
Chris@909
|
117
|
Chris@909
|
118
|
Chris@909
|
119 def to_s
|
Chris@909
|
120 case @op
|
Chris@909
|
121 when :ne
|
Chris@909
|
122 "(!(#{@left}=#{@right}))"
|
Chris@909
|
123 when :eq
|
Chris@909
|
124 "(#{@left}=#{@right})"
|
Chris@909
|
125 #when :gt
|
Chris@909
|
126 # "#{@left}>#{@right}"
|
Chris@909
|
127 #when :lt
|
Chris@909
|
128 # "#{@left}<#{@right}"
|
Chris@909
|
129 when :ge
|
Chris@909
|
130 "#{@left}>=#{@right}"
|
Chris@909
|
131 when :le
|
Chris@909
|
132 "#{@left}<=#{@right}"
|
Chris@909
|
133 when :and
|
Chris@909
|
134 "(&(#{@left})(#{@right}))"
|
Chris@909
|
135 when :or
|
Chris@909
|
136 "(|(#{@left})(#{@right}))"
|
Chris@909
|
137 when :not
|
Chris@909
|
138 "(!(#{@left}))"
|
Chris@909
|
139 else
|
Chris@909
|
140 raise "invalid or unsupported operator in LDAP Filter"
|
Chris@909
|
141 end
|
Chris@909
|
142 end
|
Chris@909
|
143
|
Chris@909
|
144
|
Chris@909
|
145 #--
|
Chris@909
|
146 # to_ber
|
Chris@909
|
147 # Filter ::=
|
Chris@909
|
148 # CHOICE {
|
Chris@909
|
149 # and [0] SET OF Filter,
|
Chris@909
|
150 # or [1] SET OF Filter,
|
Chris@909
|
151 # not [2] Filter,
|
Chris@909
|
152 # equalityMatch [3] AttributeValueAssertion,
|
Chris@909
|
153 # substrings [4] SubstringFilter,
|
Chris@909
|
154 # greaterOrEqual [5] AttributeValueAssertion,
|
Chris@909
|
155 # lessOrEqual [6] AttributeValueAssertion,
|
Chris@909
|
156 # present [7] AttributeType,
|
Chris@909
|
157 # approxMatch [8] AttributeValueAssertion
|
Chris@909
|
158 # }
|
Chris@909
|
159 #
|
Chris@909
|
160 # SubstringFilter
|
Chris@909
|
161 # SEQUENCE {
|
Chris@909
|
162 # type AttributeType,
|
Chris@909
|
163 # SEQUENCE OF CHOICE {
|
Chris@909
|
164 # initial [0] LDAPString,
|
Chris@909
|
165 # any [1] LDAPString,
|
Chris@909
|
166 # final [2] LDAPString
|
Chris@909
|
167 # }
|
Chris@909
|
168 # }
|
Chris@909
|
169 #
|
Chris@909
|
170 # Parsing substrings is a little tricky.
|
Chris@909
|
171 # We use the split method to break a string into substrings
|
Chris@909
|
172 # delimited by the * (star) character. But we also need
|
Chris@909
|
173 # to know whether there is a star at the head and tail
|
Chris@909
|
174 # of the string. A Ruby particularity comes into play here:
|
Chris@909
|
175 # if you split on * and the first character of the string is
|
Chris@909
|
176 # a star, then split will return an array whose first element
|
Chris@909
|
177 # is an _empty_ string. But if the _last_ character of the
|
Chris@909
|
178 # string is star, then split will return an array that does
|
Chris@909
|
179 # _not_ add an empty string at the end. So we have to deal
|
Chris@909
|
180 # with all that specifically.
|
Chris@909
|
181 #
|
Chris@909
|
182 def to_ber
|
Chris@909
|
183 case @op
|
Chris@909
|
184 when :eq
|
Chris@909
|
185 if @right == "*" # present
|
Chris@909
|
186 @left.to_s.to_ber_contextspecific 7
|
Chris@909
|
187 elsif @right =~ /[\*]/ #substring
|
Chris@909
|
188 ary = @right.split( /[\*]+/ )
|
Chris@909
|
189 final_star = @right =~ /[\*]$/
|
Chris@909
|
190 initial_star = ary.first == "" and ary.shift
|
Chris@909
|
191
|
Chris@909
|
192 seq = []
|
Chris@909
|
193 unless initial_star
|
Chris@909
|
194 seq << ary.shift.to_ber_contextspecific(0)
|
Chris@909
|
195 end
|
Chris@909
|
196 n_any_strings = ary.length - (final_star ? 0 : 1)
|
Chris@909
|
197 #p n_any_strings
|
Chris@909
|
198 n_any_strings.times {
|
Chris@909
|
199 seq << ary.shift.to_ber_contextspecific(1)
|
Chris@909
|
200 }
|
Chris@909
|
201 unless final_star
|
Chris@909
|
202 seq << ary.shift.to_ber_contextspecific(2)
|
Chris@909
|
203 end
|
Chris@909
|
204 [@left.to_s.to_ber, seq.to_ber].to_ber_contextspecific 4
|
Chris@909
|
205 else #equality
|
Chris@909
|
206 [@left.to_s.to_ber, @right.to_ber].to_ber_contextspecific 3
|
Chris@909
|
207 end
|
Chris@909
|
208 when :ge
|
Chris@909
|
209 [@left.to_s.to_ber, @right.to_ber].to_ber_contextspecific 5
|
Chris@909
|
210 when :le
|
Chris@909
|
211 [@left.to_s.to_ber, @right.to_ber].to_ber_contextspecific 6
|
Chris@909
|
212 when :and
|
Chris@909
|
213 ary = [@left.coalesce(:and), @right.coalesce(:and)].flatten
|
Chris@909
|
214 ary.map {|a| a.to_ber}.to_ber_contextspecific( 0 )
|
Chris@909
|
215 when :or
|
Chris@909
|
216 ary = [@left.coalesce(:or), @right.coalesce(:or)].flatten
|
Chris@909
|
217 ary.map {|a| a.to_ber}.to_ber_contextspecific( 1 )
|
Chris@909
|
218 when :not
|
Chris@909
|
219 [@left.to_ber].to_ber_contextspecific 2
|
Chris@909
|
220 else
|
Chris@909
|
221 # ERROR, we'll return objectclass=* to keep things from blowing up,
|
Chris@909
|
222 # but that ain't a good answer and we need to kick out an error of some kind.
|
Chris@909
|
223 raise "unimplemented search filter"
|
Chris@909
|
224 end
|
Chris@909
|
225 end
|
Chris@909
|
226
|
Chris@909
|
227 #--
|
Chris@909
|
228 # coalesce
|
Chris@909
|
229 # This is a private helper method for dealing with chains of ANDs and ORs
|
Chris@909
|
230 # that are longer than two. If BOTH of our branches are of the specified
|
Chris@909
|
231 # type of joining operator, then return both of them as an array (calling
|
Chris@909
|
232 # coalesce recursively). If they're not, then return an array consisting
|
Chris@909
|
233 # only of self.
|
Chris@909
|
234 #
|
Chris@909
|
235 def coalesce operator
|
Chris@909
|
236 if @op == operator
|
Chris@909
|
237 [@left.coalesce( operator ), @right.coalesce( operator )]
|
Chris@909
|
238 else
|
Chris@909
|
239 [self]
|
Chris@909
|
240 end
|
Chris@909
|
241 end
|
Chris@909
|
242
|
Chris@909
|
243
|
Chris@909
|
244
|
Chris@909
|
245 #--
|
Chris@909
|
246 # We get a Ruby object which comes from parsing an RFC-1777 "Filter"
|
Chris@909
|
247 # object. Convert it to a Net::LDAP::Filter.
|
Chris@909
|
248 # TODO, we're hardcoding the RFC-1777 BER-encodings of the various
|
Chris@909
|
249 # filter types. Could pull them out into a constant.
|
Chris@909
|
250 #
|
Chris@909
|
251 def Filter::parse_ldap_filter obj
|
Chris@909
|
252 case obj.ber_identifier
|
Chris@909
|
253 when 0x87 # present. context-specific primitive 7.
|
Chris@909
|
254 Filter.eq( obj.to_s, "*" )
|
Chris@909
|
255 when 0xa3 # equalityMatch. context-specific constructed 3.
|
Chris@909
|
256 Filter.eq( obj[0], obj[1] )
|
Chris@909
|
257 else
|
Chris@909
|
258 raise LdapError.new( "unknown ldap search-filter type: #{obj.ber_identifier}" )
|
Chris@909
|
259 end
|
Chris@909
|
260 end
|
Chris@909
|
261
|
Chris@909
|
262
|
Chris@909
|
263 #--
|
Chris@909
|
264 # We got a hash of attribute values.
|
Chris@909
|
265 # Do we match the attributes?
|
Chris@909
|
266 # Return T/F, and call match recursively as necessary.
|
Chris@909
|
267 def match entry
|
Chris@909
|
268 case @op
|
Chris@909
|
269 when :eq
|
Chris@909
|
270 if @right == "*"
|
Chris@909
|
271 l = entry[@left] and l.length > 0
|
Chris@909
|
272 else
|
Chris@909
|
273 l = entry[@left] and l = l.to_a and l.index(@right)
|
Chris@909
|
274 end
|
Chris@909
|
275 else
|
Chris@909
|
276 raise LdapError.new( "unknown filter type in match: #{@op}" )
|
Chris@909
|
277 end
|
Chris@909
|
278 end
|
Chris@909
|
279
|
Chris@909
|
280 # Converts an LDAP filter-string (in the prefix syntax specified in RFC-2254)
|
Chris@909
|
281 # to a Net::LDAP::Filter.
|
Chris@909
|
282 def self.construct ldap_filter_string
|
Chris@909
|
283 FilterParser.new(ldap_filter_string).filter
|
Chris@909
|
284 end
|
Chris@909
|
285
|
Chris@909
|
286 # Synonym for #construct.
|
Chris@909
|
287 # to a Net::LDAP::Filter.
|
Chris@909
|
288 def self.from_rfc2254 ldap_filter_string
|
Chris@909
|
289 construct ldap_filter_string
|
Chris@909
|
290 end
|
Chris@909
|
291
|
Chris@909
|
292 end # class Net::LDAP::Filter
|
Chris@909
|
293
|
Chris@909
|
294
|
Chris@909
|
295
|
Chris@909
|
296 class FilterParser #:nodoc:
|
Chris@909
|
297
|
Chris@909
|
298 attr_reader :filter
|
Chris@909
|
299
|
Chris@909
|
300 def initialize str
|
Chris@909
|
301 require 'strscan'
|
Chris@909
|
302 @filter = parse( StringScanner.new( str )) or raise Net::LDAP::LdapError.new( "invalid filter syntax" )
|
Chris@909
|
303 end
|
Chris@909
|
304
|
Chris@909
|
305 def parse scanner
|
Chris@909
|
306 parse_filter_branch(scanner) or parse_paren_expression(scanner)
|
Chris@909
|
307 end
|
Chris@909
|
308
|
Chris@909
|
309 def parse_paren_expression scanner
|
Chris@909
|
310 if scanner.scan(/\s*\(\s*/)
|
Chris@909
|
311 b = if scanner.scan(/\s*\&\s*/)
|
Chris@909
|
312 a = nil
|
Chris@909
|
313 branches = []
|
Chris@909
|
314 while br = parse_paren_expression(scanner)
|
Chris@909
|
315 branches << br
|
Chris@909
|
316 end
|
Chris@909
|
317 if branches.length >= 2
|
Chris@909
|
318 a = branches.shift
|
Chris@909
|
319 while branches.length > 0
|
Chris@909
|
320 a = a & branches.shift
|
Chris@909
|
321 end
|
Chris@909
|
322 a
|
Chris@909
|
323 end
|
Chris@909
|
324 elsif scanner.scan(/\s*\|\s*/)
|
Chris@909
|
325 # TODO: DRY!
|
Chris@909
|
326 a = nil
|
Chris@909
|
327 branches = []
|
Chris@909
|
328 while br = parse_paren_expression(scanner)
|
Chris@909
|
329 branches << br
|
Chris@909
|
330 end
|
Chris@909
|
331 if branches.length >= 2
|
Chris@909
|
332 a = branches.shift
|
Chris@909
|
333 while branches.length > 0
|
Chris@909
|
334 a = a | branches.shift
|
Chris@909
|
335 end
|
Chris@909
|
336 a
|
Chris@909
|
337 end
|
Chris@909
|
338 elsif scanner.scan(/\s*\!\s*/)
|
Chris@909
|
339 br = parse_paren_expression(scanner)
|
Chris@909
|
340 if br
|
Chris@909
|
341 ~ br
|
Chris@909
|
342 end
|
Chris@909
|
343 else
|
Chris@909
|
344 parse_filter_branch( scanner )
|
Chris@909
|
345 end
|
Chris@909
|
346
|
Chris@909
|
347 if b and scanner.scan( /\s*\)\s*/ )
|
Chris@909
|
348 b
|
Chris@909
|
349 end
|
Chris@909
|
350 end
|
Chris@909
|
351 end
|
Chris@909
|
352
|
Chris@909
|
353 # Added a greatly-augmented filter contributed by Andre Nathan
|
Chris@909
|
354 # for detecting special characters in values. (15Aug06)
|
Chris@909
|
355 def parse_filter_branch scanner
|
Chris@909
|
356 scanner.scan(/\s*/)
|
Chris@909
|
357 if token = scanner.scan( /[\w\-_]+/ )
|
Chris@909
|
358 scanner.scan(/\s*/)
|
Chris@909
|
359 if op = scanner.scan( /\=|\<\=|\<|\>\=|\>|\!\=/ )
|
Chris@909
|
360 scanner.scan(/\s*/)
|
Chris@909
|
361 #if value = scanner.scan( /[\w\*\.]+/ ) (ORG)
|
Chris@909
|
362 if value = scanner.scan( /[\w\*\.\+\-@=#\$%&!]+/ )
|
Chris@909
|
363 case op
|
Chris@909
|
364 when "="
|
Chris@909
|
365 Filter.eq( token, value )
|
Chris@909
|
366 when "!="
|
Chris@909
|
367 Filter.ne( token, value )
|
Chris@909
|
368 when "<"
|
Chris@909
|
369 Filter.lt( token, value )
|
Chris@909
|
370 when "<="
|
Chris@909
|
371 Filter.le( token, value )
|
Chris@909
|
372 when ">"
|
Chris@909
|
373 Filter.gt( token, value )
|
Chris@909
|
374 when ">="
|
Chris@909
|
375 Filter.ge( token, value )
|
Chris@909
|
376 end
|
Chris@909
|
377 end
|
Chris@909
|
378 end
|
Chris@909
|
379 end
|
Chris@909
|
380 end
|
Chris@909
|
381
|
Chris@909
|
382 end # class Net::LDAP::FilterParser
|
Chris@909
|
383
|
Chris@909
|
384 end # class Net::LDAP
|
Chris@909
|
385 end # module Net
|
Chris@909
|
386
|
Chris@909
|
387
|