To check out this repository please hg clone the following URL, or open the URL using EasyMercurial or your preferred Mercurial client.

Statistics Download as Zip
| Branch: | Tag: | Revision:

root / .svn / pristine / bc / bc0c969dde0ae26fdb5eed30992ca79327b8efb8.svn-base @ 1297:0a574315af3e

History | View | Annotate | Download (12.1 KB)

1
# $Id: filter.rb 151 2006-08-15 08:34:53Z blackhedd $
2
#
3
#
4
#----------------------------------------------------------------------------
5
#
6
# Copyright (C) 2006 by Francis Cianfrocca. All Rights Reserved.
7
#
8
# Gmail: garbagecat10
9
#
10
# This program is free software; you can redistribute it and/or modify
11
# it under the terms of the GNU General Public License as published by
12
# the Free Software Foundation; either version 2 of the License, or
13
# (at your option) any later version.
14
#
15
# This program is distributed in the hope that it will be useful,
16
# but WITHOUT ANY WARRANTY; without even the implied warranty of
17
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18
# GNU General Public License for more details.
19
#
20
# You should have received a copy of the GNU General Public License
21
# along with this program; if not, write to the Free Software
22
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
23
#
24
#---------------------------------------------------------------------------
25
#
26
#
27

    
28

    
29
module Net
30
class LDAP
31

    
32

    
33
# Class Net::LDAP::Filter is used to constrain
34
# LDAP searches. An object of this class is
35
# passed to Net::LDAP#search in the parameter :filter.
36
#
37
# Net::LDAP::Filter supports the complete set of search filters
38
# available in LDAP, including conjunction, disjunction and negation
39
# (AND, OR, and NOT). This class supplants the (infamous) RFC-2254
40
# standard notation for specifying LDAP search filters.
41
#
42
# Here's how to code the familiar "objectclass is present" filter:
43
#  f = Net::LDAP::Filter.pres( "objectclass" )
44
# The object returned by this code can be passed directly to
45
# the <tt>:filter</tt> parameter of Net::LDAP#search.
46
#
47
# See the individual class and instance methods below for more examples.
48
#
49
class Filter
50

    
51
  def initialize op, a, b
52
    @op = op
53
    @left = a
54
    @right = b
55
  end
56

    
57
  # #eq creates a filter object indicating that the value of
58
  # a paticular attribute must be either <i>present</i> or must
59
  # match a particular string.
60
  #
61
  # To specify that an attribute is "present" means that only
62
  # directory entries which contain a value for the particular
63
  # attribute will be selected by the filter. This is useful
64
  # in case of optional attributes such as <tt>mail.</tt>
65
  # Presence is indicated by giving the value "*" in the second
66
  # parameter to #eq. This example selects only entries that have
67
  # one or more values for <tt>sAMAccountName:</tt>
68
  #  f = Net::LDAP::Filter.eq( "sAMAccountName", "*" )
69
  #
70
  # To match a particular range of values, pass a string as the
71
  # second parameter to #eq. The string may contain one or more
72
  # "*" characters as wildcards: these match zero or more occurrences
73
  # of any character. Full regular-expressions are <i>not</i> supported
74
  # due to limitations in the underlying LDAP protocol.
75
  # This example selects any entry with a <tt>mail</tt> value containing
76
  # the substring "anderson":
77
  #  f = Net::LDAP::Filter.eq( "mail", "*anderson*" )
78
  #--
79
  # Removed gt and lt. They ain't in the standard!
80
  #
81
  def Filter::eq attribute, value; Filter.new :eq, attribute, value; end
82
  def Filter::ne attribute, value; Filter.new :ne, attribute, value; end
83
  #def Filter::gt attribute, value; Filter.new :gt, attribute, value; end
84
  #def Filter::lt attribute, value; Filter.new :lt, attribute, value; end
85
  def Filter::ge attribute, value; Filter.new :ge, attribute, value; end
86
  def Filter::le attribute, value; Filter.new :le, attribute, value; end
87

    
88
  # #pres( attribute ) is a synonym for #eq( attribute, "*" )
89
  #
90
  def Filter::pres attribute; Filter.eq attribute, "*"; end
91

    
92
  # operator & ("AND") is used to conjoin two or more filters.
93
  # This expression will select only entries that have an <tt>objectclass</tt>
94
  # attribute AND have a <tt>mail</tt> attribute that begins with "George":
95
  #  f = Net::LDAP::Filter.pres( "objectclass" ) & Net::LDAP::Filter.eq( "mail", "George*" )
96
  #
97
  def & filter; Filter.new :and, self, filter; end
98

    
99
  # operator | ("OR") is used to disjoin two or more filters.
100
  # This expression will select entries that have either an <tt>objectclass</tt>
101
  # attribute OR a <tt>mail</tt> attribute that begins with "George":
102
  #  f = Net::LDAP::Filter.pres( "objectclass" ) | Net::LDAP::Filter.eq( "mail", "George*" )
103
  #
