socialcast-net-ldap 0.1.5
Sign up to get free protection for your applications and to get access to all the features.
- data/.gemtest +0 -0
- data/COPYING +272 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +29 -0
- data/Hacking.rdoc +16 -0
- data/History.txt +137 -0
- data/LICENSE +56 -0
- data/Manifest.txt +45 -0
- data/README.txt +70 -0
- data/Rakefile +124 -0
- data/lib/net-ldap.rb +1 -0
- data/lib/net/ber.rb +341 -0
- data/lib/net/ber/ber_parser.rb +168 -0
- data/lib/net/ber/core_ext.rb +72 -0
- data/lib/net/ber/core_ext/array.rb +79 -0
- data/lib/net/ber/core_ext/bignum.rb +19 -0
- data/lib/net/ber/core_ext/false_class.rb +7 -0
- data/lib/net/ber/core_ext/fixnum.rb +63 -0
- data/lib/net/ber/core_ext/string.rb +57 -0
- data/lib/net/ber/core_ext/true_class.rb +9 -0
- data/lib/net/ldap.rb +1539 -0
- data/lib/net/ldap/dataset.rb +174 -0
- data/lib/net/ldap/entry.rb +208 -0
- data/lib/net/ldap/filter.rb +781 -0
- data/lib/net/ldap/password.rb +52 -0
- data/lib/net/ldap/pdu.rb +279 -0
- data/lib/net/ldif.rb +34 -0
- data/lib/net/snmp.rb +295 -0
- data/spec/integration/ssl_ber_spec.rb +33 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +5 -0
- data/spec/unit/ber/ber_spec.rb +109 -0
- data/spec/unit/ber/core_ext/string_spec.rb +51 -0
- data/spec/unit/ldap/entry_spec.rb +51 -0
- data/spec/unit/ldap/filter_spec.rb +83 -0
- data/spec/unit/ldap_spec.rb +48 -0
- data/test/common.rb +3 -0
- data/test/test_entry.rb +59 -0
- data/test/test_filter.rb +115 -0
- data/test/test_ldif.rb +68 -0
- data/test/test_password.rb +17 -0
- data/test/test_rename.rb +79 -0
- data/test/test_snmp.rb +114 -0
- data/test/testdata.ldif +101 -0
- data/testserver/ldapserver.rb +210 -0
- data/testserver/testdata.ldif +101 -0
- metadata +178 -0
@@ -0,0 +1,174 @@
|
|
1
|
+
#----------------------------------------------------------------------------
|
2
|
+
#
|
3
|
+
# Copyright (C) 2006 by Francis Cianfrocca. All Rights Reserved.
|
4
|
+
#
|
5
|
+
# Gmail: garbagecat10
|
6
|
+
#
|
7
|
+
# This program is free software; you can redistribute it and/or modify
|
8
|
+
# it under the terms of the GNU General Public License as published by
|
9
|
+
# the Free Software Foundation; either version 2 of the License, or
|
10
|
+
# (at your option) any later version.
|
11
|
+
#
|
12
|
+
# This program is distributed in the hope that it will be useful,
|
13
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
14
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
15
|
+
# GNU General Public License for more details.
|
16
|
+
#
|
17
|
+
# You should have received a copy of the GNU General Public License
|
18
|
+
# along with this program; if not, write to the Free Software
|
19
|
+
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
20
|
+
#
|
21
|
+
#---------------------------------------------------------------------------
|
22
|
+
|
23
|
+
##
|
24
|
+
# An LDAP Dataset. Used primarily as an intermediate format for converting
|
25
|
+
# to and from LDIF strings and Net::LDAP::Entry objects.
|
26
|
+
class Net::LDAP::Dataset < Hash
|
27
|
+
##
|
28
|
+
# Dataset object comments.
|
29
|
+
attr_reader :comments
|
30
|
+
|
31
|
+
class << self
|
32
|
+
class ChompedIO #:nodoc:
|
33
|
+
def initialize(io)
|
34
|
+
@io = io
|
35
|
+
end
|
36
|
+
def gets
|
37
|
+
s = @io.gets
|
38
|
+
s.chomp if s
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
##
|
43
|
+
# Reads an object that returns data line-wise (using #gets) and parses
|
44
|
+
# LDIF data into a Dataset object.
|
45
|
+
def read_ldif(io) #:yields: entry-type, value Used mostly for debugging.
|
46
|
+
ds = Net::LDAP::Dataset.new
|
47
|
+
io = ChompedIO.new(io)
|
48
|
+
|
49
|
+
line = io.gets
|
50
|
+
dn = nil
|
51
|
+
|
52
|
+
while line
|
53
|
+
new_line = io.gets
|
54
|
+
|
55
|
+
if new_line =~ /^[\s]+/
|
56
|
+
line << " " << $'
|
57
|
+
else
|
58
|
+
nextline = new_line
|
59
|
+
|
60
|
+
if line =~ /^#/
|
61
|
+
ds.comments << line
|
62
|
+
yield :comment, line if block_given?
|
63
|
+
elsif line =~ /^dn:[\s]*/i
|
64
|
+
dn = $'
|
65
|
+
ds[dn] = Hash.new { |k,v| k[v] = [] }
|
66
|
+
yield :dn, dn if block_given?
|
67
|
+
elsif line.empty?
|
68
|
+
dn = nil
|
69
|
+
yield :end, nil if block_given?
|
70
|
+
elsif line =~ /^([^:]+):([\:]?)[\s]*/
|
71
|
+
# $1 is the attribute name
|
72
|
+
# $2 is a colon iff the attr-value is base-64 encoded
|
73
|
+
# $' is the attr-value
|
74
|
+
# Avoid the Base64 class because not all Ruby versions have it.
|
75
|
+
attrvalue = ($2 == ":") ? $'.unpack('m').shift : $'
|
76
|
+
ds[dn][$1.downcase.to_sym] << attrvalue
|
77
|
+
yield :attr, [$1.downcase.to_sym, attrvalue] if block_given?
|
78
|
+
end
|
79
|
+
|
80
|
+
line = nextline
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
ds
|
85
|
+
end
|
86
|
+
|
87
|
+
##
|
88
|
+
# Creates a Dataset object from an Entry object. Used mostly to assist
|
89
|
+
# with the conversion of
|
90
|
+
def from_entry(entry)
|
91
|
+
dataset = Net::LDAP::Dataset.new
|
92
|
+
hash = { }
|
93
|
+
entry.each_attribute do |attribute, value|
|
94
|
+
next if attribute == :dn
|
95
|
+
hash[attribute] = value
|
96
|
+
end
|
97
|
+
dataset[entry.dn] = hash
|
98
|
+
dataset
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def initialize(*args, &block) #:nodoc:
|
103
|
+
super
|
104
|
+
@comments = []
|
105
|
+
end
|
106
|
+
|
107
|
+
##
|
108
|
+
# Outputs an LDAP Dataset as an array of strings representing LDIF
|
109
|
+
# entries.
|
110
|
+
def to_ldif
|
111
|
+
ary = []
|
112
|
+
ary += @comments unless @comments.empty?
|
113
|
+
keys.sort.each do |dn|
|
114
|
+
ary << "dn: #{dn}"
|
115
|
+
|
116
|
+
attributes = self[dn].keys.map { |attr| attr.to_s }.sort
|
117
|
+
attributes.each do |attr|
|
118
|
+
self[dn][attr.to_sym].each do |value|
|
119
|
+
if attr == "userpassword" or value_is_binary?(value)
|
120
|
+
value = [value].pack("m").chomp.gsub(/\n/m, "\n ")
|
121
|
+
ary << "#{attr}:: #{value}"
|
122
|
+
else
|
123
|
+
ary << "#{attr}: #{value}"
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
ary << ""
|
129
|
+
end
|
130
|
+
block_given? and ary.each { |line| yield line}
|
131
|
+
|
132
|
+
ary
|
133
|
+
end
|
134
|
+
|
135
|
+
##
|
136
|
+
# Outputs an LDAP Dataset as an LDIF string.
|
137
|
+
def to_ldif_string
|
138
|
+
to_ldif.join("\n")
|
139
|
+
end
|
140
|
+
|
141
|
+
##
|
142
|
+
# Convert the parsed LDIF objects to Net::LDAP::Entry objects.
|
143
|
+
def to_entries
|
144
|
+
ary = []
|
145
|
+
keys.each do |dn|
|
146
|
+
entry = Net::LDAP::Entry.new(dn)
|
147
|
+
self[dn].each do |attr, value|
|
148
|
+
entry[attr] = value
|
149
|
+
end
|
150
|
+
ary << entry
|
151
|
+
end
|
152
|
+
ary
|
153
|
+
end
|
154
|
+
|
155
|
+
# This is an internal convenience method to determine if a value requires
|
156
|
+
# base64-encoding before conversion to LDIF output. The standard approach
|
157
|
+
# in most LDAP tools is to check whether the value is a password, or if
|
158
|
+
# the first or last bytes are non-printable. Microsoft Active Directory,
|
159
|
+
# on the other hand, sometimes sends values that are binary in the middle.
|
160
|
+
#
|
161
|
+
# In the worst cases, this could be a nasty performance killer, which is
|
162
|
+
# why we handle the simplest cases first. Ideally, we would also test the
|
163
|
+
# first/last byte, but it's a bit harder to do this in a way that's
|
164
|
+
# compatible with both 1.8.6 and 1.8.7.
|
165
|
+
def value_is_binary?(value)
|
166
|
+
value = value.to_s
|
167
|
+
return true if value[0] == ?: or value[0] == ?<
|
168
|
+
value.each_byte { |byte| return true if (byte < 32) || (byte > 126) }
|
169
|
+
false
|
170
|
+
end
|
171
|
+
private :value_is_binary?
|
172
|
+
end
|
173
|
+
|
174
|
+
require 'net/ldap/entry' unless defined? Net::LDAP::Entry
|
@@ -0,0 +1,208 @@
|
|
1
|
+
# LDAP Entry (search-result) support classes
|
2
|
+
#
|
3
|
+
#----------------------------------------------------------------------------
|
4
|
+
#
|
5
|
+
# Copyright (C) 2006 by Francis Cianfrocca. All Rights Reserved.
|
6
|
+
#
|
7
|
+
# Gmail: garbagecat10
|
8
|
+
#
|
9
|
+
# This program is free software; you can redistribute it and/or modify
|
10
|
+
# it under the terms of the GNU General Public License as published by
|
11
|
+
# the Free Software Foundation; either version 2 of the License, or
|
12
|
+
# (at your option) any later version.
|
13
|
+
#
|
14
|
+
# This program is distributed in the hope that it will be useful,
|
15
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
16
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
17
|
+
# GNU General Public License for more details.
|
18
|
+
#
|
19
|
+
# You should have received a copy of the GNU General Public License
|
20
|
+
# along with this program; if not, write to the Free Software
|
21
|
+
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
22
|
+
#
|
23
|
+
#---------------------------------------------------------------------------
|
24
|
+
|
25
|
+
##
|
26
|
+
# Objects of this class represent individual entries in an LDAP directory.
|
27
|
+
# User code generally does not instantiate this class. Net::LDAP#search
|
28
|
+
# provides objects of this class to user code, either as block parameters or
|
29
|
+
# as return values.
|
30
|
+
#
|
31
|
+
# In LDAP-land, an "entry" is a collection of attributes that are uniquely
|
32
|
+
# and globally identified by a DN ("Distinguished Name"). Attributes are
|
33
|
+
# identified by short, descriptive words or phrases. Although a directory is
|
34
|
+
# free to implement any attribute name, most of them follow rigorous
|
35
|
+
# standards so that the range of commonly-encountered attribute names is not
|
36
|
+
# large.
|
37
|
+
#
|
38
|
+
# An attribute name is case-insensitive. Most directories also restrict the
|
39
|
+
# range of characters allowed in attribute names. To simplify handling
|
40
|
+
# attribute names, Net::LDAP::Entry internally converts them to a standard
|
41
|
+
# format. Therefore, the methods which take attribute names can take Strings
|
42
|
+
# or Symbols, and work correctly regardless of case or capitalization.
|
43
|
+
#
|
44
|
+
# An attribute consists of zero or more data items called <i>values.</i> An
|
45
|
+
# entry is the combination of a unique DN, a set of attribute names, and a
|
46
|
+
# (possibly-empty) array of values for each attribute.
|
47
|
+
#
|
48
|
+
# Class Net::LDAP::Entry provides convenience methods for dealing with LDAP
|
49
|
+
# entries. In addition to the methods documented below, you may access
|
50
|
+
# individual attributes of an entry simply by giving the attribute name as
|
51
|
+
# the name of a method call. For example:
|
52
|
+
#
|
53
|
+
# ldap.search( ... ) do |entry|
|
54
|
+
# puts "Common name: #{entry.cn}"
|
55
|
+
# puts "Email addresses:"
|
56
|
+
# entry.mail.each {|ma| puts ma}
|
57
|
+
# end
|
58
|
+
#
|
59
|
+
# If you use this technique to access an attribute that is not present in a
|
60
|
+
# particular Entry object, a NoMethodError exception will be raised.
|
61
|
+
#
|
62
|
+
#--
|
63
|
+
# Ugly problem to fix someday: We key off the internal hash with a canonical
|
64
|
+
# form of the attribute name: convert to a string, downcase, then take the
|
65
|
+
# symbol. Unfortunately we do this in at least three places. Should do it in
|
66
|
+
# ONE place.
|
67
|
+
class Net::LDAP::Entry
|
68
|
+
##
|
69
|
+
# This constructor is not generally called by user code.
|
70
|
+
def initialize(dn = nil) #:nodoc:
|
71
|
+
@myhash = {}
|
72
|
+
@myhash[:dn] = [dn]
|
73
|
+
end
|
74
|
+
|
75
|
+
##
|
76
|
+
# Use the LDIF format for Marshal serialization.
|
77
|
+
def _dump(depth) #:nodoc:
|
78
|
+
to_ldif
|
79
|
+
end
|
80
|
+
|
81
|
+
##
|
82
|
+
# Use the LDIF format for Marshal serialization.
|
83
|
+
def self._load(entry) #:nodoc:
|
84
|
+
from_single_ldif_string(entry)
|
85
|
+
end
|
86
|
+
|
87
|
+
class << self
|
88
|
+
##
|
89
|
+
# Converts a single LDIF entry string into an Entry object. Useful for
|
90
|
+
# Marshal serialization. If a string with multiple LDIF entries is
|
91
|
+
# provided, an exception will be raised.
|
92
|
+
def from_single_ldif_string(ldif)
|
93
|
+
ds = Net::LDAP::Dataset.read_ldif(::StringIO.new(ldif))
|
94
|
+
|
95
|
+
return nil if ds.empty?
|
96
|
+
|
97
|
+
raise Net::LDAP::LdapError, "Too many LDIF entries" unless ds.size == 1
|
98
|
+
|
99
|
+
entry = ds.to_entries.first
|
100
|
+
|
101
|
+
return nil if entry.dn.nil?
|
102
|
+
entry
|
103
|
+
end
|
104
|
+
|
105
|
+
##
|
106
|
+
# Canonicalizes an LDAP attribute name as a \Symbol. The name is
|
107
|
+
# lowercased and, if present, a trailing equals sign is removed.
|
108
|
+
def attribute_name(name)
|
109
|
+
name = name.to_s.downcase
|
110
|
+
name = name[0..-2] if name[-1] == ?=
|
111
|
+
name.to_sym
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
##
|
116
|
+
# Sets or replaces the array of values for the provided attribute. The
|
117
|
+
# attribute name is canonicalized prior to assignment.
|
118
|
+
#
|
119
|
+
# When an attribute is set using this, that attribute is now made
|
120
|
+
# accessible through methods as well.
|
121
|
+
#
|
122
|
+
# entry = Net::LDAP::Entry.new("dc=com")
|
123
|
+
# entry.foo # => NoMethodError
|
124
|
+
# entry["foo"] = 12345 # => [12345]
|
125
|
+
# entry.foo # => [12345]
|
126
|
+
def []=(name, value)
|
127
|
+
@myhash[self.class.attribute_name(name)] = Kernel::Array(value)
|
128
|
+
end
|
129
|
+
|
130
|
+
##
|
131
|
+
# Reads the array of values for the provided attribute. The attribute name
|
132
|
+
# is canonicalized prior to reading. Returns an empty array if the
|
133
|
+
# attribute does not exist.
|
134
|
+
def [](name)
|
135
|
+
name = self.class.attribute_name(name)
|
136
|
+
@myhash[name] || []
|
137
|
+
end
|
138
|
+
|
139
|
+
##
|
140
|
+
# Returns the first distinguished name (dn) of the Entry as a \String.
|
141
|
+
def dn
|
142
|
+
self[:dn].first.to_s
|
143
|
+
end
|
144
|
+
|
145
|
+
##
|
146
|
+
# Returns an array of the attribute names present in the Entry.
|
147
|
+
def attribute_names
|
148
|
+
@myhash.keys
|
149
|
+
end
|
150
|
+
|
151
|
+
##
|
152
|
+
# Accesses each of the attributes present in the Entry.
|
153
|
+
#
|
154
|
+
# Calls a user-supplied block with each attribute in turn, passing two
|
155
|
+
# arguments to the block: a Symbol giving the name of the attribute, and a
|
156
|
+
# (possibly empty) \Array of data values.
|
157
|
+
def each # :yields: attribute-name, data-values-array
|
158
|
+
if block_given?
|
159
|
+
attribute_names.each {|a|
|
160
|
+
attr_name,values = a,self[a]
|
161
|
+
yield attr_name, values
|
162
|
+
}
|
163
|
+
end
|
164
|
+
end
|
165
|
+
alias_method :each_attribute, :each
|
166
|
+
|
167
|
+
##
|
168
|
+
# Converts the Entry to an LDIF-formatted String
|
169
|
+
def to_ldif
|
170
|
+
Net::LDAP::Dataset.from_entry(self).to_ldif_string
|
171
|
+
end
|
172
|
+
|
173
|
+
def respond_to?(sym) #:nodoc:
|
174
|
+
return true if valid_attribute?(self.class.attribute_name(sym))
|
175
|
+
return super
|
176
|
+
end
|
177
|
+
|
178
|
+
def method_missing(sym, *args, &block) #:nodoc:
|
179
|
+
name = self.class.attribute_name(sym)
|
180
|
+
|
181
|
+
if valid_attribute?(name )
|
182
|
+
if setter?(sym) && args.size == 1
|
183
|
+
value = args.first
|
184
|
+
value = Array(value)
|
185
|
+
self[name]= value
|
186
|
+
return value
|
187
|
+
elsif args.empty?
|
188
|
+
return self[name]
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
super
|
193
|
+
end
|
194
|
+
|
195
|
+
# Given a valid attribute symbol, returns true.
|
196
|
+
def valid_attribute?(attr_name)
|
197
|
+
attribute_names.include?(attr_name)
|
198
|
+
end
|
199
|
+
private :valid_attribute?
|
200
|
+
|
201
|
+
# Returns true if the symbol ends with an equal sign.
|
202
|
+
def setter?(sym)
|
203
|
+
sym.to_s[-1] == ?=
|
204
|
+
end
|
205
|
+
private :setter?
|
206
|
+
end # class Entry
|
207
|
+
|
208
|
+
require 'net/ldap/dataset' unless defined? Net::LDAP::Dataset
|
@@ -0,0 +1,781 @@
|
|
1
|
+
# Encoding: UTF-8
|
2
|
+
# Copyright (C) 2006 by Francis Cianfrocca and other contributors. All
|
3
|
+
# Rights Reserved.
|
4
|
+
#
|
5
|
+
# Gmail: garbagecat10
|
6
|
+
#
|
7
|
+
# This program is free software; you can redistribute it and/or modify it
|
8
|
+
# under the terms of the GNU General Public License as published by the Free
|
9
|
+
# Software Foundation; either version 2 of the License, or (at your option)
|
10
|
+
# any later version.
|
11
|
+
#
|
12
|
+
# This program is distributed in the hope that it will be useful, but
|
13
|
+
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
14
|
+
# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
15
|
+
# for more details.
|
16
|
+
#
|
17
|
+
# You should have received a copy of the GNU General Public License along
|
18
|
+
# with this program; if not, write to:
|
19
|
+
# Free Software Foundation, Inc.
|
20
|
+
# 51 Franklin St, Fifth Floor
|
21
|
+
# Boston, MA 02110-1301
|
22
|
+
# USA
|
23
|
+
|
24
|
+
##
|
25
|
+
# Class Net::LDAP::Filter is used to constrain LDAP searches. An object of
|
26
|
+
# this class is passed to Net::LDAP#search in the parameter :filter.
|
27
|
+
#
|
28
|
+
# Net::LDAP::Filter supports the complete set of search filters available in
|
29
|
+
# LDAP, including conjunction, disjunction and negation (AND, OR, and NOT).
|
30
|
+
# This class supplants the (infamous) RFC 2254 standard notation for
|
31
|
+
# specifying LDAP search filters.
|
32
|
+
#--
|
33
|
+
# NOTE: This wording needs to change as we will be supporting LDAPv3 search
|
34
|
+
# filter strings (RFC 4515).
|
35
|
+
#++
|
36
|
+
#
|
37
|
+
# Here's how to code the familiar "objectclass is present" filter:
|
38
|
+
# f = Net::LDAP::Filter.present("objectclass")
|
39
|
+
#
|
40
|
+
# The object returned by this code can be passed directly to the
|
41
|
+
# <tt>:filter</tt> parameter of Net::LDAP#search.
|
42
|
+
#
|
43
|
+
# See the individual class and instance methods below for more examples.
|
44
|
+
class Net::LDAP::Filter
|
45
|
+
##
|
46
|
+
# Known filter types.
|
47
|
+
FilterTypes = [ :ne, :eq, :ge, :le, :and, :or, :not, :ex ]
|
48
|
+
|
49
|
+
def initialize(op, left, right) #:nodoc:
|
50
|
+
unless FilterTypes.include?(op)
|
51
|
+
raise Net::LDAP::LdapError, "Invalid or unsupported operator #{op.inspect} in LDAP Filter."
|
52
|
+
end
|
53
|
+
@op = op
|
54
|
+
@left = left
|
55
|
+
@right = right
|
56
|
+
end
|
57
|
+
|
58
|
+
class << self
|
59
|
+
# We don't want filters created except using our custom constructors.
|
60
|
+
private :new
|
61
|
+
|
62
|
+
##
|
63
|
+
# Creates a Filter object indicating that the value of a particular
|
64
|
+
# attribute must either be present or match a particular string.
|
65
|
+
#
|
66
|
+
# Specifying that an attribute is 'present' means only directory entries
|
67
|
+
# which contain a value for the particular attribute will be selected by
|
68
|
+
# the filter. This is useful in case of optional attributes such as
|
69
|
+
# <tt>mail.</tt> Presence is indicated by giving the value "*" in the
|
70
|
+
# second parameter to #eq. This example selects only entries that have
|
71
|
+
# one or more values for <tt>sAMAccountName:</tt>
|
72
|
+
#
|
73
|
+
# f = Net::LDAP::Filter.eq("sAMAccountName", "*")
|
74
|
+
#
|
75
|
+
# To match a particular range of values, pass a string as the second
|
76
|
+
# parameter to #eq. The string may contain one or more "*" characters as
|
77
|
+
# wildcards: these match zero or more occurrences of any character. Full
|
78
|
+
# regular-expressions are <i>not</i> supported due to limitations in the
|
79
|
+
# underlying LDAP protocol. This example selects any entry with a
|
80
|
+
# <tt>mail</tt> value containing the substring "anderson":
|
81
|
+
#
|
82
|
+
# f = Net::LDAP::Filter.eq("mail", "*anderson*")
|
83
|
+
#
|
84
|
+
# This filter does not perform any escaping
|
85
|
+
def eq(attribute, value)
|
86
|
+
new(:eq, attribute, value)
|
87
|
+
end
|
88
|
+
|
89
|
+
##
|
90
|
+
# Creates a Filter object indicating extensible comparison. This Filter
|
91
|
+
# object is currently considered EXPERIMENTAL.
|
92
|
+
#
|
93
|
+
# sample_attributes = ['cn:fr', 'cn:fr.eq',
|
94
|
+
# 'cn:1.3.6.1.4.1.42.2.27.9.4.49.1.3', 'cn:dn:fr', 'cn:dn:fr.eq']
|
95
|
+
# attr = sample_attributes.first # Pick an extensible attribute
|
96
|
+
# value = 'roberts'
|
97
|
+
#
|
98
|
+
# filter = "#{attr}:=#{value}" # Basic String Filter
|
99
|
+
# filter = Net::LDAP::Filter.ex(attr, value) # Net::LDAP::Filter
|
100
|
+
#
|
101
|
+
# # Perform a search with the Extensible Match Filter
|
102
|
+
# Net::LDAP.search(:filter => filter)
|
103
|
+
#--
|
104
|
+
# The LDIF required to support the above examples on the OpenDS LDAP
|
105
|
+
# server:
|
106
|
+
#
|
107
|
+
# version: 1
|
108
|
+
#
|
109
|
+
# dn: dc=example,dc=com
|
110
|
+
# objectClass: domain
|
111
|
+
# objectClass: top
|
112
|
+
# dc: example
|
113
|
+
#
|
114
|
+
# dn: ou=People,dc=example,dc=com
|
115
|
+
# objectClass: organizationalUnit
|
116
|
+
# objectClass: top
|
117
|
+
# ou: People
|
118
|
+
#
|
119
|
+
# dn: uid=1,ou=People,dc=example,dc=com
|
120
|
+
# objectClass: person
|
121
|
+
# objectClass: organizationalPerson
|
122
|
+
# objectClass: inetOrgPerson
|
123
|
+
# objectClass: top
|
124
|
+
# cn:: csO0YsOpcnRz
|
125
|
+
# sn:: YsO0YiByw7Riw6lydHM=
|
126
|
+
# givenName:: YsO0Yg==
|
127
|
+
# uid: 1
|
128
|
+
#
|
129
|
+
# =Refs:
|
130
|
+
# * http://www.ietf.org/rfc/rfc2251.txt
|
131
|
+
# * http://www.novell.com/documentation/edir88/edir88/?page=/documentation/edir88/edir88/data/agazepd.html
|
132
|
+
# * https://docs.opends.org/2.0/page/SearchingUsingInternationalCollationRules
|
133
|
+
#++
|
134
|
+
def ex(attribute, value)
|
135
|
+
new(:ex, attribute, value)
|
136
|
+
end
|
137
|
+
|
138
|
+
##
|
139
|
+
# Creates a Filter object indicating that a particular attribute value
|
140
|
+
# is either not present or does not match a particular string; see
|
141
|
+
# Filter::eq for more information.
|
142
|
+
#
|
143
|
+
# This filter does not perform any escaping
|
144
|
+
def ne(attribute, value)
|
145
|
+
new(:ne, attribute, value)
|
146
|
+
end
|
147
|
+
|
148
|
+
##
|
149
|
+
# Creates a Filter object indicating that the value of a particular
|
150
|
+
# attribute must match a particular string. The attribute value is
|
151
|
+
# escaped, so the "*" character is interpreted literally.
|
152
|
+
def equals(attribute, value)
|
153
|
+
new(:eq, attribute, escape(value))
|
154
|
+
end
|
155
|
+
|
156
|
+
##
|
157
|
+
# Creates a Filter object indicating that the value of a particular
|
158
|
+
# attribute must begin with a particular string. The attribute value is
|
159
|
+
# escaped, so the "*" character is interpreted literally.
|
160
|
+
def begins(attribute, value)
|
161
|
+
new(:eq, attribute, escape(value) + "*")
|
162
|
+
end
|
163
|
+
|
164
|
+
##
|
165
|
+
# Creates a Filter object indicating that the value of a particular
|
166
|
+
# attribute must end with a particular string. The attribute value is
|
167
|
+
# escaped, so the "*" character is interpreted literally.
|
168
|
+
def ends(attribute, value)
|
169
|
+
new(:eq, attribute, "*" + escape(value))
|
170
|
+
end
|
171
|
+
|
172
|
+
##
|
173
|
+
# Creates a Filter object indicating that the value of a particular
|
174
|
+
# attribute must contain a particular string. The attribute value is
|
175
|
+
# escaped, so the "*" character is interpreted literally.
|
176
|
+
def contains(attribute, value)
|
177
|
+
new(:eq, attribute, "*" + escape(value) + "*")
|
178
|
+
end
|
179
|
+
|
180
|
+
##
|
181
|
+
# Creates a Filter object indicating that a particular attribute value
|
182
|
+
# is greater than or equal to the specified value.
|
183
|
+
def ge(attribute, value)
|
184
|
+
new(:ge, attribute, value)
|
185
|
+
end
|
186
|
+
|
187
|
+
##
|
188
|
+
# Creates a Filter object indicating that a particular attribute value
|
189
|
+
# is less than or equal to the specified value.
|
190
|
+
def le(attribute, value)
|
191
|
+
new(:le, attribute, value)
|
192
|
+
end
|
193
|
+
|
194
|
+
##
|
195
|
+
# Joins two or more filters so that all conditions must be true. Calling
|
196
|
+
# <tt>Filter.join(left, right)</tt> is the same as <tt>left &
|
197
|
+
# right</tt>.
|
198
|
+
#
|
199
|
+
# # Selects only entries that have an <tt>objectclass</tt> attribute.
|
200
|
+
# x = Net::LDAP::Filter.present("objectclass")
|
201
|
+
# # Selects only entries that have a <tt>mail</tt> attribute that begins
|
202
|
+
# # with "George".
|
203
|
+
# y = Net::LDAP::Filter.eq("mail", "George*")
|
204
|
+
# # Selects only entries that meet both conditions above.
|
205
|
+
# z = Net::LDAP::Filter.join(x, y)
|
206
|
+
def join(left, right)
|
207
|
+
new(:and, left, right)
|
208
|
+
end
|
209
|
+
|
210
|
+
##
|
211
|
+
# Creates a disjoint comparison between two or more filters. Selects
|
212
|
+
# entries where either the left or right side are true. Calling
|
213
|
+
# <tt>Filter.intersect(left, right)</tt> is the same as <tt>left |
|
214
|
+
# right</tt>.
|
215
|
+
#
|
216
|
+
# # Selects only entries that have an <tt>objectclass</tt> attribute.
|
217
|
+
# x = Net::LDAP::Filter.present("objectclass")
|
218
|
+
# # Selects only entries that have a <tt>mail</tt> attribute that begins
|
219
|
+
# # with "George".
|
220
|
+
# y = Net::LDAP::Filter.eq("mail", "George*")
|
221
|
+
# # Selects only entries that meet either condition above.
|
222
|
+
# z = x | y
|
223
|
+
def intersect(left, right)
|
224
|
+
new(:or, left, right)
|
225
|
+
end
|
226
|
+
|
227
|
+
##
|
228
|
+
# Negates a filter. Calling <tt>Fitler.negate(filter)</tt> i s the same
|
229
|
+
# as <tt>~filter</tt>.
|
230
|
+
#
|
231
|
+
# # Selects only entries that do not have an <tt>objectclass</tt>
|
232
|
+
# # attribute.
|
233
|
+
# x = ~Net::LDAP::Filter.present("objectclass")
|
234
|
+
def negate(filter)
|
235
|
+
new(:not, filter, nil)
|
236
|
+
end
|
237
|
+
|
238
|
+
##
|
239
|
+
# This is a synonym for #eq(attribute, "*"). Also known as #present and
|
240
|
+
# #pres.
|
241
|
+
def present?(attribute)
|
242
|
+
eq(attribute, "*")
|
243
|
+
end
|
244
|
+
alias_method :present, :present?
|
245
|
+
alias_method :pres, :present?
|
246
|
+
|
247
|
+
# http://tools.ietf.org/html/rfc4515 lists these exceptions from UTF1
|
248
|
+
# charset for filters. All of the following must be escaped in any normal
|
249
|
+
# string using a single backslash ('\') as escape.
|
250
|
+
#
|
251
|
+
ESCAPES = {
|
252
|
+
'!' => '21', # EXCLAMATION = %x21 ; exclamation mark ("!")
|
253
|
+
'&' => '26', # AMPERSAND = %x26 ; ampersand (or AND symbol) ("&")
|
254
|
+
'*' => '2A', # ASTERISK = %x2A ; asterisk ("*")
|
255
|
+
':' => '3A', # COLON = %x3A ; colon (":")
|
256
|
+
'|' => '7C', # VERTBAR = %x7C ; vertical bar (or pipe) ("|")
|
257
|
+
'~' => '7E', # TILDE = %x7E ; tilde ("~")
|
258
|
+
}
|
259
|
+
# Compiled character class regexp using the keys from the above hash.
|
260
|
+
ESCAPE_RE = Regexp.new(
|
261
|
+
"[" +
|
262
|
+
ESCAPES.keys.map { |e| Regexp.escape(e) }.join +
|
263
|
+
"]")
|
264
|
+
|
265
|
+
##
|
266
|
+
# Escape a string for use in an LDAP filter
|
267
|
+
def escape(string)
|
268
|
+
string.gsub(ESCAPE_RE) { |char| "\\" + ESCAPES[char] }
|
269
|
+
end
|
270
|
+
|
271
|
+
##
|
272
|
+
# Converts an LDAP search filter in BER format to an Net::LDAP::Filter
|
273
|
+
# object. The incoming BER object most likely came to us by parsing an
|
274
|
+
# LDAP searchRequest PDU. See also the comments under #to_ber, including
|
275
|
+
# the grammar snippet from the RFC.
|
276
|
+
#--
|
277
|
+
# We're hardcoding the BER constants from the RFC. These should be
|
278
|
+
# broken out insto constants.
|
279
|
+
def parse_ber(ber)
|
280
|
+
case ber.ber_identifier
|
281
|
+
when 0xa0 # context-specific constructed 0, "and"
|
282
|
+
ber.map { |b| parse_ber(b) }.inject { |memo, obj| memo & obj }
|
283
|
+
when 0xa1 # context-specific constructed 1, "or"
|
284
|
+
ber.map { |b| parse_ber(b) }.inject { |memo, obj| memo | obj }
|
285
|
+
when 0xa2 # context-specific constructed 2, "not"
|
286
|
+
~parse_ber(ber.first)
|
287
|
+
when 0xa3 # context-specific constructed 3, "equalityMatch"
|
288
|
+
if ber.last == "*"
|
289
|
+
else
|
290
|
+
eq(ber.first, ber.last)
|
291
|
+
end
|
292
|
+
when 0xa4 # context-specific constructed 4, "substring"
|
293
|
+
str = ""
|
294
|
+
final = false
|
295
|
+
ber.last.each { |b|
|
296
|
+
case b.ber_identifier
|
297
|
+
when 0x80 # context-specific primitive 0, SubstringFilter "initial"
|
298
|
+
raise Net::LDAP::LdapError, "Unrecognized substring filter; bad initial value." if str.length > 0
|
299
|
+
str += b
|
300
|
+
when 0x81 # context-specific primitive 0, SubstringFilter "any"
|
301
|
+
str += "*#{b}"
|
302
|
+
when 0x82 # context-specific primitive 0, SubstringFilter "final"
|
303
|
+
str += "*#{b}"
|
304
|
+
final = true
|
305
|
+
end
|
306
|
+
}
|
307
|
+
str += "*" unless final
|
308
|
+
eq(ber.first.to_s, str)
|
309
|
+
when 0xa5 # context-specific constructed 5, "greaterOrEqual"
|
310
|
+
ge(ber.first.to_s, ber.last.to_s)
|
311
|
+
when 0xa6 # context-specific constructed 6, "lessOrEqual"
|
312
|
+
le(ber.first.to_s, ber.last.to_s)
|
313
|
+
when 0x87 # context-specific primitive 7, "present"
|
314
|
+
# call to_s to get rid of the BER-identifiedness of the incoming string.
|
315
|
+
present?(ber.to_s)
|
316
|
+
when 0xa9 # context-specific constructed 9, "extensible comparison"
|
317
|
+
raise Net::LDAP::LdapError, "Invalid extensible search filter, should be at least two elements" if ber.size<2
|
318
|
+
|
319
|
+
# Reassembles the extensible filter parts
|
320
|
+
# (["sn", "2.4.6.8.10", "Barbara Jones", '1'])
|
321
|
+
type = value = dn = rule = nil
|
322
|
+
ber.each do |element|
|
323
|
+
case element.ber_identifier
|
324
|
+
when 0x81 then rule=element
|
325
|
+
when 0x82 then type=element
|
326
|
+
when 0x83 then value=element
|
327
|
+
when 0x84 then dn='dn'
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
attribute = ''
|
332
|
+
attribute << type if type
|
333
|
+
attribute << ":#{dn}" if dn
|
334
|
+
attribute << ":#{rule}" if rule
|
335
|
+
|
336
|
+
ex(attribute, value)
|
337
|
+
else
|
338
|
+
raise Net::LDAP::LdapError, "Invalid BER tag-value (#{ber.ber_identifier}) in search filter."
|
339
|
+
end
|
340
|
+
end
|
341
|
+
|
342
|
+
##
|
343
|
+
# Converts an LDAP filter-string (in the prefix syntax specified in RFC-2254)
|
344
|
+
# to a Net::LDAP::Filter.
|
345
|
+
def construct(ldap_filter_string)
|
346
|
+
FilterParser.parse(ldap_filter_string)
|
347
|
+
end
|
348
|
+
alias_method :from_rfc2254, :construct
|
349
|
+
alias_method :from_rfc4515, :construct
|
350
|
+
|
351
|
+
##
|
352
|
+
# Convert an RFC-1777 LDAP/BER "Filter" object to a Net::LDAP::Filter
|
353
|
+
# object.
|
354
|
+
#--
|
355
|
+
# TODO, we're hardcoding the RFC-1777 BER-encodings of the various
|
356
|
+
# filter types. Could pull them out into a constant.
|
357
|
+
#++
|
358
|
+
def parse_ldap_filter(obj)
|
359
|
+
case obj.ber_identifier
|
360
|
+
when 0x87 # present. context-specific primitive 7.
|
361
|
+
eq(obj.to_s, "*")
|
362
|
+
when 0xa3 # equalityMatch. context-specific constructed 3.
|
363
|
+
eq(obj[0], obj[1])
|
364
|
+
else
|
365
|
+
raise Net::LDAP::LdapError, "Unknown LDAP search-filter type: #{obj.ber_identifier}"
|
366
|
+
end
|
367
|
+
end
|
368
|
+
end
|
369
|
+
|
370
|
+
##
|
371
|
+
# Joins two or more filters so that all conditions must be true.
|
372
|
+
#
|
373
|
+
# # Selects only entries that have an <tt>objectclass</tt> attribute.
|
374
|
+
# x = Net::LDAP::Filter.present("objectclass")
|
375
|
+
# # Selects only entries that have a <tt>mail</tt> attribute that begins
|
376
|
+
# # with "George".
|
377
|
+
# y = Net::LDAP::Filter.eq("mail", "George*")
|
378
|
+
# # Selects only entries that meet both conditions above.
|
379
|
+
# z = x & y
|
380
|
+
def &(filter)
|
381
|
+
self.class.join(self, filter)
|
382
|
+
end
|
383
|
+
|
384
|
+
##
|
385
|
+
# Creates a disjoint comparison between two or more filters. Selects
|
386
|
+
# entries where either the left or right side are true.
|
387
|
+
#
|
388
|
+
# # Selects only entries that have an <tt>objectclass</tt> attribute.
|
389
|
+
# x = Net::LDAP::Filter.present("objectclass")
|
390
|
+
# # Selects only entries that have a <tt>mail</tt> attribute that begins
|
391
|
+
# # with "George".
|
392
|
+
# y = Net::LDAP::Filter.eq("mail", "George*")
|
393
|
+
# # Selects only entries that meet either condition above.
|
394
|
+
# z = x | y
|
395
|
+
def |(filter)
|
396
|
+
self.class.intersect(self, filter)
|
397
|
+
end
|
398
|
+
|
399
|
+
##
|
400
|
+
# Negates a filter.
|
401
|
+
#
|
402
|
+
# # Selects only entries that do not have an <tt>objectclass</tt>
|
403
|
+
# # attribute.
|
404
|
+
# x = ~Net::LDAP::Filter.present("objectclass")
|
405
|
+
def ~@
|
406
|
+
self.class.negate(self)
|
407
|
+
end
|
408
|
+
|
409
|
+
##
|
410
|
+
# Equality operator for filters, useful primarily for constructing unit tests.
|
411
|
+
def ==(filter)
|
412
|
+
# 20100320 AZ: We need to come up with a better way of doing this. This
|
413
|
+
# is just nasty.
|
414
|
+
str = "[@op,@left,@right]"
|
415
|
+
self.instance_eval(str) == filter.instance_eval(str)
|
416
|
+
end
|
417
|
+
|
418
|
+
def to_raw_rfc2254
|
419
|
+
case @op
|
420
|
+
when :ne
|
421
|
+
"!(#{@left}=#{@right})"
|
422
|
+
when :eq
|
423
|
+
"#{@left}=#{@right}"
|
424
|
+
when :ex
|
425
|
+
"#{@left}:=#{@right}"
|
426
|
+
when :ge
|
427
|
+
"#{@left}>=#{@right}"
|
428
|
+
when :le
|
429
|
+
"#{@left}<=#{@right}"
|
430
|
+
when :and
|
431
|
+
"&(#{@left.to_raw_rfc2254})(#{@right.to_raw_rfc2254})"
|
432
|
+
when :or
|
433
|
+
"|(#{@left.to_raw_rfc2254})(#{@right.to_raw_rfc2254})"
|
434
|
+
when :not
|
435
|
+
"!(#{@left.to_raw_rfc2254})"
|
436
|
+
end
|
437
|
+
end
|
438
|
+
|
439
|
+
##
|
440
|
+
# Converts the Filter object to an RFC 2254-compatible text format.
|
441
|
+
def to_rfc2254
|
442
|
+
"(#{to_raw_rfc2254})"
|
443
|
+
end
|
444
|
+
|
445
|
+
def to_s
|
446
|
+
to_rfc2254
|
447
|
+
end
|
448
|
+
|
449
|
+
##
|
450
|
+
# Converts the filter to BER format.
|
451
|
+
#--
|
452
|
+
# Filter ::=
|
453
|
+
# CHOICE {
|
454
|
+
# and [0] SET OF Filter,
|
455
|
+
# or [1] SET OF Filter,
|
456
|
+
# not [2] Filter,
|
457
|
+
# equalityMatch [3] AttributeValueAssertion,
|
458
|
+
# substrings [4] SubstringFilter,
|
459
|
+
# greaterOrEqual [5] AttributeValueAssertion,
|
460
|
+
# lessOrEqual [6] AttributeValueAssertion,
|
461
|
+
# present [7] AttributeType,
|
462
|
+
# approxMatch [8] AttributeValueAssertion,
|
463
|
+
# extensibleMatch [9] MatchingRuleAssertion
|
464
|
+
# }
|
465
|
+
#
|
466
|
+
# SubstringFilter ::=
|
467
|
+
# SEQUENCE {
|
468
|
+
# type AttributeType,
|
469
|
+
# SEQUENCE OF CHOICE {
|
470
|
+
# initial [0] LDAPString,
|
471
|
+
# any [1] LDAPString,
|
472
|
+
# final [2] LDAPString
|
473
|
+
# }
|
474
|
+
# }
|
475
|
+
#
|
476
|
+
# MatchingRuleAssertion ::=
|
477
|
+
# SEQUENCE {
|
478
|
+
# matchingRule [1] MatchingRuleId OPTIONAL,
|
479
|
+
# type [2] AttributeDescription OPTIONAL,
|
480
|
+
# matchValue [3] AssertionValue,
|
481
|
+
# dnAttributes [4] BOOLEAN DEFAULT FALSE
|
482
|
+
# }
|
483
|
+
#
|
484
|
+
# Matching Rule Suffixes
|
485
|
+
# Less than [.1] or .[lt]
|
486
|
+
# Less than or equal to [.2] or [.lte]
|
487
|
+
# Equality [.3] or [.eq] (default)
|
488
|
+
# Greater than or equal to [.4] or [.gte]
|
489
|
+
# Greater than [.5] or [.gt]
|
490
|
+
# Substring [.6] or [.sub]
|
491
|
+
#
|
492
|
+
#++
|
493
|
+
def to_ber
|
494
|
+
case @op
|
495
|
+
when :eq
|
496
|
+
if @right == "*" # presence test
|
497
|
+
@left.to_s.to_ber_contextspecific(7)
|
498
|
+
elsif @right =~ /[*]/ # substring
|
499
|
+
# Parsing substrings is a little tricky. We use String#split to
|
500
|
+
# break a string into substrings delimited by the * (star)
|
501
|
+
# character. But we also need to know whether there is a star at the
|
502
|
+
# head and tail of the string, so we use a limit parameter value of
|
503
|
+
# -1: "If negative, there is no limit to the number of fields
|
504
|
+
# returned, and trailing null fields are not suppressed."
|
505
|
+
#
|
506
|
+
# 20100320 AZ: This is much simpler than the previous verison. Also,
|
507
|
+
# unnecessary regex escaping has been removed.
|
508
|
+
|
509
|
+
ary = @right.split(/[*]+/, -1)
|
510
|
+
|
511
|
+
if ary.first.empty?
|
512
|
+
first = nil
|
513
|
+
ary.shift
|
514
|
+
else
|
515
|
+
first = ary.shift.to_ber_contextspecific(0)
|
516
|
+
end
|
517
|
+
|
518
|
+
if ary.last.empty?
|
519
|
+
last = nil
|
520
|
+
ary.pop
|
521
|
+
else
|
522
|
+
last = ary.pop.to_ber_contextspecific(2)
|
523
|
+
end
|
524
|
+
|
525
|
+
seq = ary.map { |e| e.to_ber_contextspecific(1) }
|
526
|
+
seq.unshift first if first
|
527
|
+
seq.push last if last
|
528
|
+
|
529
|
+
[@left.to_s.to_ber, seq.to_ber].to_ber_contextspecific(4)
|
530
|
+
else # equality
|
531
|
+
[@left.to_s.to_ber, unescape(@right).to_ber].to_ber_contextspecific(3)
|
532
|
+
end
|
533
|
+
when :ex
|
534
|
+
seq = []
|
535
|
+
|
536
|
+
unless @left =~ /^([-;\d\w]*)(:dn)?(:(\w+|[.\d\w]+))?$/
|
537
|
+
raise Net::LDAP::LdapError, "Bad attribute #{@left}"
|
538
|
+
end
|
539
|
+
type, dn, rule = $1, $2, $4
|
540
|
+
|
541
|
+
seq << rule.to_ber_contextspecific(1) unless rule.to_s.empty? # matchingRule
|
542
|
+
seq << type.to_ber_contextspecific(2) unless type.to_s.empty? # type
|
543
|
+
seq << unescape(@right).to_ber_contextspecific(3) # matchingValue
|
544
|
+
seq << "1".to_ber_contextspecific(4) unless dn.to_s.empty? # dnAttributes
|
545
|
+
|
546
|
+
seq.to_ber_contextspecific(9)
|
547
|
+
when :ge
|
548
|
+
[@left.to_s.to_ber, unescape(@right).to_ber].to_ber_contextspecific(5)
|
549
|
+
when :le
|
550
|
+
[@left.to_s.to_ber, unescape(@right).to_ber].to_ber_contextspecific(6)
|
551
|
+
when :ne
|
552
|
+
[self.class.eq(@left, @right).to_ber].to_ber_contextspecific(2)
|
553
|
+
when :and
|
554
|
+
ary = [@left.coalesce(:and), @right.coalesce(:and)].flatten
|
555
|
+
ary.map {|a| a.to_ber}.to_ber_contextspecific(0)
|
556
|
+
when :or
|
557
|
+
ary = [@left.coalesce(:or), @right.coalesce(:or)].flatten
|
558
|
+
ary.map {|a| a.to_ber}.to_ber_contextspecific(1)
|
559
|
+
when :not
|
560
|
+
[@left.to_ber].to_ber_contextspecific(2)
|
561
|
+
end
|
562
|
+
end
|
563
|
+
|
564
|
+
##
|
565
|
+
# Perform filter operations against a user-supplied block. This is useful
|
566
|
+
# when implementing an LDAP directory server. The caller's block will be
|
567
|
+
# called with two arguments: first, a symbol denoting the "operation" of
|
568
|
+
# the filter; and second, an array consisting of arguments to the
|
569
|
+
# operation. The user-supplied block (which is MANDATORY) should perform
|
570
|
+
# some desired application-defined processing, and may return a
|
571
|
+
# locally-meaningful object that will appear as a parameter in the :and,
|
572
|
+
# :or and :not operations detailed below.
|
573
|
+
#
|
574
|
+
# A typical object to return from the user-supplied block is an array of
|
575
|
+
# Net::LDAP::Filter objects.
|
576
|
+
#
|
577
|
+
# These are the possible values that may be passed to the user-supplied
|
578
|
+
# block:
|
579
|
+
# * :equalityMatch (the arguments will be an attribute name and a value
|
580
|
+
# to be matched);
|
581
|
+
# * :substrings (two arguments: an attribute name and a value containing
|
582
|
+
# one or more "*" characters);
|
583
|
+
# * :present (one argument: an attribute name);
|
584
|
+
# * :greaterOrEqual (two arguments: an attribute name and a value to be
|
585
|
+
# compared against);
|
586
|
+
# * :lessOrEqual (two arguments: an attribute name and a value to be
|
587
|
+
# compared against);
|
588
|
+
# * :and (two or more arguments, each of which is an object returned
|
589
|
+
# from a recursive call to #execute, with the same block;
|
590
|
+
# * :or (two or more arguments, each of which is an object returned from
|
591
|
+
# a recursive call to #execute, with the same block; and
|
592
|
+
# * :not (one argument, which is an object returned from a recursive
|
593
|
+
# call to #execute with the the same block.
|
594
|
+
def execute(&block)
|
595
|
+
case @op
|
596
|
+
when :eq
|
597
|
+
if @right == "*"
|
598
|
+
yield :present, @left
|
599
|
+
elsif @right.index '*'
|
600
|
+
yield :substrings, @left, @right
|
601
|
+
else
|
602
|
+
yield :equalityMatch, @left, @right
|
603
|
+
end
|
604
|
+
when :ge
|
605
|
+
yield :greaterOrEqual, @left, @right
|
606
|
+
when :le
|
607
|
+
yield :lessOrEqual, @left, @right
|
608
|
+
when :or, :and
|
609
|
+
yield @op, (@left.execute(&block)), (@right.execute(&block))
|
610
|
+
when :not
|
611
|
+
yield @op, (@left.execute(&block))
|
612
|
+
end || []
|
613
|
+
end
|
614
|
+
|
615
|
+
##
|
616
|
+
# This is a private helper method for dealing with chains of ANDs and ORs
|
617
|
+
# that are longer than two. If BOTH of our branches are of the specified
|
618
|
+
# type of joining operator, then return both of them as an array (calling
|
619
|
+
# coalesce recursively). If they're not, then return an array consisting
|
620
|
+
# only of self.
|
621
|
+
def coalesce(operator) #:nodoc:
|
622
|
+
if @op == operator
|
623
|
+
[@left.coalesce(operator), @right.coalesce(operator)]
|
624
|
+
else
|
625
|
+
[self]
|
626
|
+
end
|
627
|
+
end
|
628
|
+
|
629
|
+
##
|
630
|
+
#--
|
631
|
+
# We got a hash of attribute values.
|
632
|
+
# Do we match the attributes?
|
633
|
+
# Return T/F, and call match recursively as necessary.
|
634
|
+
#++
|
635
|
+
def match(entry)
|
636
|
+
case @op
|
637
|
+
when :eq
|
638
|
+
if @right == "*"
|
639
|
+
l = entry[@left] and l.length > 0
|
640
|
+
else
|
641
|
+
l = entry[@left] and l = Array(l) and l.index(@right)
|
642
|
+
end
|
643
|
+
else
|
644
|
+
raise Net::LDAP::LdapError, "Unknown filter type in match: #{@op}"
|
645
|
+
end
|
646
|
+
end
|
647
|
+
|
648
|
+
##
|
649
|
+
# Converts escaped characters (e.g., "\\28") to unescaped characters
|
650
|
+
# ("(").
|
651
|
+
def unescape(right)
|
652
|
+
right.gsub(/\\([a-fA-F\d]{2})/) { [$1.hex].pack("U") }
|
653
|
+
end
|
654
|
+
private :unescape
|
655
|
+
|
656
|
+
##
|
657
|
+
# Parses RFC 2254-style string representations of LDAP filters into Filter
|
658
|
+
# object hierarchies.
|
659
|
+
class FilterParser #:nodoc:
|
660
|
+
##
|
661
|
+
# The constructed filter.
|
662
|
+
attr_reader :filter
|
663
|
+
|
664
|
+
class << self
|
665
|
+
private :new
|
666
|
+
|
667
|
+
##
|
668
|
+
# Construct a filter tree from the provided string and return it.
|
669
|
+
def parse(ldap_filter_string)
|
670
|
+
new(ldap_filter_string).filter
|
671
|
+
end
|
672
|
+
end
|
673
|
+
|
674
|
+
def initialize(str)
|
675
|
+
require 'strscan' # Don't load strscan until we need it.
|
676
|
+
@filter = parse(StringScanner.new(str))
|
677
|
+
raise Net::LDAP::LdapError, "Invalid filter syntax." unless @filter
|
678
|
+
end
|
679
|
+
|
680
|
+
##
|
681
|
+
# Parse the string contained in the StringScanner provided. Parsing
|
682
|
+
# tries to parse a standalone expression first. If that fails, it tries
|
683
|
+
# to parse a parenthesized expression.
|
684
|
+
def parse(scanner)
|
685
|
+
parse_filter_branch(scanner) or parse_paren_expression(scanner)
|
686
|
+
end
|
687
|
+
private :parse
|
688
|
+
|
689
|
+
##
|
690
|
+
# Join ("&") and intersect ("|") operations are presented in branches.
|
691
|
+
# That is, the expression <tt>(&(test1)(test2)</tt> has two branches:
|
692
|
+
# test1 and test2. Each of these is parsed separately and then pushed
|
693
|
+
# into a branch array for filter merging using the parent operation.
|
694
|
+
#
|
695
|
+
# This method parses the branch text out into an array of filter
|
696
|
+
# objects.
|
697
|
+
def parse_branches(scanner)
|
698
|
+
branches = []
|
699
|
+
while branch = parse_paren_expression(scanner)
|
700
|
+
branches << branch
|
701
|
+
end
|
702
|
+
branches
|
703
|
+
end
|
704
|
+
private :parse_branches
|
705
|
+
|
706
|
+
##
|
707
|
+
# Join ("&") and intersect ("|") operations are presented in branches.
|
708
|
+
# That is, the expression <tt>(&(test1)(test2)</tt> has two branches:
|
709
|
+
# test1 and test2. Each of these is parsed separately and then pushed
|
710
|
+
# into a branch array for filter merging using the parent operation.
|
711
|
+
#
|
712
|
+
# This method calls #parse_branches to generate the branch list and then
|
713
|
+
# merges them into a single Filter tree by calling the provided
|
714
|
+
# operation.
|
715
|
+
def merge_branches(op, scanner)
|
716
|
+
filter = nil
|
717
|
+
branches = parse_branches(scanner)
|
718
|
+
|
719
|
+
if branches.size >= 2
|
720
|
+
filter = branches.shift
|
721
|
+
while not branches.empty?
|
722
|
+
filter = filter.__send__(op, branches.shift)
|
723
|
+
end
|
724
|
+
end
|
725
|
+
|
726
|
+
filter
|
727
|
+
end
|
728
|
+
private :merge_branches
|
729
|
+
|
730
|
+
def parse_paren_expression(scanner)
|
731
|
+
if scanner.scan(/\s*\(\s*/)
|
732
|
+
expr = if scanner.scan(/\s*\&\s*/)
|
733
|
+
merge_branches(:&, scanner)
|
734
|
+
elsif scanner.scan(/\s*\|\s*/)
|
735
|
+
merge_branches(:|, scanner)
|
736
|
+
elsif scanner.scan(/\s*\!\s*/)
|
737
|
+
br = parse_paren_expression(scanner)
|
738
|
+
~br if br
|
739
|
+
else
|
740
|
+
parse_filter_branch(scanner)
|
741
|
+
end
|
742
|
+
|
743
|
+
if expr and scanner.scan(/\s*\)\s*/)
|
744
|
+
expr
|
745
|
+
end
|
746
|
+
end
|
747
|
+
end
|
748
|
+
private :parse_paren_expression
|
749
|
+
|
750
|
+
##
|
751
|
+
# This parses a given expression inside of parentheses.
|
752
|
+
def parse_filter_branch(scanner)
|
753
|
+
scanner.scan(/\s*/)
|
754
|
+
if token = scanner.scan(/[-\w\d_:.]*[\d\w]/)
|
755
|
+
scanner.scan(/\s*/)
|
756
|
+
if op = scanner.scan(/<=|>=|!=|:=|=/)
|
757
|
+
scanner.scan(/\s*/)
|
758
|
+
if value = scanner.scan(/(?:[-\w*.+@=,#\$%&!'\s]|\\[a-fA-F\d]{2})+/)
|
759
|
+
# 20100313 AZ: Assumes that "(uid=george*)" is the same as
|
760
|
+
# "(uid=george* )". The standard doesn't specify, but I can find
|
761
|
+
# no examples that suggest otherwise.
|
762
|
+
value.strip!
|
763
|
+
case op
|
764
|
+
when "="
|
765
|
+
Net::LDAP::Filter.eq(token, value)
|
766
|
+
when "!="
|
767
|
+
Net::LDAP::Filter.ne(token, value)
|
768
|
+
when "<="
|
769
|
+
Net::LDAP::Filter.le(token, value)
|
770
|
+
when ">="
|
771
|
+
Net::LDAP::Filter.ge(token, value)
|
772
|
+
when ":="
|
773
|
+
Net::LDAP::Filter.ex(token, value)
|
774
|
+
end
|
775
|
+
end
|
776
|
+
end
|
777
|
+
end
|
778
|
+
end
|
779
|
+
private :parse_filter_branch
|
780
|
+
end # class Net::LDAP::FilterParser
|
781
|
+
end # class Net::LDAP::Filter
|