104
  def | filter; Filter.new :or, self, filter; end
105

    
106

    
107
  #
108
  # operator ~ ("NOT") is used to negate a filter.
109
  # This expression will select only entries that <i>do not</i> have an <tt>objectclass</tt>
110
  # attribute:
111
  #  f = ~ Net::LDAP::Filter.pres( "objectclass" )
112
  #
113
  #--
114
  # This operator can't be !, evidently. Try it.
115
  # Removed GT and LT. They're not in the RFC.
116
  def ~@; Filter.new :not, self, nil; end
117

    
118

    
119
  def to_s
120
    case @op
121
    when :ne
122
      "(!(#{@left}=#{@right}))"
123
    when :eq
124
      "(#{@left}=#{@right})"
125
    #when :gt
126
     # "#{@left}>#{@right}"
127
    #when :lt
128
     # "#{@left}<#{@right}"
129
    when :ge
130
      "#{@left}>=#{@right}"
131
    when :le
132
      "#{@left}<=#{@right}"
133
    when :and
134
      "(&(#{@left})(#{@right}))"
135
    when :or
136
      "(|(#{@left})(#{@right}))"
137
    when :not
138
      "(!(#{@left}))"
139
    else
140
      raise "invalid or unsupported operator in LDAP Filter"
141
    end
142
  end
143

    
144

    
145
  #--
146
  # to_ber
147
  # Filter ::=
148
  #     CHOICE {
149
  #         and            [0] SET OF Filter,
150
  #         or             [1] SET OF Filter,
151
  #         not            [2] Filter,
152
  #         equalityMatch  [3] AttributeValueAssertion,
153
  #         substrings     [4] SubstringFilter,
154
  #         greaterOrEqual [5] AttributeValueAssertion,
155
  #         lessOrEqual    [6] AttributeValueAssertion,
156
  #         present        [7] AttributeType,
157
  #         approxMatch    [8] AttributeValueAssertion
158
  #     }
159
  #
160
  # SubstringFilter
161
  #     SEQUENCE {
162
  #         type               AttributeType,
163
  #         SEQUENCE OF CHOICE {
164
  #             initial        [0] LDAPString,
165
  #             any            [1] LDAPString,
166
  #             final          [2] LDAPString
167
  #         }
168
  #     }
169
  #
170
  # Parsing substrings is a little tricky.
171
  # We use the split method to break a string into substrings
172
  # delimited by the * (star) character. But we also need
173
  # to know whether there is a star at the head and tail
174
  # of the string. A Ruby particularity comes into play here:
175
  # if you split on * and the first character of the string is
176
  # a star, then split will return an array whose first element
177
  # is an _empty_ string. But if the _last_ character of the
178
  # string is star, then split will return an array that does
179
  # _not_ add an empty string at the end. So we have to deal
180
  # with all that specifically.
181
  #
182
  def to_ber
183
    case @op
184
    when :eq
185
      if @right == "*"          # present
186
        @left.to_s.to_ber_contextspecific 7
187
      elsif @right =~ /[\*]/    #substring
188
        ary = @right.split( /[\*]+/ )
189
        final_star = @right =~ /[\*]$/
190
        initial_star = ary.first == "" and ary.shift
191

    
192
        seq = []
193
        unless initial_star
194
          seq << ary.shift.to_ber_contextspecific(0)
195
        end
196
        n_any_strings = ary.length - (final_star ? 0 : 1)
197
        #p n_any_strings
198
        n_any_strings.times {
199
          seq << ary.shift.to_ber_contextspecific(1)
200
        }
201
        unless final_star
202
          seq << ary.shift.to_ber_contextspecific(2)
203
        end
204
        [@left.to_s.to_ber, seq.to_ber].to_ber_contextspecific 4
205
      else                      #equality
206
        [@left.to_s.to_ber, @right.to_ber].to_ber_contextspecific 3
207
      end
208
    when :ge
209
      [@left.to_s.to_ber, @right.to_ber].to_ber_contextspecific 5
210
    when :le
211
      [@left.to_s.to_ber, @right.to_ber].to_ber_contextspecific 6
212
    when :and
213
      ary = [@left.coalesce(:and), @right.coalesce(:and)].flatten
214
      ary.map {|a| a.to_ber}.to_ber_contextspecific( 0 )
215
    when :or
216
      ary = [@left.coalesce(:or), @right.coalesce(:or)].flatten
217
      ary.map {|a| a.to_ber}.to_ber_contextspecific( 1 )
218
    when :not
219
        [@left.to_ber].to_ber_contextspecific 2
220
    else
221
      # ERROR, we'll return objectclass=* to keep things from blowing up,
222
      # but that ain't a good answer and we need to kick out an error of some kind.
223
      raise "unimplemented search filter"
224
    end
225
  end
226

    
227
  #--
228
  # coalesce
229
  # This is a private helper method for dealing with chains of ANDs and ORs
230
  # that are longer than two. If BOTH of our branches are of the specified
231
  # type of joining operator, then return both of them as an array (calling
232
  # coalesce recursively). If they're not, then return an array consisting
233
  # only of self.
234
  #
235
  def coalesce operator
236
    if @op == operator
237
      [@left.coalesce( operator ), @right.coalesce( operator )]
238
    else
239
      [self]
240
    end
241
  end
242

    
243

    
244

    
245
  #--
246
  # We get a Ruby object which comes from parsing an RFC-1777 "Filter"
247
  # object. Convert it to a Net::LDAP::Filter.
248
  # TODO, we're hardcoding the RFC-1777 BER-encodings of the various
249
  # filter types. Could pull them out into a constant.
250
  #
251
  def Filter::parse_ldap_filter obj
252
    case obj.ber_identifier
253
    when 0x87         # present. context-specific primitive 7.
254
      Filter.eq( obj.to_s, "*" )
255
    when 0xa3         # equalityMatch. context-specific constructed 3.
256
      Filter.eq( obj[0], obj[1] )
257
    else
258
      raise LdapError.new( "unknown ldap search-filter type: #{obj.ber_identifier}" )
259
    end
260
  end
261

    
262

    
263
  #--
264
  # We got a hash of attribute values.
265
  # Do we match the attributes?
266
  # Return T/F, and call match recursively as necessary.
267
  def match entry
268
    case @op
269
    when :eq
270
      if @right == "*"
271
        l = entry[@left] and l.length > 0
272
      else
273
        l = entry[@left] and l = l.to_a and l.index(@right)
274
      end
275
    else
276
      raise LdapError.new( "unknown filter type in match: #{@op}" )
277
    end
278
  end
279

    
280
  # Converts an LDAP filter-string (in the prefix syntax specified in RFC-2254)
281
  # to a Net::LDAP::Filter.
282
  def self.construct ldap_filter_string
283
    FilterParser.new(ldap_filter_string).filter
284
  end
285

    
286
  # Synonym for #construct.
287
  # to a Net::LDAP::Filter.
288
  def self.from_rfc2254 ldap_filter_string
289
    construct ldap_filter_string
290
  end
291

    
292
end # class Net::LDAP::Filter
293

    
294

    
295

    
296
class FilterParser #:nodoc:
297

    
298
  attr_reader :filter
299

    
300
  def initialize str
301
    require 'strscan'
302
    @filter = parse( StringScanner.new( str )) or raise Net::LDAP::LdapError.new( "invalid filter syntax" )
303
  end
304

    
305
  def parse scanner
306
    parse_filter_branch(scanner) or parse_paren_expression(scanner)
307
  end
308

    
309
  def parse_paren_expression scanner
310
    if scanner.scan(/\s*\(\s*/)
311
      b = if scanner.scan(/\s*\&\s*/)
312
        a = nil
313
        branches = []
314
        while br = parse_paren_expression(scanner)
315
          branches << br
316
        end
317
        if branches.length >= 2
318
          a = branches.shift
319
          while branches.length > 0
320
            a = a & branches.shift
321
          end
322
          a
323
        end
324
      elsif scanner.scan(/\s*\|\s*/)
325
        # TODO: DRY!
326
        a = nil
327
        branches = []
328
        while br = parse_paren_expression(scanner)
329
          branches << br
330
        end
331
        if branches.length >= 2
332
          a = branches.shift
333
          while branches.length > 0
334
            a = a | branches.shift
335
          end
336
          a
337
        end
338
      elsif scanner.scan(/\s*\!\s*/)
339
        br = parse_paren_expression(scanner)
340
        if br
341
          ~ br
342
        end
343
      else
344
        parse_filter_branch( scanner )
345
      end
346

    
347
      if b and scanner.scan( /\s*\)\s*/ )
348
        b
349
      end
350
    end
351
  end
352

    
353
  # Added a greatly-augmented filter contributed by Andre Nathan
354
  # for detecting special characters in values. (15Aug06)
355
  def parse_filter_branch scanner
356
    scanner.scan(/\s*/)
357
    if token = scanner.scan( /[\w\-_]+/ )
358
      scanner.scan(/\s*/)
359
      if op = scanner.scan( /\=|\<\=|\<|\>\=|\>|\!\=/ )
360
        scanner.scan(/\s*/)
361
        #if value = scanner.scan( /[\w\*\.]+/ ) (ORG)
362
        if value = scanner.scan( /[\w\*\.\+\-@=#\$%&!]+/ )
363
          case op
364
          when "="
365
            Filter.eq( token, value )
366
          when "!="
367
            Filter.ne( token, value )
368
          when "<"
369
            Filter.lt( token, value )
370
          when "<="
371
            Filter.le( token, value )
372
          when ">"
373
            Filter.gt( token, value )
374
          when ">="
375
            Filter.ge( token, value )
376
          end
377
        end
378
      end
379
    end
380
  end
381

    
382
end # class Net::LDAP::FilterParser
383

    
384
end # class Net::LDAP
385
end # module Net
386

    
387